返回博客
西门子 S7-1200 PLC 与自研设备 Modbus RTU 通讯实战

西门子 S7-1200 PLC 与自研设备 Modbus RTU 通讯实战

2025年5月18日PLC, S7-1200, Modbus RTU

S7-1200 Modbus RTU 主站配置

S7-1200 作为 Modbus RTU 主站需要 RS485 通讯模块,可选 CM1241 RS485 通讯板或 CB1241 通讯信号板。CM1241 为独立模块,安装在 PLC 左侧扩展槽;CB1241 为信号板,安装在 CPU 模块前盖下方。

CM1241 RS485 模块支持 9.6kbps 到 115.2kbps 波特率,隔离电压 500V AC,适合工业现场长距离通讯。接线时注意 A 线接 3 号端子,B 线接 8 号端子,屏蔽层接机壳地。

TIA Portal 工程配置

打开 TIA Portal V16 或更高版本,新建项目并添加 S7-1200 CPU(如 CPU 1214C DC/DC/DC)。在硬件组态中添加 CM1241 RS485 模块,模块地址自动分配为 0。

进入 CPU 属性设置,启用"启用系统存储器字节"和"启用时钟存储器字节"。时钟存储器字节设为 MB100,用于生成周期脉冲。

MB_COMM_LOAD 功能块配置

MB_COMM_LOAD 用于初始化 Modbus RTU 通讯参数,必须在 OB1 的第一个扫描周期调用一次。在全局 DB 块中定义静态变量:

DATA_BLOCK "Modbus_Global_DB"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
   VAR 
      MB_COMM_LOAD_INST : MB_COMM_LOAD;
      MB_MASTER_INST : MB_MASTER;
      REQ_CommLoad : Bool := TRUE;
      PORT : UInt := 0;
      BAUD : UInt := 19200;
      PARITY : Byte := 1;
      STOP_BITS : Byte := 1;
      DONE_CommLoad : Bool;
      ERROR_CommLoad : Bool;
      STATUS_CommLoad : Word;
      REQ_Master : Bool;
      MB_ADDR : UInt := 1;
      MODE : Byte := 0;
      DATA_PTR : Variant;
      DONE_Master : Bool;
      BUSY_Master : Bool;
      ERROR_Master : Bool;
      STATUS_Master : Word;
      DATA_ADDR : UInt := 0;
      DATA_LEN : UInt := 10;
      ReadData : Array[0..99] of Word;
      WriteData : Array[0..99] of Word;
   END_VAR
BEGIN
END_DATA_BLOCK

在 OB1 中编写初始化逻辑:

// 第一次扫描周期初始化通讯参数
IF "FirstScan" THEN
    "Modbus_Global_DB".REQ_CommLoad := TRUE;
    "Modbus_Global_DB".PORT := 0;           // CM1241 模块地址
    "Modbus_Global_DB".BAUD := 19200;       // 波特率
    "Modbus_Global_DB".PARITY := 0;         // 0=无校验, 1=奇校验, 2=偶校验
    "Modbus_Global_DB".STOP_BITS := 1;      // 停止位
END_IF;

// 调用 MB_COMM_LOAD
"Modbus_Global_DB".MB_COMM_LOAD_INST(
    REQ := "Modbus_Global_DB".REQ_CommLoad,
    PORT := "Modbus_Global_DB".PORT,
    BAUD := "Modbus_Global_DB".BAUD,
    PARITY := "Modbus_Global_DB".PARITY,
    STOP_BITS := "Modbus_Global_DB".STOP_BITS,
    DONE => "Modbus_Global_DB".DONE_CommLoad,
    ERROR => "Modbus_Global_DB".ERROR_CommLoad,
    STATUS => "Modbus_Global_DB".STATUS_CommLoad
);

// 初始化完成后清除请求信号
IF "Modbus_Global_DB".DONE_CommLoad THEN
    "Modbus_Global_DB".REQ_CommLoad := FALSE;
END_IF;

MB_MASTER 功能块配置

MB_MASTER 用于发送 Modbus 请求并接收响应,支持功能码 01/02/03/04/05/06/15/16。在 OB1 中周期性调用:

