FPGA 小数据字如何纠错:完整实现并验证扩展 Hamming (13,8) SECDED

一个 8 位配置字段经过存储或传输后,如果只有一位翻转,能否直接恢复?如果两位一起翻转,能否明确告诉上层"这次不能使用"?本实验给数据增加 5 个校验位,用 13 位码字承载 8 位信息,并把每一种单错和双错都送进真实 RTL 仿真。

本文是自定义的组合编码/解码核心。它不包含 RAM、总线控制器、时钟域跨越或错误注入硬件。2026-10-23 在 DESKTOP-396TDVB 上用已安装的 XSim 2022.1 执行;没有综合、STA 或板级测试。源码按可综合组合逻辑编写,是否满足目标器件的资源和时序要求仍须后续验证。

从码位约定开始

文中"位置"从 1 开始,SystemVerilog 向量下标从 0 开始:位置 p 对应 code[p-1]。先固定这件事,后面的 syndrome 才有意义。

1-based 位置 1 2 3 4 5 6 7 8 9 10 11 12 13
内容 p1 p2 d0 p4 d1 d2 d3 p8 d4 d5 d6 d7 overall

位置 1、2、4、8 是 Hamming 偶校验位。p1 检查位置编号二进制最低位为 1 的位置;p2 检查次低位为 1 的位置,以此类推。四个校验集合都只覆盖位置 1...12。位置 13 再使全部 13 位的异或为 0。

8 位数据需要 4 个 Hamming 校验位,因为 8+4+1 <= 2^4;再添加 overall parity,得到这里的 13 位扩展码。AMD/Xilinx 的 XAPP645 v2.2,第 1--2 页 解释了这个容量条件和额外整体校验位的作用。该文参考设计面向 32/64 位数据,本文的映射、RTL 和实测结果是独立的 8 位教学实现,不使用其中的性能数字。

接收端重新计算四个校验结果,组成 4 位 syndrome;也可理解为把位置 1...12 中值为 1 的位置编号全部异或。overall 是接收码字全部 13 位的异或值。

syndrome overall 本核心处理 corrected uncorrectable
0 0 保持接收码字 0 0
0 1 翻转位置 13 1 0
1...12 1 翻转 syndrome 指向的位置 1 0
非零 0 保持接收码字,报告不可纠正 0 1
13...15 1 非法定位,保持接收码字 0 1

最后一行不能省略:这是缩短后的码,位置 14、15 根本不存在;syndrome=13 也不是此校验矩阵中的 Hamming 位,不能把它当成位置 13 的整体校验位来处理。只有先检查范围,才允许动态位索引。

这张表描述的是接收模式分类。corrected=1 表示解码器执行了单错纠正路径,不证明真实世界只发生过一个翻转;uncorrectable=0 也不保证没有三位及以上错误。

接口和一个可追踪例子

data_in -> code_out 是编码路径;code_in -> corrected_code/data_out/flags 是独立解码路径,二者均为组合逻辑。data_out 始终从 corrected_code 的数据位置取出。若 uncorrectable=1,调用方必须丢弃或另行处置该数据,不能因为总线上还有数值就继续使用。接口没有 clk、复位、valid 或 ready;使用时可在外部按系统时序寄存输出。

数据 8'hA5 编码为 13'h0A27。翻转位置 3,接收 0A23,syndrome=3,纠正后恢复 0A27/A5;仅翻转 overall 位,接收 1A27,syndrome=0,仍能恢复。翻转位置 1 和 3,则接收 0A22,syndrome=2、overall=0,应报告双错而不能按 syndrome=2 再翻转一位。

以下来自本次 runs_original/secded_original/trace.csv 前 9 行。测试每次驱动输入后等待 5 ns 再检查,检查结束时立即驱动下一组。因此 CSV 的检查时刻比对应 VCD 输入变化晚 5 ns;这是 testbench 的观察间隔,不是器件传播延迟。

