ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FPGA ADC 码流窗口统计:码型转换、峰峰值与最后一个样本的自检实验

FPGA ADC 码流窗口统计:码型转换、峰峰值与最后一个样本的自检实验

ADC 采样数据到达 FPGA 后,常需要每收到 N 个样本就报告一次最小值、最大值、峰峰值以及端点码出现次数。这里的难点并不在比较器本身:offset binary 不能直接当二补码比较,最后一个样本不能漏算,下游停顿时也不能把结果覆盖。

本文实现一个独立的数字统计核心,统计窗口由N 次实际输入握手定义。气泡和背压会改变窗口持续时间,所以它不是固定时间长度的测量窗,也不是 ADC 引脚接收器。

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

2026-10-24,在实际核实的DESKTOP-396TDVB上使用已安装的 Vivado Simulator2022.1,SW Build3526262、IP Build3524634。时钟周期为 TB 设置的 10 ns,不是目标板已验证的工作频率。本次只做 RTL 仿真,综合、STA、上板均未执行。

主自检的 12 个参数实例全部通过,独立 reviewer 的 20 个实例也通过。主测试实际接受 5948 个样本,消费 2212 个窗口;这些窗口对应 5876 个样本,其余 72 个样本因复位丢弃。独立测试接受 22211 个样本,消费 6162 个窗口,另有 170 个复位丢弃样本。独立小位宽穷举的2812 个不同窗口都实际被消费,不是仅生成了输入向量。

TEST_PASS ADC_MAIN instances=12 TEST_PASS ADC_REVIEW cases=20 all_small_width_tuples_consumed

正常测试的 xvlog、xelab、xsim 退出码均为 0,且日志无 Fatal/ERROR。四个有意错误变体都成功编译、展开后触发功能断言;后文说明它们各自暴露的问题。结论来自当天新日志,不复用旧实验成绩。

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

$vivadoBin='D:\tools\Xilinx\Vivado\2022.1\bin'New-Item-ItemType Directory-Path'manual_adc_window_stats'-ErrorAction StopSet-Location'manual_adc_window_stats'&"$vivadoBin\xvlog.bat"--sv'../src/adc_window_stats.sv''../src/tb_adc_window_stats.sv'if($LASTEXITCODE-ne0){throw'xvlog failed'}&"$vivadoBin\xelab.bat"tb_adc_window_stats-s sim-debug typicalif($LASTEXITCODE-ne0){throw'xelab failed'}@' open_vcd trace.vcd log_vcd /tb_adc_window_stats/* 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. 先把码型变成同一种有符号整数

参数W>=1,输入原始位串in_code[W-1:0]。OFFSET_BINARY是编译参数,不能在一个窗口内动态切换:

OFFSET_BINARY=1:x = unsigned(in_code) - 2^(W-1) OFFSET_BINARY=0:按 W 位二补码解释 in_code L = -2^(W-1), H = 2^(W-1)-1

offset binary 与二补码的位串转换只需翻转 MSB。这个编码关系可在 ADI《Fundamentals of Sampled Data Systems》第 2 章、印刷页 2.9 的 编码章节 核对,本文于 2026-10-24 实际访问。这里使用的是整数码单位;实际电压换算仍取决于转换器量程、参考电压和具体传输函数。

例如 W=8:

规范化整数 xoffset binary 原码二补码原码
-1280080
-17FFF
08000
127FF7F

表中原码为十六进制。转换后比较对象必须声明为 signed,不能只在输入端做一次$signed,后面却又用无符号比较破坏顺序。

out_low_hits统计x==L的次数,out_high_hits统计x==H的次数。它们不是窗口最小值/最大值的出现次数。比如窗口[3,3,7,7]的 min/max 是 3/7,但在 W=8 时两个端点计数都为 0。端点码可能是正常量化结果,单凭这个计数不能判定模拟过载。

2. 接口契约与位宽

接口约定
W / N正整数;包括 W=1、N=1
clk / rst上升沿工作,高有效同步复位,优先级最高
in_valid / in_ready / in_code边沿前 valid&&ready 才接受样本;阻塞时上游保持 valid 和 code
out_valid / out_ready边沿前 valid&&ready 才消费结果
out_min / out_maxW 位有符号数
out_ppW 位无符号数,表示 max-min
out_low_hits / out_high_hitsC=$clog2(N+1)位,可表示 0…N

