流水线 valid 对齐自检:输入气泡和中途复位怎么测
连续输入时,每拍都有结果,valid 即使提前一拍也可能不容易从几行打印中看出来。把输入改成孤立脉冲和气泡,再在事务还没走完时复位,问题就清楚得多。
本文沿着已有的 3 级乘法器文章继续做验证,但换成一个便于手算的四级算术流水线:8 位无符号输入,计算y=(3x+5) mod 256。重点是逐拍对应关系、自检参考模型和 reset 取消在途事务的边界,不比较 Fmax,也不声称使用了 DSP 或完成上板。
1 运行环境与应该看到的结果
本次实际运行于 Windows、Vivado Simulator 2022.1,使用 SystemVerilog。时钟周期 10 ns,复位同步高有效;模块没有 ready、背压或全局 clock enable,每个非复位上升沿都会推进。
把第 4、5 节的代码保存为02_pipeline/rtl.sv与02_pipeline/tb.sv,新建与02_pipeline同级的空目录run_pipeline。工具加入 PATH 后,在这个目录中执行:
xvlog -sv ../02_pipeline/rtl.sv ../02_pipeline/tb.sv xelab work.tb -s sim -debug typical xsim sim -tclbatch simulate.tcl同目录的simulate.tcl:
open_vcd trace.vcd log_vcd /tb/* run all close_vcd quit上述执行链与实测一致,源码路径改成了便于复制的相对路径。三个工具实际报告的版本都是Vivado Simulator v2022.1;本次没有验证 Linux 环境。
预期有三条:有效输入在约定边沿输出;输入气泡也按同样的距离传到输出;reset 取消全部未完成事务,之后不泄漏旧 valid。原版实际日志为:
TEST_PASS PIPE cycles=599 assertions=1025 accepted=423 delivered=419 flushed=4 resets=6 random_valid=132 latency=3这里latency=3统计的是“接受输入的边沿”到“输出更新的边沿”之间的三个完整周期。RTL 有四级寄存器,下面把计数过程展开,避免把日志数字误读成三级结构。
2 四级寄存器,为何日志里写 latency=3
结构很简单:d0接收输入,d1加 1,d2乘 3,out_data加 2。每一级都是 8 位,赋值时截断;最终与(3x+5) mod 256等价,不是饱和运算。
设某笔输入在第 n 个上升沿被接受:
| 上升沿之后,非阻塞赋值已完成 | 这笔输入所在位置 |
|---|---|
| n | d0=x,valid_pipe[0]=1 |
| n+1 | d1=(x+1) mod 256,valid_pipe[1]=1 |
| n+2 | d2=3(x+1) mod 256,valid_pipe[2]=1 |
| n+3 | out_data=(3x+5) mod 256,out_valid=1 |
因此,接受边沿与输出更新边沿之间隔了三个完整周期,经过四个寄存级。TB 在每个上升沿后 1 ns 读取结果,检查的就是表里的“沿后状态”。
如果下游也是always_ff @(posedge clk),它在 n+3 这个沿采到的仍是更新前的输出,要到 n+4 沿才能采到这笔结果。讨论延迟时,应先说清观察位置,不能把“沿后可见”和“另一个寄存器沿上采样”混用。
图 1:从本次pipeline_trace.csv重绘的逐拍快照,每列取上升沿后 1 ns。灰色数据表示没有有效事务;这不是 Vivado GUI 截图。
实际记录中,cycle 5 接受00,cycle 8 输出05;cycle 10 接受FF,cycle 13 输出02。后一个结果也验证了模 256 的溢出约定:3×255+5=770,低 8 位为02。
3 气泡是空槽,流水线仍然前进
这个接口只在rst=0 && in_valid=1的上升沿接受一笔输入。in_valid=0表示当前没有新事务,但前面已经接受的事务仍要继续前移。
所以 valid 链每个非复位周期都更新:
valid_pipe <= {valid_pipe[2:0], in_valid};如果把整条链放进if(in_valid)里面,孤立输入后面一旦都是气泡,那笔输入的 valid 就会卡在中途。本次“单笔输入后连续四个空拍”的用例正是为了让这种问题显现。
数据寄存器也会在气泡期间计算。图中某些out_valid=0的周期,out_data仍在变化,这是当前实现允许的行为。下游只能在out_valid=1时把它当作结果;TB 不要求无效输出必须是 0 或保持不变。
但 valid 本身不能跳过检查。如果只在out_valid=1时比较数据,多出来的输出、丢失的输出或提前一拍的有效脉冲,都可能漏测。因此下面的 TB 每一拍都先检查out_valid,再决定是否检查数据。
4 完整 RTL
保存为02_pipeline/rtl.sv。两个MUT_宏是故意保留的错误注入开关;正常编译不定义它们。
`timescale 1ns/1ps // Four register banks; accept at edge n, output at n+3 (3 full periods). // No backpressure: in_valid=0 inserts a bubble, not a clock stall. module fixed_pipeline(input logic clk,rst,in_valid, input logic [7:0] in_data,output logic out_valid, output logic [7:0] out_data); logic [7:0] d0,d1,d2; logic [3:0] valid_pipe; `ifdef MUT_VALID_EARLY assign out_valid=valid_pipe[2]; // DELIBERATE one-edge misalignment. `else assign out_valid=valid_pipe[3]; `endif always_ff @(posedge clk) begin if(rst) begin d0<=0; d1<=0; d2<=0; out_data<=0; `ifdef MUT_RESET_LEAK // DELIBERATE bug: reset with in_valid=1 fails to flush pending valids. if(in_valid) valid_pipe<={valid_pipe[2:0],1'b0}; else valid_pipe<=0; `else valid_pipe<=0; `endif end else begin d0<=in_data; d1<=d0+8'd1; d2<=d1*8'd3; out_data<=d2+8'd2; valid_pipe<={valid_pipe[2:0],in_valid}; end end endmodule复位优先于输入。复位沿会清空数据和全部 valid,不接收同沿输入,也不兑现本来在该沿到期的结果。这里定义的是“取消在途事务”,没有保存、重放或补发机制。
5 用接受日志建立参考模型
参考模型不再复制一条与 DUT 一模一样的 valid 移位链,而是记账:每接受一笔数据,就直接计算数学结果,并记录它的绝对到期周期cycle+3。到期时应当有输出,其他周期不应冒出有效事务。
复位到来时,尚未到期或尚未交付的队列项全部取消。最终应满足:
accepted = delivered + flushed pending = 0保存为02_pipeline/tb.sv:
`timescale 1ns/1ps module tb; logic clk=0,rst=1,in_valid=0; logic [7:0] in_data=0; wire out_valid; wire [7:0] out_data; // Independent acceptance log: absolute deadlines, no DUT-like shift model. integer due[0:1023]; logic [7:0] expected[0:1023]; integer head=0,tail=0,cycle=0,assertions=0,accepted=0,delivered=0,flushed=0; integer resets=0,trace_fd,random_valid=0; logic [31:0] rng=32'h13579bdf; always #5 clk=~clk; fixed_pipeline dut(.*); task automatic step(input bit reset_i,valid_i,input logic [7:0] value_i,input string label_i); bit expected_valid; logic [7:0] expected_data; @(negedge clk); rst=reset_i; in_valid=valid_i; in_data=value_i; cycle++; expected_valid=0; expected_data=0; if(reset_i) begin flushed+=tail-head; head=tail; resets++; end else begin if(valid_i) begin due[tail]=cycle+3; expected[tail]=(3*int'(value_i)+5)%256; tail++; accepted++; end if(head<tail && due[head]==cycle) begin expected_valid=1; expected_data=expected[head]; head++; delivered++; end end @(posedge clk); #1; assertions++; if(out_valid!==expected_valid) $fatal(1,"CHECK_FAIL PIPE valid cycle=%0d %s got=%b expected=%b pending=%0d", cycle,label_i,out_valid,expected_valid,tail-head); if(expected_valid || reset_i) begin assertions++; if(out_data!==expected_data) $fatal(1,"CHECK_FAIL PIPE data cycle=%0d %s got=%02h expected=%02h", cycle,label_i,out_data,expected_data); end // Data is intentionally don't-care when invalid and not in reset. $fdisplay(trace_fd,"%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%s", cycle,rst,in_valid,in_data,out_valid,out_data,expected_valid,expected_data,tail-head,label_i); endtask task automatic drain; repeat(4) step(0,0,8'he7,"bubble drains pipeline"); endtask initial begin trace_fd=$fopen("pipeline_trace.csv","w"); if(!trace_fd) $fatal(1,"CHECK_FAIL PIPE trace open"); $fdisplay(trace_fd,"cycle,rst,in_valid,in_data,out_valid,out_data,expected_valid,expected_data,pending,scenario"); repeat(4) step(1,0,0,"startup synchronous reset"); step(0,1,0,"isolated zero"); drain(); step(0,1,255,"isolated overflow FF"); drain(); for(int i=0;i<16;i++) step(0,1,i*17,"continuous values"); drain(); for(int i=0;i<20;i++) step(0,(i%2)==0,8'h80+i,"alternating valid and bubbles"); drain(); // One pending transaction: leak appears AFTER reset, not necessarily on it. step(0,1,8'h42,"pending transaction to cancel"); step(1,1,8'hff,"midstream reset ignores valid input"); drain(); for(int i=0;i<5;i++) step(0,1,8'h20+i,"fill before reset"); step(1,0,0,"reset on a due output cancels all pending"); drain(); step(0,1,8'h7f,"resume immediately after reset drain"); drain(); for(int i=0;i<256;i++) begin // Deterministic xorshift32, independent of simulator random seed. rng=rng^(rng<<13); rng=rng^(rng>>17); rng=rng^(rng<<5); random_valid+=rng[0]; step(0,rng[0],rng[15:8],"fixed-seed random valid"); end drain(); // All arithmetic inputs at full throughput, covers modular overflow. for(int i=0;i<256;i++) step(0,1,i,"all 256 arithmetic inputs"); drain(); assertions++; if(head!=tail || accepted!=delivered+flushed) $fatal(1,"CHECK_FAIL PIPE accounting accepted=%0d delivered=%0d flushed=%0d pending=%0d", accepted,delivered,flushed,tail-head); $fclose(trace_fd); $display("TEST_PASS PIPE cycles=%0d assertions=%0d accepted=%0d delivered=%0d flushed=%0d resets=%0d random_valid=%0d latency=3", cycle,assertions,accepted,delivered,flushed,resets,random_valid); $finish; end initial begin #20000; $fatal(1,"WATCHDOG PIPE"); end endmodule这里的检查是过程式比较与$fatal,没有依赖 SVA 覆盖数据库。!==会把意外 X/Z 也判为不一致;#1用于在当前上升沿的非阻塞赋值更新后观察 DUT,不能搬进可综合逻辑当作延迟实现。
测试安排包含孤立输入、连续输入、交替 valid、连续四个空拍、固定种子的 256 周期 xorshift 输入,以及全部 256 个算术输入值。固定种子随机段实际有 132 个有效输入,所以再次运行能对照相同的计数和轨迹。
原版 1025 次检查由 599 次 valid 检查、419 次有效数据检查、6 次复位数据检查和 1 次最终记账检查组成。全部 256 个输入值都测过,不代表所有可能的控制时序都穷举过;这是有限的定向与确定性伪随机测试。
6 两次中途复位分别取消了什么
图 2:本次 CSV 的两段记录。红列为复位沿,蓝格为接受输入,绿格为有效输出;pending是 TB 完成当前周期处理后的待交付数量。
第一次复位专门覆盖“rst 与 in_valid 同时为 1”:
- cycle 59 接受
42,按原计划应在 cycle 62 更新输出 - cycle 60 复位,同时输入端给出
in_valid=1、in_data=FF 42被取消,FF不被接受;cycle 62 的out_valid必须为 0
只检查复位当拍还不够。错误 valid 可能先残留在中间级,复位释放两拍后才走到输出。因此 TB 在复位之后继续送空拍,检查旧事务有没有“回来”。
第二次复位发生在连续输入期间:cycle 65~69 接受20、21、22、23、24,其中前两笔已在 cycle 68、69 输出。cycle 70 复位时,队列还有三笔,原本分别在 cycle 70、71、72 到期;本次约定把这三笔全部取消,包括刚好在复位沿到期的那一笔。
复位结束后,cycle 75 接受7F,cycle 78 输出82,固定延迟没有改变。两次中途复位合计取消四笔,最终423=419+4。日志中的resets=6是六个被检查的复位周期,其中四个在启动阶段,两个在中途,不是六次中途故障。
这里的 delivered 统计的是 TB 在沿后观察到的有效输出。它没有模拟下游寄存器的握手或采样反馈,也没有ready;不能把这个计数扩大成带背压系统中的交付证明。
7 两个错误注入,验证自检是否敏感
两个故障变体都使用同一份 TB,不修改期望。各自在新的空目录内编译、展开和运行,避免宏定义或旧快照混用。
第一种故意把out_valid接到valid_pipe[2],比正确输出提前一级:
xvlog -sv -d MUT_VALID_EARLY ../02_pipeline/rtl.sv ../02_pipeline/tb.sv xelab work.tb -s sim -debug typical xsim sim -tclbatch simulate.tcl实际捕获结果:
Fatal: CHECK_FAIL PIPE valid cycle=7 bubble drains pipeline got=1 expected=0 pending=1cycle 5 的输入应在 cycle 8 才有效,cycle 7 提前冒出的 valid 被抓住。这个用例直接检查时间关系,不是仅比较算术结果。
第二种故意在rst && in_valid时继续移位 valid,而没有完整清空:
xvlog -sv -d MUT_RESET_LEAK ../02_pipeline/rtl.sv ../02_pipeline/tb.sv xelab work.tb -s sim -debug typical xsim sim -tclbatch simulate.tcl实际捕获结果:
Fatal: CHECK_FAIL PIPE valid cycle=62 bubble drains pipeline got=1 expected=0 pending=0这正好对应 cycle 59 接受、cycle 60 本应取消的事务。到 cycle 62,参考队列已空,故障实现却重新输出有效脉冲。
两个变体都成功编译、展开,然后由功能断言抓到指定错误;它们不是原版运行失败。需要注意,本机 xsim 在这两个$fatal之后都返回了退出码 0,所以运行脚本还检查CHECK_FAIL与TEST_PASS。不能仅用进程是否返回非零来判断这种自检仿真的成败,也不能把 watchdog 当作成功抓到设计错误。
8 这组检查可以复用到哪里
这套“接受时记到期周期”的方法适合无背压、无停顿、固定边沿延迟的模块。替换算式时,应同步调整参考计算的位宽、有符号性、溢出约定和到期周期,再保留每拍 valid 检查与复位记账。
如果以后加入全局 CE、ready/valid 背压或弹性缓冲,事务可能停在某一级,按绝对时钟周期cycle+3判定就不再成立,需要按推进条件或握手重新建模。不能只给现有代码加一个if(ready),就继续沿用本次固定延迟结论。
本次没有综合、DSP 资源报告、时序分析或板级测试;10 ns 是仿真激励的时钟周期,不是对器件 100 MHz 时序收敛的证明。仿真已经回答的是:在这 599 个检查周期里,气泡按约定传播,中途 reset 取消了四笔在途事务,两个故意错位的实现也确实被同一份 TB 捕获。
相关阅读
- 流水线设计 Pipeline:提升时序性能的核心技巧(附3级流水线乘法器实操):前置的流水线与数据对齐内容;本文新增控制时序自检
- AMD UG900 2022.1:Running SystemVerilog in Standalone or prj Mode:
xvlog -sv与独立运行方式 - AMD UG900 2022.1:Parsing Design Files, xvhdl and xvlog:编译工具与命令说明
- AMD UG901 2022.1:综合与 HDL 编码参考;功能仿真与实际资源映射应分别验证