ModBus 协议详解
ModBus 是一种工业领域的串行通信协议 ,由 Modicon(现隶属于施耐德电气)于 1979 年开发,主要用于工业自动化系统中控制器与远程设备 之间的通信。它具有结构简单、开源免费、跨平台兼容的特点,是目前工业物联网(IIoT)中应用最广泛的通信协议之一。
ModbusModbus是⼀个请求/应答协议,并且提供功能码规定的服务。 Modbus功能码是 Modbus请求/应答PDU的元素。

核心特点
-
主从架构 ModBus 采用主 - 从(Master-Slave)通信模式,总线上只有 1 个主设备(如 PLC、工控机),可以有多个从设备(如传感器、变频器、仪表)。
- 主设备主动发起请求,从设备只能被动响应,不能主动发送数据。
- 每个从设备有唯一的设备地址(1~247),主设备通过地址指定通信对象。
-
多种传输层支持 ModBus 是一个应用层协议,可以运行在不同的物理层和数据链路层上,常见的变体有:
- ModBus RTU:基于串行总线(RS-232/RS-485),二进制编码,传输效率高,是工业现场最常用的版本。
- ModBus ASCII:基于串行总线,ASCII 字符编码,可读性强,调试方便,但传输效率低。
- ModBus TCP:基于以太网(TCP/IP),去掉了串行链路的限制,支持长距离、高速通信,是工业以太网的主流协议之一。
- ModBus UDP:基于 UDP 协议,适用于对实时性要求高、允许少量丢包的场景。