// 每 100ms 发送一次读取请求(使用时钟存储器 M100.3)
"Modbus_Global_DB".REQ_Master := "M100.3";

// 读取从站地址 1 的保持寄存器,起始地址 0,读取 10 个寄存器
"Modbus_Global_DB".MB_MASTER_INST(
    REQ := "Modbus_Global_DB".REQ_Master,
    MB_ADDR := "Modbus_Global_DB".MB_ADDR,      // 从站地址
    MODE := "Modbus_Global_DB".MODE,            // 0=读, 1=写
    DATA_ADDR := "Modbus_Global_DB".DATA_ADDR,  // 寄存器起始地址
    DATA_LEN := "Modbus_Global_DB".DATA_LEN,    // 寄存器数量
    DATA_PTR := "Modbus_Global_DB".ReadData,    // 数据缓冲区
    DONE => "Modbus_Global_DB".DONE_Master,
    BUSY => "Modbus_Global_DB".BUSY_Master,
    ERROR => "Modbus_Global_DB".ERROR_Master,
    STATUS => "Modbus_Global_DB".STATUS_Master
);

MODE 参数选择:

  • 0:读取输出线圈(功能码 01)
  • 1:读取离散输入(功能码 02)
  • 2:读取保持寄存器(功能码 03)
  • 3:读取输入寄存器(功能码 04)
  • 4:写入单个线圈(功能码 05)
  • 5:写入单个寄存器(功能码 06)
  • 6:写入多个线圈(功能码 15)
  • 7:写入多个寄存器(功能码 16)

数据块(DB)映射设计

为便于管理,为每个从站创建独立的 DB 块。从站地址 1 的数据映射:

DATA_BLOCK "Slave1_DB"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
   VAR 
      Temperature : Word;      // 地址 40001
      Humidity : Word;         // 地址 40002
      Pressure : Word;         // 地址 40003
      Status : Byte;           // 地址 40004(低字节)
      ErrorCode : Byte;        // 地址 40004(高字节)
      Output1 : Word;          // 地址 40005
      Output2 : Word;          // 地址 40006
      Reserved : Array[7..10] of Word;
   END_VAR
BEGIN
END_DATA_BLOCK

在 OB1 中将 Modbus 读取的数据映射到 DB 块:

// 通讯完成后更新数据块
IF "Modbus_Global_DB".DONE_Master THEN
    "Slave1_DB".Temperature := "Modbus_Global_DB".ReadData[0];
    "Slave1_DB".Humidity := "Modbus_Global_DB".ReadData[1];
    "Slave1_DB".Pressure := "Modbus_Global_DB".ReadData[2];
    "Slave1_DB".Status := BYTE_TO_WORD("Modbus_Global_DB".ReadData[3]) AND 16#00FF;
    "Slave1_DB".ErrorCode := SHR(IN:=WORD_TO_BYTE("Modbus_Global_DB".ReadData[3]), N:=8);
    "Slave1_DB".Output1 := "Modbus_Global_DB".ReadData[4];
    "Slave1_DB".Output2 := "Modbus_Global_DB".ReadData[5];
END_IF;

通讯周期与超时参数调优

Modbus RTU 主站轮询周期取决于从站响应时间和总线负载。单次通讯时间计算:

单帧时间 = 帧字节数 × 11 / 波特率
3.5 字符时间 = 3.5 × 11 / 波特率

19200 波特率下,3.5 字符时间约为 2ms。建议轮询周期设置为 100ms-500ms,避免总线拥堵。

MB_MASTER 超时参数在硬件配置中设置,默认 1000ms。对于长距离或高噪声环境,适当增加到 2000ms-3000ms。超时过短会导致频繁重试,过长则影响实时性。

多从站轮询策略

当需要轮询多个从站时,采用时间片分配策略。在全局 DB 中定义轮询状态机:

VAR
    CurrentSlave : Int := 1;
    SlaveCount : Int := 5;
    PollTimer : TON;
    PollInterval : Time := T#500MS;
END_VAR

// 定时器触发轮询
PollTimer(IN := NOT PollTimer.Q, PT := PollInterval);