峰峰值最大为H-L=2^W-1,所以 W 位无符号足够;W 位有符号数却只能表示到2^(W-1)-1。RTL 先将两个极值分别符号扩展到 W+1 位,再相减,最后保留 W 位非负结果。例如 W=16、min=-32768、max=32767,峰峰值为 65535。

计数宽度必须能装下 N。N=4 时clog2(N)=2只能计到 3;这里用clog2(N+1)=3。N=1 则 C=1,不能产生零宽向量。

本设计只有一个结果槽。out_valid && !out_ready时,结果字段稳定,in_ready=0;消费旧结果的同一边沿允许接收下一窗口首样本。N=1 时每个样本立即形成结果,同拍消费旧结果并产生新结果时,out_valid 可以连续为 1。输出无效时字段保持上次值,不能只看总线值判断有新结果。

同步复位丢弃部分窗口和未消费结果,清空输出字段;已经消费的结果不能被撤销。虽然in_ready组合地被 rst 抑制,统计状态仍只在上升沿复位。

3. 用真实逐拍例子检查背压

下面取自本次 W=8、N=4、offset binary 的adc_w8_n4_ob1.csv。CSV 在上升沿后 1 ns 写出:输入握手信号保存的是边沿前值,结果字段是边沿后值。

时钟上升沿 / CSV 时刻 ns事件结果
35/36、45/46、55/56、65/66连续接受四个原码 00,即四个 -12865 ns 边沿后 valid=1,min=max=-128,pp=0,low_hits=4
75/76、85/86、95/96ready=0;下一样本的 valid/code 保持不接收新样本,所有结果字段保持
105/106消费旧结果,同时接受下一窗口首个 -128消费计数加 1,新窗口已有 1 个样本
115/116、125/126、135/136接受三个 127135 ns 后 min=-128、max=127、pp=255,端点次数 1/3
145/146消费第二个结果窗口消费计数为 2

图 1 按 CSV 采样点画阶梯连线,明确区分 pre/post 字段;它不是示波器截图,也不把两次采样之间的线段当作模拟电路波形。前 4000 ns 的 VCD 保存在本次运行目录,完整测试用 CSV 核查。

图 2 展示前 10 个已消费窗口。全低、全高窗口验证计数能达到 N;后面的定向窗口把极值放在最后一项,验证它确实被计入。图来自实际消费记录,不是只对预期数据作图。

4. 完整 RTL

保存为src/adc_window_stats.sv。代码块与最终实际编译文件逐字一致。

