本章目录 37 节

← 返回课程首页

第 20 章 仿真、测试平台与基础验证

1. 本章要解决的问题

第 19 章把组合逻辑、寄存器、存储器和 FSM 写成了 SystemVerilog。代码能够编译,只说明语法和基本连接被工具接受;它还没有回答以下问题:

仿真(simulation)让一个软件模型按照 HDL 的事件和时间规则运行。测试平台(testbench)负责产生输入、观察输出并判断结果。验证(verification)则是一套更完整的工作:从规格提取要检查的行为,设计测试,自动比较结果,记录覆盖情况并定位失败原因。

本章回答十个问题:

  1. 仿真器怎样在没有真实晶体管的情况下推进时间?
  2. 同一个仿真时刻为什么还会有多个零时间调度步骤?
  3. DUT、激励、监视器、参考模型和检查器怎样协作?
  4. 怎样稳定地产生时钟、复位和同步输入?
  5. 怎样避免在时钟边沿制造测试平台竞争?
  6. 怎样把“看波形”升级为自动报告通过或失败?
  7. 组合逻辑、寄存器、计数器和存储器分别怎样测试?
  8. 即时断言和简单并发断言能检查什么?
  9. 定向、边界、穷举和随机测试怎样配合?
  10. 怎样用覆盖记录与失败复现判断测试是否充分?

本章使用基础 SystemVerilog 测试平台语法。UVM、约束随机、复杂覆盖模型和形式验证属于进阶验证体系,不进入本课程主线。

2. 与第 19 章的联系

第 19 章区分了可综合设计代码和测试平台代码:

这两个层次必须分开。设计模块回答“芯片应实现什么”;测试平台回答“给它什么输入、何时观察、怎样判定”。测试平台发现的是模型相对规格的矛盾。RTL 仿真通过后,仍要在第 21 章检查综合结构与时序约束,在第 22 章理解物理实现影响。

3. 前置知识快速检查

  1. always_comb 中一个输入变化后,输出是否需要等待时钟边沿?
  2. always_ff @(posedge clk) 在什么事件更新状态?
  3. 非阻塞赋值右侧读取边沿前还是边沿后的值?
  4. x 在四状态仿真中表示什么?
  5. 模 N 计数器从 N-1 回到 0 时,边界错误最常发生在哪两个相邻状态?

答案是:不需要;clk 的上升沿;读取边沿前状态;未知;N-1 与 0。若第 1~4 题不熟,回看第 19 章;若第 5 题不熟,回看第 13 章。

4. 学习目标

完成本章后,你应能够:

5. 仿真不是让真实电路“慢动作运行”

RTL 仿真器保存的是信号值、过程状态和未来事件。它不求解每只 MOSFET 的连续电压电流,也不自动包含真实门延迟。未写延迟的 RTL 常被看作理想零延迟逻辑:

assign n = ~a;
assign y = n & b;

a 改变时,仿真器安排 n 更新;n 更新又触发 y 重新计算。两次计算可以发生在同一个物理仿真时刻,只是处于不同的零时间调度步骤。这种步骤常称为 delta cycle(增量周期)。

观察图 20-1:横向大刻度是仿真时间,纵向展开的是同一时刻内的事件求值顺序。只有事件队列稳定后,仿真器才前进到下一个有事件的时间点。

事件驱动仿真的时间与增量周期

图 20-1 事件驱动仿真只重新计算受变化影响的过程;同一时间点可经历多个零时间调度步骤直到信号稳定

这解释了两个现象:

例题 1:同一时刻内的组合传播

logic a, b, n, y;

assign n = ~a;
assign y = n & b;

初始 a=0b=1,稳定后 n=1y=1。若测试平台在 10 ns 把 a 改为 1:

  1. 10 ns 的输入赋值事件先更新 a
  2. assign n=~a 被触发,安排 n=0
  3. n 的变化触发 assign y=n&b,得到 y=0
  4. 仿真时间仍可显示为 10 ns,但事件队列经过了多个调度步骤。

因此,测试平台应在组合逻辑稳定后检查。教学代码可以在施加组合输入后等待一个明确的小时间,例如 #1ns;更复杂的同步环境则按协议边沿驱动和采样。

变式:若两条连续赋值交换文本顺序,稳定结果不变。事件依赖由信号连接决定,不由代码上下位置决定。

6. 编译、展开、仿真与综合是四件事

一个常见流程包含:

  1. 编译(compile):解析语法、类型和引用,发现拼写、端口和部分类型错误;
  2. 设计展开(elaboration):确定顶层模块、实例层次、参数值和生成结构;
  3. 仿真:按时间与事件规则运行展开后的模型;
  4. 综合:把可综合 RTL 转换为门级网表。

编译成功不证明功能正确;仿真通过不证明所有行为都已测试;RTL 仿真也不证明综合后一定满足面积和时序。每个阶段回答不同问题。

立即检查

测试平台中的 #5ns 能在仿真中产生等待。它是否会自动综合成一个“等待 5 ns”的硬件元件?

不会。延迟控制用于仿真调度。真实硬件若要等待时间,需要时钟、计数器和状态机。

7. 测试平台围绕 DUT 建立闭环

被测设计(design under test, DUT)是当前要验证的模块。一个可自动判定的基础测试平台通常包含:

