将 ADC 计数、测量结果或采样频率显示到数码管时,内部二进制值不能直接作为十进制位使用。这里实现无符号二进制到 packed BCD 的逐位转换:每个十进制位占 4 bit,最低 nibble 为个位,例如十进制 123 对应 12'h123,不是字符串,也不是二进制的 12'd123。
本文只负责数值表示转换,与既有数码管扫描/计数器文章不同:不含动态扫描、位选、段选和按键逻辑,可独立接到数码管或串口格式化链路。
先明确接口与容量
输入为 s_valid/s_ready/s_data,仅在上升沿二者同高时接收一个数。输出同理。W 支持 1~32,D 支持 1~10,并要求 10^D > 2^W-1。例如 W=12 要覆盖 4095,D 至少为 4;D=3 不能静默截掉千位。支持额外前导零位,不支持负数、补码或 ASCII 编码。
本实现刻意串行处理:接收边沿记为 t0,此后进行 W 次逐位迭代,t0 后第 W 个上升沿结束时设置 m_valid。若下游当时已就绪,最早在再下一个上升沿完成输出握手。结果等待期间 m_valid 和 m_data 必须保持,且不接收新输入。输出消费当拍也不同时接新输入,这是简单接口的有意吞吐代价,不是满流水线转换器。
为什么先加 3 再移位
从输入最高位开始,每一轮先检查 BCD 中的每个十进制位:大于等于 5 的位加 3;然后整体左移一位,并把当前二进制最高位放入最低位。加 3 校正抵消二进制 nibble 的 16 进位与十进制的 10 进位差异。最终恰好执行 W 轮,不能多移或少移。最后一轮输出必须取本轮 shifted,而不是非阻塞赋值更新前的旧 digits。
Microchip AN526 收录二进制与 BCD 等数值转换例程,可作问题背景资料。这里没有复制其中汇编或任何 GitHub 实现;本稿代码是单独编写的 RTL 候选,算法正确性仍以独立十进制除余 oracle 的实际 RTL 回归为准。
完整实现
rtl.sv:
systemverilog
module bin2bcd #(parameter integer W=12,D=4)(
input wire clk,rst,input wire s_valid,output wire s_ready,
input wire [W-1:0] s_data,output reg m_valid,input wire m_ready,
output reg [4*D-1:0] m_data);
reg busy;reg[W-1:0] binary;
reg[4*D-1:0] digits,adjusted,shifted;
integer remaining,k;reg[63:0] capacity;integer p;
// This reference supports W=1..32,D=1..10. Guard against truncation.
initial begin
capacity=1;for(p=0;p<D;p=p+1)capacity=capacity*10;
if(W<1||W>32||D<1||D>10||capacity<=(64'd1<<W)-1)
$fatal(1,"invalid W/D or insufficient decimal digits");
end
assign s_ready=!rst&&!busy&&!m_valid;
always @* begin
adjusted=digits;
for(k=0;k<D;k=k+1)if(digits[4*k+:4]>=5)adjusted[4*k+:4]=digits[4*k+:4]+4'd3;
shifted=(adjusted<<1);shifted[0]=binary[W-1];
end
always @(posedge clk)begin
if(rst)begin busy<=0;binary<=0;digits<=0;remaining<=0;m_valid<=0;m_data<=0;end
else begin
if(m_valid&&m_ready)m_valid<=0;
if(s_valid&&s_ready)begin busy<=1;binary<=s_data;digits<=0;remaining<=W;end
else if(busy)begin
digits<=shifted;binary<=binary<<1;remaining<=remaining-1;
if(remaining==1)begin busy<=0;m_data<=shifted;m_valid<=1;end
end
end
end
endmodule
不重复算法的自检测试台
测试台用 %10 与 /10 构造期望 BCD,不在 oracle 中重做 add-3/shift。对 W≤12 穷举全部输入;更宽参数执行 0~4095 加最大值、0、1,以及 2048 个覆盖高位的确定性 LFSR 序列值;同时检查范围内 10 的幂的前后边界,并不宣称穷举 16/32 bit。固定周期背压验证输出保持;默认 W=12 等多轮转换配置检查运算中复位,W=1 在该测试时序下已结束转换,不宣称覆盖 busy 中复位;输出停顿时复位也丢弃未完成事务。输入发送器在接收前保持 valid 和 data,测试台设超时避免挂死。
tb.sv:
systemverilog
`timescale 1ns/1ps
module tb;
parameter W=12, D=4;
reg clk=0; always #5 clk=~clk;
reg rst=1,sv=0,mr=0; reg [W-1:0] sd=0;
wire sr,mv; wire [4*D-1:0] md;
bin2bcd #(.W(W),.D(D)) dut(clk,rst,sv,sr,sd,mv,mr,md);
reg [4*D-1:0] expected[0:20000]; integer wr=0,rd=0,cycles=0,j;
reg [31:0] rng=32'h13579bdf; reg [63:0] power10;
reg stalled=0; reg [4*D-1:0] held;
function automatic [4*D-1:0] decimal_digits(input reg [W-1:0] v);
reg [63:0] n; integer k;
begin n=v; decimal_digits=0; for(k=0;k<D;k=k+1) begin decimal_digits[4*k+:4]=n%10; n=n/10; end end
endfunction
always @(posedge clk) begin
if(rst) begin wr=0;rd=0;stalled=0;end
else begin
cycles=cycles+1;
if(stalled && (!mv || md!==held)) $fatal(1,"output changed under backpressure");
if(sv&&sr) begin expected[wr]=decimal_digits(sd);wr=wr+1;end
if(mv&&mr) begin
if(rd>=wr || md!==expected[rd]) $fatal(1,"BCD mismatch rd=%0d got=%h expected=%h",rd,md,expected[rd]);
rd=rd+1;
end
stalled=mv&&!mr; held=md;
end
end
task automatic reset_dut;
begin @(negedge clk);rst=1;sv=0;mr=0;repeat(2) @(negedge clk);rst=0;end
endtask
task automatic send(input reg [W-1:0] n);
begin @(negedge clk);sv=1;sd=n; do @(posedge clk); while(!sr); @(negedge clk);sv=0;end
endtask
// Deterministic backpressure, including runs of consecutive stalls.
reg drive_ready=0; integer phase=0;
always @(negedge clk) if(drive_ready) begin phase=phase+1;mr=(phase%13)>=5;end
initial begin
reset_dut();drive_ready=1;
for(j=0;j<(W<=12?(1<<W):4096);j=j+1) send(j);
send({W{1'b1}});send(0);send(1);
if(W>12)begin
for(j=0;j<2048;j=j+1)begin
rng={rng[30:0],rng[31]^rng[21]^rng[1]^rng[0]};send(rng);
end
end
power10=10;
while(power10<=(64'd1<<W)-1)begin
send(power10-1);send(power10);send(power10+1);power10=power10*10;
end
wait(rd==wr);@(negedge clk);drive_ready=0;mr=0;
// Abort work; no result from the aborted transaction may leak after reset.
send({W{1'b1}});reset_dut();drive_ready=1;send(7);wait(rd==wr);
@(negedge clk);drive_ready=0;mr=0;
send({W{1'b1}});wait(mv);repeat(4) @(negedge clk);reset_dut();
drive_ready=1;send(0);wait(rd==wr);repeat(5) @(negedge clk);
$display("PASS bin2bcd W=%0d D=%0d cycles=%0d",W,D,cycles);$finish;
end
initial begin #10000000;$fatal(1,"timeout");end
endmodule
接入数码管/串口的步骤
- 根据最大显示量选择 W、D,并先验证容量检查。
- 上游保存待转换数据,置 valid,握手后再撤销;不要只给一拍脉冲却忽视 ready。
- 下游仅在真正能存下整份 BCD 时给 ready;拿到结果后再从个位开始做译码,不把每个 nibble 当 ASCII。
- 复位中止当前转换;若业务不能丢结果,由上层记录事务并重发。
- 仿真完成后另做综合、时序与引脚约束检查。串行迭代不等于已经证明资源占用更低或频率更高。
当前验证边界与复现环境
本稿是待实测的工程候选稿,不能按已验证文章发布 。2026-10-27 云工作区仅完成文件准备和工具可用性检查;未发现 Icarus Verilog、Verilator 或 Vivado xsim。运行批量脚本得到 BLOCKED: iverilog not installed; no RTL executed。因此没有 RTL 编译成功、波形、仿真 PASS、综合资源、时序收敛或上板结果;也没有用 Python 结果代替 RTL 仿真。
代码使用 SystemVerilog,时钟为测试台中的 10 ns 周期,同步高有效复位。实际工程应替换为自己的时钟约束和复位同步方案。下列运行命令是待执行步骤,不代表工具已经运行。前置条件为已合法安装并可执行的 Icarus Verilog(支持 -g2012)及 vvp,本次不安装或购买软件。
在附件目录执行:
bash
bash run.sh
脚本为每次运行创建独立 runs/run-* 目录,保存工具版本、编译输出和运行输出,避免覆盖旧证据,并要求进程成功且日志含 PASS。还需要人工检查日志没有 fatal/error、核对执行文件与源码哈希、进行独立复核。源码内参数范围检查供仿真提前暴露配置错误,不替代目标综合器的可综合性检查。
若已有 Vivado,可使用官方三阶段流程(在对应子目录执行,默认参数):
text
xvlog -sv rtl.sv tb.sv
xelab work.tb -s tb_snapshot
xsim tb_snapshot -R
以上流程依据 AMD UG900:xelab、xvhdl、xvlog 和 xsim 命令选项。实际版本与日志待补,不据此宣称厂商工具已执行。
下一步验证和相关实践
待补证据:完整参数矩阵日志、非法 D 的预期失败、独立复核结论、实际使用版本。尤其应注入"阈值改为 >5""最后一轮取旧 digits""背压仍清 valid"等故障,确认测试确实能失败。未执行故障注入,不能宣称覆盖有效。
可继续阅读:逐位恢复整数平方根、ready/valid 握手与背压。你的显示刷新频率和数据产生频率分别是多少?是否允许一个结果等待时阻塞新输入,还是需要再加独立输入队列?