4. 测试用例开发:SystemVerilog/UVM基础回顾、GEM测试用例模板、断言与检查器编写
好,咱们进入第四章。这一章是实战的起点,说白了就是教你如何写出第一条能跑的GEM测试用例。很多新人一上来就盯着复杂的验证环境看,结果越看越懵。我的建议是——先把手弄脏,写一个最简单的用例,跑通了再说。
我会带着你过一遍SystemVerilog和UVM里最常用的几个概念,然后直接套到GEM的测试模板里。最后再聊聊断言和检查器,这东西在GEM验证里特别重要,我吃过不少亏。
4.1 SystemVerilog/UVM核心回顾
咱们不搞教科书式的复习,只挑GEM验证里真正用得上的东西讲。
4.1.1 接口与时钟块
GEM的接口信号很多,用interface来封装是最合理的做法。我个人习惯把时钟和复位单独拎出来,放在一个clocking block里,这样驱动和采样的时序关系一目了然。
interface gem_if (input bit clk, input bit rst_n);
logic [31:0] addr;
logic [63:0] data;
logic req;
logic ack;
clocking cb @(posedge clk);
default input #1ns output #1ns;
output addr, data, req;
input ack;
endclocking
modport TB (clocking cb, input rst_n);
endinterface
你看,这样写之后,测试用例里驱动信号就变成了if.cb.addr <= value,干净利落。
4.1.2 UVM中的sequence与sequencer
UVM里最常用的就是sequence。GEM的测试用例本质上就是一个sequence。你只需要记住三件事:
- body():你的测试逻辑写在这里
- `uvm_do:发送transaction的宏
- sequencer:负责把transaction发给driver
我在项目中见过有人把几百行代码全塞进body()里,结果调试的时候根本找不到问题在哪。我的建议是——把不同的操作拆成独立的sequence,然后用p_sequencer来组合它们。
4.1.3 工厂覆盖与配置机制
GEM验证里经常需要替换某个组件的行为。比如你想让某个agent在异常模式下工作,用set_type_override_by_type就能搞定,不用改一行环境代码。
uvm_config_db来传递配置对象,比用全局变量安全得多。我习惯在build_phase里统一读取配置,这样后期维护起来特别省心。
4.2 GEM测试用例模板
好,咱们直接看GEM的测试用例长什么样。下面这个模板是我在多个项目中打磨出来的,你可以直接拿来用。
class gem_base_test extends uvm_test;
`uvm_component_utils(gem_base_test)
gem_env m_env;
gem_vseqr m_vseqr;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_env = gem_env::type_id::create("m_env", this);
m_vseqr = gem_vseqr::type_id::create("m_vseqr", this);
endfunction
task run_phase(uvm_phase phase);
gem_base_vseq vseq;
phase.raise_objection(this);
vseq = gem_base_vseq::type_id::create("vseq");
vseq.start(m_vseqr);
phase.drop_objection(this);
endtask
endclass
这个模板有几个关键点:
- base test:只做环境创建和sequence启动,不写具体测试逻辑
- virtual sequencer:用来协调多个agent的sequence
- objection机制:确保仿真不会提前结束
你想想看,如果每个测试用例都从零开始写,那得重复多少代码?所以我的做法是——所有公共的东西都放在base test里,子类只覆盖需要改的部分。
4.2.1 一个具体的GEM测试用例
假设我们要测试GEM的寄存器写操作,可以这样写:
class gem_reg_write_test extends gem_base_test;
`uvm_component_utils(gem_reg_write_test)
function void build_phase(uvm_phase phase);
// 覆盖配置,比如让寄存器模型工作在非阻塞模式
uvm_config_db#(bit)::set(this, "m_env.*", "blocking_mode", 0);
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
gem_reg_write_vseq vseq;
phase.raise_objection(this);
vseq = gem_reg_write_vseq::type_id::create("vseq");
// 设置寄存器地址和数据
vseq.reg_addr = 32'hA000_0000;
vseq.reg_data = 64'hDEAD_BEEF;
vseq.start(m_vseqr);
phase.drop_objection(this);
endtask
endclass
你看,只需要几十行代码,一个完整的测试用例就出来了。这就是模板化的好处。
4.3 断言与检查器编写
断言这东西,说白了就是给仿真装上一双眼睛。GEM的协议比较复杂,靠人工看波形根本盯不过来。我建议你从一开始就把断言写进去,别等到发现问题了再补。
4.3.1 立即断言 vs 并发断言
| 类型 | 使用场景 | 示例 |
|---|---|---|
| 立即断言 | 检查组合逻辑、状态机跳转 | assert (state == IDLE); |
| 并发断言 | 检查时序关系、协议握手 | assert property (@(posedge clk) req |=> ack); |
我个人更偏爱并发断言,因为它能描述时序关系。比如GEM的请求-应答协议,用一条|=> 就能搞定。
4.3.2 GEM协议断言示例
咱们写一个最常用的——请求后必须在5个时钟内收到应答:
property req_ack_timing;
@(posedge gem_if.clk)
disable iff (!gem_if.rst_n)
gem_if.req |=> ##[1:5] gem_if.ack;
endproperty
assert_req_ack: assert property (req_ack_timing)
else $error("Request not acknowledged within 5 cycles");
else分支,哪怕只是打印一条信息。这样至少能知道断言被触发了。
4.3.3 检查器编写
断言适合检查协议层面的东西,但有些功能检查需要更复杂的逻辑。这时候就要写检查器(checker)了。
检查器本质上是一个独立的验证组件,它监听接口上的信号,然后做数据比对。比如GEM的DMA传输,你需要检查传输的数据是否正确:
class gem_data_checker extends uvm_component;
`uvm_component_utils(gem_data_checker)
uvm_analysis_imp #(gem_transaction, gem_data_checker) m_imp;
function void write(gem_transaction t);
if (t.data !== t.expected_data) begin
`uvm_error("DATA_CHECK", $sformatf(
"Data mismatch: addr=%0h, got=%0h, exp=%0h",
t.addr, t.data, t.expected_data))
end
endfunction
endclass
你看,检查器的核心就是一个write函数,每次收到transaction就做一次比对。简单、直接、有效。
4.4 本章知识体系
下面这张图把本章的核心内容串起来了,你可以对照着看看自己掌握了多少。
本章要点:
- 用interface封装GEM接口信号,用clocking block控制时序
- 测试用例模板化,公共逻辑放在base test里
- 断言要写早、写全,每个断言都加上else分支
- 检查器负责数据比对,和断言互补