`timescale 1ns/1ps module adc_window_stats #( parameter integer W=8,N=4, parameter bit OFFSET_BINARY=1 )( input wire clk,rst,in_valid, output wire in_ready, input wire [W-1:0] in_code, output reg out_valid, input wire out_ready, output reg signed [W-1:0] out_min,out_max, output reg [W-1:0] out_pp, output reg [$clog2(N+1)-1:0] out_low_hits,out_high_hits ); localparam integer C=$clog2(N+1); localparam signed [W-1:0] LOW={1'b1,{(W-1){1'b0}}}; localparam signed [W-1:0] HIGH={1'b0,{(W-1){1'b1}}}; reg [C-1:0] count,low_hits,high_hits; reg signed [W-1:0] running_min,running_max; wire signed [W-1:0] sample=$signed(in_code ^ (OFFSET_BINARY ? LOW : {W{1'b0}})); wire signed [W-1:0] next_min=(count==0 || sample<running_min)?sample:running_min; wire signed [W-1:0] next_max=(count==0 || sample>running_max)?sample:running_max; wire [C-1:0] next_low=(count==0 ? {C{1'b0}} : low_hits)+(sample==LOW); wire [C-1:0] next_high=(count==0 ? {C{1'b0}} : high_hits)+(sample==HIGH); wire [W:0] span=$signed({next_max[W-1],next_max})-$signed({next_min[W-1],next_min}); assign in_ready=!rst && (!out_valid || out_ready); always @(posedge clk) begin if(rst) begin count<=0;low_hits<=0;high_hits<=0;running_min<=0;running_max<=0; out_valid<=0;out_min<=0;out_max<=0;out_pp<=0;out_low_hits<=0;out_high_hits<=0; end else begin if(out_valid && out_ready) out_valid<=0; if(in_valid && in_ready) begin running_min<=next_min;running_max<=next_max;low_hits<=next_low;high_hits<=next_high; if(count==N-1) begin count<=0;out_valid<=1; out_min<=next_min;out_max<=next_max;out_pp<=span[W-1:0]; out_low_hits<=next_low;out_high_hits<=next_high; end else count<=count+1'b1; end end end endmodule

next_min/next_max/next_low/next_high已包含当前输入样本,第 N 个样本到来时直接把这些 next 值锁存到输出。若写成out_min<=running_min,非阻塞赋值会取旧寄存器值,最后一项就被漏掉。

count==0表示当前样本是新窗口第一项,因此初始化极值为这个样本,而不是初始化成 0。否则全正窗口的 min 或全负窗口的 max 会被错误的初值污染。

5. 完整原始自检 TB

参考模型把接受的样本存入数组,窗口完成时遍历整个数组计算极值与端点计数。它不读取 DUT 内部状态,不调用 DUT 的组合函数。每拍检查 ready、valid、输出数据、未知态以及样本守恒:

accepted_samples = delivered_windows*N + discarded_samples + partial_samples + (pending_result ? N : 0)

主 TB 的小宽度穷举为 W1/N1、W2/N3、W3/N2,两种码型合计 260 个窗口。随机部分有输入气泡及样本变化,输出阻塞另有明确的 3 拍定向场景;更密集的随机背压由独立 TB 覆盖,不能仅因驱动里有随机 ready 就声称所有时序已覆盖。

`timescale 1ns/1ps module adc_case #(parameter W=3,N=3,OB=1)(output reg finished); localparam C=$clog2(N+1); reg clk=0,rst=1,in_valid=0,out_ready=0; reg [W-1:0] in_code=0; wire in_ready,out_valid; wire signed [W-1:0] out_min,out_max; wire [W-1:0] out_pp; wire [C-1:0] out_low_hits,out_high_hits; adc_window_stats #(.W(W),.N(N),.OFFSET_BINARY(OB)) dut(.*); always #5 if(!finished) clk=~clk; longint signed samples[0:N-1],lo,hi,emin=0,emax=0,x,raw_mask; longint unsigned epp=0; integer elow=0,ehigh=0,partial=0,accepted=0,delivered=0,discarded=0; integer cycles=0,stalls=0,duplex=0,fd,phase=0,k,j,p,total,word,temporary,reset_age; reg ev=0,fire,source_blocked=0; reg [W-1:0] saved_source; reg [31:0] seed=32'h2410a5c3; function automatic [31:0] rng; begin seed=seed^(seed<<13);seed=seed^(seed>>17);seed=seed^(seed<<5);rng=seed;end endfunction function automatic longint signed decode(input [W-1:0] v); longint signed z; begin z=$unsigned(v);decode=OB ? z-(64'sd1<<(W-1)) : (v[W-1] ? z-(64'sd1<<W) : z);end endfunction function automatic [W-1:0] encode(input longint signed v); begin encode=OB ? v+(64'sd1<<(W-1)) : v;end endfunction task automatic tick(input bit v,input [W-1:0] code,input bit ready,input bit reset); bit pre_ready,pre_valid; begin @(negedge clk);in_valid=v;in_code=code;out_ready=ready;rst=reset; @(posedge clk);cycles++;pre_ready=in_ready;pre_valid=out_valid; if(in_ready !== (!reset && (!ev || ready))) $fatal(1,"ADC_ASSERT ready W=%0d N=%0d OB=%0d phase=%0d",W,N,OB,phase); fire=v&&pre_ready; if(reset) begin discarded+=partial+(ev?N:0);partial=0;ev=0; emin=0;emax=0;epp=0;elow=0;ehigh=0;source_blocked=0; end else begin if(source_blocked && (!v || code!==saved_source)) $fatal(1,"ADC_STIMULUS_ASSERT blocked source changed"); if(ev && !ready) stalls++; if(ev && ready) begin ev=0;delivered++;if(fire)duplex++;end if(fire) begin samples[partial]=decode(code);partial++;accepted++; if(partial==N) begin emin=samples[0];emax=samples[0];elow=0;ehigh=0; for(integer a=0;a<N;a++) begin if(samples[a]<emin)emin=samples[a]; if(samples[a]>emax)emax=samples[a]; if(samples[a]==lo)elow++; if(samples[a]==hi)ehigh++; end epp=emax-emin;partial=0;ev=1; end end source_blocked=v&&!pre_ready;saved_source=code; end #1; if($isunknown({out_valid,out_min,out_max,out_pp,out_low_hits,out_high_hits}) || out_valid!==ev || $signed(out_min)!=emin || $signed(out_max)!=emax || $unsigned(out_pp)!=epp || out_low_hits!=elow || out_high_hits!=ehigh) $fatal(1,"ADC_ASSERT W=%0d N=%0d OB=%0d phase=%0d valid=%b/%b min=%0d/%0d max=%0d/%0d pp=%0d/%0d hits=%0d,%0d/%0d,%0d",W,N,OB,phase,out_valid,ev,out_min,emin,out_max,emax,out_pp,epp,out_low_hits,out_high_hits,elow,ehigh); if(accepted!=delivered*N+discarded+partial+(ev?N:0)) $fatal(1,"ADC_ASSERT conservation"); $fdisplay(fd,"%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d", $time,W,N,OB,phase,reset,v,pre_ready,$unsigned(code),pre_valid,ready,out_valid,$signed(out_min),$signed(out_max),$unsigned(out_pp),out_low_hits,out_high_hits,accepted,delivered,discarded,partial,cycles); end endtask task automatic push(input [W-1:0] code); begin fire=0; while(!fire)tick(1,code,(rng()%4)!=0,0); end endtask task automatic drain; begin while(ev)tick(0,0,1,0);end endtask initial begin finished=0;lo=-(64'sd1<<(W-1));hi=(64'sd1<<(W-1))-1;raw_mask=(64'sd1<<W)-1; fd=$fopen($sformatf("adc_w%0d_n%0d_ob%0d.csv",W,N,OB),"w"); $fdisplay(fd,"time_ns,w,n,ob,phase,rst,in_valid,in_ready,in_code,out_valid_pre,out_ready,out_valid_post,out_min,out_max,out_pp,low_hits,high_hits,accepted,delivered,discarded,partial,cycles"); tick(0,0,0,1);tick(0,0,1,0); phase=1; for(p=0;p<5;p++) begin for(k=0;k<N;k++) begin case(p) 0:x=lo;1:x=hi;2:x=(k==N-1)?hi:0; 3:x=(k==N-1)?lo:0;default:x=(k%2)?hi:lo; endcase push(encode(x)); end repeat(3)tick(1,encode(lo),0,0); tick(1,encode(lo),1,0); for(k=1;k<N;k++)push(encode(hi)); drain(); end phase=2; if(W*N<=9) begin total=1<<(W*N); for(word=0;word<total;word++) begin temporary=word; for(k=0;k<N;k++) begin if((word+k)%3==0)tick(0,rng(),1,0); push(temporary & raw_mask);temporary=temporary>>W; end drain(); end end phase=3; for(j=0;j<150;j++) begin for(k=0;k<N;k++) begin if(rng()%3==0)tick(0,rng(),1,0); push(rng()); end drain(); end phase=4; for(reset_age=1;reset_age<=N;reset_age++) begin for(k=0;k<reset_age;k++)push(encode(k%2?hi:lo)); tick(1,encode(hi),1,1);tick(0,0,1,0); for(k=0;k<N;k++)push(encode(hi)); drain(); end phase=5;tick(0,0,1,0); if(partial || ev || accepted!=delivered*N+discarded || stalls<15 || duplex<5) $fatal(1,"ADC_ASSERT final coverage"); $display("CASE_PASS ADC W=%0d N=%0d OB=%0d accepted=%0d delivered=%0d discarded=%0d stalls=%0d duplex=%0d cycles=%0d",W,N,OB,accepted,delivered,discarded,stalls,duplex,cycles); $fclose(fd);finished=1; end endmodule module tb_adc_window_stats; wire [11:0] done; adc_case #(1,1,0) a0(done[0]);adc_case #(1,1,1) a1(done[1]); adc_case #(2,3,0) a2(done[2]);adc_case #(2,3,1) a3(done[3]); adc_case #(3,2,0) a4(done[4]);adc_case #(3,2,1) a5(done[5]); adc_case #(8,4,0) a6(done[6]);adc_case #(8,4,1) a7(done[7]); adc_case #(16,5,0) a8(done[8]);adc_case #(16,5,1) a9(done[9]); adc_case #(16,1,0) a10(done[10]);adc_case #(16,1,1) a11(done[11]); initial begin wait(&done);$display("TEST_PASS ADC_MAIN instances=12");$finish;end initial begin #1000000;$fatal(1,"ADC_WATCHDOG");end endmodule