⼀般的来说,⼀个数据帧可以分为:设备码、功能码、数据码、校验码这四个部分。下面会逐一介绍
通信流程
Modbus协议不规定⼀个字节如何传输,⽽是规定如何进⾏⼀次数据帧的传输。数据帧可以理解为若⼲个具有特殊功能意义的字节的组合。那么Modbus如何定义⼀个数据帧?对于Modbus来说,当进⾏数据传输过程中,出现空闲时间超过3.5个字节持续时间,就认为⼀次
数据帧的结束,之前接收到的字节就是这次数据帧的所有字节。之后再接收到的字节则为下⼀个数据帧的字节。
例如,在9600bit/s的传输速率下,⼀个字节传输的时间约为0.8ms,那么当数据传输中,出现约3ms的空闲时间时,设备就认为⼀帧数据接收完成。
每⼀帧数据要实现功能,就需要通讯双⽅采⽤相同的语法规则,因此对于每个数据帧的构Modbus进⾏了⼀些规定。
例如⼀个主机发送的读取功能的数据帧,其组成为:
| 设备地址 | 功能码 | 寄存器起始地址 | 读取寄存器的个数 | CRC 校验 |
|---|---|---|---|---|
| 1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 |
| 0x02 | 0x03 | 0x00 0x00 | 0x00 0x01(n) | 0x44 0x3F |
从机响应的数据帧为:
| 设备地址 | 功能码 | 返回的字节的个数 | 对应寄存器的数据 | CRC 校验 |
|---|---|---|---|---|
| 1 字节 | 1 字节 | 1 字节 | 2 字节(2*n 字节) | 2 字节 |
| 0x02 | 0x03 | 0x02(2*n) | 0x00 0x01(...) | 0x... 0x... |
Modbus设备码(从机地址)
就是从机地址。
对于⼀主多从的通讯架构中,主机的设备码为0,其他各个从机都有独⽴的设备码,总共分配255个从机设备码,在通讯过程中,主机把数
据发送AB总线中,各个从机从AB总线中接收数据,对于不属于该设备码的数据帧,将数据放回⾄总线,对于属于该设备码的数据帧,设
备进⾏相应的处理后,对主机进⾏响应,此时设备码的含义就是指定主机的通讯⽬标设备。
Modbus功能码
就是区分是读还是写的。
功能码的含义是表明该通讯帧的功能或⽬的。
具体来说,例如功能码表明主机要求读取从机若⼲个寄存器的数值,从机接收到通讯帧后进⾏⼀系列的处理,返回给主机相同的功能码,表明从机对该功能进⾏了响应。
| 功能码(十六进制) | 功能码(十进制) | 功能名称 | 操作对象 | 读写属性 | 适用场景 |
|---|---|---|---|---|---|
0x01 |
1 | 读取线圈状态 | 线圈(Coil) | 只读 | 获取开关量输出状态(如继电器、指示灯) |
0x02 |
2 | 读取离散输入状态 | 离散输入(Discrete Input) | 只读 | 获取开关量输入状态(如传感器、按钮) |
0x03 |
3 | 读取保持寄存器 | 保持寄存器(Holding Register) | 读写 | 获取 / 修改设备参数、控制指令(如频率、转速) |
0x04 |
4 | 读取输入寄存器 | 输入寄存器(Input Register) | 只读 | 获取模拟量输入数据(如温度、压力、电流) |
0x05 |
5 | 写单个线圈 | 线圈(Coil) | 只写 | 控制单个开关输出(如启动 / 停止设备) |
0x06 |
6 | 写单个保持寄存器 | 保持寄存器(Holding Register) | 只写 | 修改单个设备参数(如设定目标温度) |
0x0F(15) |
15 | 写多个线圈 | 线圈(Coil) | 只写 | 批量控制多个开关输出(如多路继电器) |
0x10(16) |
16 | 写多个保持寄存器 | 保持寄存器(Holding Register) | 只写 | 批量修改设备参数(如一组配置值) |
- 线圈 vs 离散输入
- 线圈:可读写,对应设备的输出状态(由主设备控制)。
- 离散输入:只读,对应设备的输入状态(由现场信号决定)。
- 保持寄存器 vs 输入寄存器
- 保持寄存器:可读写,断电后数据不丢失,用于存储设备参数和控制指令。
- 输入寄存器:只读,实时刷新现场采集的模拟量数据。
- 异常功能码 当从设备无法执行请求时,会返回异常响应,功能码为
原功能码 + 0x80。例如:主设备发送0x03(读保持寄存器),若地址错误,从设备返回0x83,并附带异常码说明原因。
| 寄存器类型 | 功能 | 数据类型 | 常用功能码 |
|---|---|---|---|
| 离散输入(只读) | 存储传感器的开关状态 | 1 位(0/1) | 02(读取) |
| 线圈(读写) | 控制执行器的开关状态 | 1 位(0/1) | 01(读)、05(写单个)、15(写多个) |
| 输入寄存器(只读) | 存储传感器的模拟量 | 16 位整数 | 04(读取) |
| 保持寄存器(读写) | 存储控制参数 / 状态值 | 16 位整数 | 03(读)、06(写单个)、16(写多个) |
Modbus数据码
数据码是对功能码的进⼀步补充和解释,常⻅的功能码的数据码格式⼀般在Modbus通⽤协议中已经做了规范。
例如对于3功能码,后⾯跟的数据码包括2个字节表⽰寄存器地址,2个字节表⽰读取的寄存器个数(寄存器的位数为16位,因此⼀个寄存
器有两个字节的数据)。
返回的3功能码,后接1个字节的返回字节个数(该个数应为上述读取寄存器个数的两倍,因为⼀个寄存器对应两个字节),和若⼲个字节
的数据。
Modbus校验码
Modbus⼀般采⽤的16位的CRC校验。
什么是CRC校验呢,简单来说,⽐如你要发⼀段数据,最后想在后⾯加两个字节,这两个字节是通过前⾯所发的数据通过某种算法计算出来的唯⼀的两个字节。
如果发送过程中⽆问题,那么接收端通过相同的算法能够得到同样的最后两位字节,这就表明该帧的所有的数据都是正确的。
如果接收端通过同样的算法算出来的两个字节数据和发送过来的最后两个字节不同,那么就表明在发送和接收过程中,有些数据接收或发送错误。通过校验位来判断该帧是否正确发送和接收。
CRC就是其中⼀种校验⽅法,其算法是对除数和被除数进⾏按位异或,然后除数移位,然后两个进⾏异或,然后除数移位,如此循环,直到除数和被除数位数相同,结束运算。
例子
⽐如我们现在要使⽤STM32查询某传感器的数据,该传感器的地址为01。
主机发送: 01 03 00 00 00 01 84 0A
从机回复: 01 03 02 19 98 B2 7E
什么意思?解析如下:

