3. Modbus协议详解(上):Modbus历史、RTU与ASCII模式、报文结构、功能码分类

各位同学,今天我们来啃Modbus协议这块硬骨头。说实话,在我二十多年的工业通信生涯里,Modbus是我打交道最多的协议,没有之一。它就像工业界的"普通话"——虽然不够华丽,但谁都能听懂。

3.1 Modbus的前世今生

Modbus诞生于1979年,由Modicon公司(现在的施耐德电气)提出。那时候我还没入行,但听老工程师讲,当时PLC之间通信简直是噩梦——每家都有自己的私有协议,互不兼容。Modbus的出现,说白了就是给工业设备定了个"说话规矩"。

为什么它能活到现在?我总结三点:

  • 开放免费:Modbus协议完全公开,不收一分钱许可费
  • 简单可靠:报文结构极其简洁,单片机都能轻松实现
  • 硬件门槛低:RS-232/RS-485串口就能跑,成本几乎为零

我记得2015年做一个水处理项目,甲方要求用Profinet,但现场几十台老旧仪表只支持Modbus。最后怎么办?加个协议转换器,照样跑得稳稳的。这就是Modbus的生命力——你永远不知道它会在什么犄角旮旯里出现。

3.2 RTU模式 vs ASCII模式

Modbus有两种传输模式,我习惯叫它们"二进制模式"和"文本模式"。

特性 RTU模式 ASCII模式
数据表示 二进制(0x00-0xFF) ASCII字符('0'-'9', 'A'-'F')
字节效率 1字节=1个数据 1字节=2个ASCII字符
帧间隔 3.5字符时间 1秒
校验方式 CRC-16 LRC(纵向冗余校验)
典型速率 9600-115200 bps 9600 bps以下

我的建议:除非你有特殊需求,否则一律用RTU。为什么?效率差太多了。同样读取10个寄存器,RTU只需要约15毫秒,ASCII要30毫秒以上。你想想看,在100个从站的系统里,这差距就大了去了。

避坑指南:我曾经在一个项目中,RTU和ASCII设备混用,结果总线乱成一锅粥。记住:同一总线上所有设备必须使用相同模式,不能混搭!

3.3 报文结构深度解析

Modbus报文结构其实就四部分,我管它叫"四件套":

| 地址码 | 功能码 | 数据区 | 校验码 |
| 1字节  | 1字节  | N字节  | 2字节  |

3.3.1 地址码(1字节)

取值范围1-247,0是广播地址。这里有个坑:有些设备地址从1开始,有些从0开始。我遇到过一台国产温控器,地址设成0就死机,折腾了我半天。

3.3.2 功能码(1字节)

这是Modbus的灵魂。1-127是标准功能码,128-255是异常响应。后面我会详细讲。

3.3.3 数据区(N字节)

根据功能码不同,数据区格式也不同。比如读线圈时,数据区包含起始地址和数量;写寄存器时,包含地址和要写入的值。

3.3.4 CRC校验(2字节)

RTU模式用CRC-16,多项式是0x8005。我手算过几次,太痛苦了,后来都用查表法。给你个参考代码:

// CRC-16查表法实现
uint16_t crc16(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;
}
经验之谈:CRC计算一定要放在中断外做,否则容易丢数据。我早期一个项目就是因为在中断里算CRC,导致波特率高了就丢帧,查了三天才找到原因。

3.4 功能码分类详解

Modbus功能码很多,但实际项目中常用的就这几个。我按使用频率排序:

3.4.1 位操作类

  • 01 (0x01) - 读线圈:读取DO状态,返回1位/线圈
  • 02 (0x02) - 读离散输入:读取DI状态,只读不写
  • 05 (0x05) - 写单个线圈:控制单个DO,0xFF00=ON,0x0000=OFF
  • 15 (0x0F) - 写多个线圈:批量控制DO,效率高

3.4.2 寄存器操作类

  • 03 (0x03) - 读保持寄存器:最常用的功能码,读模拟量、参数等
  • 04 (0x04) - 读输入寄存器:读AI值,只读
  • 06 (0x06) - 写单个寄存器:写单个16位数据
  • 16 (0x10) - 写多个寄存器:批量写,常用于参数下载

为什么03和06最常用?因为大部分工业参数都是16位的,比如温度、压力、速度。我做过一个项目,用03功能码每秒轮询200个寄存器,CPU占用率才5%,稳得很。

实战案例:用03功能码读取变频器频率

请求:01 03 00 00 00 01 84 0A
  |    |  |    |    |    |
  |    |  起始地址     CRC
  |    |  寄存器数量=1
  |    功能码=03
  地址=01

响应:01 03 02 13 88 B5 F6
  |    |  |  |    |
  |    |  数据=0x1388=5000
  |    |  字节数=2
  |    功能码=03
  地址=01
  

0x1388换算成十进制是5000,如果量程是0-100Hz,那实际频率就是50.00Hz。嗯,这里要注意字节序——Modbus默认是大端模式,高位在前。

3.5 功能码使用避坑指南

我踩过的坑,你们就别再踩了:

  1. 功能码15和16的批量操作:一次最多写2000个线圈或125个寄存器。别问我怎么知道的——我曾经试图一次写300个寄存器,结果从站直接罢工了。
  2. 功能码05和06的"写后即读":写完一定要读回来确认。有些设备写操作不报错,但实际没写进去。我习惯写完等50ms再读。
  3. 功能码01和02的区别:01读的是可读可写的线圈(DO),02读的是只读的离散输入(DI)。别搞混了,否则读回来的数据全是0xFF。

最后说一句:Modbus协议虽然简单,但越简单的东西越容易出幺蛾子。我见过太多"Modbus通信不稳定"的问题,最后发现都是报文格式写错了。所以,写代码前先把报文结构画清楚,能省你三天调试时间。

下一章我们讲Modbus的异常处理、多主站通信和实际项目中的性能优化。到时候我会分享一个我处理过的"Modbus总线崩溃"的案例,保证让你大开眼界。