FPGA实现CRC-8/SMBUS:从参数和位序核对到1041次自检

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只是最后一个检查点,前面的每一拍也都有依据。

相关推荐
星夜离尘566 小时前
FPGA正交编码器非法跳变检测:把16种状态转移和采样边界测清楚
verilog·fpga·编码器
匠行科技10 小时前
6U‑VPX 高密度异构处理板硬件方案实践
fpga开发·硬件工程
努力搬砖的小H10 小时前
FPGA应用技巧之AXI4-FULL-uiFDMA IP仿真验证
经验分享·fpga开发·axi
hexiaoyan8271 天前
高速图像采集卡处理板卡设计原理图:10-基于TMS320C6678+XC7K325T的6U CPCI Full Camera Link图像处理平台
fpga开发·高速图像采集·图像采集卡·c6678板卡
szxinmai主板定制专家1 天前
储能EMS + 协调控制器实战:RK3568/3576/3588+FPGA+AI 故障录波与诊断
人工智能·深度学习·机器学习·fpga开发·rk3588·rk3576·协调控制器
星夜离尘561 天前
FPGA 定点数缩位:截断、舍入与饱和,避免溢出回绕
fpga开发·定点数
星夜离尘561 天前
I²C总线卡死:先分清SCL拉低、SDA拉低,再谈9个时钟恢复
fpga开发·verilog·i2c
鸥梨菌Honevid1 天前
STA 与 CDC
fpga开发
科恒盛远1 天前
ZU902 与 FMC200 高速采集示波器实测
fpga开发