图 20-2 中,蓝色路径把测试输入送入 DUT;橙色路径把实际输出送给检查器;绿色路径从同一事务产生期望结果。实际值和期望值必须走相对独立的计算路径,否则测试平台可能复制 DUT 的同一个错误。

基础自检查测试平台结构

图 20-2 测试平台不只产生波形,还要根据规格生成期望值并自动比较实际输出

最外层测试平台模块通常没有端口:

module tb_mux2;
    timeunit 1ns;
    timeprecision 1ps;

    logic a, b, sel;
    logic y;

    mux2_1bit dut (
        .a(a), .b(b), .sel(sel), .y(y)
    );

    // 激励、检查和仿真控制写在这里
endmodule

dut 是被测模块实例名。测试平台直接声明信号并连接 DUT,不需要从外部芯片端口接收数据。

8. 时间单位与时间精度

timeunit 1ns;
timeprecision 1ps;

timeunit 规定没有显式单位的延迟数值如何解释;timeprecision 规定仿真时间量化精度。推荐在延迟后直接写单位:

#5ns;
#250ps;

若时间精度为 1 ps,#250ps 可以精确表示;#0.4ps 会按工具和标准规定量化到当前精度,无法保留无限小数。

同一工程中的设计文件、测试平台和库模型应采用一致且受控的时间单位。否则同一个 #1 可能在不同作用域表示不同物理时间。

9. initial:测试从哪里开始

initial 过程在仿真开始时启动一次:

initial begin
    a   = 1'b0;
    b   = 1'b0;
    sel = 1'b0;

    #10ns;
    a = 1'b1;

    #10ns;
    $finish;
end

测试平台可以有多个 initial,它们在仿真开始时并行启动。一个过程产生时钟,另一个施加复位,第三个管理超时,是常见组织方式。

测试平台信号在时间 0 若没有初始化,四状态 logic 通常为 x。初始化输入能让第一个有效测试场景有明确起点;与此同时,状态元件是否依靠复位变为已知值仍应按 DUT 规格验证。

10. 组合逻辑的穷举与自检查

例题 2:穷举自检查 2 选 1 MUX

MUX 有三个一位输入,共有

\[ 2^3=8 \]

种二进制组合,可以全部测试:

module tb_mux2;
    timeunit 1ns;
    timeprecision 1ps;

    logic a, b, sel;
    logic y;
    logic expected;

    mux2_1bit dut (.*);

    initial begin
        for (int i = 0; i < 8; i++) begin
            {sel, b, a} = i[2:0];
            #1ns;

            expected = sel ? b : a;
            if (y !== expected) begin
                $error("MUX mismatch: sel=%0b a=%0b b=%0b y=%0b exp=%0b",
                       sel, a, b, y, expected);
            end
        end

        $display("MUX exhaustive test completed");
        $finish;
    end
endmodule

这段测试包含四个关键动作:

  1. for 依次产生 8 个输入组合;
  2. 拼接赋值同时设置 selba
  3. #1ns 让组合事件传播并稳定;
  4. !== 比较实际值和期望值,实际值为 xz 时也会判失败。

mux2_1bit dut (.*); 按同名自动连接端口,短例子里很紧凑。工程代码更常使用显式命名连接,避免误连和隐藏新增端口。本章后续示例采用显式连接。

变式:若有 12 个一位输入,穷举需要 \(2^{12}=4096\) 个组合,仍可能接受;若有 64 个输入位,\(2^{64}\) 已不现实,需要定向、边界、随机和形式方法分工。

11. 为什么使用 !== 检查四状态结果

普通相等 == 在任一参与位为 xz 时,结果可能也是 x。测试条件中的未知值容易让判断语义不直观。

四状态全等 === 和不全等 !==xz 也作为可比较状态,结果一定是 0 或 1。若期望值只有 0/1:

if (actual !== expected)
    $error("mismatch");

那么 actual=x 会明确失败。这正适合基础检查器。

不要让参考模型也随意产生 x,再用 === 把两个未知值判为相等。正常合法输入下,期望结果应是确定的 0 或 1;未知值需要单独定位来源。

12. 自动报告比只看波形可靠

只生成波形有三个问题:

自检查测试把判定规则写进测试平台:

if (actual !== expected)
    $error("time=%0t actual=%0h expected=%0h", $time, actual, expected);

推荐失败信息至少包含:

波形仍然重要,但它的任务变为定位已经被自动检查器发现的问题。

13. 时钟发生器

10 ns 周期、50% 占空比时钟可以写成:

initial begin
    clk = 1'b0;
    forever #5ns clk = ~clk;
end

时间 0 时 clk=0;5 ns 变 1,10 ns 变 0,15 ns 再变 1。周期为 10 ns,频率为 100 MHz。

时钟过程会永久运行,因此主测试流程结束时要调用 $finish。还应设置独立超时;如果 DUT 卡死或等待条件永远不成立,测试不能无限运行。

initial begin
    #10us;
    $fatal(1, "Timeout");
end

正常测试应在 10 μs 前 $finish;否则 $fatal 终止并报告失败。

14. 在边沿前驱动,在状态更新后检查

同步 DUT 在上升沿采样输入。测试平台若也恰在上升沿用普通过程修改输入,DUT 与测试平台可能争抢同一仿真调度区,形成竞争(race condition):不同代码顺序或工具调度可能让 DUT 看到旧值或新值。

