SystemVerilog 回调机制 callback:不改类也能注入错误
更新时间:2026-10-01。driver 的主流程应当稳定通用,可不同用例又想“这一笔插个延迟”“那一笔造个错”。靠子类继承会类爆炸,回调(callback)才是工业界答案。
本文要回答的问题
- 回调和继承、虚方法有什么不同,何时用哪个?
- 怎么在 driver 里预留钩子又不污染主流程?
- 一个用例想挂多个回调(既延迟又报错)怎么办?
- UVM 里对应机制是什么?
一、思路:预留空钩子,用例去填
把“可能变化的动作”抽象成回调类,driver 在事务处理前后调用钩子;默认钩子什么都不做,用例按需子类化并挂上去。
二、定义回调基类
systemverilog
class drv_callback;
virtual task pre_tx (ref transaction tr); endtask
virtual task post_tx(ref transaction tr); endtask
virtual function void error_inject(ref transaction tr); endfunction
endclass方法声明为 virtual 且默认空实现——这就是扩展点。
三、driver 持有回调队列并调用
systemverilog
class driver;
drv_callback cbs[$];
function void add_callback(drv_callback cb); cbs.push_back(cb); endfunction
task send(transaction tr);
foreach (cbs[i]) cbs[i].pre_tx(tr);
// ...正常驱动到接口...
foreach (cbs[i]) begin
cbs[i].error_inject(tr);
cbs[i].post_tx(tr);
end
endtask
endclassdriver 完全不知道具体回调内容,主流程零改动。
四、用例挂自定义回调
systemverilog
class delay_cb extends drv_callback;
virtual task pre_tx(ref transaction tr);
#50; // 每笔前插入延迟
endtask
endclass
class crc_cb extends drv_callback;
virtual function void error_inject(ref transaction tr);
tr.bad_crc = 1; // 故意造错
endfunction
endclass
initial begin
drv.add_callback(new delay_cb);
drv.add_callback(new crc_cb); // 多个回调叠加
end一个驱动可同时挂多个回调,分别负责延迟、报错、采样,互不耦合。
五、回调 vs 继承

- 继承适合“换一种 driver”;回调适合“同一个 driver,这一次插点动作”。
- 回调按用例动态增删,继承是编译期定型。
六、常见坑
- 钩子不是 virtual,自定义回调不生效。
- 在回调里直接改 driver 内部状态而非传入的事务,造成隐蔽耦合。
- 回调执行顺序敏感却依赖队列顺序,未文档化。
- 挂了回调忘记摘,污染后续用例。
- 用继承硬塞十几种“带延迟/带错误”的组合,类爆炸。
小结
总结
callback 把"会随用例变化的动作"抽成带空 virtual 钩子(pre_tx/post_tx/error_inject)的基类,driver 只持有一个回调队列并在事务前后逐个调用、主流程零改动,用例按需子类化并可同时挂多个(延迟、报错、采样叠加);它适合"同一个 driver 这一次插点动作",区别于继承那种"换一种 driver"的编译期定型,UVM 的 uvm_callback/do_* 钩子即此思想。