什么是 CRC16?
CRC (Cyclic Redundancy Check),即循环冗余校验,是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后是否出现错误。
CRC16 是指使用 16 位(即 2 个字节)的校验码,它具有以下特点:
- 检错能力强:可以检测出几乎所有单比特和双比特的错误,以及奇数位错误。
- 实现简单:使用位运算即可实现,计算速度快。
- 应用广泛:常用于通信协议(如 Modbus、CAN、USB)、数据存储(如磁盘、光盘)等领域。
CRC16 的核心原理
CRC 的本质是多项式除法在模2运算下的体现。
-
多项式表示:一个 n 位的二进制数可以看作一个 n-1 次的多项式,二进制数
1101对应多项式x³ + x² + 1,CRC16 算法中使用的多项式被称为“生成多项式”(Generator Polynomial),标准有很多种,最常见的是CRC-16-IBM(或CRC-16-MODBUS),其多项式为x¹⁶ + x¹⁵ + x² + 1,对应的二进制值为0xA001。 -
计算过程: a. 在原始数据(信息码)的末尾附加 n 个 0(对于 CRC16,n=16)。 b. 将这个新的二进制数除以生成多项式(使用模2除法,即不借位,只做异或运算)。 c. 所得的余数(比生成多项式少一位)CRC 校验码。
-
验证过程: a. 将原始数据和计算出的 CRC 校验码拼接在一起。 b. 将这个完整的二进制数除以同一个生成多项式。 c. 如果数据传输无误,最终的余数应该是 0。
C 语言实现
在 C 语言中,我们不会真的去做二进制长除法,而是通过高效的位运算来模拟这个过程,主要有两种方法:直接计算法和查表法。
关键参数
在实现之前,必须明确以下几个参数,不同参数会导致不同的 CRC 结果:
- 多项式:
0xA001(对于 CRC-16-IBM/MODBUS)。 - 初始值:通常是
0xFFFF。 - 结果异或值:通常是
0x0000。 - 输入数据是否反转:有些算法在计算前会反转每个字节的位序。
下面我们以最常见的 CRC-16-MODBUS 参数为例进行实现。
直接计算法 (Direct Computation)
这种方法直观地模拟了除法的过程,代码易读,但计算速度相对较慢。
算法步骤:
- 初始化一个 16 位的累加器
crc,通常为0xFFFF。 - 遍历数据的每一个字节。
- 将累加器的低 8 位与当前数据字节进行异或。
- 对累加器的 16 位值循环 8 次:
a. 检查累加器的最低位(LSB)。
b. LSB 是 1,则将累加器右移一位,然后与多项式
0xA001进行异或。 c. LSB 是 0,则仅将累加器右移一位。 - 处理完所有字节后,累加器中的值就是 CRC 校验码。
C 语言代码实现:
#include <stdio.h>
#include <stdint.h> // 使用 uint16_t 确保是16位无符号整数
// CRC-16/MODBUS 多项式
#define CRC16_POLYNOMIAL 0xA001
#define CRC16_INIT 0xFFFF
uint16_t crc16_direct(const uint8_t *data, size_t length) {
uint16_t crc = CRC16_INIT;
size_t i, j;
for (i = 0; i < length; i++) {
// 将当前字节与CRC值的低8位异或
crc ^= (uint16_t)data[i];
// 循环处理8位
for (j = 0; j < 8; j++) {
// 如果最低位是1
if (crc & 0x0001) {
// 右移一位,然后与多项式异或
crc = (crc >> 1) ^ CRC16_POLYNOMIAL;
} else {
// 否则只右移一位
crc = (crc >> 1);
}
}
}
return crc;
}
// 测试代码
int main() {
const uint8_t data[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A};
// 这是一个 Modbus-RTU 请求帧,其 CRC16 校验码应为 0x84 0x0A
// 注意:多出来的 0x84 0x0A 是我们用来验证的
size_t data_len = sizeof(data) / sizeof(data[0]);
// 计算数据(不含末尾的CRC)的校验码
uint16_t calculated_crc = crc16_direct(data, data_len - 2);
printf("数据: ");
for(size_t i = 0; i < data_len; i++) {
printf("%02X ", data[i]);
}
printf("\n");
printf("计算出的 CRC16: %04X\n", calculated_crc);
// 比较计算出的 CRC 和数据中自带的 CRC
// 注意字节序 (Endianness)
uint16_t received_crc = (data[data_len - 1] << 8) | data[data_len - 2];
if (calculated_crc == received_crc) {
printf("校验成功!\n");
} else {
printf("校验失败!\n");
}
return 0;
}
查表法 (Lookup Table)
这是在实际应用中最常用、性能最高的方法,它通过空间换时间,将计算过程中的中间结果预先计算并存储在一个查找表中。
算法步骤:
- 生成 CRC 表:在程序开始时(或编译时),使用直接计算法计算一个包含 256 个条目的表,每个条目代表一个 8 位数据字节(0x00 到 0xFF)经过一轮 CRC 计算后的结果。
- 使用 CRC 表进行计算:
a. 初始化
crc为0xFFFF。 b. 遍历数据的每一个字节byte。 c.crc的低 8 位与byte异或,得到一个索引index。 d. 用crc的高 8 位与查表表中index位置的值进行异或,结果作为新的crc。 e. 将crc右移 8 位。 f. 重复 b-e 直到所有字节处理完毕。
C 语言代码实现:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h> // 用于 exit()
// CRC-16/MODBUS 参数
#define CRC16_POLYNOMIAL 0xA001
#define CRC16_INIT 0xFFFF
// 全局或静态的 CRC 查找表
static uint16_t crc16_table[256];
// 生成 CRC 表的函数
void generate_crc16_table() {
uint16_t value;
uint16_t temp;
for (uint16_t i = 0; i < 256; i++) {
value = 0;
temp = i;
for (uint16_t j = 0; j < 8; j++) {
if ((value ^ temp) & 0x0001) {
value = (value >> 1) ^ CRC16_POLYNOMIAL;
} else {
value >>= 1;
}
temp >>= 1;
}
crc16_table[i] = value;
}
}
// 使用查表法计算 CRC16
uint16_t crc16_lookup(const uint8_t *data, size_t length) {
uint16_t crc = CRC16_INIT;
size_t i, j;
for (i = 0; i < length; i++) {
// 1. 取crc的低8位与当前数据字节异或,作为查表索引
j = (crc ^ data[i]) & 0xFF;
// 2. 将crc右移8位,然后与查表结果异或
crc = (crc >> 8) ^ crc16_table[j];
}
return crc;
}
// 测试代码
int main() {
// 程序启动时先生成表
generate_crc16_table();
printf("CRC 查找表已生成,\n");
const uint8_t data[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01};
size_t data_len = sizeof(data) / sizeof(data[0]);
uint16_t calculated_crc = crc16_lookup(data, data_len);
printf("数据: ");
for(size_t i = 0; i < data_len; i++) {
printf("%02X ", data[i]);
}
printf("\n");
printf("计算出的 CRC16: %04X\n", calculated_crc);
// Modbus 协议中,CRC 低位在前,高位在后
// 0xBB 0x1D 存储在数据中是 0x1D 0xBB
uint16_t expected_crc = 0xBB1D; // 预期的校验码
if (calculated_crc == expected_crc) {
printf("校验成功!\n");
} else {
printf("校验失败!\n");
}
return 0;
}
总结与对比
| 特性 | 直接计算法 | 查表法 |
|---|---|---|
| 速度 | 较慢,每个字节需要循环8次 | 非常快,每个字节只需几次位运算和一次查表 |
| 内存占用 | 极低,无需额外内存 | 较高,需要一个256条目的表(通常512字节) |
| 代码复杂度 | 简单,逻辑直观 | 稍复杂,需要预先生成表 |
| 适用场景 | 对内存极度敏感、对速度要求不高的场景 | 绝大多数应用场景,特别是通信、数据校验等需要高性能计算的地方 |
推荐:在绝大多数情况下,强烈推荐使用查表法,其带来的性能提升非常显著,而 512 字节的内存开销在现代系统中完全可以忽略不计。
其他 CRC16 变种
CRC16 并不是唯一的,不同标准有不同的参数,如果你需要实现其他 CRC16 算法,只需修改以下参数即可:
- CRC-16-CCITT (0x1021): 多项式为
0x1021,初始值为0xFFFF。 - CRC-16-CCITT-FALSE (0x1021): 多项式为
0x1021,初始值为0x0000。
实现逻辑完全相同,只需替换 CRC16_POLYNOMIAL 和 CRC16_INIT 的宏定义即可。
