SystemVerilog语法(9)-验证基础与简单Testbench

📌 本篇导读

知识点 难度 实战价值 典型场景
initial 过程块 ⭐⭐⭐⭐⭐ 测试激励生成、初始化
$display / $write ⭐⭐⭐⭐⭐ 打印信息、调试
$monitor / $strobe ⭐⭐ ⭐⭐⭐⭐ 自动跟踪信号变化
时间控制(#@repeat ⭐⭐ ⭐⭐⭐⭐⭐ 延迟、事件同步
$finish / $stop ⭐⭐⭐⭐ 控制仿真结束
时钟生成模板 ⭐⭐ ⭐⭐⭐⭐⭐ always + # 生成时钟
复位生成模板 ⭐⭐⭐⭐⭐ 异步/同步复位序列
force / release ⭐⭐ ⭐⭐⭐⭐ 强制赋值、故障注入
$dumpvars / $dumpfile ⭐⭐⭐⭐ 生成波形文件
简单 Testbench 结构 ⭐⭐ ⭐⭐⭐⭐⭐ 顶层封装、DUT例化

💡 常见误区

  • #10@(posedge clk) 混用导致时序错位
  • $display$strobe 在非阻塞赋值后的显示差异
  • 忘记用 $finish 导致仿真永不结束
  • force 后忘记 release 导致后续测试异常
  • 在多时钟域中时间单位设置不合理

🧭 本文使用 【综合】 标识电路设计相关语法,使用 【验证】 标识测试与仿真专用语法。本章主要面向验证,大量语法不可综合。


1 验证与 Testbench 概述

Testbench 是对设计(DUT)进行功能验证的仿真环境,通常不综合,但会:

  • 生成时钟和复位信号
  • 产生输入激励数据
  • 接收并检查 DUT 输出
  • 报告测试结果

最简单的 Testbench 结构:


2 initial 过程块【验证】

initial 块在仿真时间 0 时刻 仅执行一次,是 Testbench 核心激励描述方式。

systemverilog 复制代码
initial begin
    $display("仿真开始 @ %0t", $time);
    #10  rst_n = 1;      // 10 ns 后释放复位
    #100 $display("完成初始序列");
    #20  $finish;        // 结束仿真
end

多个 initial并行执行,均从 0 时刻启动。

systemverilog 复制代码
initial begin
    clk = 0;
    forever #5 clk = ~clk;
end

initial begin
    #100 $display("时钟已运行 100 个时间单位");
end

3.1 $display$write

函数 行为 示例
$display 自动换行 $display("a=%0d", a);
$write 不换行 $write(".");
$display(带格式) 支持 %d/%h/%b/%s/%t/%p $display("data=0x%h", data);

常用格式符

  • %d:十进制;%0d:去掉前导空格(仿真器扩展)
  • %h/%0h:十六进制
  • %b:二进制
  • %s:字符串
  • %t:仿真时间(配合 $time
  • %m:当前模块层次路径
  • %p打印聚合数据类型(数组、队列、结构体、联合体、类等),自动展开层次结构,非常适合调试

示例

systemverilog 复制代码
module tb_print;
    initial begin
        // 变量声明
        logic [7:0] val = 8'hA5;
        int         arr[] = '{1, 2, 3};
        struct { logic [3:0] a; logic b; } s = '{4'hF, 1'b1};
        
        $display("======== 仿真开始 ========");
        
        // 基本格式符
        $display("原始: val=%d, hex=%h, bin=%b", val, val, val);
        // 输出: 原始: val=165, hex=a5, bin=10100101
        $display("无前导零: %0d, %0h", val, val);
        // 输出: 无前导零: 165, a5

        // %p 聚合数据类型
        $display("arr = %p", arr);
        // 输出: arr = '{1, 2, 3}
        $display("s = %p", s);
        // 输出: s = '{a:15, b:1}

        $display("======== 仿真结束 ========");
        $finish;
    end
endmodule

仿真打印:


3.2 $monitor 自动监视

信号变化时自动打印;全局仅一个活跃 $monitor,后调用覆盖前。

systemverilog 复制代码
initial begin
    $monitor("TIME=%0t, clk=%b, data=%h", $time, clk, data);
end

3.3 $strobe 同步打印

当前时间所有赋值完成后执行,适合观察非阻塞赋值最终值。

systemverilog 复制代码
always_ff @(posedge clk) begin
    q <= d;
    $display("display: q=%0d", q);   // 显示旧值
    $strobe ("strobe: q=%0d", q);    // 显示新值
end

3.4 $time / $realtime

  • $time:按 timescale 舍入为整数
  • $realtime:保留小数的实数时间
systemverilog 复制代码
`timescale 1ns/1ps
initial begin
    #1.55;
    $display("$time = %0d ns", $time);        // 输出 2
    $display("$realtime = %0t ns", $realtime); // 输出 1.55
end

4 时间控制与等待【验证】

4.1 # 延迟控制

systemverilog 复制代码
#10;           // 延迟 10 个时间单位
#(5.3);        // 延迟 5.3(需精度匹配)
#(a + b);      // 延迟表达式(验证可用变量)

4.2 @ 边沿敏感

systemverilog 复制代码
@(posedge clk);    // 时钟上升沿
@(negedge rst_n);  // 复位下降沿
@(clk);             // 任意变化(不推荐)

4.3 repeat 重复等待

systemverilog 复制代码
repeat(5) @(posedge clk); // 等待 5 个时钟上升沿

4.4 wait 电平敏感

systemverilog 复制代码
wait (ready == 1);        // 等待高电平
wait (busy == 0) @(posedge clk); // 组合使用

4.5 event 自定义事件

systemverilog 复制代码
event start_sim;          // 声明事件
initial begin
    -> start_sim;         // 触发事件
end
initial begin
    @(start_sim);         // 等待触发
    $display("事件已收到");
end

好的,优化后的内容如下,增加工业界主流的 VCS + FSDB + Verdi 流程,同时保留通用的 VCD 方案作为参考。


5 仿真控制系统函数 【验证】

函数 行为
$finish 结束仿真,退出仿真器
$stop 暂停仿真,进入交互模式

5.1 波形文件生成

方案一:通用 VCD
systemverilog 复制代码
initial begin
    $dumpfile("wave.vcd");      // 指定波形文件名
    $dumpvars(0, tb_top);       // 记录 tb_top 及其所有子模块信号
    #1000 $finish;
end
方案二:VCS + FSDB + Verdi

在 Synopsys VCS 环境中,通常使用 FSDB(Fast Signal Database) 格式配合 Verdi 调试波形,速度更快、压缩比更高、支持信号追溯。

systemverilog 复制代码
initial begin
    $fsdbDumpfile("wave.fsdb"); // 指定 FSDB 文件名
    $fsdbDumpvars(0, tb_top);   // 记录所有层次信号
    #1000 $finish;
end

编译与运行(VCS 命令行):

bash 复制代码
# 编译时需加 -fsdb 选项,并链接 Verdi 库
vcs -full64 -sverilog -fsdb tb.sv -o simv

# 运行仿真,生成 wave.fsdb
./simv

# 用 Verdi 查看波形
verdi -ssf wave.fsdb &

常用 FSDB 函数扩展

函数 行为
$fsdbDumpfile("xxx.fsdb") 指定 FSDB 文件名
$fsdbDumpvars(0, top) 记录所有层次信号
$fsdbDumpvars(1, top.sub) 仅记录指定模块及其子层次
$fsdbDumpon / $fsdbDumpoff 动态开启/关闭波形记录

6 时钟与复位生成模板

6.1 时钟生成

systemverilog 复制代码
`timescale 1ns/1ps
module tb;
    logic clk;

    // 50% 占空比
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    // 等价写法
    always #5 clk = ~clk;
endmodule

相位偏移时钟

systemverilog 复制代码
initial begin
    clk = 0;
    #2.5; // 偏移 2.5ns
    forever #5 clk = ~clk;
end

6.2 复位生成

异步复位

systemverilog 复制代码
initial begin
    rst_n = 0;
    #20;
    rst_n = 1;
    #100;
    rst_n = 0; // 可选
    #20 rst_n = 1;
end

同步复位(推荐)

systemverilog 复制代码
initial begin
    rst_n = 0;
    repeat(3) @(posedge clk);
    rst_n = 1;
end

7 简单 Testbench 完整结构

DUT(8位计数器)

systemverilog 复制代码
`timescale 1ns/1ps
module counter #(parameter WIDTH=8) (
    input  logic clk,
    input  logic rst_n,
    input  logic en,
    output logic [WIDTH-1:0] cnt
);
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 0;
        else if (en)
            cnt <= cnt + 1;
    end
endmodule

Testbench

systemverilog 复制代码
`timescale 1ns/1ps
module tb_counter;
    logic        clk, rst_n, en;
    logic [7:0]  cnt;

    // 例化 DUT
    counter #(.WIDTH(8)) dut (
        .clk   (clk),
        .rst_n (rst_n),
        .en    (en),
        .cnt   (cnt)
    );

    // 时钟
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    // 复位
    initial begin
        rst_n = 0;
        #20;
        rst_n = 1;
    end

    // 激励与检查
    initial begin
        en = 0;
        wait(rst_n == 1);
        @(posedge clk);
        repeat(2) @(posedge clk);

        en = 1;
        repeat(10) @(posedge clk);
        if (cnt == 10)
            $display("[PASS] 计数到 10");
        else
            $display("[FAIL] 计数错误: %0d", cnt);

        en = 0;
        repeat(3) @(posedge clk);
        if (cnt === 10)
            $display("[PASS] 计数保持");
        else
            $display("[FAIL] 计数改变");

        rst_n = 0;
        @(negedge clk);
        if (cnt === 0)
            $display("[PASS] 复位清零");
        else
            $display("[FAIL] 复位错误");

        #50;
        $display("=== 仿真完成 ===");
        $finish;
    end

    // 波形
    initial begin
        $fsdbDumpfile("wave.fsdb");
        $fsdbDumpvars(0, tb_counter);
    end

    // 监视器
    initial begin
        $monitor("TIME=%0t, en=%b, cnt=%0d", $time, en, cnt);
    end
endmodule

仿真输出:


8 force / release【验证】

仅仿真有效,用于故障注入/调试。

systemverilog 复制代码
initial begin
    #100;
    force dut.cnt = 8'hFF;
    #20;
    release dut.cnt;
end

⚠️ :不可综合,仅用于验证


9 timescale 时间尺度【验证】

systemverilog 复制代码
`timescale <单位> / <精度>

常见设置:

  • 1ns/1ps:单位 1ns,精度 1ps
  • 10ns/1ns:单位 10ns,精度 1ns
  • 1ps/1ps:单位 1ps

示例:

systemverilog 复制代码
`timescale 1ns/1ps
initial begin
    #1.55;
    #1.555;
end

10 序列检测器仿真示例

DUT

systemverilog 复制代码
`timescale 1ns/1ps

module seq_detector (
    input  logic clk,
    input  logic rst_n,
    input  logic din,
    output logic dout
);
    typedef enum logic [2:0] { S0=0, S1=1, S2=3, S3=2, S4=6 } state_t;
    state_t cur, nxt;

    always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) cur <= S0;
        else cur <= nxt;

    always_comb begin
        nxt = cur;
        case (cur)
            S0: nxt = din ? S1 : S0;
            S1: nxt = din ? S2 : S0;
            S2: nxt = din ? S2 : S3;
            S3: nxt = din ? S4 : S0;
            S4: nxt = S0;
            default: nxt = S0;
        endcase
    end
    assign dout = (cur == S4);
endmodule

Testbench

systemverilog 复制代码
`timescale 1ns/1ps
module tb_seq;
    logic clk, rst_n, din, dout;
    seq_detector dut (.*);

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        $display("======== Simulation Start ========");
        rst_n = 0; din = 0;
        $display("[%0t] Reset asserted (rst_n=0)", $time);

        #20 rst_n = 1;
        $display("[%0t] Reset released (rst_n=1)", $time);

        // 发送序列 1,1,0,1
        @(posedge clk);
        din = 1;
        $display("[%0t] Send din = 1", $time);

        @(posedge clk);
        din = 1;
        $display("[%0t] Send din = 1", $time);

        @(posedge clk);
        din = 0;
        $display("[%0t] Send din = 0", $time);

        @(posedge clk);
        din = 1;
        $display("[%0t] Send din = 1", $time);

        @(posedge clk);
        din = 0;   // 停止输入
        $display("[%0t] Input stopped", $time);

        #10;  // 等待输出稳定
        $display("[%0t] Check output: dout = %b", $time, dout);
        if (dout === 1) $display("PASS: 1101 detected");
        else $display("FAIL: 1101 test");

        #50;
        $display("======== Simulation Stop ========");
        $finish;
    end

    //initial begin
    //    $fsdbDumpfile("wave.fsdb");
    //    $fsdbDumpvars(0, tb_seq);
    //end

endmodule

仿真输出:


11 常见错误与编码建议

  1. 忘记 timescale → 所有文件开头统一写 ```timescale 1ns/1ps``
  2. $display/$strobe 混用 → 非阻塞赋值用 $strobe
  3. 时钟无初值initial clk=0; 配合 always #5 clk=~clk
  4. 复位与时钟沿对齐风险 → 同步复位用 @(posedge clk) rst_n=1;
  5. force 不释放 → 必须配对 release
  6. $monitor 多实例覆盖 → 全局仅设一个
  7. 仿真不结束 → 测试完成后调用 $finish


📢 关于文章 :原创分享,转载需注明出处。

🔔 点击关注,第一时间收到后续内容推送(第10篇:面向对象编程基础)。
📚 参考文档IEEE Standard for SystemVerilog

相关推荐
清风6666668 小时前
基于单片机与DAC0832的双路波形信号发生系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
azwsm9 小时前
电路元器件和GPIO控制器
单片机·嵌入式硬件
kebidaixu12 小时前
FreeRTOS 移植到 STM32F407VETX 记录(一)
stm32·单片机·嵌入式硬件
CSDN官方博客13 小时前
「谁说嵌入式只是调包和焊板子?」—— 2026嵌入式全栈技术征锋令
嵌入式硬件·物联网·embedding
点灯小铭13 小时前
基于单片机的数码管定时插座设计与定时开关功能实现
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
云栖梦泽14 小时前
玩转RK3506SDK
linux·嵌入式硬件
2601_9618454215 小时前
2027考研数学大纲|数一数二数三
考研·fpga开发·ar·vr·mr·oneflow
数智工坊15 小时前
机器人四大主控板系统分层选型指南:树莓派、ESP32、STM32与Arduino的能力边界与实战定位
stm32·嵌入式硬件·机器人
进击的小头16 小时前
第8篇:IGBT 从零到精通:核心原理、关键参数、选型指南与工业级应用要点
经验分享·嵌入式硬件·学习
点灯小铭16 小时前
基于单片机的多模式智能洗衣机设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业