一个 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 中的本次原始日志;官方文档只提供背景和工具说明。