CRC(Cyclic Redundancy Check,循环冗余校验)是错误检测技术。

CRC校验的基本原理

CRC基于多项式除法的数学原理。它将数据视为一个大的二进制数,对应一个多项式,然后用一个预定的生成多项式进行模2除法运算,得到的余数就是CRC校验码。

核心概念

模2运算:在CRC中使用的是模2运算,其特点是:

  • 加法和减法都等同于异或(XOR)运算
  • 不考虑进位和借位
  • 1 + 1 = 0, 1 + 0 = 1, 0 + 0 = 0

生成多项式:这是CRC算法的关键,不同的CRC标准使用不同的生成多项式。例如:

  • CRC-16: x^16 + x^15 + x^2 + 1 (二进制:11000000000000101)
  • CRC-32: x^32 + x^26 + x^23 + … + x^2 + x + 1

CRC计算过程

发送端计算步骤

  1. 准备数据:假设原始数据为M(x),生成多项式为G(x),G(x)的最高次幂为r

  2. 左移数据:将M(x)左移r位,相当于M(x) × 2^r,在数据末尾添加r个0

  3. 模2除法:用左移后的数据除以生成多项式G(x),得到余数R(x)

  4. 附加校验码:将余数R(x)附加到原始数据M(x)后面,形成要传输的数据

接收端验证步骤

  1. 接收数据:接收包含原始数据和CRC校验码的完整数据

  2. 执行除法:用相同的生成多项式G(x)对接收到的数据进行模2除法

  3. 检查余数:如果余数为0,说明数据传输正确;否则说明出现错误

具体计算示例

让我通过一个简单的例子来演示CRC计算:

假设:

  • 原始数据:1101(对应多项式 x^3 + x^2 + 1)
  • 生成多项式:1011(对应 x^3 + x + 1),r = 3

计算过程

  1. 数据左移3位:1101000

  2. 进行模2除法:

        1110 (商,我们不需要)
      -------
1011 | 1101000
       1011
       ----
        1100
        1011
        ----
         1110
         1011
         ----
          1010
          1011
          ----
           001 (余数)
  1. CRC校验码为:001

  2. 最终传输的数据:1101001

常见CRC标准

不同的应用场景使用不同的CRC标准:

  • CRC-8:用于短消息
  • CRC-16:用于Modbus、USB等
  • CRC-32:用于以太网、ZIP文件等
  • CRC-64:用于更高可靠性要求的场合

Modbus CRC-16 特点

Modbus使用的是CRC-16/MODBUS标准,具有以下特征:

  • 生成多项式:0x8005(二进制:1000000000000101,对应 x^16 + x^15 + x^2 + 1)
  • 初始值:0xFFFF
  • 结果异或值:0x0000
  • 输入/输出反转:是
  • 校验值用于验证:"123456789"的CRC-16/MODBUS结果是0x4B37

实际Modbus消息示例

假设我们有一个Modbus RTU读取保持寄存器的请求:

从机地址:01
功能码:  03(读保持寄存器)
起始地址:0000
寄存器数:0002

完整消息(不含CRC):01 03 00 00 00 02

Modbus CRC-16 计算步骤

方法1:逐字节计算法(易理解)

def calculate_modbus_crc(data):
    # 初始化CRC为0xFFFF
    crc = 0xFFFF
    
    # 对每个字节进行处理
    for byte in data:
        # CRC与当前字节异或
        crc ^= byte
        
        # 对该字节的8个位进行处理
        for _ in range(8):
            # 检查最低位
            if crc & 0x0001:
                # 如果最低位是1,右移并与多项式异或
                crc = (crc >> 1) ^ 0xA001  # 0xA001是0x8005的反转
            else:
                # 如果最低位是0,只右移
                crc = crc >> 1
    
    return crc

# 计算示例消息的CRC
data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x02]
crc_result = calculate_modbus_crc(data)
print(f"CRC-16: {crc_result:04X}")  # 结果:C40B

详细计算过程演示

让我手工演示前两个字节的计算:

处理第1个字节(0x01):

初始CRC = 0xFFFF (1111111111111111)
字节值  = 0x01   (0000000000000001)

CRC ^= 0x01 → 0xFFFE (1111111111111110)

8次位处理:
第1次:最低位=0,右移 → 0x7FFF
第2次:最低位=1,右移并异或0xA001 → 0xBFFF ^ 0xA001 = 0x1FFE
第3次:最低位=0,右移 → 0x0FFF
第4次:最低位=1,右移并异或0xA001 → 0x87FF ^ 0xA001 = 0x27FE
第5次:最低位=0,右移 → 0x13FF
第6次:最低位=1,右移并异或0xA001 → 0x89FF ^ 0xA001 = 0x29FE
第7次:最低位=0,右移 → 0x14FF
第8次:最低位=1,右移并异或0xA001 → 0x8A7F ^ 0xA001 = 0x2A7E

处理完0x01后,CRC = 0x2A7E

处理第2个字节(0x03):

当前CRC = 0x2A7E
字节值  = 0x03

CRC ^= 0x03 → 0x2A7D

继续8次位处理...
最终得到新的CRC值

方法2:查表法(实际应用)

实际应用中,为了提高效率,通常使用预计算的CRC表:

# CRC-16/MODBUS查找表(部分)
CRC_TABLE = [
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    # ... 完整表有256个值
]

def calculate_modbus_crc_table(data):
    crc = 0xFFFF
    
    for byte in data:
        # 取CRC低8位与数据字节异或,作为表索引
        index = (crc ^ byte) & 0xFF
        # CRC右移8位,与表值异或
        crc = (crc >> 8) ^ CRC_TABLE[index]
    
    return crc

完整的Modbus RTU帧

对于我们的示例消息:

原始数据:01 03 00 00 00 02
计算得CRC:0x0BC4(低字节在前)
完整帧:01 03 00 00 00 02 C4 0B

注意:Modbus发送CRC时,低字节在前,高字节在后

接收端验证

接收端收到完整帧后有两种验证方法:

方法1:重新计算CRC

received_frame = [0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B]
# 对除了最后两个CRC字节外的数据计算CRC
data = received_frame[:-2]
calculated_crc = calculate_modbus_crc(data)
received_crc = (received_frame[-1] << 8) | received_frame[-2]

if calculated_crc == received_crc:
    print("数据正确")
else:
    print("数据错误")

方法2:对整个帧计算CRC

# 对包含CRC的整个帧计算,正确时结果应为0
result = calculate_modbus_crc(received_frame)
if result == 0:
    print("数据正确")

实际应用建议

  1. 使用查表法:预计算的CRC表大大提高计算速度
  2. 注意字节序:Modbus CRC是小端序(低字节在前)
  3. 硬件支持:许多微控制器有硬件CRC模块,可以进一步加速
  4. 错误处理:CRC错误时,通常请求重发而不是尝试纠错

为什么0xA001?

你可能注意到代码中用的是0xA001而不是0x8005:

  • 0x8005 = 1000 0000 0000 0101(正常表示)
  • 0xA001 = 1010 0000 0000 0001(位反转后)

Modbus使用位反转算法(LSB first),所以实际运算时用反转后的多项式值。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