主测试各实例实测计数如下,表由最终 CSV 重放结果生成:

WNoffset binary周期接受样本消费窗口丢弃样本穷举消费窗口
11039416416312
11139416416312
2301146687227664
2311146687227664
320847455226364
321847455226364
8401049666164100
8411049666164100
161038916216110
161138916216110
16501275840165150
16511275840165150

6. 独立复核与故意错误

独立 reviewer 在编写自己的 TB 时只读接口契约,尚未读取主 RTL/TB;完成后再审代码。其整数参考和驱动单独编写,覆盖 W1/W2、N1/3/4/5、两种码型的全部输入元组,另测 W8/N5、W16/N3。每个穷举窗口必须看到真实输出握手,才算完成一项。

WNoffset binary周期接受样本消费窗口丢弃样本穷举消费窗口
11063536336212
11163534734612
13069547415668
13169545915168
1407645221281016
1417635621381016
1509016851341532
1518986751321532
21063837036914
21163735235114
230917645213664
231919639211664
2401962150237310256
2411964149037010256
250685156251122151024
251685256251122151024
85070649596150
85170649095150
163066145014860
163166244114560

独立运行共 29461 个实例观察边沿,记录 1649 个结果阻塞边沿、2568 次同时输入/输出握手,其中 N=1 替换结果 1126 次;600 周期随机流在每个实例都执行,实际随机气泡 2648 次、随机输入受阻 1275 次。输入受阻指 in_valid&&!in_ready,不能与 out_valid&&!out_ready 的结果阻塞计数混用。每个部分窗口长度和 pending 结果状态都执行复位,并核实丢弃计数。以上统计不是功能覆盖率百分比,也不是器件性能指标。

