流水线 valid 对齐自检:输入气泡和中途复位怎么测

流水线 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 后,在这个目录中执行:

text 复制代码
xvlog -sv ../02_pipeline/rtl.sv ../02_pipeline/tb.sv
xelab work.tb -s sim -debug typical
xsim sim -tclbatch simulate.tcl

同目录的 simulate.tcl:

tcl 复制代码
open_vcd trace.vcd
log_vcd /tb/*
run all
close_vcd
quit

上述执行链与实测一致,源码路径改成了便于复制的相对路径。三个工具实际报告的版本都是 Vivado Simulator v2022.1;本次没有验证 Linux 环境。

预期有三条:有效输入在约定边沿输出;输入气泡也按同样的距离传到输出;reset 取消全部未完成事务,之后不泄漏旧 valid。原版实际日志为:

text 复制代码
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_pipe0=1
n+1 d1=(x+1) mod 256,valid_pipe1=1
n+2 d2=3(x+1) mod 256,valid_pipe2=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 链每个非复位周期都更新:

systemverilog 复制代码
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_ 宏是故意保留的错误注入开关;正常编译不定义它们。

systemverilog 复制代码
`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。到期时应当有输出,其他周期不应冒出有效事务。

复位到来时,尚未到期或尚未交付的队列项全部取消。最终应满足:

text 复制代码
accepted = delivered + flushed
pending = 0

保存为 02_pipeline/tb.sv:

systemverilog 复制代码
`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],比正确输出提前一级:

text 复制代码
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

实际捕获结果:

text 复制代码
Fatal: CHECK_FAIL PIPE valid cycle=7 bubble drains pipeline got=1 expected=0 pending=1

cycle 5 的输入应在 cycle 8 才有效,cycle 7 提前冒出的 valid 被抓住。这个用例直接检查时间关系,不是仅比较算术结果。

第二种故意在 rst && in_valid 时继续移位 valid,而没有完整清空:

text 复制代码
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

实际捕获结果:

text 复制代码
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 捕获。

相关阅读

相关推荐
星夜离尘561 天前
FPGA实现CRC-8/SMBUS:从参数和位序核对到1041次自检
fpga开发·verilog·fpga·crc
星夜离尘561 天前
FPGA正交编码器非法跳变检测:把16种状态转移和采样边界测清楚
verilog·fpga·编码器
FPGA工程笔记3 天前
从1024点滑动平均到EMA:双路5GS/s ADC Offset校准的FPGA工程实现
fpga·adc·ema·rfsoc·tiadc·offset校准·滑动平均
pengdott3 天前
Rocky linux9安装Jenkins最新版本2.585
运维·jenkins·流水线
星夜离尘563 天前
Testbench不只看波形:用scoreboard抓住丢包、重复和乱序
fpga开发·verilog·testbench·scoreboard
pengdott4 天前
Jenkins最新版本2.585修改插件源的方法
jenkins·流水线
NamoAmitabha1284 天前
xilinx/MMCM/DRP/使用
fpga·xilinx·mmcm
北城笑笑5 天前
FPGA 55 ,FPGA 行业 2026 发展分析与报告
fpga开发·fpga
ChipCamp7 天前
零门槛上手的RISC-V项目:nerv(附Verilog-vs-SysVerilog)
risc-v·systemverilog·interface·nerv·modport