SV monitor 协议检查:引脚级采集、事务打包与 analysis 广播实战
更新时间:2026-09-28。在 SystemVerilog TLM 通信 里我们已经看到 monitor 是事务的生产者:它把
analysis_port.write(tx)一调,scoreboard 与 coverage 各自取走。但 TLM 那篇刻意跳过了一个问题——write之前的tx从哪来。答案全在 monitor 的采集逻辑里:它贴着 DUT 的引脚,把一拍一拍的 valid/ready 波形还原成一笔一笔有意义的事务,顺手再做协议合法性检查。这一篇就把这段「从信号到事务」的旅程拆开讲。
本文要回答的问题
- monitor 为什么要用 interface 的 clocking block 采样,直接
@(posedge clk)读信号差在哪? - 一个跨多拍的 burst 握手,怎样被打包成单个 transaction 对象?
- 协议检查(如 valid 一旦拉高不许掉、地址对齐)应该写在哪:monitor 内联检查、SVA 断言,还是 scoreboard?
- 一笔事务要同时送给 scoreboard、coverage 和协议日志,analysis 广播怎么接?
- monitor 里常见的并发 bug(句柄复用、采样竞争、漏检 back-to-back)长什么样?
一、monitor 的职责边界:只做三件事
写 monitor 之前先把职责划清,否则这个类很快就会膨胀成什么都干一点的怪物。一个健康的 monitor 只做三件事:
- 采集:通过 virtual interface 在正确的时钟沿读引脚,消掉采样竞争。
- 打包:把若干拍的引脚状态拼成一笔完整的 transaction 对象,填好地址、数据、方向、时间戳等字段。
- 分发:通过 analysis_port 把事务广播出去,并在广播前或广播中做轻量的协议合法性检查。
它不做的事同样重要:不驱动任何信号(那是 driver 的活,也是 passive agent 的命名由来)、不做数据正确性比对(scoreboard 的活)、不统计功能覆盖(coverage collector 的活)。边界守住,monitor 才能在任何项目里原封不动地复用;协议换了,改的只有采样与打包逻辑,对外暴露的 analysis_port 面孔不变。
这张图同时画出了 monitor 的「输入只有一根、输出可以很多」的形态:左边永远是一个 interface,右边的订阅者数量与类型对 monitor 完全透明。
二、采集第一步:clocking block 消掉采样竞争
monitor 读引脚最忌讳直接 @(posedge clk); x = vif.valid;。这句代码在 active region 执行,与 DUT 内部在同一拍更新信号的 process 构成竞争:到底读到的是旧值还是新值,由仿真器调度顺序决定,换一家仿真器结果就可能不同。这是 testbench 最经典的坑,解法是 interface 与 clocking block 里讲过的机制:clocking block 给采样约定一个相对时钟沿的偏移(input #1step),保证 monitor 永远读到该拍沿之前的稳定值,与 DUT 的更新错开调度区。
interface bus_if(input logic clk);
logic valid, ready;
logic [31:0] addr, data;
clocking mon_cb @(posedge clk);
default input #1step;
input valid, ready, addr, data;
endclocking
modport MON(clocking mon_cb);
endinterfacemonitor 通过 vif.mon_cb 采样,代码里的等待一律写成 @(vif.mon_cb)。这样拍与拍之间的推进由 clocking block 统一定义,后面打包逻辑拿到的 valid/ready 都是「这一拍的逻辑值」,不用关心 DUT 是在沿前还是沿后更新的。
一个容易忽略的细节:monitor 里不要出现 #0 或额外的 #1 延时去「等一下再采」。所有时序对齐都应该由 clocking block 的 skew 表达,手写的绝对延时在门级仿真(时钟带 SDF 反标)下立刻失真。另外,input #1step 的语义是「采样沿前最近的一个时刻」,而不是物理 1 个时间单位,所以它天然适配任何精度设置。

这张时序图把竞争画了出来:没有 clocking block 时,DUT 的赋值和 monitor 的读取挤在同一个调度区,谁先谁后没有语言级保证;有了 skew 约定后,采样被固定到「沿之前」,读到的永远是上一拍驱动的稳定值。这个约定是后面所有打包逻辑能正确工作的地基。
三、打包:把握手波形还原成 transaction
采集解决「每拍读到什么」,打包解决「这几拍合起来是什么」。以最典型的 valid/ready 握手为例:valid 与 ready 同拍为高才算一次 transfer,一笔 burst 事务可能跨多个 transfer。打包逻辑就是一个小的状态机:等第一拍握手,锁存地址与属性;之后每拍握手锁存一拍数据;末拍(如 last 拉高)事务闭合,填上收尾字段后广播。
状态机对应的代码骨架如下,注意每一次循环都 new 一个新对象:
task run_phase(uvm_phase phase);
txn_t tx;
forever begin
@(vif.mon_cb);
if (vif.mon_cb.valid && vif.mon_cb.ready) begin
if (tx == null) begin
tx = txn_t::type_id::create("tx"); // 首拍:开新事务
tx.addr = vif.mon_cb.addr;
tx.start_time = $time;
end
tx.data_q.push_back(vif.mon_cb.data);
if (vif.mon_cb.last) begin
tx.end_time = $time;
protocol_check(tx); // 广播前做轻量检查
ap.write(tx); // 广播,句柄让渡给订阅者
tx = null; // 关键:不再持有这笔事务
end
end
end
endtask打包完成的事务与逐拍信号之间的关系,可以用一张表对照,写 transaction 字段定义时按这张表自查一遍通常不会漏:
| transaction 字段 | 来源 | 采集时机 |
|---|---|---|
| addr / 属性字段 | 首拍握手时的引脚 | 状态机从 s0 进入 s1 那一拍 |
| data_q 动态数组 | 每拍握手的 data 引脚 | s1 内每次握手 push 一次 |
| start_time / end_time | $time | 首拍与末拍各记一次 |
| 方向(读/写) | 读写控制引脚 | 首拍锁存 |
| 错误/响应码 | 响应类引脚 | 末拍或响应拍锁存 |
两个工程要点。其一,每笔事务必须是新对象。如果把 tx 写成成员变量反复复用,ap.write(tx) 广播出去的是同一个句柄,下一笔事务开写时会把 scoreboard 还没来得及比对的上一笔覆盖掉——这是 monitor 最高发的 bug,症状是 scoreboard 偶发比对失败、且失败事务的字段总像「下一笔的值」。其二,广播后立刻把局部句柄置空,既是防止误改订阅者手里的对象,也让「句柄所有权已让渡」这件事在代码里显式可见。back-to-back 场景(last 的下一拍马上是新事务首拍)由状态机自然覆盖:同一拍先闭合旧事务、再开新事务,两个 if 分支在同一轮循环里都成立,不会丢首拍。
四、协议检查写在哪:三层分工
「协议检查」这四个字在验证里其实对应三层不同机制,放错层是新手最常踩的坑。
| 层级 | 机制 | 检查内容 | 特点 |
|---|---|---|---|
| 引脚时序层 | SVA 断言(bind 到 interface) | valid 拉高后到 ready 前不许掉、last 与 burst 长度匹配 | 逐拍可见、与采样无关、可被形式工具复用 |
| 事务层 | monitor 内联 protocol_check | 事务级合法性:地址对齐、长度字段与 data_q 深度一致 | 拿到完整事务才好判,一次检查覆盖整笔 |
| 系统层 | scoreboard 比对 | 数据内容对不对、事务有没有丢/多/乱序 | 需要参考模型或对端数据 |
分工原则一句话:逐拍规则交给断言,跨拍规则交给 monitor,内容裁决交给 scoreboard。把所有检查都堆进 monitor 的后果是它变成巨型 if-else,且逐拍规则用 if 写在任务里会受采样点限制漏掉毛刺;全堆进断言的后果是断言里被迫维护跨拍状态,可读性崩塌。SVA 的写法与绑定技巧见 SVA 断言实践,本篇只界定边界。
monitor 内联检查保持「轻」:只判字段一致性并 uvm_error 上报,不在里面挂起等待或访问其他组件。报告要带上事务的可打印信息,这样日志里一行就能定位是哪笔事务违规。
function void protocol_check(txn_t tx);
if (tx.addr[1:0] != 2'b00)
`uvm_error("PROTO", $sformatf("unaligned addr: %s", tx.sprint()))
if (tx.data_q.size() != tx.len)
`uvm_error("PROTO", $sformatf("len mismatch: %s", tx.sprint()))
endfunction注意检查写成 function 而不是 task:它消耗零仿真时间,不会打断采集循环的节拍,也不会因为里面手滑加了 @ 而把 back-to-back 流量漏掉。检查失败只上报不阻断——协议违规时后续事务仍要照常采集广播,scoreboard 与波形需要完整的现场来定位根因,monitor 自己「罢工」反而销毁证据。
五、广播:analysis_port 一份事务多方订阅
打包完成的事务通过 uvm_analysis_port 扇出。它内部维护订阅者列表,一次 write 按 connect 顺序逐个调用各订阅者的 write 方法,零订阅者也是合法状态(比如只开断言不开 scoreboard 的冒烟用例)。机制细节在 SystemVerilog TLM 通信 的第五节已经展开,这里只强调 monitor 侧的两个约束。
第一,广播共享同一句柄,所有订阅者看到的是同一个对象,任何订阅者修改字段都会污染其他人;订阅者想留存或加工,自己 clone。第二,write 是非阻塞的 function 链式调用,某个订阅者的 write 写得慢会直接拖慢 monitor 的采集循环,back-to-back 流量下可能漏拍——所以订阅者的 write 要轻,重活(如查参考模型)先丢进 fifo 由后台线程消化。接线方式与 fifo 缓冲的取舍在 scoreboard 的 imp 与队列实现 里有完整对照。
一个 agent 里 monitor 与 driver 共存时(active agent),monitor 照常工作,只是它采的是自己 driver 驱动的信号;纯观测环境(passive agent)里只有 monitor,接线完全一样。这种「驱动与否不影响观测侧」的对称性,正是 monitor 只依赖 virtual interface 与 analysis_port 两个抽象换来的。
复位处理是广播通路之外另一条要写进 monitor 的纪律。采集循环里必须并行盯着 reset:一旦复位生效,手里攒了一半的事务立即丢弃、状态机归位、重新开始等首拍。常见写法是 fork 出两条线程,一条跑采集主循环,一条等复位沿,复位到了就 disable 主线程再重启。漏做这件事的后果很隐蔽:复位前一笔事务采到第三拍,复位后新激励的首拍被当成第四拍续上,广播出一笔「前半截是旧激励、后半截是新激励」的缝合事务,scoreboard 报的数据错会让你在 DUT 里找半天并不存在的 bug。
六、常见坑清单
- 不用 clocking block 直接采样:active region 竞争,换仿真器或改一行 DUT 代码就出随机误报。
- 事务对象复用:广播句柄被下一笔覆盖,scoreboard 看到「上一笔变下一笔」的诡异失败;每笔
create新对象。 - 状态机漏掉 back-to-back:末拍与下一笔首拍同拍时丢事务,对照第三节的双分支写法。
- 复位期间不丢半成品事务:reset 到来时手里攒了一半的
tx必须丢弃并复位状态机,否则复位后被广播出一笔「拼错了头」的事务。 - 在 monitor 里做数据比对:职责越界,比对逻辑改动时被迫动采集代码,复用性归零。
- 协议检查全写进 monitor 的
if:逐拍规则漏毛刺,应该下沉为 SVA 断言。 - 订阅者
write里做重活:拖慢采集循环漏拍;重活进 fifo,见 scoreboard 的 imp 与队列实现。
这一篇在知识体系里的位置
- 向上承接:UVM 组件与 phase 给出 monitor 的组件骨架与 phase 位置,本篇填充它的 run_phase 内部。
- 横向相关:interface 与 clocking block 解释采样机制本身;SystemVerilog TLM 通信 解释广播通路的端口体系。
- 下游消费:scoreboard 的 imp 与队列实现 是这笔事务的第一个订阅者;SVA 断言实践 承接逐拍协议规则。
- 落地延伸:分层 testbench 架构与复用 展示 monitor 在 agent/env 分层中的位置。
总结
SV monitor 协议检查的核心是一条流水线:用 clocking block 在正确调度区采引脚,用状态机把握手波形打包成每笔独立 new 出来的 transaction,按「逐拍归断言、跨拍归 monitor、内容归 scoreboard」的三层分工做检查,最后经 analysis_port 一份句柄广播给所有订阅者——守住「只观测不驱动、广播即让渡」两条纪律,monitor 就能跨项目复用。