ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA DAC 目标码限速器:每拍限步、目标反向与满量程跨越的实测验证

FPGA DAC 目标码限速器:每拍限步、目标反向与满量程跨越的实测验证

给 DAC 更新目标码时,目标可能从正值突然跳到负值。若希望数字控制量逐步靠近目标,需要约束的是“本次更新离旧值最多多远”,并在最后一步恰好停在目标。它与一直加固定增量的累加器不同:目标和最大步幅都可以变化,方向也会反转。

本文实现一个 W 位有符号目标码限速器。它限制每个有效更新 tick 的数字码差,没有模拟 DAC 输出级,不保证真实电压无过冲或某个器件的建立时间。

先给真实环境、执行命令和结果

2026-10-24 在DESKTOP-396TDVB上执行已安装的 XSim2022.1,SW Build3526262、IP Build3524634。主 TB 的 5 个参数实例检查 8665 拍,独立 TB 的 7 个实例检查 13308 拍;独立小位宽穷举4680 个旧输出/目标/步幅组合。正常运行三阶段退出码均为 0,且有明确 TEST_PASS、没有 Fatal/ERROR。

TEST_PASS DAC_MAIN instances=5 TEST_PASS DAC_REVIEW

四个故意错误实现都成功编译和展开,再被独立功能断言拒绝。没有综合、STA 或上板;TB 的 10 ns 时钟只是仿真激励,不是 FPGA 已验证的性能结论。

先创建src子目录,将后文两个完整代码块保存为src/dac_slew_limiter.sv、src/tb_dac_slew_limiter.sv,然后在包含src子目录的项目根目录打开 PowerShell。以下命令不需要 Python 或私人 ZIP,使用已经安装的 Vivado。只需按自己的安装位置修改$vivadoBin;新输出目录不可已存在。

$vivadoBin='D:\tools\Xilinx\Vivado\2022.1\bin'New-Item-ItemType Directory-Path'manual_dac_slew_limiter'-ErrorAction StopSet-Location'manual_dac_slew_limiter'&"$vivadoBin\xvlog.bat"--sv'../src/dac_slew_limiter.sv''../src/tb_dac_slew_limiter.sv'if($LASTEXITCODE-ne0){throw'xvlog failed'}&"$vivadoBin\xelab.bat"tb_dac_slew_limiter-s sim-debug typicalif($LASTEXITCODE-ne0){throw'xelab failed'}@' open_vcd trace.vcd log_vcd /tb_dac_slew_limiter/* run 4000 ns close_vcd run all quit '@|Set-Content-Encoding ascii'simulate.tcl'&"$vivadoBin\xsim.bat"sim-tclbatch simulate.tclif($LASTEXITCODE-ne0){throw'xsim process failed'}$simText=Get-Content-LiteralPath'xsim.log'-Rawif($simText-notmatch'TEST_PASS'-or$simText-match'Fatal:|ERROR:'){throw'RTL self-check failed'}Set-Location'..'

本次自动运行同样执行这三种工具及对应选项,并把完整 argv、UTC 开始/结束时间、退出码、编译源文件副本和 SHA256 留存。xsim.log、console 日志、CSV、trace.vcd与sim.wdb都有保存;VCD 只记录前 4000 ns,随后run all完成剩余测试,CSV 覆盖全程。不能把局部 VCD 说成全部用例的波形。

1. 数学规则先于 RTL

旧输出为 Y,有效目标为 T,无符号步幅为 S。每个有效更新 tick:

d = T - Y move = min(abs(d), S) Y' = Y + move,若 d>=0 Y' = Y - move,若 d<0

