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。计算过程是这样的:

  1. 初始化CRC寄存器为0xFFFF
  2. 取第一个字节与CRC低字节异或
  3. 右移一位,如果移出的位是1,则与多项式0xA001异或
  4. 重复8次,处理完一个字节
  5. 取下一个字节,重复步骤2-4
  6. 所有字节处理完后,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模式 帧结构 地址码 功能码 数据区 CRC校验 功能码01-06 01:读线圈 02:读离散输入 03:读保持寄存器 04:读输入寄存器 05:写单个线圈 06:写单个寄存器 CRC校验原理与计算 多项式0x8005 初始值0xFFFF 最终取反

好了,Modbus协议的上半部分就讲到这里。RTU和ASCII的区别、帧结构的四个要素、01到06功能码的用法、CRC校验的计算方法,这些都是你写Modbus程序必须掌握的基础。

下一章咱们继续聊Modbus的下半部分——07到16功能码、异常处理、还有多从站通信的实战技巧。到时候我会分享几个我在现场踩过的坑,保证让你少走弯路。


公众号:蓝海资料掘金营,微信deep3321