单片机 MCU(Microcontroller Unit)输出 TTL 板子内部通信,不同超过1m,0~3.3V
RS485: 差分串口,上层常用 Modbus-RTU、厂家私有协议,信号线:A(DA)、B(DB),额外供电 VCC/GND ;
RS485:只是物理层的电气标准(怎么传电平信号)。


CAN/CANFD: 向下兼容 CAN2.0;单帧最大 64 字节,带宽提升 5~8 倍,最高 12Mbps;
485 + Modbus-RTU



URAT 串口 全双工 同时发送和接收
485 差分信号 同一时刻只能一个方向的传输 半双工
Modbus主控信息
Modbus是一主多从的协议,主控发出的数据里,必定含有如下信息:
- 设备地址:你要访问从设备1,还是访问从设备2
- 访问哪类寄存器,是读还是写,只访问1个寄存器,还是多个寄存器:这被称为功能码
- 起始寄存器地址、寄存器数量:这在数据里定义
- 为了保证数据传输的可靠,还附带有CRC检验码
Modbus RTU 数据包格式
以Modbus RTU协议为例,主控发出的数据包格式如下:
| 起始间隙 | 设备地址 | 功能代码 | 数据 | CRC校验 | 停止间隙 |
|---|---|---|---|---|---|
| ≥3.5个字符 | 8位 | 8位 | Nx8位 | 16位 | ≥3.5个字符 |
功能代码有哪些?常用的功能码如下:
- 读线圈状态(01)
- 读离散输入状态(02)
- 写单个线圈(05)、写多个线圈(15)
- 读保持寄存器(03)
- 读输入寄存器(04)
- 写单个保存寄存器(06)、写多个保存寄存器(16)
功能码含义拆解
- Read/Write
- 哪一类寄存器
- 单个或多个
Modbus数据包示例
数据的格式,由功能代码确定。
以"读线圈状态"为例,主控发出的请求格式如下:
| 字段 | 例(Hex) | ASCII模式字符型 | RTU模式8位(Hex) |
|---|---|---|---|
| 帧头 | ":" | ||
| 从设备地址 | 0x03 | "0", "3" | 0x03 |
| 功能码 | 0x01 | "0", "1" | 0x01 |
| 起始地址(高位) | 0x00 | "0", "0" | 0x00 |
| 起始地址(低位) | 0x13 | "1", "3" | 0x13 |
| 寄存器数(高位) | 0x00 | "0", "0" | 0x00 |
| 寄存器数(低位) | 0x25 | "2", "5" | 0x25 |
| 差错校验 | LRC(2字符) | CRC(2字节) | |
| 帧尾 | CR/LF | ||
| 合计字节数 | 17 | 8 |
设备发出的响应报文格式如下:
| 字段 | 例(Hex) | ASCII模式字符型 | RTU模式8位(Hex) |
|---|---|---|---|
| 帧头 | ":" | ||
| 从设备地址 | 0x03 | "0", "3" | 0x03 |
| 功能码 | 0x01 | "0", "1" | 0x01 |
| 数据域字节数 | 0x05 | "0", "5" | 0x05 |
| 数据1 | 0x53 | "5", "3" | 0x53 |
| 数据2 | 0x6B | "6", "B" | 0x6B |
| 数据3 | 0x01 | "0", "1" | 0x01 |
| 数据4 | 0xF4 | "F", "4" | 0xF4 |
| 数据5 | 0x1B | "1", "B" | 0x1B |
| 差错校验 | LRC(2字符) | CRC(2字节) | |
| 帧尾 | CR/LF | ||
| 合计字节数 | 21 | 10 |
数据长度换算示例
0X25 ⇒ 32+5=37
37bit
37bit/8=4.625byte
返回 传输 5byte
位地址示例
bit0 ⇒ 0x13
bit1 ⇒ 0x14
传感器点表(寄存器说明):
| 设备地址 | 寄存器地址 | 寄存器类别 | 用途 | 描述 |
|---|---|---|---|---|
| 03H | 0000H | DO | 控制蜂鸣器1 | 1-响 |
| 0001H | DO | 控制蜂鸣器2 | 1-响 | |
| 0002H | DO | 控制LED1 | 1-亮 | |
| 0003H | DO | 控制LED2 | 1-亮 | |
| 0004H | DO | 控制LED3 | 1-亮 | |
| 0000H | AI | 读取温度 | 单位0.1摄氏度 16位有符号整数 | |
| 0001H | AI | 读取湿度 | 单位0.1%RH 16位有符合整数 |
应用层调用代码
c
do_write_bit(uartbus, 0x03, 0x0002, 1);
HAL_Delay(300);
do_write_bit(uartbus, 0x03, 0x0002, 0);
HAL_Delay(300);
if(ai_read_bit(uartbus, 0x03, 0x0000, &rx_val) == 0)
{
Read_Temp(&rx_val, &temp);
sprintf(buf, "temp : %.1f C ", temp);
Draw_String(0, 0, buf, 0x0000ff00, 0);
}
HAL_Delay(300);
底层驱动实现代码
c
/* 写单个DO */
int do_write_bit(struct Modbus *bus, int dev_addr, int reg_addr, int val)
{
/* 构造包 */
uint8_t tx_buf[8] = {0};
uint8_t rx_buf[8] = {0};
uint16_t crc_val = 0;
tx_buf[0] = dev_addr;
tx_buf[1] = 0x05;
tx_buf[2] = reg_addr >> 8;
tx_buf[3] = reg_addr;
tx_buf[4] = val ? 0xFF : 0x00;
tx_buf[5] = 0x00;
/* 构造发送CRC */
crc_val = CRC16(tx_buf, 6);
tx_buf[6] = crc_val & 0xFF;
tx_buf[7] = crc_val >> 8;
/* 发送数据包 */
bus->Send(bus, tx_buf, 8, 100);
/* 接收数据包 */
bus->Recv(bus, rx_buf, 8, 100);
/* CRC校验 */
crc_val = CRC16(tx_buf, 6);
if(rx_buf[6] == (crc_val & 0xFF) && rx_buf[7] == (crc_val >> 8))
{
return 0;
}
return -1;
}
c
/* 读单个Input Reg */
int ai_read_bit(struct Modbus *bus, int dev_addr, int reg_addr, int *val)
{
/* 构造包 */
uint8_t tx_buf[8] = {0};
uint8_t rx_buf[7] = {0};
uint16_t crc_val = 0;
tx_buf[0] = dev_addr;
tx_buf[1] = 0x04;
tx_buf[2] = reg_addr >> 8;
tx_buf[3] = reg_addr;
tx_buf[4] = 0x00;
tx_buf[5] = 0x01;
/* 构造发送CRC */
crc_val = CRC16(tx_buf, 6);
tx_buf[6] = crc_val & 0xFF;
tx_buf[7] = crc_val >> 8;
/* 发送数据包 */
bus->Send(bus, tx_buf, 8, 100);
/* 接收数据包 */
bus->Recv(bus, rx_buf, 7, 100);
/* CRC校验 */
crc_val = CRC16(rx_buf, 5);
if(rx_buf[5] == (crc_val & 0xFF) && rx_buf[6] == (crc_val >> 8))
{
/* 提取数据 */
*val = (rx_buf[3] << 8) | rx_buf[4];
return 0;
}
return -1;
}
一、 RS485 总线参数:
总线接口:RS485
波特率(常规模式,单电机命令):
9600bps
19200bps
38400bps
57600bps
115200bps(默认)
230400bps
460800bps
1Mbps
2Mbps
4Mbps
波特率(广播模式,多电机命令):
1Mbps
2Mbps
4Mbps
数据位:8
奇偶校验:无
停止位:1
二、 单电机命令:
同一总线上共可以挂载多达 32 个(视总线负载情况而定)驱动,为了防止总线冲突,每个驱动需要设置不同的 ID,ID 编号为 1~32。
主控向总线发送单电机命令帧,对应 ID 的电机在收到命令后执行,并在一段时间后(0.25ms 内)向主控发送相同 ID 的回复帧。命令帧报文和回复帧报文格式如下:帧命令 + 帧数据(可选),具体描述如下表所示
| 数据描述 | 数据长度(byte) | 说明 | |
|---|---|---|---|
| 帧命令 | 帧头 | 1 | 帧头识别,0x3E |
| 命令 | 1 | CMD | |
| ID | 1 | 1~32,对应电机的 ID | |
| 数据长度 | 1 | 描述帧命令附带的数据长度,视不同命令而定 | |
| 帧命令校验字节 | 1 | CMD_SUM,帧命令所有字节校验和,保留低 8 位,高位抛弃 | |
| 帧数据 | 数据 | 0~100 | 帧命令附带的数据 |
| 帧数据校验字节 | 0 或 1 | DATA_SUM,帧数据所有字节校验和,保留低 8 位,高位抛弃 |
串口 TTL差分信号
RS232 全双工
485

