3. Modbus协议基础(下):异常码定义、CRC校验原理、广播与单播机制、协议栈分层思想
好,咱们接着聊Modbus协议的下半部分。上一节我们把协议的基本框架搭起来了,这一节要深入几个关键点——异常码、CRC校验、广播单播,还有协议栈怎么分层。这些内容,说实在的,你在实际项目中几乎天天都要打交道。
3.1 异常码定义:从站说“不”的方式
你想想看,主站发了一条指令,从站不一定每次都能乖乖执行。比如你让从站读一个不存在的寄存器,或者写一个只读的线圈,这时候从站就得告诉主站:“兄弟,这事儿我办不了。” 怎么告诉?靠的就是异常码。
Modbus的异常响应帧结构很简单:功能码 + 0x80(把最高位置1),后面跟一个字节的异常码。举个例子,主站发功能码0x03读保持寄存器,从站返回0x83,后面跟着异常码02,意思就是“你要读的地址不存在”。
我刚开始做Modbus从站的时候,犯过一个低级错误——异常码返回格式不对。当时从站返回了0x03而不是0x83,主站那边直接解析成正常响应了,结果数据全乱套。排查了一下午才发现,原来是异常码的标识位没置上去。嗯,这个坑你们要注意。
常见的异常码有这么几个:
| 异常码 | 名称 | 含义 |
|---|---|---|
| 01 | 非法功能码 | 从站不支持这个功能码 |
| 02 | 非法数据地址 | 你要读写的地址超出了范围 |
| 03 | 非法数据值 | 数据值不合法,比如写线圈时写了0x02 |
| 04 | 从站设备故障 | 从站内部出问题了,比如硬件异常 |
| 06 | 从站忙 | 从站正在处理其他任务,没空理你 |
3.2 CRC校验原理:数据完整性的守护者
Modbus RTU模式用的是CRC-16校验,多项式是0x8005。说白了,就是给每一帧数据算一个16位的校验值,接收方收到后重新算一遍,如果对不上,说明数据在传输过程中被污染了。
CRC的计算原理其实不复杂。你把数据看成一个很长的二进制数,用这个数除以多项式(模2除法),余数就是CRC值。但实际实现时,没人真去做除法,都是用查表法或者移位法。
我个人习惯用查表法,速度快,代码也简洁。下面是一个标准的CRC-16计算函数:
// CRC-16 查表法实现
uint16_t crc16(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
uint16_t i, j;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
注意看,这里用的多项式是0xA001,实际上是0x8005的反转形式。Modbus协议规定CRC采用小端序传输,也就是说低字节在前,高字节在后。这一点很容易搞反,我见过不少新手把CRC字节顺序弄错,结果通信死活不通。
3.3 广播与单播机制:一对多与一对一
Modbus支持两种通信模式:单播和广播。
单播就是主站给某个特定从站发指令,从站地址是1到247。从站收到后必须回复,主站通过回复来确认指令是否执行成功。这是最常用的模式,咱们平时说的“一问一答”就是它。
广播呢,主站把地址设为0,所有从站都会收到这条指令,但从站不需要回复。广播一般用于同步操作,比如让所有从站同时复位,或者同时设置某个参数。
这里有个关键点:广播只支持写操作,不支持读操作。你想想看,如果主站广播一条读指令,所有从站都回复,那总线就乱套了。所以Modbus协议规定,广播帧只能用于写单个线圈、写单个寄存器、写多个线圈、写多个寄存器这几种功能码。
我在做工业现场项目时,广播用得不多,但一旦用上就很关键。比如有一次,现场有30多个从站设备,需要同时启动某个流程。如果用单播一个一个发,时间差会导致设备不同步。用广播,一条指令下去,所有设备同时响应,效果立竿见影。
3.4 协议栈分层思想:把复杂问题拆开
好了,前面讲了这么多细节,怎么把它们组织成一个可用的协议栈?我的答案是——分层。
协议栈分层,说白了就是把不同职责的代码分开。Modbus协议栈我一般分成三层:
- 物理层/驱动层:负责串口收发、定时器管理、GPIO控制。这一层跟硬件绑定,移植时主要改这里。
- 协议核心层:负责帧解析、CRC校验、异常码生成、地址匹配。这一层是纯逻辑,跟硬件无关。
- 应用层:负责处理具体的功能码,比如读线圈、写寄存器。这一层跟你的业务逻辑相关。
为什么要分层?我举个实际例子。有一次我需要把Modbus协议栈从STM32移植到GD32上,如果代码是揉成一团的,那得从头到尾改一遍。但因为我分了层,只需要改驱动层的串口和定时器代码,协议核心层和应用层一行代码都没动。两天就搞定了移植。
下面这张图是我常用的协议栈分层结构,你可以参考一下:
你看,数据从串口进来,驱动层先收完一帧数据,然后交给协议核心层。核心层做CRC校验、地址匹配,如果地址不对直接丢弃。地址匹配上了,再根据功能码分发到应用层。应用层处理完,把结果返回给核心层,核心层封装成响应帧,最后驱动层发出去。
这个流程,每一步职责都很清晰。你在写代码的时候,也要保持这种思路——每个函数只做一件事,做好一件事。
好了,这一节的内容就到这里。异常码、CRC校验、广播单播、分层思想,这四个知识点是Modbus协议栈的核心。下一节我们会开始动手写代码,从零搭建一个Modbus从站协议栈。到时候,这些理论都会变成实实在在的代码。
公众号:蓝海资料掘金营,微信deep3321