news 2026/3/18 19:56:21

从零实现一个简单的SystemVerilog验证平台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现一个简单的SystemVerilog验证平台

从零开始搭建一个真正能跑的 SystemVerilog 验证平台

你是不是也曾经打开过 UVM 的代码,看着满屏的uvm_component_utilsbuild_phasesequencer-driver-agent层层嵌套,心里默默问了一句:“这玩意儿到底是怎么跑起来的?”

别急。我们今天不讲 UVM,也不背宏定义。我们要做一件更“原始”但也更本质的事:用最基础的 SystemVerilog,从头搭一个可以自动发激励、采数据、比结果的验证平台

你会发现,当你亲手把信号连上、把类写出来、看到$info("PASS")在控制台跳出来的时候——原来验证这件事,没那么玄。


先搞清楚:我们在验证什么?

假设我们的 DUT 是一个8 位无符号加法器,带有效信号控制:

module adder_8bit ( input clk, input rst_n, input valid_in, input [7:0] a, input [7:0] b, output reg [7:0] sum, output reg valid_out ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sum <= 8'h00; valid_out <= 1'b0; end else if (valid_in) begin sum <= a + b; valid_out <= 1'b1; end end endmodule

看起来很简单对吧?但它就是一个典型的同步设计模块:有复位、有时钟、有输入使能和输出应答。这种结构在实际项目中随处可见。

现在的问题是:你怎么证明它真的每次都算对了?

总不能每次仿真都手动看波形吧?那要是测 1000 组数据呢?边界值呢?溢出情况呢?

所以,我们需要一个“自动化裁判”。


第一步:给信号建个“通道”——Interface 不只是连线封装

传统 testbench 常见写法是这样:

adder_8bit dut ( .clk(clk), .rst_n(rst_n), .valid_in(valid_in), .a(a), .b(b), .sum(sum), .valid_out(valid_out) );

但问题来了:如果你要在一个 class 里驱动这些信号怎么办?难道要把所有 wire 都传进去?

当然不是。SystemVerilog 提供了一个强大的工具:interface

我们可以把这一堆信号打包成一个通信通道,并且还能指定什么时候采样、什么时候驱动。

interface adder_if (input logic clk); logic rst_n; logic valid_in; logic [7:0] a; logic [7:0] b; logic [7:0] sum; logic valid_out; clocking cb @(posedge clk); default input #1ns output #1ns; output a, b, valid_in, rst_n; input sum, valid_out; endclocking modport TEST (clocking cb, output rst_n); modport DUT (input clk, rst_n, valid_in, a, b, output sum, valid_out); endinterface

这里有几个关键点你要记住:

  • clocking block是你的“时间锚点”。它告诉你:所有通过 cb 访问的信号,都在 posedge clk 上下文操作
  • default input/output #1ns是为了避免竞争条件(race condition),让采样略晚于变化。
  • modport则是从不同视角看这个接口——DUT 只管输入输出,而 TEST 端可以通过 clocking block 同步驱动。

这个 interface 就像一根带时序协议的“数据管道”,连接着测试平台与 DUT。


第二步:谁来发数据?Tester 出场

接下来我们需要一个“测试员”,它能随机生成一些 a 和 b,然后通过 interface 发出去。

这就轮到面向对象编程(OOP)登场了。

我们先定义一个事务类(transaction),表示一次加法操作的数据包:

class AdderTransaction; rand bit [7:0] a; rand bit [7:0] b; // 加个约束,避免全范围随机导致溢出太多干扰分析 constraint reasonable_range { a < 200; b < 200; } function string sprint(); return $sformatf("AdderTx: %0d + %0d", a, b); endfunction endclass

注意用了rand关键字,意味着这些字段可以在调用randomize()时自动生成合法值。

然后我们做一个 Tester 类,负责把这些数据送进 DUT:

class AdderTester; virtual adder_if.TEST intf; AdderTransaction pkt; function new(virtual adder_if.TEST intf); this.intf = intf; this.pkt = new(); endfunction task run(); // 施展魔法前先复位 reset_dut(); repeat(10) begin assert(pkt.randomize()) else $fatal("随机化失败!"); @(intf.cb); // 等待下一个时钟上升沿 intf.cb.a <= pkt.a; intf.cb.b <= pkt.b; intf.cb.valid_in <= 1'b1; @(intf.cb); intf.cb.valid_in <= 1'b0; // 单周期脉冲 $display("【发送】%s", pkt.sprint()); end endtask task reset_dut(); intf.cb.rst_n <= 1'b0; repeat(5) @(intf.cb); intf.cb.rst_n <= 1'b1; @(intf.cb); endtask endclass

看到了吗?我们不再直接操作信号,而是通过virtual interface来访问cb。这实现了物理信号与行为逻辑的解耦

而且整个流程非常清晰:
1. 复位系统;
2. 循环 10 次,每次随机生成一组数据;
3. 在 clocking block 边沿驱动输入;
4. 使用单周期 valid 脉冲触发计算。

这才是现代验证的思想起点:用类组织行为,用对象管理状态,用接口隔离层次


第三步:谁来看结果?Monitor + Checker 分工协作

Tester 把数据发出去了,那谁来确认 DUT 是否正确响应?

如果让你一个人既当运动员又当裁判,你会不会偷偷放水?

所以我们需要两个角色:

  • Monitor:只负责“看”——采集真实输出;
  • Checker:只负责“判”——拿预期结果对比。

它们之间通过一个“邮局”传递消息,这个邮局就是mailbox

Monitor:忠实的观察者

class AdderMonitor; virtual adder_if.TEST intf; mailbox #(AdderTransaction) result_mbox; function new(virtual adder_if.TEST intf, mailbox #(AdderTransaction) mbox); this.intf = intf; this.result_mbox = mbox; endfunction task run(); fork forever begin // 只有 valid_out 为高时才采样 @(intf.cb iff intf.cb.valid_out); AdderTransaction tr = new(); tr.a = intf.cb.a; tr.b = intf.cb.b; tr.sum = intf.cb.sum; result_mbox.put(tr); // 把观测结果寄出去 $display("【监控】捕获输出:%0d + %0d = %0d", tr.a, tr.b, tr.sum); end join_none endtask endclass

重点在于iff intf.cb.valid_out——这是条件事件触发,确保我们只在有效输出时采样,避免误抓无效数据。

同时,我们将完整的事务信息封装后放入 mailbox,交给下游处理。

Checker:冷静的判决官

class AdderChecker; mailbox #(AdderTransaction) result_mbox; int pass_cnt = 0; int fail_cnt = 0; function new(mailbox #(AdderTransaction) mbox); this.result_mbox = mbox; endfunction task check(); fork forever begin AdderTransaction tr; result_mbox.get(tr); // 取回监控到的结果 byte unsigned expected = tr.a + tr.b; if (tr.sum == expected) begin $info("✅ PASS: %0d + %0d = %0d", tr.a, tr.b, tr.sum); pass_cnt++; end else begin $error("❌ FAIL: %0d + %0d, 实际=%0d, 期望=%0d", tr.a, tr.b, tr.sum, expected); fail_cnt++; end end join_none endtask function void report(); $display("\n📊 测试报告:共%d次,成功%d,失败%d", pass_cnt+fail_cnt, pass_cnt, fail_cnt); if (fail_cnt == 0) $display("🎉 所有测试通过!"); else $warning("%d 个错误需排查", fail_cnt); endfunction endclass

Checker 干的事很简单:拿到数据 → 算黄金模型 → 对比 → 输出结论。

但它的价值在于自动化判定。从此你不需要打开波形图一个个数,只要看终端输出就知道有没有 bug。


最后一环:把所有人串起来——Testbench 顶层

现在四个组件都有了:DUT、interface、Tester、Monitor、Checker。

我们来写个 top module 把它们粘合在一起:

module tb_adder; logic clk; initial begin clk = 0; forever #5 clk = ~clk; // 10ns 周期时钟 end adder_if if0(clk); // 实例化 DUT adder_8bit dut ( .clk (if0.clk), .rst_n (if0.rst_n), .valid_in (if0.valid_in), .a (if0.a), .b (if0.b), .sum (if0.sum), .valid_out (if0.valid_out) ); // 测试组件 initial begin mailbox #(AdderTransaction) result_mbox = new(); AdderTester tester = new(if0); AdderMonitor monitor = new(if0, result_mbox); AdderChecker checker = new(result_mbox); // 启动各线程 fork tester.run(); monitor.run(); checker.check(); join_none // 等待足够长时间让测试完成 #2000; checker.report(); $finish(); end endmodule

几点说明:

  • 所有组件共享同一个virtual interface,因此能看到相同的信号视图;
  • mailbox 是跨线程通信的关键桥梁;
  • 使用fork...join_none实现并发执行;
  • 最后调用report()输出统计结果。

运行一下,你会看到类似这样的输出:

【发送】AdderTx: 123 + 67 【监控】捕获输出:123 + 67 = 190 ✅ PASS: 123 + 67 = 190 ... 📊 测试报告:共10次,成功10,失败0 🎉 所有测试通过!

看到这个“🎉”,你就知道——你的验证平台真的跑起来了。


这个平台教会我们的,远不止加法器

虽然我们验证的是一个简单的加法器,但这套架构已经包含了现代验证方法学的核心思想:

技术点实现方式意义
信号抽象interface + clocking block解决时序同步与连接混乱
激励生成class + rand + randomize()实现智能、可约束的测试向量
行为封装virtual interface实现测试组件与硬件解耦
自动化检查monitor + checker替代人工比对,提升效率
线程通信mailbox安全传递事务级数据

这些正是 UVM 框架背后的基本原理。只不过 UVM 把它们标准化、泛化、工厂化了而已。

你现在懂了底层机制,再去学 UVM,就不会再觉得它是“黑盒子”。


新手避坑指南:那些没人告诉你的细节

  1. 不要忘了@(intf.cb)
    如果你在非 clocking block 边沿驱动信号,很可能遇到 race condition,导致采样错位。

  2. mailbox 容量有限,小心阻塞
    默认 mailbox 是无限容量的,但在复杂场景下建议使用mailbox #(T, N)设置上限,防止内存爆掉。

  3. reset 必须做好
    我们在 Tester 中加入了reset_dut(),这是好习惯。很多功能错误其实是状态机没清零导致的。

  4. 黄金模型必须可信
    Checker 的判断依据是a + b,但如果 DUT 是除法器或 CRC,你的预测模型就得更复杂。务必保证模型本身无误。

  5. display 多一点没关系
    初学者常怕打印太多影响性能。其实仿真阶段日志越多越好,方便追踪执行流。优化是后期的事。


下一步你可以怎么玩?

这个平台虽小,但延展性很强。试试这几个升级方向:

  • 加入覆盖率收集:用covergroup统计 a 和 b 的取值分布,看看是否覆盖了边界(如 0、255、接近溢出等);
  • 🔁支持 backpressure:让 DUT 支持忙信号(busy),Tester 要能等待;
  • 🧩替换 DUT 为 FIFO 或 UART TX:改改 interface 和 checker,就能验证其他模块;
  • 📦封装成 reusable agent:把 tester/monitor/checker 包装成一个 agent 类,以后直接复用;
  • 🚀过渡到 UVM:你会发现 UVM 的uvm_driveruvm_monitoruvm_scoreboard不过是我们写的这些类的增强版。

写在最后:验证的本质是什么?

不是会用 UVM,不是会写 sequence,也不是背熟 macro。

验证的本质是:构造一个环境,让错误无处藏身。

而要做到这一点,你需要掌握两件事:

  1. 如何高效地制造“麻烦”(激励生成);
  2. 如何精准地发现“破绽”(结果检查)。

今天我们做的,就是用最朴素的方式,把这两个能力组装了起来。

当你下次面对一个新的模块时,不妨问问自己:

“我该怎么设计 interface?”
“tester 应该怎么发包?”
“monitor 怎么抓结果?”
“checker 怎么才算‘对’?”

一旦你能回答这些问题,你就已经是一名合格的验证工程师了。

至于框架?那不过是锦上添花罢了。

如果你正在学习systemverilog菜鸟教程,希望这篇文能帮你跨过第一道坎。欢迎在评论区贴出你实现的第一个验证平台截图,我们一起 review!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/14 1:13:00

【C++】二叉搜索树

&#xff0c;二叉搜索树的概念 二叉搜索树又称二叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树: • 若它的左⼦树不为空&#xff0c;则左⼦树上所有结点的值都⼩于等于根结点的值。 • 若它的右⼦树不为空&#xff0c;则右⼦树上所有结点的值…

作者头像 李华
网站建设 2026/3/14 22:01:15

企业级应用中处理API连接失败的5个真实案例

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个案例库应用&#xff0c;收集和展示各种API连接失败的解决方案。功能包括&#xff1a;1. 案例分类&#xff08;网络问题、认证问题、配置问题等&#xff09;&#xff1b;2.…

作者头像 李华
网站建设 2026/3/13 14:25:59

LightOnOCR-1B:终极OCR引擎,10亿参数5倍速解析

LightOnOCR-1B&#xff1a;终极OCR引擎&#xff0c;10亿参数5倍速解析 【免费下载链接】LightOnOCR-1B-1025 项目地址: https://ai.gitcode.com/hf_mirrors/lightonai/LightOnOCR-1B-1025 导语&#xff1a;LightOn推出的10亿参数OCR专用模型LightOnOCR-1B-1025&#xf…

作者头像 李华
网站建设 2026/3/13 12:50:36

对比:传统vs容器化SQL Server安装效率

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个SQL Server容器化部署工具&#xff0c;功能&#xff1a;1.自动拉取官方Docker镜像 2.生成自定义docker-compose.yml 3.配置持久化存储 4.设置资源限制 5.集成健康检查。支…

作者头像 李华
网站建设 2026/3/12 13:45:02

腾讯Hunyuan-4B-FP8:256K上下文+高效智能体大模型

腾讯Hunyuan-4B-FP8&#xff1a;256K上下文高效智能体大模型 【免费下载链接】Hunyuan-4B-Instruct-FP8 腾讯开源混元高效大语言模型系列成员&#xff0c;专为多场景部署优化。支持FP8量化与256K超长上下文&#xff0c;具备混合推理模式与强大智能体能力&#xff0c;在数学、编…

作者头像 李华
网站建设 2026/3/4 0:54:25

POTPLAYER快捷键大全:提升操作效率300%

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个POTPLAYER快捷键训练应用&#xff0c;功能包括&#xff1a;1. 分类展示所有快捷键&#xff08;播放控制、音量调节、画面处理等&#xff09;&#xff1b;2. 交互式练习模式…

作者头像 李华