Altifigence Academy

58 / 63 · 概念

FIFO、存储器验证计划与独立 scoreboard

编写不依赖实现指针的 queue scoreboard,将边界、reset、valid 和未初始化读取纳入验证计划。

生成参考答案的模型应不同于 DUT

验证器将约定的预期值与 DUT 比较。若复制 FIFOFIFO 先存入的数据先取出的队列。满、空以及同时读写的允许规则,都是行为约定的一部分。了解更多内部的 rptr、wptr 来计算预期值,可能重现同样的指针错误。独立 scoreboardscoreboard 验证环境中按顺序对照预期数据与实际输出的参考状态。应独立计算,而非直接复制实现内部状态。用 SystemVerilog queue 只记录逻辑顺序:接受 push 就在末尾加入,接受 pop 就从头部取出。

验证计划应列出状态与请求的交叉组合:在空、中间、满三种状态下,分别检查无请求、仅 push、仅 pop、同时请求,再加入重复回绕、连续 pop 和仍有数据时的 reset。记录各情况是否被观察到,并保存随机种子与失败输入以便复现。

预期接受条件应依据 queue 的旧长度 NN 计算,而非 DUT 的 full 或 occupancy。

q=pop∧(N>0)p=push∧((N<4)∨q)N+=N+p−q\begin{aligned} q&=\mathrm{pop}\land(N>0)\\ p&=\mathrm{push}\land((N<4)\lor q)\\ N^{+}&=N+p-q \end{aligned}

先弹出旧 head,再追加新数据,才能在满状态同时请求时保留正确旧 head。只有预期 q=1q=1 时,才比较 dout。

完整测试平台

以下是与前课 fifo4 一起编译的完整测试平台,属于使用延迟与 queue 的验证代码。输入在下降沿改变,并在上升沿后 1ps 比较 DUT 的非阻塞非阻塞赋值 时序 RTL 中以 <= 表示的赋值。先计算右侧,再预约更新,使同一边沿的各寄存器能够依据旧状态计算。了解更多更新结果。

SystemVerilog
module tb_fifo4;
  timeunit 1ns;
  timeprecision 1ps;
  typedef logic [7:0] word_t;
  logic clk=0, rst=1, push=0, pop=0;
  word_t din=0, dout;
  logic dout_valid, full, empty;
  logic [2:0] occupancy;
  word_t model[$], expected;
  bit expect_pop, expect_push;
  fifo4 dut (.*);
  always #5ns clk = ~clk;

  always @(posedge clk) begin
    if (rst) begin
      model.delete();
      expect_pop = 0;
    end else begin
      expect_pop = pop && (model.size() != 0);
      expect_push = push && ((model.size() < 4) || expect_pop);
      if (expect_pop) expected = model.pop_front();
      if (expect_push) model.push_back(din);
    end
    #1ps;
    assert (dout_valid === expect_pop)
      else $fatal(1, "valid mismatch");
    if (expect_pop)
      assert (dout === expected)
        else $fatal(1, "data mismatch");
    assert (int'(occupancy) === model.size())
      else $fatal(1, "count mismatch");
    assert (empty === (model.size() == 0))
      else $fatal(1, "empty mismatch");
    assert (full === (model.size() == 4))
      else $fatal(1, "full mismatch");
  end

  task automatic step(input bit r, p, o, input word_t d);
    @(negedge clk);
    rst=r; push=p; pop=o; din=d;
  endtask

  initial begin
    step(0,0,1,0);
    step(0,1,1,8'hA0);
    for (int i=1; i<4; i++)
      step(0,1,0,word_t'(160+i));
    step(0,1,0,8'hEE);
    step(0,1,1,8'hB0);
    repeat (5) step(0,0,1,0);
    repeat (3) begin
      for (int i=0; i<4; i++) step(0,1,0,word_t'(i));
      repeat (4) step(0,0,1,0);
    end
    step(0,1,0,8'h5A);
    step(1,1,1,8'hFF);
    step(0,0,1,0);
    repeat (100)
      step(0,bit'($urandom_range(1,0)),bit'($urandom_range(1,0)),
           word_t'($urandom));
    repeat (4) step(0,0,1,0);
    @(negedge clk);
    $display("PASS");
    $finish;
  end
endmodule

第一个上升沿 rst=1,因此 DUT 与模型都被清空。=== 连同 X 一起严格比较。valid=0 时不比较未初始化的 dout,但 valid 本身若为 X 就失败。最后四次 pop 检查所有剩余数据。PASS 是实际运行测试且所有 assertion 通过时输出的消息,并不表示此处展示的代码已经运行过。

在加入写数据 B0 之前,先取出旧 head A0 作为预期输出。这比较的是数据逻辑顺序,而非指针或物理槽位。

满状态同时请求时 scoreboard 的处理顺序
  1. 原队列: [A0, A1, A2, A3]
  2. pop 预期值: 保存旧 head A0
  3. 执行 push 更新: [A1, A2, A3, B0]
  4. 比较响应: valid=1 时 dout=A0

用边界例子检查 scoreboard

queue=[A0,A1,A2,A3] 时,若只有 push,请求数据 EE 因长度已为 4 而被拒绝。下一次同时请求,应先保存 expected=A0,再把 B0 加到末尾,得到 [A1,A2,A3,B0]。若 DUT 立即输出新 B0,数据断言必须失败。空队列同时请求则 expect_pop=0、expect_push=1,只存入一项,不比较输出。reset 期间忽略两个请求,清空 queue,并检查 valid=0。

存储器验证也使用同一原则:设置逐地址参考数组与 initialized 表;read-first 在更新参考写入之前取得读取预期值。寄存器寄存器 存储多位状态的电路。本课程中的同步寄存器在时钟边沿存储指定输入。了解更多文件旁路在冲突时选择 wr_data,然后再把写入反映到参考数组。测试前提应说明是禁止读取未初始化地址,还是允许未定义响应,不能任意把 0 当作正确答案。按地址访问的存储器需要独立验证器。

分别注入“满状态同时 pop 时输出新值”“同时接受时递增数量”“空队列 pop 时拉高 valid”等缺陷,对应检查都应失败。通过这种审查与各边界覆盖率,发现测试本身的遗漏。RAM 资源布局与时序则需另行通过综合及时序验证确认。

自己试试

某错误 scoreboard 先将 push 请求加入 queue,再根据更新后的长度判断是否允许 pop。空队列同时 push/pop 时会误判什么?满队列同时请求时,应何时保存预期数据?再评价“每次 reset 后都检查 dout=0”的规则。

阅读解释

它会在空队列同时请求时,误以为刚加入的条目可立即弹出,从而得到 expect_pop=1。但约定要求依据边沿前长度决定接受,只能接受 push,输出 valid 应为 0。满队列时,应先把旧 head 保存为 expected 并弹出,再将新数据追加到末尾。reset 不将 dout 清零,因此只检查 valid=0,以及 queue、occupancy 已清空。数据只在 expect_pop=1 时比较,但还必须独立比较 DUT valid,才能发现 valid 永远为 0 的错误。

你的选择适用于此浏览器,可随时在页脚更改。