2. Modbus协议基础
Modbus 协议自 1979 年由 Modicon(现施耐德电气)提出以来,凭借其开放、简单、可靠的特点,已成为工业自动化领域应用最广泛的通信协议之一。在嵌入式开发中,理解 Modbus 的三种主要传输模式——RTU、ASCII 和 TCP——是进行协议栈移植的前提。本章将深入剖析三种模式在数据封装、传输效率、应用场景上的本质差异,详细拆解各自的帧格式与 CRC/LRC 校验机制,并对功能码进行分类讲解,最后通过一个实际工程案例帮助读者建立从协议理解到代码实现的完整认知链路。
上图从宏观层面展示了三种模式的定位差异。RTU 模式以二进制编码实现高密度数据传输,是工业现场默认的首选;ASCII 模式将每个字节拆分为两个 ASCII 字符,虽然传输效率减半,但便于人工阅读和调试;TCP 模式则完全摒弃了链路层的 CRC 校验,转而依赖 TCP/IP 协议栈的可靠传输机制,同时引入 MBAP 头实现多主站访问。下面我们逐一深入分析。
2.1 三种模式的本质区别与选型判断
核心原理:从物理层到应用层的分层差异
Modbus 协议本身定义在 OSI 模型的应用层(第 7 层),但 RTU 和 ASCII 模式同时规定了数据链路层(第 2 层)的封装方式,而 TCP 模式则借助 TCP/IP 协议栈完成传输。这种分层差异直接决定了三种模式在帧结构、校验机制和组网能力上的不同。
- RTU(Remote Terminal Unit):每个 8 位字节直接作为一个二进制字节传输,数据密度最高。帧间空闲时间 ≥ 3.5 个字符时间,用于帧同步。
- ASCII(American Standard Code for Information Interchange):每个 8 位字节被拆分为两个十六进制 ASCII 字符(如 0x1A 变为 '1' 和 'A'),帧以冒号 ':' 起始,以 CRLF 结束。帧间空闲时间要求 ≥ 1 秒。
- TCP(Transmission Control Protocol):在 TCP 报文之上增加 7 字节的 MBAP(Modbus Application Protocol)头,包含事务处理标识符、协议标识符、长度字段和单元标识符。无 CRC/LRC,因为 TCP 的校验和与重传机制已保证数据完整性。
判断方法:根据工程约束选择模式
| 考量维度 | RTU | ASCII | TCP |
|---|---|---|---|
| 传输效率 | 高(二进制) | 低(减半) | 中(有 TCP/IP 头开销) |
| 硬件接口 | RS-232/485/422 | RS-232/485/422 | 以太网 RJ45 |
| 校验方式 | CRC16(2字节) | LRC(1字节) | TCP 校验和(4层) |
| 帧同步机制 | 3.5字符时间间隔 | 起始/结束字符 | 长度字段 |
| 多主站支持 | 不支持 | 不支持 | 支持 |
| 典型应用 | PLC、传感器、变频器 | 老旧设备、调试链路 | SCADA、跨车间组网 |
在实际工程中,如果通信链路是 RS-485 且节点数较多、波特率受限,优先选择 RTU;如果链路中存在中继器或无线模块导致帧间隔不稳定,ASCII 的显式帧头帧尾反而更可靠;如果设备已具备以太网接口,直接采用 TCP 模式可避免串口瓶颈。
案例拆解:某水处理项目中的模式选型
某水厂需要将 32 台远程 IO 采集站的数据汇聚到中控室,链路采用光纤收发器 + RS-485 总线,波特率 9600。初始方案采用 RTU 模式,但运行中发现偶发性数据错乱。分析日志发现,光纤收发器在光电转换时引入了约 20ms 的不确定延迟,导致 RTU 的 3.5 字符时间帧间隔被破坏。解决方案:将通信模式切换为 ASCII,利用 ':' 和 CRLF 作为帧边界,彻底规避了时序问题。虽然传输效率降低,但 9600 波特率下每帧数据量不大,实际轮询周期仅从 800ms 增加到 1.2s,完全满足工艺要求。
常见误区与复盘
- 误区一:认为 RTU 一定优于 ASCII。实际上在无线或中继链路中,ASCII 的显式帧边界更可靠。
- 误区二:TCP 模式不需要考虑帧超时。虽然 TCP 保证数据到达,但应用层仍需设置响应超时(通常 100ms~1s),防止从站无响应时主站死等。
- 误区三:忽略 RTU 的帧间隔实现。在嵌入式裸机环境中,若使用中断接收,必须用定时器精确测量 3.5 字符时间,否则帧拼接会出错。
2.2 帧格式详解与校验算法
核心原理:RTU 与 ASCII 的字节级差异
RTU 帧格式为:地址码(1字节)+ 功能码(1字节)+ 数据区(N字节)+ CRC16(2字节,低字节在前)。CRC16 的生成多项式为 0x8005(初始值 0xFFFF),对地址码到数据区末尾的所有字节进行计算。ASCII 帧格式为:起始符 ':'(0x3A)+ 地址码(2字符)+ 功能码(2字符)+ 数据区(2N字符)+ LRC(2字符)+ 结束符 CRLF(0x0D 0x0A)。LRC 的计算方式为:将地址码到数据区末尾的所有字节累加,取补码(即 0x100 - 累加和)。
<code>// RTU CRC16 计算示例(C语言)
uint16_t crc16_modbus(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc; // 低字节在前发送
}
// ASCII LRC 计算示例
uint8_t lrc_calc(uint8_t *data, uint16_t len) {
uint8_t sum = 0;
for (uint16_t i = 0; i < len; i++) {
sum += data[i];
}
return (uint8_t)(0x100 - sum); // 补码
}
操作流程:RTU 帧接收的完整状态机
- 初始化定时器,设定 3.5 字符时间(如 9600 波特率下约 4ms)。
- 串口中断接收每个字节,存入缓冲区,同时重置定时器。
- 若定时器超时且缓冲区长度 ≥ 4 字节(地址+功能+CRC),则认为一帧结束。
- 解析地址码,判断是否匹配本机地址或广播地址(0x00)。
- 校验 CRC16,若错误则丢弃并记录错误计数。
- 根据功能码分发到对应处理函数。
案例拆解:TCP 模式下的 MBAP 头解析
Modbus TCP 的帧格式为:MBAP 头(7字节)+ 功能码(1字节)+ 数据区。MBAP 头包含:事务处理标识符(2字节,用于匹配请求/响应)、协议标识符(2字节,固定为 0x0000)、长度字段(2字节,表示后续字节数)、单元标识符(1字节,相当于 RTU 的地址码)。在移植 TCP 协议栈时,需要注意 TCP 是流式协议,没有消息边界,必须依靠长度字段来切分完整的 Modbus 报文。例如,收到 TCP 数据后,先读取前 6 字节获取长度字段,再等待剩余字节到达。
常见误区与总结
- 误区一:CRC 计算时字节序错误。RTU 发送时先发低字节,接收时也要先收低字节再组合。
- 误区二:ASCII 模式下 LRC 计算范围遗漏。LRC 只计算地址码到数据区末尾,不包含 ':' 和 CRLF。
- 误区三:TCP 模式下忽略长度字段。有些初学者直接按固定长度解析,导致粘包时数据错位。
2.3 功能码分类与典型应用场景
核心原理:功能码决定操作类型
Modbus 功能码分为三类:公共功能码(已定义)、用户自定义功能码(65~72 和 100~110)、保留功能码。实际工程中最常用的是公共功能码中的位操作和寄存器操作。位操作针对线圈(Coil)和离散输入(Discrete Input),寄存器操作针对输入寄存器(Input Register)和保持寄存器(Holding Register)。
| 功能码 | 名称 | 操作对象 | 读写属性 | 典型应用 |
|---|---|---|---|---|
| 0x01 | Read Coils | 线圈 | 只读 | 读取继电器状态 |
| 0x02 | Read Discrete Inputs | 离散输入 | 只读 | 读取按钮、限位开关 |
| 0x03 | Read Holding Registers | 保持寄存器 | 读写 | 读取设定值、PID参数 |
| 0x04 | Read Input Registers | 输入寄存器 | 只读 | 读取模拟量采集值 |
| 0x05 | Write Single Coil | 线圈 | 写 | 控制单路开关 |
| 0x06 | Write Single Register | 保持寄存器 | 写 | 修改单个设定值 |
| 0x0F | Write Multiple Coils | 线圈 | 写 | 批量控制输出 |
| 0x10 | Write Multiple Registers | 保持寄存器 | 写 | 批量下发参数表 |
| 0x08 | Diagnostics | — | — | 通信测试、复位 |
判断方法:根据数据类型选择功能码
在协议栈移植时,需要建立一张寄存器映射表,明确每个地址对应的数据类型和读写权限。例如,一个温控器设备:保持寄存器 0x0000 存储目标温度(可写),输入寄存器 0x0000 存储当前温度(只读),线圈 0x0000 控制加热开关。主站通过功能码 0x03 读取当前温度,通过 0x06 写入目标温度,通过 0x05 控制加热启停。
案例拆解:变频器通信的寄存器规划
某项目需要控制一台变频器的启停和频率设定。变频器手册规定:保持寄存器 0x2000 为运行命令字(0x0001 启动,0x0002 停止),保持寄存器 0x2001 为频率设定值(单位 0.01Hz)。协议栈实现时,主站发送:地址 0x01 + 功能码 0x06 + 寄存器地址 0x2000 + 数据 0x0001 + CRC16。从站响应相同帧表示写入成功。若需同时设定频率,则发送功能码 0x10 批量写入两个寄存器。实际调试中发现,变频器对写入时序有要求——必须先写命令字再写频率,否则频率不生效。因此协议栈需要支持寄存器写入顺序控制。
常见误区与复盘
- 误区一:混淆输入寄存器和保持寄存器。输入寄存器只读,保持寄存器可读写,地址空间独立。
- 误区二:功能码 0x05 写线圈时,数据必须是 0xFF00(ON)或 0x0000(OFF),其他值非法。
- 误区三:批量读写时字节序错误。Modbus 规定寄存器数据高字节在前(Big-Endian),但有些设备厂商使用小端模式,移植时需做转换。