驱动 / 检查时刻 ns 原数据 翻转位置 接收码字 syndrome c/u 解码数据
0 / 5 A5 无 0A27 0 0/0 A5
5 / 10 A5 3 0A23 3 1/0 A5
10 / 15 A5 13 1A27 0 1/0 A5
15 / 20 A5 1,3 0A22 2 0/1 A4,不可使用
20 / 25 00 1,2,3 0007 0 1/0 01,误纠正
25 / 30 00 1,2,3,13 1007 0 0/0 01,未检出
30 / 35 00 1,4,8 0089 13 0/1 00,不可使用
35 / 40 00 2,4,8 008A 14 0/1 00,不可使用
40 / 45 00 3,4,8 008C 15 0/1 01,不可使用

图 1 是本次 VCD 的组合信号变化,不是手画示意图。c/u 分别指 corrected/uncorrectable。三错和四错用例的结果符合这里的解码规则,也同时展示了规则的能力边界。

完整 RTL

保存为 src/secded13_8.sv。以下代码块与本次实际编译的最终文件逐字一致。

systemverilog 复制代码
`timescale 1ns/1ps
module secded13_8 (
    input wire [7:0] data_in,
    output reg [12:0] code_out,
    input wire [12:0] code_in,
    output reg [7:0] data_out,
    output reg [12:0] corrected_code,
    output reg [3:0] syndrome,
    output reg corrected, uncorrectable
);
    integer pos, bitno, check;
    reg parity;
    always @* begin
        code_out=0;
        bitno=0;
        for(pos=1;pos<=12;pos=pos+1)
            if(pos!=1 && pos!=2 && pos!=4 && pos!=8) begin
                code_out[pos-1]=data_in[bitno];bitno=bitno+1;
            end
        for(check=1;check<=8;check=check*2) begin
            parity=0;
            for(pos=1;pos<=12;pos=pos+1)
                if((pos & check)!=0) parity=parity ^ code_out[pos-1];
            code_out[check-1]=parity;
        end
        code_out[12]=^code_out[11:0];
    end
    integer idx;
    reg overall;
    always @* begin
        syndrome=0;
        for(idx=1;idx<=12;idx=idx+1)
            if(code_in[idx-1]) syndrome=syndrome ^ idx;
        overall=^code_in;
        corrected_code=code_in;corrected=0;uncorrectable=0;
        if(overall) begin
            if(syndrome==0) begin
                corrected_code[12]=~code_in[12];corrected=1;
            end else if(syndrome<=12) begin
                corrected_code[syndrome-1]=~code_in[syndrome-1];corrected=1;
            end else uncorrectable=1;
        end else if(syndrome!=0) uncorrectable=1;
        data_out={corrected_code[11:8],corrected_code[6:4],corrected_code[2]};
    end
endmodule

编码器先把所有校验位置清零,再填数据并生成偶校验位。解码器先给所有输出默认值,再根据 syndrome 与 overall 修改,避免组合分支漏赋值。循环变量是静态范围;仿真通过不等于已经完成综合验证。

完整自检 testbench

保存为 src/tb_secded13_8.sv。参考编码器使用四个明确的异或公式,而不是调用 DUT。每例同时检查编码结果、syndrome、纠正后的完整码字、数据和两个标志。测试包含 256 个无错输入、每个数据的 13 个单错、每个数据的全部 C(13,2)=78 个双错,以及前面的 9 个定向例子。

systemverilog 复制代码
`timescale 1ns/1ps
module tb_secded13_8;
    reg [7:0] data_in=0;
    reg [12:0] code_in=0;
    wire [12:0] code_out,corrected_code;
    wire [7:0] data_out;
    wire [3:0] syndrome;
    wire corrected,uncorrectable;
    secded13_8 dut(.*);
    integer fd,checks=0,clean=0,single_count=0,double_count=0,d,i,j;
    reg [12:0] c,mask;
    function automatic [12:0] encode_ref(input [7:0] x);
        reg [12:0] v;
        begin
            v=0;
            v[2]=x[0];v[4]=x[1];v[5]=x[2];v[6]=x[3];
            v[8]=x[4];v[9]=x[5];v[10]=x[6];v[11]=x[7];
            v[0]=x[0]^x[1]^x[3]^x[4]^x[6];
            v[1]=x[0]^x[2]^x[3]^x[5]^x[6];
            v[3]=x[1]^x[2]^x[3]^x[7];
            v[7]=x[4]^x[5]^x[6]^x[7];
            v[12]=^v[11:0];encode_ref=v;
        end
    endfunction
    task automatic check_case(input [7:0] x,input [12:0] flips,
        input bit exp_c,input bit exp_u,input [12:0] exp_code,input integer category);
        reg [3:0] expected_syndrome;
        begin
            data_in=x;code_in=encode_ref(x)^flips;#5;
            expected_syndrome=0;
            for(integer p=1;p<=12;p=p+1)
                if(flips[p-1]) expected_syndrome=expected_syndrome^p;
            if(code_out!==encode_ref(x) || syndrome!==expected_syndrome ||
               corrected!==exp_c || uncorrectable!==exp_u || corrected_code!==exp_code ||
               data_out!=={exp_code[11:8],exp_code[6:4],exp_code[2]})
                $fatal(1,"SECDED_ASSERT x=%h flips=%h syn=%d corr=%b uncorr=%b code=%h/%h data=%h",x,flips,syndrome,corrected,uncorrectable,corrected_code,exp_code,data_out);
            checks=checks+1;
            $fdisplay(fd,"%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d",$time,category,x,flips,code_out,code_in,syndrome,corrected,uncorrectable,corrected_code,data_out);
        end
    endtask
    initial begin
        fd=$fopen("trace.csv","w");
        $fdisplay(fd,"time_ns,category,data,flips,code_out,code_in,syndrome,corrected,uncorrectable,corrected_code,data_out");
        c=encode_ref(8'ha5);
        check_case(8'ha5,0,0,0,c,0);
        check_case(8'ha5,13'h004,1,0,c,1);
        check_case(8'ha5,13'h1000,1,0,c,1);
        check_case(8'ha5,13'h005,0,1,c^13'h005,2);
        check_case(0,13'h007,1,0,13'h1007,3);
        check_case(0,13'h1007,0,0,13'h1007,4);
        check_case(0,13'h089,0,1,13'h089,5); // 1^4^8 = 13
        check_case(0,13'h08a,0,1,13'h08a,5); // 2^4^8 = 14
        check_case(0,13'h08c,0,1,13'h08c,5); // 3^4^8 = 15
        for(d=0;d<256;d=d+1) begin
            c=encode_ref(d);
            check_case(d,0,0,0,c,0);clean=clean+1;
            for(i=0;i<13;i=i+1) begin
                check_case(d,13'b1<<i,1,0,c,1);single_count=single_count+1;
            end
            for(i=0;i<13;i=i+1) for(j=i+1;j<13;j=j+1) begin
                mask=(13'b1<<i)|(13'b1<<j);
                check_case(d,mask,0,1,c^mask,2);double_count=double_count+1;
            end
        end
        $display("TEST_PASS SECDED original checks=%0d clean=%0d singles=%0d doubles=%0d directed=9",checks,clean,single_count,double_count);
        $fclose(fd);$finish;
    end
    initial begin #200000;$fatal(1,"WATCHDOG SECDED");end
endmodule

实际运行和复现

以下 PowerShell 命令假定两个 .sv 文件放在当前目录的 src/ 中。工作目录必须是新目录;本文实际使用的 Vivado 路径如下,其他电脑只需改 $vivadoBin。无需 Python 即可执行 RTL 自检。

powershell 复制代码
$vivadoBin = 'D:\tools\Xilinx\Vivado\2022.1\bin'
New-Item -ItemType Directory -Path 'manual_secded13_8' -ErrorAction Stop
Set-Location 'manual_secded13_8'
& "$vivadoBin\xvlog.bat" --sv '../src/secded13_8.sv' '../src/tb_secded13_8.sv'
if ($LASTEXITCODE -ne 0) { throw 'xvlog failed' }
& "$vivadoBin\xelab.bat" tb_secded13_8 -s sim -debug typical
if ($LASTEXITCODE -ne 0) { throw 'xelab failed' }
@'
open_vcd trace.vcd
log_vcd /tb_secded13_8/*
run 2000 ns
close_vcd
run all
quit
'@ | Set-Content -Encoding ascii 'simulate.tcl'
& "$vivadoBin\xsim.bat" sim -tclbatch simulate.tcl
if ($LASTEXITCODE -ne 0) { throw 'xsim process failed' }
$simText = Get-Content -LiteralPath 'xsim.log' -Raw
if ($simText -notmatch 'TEST_PASS' -or $simText -match 'Fatal:|ERROR:') { throw 'RTL self-check failed' }
Set-Location '..'

VCD 只记录前 2000 ns,然后 run all 完成剩余自检;CSV 覆盖整个测试。sim.wdb 是 XSim 实际生成的原生波形数据库,不能仅凭它存在推断所有信号、全时段都被记录。零退出码必须同时结合 PASS、断言和计数核验,不能代替功能检查。

完整交付包还包含自动化复现入口,使用已经安装的 Python 标准库,不安装软件。下列输出目录不能已存在,以免覆盖本次原始证据:

powershell 复制代码
python run_tests.py --suite original --out rerun_original
python run_tests.py --suite negative --out rerun_negative

这两条命令分别运行两题的原始测试和 8 个故意错误变体。每个 case 独立保存源代码副本、命令、时间、退出码、原始日志、源码哈希。负例使用包内 review/tb_secded_review.sv(顶层 tb_secded_review)和 review/tb_isqrt_review.sv(顶层 tb_isqrt_review);只有编译、展开成功且出现指定功能断言才算"被捕获",编译错误或 watchdog 不算。独立正常测试可用 python run_review.py --out rerun_review 运行;该复现入口也已实际执行通过,证据在 runs_review_reproduction/。

本次 xvlog/xelab/xsim 三阶段退出码均为 0;工具版本记录为 XSim 2022.1,SW Build 3526262、IP Build 3524634。完整 stdout/stderr、原生工具日志和阶段命令在 runs_original/secded_original/,版本在 environment/。本次日志日期为 2026-10-23;没有使用前一天日志代替。

text 复制代码
TEST_PASS SECDED original checks=23561 clean=256 singles=3328 doubles=19968 directed=9
TEST_PASS SECDED_REVIEW checks=31746 clean=256 singles=3328 doubles=19968 received_words=8192 classes=256,3328,4608 illegal_odd=768 limitations=2

独立参考怎样避免"自己证明自己"

独立 reviewer 先仅阅读接口契约编写 review/tb_secded_review.sv,然后才看 RTL;编写独立 TB 阶段没有阅读原始 TB。它先独立生成 256 个合法码字,用码字间 Hamming 距离检查最小距离,再对所有 8192 个接收字搜索最近合法码字。距离为 0 时应保持;距离为 1 时应纠正到唯一邻居;其他模式按本接口报告不可纠正。参考判定不读取 DUT 内部 syndrome 定位分支。

检查 本次独立测试结果
无错数据 256 / 256
所有单错,含 overall 位 3328 / 3328
所有双错,含 data/parity、overall/其他位 19968 / 19968
全部接收字分类 8192 / 8192
奇 overall 且 syndrome=13...15 的安全拒绝 768 / 768
三错、四错能力边界例子 2 / 2
总检查数 31746

全接收空间分类得到 256 个合法字、3328 个距合法字一位的模式、4608 个不可纠正模式。后处理从实际编码 CSV 构建码本,复核最小距离为 4,并重算两套 CSV 共 55307 行;这是对实际 RTL 输出的再检查,不是用 Python 替代 RTL 仿真。

图 2 是实际测试结果统计图,不是时间波形,也不是错误概率图。同一接收字可能来自不同原字和多位翻转,因此这些计数不能解释为真实故障次数或可靠性指标。

独立测试最终证据在 review_runs_secded_02/。首次独立 TB 把局部变量命名为 SystemVerilog 关键字 dist,编译失败;仅重命名后通过。首次失败源文件和日志保存在 review_runs_secded/。这属于 reviewer TB 语法错误,不属于原始 RTL 缺陷,也不计作负例捕获。完整复核报告为 review/SECDED_REVIEW.md。

故意写错,测试能否抓住

run_tests.py 从最终正常 RTL 各生成一个变体,每个变体只改一处。以下四例均成功编译和展开,再由独立 TB 的 SECDED_REVIEW_ASSERT 触发 $fatal。部分 XSim 功能失败仍返回进程码 0,因此脚本检查具体断言标记,不能只检查退出码。

故意错误 首个捕获例子 / 原因
overall 单错时只置 corrected、不翻转位 接收 1000,应恢复 0000,完整码字检查失败
把偶 overall 的非零 syndrome 也拿去纠正 接收 0003,应报告不可纠正并保持,却改成 0007
放宽定位范围到 syndrome<=15 接收 0089、syndrome=13,错误翻转 overall 位为 1089
编码时把数据 bit0...7 反向映射 输入 01,得到 1888,预期 1007

这些是人为注入的故障,正常实现没有上述错误。变体源码、三阶段退出码和原始失败日志位于 runs_negative/secded_*/。证明的是测试能抓住这四类具体错误,不是证明它能抓住任意可能的实现缺陷。

三位及以上错误:必须公开的限制

零码字翻转位置 1、2、3 后,syndrome=1 xor 2 xor 3=0,overall=1。解码器会把整体校验位当作单错翻转,输出合法码字 1007、数据 01,虽然原数据是 00。再额外翻转位置 13,接收字本身已经是 1007,此时两个标志都为 0,错误完全未检出。这两例在两套 RTL 测试中均实际出现。

SECDED 的保证是:本码及该译码约定下,单个二进制位翻转可纠正,两个不同位置的翻转可检测。三位及以上没有这个保证;本实验也没有进行模拟电路亚稳态仿真,或验证未知 X/Z 的物理成因及存储阵列的故障分布。它不是 CRC 替代品,更不是安全认证机制。上层还需要定义发现不可纠正错误后的丢弃、重读或告警策略。

工具命令选项参考 UG900 2022.1 的仿真命令页。本文的执行结论来自 ZIP 中的本次原始日志;官方文档只提供背景和工具说明。

相关推荐
通信王666 小时前
ZYNQ架构下的AXI_Lite 总线详解(1)
fpga开发
星夜离尘567 小时前
FPGA 不用浮点开平方:逐位恢复整数平方根、余数与完整握手验证
fpga开发
Enclustra_China7 小时前
FPGA设计的隐藏难题:算力、功耗与散热如何平衡?
fpga开发·fpga·fpga解决方案
好焊孙辉8 小时前
从六代螺柱焊机到AI质检:鸿栢科技的螺柱焊国产化路径
人工智能·python·科技·fpga开发
unicrom_深圳市由你创科技10 小时前
高帧率机器视觉 FPGA 图像板卡及传输开发定制解决方案
fpga开发
幻智星科技1 天前
纯FPGA网络通信实现微秒级确定性延迟
网络协议·fpga开发·硬件架构
泡泡糖的中文规格书1 天前
HDLbits 1.5 学习笔记
fpga开发
幻智星科技1 天前
FPGA不同品牌开发平台约束文件迁移难题解析
fpga开发·架构·硬件架构
幻智星科技1 天前
Karatsuba与CSD优化FPGA大数乘法
c++·fpga开发·密码学