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

相关推荐
Hello-FPGA2 天前
基于RFSOC FPGA ZCU216 的CoaXPress 采集demo
fpga开发
9527华安2 天前
FPGA基于ADV7182解码PAL视频,Video Processing Subsystem去隔行,提供6套工程源码和技术支持
fpga开发·音视频·pal·adv7182
hahaha60162 天前
数字图像时域滤波或者3D降噪算法FPGA实现
算法·计算机视觉·fpga开发
GateWorld2 天前
Xilinx 7系列FPGA时钟网络详解
fpga开发·时钟网络·xilinx 7系列
szxinmai主板定制专家2 天前
X86 工控机 vs RK3588 异构主控:AMR 控制器 BOM、散热、长期运维对比
运维·stm32·单片机·嵌入式硬件·fpga开发·am62x
这是谁的博客?2 天前
AI 加速器 RTL 数字系统设计深度解析:从 MAC 阵列 Verilog 实现到时序收敛与时钟功耗的芯片落地全链路
人工智能·macos·ai·verilog·数字ic设计·ai芯片·rtl
szxinmai主板定制专家3 天前
AMR 项目落地:机器人控制器硬件定制化需求复盘
arm开发·单片机·fpga开发·系统架构·机器人·am62x
FPGA小迷弟4 天前
FPGA 与卫星:从星载硬件到在轨智能的完整技术解析
fpga开发·架构·fpga·1024程序员节·无线电
NamoAmitabha1284 天前
CPU 内部结构是什么?---deepseek
fpga开发
abc light4 天前
ARM 裸机开发(6)——时钟树与定时器
arm开发·单片机·fpga开发