3. Modbus协议深度解析(上):Modbus RTU/ASCII帧结构、功能码详解(01-06)、CRC校验原理与计算
各位工程师朋友,欢迎来到Modbus协议的世界。
说实话,Modbus是我入行接触的第一个工业协议。那时候我刚从学校出来,觉得这协议也太简单了吧?就几个字节来回传。后来在项目里被它坑过几次,才明白——简单的东西,往往藏着最深的门道。
今天咱们先聊Modbus的上半部分。我会把RTU和ASCII这两种帧结构、最常用的01到06号功能码,还有那个让很多人头疼的CRC校验,一次性给你讲透。
3.1 Modbus的两种“方言”:RTU与ASCII
Modbus协议在串行链路上有两种传输模式:RTU和ASCII。你可以把它们理解成同一种语言的两种方言。
RTU模式,用的是二进制传输。每个8位字节直接发,效率高,是我个人最常用的模式。我在现场调试时,90%的情况都用RTU。
ASCII模式,每个字节拆成两个ASCII字符发。比如0x1A,在ASCII模式下要发'1'和'A'两个字符。效率低了一半,但好处是肉眼可读,调试方便。
核心区别一句话:RTU是二进制紧凑型,ASCII是文本可读型。能选RTU就别用ASCII,除非你的通信链路对ASCII字符有特殊要求。
3.2 Modbus RTU帧结构详解
RTU帧结构长这样:
| 地址码(1字节) | 功能码(1字节) | 数据区(N字节) | CRC校验(2字节) |
一共就四个部分,简单吧?但每个部分都有讲究。
3.2.1 地址码
地址码范围是1到247。0是广播地址,所有从站都要响应,但不需要回复。248到255是保留地址,别乱用。
我踩过的坑:有一次项目里,我把从站地址设成了0,结果主站发命令后所有从站都执行了,但没一个回复。排查了半天,才发现地址码写错了。嗯,这种低级错误,犯一次就记住了。
3.2.2 功能码
功能码告诉从站要干什么。1到127是标准功能码,128到255是异常响应码。今天咱们只讲01到06,这是最基础、最常用的六个。
3.2.3 数据区
数据区长度可变,取决于功能码。比如读线圈时,数据区包含起始地址和读取数量;写线圈时,还要带上要写的值。
3.2.4 CRC校验
这个我单独拿出来讲,因为它太重要了。CRC校验错了,整包数据就废了。
3.3 Modbus ASCII帧结构
ASCII帧结构稍微复杂一点:
| 起始符':' (0x3A) | 地址码(2字符) | 功能码(2字符) | 数据区(N字符) | LRC校验(2字符) | 结束符CRLF |
注意看,ASCII模式用LRC校验,不是CRC。LRC比CRC简单得多,就是把所有字节加起来取补码。但简单的东西往往不够可靠,所以我个人很少用ASCII模式。
小技巧:如果你非要用ASCII模式,记得帧与帧之间要有至少1秒的间隔。这是协议规定的,别问为什么,照做就行。
3.4 功能码详解(01-06)
这六个功能码,是Modbus协议的基石。你想想看,工业现场最常干的事不就是读开关量、读模拟量、写开关量、写模拟量吗?01到06正好覆盖了这些需求。
| 功能码 | 名称 | 操作对象 | 操作类型 |
|---|---|---|---|
| 01 (0x01) | 读线圈 | 线圈(DO) | 读 |
| 02 (0x02) | 读离散输入 | 离散输入(DI) | 读 |
| 03 (0x03) | 读保持寄存器 | 保持寄存器(AO) | 读 |
| 04 (0x04) | 读输入寄存器 | 输入寄存器(AI) | 读 |
| 05 (0x05) | 写单个线圈 | 线圈(DO) | 写 |
| 06 (0x06) | 写单个寄存器 | 保持寄存器(AO) | 写 |
3.4.1 功能码01:读线圈
读线圈就是读从站的数字量输出。比如你想知道PLC的Q0.0是通还是断,就用这个功能码。
请求帧格式:
| 地址码 | 0x01 | 起始地址高字节 | 起始地址低字节 | 线圈数量高字节 | 线圈数量低字节 | CRC低字节 | CRC高字节 |
响应帧格式:
| 地址码 | 0x01 | 字节数 | 数据1 | 数据2 | ... | CRC低字节 | CRC高字节 |
注意,线圈数量必须是8的倍数?不是的。你可以读任意数量,但响应里的字节数要按8位对齐。比如读5个线圈,响应里还是1个字节,高3位填0。
3.4.2 功能码02:读离散输入
这个和01很像,区别在于读的是数字量输入。比如按钮、限位开关的状态。
请求和响应格式跟01完全一样,只是功能码换成0x02。
3.4.3 功能码03:读保持寄存器
保持寄存器是16位的,可以读模拟量输出值、设定值等。这是我最常用的功能码,没有之一。
请求帧:
| 地址码 | 0x03 | 起始地址高字节 | 起始地址低字节 | 寄存器数量高字节 | 寄存器数量低字节 | CRC低字节 | CRC高字节 |
响应帧:
| 地址码 | 0x03 | 字节数 | 数据高字节 | 数据低字节 | ... | CRC低字节 | CRC高字节 |
每个寄存器占2个字节,高字节在前。这是大端模式,Modbus规定死了的。
注意:一次最多读125个寄存器。为什么是125?因为响应帧的数据区最大255字节,255/2=127.5,取整就是125。这是协议设计时的考量,咱们用的时候别超了。
3.4.4 功能码04:读输入寄存器
读输入寄存器和03的区别在于,它读的是模拟量输入。比如温度传感器的值、压力变送器的值。
格式和03一模一样,只是功能码换成0x04。
3.4.5 功能码05:写单个线圈
写单个线圈,就是控制一个数字量输出。比如让一个继电器吸合或释放。
请求帧:
| 地址码 | 0x05 | 线圈地址高字节 | 线圈地址低字节 | 数据高字节(0xFF/0x00) | 数据低字节(0x00) | CRC低字节 | CRC高字节 |
数据高字节:0xFF表示通,0x00表示断。其他值都是非法的。
我踩过的坑:有一次我写了个0x01,心想1就是通呗。结果从站没反应。查了半天协议才发现,必须用0xFF。你说这设计是不是有点反直觉?但协议就是这么定的,咱们只能遵守。
3.4.6 功能码06:写单个寄存器
写单个寄存器,就是设置一个16位的值。比如修改PID的设定值。
请求帧:
| 地址码 | 0x06 | 寄存器地址高字节 | 寄存器地址低字节 | 数据高字节 | 数据低字节 | CRC低字节 | CRC高字节 |
响应帧就是请求帧的完全回显。从站收到后,原封不动地把请求帧发回来,表示写成功了。
3.5 CRC校验原理与计算
CRC校验,全称循环冗余校验。说白了,就是给数据包算一个“指纹”。接收方收到后重新算一遍,如果指纹对不上,说明数据在传输过程中被篡改了。
Modbus RTU用的是CRC-16,多项式是0x8005。计算过程是这样的:
- 初始化CRC寄存器为0xFFFF
- 取第一个字节与CRC低字节异或
- 右移一位,如果移出的位是1,则与多项式0xA001异或
- 重复8次,处理完一个字节
- 取下一个字节,重复步骤2-4
- 所有字节处理完后,CRC寄存器取反,得到最终CRC值
听着复杂?我给你一个C语言实现,一看就懂:
unsigned int crc16_modbus(unsigned char *buf, unsigned int len) {
unsigned int crc = 0xFFFF;
unsigned int i, j;
for (i = 0; i < len; i++) {
crc ^= buf[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
这个函数返回的CRC值,低字节在前,高字节在后。比如返回0x1234,那么帧里先发0x34,再发0x12。
调试技巧:我刚开始写Modbus程序时,CRC总是算不对。后来发现一个笨办法——用Modbus调试助手发一帧正确的数据,然后把CRC值抄下来,跟自己的程序算出来的对比。多试几次,就能找到问题。
3.6 本章知识体系
下面这张图,把本章的核心内容串起来了。你可以把它当成一个思维导图,复习时看一眼就全想起来了。
好了,Modbus协议的上半部分就讲到这里。RTU和ASCII的区别、帧结构的四个要素、01到06功能码的用法、CRC校验的计算方法,这些都是你写Modbus程序必须掌握的基础。
下一章咱们继续聊Modbus的下半部分——07到16功能码、异常处理、还有多从站通信的实战技巧。到时候我会分享几个我在现场踩过的坑,保证让你少走弯路。
公众号:蓝海资料掘金营,微信deep3321