CRC计算与实现:软件查表法、硬件LFSR实现、位序与初始值
说实话,CRC 这块儿光讲原理,你可能觉得挺简单的——不就是多项式除法嘛。但真正落地的时候,软件怎么算快、硬件怎么省资源、位序怎么排、初始值填什么……这些坑我当年都踩过。今天咱们就把这四件事儿一次说透。
4.1 软件查表法:拿空间换时间
CRC 的纯软件计算,如果一位一位地算,那效率太低了。尤其数据量大时,你想想看,每 byte 要跑 8 个循环,CPU 全浪费在移位和异或上了。
我个人的习惯是, 只要不是在极端受限的 MCU 上,一定用查表法。核心思路很简单:提前算好所有可能的 8 位输入对应的 CRC 部分结果,存成一张 256 个条目的表。计算时每取一个字节,直接从表里查,再跟寄存器做异或和移位。这样每个字节只需要几条指令。
4.1.1 查表法的表生成
表怎么来的?其实就是对着多项式,对 0x00 ~ 0xFF 每个值,跑一遍标准的 CRC 计算。以 CRC-16/MODBUS 为例,多项式 0x8005,初始值 0xFFFF。我写个 C 代码给你看:
// 生成 CRC-16 MODBUS 查表
uint16_t crc16_table[256];
void crc16_init_table(void) {
for (int i = 0; i < 256; i++) {
uint16_t crc = i << 8;
for (int bit = 0; bit < 8; bit++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x8005;
else
crc <<= 1;
}
crc16_table[i] = crc;
}
}
你看,生成表只需要跑一次,之后查表就快了。嗯,这里要注意:表的大小和最终 CRC 宽度对应,8 位 CRC 用 256 条 8 位条目,16 位用 256 条 16 位,32 位同理。
4.1.2 快速查表计算
查表计算时,标准做法是:把当前 CRC 高 8 位与输入字节异或,作为索引查表,然后 CRC 左移 8 位再异或表值。
uint16_t crc16_fast(uint8_t *data, int len) {
uint16_t crc = 0xFFFF; // 初始值
for (int i = 0; i < len; i++) {
uint8_t index = (crc >> 8) ^ data[i];
crc = (crc << 8) ^ crc16_table[index];
}
return crc;
}
我在项目中遇到过 一个坑:有人直接把表生成的是反射版本(reflected),但查表的时候没有按反射的位序处理,结果算出来的 CRC 永远不对。所以查表之前,一定要搞清楚你的多项式位序是正常还是反射。
4.2 硬件 LFSR 实现:从移位寄存器角度看 CRC
硬件实现 CRC,说白了就是一个线性反馈移位寄存器(LFSR)。你想想看,多项式除法在硬件里根本不需要做除法,只要把异或门放在合适的位置就行。
比如实现 CRC-8-ATM(多项式 x⁸ + x² + x + 1,即 0x07),8 级移位寄存器,数据从低位或高位进去,每一步移一位,遇到反馈位就异或。标准电路长这样:
- 寄存器初值赋为初始值(比如 0x00 或 0xFF)。
- 每个时钟周期,输入 1 bit,所有寄存器右移一位。
- 移出的那一位如果是 1,则在反馈路径上异或多项式(除去最高位)。
- 全部数据移完后,寄存器的值就是 CRC(可能还要异或最终异或值)。
我记得有一次 调试一个 FPGA 项目,代码仿真 CRC 全对,上板就不对。最后发现是 LFSR 的复位信号没处理好,初始值根本没加载进去。所以硬件设计时,初始值加载一定要和有效数据时序对齐。
4.2.1 LFSR 的 Verilog 示例
// CRC-8-ATM LFSR 实现
module crc8_atm (
input clk,
input rst_n,
input data_in,
input data_valid,
output reg [7:0] crc_out
);
reg [7:0] lfsr;
wire feedback = lfsr[7] ^ data_in; // 数据从高位异或进入
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
lfsr <= 8'hFF; // 初始值 0xFF
else if (data_valid) begin
lfsr[0] <= feedback;
lfsr[1] <= lfsr[0] ^ feedback; // 多项式 x^1 位置
lfsr[2] <= lfsr[1] ^ feedback; // 多项式 x^2 位置
lfsr[7:3] <= lfsr[6:2]; // 其余直接移位
end
end
assign crc_out = lfsr;
endmodule
注意上面代码里,多项式 bit1 和 bit2 位置加了个额外的异或门。如果你用多项式 0x07(二进制 00000111),实际反馈位是 bit0、bit1、bit2,但因为 CRC-8 最高位隐含了 x⁸,所以反馈只在低 8 位中 bit2 和 bit1 处异或(bit0 是移出位自动丢弃后的反馈输入)。
4.3 位序与初始值:两个最容易翻车的细节
CRC 计算里,位序(bit ordering)和初始值(init value)看起来是小问题,但出问题概率极高。我教你一个排查方法:手动算一个短数据的 CRC,用在线计算器或 Python 脚本验证。
4.3.1 位序:MSB first 与 LSB first
大多数 CRC 标准用 MSB first(高位先送),比如 CRC-32 以太网。但 Modbus 和某些串行协议用 LSB first(低位先送)。位序不同,查表时索引的生成方式也要反过来。
| 标准 | 位序 | 字节内比特顺序 | 表索引计算 |
|---|---|---|---|
| CRC-32 以太网 | MSB first (non-reflected) | bit7 → bit0 | ((crc>>24) ^ data[i]) & 0xFF |
| CRC-16 Modbus | LSB first (reflected) | bit0 → bit7 | (crc ^ data[i]) & 0xFF |
| CRC-8 Dallas | LSB first | bit0 → bit7 | (crc ^ data[i]) & 0xFF |
我曾经 在一个串口 Bootloader 项目中,CRC 校验一直失败。排查了三天,最后发现是位序反了——我的软件用 MSB first 查表,但硬件计算器是 LSB first。从那以后,我每写一个 CRC 函数,第一件事就是打印出第一个字节的 CRC 中间值,跟已知标准值对比。
4.3.2 初始值:不只是全0或全1
CRC 的初始值决定了计算起点。常见的有 0x0000、0xFFFF、0xFFFFFFFF 等。但有些协议会在 CRC 计算后还需要异或一个最终异或值(XOROUT),比如 CRC-32 以太网的 XOROUT 是 0xFFFFFFFF。
下表列出常见 CRC 参数:
| CRC 标准 | 宽度 | 多项式 | 初始值 | 位序 | XOROUT |
|---|---|---|---|---|---|
| CRC-8/MAXIM | 8 | 0x31 | 0x00 | MSB first | 0x00 |
| CRC-16/MODBUS | 16 | 0x8005 | 0xFFFF | LSB first (reflected) | 0x0000 |
| CRC-16/CCITT | 16 | 0x1021 | 0xFFFF | MSB first | 0x0000 |
| CRC-32 以太网 | 32 | 0x04C11DB7 | 0xFFFFFFFF | LSB first (reflected) | 0xFFFFFFFF |
你可能会问: 初始值为什么有时候是 0xFFFF?其实就是为了避免前导零影响 CRC 结果。如果初始值是全 0,那么多项式除法会把前导零当作有效位,导致前面加多少零结果都一样。所以大部分协议把初始值设成全 1,或者用其他方法「加扰」一下。
4.4 本章知识体系一览
为了帮你理清思路,我画了一张图,把软件查表法、硬件 LFSR、位序、初始值这四个环节串起来。看完这张图,你应该能清晰看到每个环节的输入输出和注意事项。
4.5 实战中的避坑清单
最后,结合我的实战经验,列一个 checklist。你每次写 CRC 相关代码时,照着过一遍,能省不少调试时间。
- 多项式对不对? 看标准文档,不要从别人代码里复制一份就完事——多项式笔误率很高。
- 初始值跟标准一致吗? 包括 XOROUT,很多版本的 CRC 参数表把 XOROUT 单独列出来,别忘了最后异或。
- 位序: 硬件 LFSR 是 MSB first 还是 LSB first?软件查表时索引是不是已经做了反射?我建议生成一个单字节 0x01 的 CRC 结果,用在线计算器验证。
- 数据长度: 有些协议在数据后追加 CRC 再送,有的不追加。接收方计算时要不要包含收到的 CRC 值?
- 多字节顺序: CRC 结果是大端存放还是小端?Modbus 的 CRC 是低字节在前,以太网 CRC 是高字节在前。
- 边界情况: 输入为空时,CRC 应该等于初始值 XOROUT 之后的值,而不是 0。很多人的 bug 就出在这儿。
嗯,CRC 的计算实现其实就这些内容。你只要把位序和初始值这两个变量控制住,软件和硬件完全可以等价替换。下节课咱们聊聊怎么在实际设备中用 MAC 计数器定位 CRC 错误——那才是真正的调试利器。