它应满足两个关键不变量:abs(Y'-Y)<=S,且 Y’ 位于旧 Y 与 T 的闭区间内。目标固定时输出单调靠近目标;目标变化后则按新目标重新决定方向。若距离小于步幅,必须直接到目标,不能继续走完整步幅再发生过冲。

独立数学参考使用等价但不同写法:

Y' = min(max(T, Y-S), Y+S)

参考计算在 64 位整数中完成,覆盖本次最大 W=32 的有符号端点,不使用浮点模型。

2. 接口和同拍优先级

信号契约
W至少为 1;目标和输出是 W 位二补码
clk / rst上升沿工作,高有效同步复位,优先于所有控制
target_valid / target_code每个 clk 上升沿,若 target_valid=1 则锁存 target_code,与 enable 无关
enable / sample_tick两者都为 1 才更新输出
max_stepW 位无符号,每个有效 tick 使用当前值,不锁存
out_codeW 位有符号输出状态
sample_valid标记本边沿发生有效更新 tick,即使码值未变化也为 1

目标更新与 tick 同拍时,明确选择使用新 target_code。因此组合逻辑先形成effective_target=target_valid?target_code:target_reg,避免非阻塞赋值导致本拍仍使用旧目标。

enable=0 只暂停输出,目标依旧能更新;重新 enable 后应追赶最新已锁存目标。sample_tick=0 时同理保持输出,但允许锁存目标。max_step=0 时输出保持,同时 sample_valid 仍为 1;目标已经等于输出时也仍产生有效更新标记。

复位清目标、输出和 sample_valid 为 0。同拍所有控制都有效时仍以复位为准。所有输入均须满足同一 clk 域的同步时序;本模块不负责 CDC,rst 也只在 clk 上升沿采样。

sample_valid不是 ready/valid 背压接口,这里没有 sample_ready。连续有效 tick 可以让 sample_valid 连续为 1;下游发送器必须按这个节拍接收,每拍高电平都代表一次更新,不能只检测它的上升沿。

3. 为什么差值必须扩成 W+1 位

对于 W 位有符号码,最小值为-2^(W-1),最大值为2^(W-1)-1。从负端点到正端点的差为2^W-1,W 位 signed 放不下,W+1 位 signed 才能同时容纳正、负最大差。

例如 W=32:

Y=-2147483648,T=2147483647 d=4294967295 max_step=4294967295 时,一次有效tick准确到达T

RTL 在相减前分别符号扩展 T、Y;步幅则零扩展。不能把无符号最大步幅直接转成 W 位 signed,否则全 1 会变成 -1。

moved只在 distance>step 的分支被采用,此时向目标走一步后的结果必然仍在两个 W 位有符号端点之间。组合电路在未选中分支可能产生截断值,不能据此判断输出错误;决定正确性的是实际选中路径及其位宽条件。

4. 本次真实阶梯轨迹

下面是 W=3 的定向运行,整数范围 -4…3。主 CSV 在上升沿后 1 ns 写出。

上升沿 / CSV 时刻 ns输入或状态输出及标记
25/26enable=0,锁存目标 3输出仍 0,sample_valid=0
35/36、45/46、55/56tick=1,step=1输出 1、2、3
85/86新目标 -4 与 tick 同拍立即按新方向由 3 变 2
145/146enable=0,目标更新为 3输出保持 -3
155/156恢复 enable,step=7,target_valid=0使用已锁存目标,输出到 3
165/166、175/176step=7,目标切到 -4 再切回 3精确跨越两个端点
185/186新目标 -4,step=0输出仍 3,sample_valid=1
195/196无 tick,目标更新 3输出保持,sample_valid=0

图 1 是真实逐拍 CSV 的阶梯连线,包含目标反向、暂停更新目标、最大步幅和零步幅。它表示数字状态变化,不能把线段当成模拟 DAC 的电压斜坡。

5. 完整 RTL

保存为src/dac_slew_limiter.sv;以下与本次实际编译的最终源码逐字一致。

`timescale 1ns/1ps module dac_slew_limiter #(parameter integer W=16)( input wire clk,rst,enable,sample_tick,target_valid, input wire signed [W-1:0] target_code, input wire [W-1:0] max_step, output reg signed [W-1:0] out_code, output reg sample_valid ); reg signed [W-1:0] target_reg; wire signed [W-1:0] effective_target=target_valid ? target_code : target_reg; wire signed [W:0] delta=$signed({effective_target[W-1],effective_target})-$signed({out_code[W-1],out_code}); wire [W:0] distance=delta[W] ? $unsigned(-delta) : $unsigned(delta); wire signed [W:0] moved=delta[W] ? $signed({out_code[W-1],out_code})-$signed({1'b0,max_step}) : $signed({out_code[W-1],out_code})+$signed({1'b0,max_step}); always @(posedge clk) begin if(rst) begin target_reg<=0;out_code<=0;sample_valid<=0;end else begin if(target_valid) target_reg<=target_code; sample_valid<=enable && sample_tick; if(enable && sample_tick) begin if(distance<={1'b0,max_step}) out_code<=effective_target; else out_code<=moved[W-1:0]; end end end endmodule

这段代码没有额外“运行到目标”的状态机。目标每拍都可以变化,max_step 也可每拍变化;每个有效 tick 都重新求当前距离并夹住最后一步。它仅承诺数字域的限步与不越过目标。

6. 完整自检 TB

保存为src/tb_dac_slew_limiter.sv。主 TB 使用 64 位区间夹取参考,每次检查 out_code 和 sample_valid,不读内部目标寄存器。小位宽先用合法最大步幅让 DUT 到达指定旧输出,再测试目标和步幅组合,没有 force 寄存器绕过真实路径。

`timescale 1ns/1ps module dac_case #(parameter W=8)(output reg finished); reg clk=0,rst=1,enable=0,sample_tick=0,target_valid=0; reg signed [W-1:0] target_code=0; reg [W-1:0] max_step=0; wire signed [W-1:0] out_code; wire sample_valid; dac_slew_limiter #(.W(W)) dut(.*); always #5 if(!finished)clk=~clk; longint signed target=0,y=0,old_y,lo,hi,step_max,left_bound,right_bound; integer fd,phase=0,cycles=0,updates=0,holds=0,reversals=0,exhaustive=0,zeros=0,paused_targets=0,same_tick=0; integer i,a,b,s,previous_direction=0,direction; reg expected_valid; reg [31:0] seed=32'h2410dac5; function automatic [31:0] rng; begin seed=seed^(seed<<13);seed=seed^(seed>>17);seed=seed^(seed<<5);rng=seed;end endfunction task automatic tick(input bit en,input bit st,input bit tv,input signed [W-1:0] t,input [W-1:0] step,input bit reset); longint signed target_before,step_integer; begin @(negedge clk);rst=reset;enable=en;sample_tick=st;target_valid=tv;target_code=t;max_step=step; @(posedge clk);cycles++;old_y=y;target_before=target;step_integer=$unsigned(step); if(reset)begin y=0;target=0;expected_valid=0;previous_direction=0;end else begin if(tv)begin target=$signed(t);if(!en)paused_targets++;end expected_valid=en&&st; if(expected_valid)begin updates++;if(tv)same_tick++;if(step_integer==0)zeros++; left_bound=y-step_integer;right_bound=y+step_integer; y=(target<left_bound)?left_bound:((target>right_bound)?right_bound:target); direction=(y>old_y)?1:((y<old_y)?-1:0); if(direction && previous_direction && direction!=previous_direction)reversals++; if(direction)previous_direction=direction; end else holds++; end #1; if($isunknown({sample_valid,out_code}) || sample_valid!==expected_valid || $signed(out_code)!=y) $fatal(1,"DAC_ASSERT W=%0d phase=%0d cycle=%0d valid=%b/%b output=%0d/%0d target=%0d old=%0d step=%0d",W,phase,cycles,sample_valid,expected_valid,out_code,y,target,old_y,step_integer); if(!reset && expected_valid && ((y>old_y?y-old_y:old_y-y)>step_integer || y<lo || y>hi))$fatal(1,"DAC_ASSERT invariant"); $fdisplay(fd,"%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d", $time,W,phase,reset,en,st,tv,$signed(t),$unsigned(step),target_before,target,old_y,$signed(out_code),sample_valid,cycles,updates); end endtask initial begin finished=0;lo=-(64'sd1<<(W-1));hi=(64'sd1<<(W-1))-1;step_max=(64'sd1<<W)-1; fd=$fopen($sformatf("dac_w%0d.csv",W),"w"); $fdisplay(fd,"time_ns,w,phase,rst,enable,tick,target_valid,target_code,max_step,target_before,target_after,out_before,out_after,sample_valid,cycles,updates"); tick(1,1,1,hi,step_max,1); phase=1; tick(0,0,1,hi,1,0);repeat(5)tick(1,1,0,lo,1,0); tick(1,1,1,lo,1,0);repeat(5)tick(1,1,0,hi,1,0); tick(0,1,1,hi,step_max,0);tick(1,1,0,lo,step_max,0); tick(1,1,1,lo,step_max,0);tick(1,1,1,hi,step_max,0); tick(1,1,1,lo,0,0);tick(1,0,1,hi,step_max,0); tick(1,1,0,lo,step_max,0);tick(1,1,0,0,0,0); phase=2; if(W<=3)for(a=0;a<(1<<W);a++)for(b=0;b<(1<<W);b++)for(s=0;s<(1<<W);s++)begin tick(1,1,1,a,step_max,0); tick(1,1,1,b,s,0);exhaustive++; end phase=3; for(i=0;i<1500;i++)tick(rng()%4!=0,rng()%3!=0,rng()%2,rng(),rng(),i%137==0); phase=4; tick(1,1,1,lo,step_max,0);tick(0,0,1,hi,1,0); tick(1,1,1,hi,step_max,1);tick(1,1,0,lo,step_max,0); if(y!=0 || target!=0 || updates<10 || paused_targets<1 || zeros<2 || same_tick<3) $fatal(1,"DAC_ASSERT coverage"); $display("CASE_PASS DAC W=%0d cycles=%0d updates=%0d holds=%0d reversals=%0d exhaustive=%0d zero_steps=%0d paused_targets=%0d same_tick=%0d",W,cycles,updates,holds,reversals,exhaustive,zeros,paused_targets,same_tick); $fclose(fd);finished=1; end endmodule module tb_dac_slew_limiter; wire [4:0] done; dac_case #(1) d1(done[0]);dac_case #(3) d3(done[1]);dac_case #(8) d8(done[2]); dac_case #(16) d16(done[3]);dac_case #(32) d32(done[4]); initial begin wait(&done);$display("TEST_PASS DAC_MAIN instances=5");$finish;end initial begin #1000000;$fatal(1,"DAC_WATCHDOG");end endmodule

主测试 W1/W3 穷举组合分别为 8/512,合计 520;各宽度还有 1500 周期随机控制,并插入复位。实际结果表如下:

W检查周期有效更新 tick复位周期穷举状态/目标/步幅组合
11541761138
32549176913512
81525745130
161525745130
321525745130

主测试总共 4765 个有效更新 tick;它们包含值未变化的有效 tick。只比较相邻输出是否变化,会漏掉 max_step=0 或已经到达目标时的有效标记错误。

7. 独立 TB 和全组合验证

独立 reviewer 先读契约,独立编写 TB 后才审 RTL,编写阶段没有读取主 TB。独立 TB 对 W1/W2/W3/W4 穷举旧输出、目标和所有无符号步幅,总计8+64+512+4096=4680组;另覆盖 W8/W16/W32。

W检查周期有效更新 tick复位周期穷举状态/目标/步幅组合
1580322108
26924361064
31588135010512
487568534104096
8564313100
16564308100
32564331100

独立 CSV 逐行重放确认 13308 拍,其中 11594 个有效 tick、1644 个非复位保持拍、70 个复位拍。包含 724 个零步幅有效 tick、1722 个目标等于输出的有效 tick、476 次暂停期间目标更新;这些类别会重叠,不应相加当作总测试数。所有小位宽组合既完整又无重复,不仅是 for 循环次数正确。

图 2 汇总真实 W4 的 4096 个转移。横轴是更新前目标距离,纵轴是无符号步幅,颜色是实际输出变化量;同坐标的重复转移被合并,点大小反映数量。图是结果统计,不是时间波形。

故意错误变体首个真实功能失败,W=32
同拍更新仍使用旧目标cycle 5:应到 2147483647,实际停在 -2147483648
差值缩窄到 W 位cycle 6:应由 2147483647 减到 2147483646,却跳到 -2147483648
距离不足时仍走完整步幅cycle 4:应到 -2147483648,实际变成 1
max_step=0 时不产生 sample_validcycle 7:码值正确,但 valid 为 0,预期 1

四个变体的 xvlog、xelab 都成功,随后DAC_REVIEW_ASSERT触发 Fatal。它们的 XSim 进程退出码也为 0;这正说明测试判定必须结合功能断言。一个变体中的多个同刻 Fatal 不被重复计数,也不意味着该失败运行完成了后续全组合测试。

正常 RTL 在已执行范围内没有发现功能缺陷。测试先行占位模块曾因缺 timescale 在 xelab 阶段失败,修正后才进入预期功能失败;该工具问题及原日志保留,未当成最终负例成绩。

8. 数字限步不等于模拟 slew rate

ADI 的 Types of ADCs and DACs 页面标注 2002-07-22,本文于 2026-10-24 核读了 Settling Time 与 Slew Rate 条目。前者涉及更新命令后进入规定误差范围所需时间,后者涉及模拟输出变化速率;建立过程还会受输出级振铃和过冲影响。

这里限制的是码值:如果 tick 间隔固定为 Δt,数字码单位下每次变化不超过 S,可描述成码变化预算 S/Δt;换算电压还需要器件传输关系。这不能保证 DAC 实际输出按同一斜率变化,更不能保证无过冲、固定 settling time 或任何特定器件兼容。

若 DAC 发送器一笔事务需要多个 FPGA 周期,必须确保 sample_tick 不会快到覆盖尚未发送的数据,或者在上层加入明确的缓冲/调度。本文没有输出背压,不能把发送器忙碌当成核心会自动等待。

继续阅读:饱和累加器。具体工程问题是:当目标允许随时反向、发送器又需要 24 个时钟发送一次数据时,你应怎样产生 sample_tick,才能同时满足每次数字限步和“不丢更新”的系统契约?

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进