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
}
联调步骤:
- 硬件连接:S7-1200 的 A/B 线分别连接自研设备的 A/B 线,共地
- 参数对齐:确认双方波特率、校验位、停止位一致
- 地址配置:自研设备从站地址设为 1,与 PLC MB_ADDR 参数匹配
- 单站测试:PLC 只轮询从站 1,抓包确认报文格式正确
- 数据验证:PLC 读取的寄存器值与自研设备实际值对比
- 写入测试:PLC 写入寄存器,自研设备检查 holding_registers 数组变化
- 多站扩展:增加从站数量,验证轮询策略
- 压力测试:长时间运行观察丢包率和错误码统计
常见问题排查:
- 通讯失败:检查 A/B 线是否接反,用万用表测量电压(空闲态 A 应比 B 高 200mV 以上)
- 数据漂移:确认寄存器地址映射,PLC 的 DATA_ADDR 从 0 开始对应自研设备 holding_registers[0]
- CRC 错误:检查总线屏蔽层是否单端接地,避免形成地环路
- 响应超时:增加 MB_MASTER 超时参数,或优化自研设备中断响应速度