基础测试平台采用清楚的相位分离:

观察图 20-3,驱动点和采样点分别位于有效边沿两侧,不与 DUT 的状态更新挤在同一个瞬间。

同步测试平台的驱动与采样相位

图 20-3 在下降沿驱动输入、上升沿后检查结果,为 DUT 留出稳定输入和非阻塞更新时间

教学代码可写:

@(negedge clk);
d  = 8'hA5;
en = 1'b1;

@(posedge clk);
#1ps;
if (q !== 8'hA5)
    $error("register load failed");

这里 timeprecision 为 1 ps,#1ps 把检查放到该上升沿的非阻塞更新之后。更系统的验证环境会使用 clocking block 明确输入输出采样偏移;基础阶段先掌握“驱动与采样分相”。

15. 同步寄存器的测试节奏

例题 3:测试同步寄存器的复位、装载与保持

DUT 是第 19 章的同步复位使能寄存器。测试计划至少包含:

  1. 复位边沿清零;
  2. en=1 装载;
  3. en=0 保持;
  4. rst=1en=1 同时出现时复位优先。
module tb_reg8;
    timeunit 1ns;
    timeprecision 1ps;

    logic       clk = 1'b0;
    logic       rst;
    logic       en;
    logic [7:0] d;
    logic [7:0] q;

    reg8_en dut (
        .clk(clk), .rst(rst), .en(en), .d(d), .q(q)
    );

    always #5ns clk = ~clk;

    task automatic check_q(input logic [7:0] exp,
                           input string label_text);
        #1ps;
        if (q !== exp)
            $error("%s: q=%0h exp=%0h time=%0t",
                   label_text, q, exp, $time);
    endtask

    initial begin
        rst = 1'b1;
        en  = 1'b0;
        d   = 8'h00;

        @(posedge clk);
        check_q(8'h00, "reset");

        @(negedge clk);
        rst = 1'b0;
        en  = 1'b1;
        d   = 8'hA5;
        @(posedge clk);
        check_q(8'hA5, "load A5");

        @(negedge clk);
        en = 1'b0;
        d  = 8'h3C;
        @(posedge clk);
        check_q(8'hA5, "hold");

        @(negedge clk);
        rst = 1'b1;
        en  = 1'b1;
        d   = 8'hFF;
        @(posedge clk);
        check_q(8'h00, "reset priority");

        $display("reg8 test completed");
        $finish;
    end
endmodule

任务(task)把重复的等待后检查、错误格式和标签统一起来。automatic 让每次任务调用拥有独立局部存储,便于以后并发复用。

注意:复位是同步复位,所以时间 0 把 rst=1 不会立刻改变 q;第一个上升沿之后才要求 q=0

16. 异步复位要检查“无时钟立即生效”

若 DUT 使用:

always_ff @(posedge clk or negedge rst_n)

测试应覆盖:

  1. rst_n 在两个时钟边沿之间由 1 变 0;
  2. 不等待下一个上升沿,q 即变为复位值;
  3. rst_n 解除后,下一次正常状态更新仍发生在时钟边沿;
  4. 复位释放附近的时序策略符合系统规定。

RTL 功能仿真可以检查事件语义,却不会真实模拟触发器进入亚稳态的模拟过程。第 15 章的复位释放同步原则仍需通过结构、约束和实现检查保证。

17. 参考模型必须比 DUT 更容易相信

参考模型根据规格计算期望结果。它可以是:

例如验证饱和加法器时,参考模型可以用更宽整数计算:

int unsigned ref_sum;

ref_sum = a + b;
expected = (ref_sum > 255) ? 8'hFF : ref_sum[7:0];

若直接把 DUT 的复杂 case 和位宽写法复制到测试平台,两边可能含有同一个错误并一起给出错误答案。参考模型应尽量接近规格语言,结构上独立于 DUT 实现。

图 20-4 强调两条路径:DUT 走实际 RTL,参考模型走简单规则,最后在同一事务编号上比较。

参考模型与DUT独立计算后比较

图 20-4 同一输入同时送入 DUT 与独立参考模型;检查器按事务或周期对齐期望值和实际值

18. 时序模块的参考状态

例题 4:用参考状态检查模 6 计数器

测试平台维护一个普通整数 ref_count,它只按模 6 规格更新:

int unsigned ref_count;

task automatic tick_and_check(input logic en_value);
    @(negedge clk);
    en = en_value;

    @(posedge clk);
    if (rst)
        ref_count = 0;
    else if (en_value)
        ref_count = (ref_count == 5) ? 0 : ref_count + 1;

    #1ps;
    if (count !== ref_count[2:0])
        $error("count mismatch: count=%0d exp=%0d time=%0t",
               count, ref_count, $time);
endtask

主流程先复位,再执行:

repeat (8)
    tick_and_check(1'b1);

repeat (3)
    tick_and_check(1'b0);

它检查:

参考状态应在与 DUT 相同的逻辑边沿概念上更新,但不要读取 DUT 内部 count 来算下一个期望值,否则 DUT 第一次出错后,参考模型会跟着错误状态继续走。

变式:在任意计数值插入一拍复位,再验证序列从 0 重新开始。这能检查复位不是只在开机时有效。

19. 定向、边界、穷举和随机测试

四类测试各有任务:

图 20-5 展示推荐顺序。先用少量定向测试证明基本通路,再集中打边界,规模小就穷举,最后用可复现随机序列扩展组合。随机测试不能代替规格驱动的边界测试。

验证场景从定向到随机的组合

图 20-5 基础验证先保证每条规格规则有定向检查,再用边界、穷举和可复现随机测试扩大搜索范围

对具有 \(M\) 个一位二进制输入的无状态组合模块,完整输入组合数为:

\[ N_{vector}=2^M. \]

\(M=10\) 时只有 1024 组,穷举很方便;\(M=32\) 时超过 42 亿组,测试策略必须改变。时序模块还包含输入序列和历史状态,完整穷举会更快爆炸。

20. 随机测试必须可复现

随机测试失败后,第一件事是让同一序列再次出现。记录随机种子(seed)和迭代号:

int unsigned seed = 32'h20260829;

initial begin
    void'($urandom(seed));

    for (int k = 0; k < 1000; k++) begin
        a  = $urandom();
        b  = $urandom();
        op = $urandom();
        #1ns;
        check_result(k);
    end
end

具体随机数序列可能受仿真器实现和调用位置影响,因此失败日志还应打印关键输入。稳定回归可以保存失败向量作为新的定向测试,这样以后不依赖随机序列也能检查该缺陷。

随机数量多不代表质量高。若约束或输入生成方式一直避开终点、同址冲突或复位与使能同时有效,运行一百万次仍可能漏掉最关键的边界。

21. 存储器的自检查方法

例题 5:自检查异步读、同步写存储器

第 19 章的 16 × 8 bit 存储器规定:异步读、上升沿同步写、写请求期间同址读通过旁路看到新 wdata。测试平台可维护软件数组:

logic [7:0] ref_mem [0:15];

初始化测试平台参考数组不代表 DUT 阵列被复位。测试流程只读取已经写过的地址:

task automatic write_word(input logic [3:0] addr,
                          input logic [7:0] data);
    @(negedge clk);
    we    = 1'b1;
    waddr = addr;
    wdata = data;
    raddr = addr;

    #1ns;
    if (rdata !== data)
        $error("write bypass failed addr=%0d", addr);

    @(posedge clk);
    ref_mem[addr] = data;

    @(negedge clk);
    we = 1'b0;
endtask

task automatic read_check(input logic [3:0] addr);
    raddr = addr;
    #1ns;
    if (rdata !== ref_mem[addr])
        $error("read mismatch addr=%0d got=%0h exp=%0h",
               addr, rdata, ref_mem[addr]);
endtask

主流程至少执行:

测试未初始化地址时,期望值应按接口规格处理。若规格说上电内容未知,就不应武断要求为 0;可以检查读出含未知,或更常见地先写后读。

22. 即时断言:在当前时刻检查一个条件

即时断言(immediate assertion)像一个带标准失败分支的 if

assert (main_g !== 1'bx)
    else $error("main_g is unknown");

assert (!(main_g && side_g))
    else $error("conflicting green lights");

它在执行到该语句的当前时刻检查表达式,适合:

基础测试平台仍可使用 if (...) $error,二者核心目的相同:把判定自动化。断言的优势是意图更明确,也便于工具统一统计。

23. 并发断言:跨时钟周期表达规则

并发断言(concurrent assertion)在采样时钟上检查时间关系。一个 done 只能持续一拍的规则可以写成:

property done_one_cycle;
    @(posedge clk) disable iff (rst)
        done |=> !done;
endproperty

assert property (done_one_cycle)
    else $error("done lasted more than one cycle");

|=> 表示:若左侧在当前采样边沿成立,则右侧必须在下一个采样边沿成立。disable iff (rst) 表示复位有效时暂停这条属性。

另一个同拍安全属性:

assert property (@(posedge clk) !(main_g && side_g))
    else $error("both directions are green");

并发断言的完整语法很丰富。本章只要求能读写“同拍不变量”和“下一拍关系”。复杂握手、可变延迟序列和形式证明留到进阶学习。

24. 断言与场景覆盖协作

例题 6:交通灯控制器的安全断言与场景覆盖

交通灯控制器最重要的不变量是冲突方向不能同时为绿灯。可以直接绑定到每个时钟边沿:

property no_conflicting_green;
    @(posedge clk) disable iff (rst)
        !(main_g && side_g);
endproperty

assert property (no_conflicting_green)
    else $fatal(1, "unsafe light combination at %0t", $time);

状态顺序还可以用简单计数器记录是否访问过每个相位:

int hit_main_g, hit_main_y, hit_side_g, hit_side_y;

always @(posedge clk) begin
    if (!rst) begin
        case (state)
            MAIN_G: hit_main_g++;
            MAIN_Y: hit_main_y++;
            SIDE_G: hit_side_g++;
            SIDE_Y: hit_side_y++;
        endcase
    end
end

测试结束时检查四个计数都大于 0:

assert (hit_main_g > 0 && hit_main_y > 0 &&
        hit_side_g > 0 && hit_side_y > 0)
    else $error("not all traffic phases were visited");

这只是一个基础功能覆盖记录。它证明四个相位都出现过,却没有证明每个相位持续时间正确,也没有证明所有配置值和优先级组合都已测试。因此还要对每次进入/离开相位的周期数建立参考模型并逐拍检查。

变式:加入“黄灯状态恰好持续配置拍数”的检查,并覆盖运行中改写下一相位持续时间的场景。

25. 覆盖率是“测试到哪里”的证据

覆盖率(coverage)常分为:

最简单的覆盖比例可写成:

\[ C=\frac{N_{hit}}{N_{target}}\times100\%. \]

\(N_{target}\) 是计划中的覆盖目标数,\(N_{hit}\) 是已经命中的数量。两者无量纲,\(C\) 用百分比表示。

覆盖率高说明测试活动到达了较多目标,但它不保证检查器正确,也不保证规格没有遗漏。若错误输出没有被比较,分支覆盖 100% 仍可能放过错误。覆盖率必须与自检查、断言和人工规格审查一起使用。

26. 测试计划先于测试代码

测试计划把规格条款转成可执行检查。每一行至少写:

字段 问题
功能点 要证明哪条规格?
前置状态 测试从什么状态开始?
输入动作 在什么边沿或时间施加什么?
期望结果 哪个输出或状态何时应为何值?
检查方法 比较器、参考模型还是断言?
覆盖记录 怎样知道该场景确实发生?

以使能寄存器为例:

功能点 输入动作 期望 检查
同步复位 边沿前 rst=1 边沿后 q=0 check_q
装载 rst=0,en=1,d=A5 边沿后 q=A5 check_q
保持 en=0,d=3C 边沿后仍为 A5 check_q
优先级 rst=1,en=1 复位胜出 check_q

写测试代码前完成这张表,能更早暴露规格中的模糊处。

27. 超时、通过和失败必须有明确出口

一个测试应有三种结束路径:

建议维护错误计数:

int error_count = 0;

task automatic report_mismatch(...);
    error_count++;
    $error("...");
endtask

结束时:

if (error_count == 0)
    $display("PASS");
else
    $fatal(1, "FAIL: %0d errors", error_count);

“仿真跑完”不是通过;只有检查全部完成且错误数为 0 才是通过。

28. 波形定位:先找第一个错误周期

当检查器报告周期 37 错误,不要先盯着周期 37 的最终输出猜原因。按图 20-6 的顺序向前追踪:

  1. 找到第一个实际值与期望值不同的周期;
  2. 检查该周期 DUT 采样到的输入和控制;
  3. 检查前一周期的状态寄存器;
  4. 检查本周期组合下一状态或数据路径;
  5. 判断错误来自设计、测试驱动、检查时刻还是规格对齐。
从失败输出回溯到第一个错误状态

图 20-6 后续错误常是第一个错误状态的连锁结果;定位应从第一次分叉处检查输入、旧状态和边沿更新

常见失败模式:

29. 区分设计错误、测试错误和规格缺口

设计错误

DUT 行为与明确规格不一致。例如模 6 计数器从 5 走到 6,或两个方向同时绿灯。

测试错误

测试平台驱动或检查方式有误。例如在上升沿同时改变 en,或在非阻塞更新前读取 q,或参考模型把同步复位当成异步复位。

规格缺口

规格没有决定同址读写返回什么、持续时间 0 怎样处理、非法状态是否恢复。此时不同实现可能都自洽,测试平台无法凭空选出唯一答案。应先补充接口契约,再写检查器。

定位时先保存失败输入、时间和波形,再用最小例子判断三者中的哪一类。不要通过放宽检查器来隐藏尚未解释的差异。

30. 回归测试:旧功能每次都重新检查

回归测试(regression)把已经通过的测试集合重复运行。每次修复错误或修改接口后,重新执行:

历史失败向量应成为长期回归的一部分。这样,同类错误再次出现时会被自动发现。

一个可复现回归还应记录:

31. 常见误区

误区 1:能编译就说明设计正确

编译检查语言和部分连接规则,功能正确性必须由测试和断言证明。

误区 2:波形看起来对就可以

人眼适合定位少量失败,不适合对大量周期逐项判定。期望值比较应自动化。

误区 3:在上升沿同时驱动和检查

这会与 DUT 采样和非阻塞更新竞争。把驱动放在边沿前,把检查放在更新后。

误区 4:随机测试越多越可靠

随机测试可能重复普通场景并长期避开关键边界。先从规格建立定向和边界检查,再用随机扩展。

误区 5:覆盖率 100% 等于没有错误

覆盖说明目标被访问,不说明期望值正确,也不说明规格完整。

误区 6:参考模型直接复制 DUT 代码最保险

复制会让同一错误同时出现在实际和期望路径。参考模型应更接近规格并保持实现独立。

误区 7:看到 x 就强制改成 0

x 是问题线索。应先查未初始化、漏赋值、多驱动或非法状态来源。

误区 8:仿真通过就满足时序

零延迟 RTL 仿真主要检查功能与周期关系。建立/保持、关键路径和物理延迟要由第 21、22 章的方法检查。

32. 工程中的实际意义与本章知识链

验证成本来自状态空间,而不是代码行数。一个很短的控制器也可能包含复位、优先级、边界、暂停、配置更新和异常恢复的多重组合。测试平台把这些组合转化为可重复证据。

本章知识链为:

功能与周期规格
   ↓
测试计划:正常 + 边界 + 非法 + 恢复
   ↓
事件驱动仿真与时间单位
   ↓
DUT + driver + monitor
   ↓
独立参考模型 → expected
   ↓                 ↘ actual
检查器 / 断言:自动判定
   ↓
覆盖记录:哪些目标已经发生
   ↓
失败日志 + 波形:定位第一次分叉
   ↓
固定种子与回归:持续防止旧错误重现
   ↓
第 21 章:综合后结构与静态时序是否满足约束

本章位于 RTL 功能层和仿真时间层。它保留逻辑值、事件顺序和周期关系,通常忽略晶体管模拟细节与真实布线延迟。这个抽象足以发现大多数功能和控制错误,但不能替代综合、STA 和物理实现检查。

33. 本章小结

  1. 事件驱动仿真只在相关信号变化时重新计算过程。
  2. 同一仿真时间点可以经历多个零时间调度步骤,直到事件队列稳定。
  3. 编译、设计展开、仿真和综合检查不同问题。
  4. 测试平台围绕 DUT 组织激励、监视、参考模型、检查器和覆盖记录。
  5. timeunittimeprecision 控制时间解释和量化精度。
  6. 组合模块可在规模允许时穷举输入,并使用 !== 捕获未知输出。
  7. 同步测试应在采样边沿前驱动,在非阻塞更新后检查。
  8. 时钟、复位、正常结束和超时应由清楚的并行过程管理。
  9. 任务能统一重复的驱动、等待和检查步骤。
  10. 参考模型应根据规格独立计算期望结果。
  11. 定向、边界、穷举和可复现随机测试承担不同任务。
  12. 即时断言检查当前条件,并发断言表达跨周期规则。
  13. 覆盖率说明测试到达了哪些目标,不等于功能已经完全正确。
  14. 失败定位应从第一个实际/期望分叉周期向前检查。
  15. 回归测试持续检查旧功能和历史缺陷。

34. 练习

基础题

  1. 编译、设计展开、仿真和综合分别回答什么问题?
  2. 什么是事件驱动仿真?为什么组合链可以在同一仿真时间内传播多级?
  3. DUT、激励器、参考模型和检查器分别承担什么任务?
  4. timeunit 1ns; timeprecision 1ps; 各表示什么?
  5. 为什么测试平台顶层模块通常没有输入输出端口?
  6. $display$error$fatal$finish 的基本用途分别是什么?
  7. !== 为什么适合比较实际输出与确定的期望值?
  8. 同步测试中为什么推荐在下降沿驱动、上升沿更新后检查?
  9. 覆盖率 100% 为什么不能单独证明设计没有错误?

分析与计算题

  1. 一个组合模块有 7 个一位输入。穷举需要多少个输入向量?若每个向量等待 2 ns,纯激励等待时间是多少?

  2. clk 初值为 0,每 4 ns 翻转一次。求周期、频率和前四个上升沿时刻。

  3. 同步复位寄存器在 3 ns 时令 rst=1,时钟上升沿在 5、15、25 ns。q 最早何时保证进入复位值?若是异步复位,功能仿真中最早何时改变?

  4. 下面测试为何存在竞争?怎样修改?

    @(posedge clk);
    en = 1'b1;
    if (q !== expected) $error("bad");
  5. 一个 8 bit 组合输出实际为 8'hx5,期望为 8'hA5actual == expectedactual === expectedactual !== expected 的结果或意义分别是什么?

  6. 某测试只检查模 10 计数器的序列 0、1、2、3。列出至少五个仍未覆盖的重要场景。

  7. 参考模型每拍用 DUT 当前 count 计算 expected=count+1。为什么这会削弱错误检测?怎样改进?

  8. 一个测试计划有 24 个功能覆盖目标,已命中 18 个。覆盖比例是多少?能否因此断言 75% 的错误已经被发现?

  9. 存储器测试在未写地址 9 前要求 rdata=0,但接口规定上电内容未知。问题属于设计错误、测试错误还是规格缺口?

  10. 并发属性 req |=> ack 在周期 4 的采样边沿看到 req=1。它要求哪个周期的 ack=1?若写成 req |-> ack,基础含义怎样变化?

  11. 检查器在周期 12 首次发现输出错误,周期 13~20 也全部错误。说明应优先检查哪些信号和哪个时间范围。

综合题

  1. 为 4 bit 二输入加法器写一个穷举自检查测试平台。检查 256 组输入,期望结果保留 5 bit 完整和。
  2. 为带同步复位和使能的 8 bit 寄存器写测试计划与测试平台,覆盖复位、装载、保持、优先级和运行中再次复位。
  3. 为模 6 计数器写独立参考模型,检查连续计数、暂停、回绕和中途复位;加入 1 μs 超时。
  4. 16 × 8 bit 异步读、同步写、write-first 旁路存储器设计至少十个测试场景,并写出 write_wordread_check 两个任务的核心代码。
  5. IDLE/RUN/DONE 控制器写两条并发断言:复位有效时下一拍状态为 IDLEdone 高后下一拍必须为低。说明每条属性的采样时刻。
  6. 为交通灯控制器建立功能覆盖清单,至少包含四个相位、四种相位转换、持续时间最小/最大值、运行中配置修改、复位和“冲突方向不同时为绿灯”。指出哪些适合计数覆盖,哪些适合断言。
  7. 制定第 17~20 章贯穿式模块的完整基础验证方案:写明 DUT 接口、时钟复位、参考模型、驱动/采样协议、定向场景、边界场景、随机策略、断言、覆盖目标、超时、失败日志、波形保留和回归准入条件。

35. 练习答案

展开第 20 章练习答案

题 1

编译解析语法、名称、类型和部分连接;设计展开确定顶层、实例层次、参数和生成结构;仿真按事件与时间规则运行模型并检查功能;综合把可综合 RTL 转换为门级网表。

题 2

事件驱动仿真只在信号变化影响到某个表达式或过程时安排重新求值。组合链的上游变化会在同一物理仿真时刻依次触发下游,经过多个零时间调度步骤后稳定,因此不一定增加显示时间。

题 3

DUT 是被测 RTL;激励器按协议产生输入;参考模型独立计算期望值;检查器在正确时刻比较实际值和期望值并报告结果。

题 4

默认时间单位为 1 ns;仿真可分辨和量化的时间精度为 1 ps。显式 #5ns 始终表示 5 ns,更容易跨文件检查。

题 5

测试平台是仿真的最外层环境,它自己产生时钟、复位和输入,并在内部连接 DUT,不需要再从更高层接收或输出芯片信号。

题 6

$display 打印信息;$error 报告错误但通常允许继续收集更多失败;$fatal 报告严重错误并终止仿真;$finish 正常结束仿真。

题 7

!== 的结果始终为 0 或 1,并把实际值中的 x/z 与确定的 0/1 期望判为不相等,可以明确捕获未知输出。

题 8

下降沿驱动让输入在下一上升沿前稳定半个周期;上升沿后稍作一个时间精度的等待再检查,可避开 DUT 的采样和非阻塞更新调度,减少竞争与差一拍错误。

题 9

覆盖率只说明定义的目标被执行或命中。若目标定义遗漏、参考模型错误或输出没有检查,即使覆盖率 100%,设计仍可能存在功能错误。

题 10

输入向量数为

\[ 2^7=128. \]

每组等待 2 ns,纯等待时间为

\[ 128\times2\ \text{ns}=256\ \text{ns}. \]

不计编译、调度和打印带来的主机运行开销。

题 11

每 4 ns 翻转一次,所以半周期 4 ns、周期 8 ns,频率为

\[ f=\frac{1}{8\ \text{ns}}=125\ \text{MHz}. \]

初值为 0 时,前四个上升沿在 4、12、20、28 ns。

题 12

同步复位只在上升沿采样,所以最早在 5 ns 上升沿之后进入复位值。异步复位若由 rst 的有效边沿触发,功能仿真中可在 3 ns 的复位事件后立即改变,不等待 5 ns。

题 13

测试平台在上升沿改变 en,与 DUT 在同一边沿采样竞争;随后立即检查 q,还可能早于非阻塞更新。修改为:

@(negedge clk);
en = 1'b1;
@(posedge clk);
#1ps;
if (q !== expected) $error("bad");

同时保证 timeprecision 不粗于 1 ps。

题 14

actual == expected 因含未知位而得到 xactual === expected 得 0,因为 x 与期望位 A 中对应的确定值不全等;actual !== expected 得 1,适合触发失败。8'hx5 表示高 4 bit 未知、低 4 bit 为 5。

题 15

至少还缺:

  • 8→9 的终点前状态;
  • 9→0 的回绕;
  • en=0 保持;
  • 复位;
  • 运行中复位;
  • rsten 同时有效的优先级;
  • 非法状态恢复策略;
  • terminal 脉冲时刻与宽度。

题 16

参考模型读取 DUT 的当前错误状态后继续计算,可能与 DUT 一起沿错误轨迹运行,无法判断何时第一次偏离规格。应维护独立 ref_count,只根据测试输入、复位和模 N 规则更新,并逐拍与 DUT 比较。

题 17

\[ C=\frac{18}{24}\times100\%=75\%. \]

它只表示 24 个定义目标中命中 18 个,不能推断已发现 75% 的错误。错误总数未知,覆盖目标本身也可能不完整。

题 18

这是测试错误。接口已经明确上电内容未知,测试平台却加入了“地址 9 初值为 0”的错误期望。正确方法是先写后读,或按未知内容契约检查。

题 19

|=> 是非重叠蕴含:周期 4 看到 req=1,要求周期 5 采样时 ack=1。基础的一周期表达中,|-> 是重叠蕴含,要求同一个周期 4 的采样点满足 ack=1

题 20

先检查周期 12 及其前一个状态更新边沿,重点看:周期 11 的状态寄存器;周期 12 边沿前 DUT 实际采样的输入;下一状态/数据通路组合值;周期 12 边沿后的寄存器新值;检查器的期望值对齐。周期 13~20 很可能只是错误状态继续传播的结果。

题 21

module tb_adder4;
    timeunit 1ns;
    timeprecision 1ps;

    logic [3:0] a, b;
    logic [4:0] sum;
    logic [4:0] expected;

    adder4 dut (.a(a), .b(b), .sum(sum));

    initial begin
        for (int ia = 0; ia < 16; ia++) begin
            for (int ib = 0; ib < 16; ib++) begin
                a = ia[3:0];
                b = ib[3:0];
                #1ns;
                expected = {1'b0, a} + {1'b0, b};
                if (sum !== expected)
                    $error("a=%0d b=%0d sum=%0d exp=%0d",
                           a, b, sum, expected);
            end
        end
        $finish;
    end
endmodule

两层循环共检查 \(16\times16=256\) 组。期望计算先把两个操作数扩展到 5 bit。

题 22

测试计划包括:初始同步复位;正常装载 00/A5/FF;改变 den=0 保持;rst=en=1 时复位优先;运行若干拍后再次复位。核心测试平台可复用例题 3 的 check_q 任务,并为每个场景采用:

@(negedge clk);
rst = next_rst;
en  = next_en;
d   = next_d;
@(posedge clk);
check_q(expected_q, label_text);

参考 expected_qrsten 优先级独立更新。结束前检查错误计数并设置独立超时。

题 23

int unsigned ref_count = 0;

task automatic step(input logic next_en,
                    input logic next_rst);
    @(negedge clk);
    en  = next_en;
    rst = next_rst;

    @(posedge clk);
    if (next_rst)
        ref_count = 0;
    else if (next_en)
        ref_count = (ref_count == 5) ? 0 : ref_count + 1;

    #1ps;
    if (count !== ref_count[2:0])
        $error("count=%0d exp=%0d", count, ref_count);
endtask

initial begin
    #1us;
    $fatal(1, "counter test timeout");
end

主流程应先 step(0,1) 复位,执行至少 8 次 step(1,0) 跨过回绕,执行多次 step(0,0) 检查暂停,再在非零状态执行 step(1,1) 检查中途复位。

题 24

十个场景可选:地址 0 写后读;地址 15 写后读;连续写相邻地址;同一地址重复覆盖;写 3 同时读 7;写读同址旁路;we=0 不写;改变 wdata 不写;遍历所有 16 个地址;数据 00/FF/A5/5A 边界与图案。

核心任务:

task automatic write_word(input logic [3:0] addr,
                          input logic [7:0] data);
    @(negedge clk);
    we=1; waddr=addr; wdata=data; raddr=addr;
    #1ns;
    if (rdata !== data) $error("bypass failed");
    @(posedge clk);
    ref_mem[addr] = data;
    @(negedge clk);
    we=0;
endtask

task automatic read_check(input logic [3:0] addr);
    raddr=addr;
    #1ns;
    if (rdata !== ref_mem[addr])
        $error("read mismatch addr=%0d", addr);
endtask

题 25

假设 state 可在测试范围内观察:

assert property (@(posedge clk)
                 rst |=> (state == IDLE))
    else $error("reset did not lead to IDLE");

assert property (@(posedge clk) disable iff (rst)
                 done |=> !done)
    else $error("done wider than one cycle");

两条属性都在 clk 上升沿采样。第一条在某边沿采到 rst=1 后,要求下一采样边沿状态为 IDLE。若规格要求同步复位在同一采样边沿更新,并希望检查更新后的当前周期状态,属性的采样区域与写法需按工具语义进一步设计;基础写法保守地检查下一拍。第二条在当前边沿采到 done=1 后要求下一边沿为 0。

题 26

计数覆盖适合:四个相位分别出现;四种合法相位转换分别出现;持续时间命中过最小值、最大值和普通值;运行中配置修改完成;复位在空闲与运行中分别出现。

断言适合:主路与支路不同时绿灯;状态只能按允许边转移;相位持续周期数符合锁定的配置语义;复位后进入规定安全状态;非法状态在规定周期内恢复。

覆盖确认场景发生,断言确认发生时行为正确。持续时间检查还需要参考计数器或带周期序列的属性。

题 27

合格方案应包含:

  • 列出 DUT 全部数据、控制、状态、时钟和复位端口及周期语义;
  • 统一 timeunit/timeprecision,下降沿驱动、上升沿后检查;
  • 根据第 17 章寄存器传输维护独立控制状态、计数和存储参考模型;
  • 定向测试覆盖启动、正常完成、暂停、继续、配置读写和再次启动;
  • 边界测试覆盖 0、1、最大计数、回绕、同址读写、复位与写使能同拍、非法状态恢复;
  • 随机测试固定并记录种子,失败输入转成永久定向向量;
  • 断言覆盖输出互斥、完成脉冲宽度、合法状态转移和复位安全状态;
  • 功能覆盖记录状态、转换、配置范围、暂停位置和冲突组合;
  • 独立超时防止等待卡死,错误日志打印周期、输入、旧状态、实际值和期望值;
  • 首次失败保留波形,定位第一个分叉周期;
  • 回归准入要求所有自检查为 0 错误、所有断言通过、必需覆盖目标命中,并保存版本、参数、工具选项与种子。

36. 自测清单

若第 1、5 项不稳定,请重画图 20-1 与图 20-3,并讲出每个事件发生在哪个时间和调度阶段;若第 6~8 项不稳定,请重写例题 2、3、4 的检查器;若第 9~11 项不稳定,请为交通灯控制器重新列一张“场景—期望—检查—覆盖”表。

37. 下一章衔接

功能仿真已经检查 RTL 在选定输入与周期下是否符合规格。第 21 章将进入综合与静态时序分析:综合工具怎样把 RTL 变为门级网表,时钟与输入输出约束怎样定义分析边界,建立/保持报告怎样对应第 14、15 章公式,以及为什么“仿真全通过”的设计仍可能在目标频率下失败。