4、CRC循环冗余校验:CRC原理、多项式选择、硬件实现

各位同学,咱们今天聊聊CRC。说实话,在存储芯片里,CRC是我最常用的数据完整性保护手段之一。它不像ECC那样能纠错,但胜在硬件开销小、检错能力强。我在做NAND Flash控制器的时候,几乎每个项目都会用到它。

4.1 CRC的基本原理

CRC的原理,说白了就是做多项式除法。你想想看,我们把要保护的数据看作一个巨大的二进制数,然后除以一个事先约定好的多项式,得到的余数就是CRC校验码。

具体来说,发送端计算CRC并附加在数据后面。接收端用同样的多项式对数据+CRC做除法。如果余数为0,说明数据没毛病;不为0,那肯定出错了。

我习惯用这个公式来理解:

M(x) * x^n = Q(x) * G(x) + R(x)

其中:

  • M(x) 是原始数据多项式
  • G(x) 是生成多项式(我们选的)
  • R(x) 就是CRC校验码
  • n 是CRC的位数

嗯,这里要注意:CRC计算用的是模2除法,也就是不带进位的异或运算。这和普通除法不一样,千万别搞混了。

核心要点:CRC的本质是多项式除法,用异或运算实现。检错能力取决于生成多项式的选择。

4.2 多项式选择——这个坑我踩过

多项式选得好不好,直接决定了CRC的检错能力。我曾经在一个项目里随便选了个多项式,结果发现某些特定错误模式根本检不出来...后来老老实实查了标准。

常用的CRC多项式有这些:

名称 多项式(十六进制) 位数 典型应用
CRC-8 0x07 8 1-Wire总线
CRC-16-IBM 0x8005 16 Modbus协议
CRC-32 0x04C11DB7 32 以太网、ZIP文件
CRC-64-ECMA 0x42F0E1EBA9EA3693 64 大容量存储

选多项式时,我一般遵循这几个原则:

  • 检错能力优先:能检测所有奇数个错误、所有双比特错误、所有突发错误(长度 ≤ CRC位数)
  • 硬件实现简单:多项式中1的个数越少,硬件越省
  • 尽量用标准多项式:别自己发明,标准多项式都是经过数学验证的

我的经验:存储芯片里,如果数据块是512字节,CRC-16就够用了。如果是4KB的大块,我建议用CRC-32。别为了省那十几个门电路去选太短的多项式,得不偿失。

4.3 硬件实现——这才是重头戏

软件算CRC太慢了,存储芯片里必须用硬件。硬件实现CRC,核心就是线性反馈移位寄存器(LFSR)。

我画个简化的结构图给你看:

数据输入 → [异或门] → [D] → [D] → [D] → ... → [D] → CRC输出
                ↑        ↑     ↑           ↑
                └────────┴─────┴───────────┘
                 反馈路径由多项式决定

每个D就是一个触发器,异或门的位置由多项式决定。比如CRC-8(0x07),多项式是 x^8 + x^2 + x + 1,那就在第2位和第1位的位置加异或门。

实际工程中,我常用两种实现方式:

4.3.1 串行CRC实现

这种最简单,一个比特一个比特地算。适合低速场景。

module crc8_serial (
    input  clk,
    input  rst_n,
    input  data_in,
    input  data_valid,
    output [7:0] crc_out
);
    reg [7:0] lfsr;
    wire feedback = data_in ^ lfsr[7];
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            lfsr <= 8'h00;
        else if (data_valid) begin
            lfsr[0] <= feedback;
            lfsr[1] <= lfsr[0] ^ feedback;  // 多项式 x^1
            lfsr[2] <= lfsr[1] ^ feedback;  // 多项式 x^2
            lfsr[3] <= lfsr[2];
            lfsr[4] <= lfsr[3];
            lfsr[5] <= lfsr[4];
            lfsr[6] <= lfsr[5];
            lfsr[7] <= lfsr[6];
        end
    end
    
    assign crc_out = lfsr;
endmodule

4.3.2 并行CRC实现

串行太慢了,一个时钟只能算1比特。在DDR接口这种高速场景,必须用并行CRC。一次算8比特、16比特甚至32比特。

并行CRC的推导有点复杂,但说白了就是展开串行逻辑。我直接给你个8比特并行的例子:

// CRC-8 并行计算,一次处理8比特
// 多项式: x^8 + x^2 + x + 1 (0x07)
function [7:0] crc8_parallel;
    input [7:0] data;
    input [7:0] crc_in;
    reg [7:0] d, c;
    begin
        d = data;
        c = crc_in;
        
        crc8_parallel[0] = d[7] ^ d[6] ^ d[0] ^ c[0] ^ c[6] ^ c[7];
        crc8_parallel[1] = d[7] ^ d[1] ^ d[0] ^ c[0] ^ c[1] ^ c[7];
        crc8_parallel[2] = d[7] ^ d[2] ^ d[1] ^ d[0] ^ c[0] ^ c[1] ^ c[2] ^ c[7];
        crc8_parallel[3] = d[3] ^ d[2] ^ d[1] ^ c[1] ^ c[2] ^ c[3];
        crc8_parallel[4] = d[4] ^ d[3] ^ d[2] ^ c[2] ^ c[3] ^ c[4];
        crc8_parallel[5] = d[5] ^ d[4] ^ d[3] ^ c[3] ^ c[4] ^ c[5];
        crc8_parallel[6] = d[6] ^ d[5] ^ d[4] ^ c[4] ^ c[5] ^ c[6];
        crc8_parallel[7] = d[7] ^ d[6] ^ d[5] ^ c[5] ^ c[6] ^ c[7];
    end
endfunction

注意:并行CRC的表达式可以用工具自动生成,别手算。我曾经手算32位并行CRC,算到一半就放弃了...现在有现成的脚本和工具,比如pycrc,直接生成Verilog代码,省心多了。

4.4 实际应用中的几个坑

做了这么多年存储芯片,我总结了几条CRC的实战经验:

  • 初始值别搞错:有些标准CRC初始值是全0,有些是全1。比如CRC-32初始值是0xFFFFFFFF,算完还要取反。这个细节搞错了,整个校验就废了。
  • 字节序要注意:大端小端会影响CRC结果。我建议在项目一开始就定好规范,不然联调的时候会疯掉。
  • CRC不能替代ECC:CRC只能检错,不能纠错。在NAND Flash这种会出比特翻转的场景,必须配合ECC使用。我一般用CRC做快速检错,ECC做纠错。

一个小技巧:如果你不确定CRC实现是否正确,可以用在线CRC计算器验证。输入同样的数据和多项式,对比结果。我每次流片前都会做这个验证,确保硬件和软件算出来的一致。

好了,CRC的内容就讲到这里。说白了,CRC就是个多项式除法,硬件实现就是LFSR。选对多项式,算对初始值,基本就不会出问题。下一节咱们聊聊ECC纠错码,那个比CRC复杂多了,但也是存储芯片里最核心的技术之一。