4、Modbus RTU实战:CRC16校验算法实现(查表法与计算法)、波特率自适应策略、多机通信时序
好,咱们进入第四讲。这一章的内容,说白了就是Modbus RTU通信里最硬核的几个坎儿。CRC校验、波特率自适应、多机通信时序——这三个东西你要是搞明白了,Modbus RTU这块基本就通了。我个人习惯把这章叫做“实战三件套”,因为每个点都是我在项目里踩过坑、填过坑的地方。
4.1 CRC16校验:查表法 vs 计算法
先聊CRC。CRC16校验,是Modbus RTU的“身份证”。每个报文末尾两个字节就是它。你发出去的数据对不对,接收方拿CRC一算就知道。嗯,这里要注意:CRC算错了,整个报文直接丢弃,不会给你任何情面。
实现CRC16,主流有两种方式:查表法和计算法。我当年刚入行时,觉得计算法简单,直接撸代码。后来发现,在资源紧张的MCU上,计算法太慢了。你想想看,每收到一个字节就要循环8次,一个报文几十个字节,CPU全耗在算CRC上了。
4.1.1 计算法(直接法)
计算法也叫“逐位法”。它的原理就是模拟多项式除法。Modbus RTU用的多项式是 0x8005(反转后是 0xA001)。
我直接给代码,你们拿去就能用:
// CRC16计算法实现(标准Modbus RTU)
uint16_t CRC16_Calculate(uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF;
uint16_t i, j;
for (i = 0; i < len; i++)
{
crc ^= data[i];
for (j = 0; j < 8; j++)
{
if (crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
4.1.2 查表法
查表法,说白了就是“空间换时间”。提前把256个字节对应的CRC值算好,存成一个表。运行时直接查表,一个字节只需要一次查表+一次异或,速度飞快。
我曾经在一个项目里,MCU主频只有16MHz,波特率却要跑到115200。用计算法,CPU占用率飙到80%以上。换成查表法后,直接降到15%。嗯,差距就这么大。
先看表怎么生成:
// 生成CRC16查表法的高字节表
uint16_t crc16_table[256];
void CRC16_InitTable(void)
{
uint16_t i, j;
uint16_t crc;
for (i = 0; i < 256; i++)
{
crc = i;
for (j = 0; j < 8; j++)
{
if (crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
crc16_table[i] = crc;
}
}
然后查表计算:
// CRC16查表法计算
uint16_t CRC16_Table(uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF;
uint16_t i;
uint8_t index;
for (i = 0; i < len; i++)
{
index = (crc ^ data[i]) & 0xFF;
crc = (crc >> 8) ^ crc16_table[index];
}
return crc;
}
4.2 波特率自适应策略
波特率自适应,这是个“看起来简单,做起来坑多”的东西。你想想看,一个逆变器装到现场,上位机可能用9600,也可能用19200,甚至115200。总不能每次都要人去配置吧?
我见过最笨的办法:让用户拨码开关。嗯,十年前确实有人这么干。现在嘛,基本都是自动检测了。
波特率自适应的核心思路:测量两个下降沿之间的时间。Modbus RTU的起始位是低电平,停止位是高电平。一个字节的传输,起始位+8数据位+停止位,总共10位(无校验时)。
具体做法:
- 等待总线空闲(高电平持续至少3.5个字符时间)
- 检测到下降沿(起始位开始)
- 启动定时器,测量到下一个下降沿的时间
- 这个时间就是1个字符(10位)的传输时间
- 用这个时间反推出波特率
代码实现思路:
// 波特率自适应伪代码
uint32_t DetectBaudRate(void)
{
uint32_t time1, time2, time3;
uint32_t bit_time;
// 等待总线空闲
while(!IsBusIdle());
// 等待第一个下降沿
while(ReadPin() == 1);
time1 = GetTimer();
// 等待第二个下降沿
while(ReadPin() == 1);
time2 = GetTimer();
// 等待第三个下降沿
while(ReadPin() == 1);
time3 = GetTimer();
// 计算平均位时间(10位一个字符)
bit_time = ((time2 - time1) + (time3 - time2)) / 2 / 10;
// 反算波特率
return 1000000 / bit_time; // 假设定时器精度为1us
}
4.3 多机通信时序
多机通信,这是Modbus RTU的看家本领。一个主机带多个从机,从机地址从1到247。但这里有个关键问题:时序。
Modbus RTU的时序要求很严格。主机发完一帧后,从机必须在3.5个字符时间内回复。超过这个时间,主机就认为从机超时了。这个3.5个字符时间怎么算?
| 波特率 | 1位时间 | 1字符时间(10位) | 3.5字符时间 |
|---|---|---|---|
| 9600 | 104.2μs | 1.042ms | 3.646ms |
| 19200 | 52.1μs | 0.521ms | 1.823ms |
| 115200 | 8.68μs | 0.087ms | 0.304ms |
你看,波特率越高,时间窗口越窄。115200的时候,只有0.3ms的回复窗口。这对MCU的中断响应速度是个考验。
多机通信的时序,我总结成三步:
- 主机发送帧:主机发完最后一个字节后,启动超时定时器
- 从机处理帧:从机收到完整帧后,校验地址和CRC,然后准备回复
- 从机回复帧:从机必须在3.5字符时间内开始发送回复
我曾经在一个项目里遇到过这样的问题:主机发了广播帧(地址0x00),结果所有从机都回复了,总线直接乱掉。后来查手册才发现,广播帧从机不需要回复。嗯,这个坑我记了很久。
多机通信的避坑指南:
- 地址冲突:两个从机设成同一个地址,主机一问,两个同时回,数据全乱。解决办法:出厂时每个从机烧录唯一ID。
- 回复超时:从机处理时间太长,超过3.5字符时间。解决办法:把CRC校验和数据处理放到主循环,中断里只收数据。
- 帧间隔:主机发完一帧后,至少要等3.5字符时间再发下一帧。否则从机可能还没处理完上一帧。
好了,这一章的内容就这些。CRC校验、波特率自适应、多机通信时序,这三个东西你掌握了,Modbus RTU的实战能力就上了一个台阶。下一章我们会讲Modbus RTU的异常处理与调试技巧,到时候再聊。