CRC 校验的数学本质

CRC(循环冗余校验)本质上是将数据视为二进制多项式,用约定的生成多项式做模2除法,余数即为校验值。例如 Modbus CRC16 使用多项式 x^16 + x^15 + x^2 + 1,即 0x8005(高位省略),实际计算时通过左移或右移方式处理。

Modbus CRC16 的算法参数

  • 多项式:0x8005(反转后为0xA001)
  • 初始值:0xFFFF
  • 输入/输出反转:均反转(即处理时低位在前)
  • 结果异或:0x0000

以上参数决定了 Modbus 在 RTU 模式下的标准 CRC16。

两种实现方式:直接计算 vs 查表

直接计算适合理解原理,但逐位循环效率低。查表法用空间换时间,适合实时通信。

直接计算示例(右移版)

uint16_t crc16_modbus_direct(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    while (len--) {
        crc ^= *data++;
        for (int i = 0; i < 8; i++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

这里使用反转多项式 0xA001,与直接使用0x8005左移算法等价,但更贴近单片机小端处理。

查表法实现

生成查找表(在PC或初始化时调用)

uint16_t crc16_table[256];

void crc16_init(void) {
    for (int i = 0; i < 256; i++) {
        uint16_t crc = i;
        for (int j = 0; j < 8; j++) {
            if (crc & 1)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
        crc16_table[i] = crc;
    }
}

查表计算函数

uint16_t crc16_modbus_table(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    while (len--) {
        uint8_t idx = (crc ^ *data++) & 0xFF;
        crc = (crc >> 8) ^ crc16_table[idx];
    }
    return crc;
}

注意:查表函数与生成表的移位方向必须一致。这里表内保存的是单字节参与计算后的结果,因此直接异或高字节即可。

实用例程:Modbus 报文生成与校验

Modbus RTU 帧末尾附加两个字节 CRC,先低字节后高字节。以下是一个完整示例:

#include <stdint.h>

uint16_t crc16_modbus(uint8_t *data, uint16_t len); // 上面任意实现

void build_frame(uint8_t addr, uint8_t func, uint8_t *payload, uint8_t plen, uint8_t *frame) {
    uint8_t len = 2 + plen; // 地址、功能码 + 数据
    frame[0] = addr;
    frame[1] = func;
    for (int i = 0; i < plen; i++) {
        frame[2 + i] = payload[i];
    }
    uint16_t crc = crc16_modbus(frame, len);
    frame[len] = crc & 0xFF;       // 低字节
    frame[len + 1] = crc >> 8;     // 高字节
}

在接收端,对整帧(含CRC)做一次CRC计算,结果为0则校验通过,这是一种常见做法。

配置步骤(移植到新MCU)

  1. 定义索引表类型,确认 uint16_t 至少16位。
  2. 在初始化阶段调用 crc16_init() 生成表。
  3. crc16_modbus_table 封装到协议层。
  4. 发送时计算CRC并按“低前高后”填充。
  5. 接收时对包括CRC的整帧进行校验,结果应为0。

如果没有特殊要求,可跳过生成表,直接将预生成的静态表存入代码区,节省启动时间。

注意事项

  • 字节序:Modbus 规定 CRC 低字节在前,发送顺序不可颠倒。
  • 多字节数据:数据流按实际字节顺序输入,不要对16位数做字节序转换。
  • 查表法生成的表是全局的,若在中断中使用,需保证初始化完成。
  • 某些单片机可用硬件CRC外设,但参数需与Modbus匹配(如多项式、初值、输入/输出反转),否则结果不一致。

总结

CRC 是协议层的“安全带”,Modbus CRC16 通过标准参数和查表优化,可在任何单片机上高效实现。掌握其数学原理和实现细节,能帮助你更好地调试通信故障。