IF PollTimer.Q THEN
    CASE CurrentSlave OF
        1: // 轮询从站 1
           "Modbus_Global_DB".MB_ADDR := 1;
           "Modbus_Global_DB".DATA_ADDR := 0;
           "Modbus_Global_DB".DATA_LEN := 10;
           "Modbus_Global_DB".DATA_PTR := "Modbus_Global_DB".ReadData;
        2: // 轮询从站 2
           "Modbus_Global_DB".MB_ADDR := 2;
           "Modbus_Global_DB".DATA_ADDR := 0;
           "Modbus_Global_DB".DATA_LEN := 8;
           "Modbus_Global_DB".DATA_PTR := "Slave2_Data";
        // ... 其他从站
    END_CASE;
    
    CurrentSlave := CurrentSlave + 1;
    IF CurrentSlave > SlaveCount THEN
        CurrentSlave := 1;
    END_IF;
END_IF;

为提高效率,可将响应快的从站优先轮询,响应慢的从站降低轮询频率。关键数据(如报警信号)可单独设置快速轮询通道。

常见错误码排查

MB_MASTER 的 STATUS 输出反映通讯状态,常见错误码:

错误码 含义 原因
8188 无响应 从站未上电、地址错误、总线断路
8189 响应超时 从站处理时间过长、超时参数过小
8190 响应 CRC 错误 总线干扰、接地不良
8191 从站异常响应 功能码不支持、寄存器地址越界
8192 奇偶校验错误 波特率/校验位不匹配
8193 帧格式错误 停止位/数据位配置错误

错误处理逻辑:

IF "Modbus_Global_DB".ERROR_Master THEN
    CASE "Modbus_Global_DB".STATUS_Master OF
        16#1FFC: // 8188 无响应
            "Alarm_History".Insert("从站无响应,检查电源和地址");
        16#1FFD: // 8189 超时
            "Alarm_History".Insert("通讯超时,增加超时参数");
        16#1FFE: // 8190 CRC 错误
            "Alarm_History".Insert("CRC 错误,检查总线屏蔽");
        16#1FFF: // 8191 异常响应
            "Alarm_History".Insert("从站返回异常码:" + INT_TO_STRING("Modbus_Global_DB".ReadData[0]));
    END_CASE;
END_IF;

通讯报文抓包分析

使用 USB 转 RS485 转换器配合 Wireshark 或串口调试助手抓取报文。正常读取保持寄存器(功能码 03)报文:

请求帧:

01 03 00 00 00 0A C5 CD
  • 01:从站地址
  • 03:功能码(读保持寄存器)
  • 00 00:起始地址 0
  • 00 0A:读取 10 个寄存器
  • C5 CD:CRC-16 校验

响应帧:

01 03 14 00 64 01 2C 03 E8 00 01 00 02 00 03 00 04 00 05 00 06 12 34
  • 01:从站地址
  • 03:功能码
  • 14:字节数(20 字节)
  • 00 64 ... 00 06:数据(10 个寄存器)
  • 12 34:CRC-16 校验

异常响应(功能码 83):

01 83 02 C0 F1
  • 83:功能码 + 0x80(异常标志)
  • 02:异常码(非法数据地址)
  • C0 F1:CRC-16 校验

自研设备联调流程

自研设备作为 Modbus RTU 从站,需实现以下功能:

STM32 端代码示例(基于 HAL 库):

#include "stm32f1xx_hal.h"
#include "modbus_rtu.h"

#define MODBUS_SLAVE_ADDR 1
#define MODBUS_REG_COUNT 100

uint16_t holding_registers[MODBUS_REG_COUNT] = {0};

// 初始化 UART1 为 19200 8N1
void Modbus_UART_Init(void) {
    UART_HandleTypeDef huart1;
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 19200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    HAL_UART_Init(&huart1);
}