01 - 地址,也就是你传感器的地址
03 - 功功能码,03代表查询功能,查询传感器的数据
00 00 - 代表查询的起始寄存器地址.说明从0x0000开始查询。这⾥需要说明以下,Modbus把数据存放在寄存器中,通过查询寄存器
来得到不同变量的值,⼀个寄存器地址对应2字节数据。
00 01 - 代表查询了⼀个寄存器,结合前⾯的00 00,意思就是查询从0开始的1个寄存器值。
84 0A - 循环冗余校验,是modbus的校验公式,从⾸个字节开始到84前⾯为⽌。
回复数据解析

01 - 地址,也就是你传感器的地址
03 - 功功能码,03代表查询功能,查询传感器的数据。这⾥要注意的是注意发给从机的功能码是啥,从机就要回复同样的功能码,如果不⼀样说明这⼀帧数据有错误
02 - 代表后⾯数据的字节数,因为上⾯说到,⼀个寄存器有2个字节,所以后⾯的字节数肯定是2*查询的寄存器个数;
19 98 - 寄存器的值是19 98,结合发送的数据看出,01这个寄存器的值为19 98
B2 7E - 循环冗余校验
总结就是:
发送:从机的地址+我要⼲嘛的功能码+我要查的寄存器的地址+我要查的寄存器地址的个数+校验码
回复:从机的地址+主机发我的功能码+要发送给主机数据的字节数+数据+校验码。
在 C++/Qt 开发中的应用
对于 C++/Qt 开发者,尤其是嵌入式和工业控制领域,ModBus 是必备的通信协议,常见的实现方式有两种:
-
使用 Qt 官方库:Qt SerialBus Qt 5.8 及以上版本提供了
Qt SerialBus模块,原生支持 ModBus RTU/TCP,无需依赖第三方库,示例代码框架如下:cpp#include <QModbusRtuSerialMaster> #include <QModbusDataUnit> // 初始化 ModBus RTU 主设备 QModbusRtuSerialMaster *modbusMaster = new QModbusRtuSerialMaster(this); modbusMaster->setConnectionParameter(QModbusDevice::SerialPortNameParameter, "COM3"); modbusMaster->setConnectionParameter(QModbusDevice::SerialBaudRateParameter, QSerialPort::Baud9600); modbusMaster->setConnectionParameter(QModbusDevice::SerialParityParameter, QSerialPort::NoParity); modbusMaster->setConnectionParameter(QModbusDevice::SerialDataBitsParameter, QSerialPort::Data8); modbusMaster->setConnectionParameter(QModbusDevice::SerialStopBitsParameter, QSerialPort::OneStop); // 连接设备 if (!modbusMaster->connectDevice()) { qDebug() << "连接失败:" << modbusMaster->errorString(); } // 读取保持寄存器(功能码03,从地址0开始,读取10个寄存器) QModbusDataUnit readUnit(QModbusDataUnit::HoldingRegisters, 0, 10); if (auto *reply = modbusMaster->sendReadRequest(readUnit, 1)) { // 从机地址1 if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, this, [reply]() { if (reply->error() == QModbusDevice::NoError) { const QModbusDataUnit unit = reply->result(); for (int i = 0; i < unit.valueCount(); i++) { qDebug() << "寄存器" << unit.startAddress() + i << "值:" << unit.value(i); } } reply->deleteLater(); }); } else { reply->deleteLater(); } } -
使用第三方库
- libmodbus:开源的 C 语言 ModBus 库,支持 RTU/TCP,可通过 C++ 封装调用,跨平台性好(支持 Windows/Linux/ 嵌入式系统)。
- QtModBus 第三方封装库:针对 Qt 优化的轻量级库,适合资源受限的嵌入式设备。