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;
}
我的经验: 计算法适合在PC端调试时用,或者MCU主频很高(比如100MHz以上)的场景。如果主频只有8MHz、16MHz,建议别用计算法,否则通信一快,CPU就卡死了。

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;
}
关键点: 查表法虽然快,但表要占用512字节的ROM。对于1KB以内ROM的MCU,可能有点紧张。不过现在主流MCU至少都有8KB、16KB,512字节完全不是问题。

4.2 波特率自适应策略

波特率自适应,这是个“看起来简单,做起来坑多”的东西。你想想看,一个逆变器装到现场,上位机可能用9600,也可能用19200,甚至115200。总不能每次都要人去配置吧?

我见过最笨的办法:让用户拨码开关。嗯,十年前确实有人这么干。现在嘛,基本都是自动检测了。

波特率自适应的核心思路:测量两个下降沿之间的时间。Modbus RTU的起始位是低电平,停止位是高电平。一个字节的传输,起始位+8数据位+停止位,总共10位(无校验时)。

具体做法:

  1. 等待总线空闲(高电平持续至少3.5个字符时间)
  2. 检测到下降沿(起始位开始)
  3. 启动定时器,测量到下一个下降沿的时间
  4. 这个时间就是1个字符(10位)的传输时间
  5. 用这个时间反推出波特率
避坑指南: 我曾经在项目里直接用第一个下降沿到第二个下降沿的时间来算波特率。结果发现,如果第一个字节是0x01(二进制00000001),第二个下降沿来得特别快,算出来的波特率偏大。后来我改成测量连续3个下降沿的平均值,才稳定下来。

代码实现思路:

// 波特率自适应伪代码
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
}
我的建议: 检测到波特率后,最好再发一个回环测试帧确认一下。比如发一个0x55(二进制01010101),看看回波对不对。如果不对,重新检测。这样可以避免误判。

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的中断响应速度是个考验。

多机通信的时序,我总结成三步:

  1. 主机发送帧:主机发完最后一个字节后,启动超时定时器
  2. 从机处理帧:从机收到完整帧后,校验地址和CRC,然后准备回复
  3. 从机回复帧:从机必须在3.5字符时间内开始发送回复
关键点: 从机回复时,要确保总线空闲。如果两个从机同时回复,就会冲突。所以Modbus RTU是“一问一答”模式,主机不问,从机绝不说话。

我曾经在一个项目里遇到过这样的问题:主机发了广播帧(地址0x00),结果所有从机都回复了,总线直接乱掉。后来查手册才发现,广播帧从机不需要回复。嗯,这个坑我记了很久。

多机通信的避坑指南:

  • 地址冲突:两个从机设成同一个地址,主机一问,两个同时回,数据全乱。解决办法:出厂时每个从机烧录唯一ID。
  • 回复超时:从机处理时间太长,超过3.5字符时间。解决办法:把CRC校验和数据处理放到主循环,中断里只收数据。
  • 帧间隔:主机发完一帧后,至少要等3.5字符时间再发下一帧。否则从机可能还没处理完上一帧。
我的经验: 实际项目中,我一般把超时时间设成5个字符时间,留点余量。尤其是用无线模块时,延迟更大。但注意,不能设太大,否则通信效率会下降。

好了,这一章的内容就这些。CRC校验、波特率自适应、多机通信时序,这三个东西你掌握了,Modbus RTU的实战能力就上了一个台阶。下一章我们会讲Modbus RTU的异常处理与调试技巧,到时候再聊。