本节摘要:RTU 是最主流的 Modbus 串口模式——二进制紧凑、CRC16 校验可靠。本节讲清 RTU 帧结构、静默间隔的工程意义、CRC16 算法与实现示例,以及"帧边界怎么找"这个现场高频问题。位置:第 3 章掌握了 PDU,本节看 PDU 在串口上怎么打包成帧。
RTU 帧在 PDU 外加了地址与 CRC:
RTU 帧 = 从站地址(1B) + PDU(功能码+数据) + CRC16(2B,低字节在前) 例:读保持寄存器 40001(偏移0)1 个 01 03 00 00 00 01 84 0A 地址01 功能03 起始0000 数量0001 CRC(0A84,发送低字节在前)
三个要点:地址在前——每个从站先看地址决定是否响应;CRC 在尾——校验覆盖从地址到数据末尾的全部字节;CRC 低字节先发——很多手算工具给的是 0x840A 而线上顺序是 0A 84,收发不一致是"自己发自己收不对"的经典原因。
RTU 没有帧头帧尾标记,接收方靠时间静默切帧:帧与帧之间至少要有 3.5 个字符时间的静默;帧内字符间隔不得超过 1.5 个字符时间。9600 波特、8 位数据位 + 校验 + 停止位(共 11 位/字符)下:
1 字符时间 = 11 / 9600 ≈ 1.146ms 3.5 字符静默 ≈ 4.01ms(帧间最小间隔) 1.5 字符间隔 ≈ 1.72ms(帧内最大间隔)
实现接收时,用定时器检测串口空闲:空闲超过 3.5 字符时间 → 一帧结束。现场"报文偶发粘包/断帧",八成是波特率不一致(两台设备 9600 vs 19200 会让静默时间错乱)或帧内间隔超限(高负载中断把串口挤得断断续续)。
Modbus CRC16 采用多项式 0xA001(反射多项式),初始值 0xFFFF。C 语言实现:
uint16_t modbus_crc16(const uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; /* 线上发送:先低字节后高字节 */ }
自测向量:对 01 03 00 00 00 01 计算 CRC 应得 0x840A——先把这段向量跑通,再接入真实报文,能排除算法本身的问题。
波特率决定帧时间:9600 下 8 字节帧约 9ms,115200 下不到 1ms。选波特率要平衡速率与可靠性:距离长、干扰大、线缆差的环境用低波特率(9600 是工业默认的"稳妥档");短距离高质量布线可用 115200。整条总线上所有设备必须同波特率——混合波特率时,慢设备会"看到"快设备的半截帧然后沉默,表现就是"加了一台设备整条线全乱"。
要点回顾:RTU = 地址 + PDU + CRC16(低字节先发);帧边界靠 3.5/1.5 字符静默划分;CRC 用 0xA001 反射多项式,先跑通自测向量。RTU 看完了,下一节看被它取代的 ASCII 模式为什么处境尴尬。
正确接收 RTU 帧需要"字符超时检测"机制,实现上有两种主流形态。形态一,串口空闲中断:不少 MCU 的串口外设有"空闲检测"(IDLE line detection),检测到 3.5 字符以上的静默自动产生空闲中断——这是最可靠的方式,一帧结束即被硬件识别。形态二,软件定时器:每收到一个字节重启 1.5 字符定时器,超时即判定"帧内间隔超限",丢弃当前半帧重新等待帧头;同时用一个较长定时器检测 3.5 字符的帧间静默。两者选型看芯片外设能力,但没有空闲中断的 MCU 必须用软件定时器,且帧内间隔超限的判定不能省——否则粘帧会把两条报文当成一条解析,CRC 必然算错,表现为"从站总是沉默"。
/* 软件定时器法:每字节中断处理(伪代码) */ void on_rx_byte(uint8_t b) { rx_buf[rx_len++] = b; timer_restart(CHAR_TIMEOUT_15, 1.72f); /* 1.5 字符超时 */ if (rx_len == 1) timer_restart(FRAME_GAP_35, 4.0f); /* 3.5 字符超时 */ } void on_char_timeout(void) { rx_len = 0; } /* 断帧丢弃 */ void on_frame_gap(void) { process_frame(rx_buf, rx_len); rx_len = 0; }
通信不上的第一排查动作是确认双方波特率一致,但"一致"不只是配置相同——晶振误差会让实际波特率偏离标称值。9600 波特下允许的偏差约 ±2%,多数 UART 能容忍;但 115200 下同样误差占比更大,且一主多从时所有设备的晶振误差会叠加。验证方法:示波器量主站 TX 引脚一个字符的时长(10 或 11 位时间),反推实际波特率;或用串口助手"自动探测"功能让工具尝试常见波特率。结论:波特率不一致是"一接就通不了"的头号原因,先排除它再排查其它。
有工程师觉得"3.5 字符静默太慢,能不能缩短"。不能——静默间隔是接收方切帧的唯一依据,缩短会导致接收方把上一帧的尾巴与下一帧的头合并成"一帧",CRC 与解析全乱。反过来,静默过长(比如主站轮询间隔远大于 3.5 字符)不影响正确性,只影响吞吐。把轮询间隔当静默间隔是另一类错误:轮询间隔 50ms 远远大于 3.5 字符(约 4ms@9600),接收方早就切帧了,没有任何问题。真正要保的是"帧内间隔 < 1.5 字符",这在长响应帧(读几十个寄存器)时最容易被高负载中断破坏。