四个变体均从最终正常 RTL 单点修改,保持接口和测试不变:

故意错误首个实际失败
两种码型转换条件反转W2/N1 的最低码被解成 0,预期 -2,端点计数也错误
min 比较改成 unsignedW16/N3 最后一个 -32768 没有成为最小值,pp 错成 0,预期 65535
输出锁存旧 running_min/maxW2/N1 第一个样本就漏算,极值仍为旧 0
无视 out_ready 清 out_valid阻塞期间 valid 消失,且错误重新打开输入 ready

这四项都由ADC_REVIEW_ASSERT触发功能 Fatal,不是编译或展开失败。XSim 在这些故障运行中返回进程码 0,所以必须检查断言和通过标识。一个变体可能打印多个同一时刻的失败,仍只计一个被捕获变体,不能累计成多轮测试。

测试先行阶段还保留过一次真实工具问题:未实现占位模块缺少 timescale,xelab 拒绝展开;另将 TB 的端口初始化移入 initial 块消除了编译警告。修正后,主 TB 对未实现核心实际触发功能断言。该起点不是正常 RTL,也没有被算进四个最终负例。正常 RTL 在本次范围内未发现功能缺陷。

7. 集成时必须守住的边界

这里的背压只对可暂停的数字上游成立。真实连续采样 ADC 往往不会因为in_ready=0就暂停物理采样;必须在前面接缓存、记录丢样,或者证明系统吞吐和最大停顿预算。若采样已经丢失,统计结果也只描述实际接受的 N 个样本,不能冒充连续时域窗口。

本模块没有测量模拟电压、噪声或失真,也不能凭端点码计数保证发现削顶。码型、对齐、有效位宽和采样接口仍需核对具体 ADC 数据手册。本文不绑定特定转换器,不包含厂商 IP,也没有宣称综合或上板通过。

继续阅读:FPGA 位宽与符号扩展。一个可直接用于设计评审的问题是:若 ADC 每拍都产生数据,而下游最长会停顿 20 拍,你的前级缓存深度和丢样计数应如何确定,才能让每个统计窗口的样本来源可追踪?

返回列表