4、接收态详解:帧起始检测、地址匹配、CRC校验。接收超时与帧长度异常处理
好,咱们今天聊聊从站状态机里最核心的一个环节——接收态。说白了,就是从站怎么判断「这包数据是发给我的吗?数据有没有坏?我该不该收?」
我做了这么多年Modbus,发现很多新手甚至老手,都在接收态上栽过跟头。不是地址配错了,就是CRC算反了,再不然就是超时没处理好,导致整个从站卡死。嗯,咱们今天就把这些坑一个一个填平。
4.1 帧起始检测:从沉默中苏醒
接收态的第一步,是检测帧起始。Modbus是异步串行通信,没有专门的帧起始信号。那怎么知道一帧开始了?
答案是:总线空闲后的第一个字节。
我个人的习惯是,在接收中断里维护一个「总线空闲计时器」。只要总线上有数据,计时器就清零;一旦总线空闲超过3.5个字符时间,我就认为上一帧结束了,下一帧即将开始。
这里有个关键点:3.5字符时间怎么算?
| 波特率 | 1个字符时间(10位) | 3.5字符时间 |
|---|---|---|
| 9600 | 1.04 ms | 3.65 ms |
| 19200 | 0.52 ms | 1.82 ms |
| 115200 | 0.087 ms | 0.30 ms |
你看,波特率越高,时间越短。我见过有人用固定1ms做超时,结果9600能跑,115200就丢帧。为什么?因为115200的3.5字符时间才0.3ms,1ms的计时器根本来不及复位。
4.2 地址匹配:这包数据是给我的吗?
检测到帧起始后,第一个字节就是从站地址。这时候要做两件事:
- 判断地址是否匹配——是自己的地址吗?
- 判断是否是广播地址——0x00是广播,所有从站都要收。
代码实现其实很简单:
// 接收中断服务函数
void UART_IRQHandler(void) {
uint8_t rx_byte = UART->DR;
if (bus_idle_timer > 3.5_char_time) {
// 新帧开始
rx_index = 0;
rx_buffer[rx_index++] = rx_byte;
frame_started = 1;
} else if (frame_started) {
// 继续接收
rx_buffer[rx_index++] = rx_byte;
// 检查是否超长
if (rx_index > MAX_FRAME_LEN) {
// 帧长度异常,丢弃
frame_started = 0;
rx_index = 0;
}
}
// 复位总线空闲计时器
bus_idle_timer = 0;
}
注意看,我在接收第一个字节后,并没有立即做地址匹配。为什么?因为地址匹配可以放在帧接收完成后统一处理。你想想看,如果第一个字节地址不匹配,后面的数据还要不要收?
我的建议是:收完整个帧再做地址匹配。 原因有二:
- 万一地址不匹配,但后面有CRC错误,你提前丢弃了,主站重发时你反而收不到。
- 广播地址0x00需要接收完整帧,不能因为地址不是自己的就丢弃。
4.3 CRC校验:最后的防线
帧收完了,地址也匹配了,接下来就是CRC校验。Modbus RTU用的是CRC-16,多项式是0x8005,初始值0xFFFF。
我见过很多人直接在网上复制CRC代码,结果高低字节搞反了。Modbus的CRC是低字节在前,高字节在后。也就是说,收到的CRC字节顺序是:CRC_L, CRC_H。
校验逻辑很简单:
uint16_t calc_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; // 0xA001是0x8005的反转
} else {
crc >>= 1;
}
}
}
return crc;
}
// 校验时,计算除最后2个字节外的所有数据的CRC
uint16_t calc_crc = calc_crc16(rx_buffer, rx_len - 2);
uint16_t recv_crc = (rx_buffer[rx_len - 1] << 8) | rx_buffer[rx_len - 2];
if (calc_crc != recv_crc) {
// CRC错误,丢弃帧
// 注意:不要发送任何响应
}
这里有个细节:CRC校验失败后,从站应该怎么做?
答案是:什么都不做。不要发异常响应,不要发任何东西。因为CRC错误说明数据可能被干扰了,你发的任何响应都可能造成总线冲突。
4.4 接收超时与帧长度异常处理
这两个问题,是实际项目中最容易出Bug的地方。
4.4.1 接收超时
什么叫接收超时?就是一帧数据收了一半,突然停了。比如主站发了地址和功能码,然后掉线了。从站不能一直等下去,必须有个超时机制。
我个人的做法是:帧间超时用3.5字符时间,帧内超时用1.5字符时间。
- 帧间超时(3.5字符):用于判断上一帧结束,下一帧开始。
- 帧内超时(1.5字符):用于判断接收过程中,字节间隔是否过长。
如果帧内超时触发,说明数据不完整,直接丢弃当前帧,复位接收状态机。
// 定时器中断,每1ms执行一次
void Timer_IRQHandler(void) {
if (frame_started) {
byte_gap_timer++;
if (byte_gap_timer > 1.5_char_time_ms) {
// 帧内超时,丢弃
frame_started = 0;
rx_index = 0;
byte_gap_timer = 0;
}
}
bus_idle_timer++;
if (bus_idle_timer > 3.5_char_time_ms) {
// 总线空闲超过3.5字符,标记帧结束
if (frame_started && rx_index > 0) {
// 处理完整帧
process_frame();
}
frame_started = 0;
rx_index = 0;
}
}
4.4.2 帧长度异常
帧长度异常有两种情况:
- 太短:比如只有2个字节(地址+功能码),没有数据和CRC。
- 太长:超过了Modbus规定的最大帧长度(RTU模式下通常是256字节)。
对于太短的帧,我的做法是:等超时后丢弃。因为你不确定后面还有没有数据,万一只是字节间隔长了点呢?
对于太长的帧,我的做法是:立即丢弃,并复位接收缓冲区。因为Modbus协议规定,一帧数据不可能超过256字节。如果超过了,要么是干扰,要么是协议错误,没必要再收下去。
4.5 知识体系总览
说了这么多,咱们用一张图来总结一下接收态的核心逻辑:
这张图把接收态的核心逻辑串起来了。你仔细看,从总线空闲开始,到帧起始检测、地址匹配、CRC校验,每一步都有分支。不匹配或校验失败,就回到空闲状态继续监听;只有全部通过,才进入帧处理。
嗯,接收态的内容就这些。说白了就是三个判断:是不是给我的?数据坏没坏?收完整没有? 把这三点做好了,你的从站就稳了八成。