// Modbus 响应处理
void Modbus_Process_Request(uint8_t *rx_buf, uint16_t rx_len, uint8_t *tx_buf, uint16_t *tx_len) {
    uint8_t slave_addr = rx_buf[0];
    uint8_t function_code = rx_buf[1];
    
    if (slave_addr != MODBUS_SLAVE_ADDR) {
        *tx_len = 0;
        return;
    }
    
    tx_buf[0] = slave_addr;
    tx_buf[1] = function_code;
    
    switch (function_code) {
        case 0x03: { // 读保持寄存器
            uint16_t start_addr = (rx_buf[2] << 8) | rx_buf[3];
            uint16_t reg_count = (rx_buf[4] << 8) | rx_buf[5];
            
            if (start_addr + reg_count > MODBUS_REG_COUNT) {
                // 异常响应:非法数据地址
                tx_buf[1] = 0x83;
                tx_buf[2] = 0x02;
                *tx_len = 3;
            } else {
                tx_buf[2] = reg_count * 2;
                for (uint16_t i = 0; i < reg_count; i++) {
                    tx_buf[3 + i * 2] = holding_registers[start_addr + i] >> 8;
                    tx_buf[4 + i * 2] = holding_registers[start_addr + i] & 0xFF;
                }
                *tx_len = 3 + reg_count * 2;
            }
            break;
        }
        case 0x06: { // 写单个寄存器
            uint16_t reg_addr = (rx_buf[2] << 8) | rx_buf[3];
            uint16_t reg_value = (rx_buf[4] << 8) | rx_buf[5];
            
            if (reg_addr >= MODBUS_REG_COUNT) {
                tx_buf[1] = 0x83;
                tx_buf[2] = 0x02;
                *tx_len = 3;
            } else {
                holding_registers[reg_addr] = reg_value;
                memcpy(&tx_buf[2], &rx_buf[2], 4);
                *tx_len = 6;
            }
            break;
        }
        default: { // 不支持的功能码
            tx_buf[1] = 0x83;
            tx_buf[2] = 0x01;
            *tx_len = 3;
            break;
        }
    }
    
    // 添加 CRC 校验
    uint16_t crc = CRC16_Modbus(tx_buf, *tx_len);
    tx_buf[*tx_len] = crc & 0xFF;
    tx_buf[*tx_len + 1] = crc >> 8;
    *tx_len += 2;
}

// 主循环中周期调用
void Modbus_Main_Loop(void) {
    static uint8_t rx_buf[256];
    static uint8_t tx_buf[256];
    static uint16_t rx_len = 0;
    static uint32_t last_byte_time = 0;
    
    // 使用空闲中断接收数据(参考前文 DMA+IDLE 实现)
    if (HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY) {
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, 256);
    }
    
    // 3.5 字符超时判帧
    if (rx_len > 0 && (HAL_GetTick() - last_byte_time > 2)) {
        Modbus_Process_Request(rx_buf, rx_len, tx_buf, &rx_len);
        HAL_UART_Transmit(&huart1, tx_buf, rx_len, 100);
        rx_len = 0;
    }
    
    // 更新寄存器数据
    holding_registers[0] = (uint16_t)(Read_Temperature() * 10);  // 温度×10
    holding_registers[1] = (uint16_t)(Read_Humidity() * 10);     // 湿度×10
    holding_registers[2] = (uint16_t)(Read_Pressure() * 10);     // 压力×10
}

联调步骤:

  1. 硬件连接:S7-1200 的 A/B 线分别连接自研设备的 A/B 线,共地
  2. 参数对齐:确认双方波特率、校验位、停止位一致
  3. 地址配置:自研设备从站地址设为 1,与 PLC MB_ADDR 参数匹配
  4. 单站测试:PLC 只轮询从站 1,抓包确认报文格式正确
  5. 数据验证:PLC 读取的寄存器值与自研设备实际值对比
  6. 写入测试:PLC 写入寄存器,自研设备检查 holding_registers 数组变化
  7. 多站扩展:增加从站数量,验证轮询策略
  8. 压力测试:长时间运行观察丢包率和错误码统计

常见问题排查:

  • 通讯失败:检查 A/B 线是否接反,用万用表测量电压(空闲态 A 应比 B 高 200mV 以上)
  • 数据漂移:确认寄存器地址映射,PLC 的 DATA_ADDR 从 0 开始对应自研设备 holding_registers[0]
  • CRC 错误:检查总线屏蔽层是否单端接地,避免形成地环路
  • 响应超时:增加 MB_MASTER 超时参数,或优化自研设备中断响应速度