● RS485电平:
- 2线制(A、B),差分模式,A-B > 200mV时,总线状态表示逻辑"1";当A-B < -200mV时,总线状态表示逻辑"0";抗共模干扰能力强;
- 最大通信距离1200m;
- 半双工,一主多从通信。

自收发

ARM单片机串口硬件结构
- 起始位:位于数据帧开头,占1位,始终为低电平,用于向接收设备表示发送端开始发送1帧数据;
- 数据位:要传输的数据信息,一般为7/8位,可配置,通常由低位到高位依次传送;
- 校验位:位于数据位之后,占1位,可配置,用于发送数据的校验,或传送多机串行通信的联络信息;
- 停止位 :位于数据位末尾,占0.5~2位,可配置,始终为高电平,用于向接收端表示1帧数据已发送完毕。

空闲是高电平,所以加上下拉电阻。高阻。
120欧姆 终端电阻 多个设备
Modbus-RTU协议

Modbus协议介绍
- Modbus是一种软件协议,支持RS232、RS485、TCP/IP等多种电气接口,
- Modbus协议标准开放、公开发表且无版权要求;
- Modbus协议消息帧格式简单、通俗易懂,便于厂商开发和集成。
Modbus报文
- Modbus可以在各种介质上传输,它的传输报文类型支持三种,包括ASCII、RTU、TCP三种;使用串口传输时可以选择RTU或ASCII模式,当然常用的就是RTU模式。
Modbus-TCP 以太网
Modbus-RTU RS485
Modbus-RTU协议
- Modbus报文是指主机发送给从机的一帧数据,其中包含着从机的地址,主机想执行的操作,校验码等内容,最大为256字节:
| 从机地址 | 功能码 | 数据 | CRC |
|---|---|---|---|
| 1字节 | 1字节 | 0到252字节 | 2字节CRC低CRC高 |
- 从机地址: 每个从机都有唯一地址(机没有地址),占用一个字节,范围0-255,其中从机有效地址范围是1-247;
- 功能码: 占用一个字节,功能码的意义是告诉从机这帧数据是干啥的,可以查询从机的数据,也可以修改从机的数据;
- 数据: 根据功能码不同,对应不同内容,比如功能码是查询从机的数据,这里就包括要查询哪些数据和查询字节数等;
- 校验: 在传输过程中数据坑会发生错误,CRC检验可以检测接收的数据是否正确。
Modbus功能码表
| 描述 | 功能码 (码) | 功能码 (子码) | 功能码 (十六进制) | |
|---|---|---|---|---|
| 比特访问 | 物理离散量输入 - 读输入离散量 | 02 | 02 | |
| 内部比特或物理线圈 - 读线圈 | 01 | 01 | ||
| 内部比特或物理线圈 - 写单个线圈 | 05 | 05 | ||
| 内部比特或物理线圈 - 写多个线圈 | 15 | 0F | ||
| 16比特访问 | 输入存储器 - 读输入寄存器 | 04 | 04 | |
| 内部存储器或物理输出存储器 - 读多个寄存器 | 03 | 03 | ||
| 内部存储器或物理输出存储器 - 写单个寄存器 | 06 | 06 | ||
| 内部存储器或物理输出存储器 - 写多个寄存器 | 16 | 10 | ||
| 内部存储器或物理输出存储器 - 读/写多个寄存器 | 23 | 17 | ||
| 内部存储器或物理输出存储器 - 屏蔽写寄存器 | 22 | 16 | ||
| 文件记录访问 | 读文件记录 | 20 | 6 | 14 |
| 写文件记录 | 21 | 6 | 15 | |
| 封装接口 | 读设备识别码 | 43 | 14 | 2B |
- 寄存器,可以理解为设备上的某一个控制项,比如传感器或者继电器,并不是单片机片上外设的硬件寄存器。
Modbus寄存器地址示例表
- 寄存器,可以理解为设备上的某一个控制项,比如传感器或者继电器,对应的需要在软件中为每一个分配码值,在Mdobus中叫做寄存器地址,比如:
| 寄存器地址 | 字长 | 含义 | 初始值 | 单位 | 读写方式 | 备注 |
|---|---|---|---|---|---|---|
| 0x0000 | 1word | PM2.5 | 0 | ug/m³ | R | 0-999ug/m³ |
| 0x0001 | 1word | 温度 | 0 | C | R | 0-99℃(读取值是显示值的10倍) |
| 0x0002 | 1word | 湿度 | 0 | %RH | R | 20-95%RH(读取值是显示值的10倍) |
| 0x0003 | 1word | CO₂ | 0 | ppm | R | 0~999900m |
| 0x0004 | 1word | TVOC | 0 | mg/m³ | R | 0-9.99mg/m(读取值是显示值的100倍) |
| 0x0005 | 1word | PM10 | 0 | ug/m³ | R | 0-999ug/m³ |
| 0x0006 | 1word | 开关 | 0 | RW | 0关机,1开机 | |
| 0x0007 | 1word | 模式 | 0 | RW | 0手动,1自动 | |
| 0x0008 | 1word | 风速 | 0 | RW | 1低速,2中速,3高速 | |
| 0x0009 | 1word | 风阀 | 0 | RW | 1开阀外循环新风0关阀内循环净化 |
Modbus通信举例1:主机读取从机环境参数
- 通信举例,主机读取从机环境参数(以PM2.5 值75、温度24、湿度值38、CO₂值900、TVOC 值1.25、PM10 值100为例):
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| 设备地址 | 功能码 | 寄存器地址高字节 | 寄存器地址低字节 | 寄存器个数高字节 | 寄存器个数低字节 | CRC低字节 | CRC高字节 | |
| 0x01 | 0x03 | 0x00 | 0x00 | 0x00 | 0x06 | 0xC5 | 0xCB |
从机返回数据:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|---|
| 设备地址 | 功能码 | 应答字节数 | 数据内容1高字节 | 数据内容1低字节 | 数据内容2高字节 | 数据内容2低字节 | 数据内容3高字节 | 数据内容3低字节 | |
| 0x01 | 0x03 | 0x0C | 0x00 | 0x4B | 0x00 | 0xF0 | 0x01 | 0x7C |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | |
|---|---|---|---|---|---|---|---|---|
| 数据内容4高字节 | 数据内容4低字节 | 数据内容5高字节 | 数据内容5低字节 | 数据内容6高字节 | 数据内容6低字节 | CRC低字节 | CRC高字节 | |
| 0x03 | 0x84 | 0x00 | 0x7D | 0x00 | 0x64 | 0xFA | 0x44 |
Modbus通信举例2:控制从机开机
- 通信举例,控制从机开机:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| 设备地址 | 功能码 | 寄存器地址高字节 | 寄存器地址低字节 | 数据内容高字节 | 数据内容低字节 | CRC低字节 | CRC高字节 | |
| 0x01 | 0x06 | 0x00 | 0x06 | 0x00 | 0x01 | 0xA8 | 0x0B |
从机返回数据:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| 设备地址 | 功能码 | 寄存器地址高字节 | 寄存器地址低字节 | 数据内容高字节 | 数据内容低字节 | CRC低字节 | CRC高字节 | |
| 0x01 | 0x06 | 0x00 | 0x06 | 0x00 | 0x01 | 0xA8 | 0x0B |
Modbus帧间隔 (3.5T)
- 为了使软件更容易判断一帧数据包是否结束,Modbus规定两帧数据包之间至少有3.5 个字符时间的空闲间隔,叫做3.5T:
| 空闲 | 从机地址 | 功能码 | 数据 | CRC | 空闲 |
|---|---|---|---|---|---|
| 3.5字符 | 1字节 | 1字节 | 0到252字节 | 2字节CRC低CRC高 | 3.5字符 |
- 当串口波特率大于19200时,那么3.5T固定为1750us;
- 当串口波特率小于等于19200时,假如串口设置为:起始位1bit + 数据位8bit + 停止位1bit,
- 1个字符传送所需时间,charTime=(10*1000/baudRate)ms;
- 假设波特率为9600,charTime=11*1000/9600=1.15ms;
- 3.5个字符需要的时间,3.5*charTime=4ms。
MODBUS异常码
- MODBUS异常码:
| 代码 | 名称 | 含义 |
|---|---|---|
| 01 | 非法功能 | 从机不支持此功能码 |
| 02 | 非法地址 | 指定的地址在从机中不存在 |
| 03 | 非法数据值 | 指定的数据超过范围或者不允许使用 |
| 04 | 从机故障 | 从机处理响应的过程中,出现未知错误等 |
Modbus异常响应举例
- 通信举例,控制从机开机,如果寄存器地址不存在:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| 设备地址 | 功能码 | 寄存器地址高字节 | 寄存器地址低字节 | 数据内容高字节 | 数据内容低字节 | CRC低字节 | CRC高字节 | |
| 0x01 | 0x06 | 0x00 | 0xFF | 0x00 | 0x01 | 0xA8 | 0x0B |
在异常响应中,从机返回的功能码最高位置1,异常码0x02,表示非法地址:
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 设备地址 | 功能码 | 异常码 | CRC低字节 | CRC高字节 | |
| 0x01 | 0x86 | 0x02 | 0xC3 | 0xA1 |
提问
一、 基础概念篇
Q1:请用一句话说明 RS485 和 Modbus-RTU 的关系?
- 核心考点:分层思想。
- 参考答案 :RS485 是物理层硬件标准 (定义电气信号、连线方式),而 Modbus-RTU 是应用层软件协议(定义数据格式、通信规则)。Modbus-RTU 可以运行在 RS485 或 RS232 上;RS485 总线上也可以跑其他私有协议。
Q2:RS485 相比 RS232 有什么优势?为什么工业现场更常用 RS485?
- 核心考点:差分信号、传输距离、组网能力。
- 参考答案 :
- 抗干扰强:RS485 采用差分信号(A、B线),共模干扰能相互抵消。
- 距离远:RS232 理论距离仅 15 米,RS485 可达 1200 米。
- 支持多机:RS232 只能点对点,RS485 支持"一主多从"(通常最多挂载 32 个节点,加中继器可扩展)。
Q3:RS485 是半双工还是全双工?如何实现双向通信?
- 参考答案 :RS485 通常是半双工(2线制),不能同时收发。需要主从轮询机制。在硬件上,MCU 需要一个 GPIO 控制收发器的 DE/RE 引脚,平时处于接收态,发送时拉高使能发送,发送完毕立刻切回接收。
二、 硬件与电气篇
Q4:为什么 RS485 总线两端需要接 120Ω 的终端电阻?
- 核心考点:信号反射。
- 参考答案 :为了进行阻抗匹配,吸收总线上的信号反射。如果不接,长距离或高波特率下会出现信号震荡、过冲,导致通信误码或失败。通常只需要在总线的最远两端各接一个 120Ω 电阻。
Q5:RS485 的 A 线和 B 线接反了会发生什么?
- 参考答案:由于差分信号 A-B > 200mV 表示逻辑"1",A-B < -200mV 表示逻辑"0"。接反后,逻辑电平会反转,导致收发数据全错。如果用的是硬件串口,通常会表现为一直进入错误中断(如帧错误),无法正常接收数据。
Q6:RS485 硬件设计要注意什么?(防雷、防浪涌、共地)
- 参考答案 :
- 总线要使用双绞线(屏蔽层单点接地,防地环流)。
- 工业现场必须加 TVS 管、防雷管、自恢复保险丝进行浪涌和静电保护。
- 共地问题:如果两端设备不共地,可能烧毁收发器,建议使用隔离型 RS485 收发器(如带光耦或磁耦隔离的 ADM2483)。
三、 Modbus-RTU 协议细节篇
Q7:Modbus-RTU 的一帧数据格式是什么?
- 参考答案 :
从机地址(1Byte) + 功能码(1Byte) + 数据(N Bytes, 0~252) + CRC校验(2Bytes, 低字节在前)。最大总长度 256 字节。
Q8:Modbus 的 3.5T 间隔是什么?波特率大于 19200 时怎么算?
- 参考答案 :为了区分两帧数据,Modbus-RTU 规定帧与帧之间至少有 3.5 个字符时间的空闲间隔。
- 波特率 > 19200bps 时,3.5T 固定为 1750 微秒。
- 波特率 ≤ 19200bps 时,按公式计算(如 9600bps,1个字符 11bit,3.5 * 11 * 1000 / 9600 ≈ 4ms)。
Q9:常用的 Modbus 功能码有哪些?(举例说明)
- 参考答案 :
0x03:读保持寄存器(最常用,读传感器数据、电机参数)。0x06:写单个保持寄存器(最常用,控制设备开关、设置参数)。0x01/0x05:读/写线圈(控制继电器、开关量)。0x10:写多个保持寄存器。
Q10:Modbus 通信如果出错,从机是怎么返回异常码的?
- 参考答案 :从机返回的功能码最高位(MSB)置 1 (即原功能码 + 0x80),后跟一个字节的异常码。
- 例如:主机发
0x03,从机返回0x83+ 异常码。 - 异常码
0x01:非法功能;0x02:非法地址(最常见);0x03:非法数据值。
- 例如:主机发
Q11:Modbus 的 CRC 校验有什么特点?
- 参考答案 :CRC-16 (多项式 0xA001),校验范围是从机地址到数据末尾。在报文中,CRC 低字节在前,高字节在后。
四、 测试与排障实战篇
Q12:如果一台设备 RS485 通信不上(超时),你的排查思路是什么?
- 参考答案(分步) :
- 物理层:检查 A/B 线是否接反;是否共地;终端电阻是否匹配;供电是否正常。用示波器看 A/B 线是否有差分波形,波形是否畸变。
- 参数配置:主从设备的波特率(如 9600 或 115200)、数据位(8)、停止位(1)、校验位(无)是否完全一致;从机地址(ID)是否匹配。
- 软件层面:3.5T 间隔是否符合要求;如果是轮询,是否有总线冲突(多个从机同时回复);是否开启了大端/小端模式转换。
- 工具辅助:用 USB 转 485 配合 XCOM 或 Modbus Poll 抓包监听,看主机发了什么,从机回了什么。
Q13:如何对 Modbus-RTU 进行自动化测试或压力测试?
- 参考答案 :
- 工具 :用 Python 编写脚本(结合
pymodbus库或直接pyserial发原始 Hex 帧)。 - 测试内容 :
- 功能测试:遍历所有功能码,测试读/写是否正常。
- 边界测试:发送非法地址、非法数据值、错误 CRC,验证从机是否按标准返回异常码(0x83 + 0x02 等)。
- 异常注入:拔掉通信线、注入干扰、模拟帧间隔不足(比如连续快速发两帧,中间不加 3.5T,测试从机容错处理)。
- 压力测试:以最高支持波特率(如 115200)和最大挂载节点数(32个)循环读写,跑 24 小时甚至几天,统计丢包率、错误帧数。
- 工具 :用 Python 编写脚本(结合
Q14:RS485 和 CAN 总线有什么区别?在机器人关节模组中怎么选?
- 核心考点:架构与可靠性。
- 参考答案 :
- 架构 :RS485 是一主多从 (主站轮询,从机被动应答);CAN 是多主架构(基于优先级的 CSMA/CR 仲裁,节点可主动发送)。
- 协议层:RS485 只有物理层,靠上层软件(如 Modbus)处理冲突和重发;CAN 控制器硬件自带数据链路层(CRC校验、错误重发、报文过滤)。
- 机器人场景 :如果是简单的传感器数据采集,RS485+Modbus 成本低;如果是多个关节电机的高实时性协同控制(如人形机器人),CANopen 或 CAN FD 更具优势,因为 CAN 的硬件级仲裁和错误处理保证了极高的实时性和可靠性。
一、 核心概念:Modbus-RTU 到底是什么?
- 软件协议:Modbus 是应用层协议,而 RTU(Remote Terminal Unit)是它最常用的一种传输模式(另一种是 ASCII)。
- 依赖物理层 :它通常运行在 RS485(最常用)或 RS232 硬件之上。
- 通信模式 :一主多从(Master-Slave) 、半双工。主站发起请求,从站被动应答(不问不答)。从站之间绝对不能直接通信。
二、 物理层基础(RS485)
- 接线:2线制(A、B),差分信号(A-B > 200mV 为逻辑1,A-B < -200mV 为逻辑0)。
- 拓扑 :总线型手拉手连接,最远两端必须接 120Ω 终端电阻(防信号反射)。
- 距离与节点:最大通信距离 1200m,标准挂载 32 个节点(加中继器可扩展)。
- 抗干扰:采用双绞屏蔽线,屏蔽层单点接地。
三、 数据帧结构(核心知识点)
Modbus-RTU 的一帧数据由四部分组成,最大总长度 256 字节:
| 从机地址 | 功能码 | 数据 | CRC 校验 |
|---|---|---|---|
| 1 Byte | 1 Byte | N Bytes (0~252) | 2 Bytes (低在前, 高在后) |
- 从机地址:范围 1~247(0 是广播地址,248~255 保留)。
- 功能码:告诉从机要干什么(见下文)。
- 数据:具体参数(如寄存器地址、要读的个数、要写的值)。
- CRC 校验:对"从机地址到数据末尾"进行 CRC-16 计算。
四、 帧间隔:3.5T(避免粘包)
为了让从机区分一帧是否结束,Modbus-RTU 规定两帧数据之间至少有 3.5 个字符时间的空闲。
- 波特率 > 19200bps :3.5T 固定为 1750 微秒。
- 波特率 ≤ 19200bps :按公式计算(如 9600bps,1个字符占 11bit,3.5 * 11 * 1000 / 9600 ≈ 4ms)。
- 注:一帧数据内部,字符与字符之间的间隔不能超过 1.5 个字符时间,否则会被视为错误帧。
五、 四大常用功能码
| 功能码 (Hex) | 名称 | 作用 | 典型应用 |
|---|---|---|---|
| 0x03 | 读保持寄存器 | 读取设备的参数或数据 | 读温度、速度、电流、PM2.5 |
| 0x06 | 写单个保持寄存器 | 修改设备的某个参数 | 开关机、设置目标速度、修改PID |
| 0x01 | 读线圈 | 读取开关量状态 | 读取继电器状态、限位开关 |
| 0x05 | 写单个线圈 | 控制开关量输出 | 控制继电器吸合、电机使能 |
六、 通信过程与实战报文解析(结合您的图片案例)
案例1:读取环境参数(主机读,功能码 0x03)
- 主机发送 :
01 03 00 00 00 06 C5 CB01:目标从机地址为 1。03:功能码,读取保持寄存器。00 00:起始寄存器地址(PM2.5 的地址)。00 06:连续读取 6 个寄存器(PM2.5、温度、湿度、CO2、TVOC、PM10)。C5 CB:CRC 校验(低字节 C5,高字节 CB)。
- 从机返回 :
01 03 0C 00 4B 00 F0 01 7C ...01:本机地址。03:功能码。0C:字节数(6个寄存器 × 2字节 = 12 = 0x0C)。00 4B:第1个数据(PM2.5),0x004B = 75。00 F0:第2个数据(温度),0x00F0 = 240,由于文档注明"读取值是显示值的10倍",所以实际温度为 24.0℃。
案例2:控制从机开机(主机写,功能码 0x06)
- 主机发送 :
01 06 00 06 00 01 A8 0B01:从机地址。06:写单个寄存器。00 06:寄存器地址(控制开关的地址)。00 01:写入的值(1 表示开机)。A8 0B:CRC 校验。
- 从机返回 :原样返回主机的报文 (即
01 06 00 06 00 01 A8 0B),表示写入成功。
七、 异常处理(排障关键)
如果主机发了错误指令,从机不会沉默,而是返回异常响应帧。
- 规则 :功能码最高位置 1(即原功能码 + 0x80),后跟一个字节的异常码。
- 异常码 :
01:非法功能(从机不支持此功能码)。02:非法地址(地址不存在,如发了个0x00FF但设备只有0x0000~0x0009)。03:非法数据值(写入的值超出范围)。04:从机故障(处理时出现未知错误)。
- 示例 :主机发
01 06 00 FF 00 01 ...,从机返回01 86 02 C3 A1(0x06 + 0x80 = 0x86,异常码 02 表示非法地址)。
八、 实战排障速查指南
Q:RS485 Modbus 通信不上,怎么排查?
- 看硬件:A/B 线是否接反?是否共地?终端电阻是否接入(长距离必须接)?供电是否正常?
- 看配置:波特率(9600/115200)、数据位(8)、停止位(1)、校验位(无)是否主从完全一致?从机地址是否匹配?
- 看软件:3.5T 帧间隔是否满足?是否出现多个从机同时回复导致总线冲突?
- 看工具:用 USB 转 485 工具配合 XCOM、Modbus Poll 或 ZCANPRO(如果是 CAN 转 485 网关)抓包,看波形,看谁先回的数据,抓到底是谁在捣乱。
Q:如何做 Modbus-RTU 的自动化测试(结合 Python)?
- 使用 Python 的
pyserial库打开串口,或者使用pymodbus库直接调用高层 API。 - 功能测试:遍历所有支持的功能码和寄存器地址。
- 边界/异常测试:发送非法地址、非法数据、错误的 CRC、破坏 3.5T 间隔(连续快速发两帧),看从机是否按标准返回异常码。
- 压力测试:以最高波特率 115200 和最大节点数,长时间(24h+)循环读写,统计丢包率、CRC 错误率,验证总线稳定性。
CRC
在 Modbus-RTU 中,CRC(循环冗余校验)采用的是 CRC-16/MODBUS 标准。
基于多项式除法。
一、 CRC-16/MODBUS 的核心规则
在开始计算前,必须牢记 Modbus CRC 的 4 个死规定:
- 初始值 :
0xFFFF(16位全1)。 - 多项式 :
0xA001(这是 0x8005 反转后的结果,因为 Modbus 采用低位在前的传输方式)。 - 计算范围 :从机地址、功能码、数据 (即不包含 CRC 本身的所有字节)。
- 结果处理 :计算出的 16 位结果,低字节在前,高字节在后(小端模式)放入报文中。
二、 推导:以案例 1 为例
案例 1 主机发送的前 6 个字节 01 03 00 00 00 06 为例,来推导它是如何得出 C5 CB 的。
准备工作:
- 将每个字节看作 8 位二进制。
- 准备一个 16 位的寄存器,初始值为
0xFFFF。
核心算法(逐字节异或与移位) :
每处理一个字节,执行以下步骤:
- 将 16 位寄存器的低 8 位与当前字节进行 异或(XOR) 运算。
- 将寄存器右移 1 位,最高位补 0。
- 如果移出的最低位是 1 ,则将寄存器与多项式
0xA001进行 异或。 - 如果移出的最低位是 0,则不做异或。
- 重复步骤 2-4,共 8 次(处理完一个字节的 8 位)。
- 处理下一个字节,重复上述所有步骤,直到所有字节处理完毕。
(由于手动推导 6 个字节的 16 位运算非常冗长,这里展示简化逻辑)
- 假设初始
0xFFFF,处理完01后,寄存器变为某个值。 - 依次处理
03,00,00,00,06。 - 最终得到的 16 位结果是:高字节为
0xCB,低字节为0xC5。 - 按照 Modbus 规定,低字节在前 ,所以报文末尾是
C5 CB。
补充:上面您写的从机返回数据
01 03 0C 00 4B 00 F0 01 7C ...后面的...就是 CRC,计算时要把前面的01 03 0C 00 4B 00 F0 01 7C全部算进去,得出 CRC 放在最后。
三、 实际工程中,CRC 怎么来的?
1. 软件代码自动计算(最常用)
实际开发中,开发者不会用手算,而是写一个 C 语言函数或 Python 脚本,让 MCU 自动计算。
c
// 经典的 Modbus CRC16 C语言实现
uint16_t Modbus_CRC16(uint8_t *buffer, uint16_t length) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
crc ^= buffer[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc; // 返回值低字节在前,高字节在后
}
在发送数据前,调用这个函数,把算出的低字节和高字节分别拼接到报文末尾。
2. 查表法(空间换时间)
在资源紧张的 MCU 中,为了节省计算时间,会预先计算好 0~255 对应的 CRC 值存入数组,运行时直接查表,速度极快。这是工业级代码的标配。
3. 工具自动生成
在调试阶段,如果您用的是 XCOM 串口调试助手、Modbus Poll 或 SSCOM,这些工具都有自带的 CRC 计算功能 。您在发送框输入 01 03 00 00 00 06,点击"添加CRC校验",工具会自动帮您追加 C5 CB。
4. 从机校验(排障关键)
从机收到报文后,会把前面所有字节(不含收到的 CRC)重新算一遍 CRC。如果算出来的 CRC 与收到的 CRC 一致,说明数据没出错;如果不一致,从机直接丢弃,不回复(这就是为什么有时主站发送了,但从机没反应,可能是 CRC 错了)。
四、 总结
- 原理 :基于 CRC-16/MODBUS 标准,多项式是
0xA001,初始值是0xFFFF。 - 算法:将报文每个字节与寄存器异或,然后循环右移 8 次,若最低位为 1 则异或多项式,最终得出 16 位结果。
- 工程实现 :实际开发不用手算,用 C语言循环算法 或 查表法 在 MCU 中自动生成,报文里低字节在前,高字节在后,接收方会重新计算比对,校验失败直接丢帧不回复。