FPGA实现CRC-8/SMBUS:从参数和位序核对到1041次自检
CRC对不上时,先把实际送入计算器的字节写下来。"123456789" 是9个ASCII字节,01 02 03 04 05 06 07 08 09 是另一组数据,再加一个字符串结尾的 00 又会变成第三组消息。
本文实现一个每拍接收一个字节的CRC-8/SMBUS计算核。先固定参数,再把位递推、清零优先级、逐字节参考值和故意改错的测试接起来。完整RTL和testbench都放在文中,保存为两个文件即可运行。
1. CRC-8必须写清楚哪些参数
本文采用以下参数:
| 参数 | 取值 |
|---|---|
| Width | 8 |
| Poly | 0x07 |
| Init | 0x00 |
| RefIn | false |
| RefOut | false |
| XorOut | 0x00 |
| Check | ASCII 123456789 的结果为 0xF4 |
对应生成多项式为 x^8+x^2+x+1。完整二进制除数是 1_0000_0111,即 0x107;参数中的 0x07 省略最高的 x^8 项。TI的PMBus API文档也明确采用这种最高项隐含的写法。TI PMBus_generateCRCTable
RefIn=false 对应本文每个输入字节从bit7处理到bit0;RefOut=false 表示不把最终寄存器的8位再反转。结果也不额外异或一个非零常数。
因此只说"CRC-8,多项式07"还不足以确认两端在算同一种东西。至少要一起核对上面这些参数,以及消息究竟包含哪些字节。
2. 一位递推怎样展开成一个字节
当前CRC寄存器记为 v,下一位数据为 b。每处理一位,做三步:
text
feedback = v[7] XOR b
v = {v[6:0], 1'b0}
如果feedback为1,v再异或0x07
字节 d 依次提供 d[7]、d[6]......d[0],连续做8轮即可得到接收这个字节之后的CRC。

图中的8轮是组合逻辑展开,不是8个时钟周期。一个 byte_en=1 的上升沿接收一个完整字节,把8轮运算的结果写回寄存器。这种写法容易阅读;若目标时钟较高,是否满足时序仍要由综合和时序报告判断。
例如从初值00处理单字节01,数据前7位都是0,最后一位才产生反馈,结果为07。换成80,高位首先参与反馈,后续还有7轮递推,结果为89。这个不对称性很适合检查位序。
3. 先确定清零和数据同拍时发生什么
模块接口如下:
| 信号 | 含义 |
|---|---|
clk |
上升沿工作时钟 |
rst |
同步高有效复位 |
clear |
开始新消息前,把CRC清为00 |
byte_en |
本拍输入字节有效 |
data[7:0] |
本拍字节 |
crc[7:0] |
已接收字节序列的当前CRC |
优先级是:rst/clear 高于 byte_en。
这意味着 clear=1 和 byte_en=1 同拍时,数据不会参加计算。清零拍与首字节拍要分开。如果上层希望一拍同时开始新消息并处理首字节,需要另行定义接口和数据通路,不能假设下面代码已经支持。
以ASCII字符串的前两个字节为例,表中CRC是该上升沿更新后的值:
| 时钟边沿 | clear | byte_en | data | 更新后crc | 发生的操作 |
|---|---|---|---|---|---|
| t0 | 1 | 1 | FF | 00 | 清零优先,FF被忽略 |
| t1 | 0 | 1 | 31 | 97 | 接收字符1 |
| t2 | 0 | 0 | AA | 97 | 无效字节不参与,CRC保持 |
| t3 | 0 | 1 | 32 | 72 | 接着接收字符2 |
模块没有 last 或 crc_valid。上层掌握消息长度,就能知道哪个有效字节是最后一个。最后一个字节接受后,输出已经包含它,不需要再喂一个"flush字节"。
还要注意非阻塞赋值的时序:另一个 always_ff 如果在接收末字节的同一上升沿锁存 crc,读到的仍是旧值。可以在下一拍锁存已经更新的输出;下一拍不必继续提供数据。
4. 完整RTL
保存为 rtl.sv。正常编译不定义任何 MUT_ 宏;两个宏保留用于后面的故障测试。
systemverilog
`timescale 1ns/1ps
// CRC-8: poly=07 init=00 refin=false refout=false xorout=00.
// rst or clear takes priority over byte_en. crc updates on each accepted byte.
module crc8(input logic clk, rst, clear, byte_en,
input logic [7:0] data, output logic [7:0] crc);
function automatic [7:0] next_crc(input logic [7:0] c, d);
logic [7:0] v;
logic feedback;
v=c;
for (int i=7; i>=0; i--) begin
`ifdef MUT_LSB_FIRST
feedback=v[7]^d[7-i];
`else
feedback=v[7]^d[i];
`endif
v={v[6:0],1'b0};
if (feedback) v=v^8'h07;
end
return v;
endfunction
always_ff @(posedge clk) begin
if (rst || clear) begin
`ifdef MUT_INIT_FF
crc <= 8'hff;
`else
crc <= 8'h00;
`endif
end else if (byte_en) crc <= next_crc(crc,data);
end
endmodule
next_crc 内部使用阻塞赋值,是因为后一次迭代必须接着使用前一次迭代的值。外层时序块使用非阻塞赋值,把这一字节计算完成的结果写入寄存器。
这里既没有输出反转,也没有额外异或,所以 crc 就是要使用的数值。若已经处理完整消息,再无条件输入一个00,算出来的是"原消息后面又追加一个零字节"的CRC。
5. 参考模型不要照抄RTL
如果testbench把 next_crc 原封不动复制一份,它能检查接口时序,却很可能和DUT带着相同的位序错误一起通过。
本例参考模型使用GF(2)多项式长除法。对旧CRC prior 和新字节 d,先构造:
text
remainder = (prior XOR d) << 8
然后从bit15到bit8,遇到1就把完整除数 0x107 对齐到该位并异或,最后取低8位。这里的减法就是异或,没有借位。
它与RTL的逐位反馈法表达方式不同。同时再加几组写死的已知结果,防止两个实现恰好犯同一种错。
其中ASCII 12345678 的C7结果来自Microchip官方CRC-SMBus算例;该算例同时给出了07多项式和00初值。Microchip CRC-SMBus示例
5.1 完整testbench
保存为 tb.sv,与RTL放在同一目录:
systemverilog
`timescale 1ns/1ps
module tb;
logic clk=0,rst=1,clear=0,byte_en=0;
logic [7:0] data=0;
wire [7:0] crc;
logic [7:0] reference=0;
integer checks=0;
string vector_s="123456789";
always #5 clk=~clk;
crc8 dut(.*);
// Independent polynomial long division, not the DUT bit recurrence.
function automatic [7:0] divide(input logic [7:0] prior, d);
logic [15:0] remainder;
remainder={prior^d,8'h00};
for (int i=15;i>=8;i--)
if(remainder[i]) remainder ^= (16'h0107 << (i-8));
return remainder[7:0];
endfunction
task automatic step(input bit reset_i, clear_i, en_i,
input logic [7:0] d, input string label_i);
@(negedge clk); rst=reset_i; clear=clear_i; byte_en=en_i; data=d;
if(reset_i || clear_i) reference=0;
else if(en_i) reference=divide(reference,d);
@(posedge clk); #1; checks++;
if(crc !== reference)
$fatal(1,"CHECK_FAIL CRC %s got=%02h expected=%02h",label_i,crc,reference);
endtask
initial begin
step(1,0,1,8'hff,"reset priority and empty CRC");
step(0,0,0,8'h80,"hold empty CRC");
for(int i=0;i<8;i++) step(0,0,1,vector_s[i],"12345678");
if(crc !== 8'hc7) $fatal(1,"CHECK_FAIL CRC Microchip 12345678 must be C7");
$display("VECTOR CRC 12345678 = %02h (Microchip)",crc);
step(0,0,1,vector_s[8],"123456789");
if(crc !== 8'hf4) $fatal(1,"CHECK_FAIL CRC known vector must be F4");
$display("VECTOR CRC 123456789 = %02h",crc);
step(0,1,1,8'hff,"clear beats data");
step(0,0,1,8'h01,"single byte 01");
if(crc !== 8'h07) $fatal(1,"CHECK_FAIL CRC 01 must be 07");
step(0,1,0,0,"new message");
step(0,0,1,8'h80,"MSB vector");
if(crc !== 8'h89) $fatal(1,"CHECK_FAIL CRC 80 must be 89");
for(int i=0;i<256;i++) begin
step(0,1,0,0,"clear per byte");
step(0,0,1,i,"all single bytes");
step(0,0,0,~i,"disabled input holds CRC");
end
step(0,1,0,0,"clear stream");
for(int i=0;i<256;i++) step(0,0,1,i,"chained byte stream");
step(1,0,0,0,"reset nonzero state");
$display("TEST_PASS CRC checks=%0d single_bytes=256 stream_bytes=256",checks); $finish;
end
initial begin #30000; $fatal(1,"WATCHDOG CRC"); end
endmodule
这个testbench检查了三类问题。
第一类是固定答案:空消息00、单字节01得到07、单字节80得到89,以及8字节C7、9字节F4。字符串不包含结尾零字节。
第二类是数据通路:全部256种单字节输入分别从零开始计算,再把00到FF串成一条256字节连续流,逐字节对照长除法。单字节测试检查输入组合,连续流还会让前一个CRC参与下一个字节的计算。
第三类是接口:复位压过有效数据、清零压过有效数据、无使能时即使data改变也保持CRC,以及清掉已有计算状态。
激励在下降沿驱动,上升沿后1 ns检查,不与DUT的非阻塞赋值更新竞争。checks 统计的是 step 中的1041次逐拍比较,此外还有几个固定常量断言。
5.2 运行与实际输出
本次实测使用Windows PowerShell和Vivado Simulator 2022.1,时钟周期10 ns;验证范围是数字RTL仿真,没有进行综合、时序分析或板级测试。
在新的空目录里保存上述两份源码,确保终端能找到Vivado仿真工具,依次执行:
text
xvlog -sv rtl.sv tb.sv
xelab work.tb -s sim -debug typical
xsim sim -runall
Windows使用已配置Vivado环境的PowerShell或命令提示符;Linux先加载本机Vivado的 settings64.sh,再在Bash里运行这三条命令。它们是独立仿真器命令,不依赖PowerShell脚本或第三方HDL库。AMD UG900 2022.1
原版日志的关键输出为:
text
VECTOR CRC 12345678 = c7 (Microchip)
VECTOR CRC 123456789 = f4
TEST_PASS CRC checks=1041 single_bytes=256 stream_bytes=256
$finish called at time : 10416 ns
如果要在波形窗口定位第一个出错字节,可以用下面这组中间值作对照。它们由上述长除法逐字节计算,表格是数值对照,不是仿真器截图:
| 已输入字符 | 当前字节 | 更新后CRC |
|---|---|---|
| 1 | 31 | 97 |
| 12 | 32 | 72 |
| 123 | 33 | C0 |
| 1234 | 34 | C2 |
| 12345 | 35 | CB |
| 123456 | 36 | FD |
| 1234567 | 37 | 78 |
| 12345678 | 38 | C7 |
| 123456789 | 39 | F4 |
第一个字节就不同,先查初值、位序和输入编码。前面都一致、最后才不同,则沿着首个分歧检查是否多喂、漏喂了字节,或者末字节同拍清零。
6. 分别把位序和初值改错
6.1 让输入从bit0开始
另建一个空目录,复制同样两份源码,加宏编译:
text
xvlog -sv -d MUT_LSB_FIRST rtl.sv tb.sv
xelab work.tb -s sim -debug typical
xsim sim -runall
这个宏只改变输入位的访问次序,仍然使用左移和07多项式。它是对本文契约的故意破坏,不能把这个组合直接当作另一套标准反射CRC实现。
实际在36 ns被抓到:
text
Fatal: CHECK_FAIL CRC 12345678 got=ad expected=97
Time: 36 ns
这里标签是 12345678,但逐字节检查在第一个字符1就已经失败。97 是单字节ASCII 1,即31的CRC,不是完整8字节字符串的结果。
6.2 把初值改成FF
再换一个新的空目录,只定义初值故障宏:
text
xvlog -sv -d MUT_INIT_FF rtl.sv tb.sv
xelab work.tb -s sim -debug typical
xsim sim -runall
实际输出:
text
Fatal: CHECK_FAIL CRC reset priority and empty CRC got=ff expected=00
Time: 16 ns
两个故障版本都成功编译和展开,随后被同一testbench的数值检查捕获。它们没有修改参考模型。
这次运行中,xsim遇到 $fatal 后仍返回退出码0。自动判断结果时,要同时查看成功标记和错误标记;不能把"进程正常退出"直接等同于"测试通过"。每个宏版本用独立空目录,也能避免旧编译结果或旧快照混进下一次测试。
7. 接到SMBus时,真正容易漏的是输入范围
CRC计算核只知道 data 和 byte_en。它不会识别总线上的地址、ACK或重复START,协议层必须决定哪些字节送进来。
SMBus规范要求PEC按收到的位序计算,字节内部MSB先行;计算覆盖消息里的地址、命令和数据,不包含ACK/NACK位,也不把START、STOP、REPEATED START条件本身当作数据。SMBus 3.3.1,5.2与6.4.1
以7位地址为0x2A的Write Byte事务为例,送入CRC核的顺序是:
text
54 command data
│
└─ 地址字节 = (0x2A << 1) | 0,包含写方向位
不能只把7位地址值2A当作完整地址字节,也不能只计算data而漏掉地址和command。
对应Read Byte事务,CRC输入顺序为:
text
54 command 55 data
│
└─ 重复START之后的地址字节,读方向位为1
重复START本身不是CRC输入字节,但它前后的字节属于这条完整消息。不能看到重复START就把本例累计CRC清掉。
收到PEC时有两种常见检查接法:
- 计算消息字节的CRC,与收到的PEC直接比较
- 把PEC也接着送入计算,检查最后是否为00
Microchip的AVR316说明了第二种做法:消息和其CRC一起参与计算,正确码字的余数为零。Microchip AVR316,第12页 对本文参数,处理 123456789 得到F4后,再接收F4,按递推可得00;这属于参数性质说明,前面的1041次测试并未包含独立的PEC接收检查。
两种接法都要先想清楚输出所代表的阶段:已经吃掉PEC之后,就不要还拿结果与PEC比较;只计算完消息时,也不要要求它必须为00。
8. 联调时按首个分歧定位
先把实际输入的字节按十六进制打印或抓出来,与预期消息逐项对照。字符1是31,数值1是01,这一步错了,后面的计算即使完全正确也不会得到同样结果。
然后确认消息起点:本模块clear会丢弃同拍有效字节,所以首字节应在清零后的下一次有效拍进入。再核对每字节bit7到bit0、完整参数以及末字节的锁存时刻。
最后再回到协议层核对覆盖范围。多字节字段的高低字节顺序由协议决定;它与一个字节内部是否反射是两件事。不要为了凑出某个结果,同时乱改移位方向、字节顺序和初值。
CRC-8只有256种结果,不同消息会碰撞。它适合检测传输错误,不能从一个错误CRC推回原消息,也不能代替身份认证或防篡改机制。
对这类小核,最有用的调试证据是"从哪一个字节开始不同"。固定参数、逐字节参考值和清零时序都明确以后,F4只是最后一个检查点,前面的每一拍也都有依据。