3、验证环境搭建:验证组件的搭建与Testbench架构设计
好,咱们接着聊。上一章我们把验证策略和计划定下来了,现在要动真格的了——搭环境。
说实话,我见过不少团队,计划写得天花乱坠,一到搭环境就露怯。为什么?因为验证环境的好坏,直接决定了你后面几个月的效率。环境搭得烂,你天天在改bug;环境搭得好,你天天在找bug。这区别可大了去了。
3.1 验证组件的核心:Driver、Monitor、Scoreboard
先说说这几个最基础的组件。你想想看,一个DUT(待测设计)摆在那,你怎么跟它打交道?无非就是三件事:发激励、看响应、判对错。对应的就是Driver、Monitor和Scoreboard。
3.1.1 Driver——发激励的“手”
Driver说白了就是DUT的“输入接口”。它把事务级的数据(transaction)转成DUT能认的物理信号(比如并行的数据总线、时钟、使能信号等)。
我个人习惯,Driver里一定要做两件事:
- 协议时序的精确控制——不能多一拍,也不能少一拍
- 驱动能力的检查——比如信号冲突、驱动强度不够等问题
我曾经在一个项目中,Driver里忘了加复位同步逻辑,结果仿真前100个周期数据全是乱的。排查了两天才发现,原来是Driver在复位还没释放完就开始发数据了。嗯,从那以后,我每个Driver里都会加一个wait(reset_done)的检查。
来个简单的代码示例,一个典型的Driver长这样:
class driver extends uvm_driver #(packet_t);
virtual dut_if vif;
function void build_phase(uvm_phase phase);
if(!uvm_config_db #(virtual dut_if)::get(this, "", "vif", vif))
`uvm_fatal("DRV", "No vif found")
endfunction
task run_phase(uvm_phase phase);
packet_t pkt;
forever begin
seq_item_port.get_next_item(pkt);
// 等待DUT准备好
@(posedge vif.clk);
while(!vif.ready) @(posedge vif.clk);
// 驱动数据
vif.data <= pkt.data;
vif.valid <= 1'b1;
@(posedge vif.clk);
vif.valid <= 1'b0;
seq_item_port.item_done();
end
endtask
endclass
@(posedge clk)而不是#10这样的延时。为什么?因为延时是死的,时钟是活的。你换个时钟频率,延时就得全改,太傻了。
3.1.2 Monitor——看响应的“眼”
Monitor是DUT的“输出监听器”。它不驱动任何信号,只负责“看”。把DUT输出的物理信号,重新转回事务级的数据包,然后发给Scoreboard去比对。
这里有个坑,我踩过好几次:Monitor的采样时机。你采样早了,数据还没稳定;采样晚了,下一拍数据已经来了。我个人建议,Monitor里一定要用@(negedge clk)来采样,或者用时钟的采样边沿(比如DDR就用双边沿)。
另外,Monitor里最好加一个monitor_policy的配置项,用来控制它是否检查协议违规。比如:
MONITOR_ONLY:只收集数据,不检查协议MONITOR_CHECK:收集数据的同时,检查协议是否违规
我在一个PCIe项目中,就是靠Monitor的协议检查功能,抓到了一个DUT在TLP包长度字段上的bug。那个bug在功能仿真里根本看不出来,但Monitor一查协议就报错了。
3.1.3 Scoreboard——判对错的“脑”
Scoreboard是整个验证环境的“裁判”。它接收来自Driver的激励数据和来自Monitor的响应数据,然后比对两者是否一致。
但这里有个问题:激励和响应不是一一对应的。比如一个流水线设计的DUT,你发10个包进去,它可能在第5个周期才吐出第一个结果。所以Scoreboard里必须有一个预测模型(predictor),用来计算“如果DUT是对的,它应该输出什么”。
我常用的Scoreboard结构是这样的:
class scoreboard extends uvm_scoreboard;
// 两个fifo,一个存期望值,一个存实际值
uvm_tlm_analysis_fifo #(packet_t) exp_fifo;
uvm_tlm_analysis_fifo #(packet_t) act_fifo;
task run_phase(uvm_phase phase);
packet_t exp_pkt, act_pkt;
forever begin
// 同时从两个fifo取数据
exp_fifo.get(exp_pkt);
act_fifo.get(act_pkt);
// 比对
if(!exp_pkt.compare(act_pkt))
`uvm_error("SCOREBOARD",
$sformatf("Mismatch: exp=%0h, act=%0h",
exp_pkt.data, act_pkt.data))
end
endtask
endclass
compare_with_mask来忽略某些位。
3.2 Agent——组件的“管家”
Agent是什么?说白了,它就是Driver、Monitor、Sequencer的“集合体”。一个Agent对应一个接口协议。比如你有AXI接口,就建一个AXI Agent;有I2C接口,就建一个I2C Agent。
Agent的好处是封装和复用。你想想看,如果每个测试用例都要自己去例化Driver和Monitor,那得多麻烦?Agent把这些都包好了,你只需要在环境里例化Agent,然后通过配置来控制它是active(主动发数据)还是passive(只监听)。
我一般把Agent设计成可配置的:
| 配置项 | 取值 | 说明 |
|---|---|---|
| is_active | UVM_ACTIVE / UVM_PASSIVE | 是否包含Driver和Sequencer |
| interface_type | AXI / AHB / APB / 自定义 | 接口协议类型 |
| check_enable | 0 / 1 | 是否启用协议检查 |
| coverage_enable | 0 / 1 | 是否收集覆盖率 |
Agent的代码结构大致如下:
class agent extends uvm_agent;
driver drv;
monitor mon;
sequencer seqr;
function void build_phase(uvm_phase phase);
// Monitor总是需要
mon = monitor::type_id::create("mon", this);
// Driver和Sequencer只在active模式下创建
if(get_is_active() == UVM_ACTIVE) begin
drv = driver::type_id::create("drv", this);
seqr = sequencer::type_id::create("seqr", this);
end
endfunction
function void connect_phase(uvm_phase phase);
if(get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(seqr.seq_item_export);
endfunction
endclass
3.3 Testbench架构设计——搭积木的艺术
好,组件都有了,怎么把它们拼起来?这就是Testbench架构设计。
我习惯把Testbench分成三层:
- 底层:接口层(Interface Layer)——包含DUT的接口声明、时钟生成、复位逻辑
- 中间层:验证组件层(Verification Component Layer)——包含Agent、Scoreboard、Reference Model等
- 顶层:测试用例层(Test Layer)——包含具体的测试用例、sequence、配置
为什么要分层?说白了就是解耦。你改一个测试用例,不需要动到底层的接口;你换一个DUT版本,只需要改接口层,验证组件层基本不动。
我见过最糟糕的Testbench,是把所有代码都写在一个文件里,Driver、Monitor、Scoreboard全混在一起。结果呢?改一个bug,引入三个新bug。嗯,那滋味,谁改谁知道。
一个典型的Testbench架构图(用文字描述):
+--------------------------------------------------+
| Test Layer |
| +-----------+ +-----------+ +-----------+ |
| | test_case1| | test_case2| | test_case3| |
| +-----------+ +-----------+ +-----------+ |
+--------------------------------------------------+
| Verification Component Layer |
| +--------+ +--------+ +-----------+ |
| | Agent1 | | Agent2 | | Scoreboard| |
| | (AXI) | | (APB) | | | |
| +--------+ +--------+ +-----------+ |
+--------------------------------------------------+
| Interface Layer |
| +--------+ +--------+ +--------+ |
| | AXI IF | | APB IF | | Clk/Rst| |
| +--------+ +--------+ +--------+ |
+--------------------------------------------------+
| DUT |
+--------------------------------------------------+
在实际项目中,我还会加一个环境配置类(env_config),用来统一管理所有组件的配置。比如:
- 是否启用覆盖率收集
- 仿真超时时间
- 日志级别(INFO / WARNING / ERROR)
- 接口的时序参数(比如时钟周期、建立时间、保持时间)
这样做的好处是,你只需要改一个配置文件,就能控制整个验证环境的行为。不用到处找代码去改参数。
uvm_config_db来传递。
3.4 小结
好了,这一章的内容就这些。总结一下:
- Driver:发激励,注意时序和复位同步
- Monitor:看响应,注意采样时机和协议检查
- Scoreboard:判对错,注意预测模型和乱序处理
- Agent:封装Driver+Monitor+Sequencer,可配置active/passive
- Testbench架构:分层设计,接口层、组件层、测试层各司其职
下一章,我们会讲验证环境的启动流程和仿真控制。到时候我会聊聊怎么用UVM的phase机制来控制整个仿真流程。嗯,那个东西,用好了是真香,用不好是真坑。