是什么:Verilog 是 IEEE 1364 标准(1995 年首次标准化,常用 Verilog-2001/2005);SystemVerilog 是 IEEE 1800 标准(2005 年首版)。从 1800-2009 开始,Verilog 1364 的全部内容被并入 1800,两者合并为单一标准 —— 也就是说,Verilog 是 SystemVerilog 的子集,任何合法 Verilog-2005 代码都是合法 SystemVerilog。最新的语言标准是 IEEE 1800-2023;而 UVM 类库的标准是 IEEE 1800.2(详见第 4 节)。
为什么验证岗必学 SV:因为 SV 不是"更好的 Verilog",而是给语言补上了验证的半边天。写 RTL 需要的是"描述电路";做验证需要的却是"写软件去折磨电路"——用面向对象组织 testbench、用约束随机生成海量激励、用功能覆盖率量化验证进度、用断言检查时序协议。这四件事纯 Verilog 一件都做不了。所以 IC 验证岗的招聘要求里,"SystemVerilog + UVM"是和"Verilog + RTL"并列的标配。
| 维度 | Verilog(IEEE 1364) | SystemVerilog(IEEE 1800) |
|---|---|---|
| 定位 | 硬件描述语言(建模电路) | 设计 + 验证双用途语言 |
| 抽象层级 | 开关级 / 门级 / RTL | 门级 / RTL / 事务级、类级 testbench |
| 数据类型 | wire / reg,4 态 | + logic、enum、struct、动态数组/队列/关联数组 |
| 过程块 | always / initial | + always_ff / always_comb / always_latch(意图明确) |
| 验证设施 | 只有 initial 手写定向激励 | + class/OOP、约束随机、功能覆盖率、SVA 断言、DPI-C |
| 与另一者的关系 | 子集 | 超集,完全兼容 Verilog |
怎么做:学习路线分三步 —— ①先会用 Verilog 写可综合 RTL(本站 08-03);②补 SV 的验证扩展:新类型 → 类与 OOP → 约束随机 → interface 与线程(本页第 1~3 节);③进 UVM 方法学(本页第 4~8 节)。写 RTL 时也可以直接用 SV 子集(always_ff/always_comb/logic),意图更清晰、综合器检查更严。
是什么:SV 在 wire/reg 之上补了一组类型,目标是两件事:①终结 reg 还是 wire 的无谓之争;②给验证代码提供灵活的"数据容器"(类似软件语言的数组/哈希表)。
enum {IDLE, RUN} state;),状态机首选:编译器/仿真器能检查非法赋值,比 localparam 更安全。typedef logic [31:0] word_t;),大工程里让信号位宽语义化、可维护。typedef struct packed {logic [31:0] addr; logic [7:0] len;} req_t;),描述协议帧、总线请求非常直观,且 packed struct 可综合。int da[]; da = new[16]; da.delete();,适合"大小要到运行时才知道"的数据(如一帧图像)。int q[$]; q.push_back(x); x = q.pop_front();,scoreboard 的比对缓存、参考模型 FIFO 的首选。bit [31:0] mem[bit [31:0]];,建模稀疏存储器(只为被写过的地址存数据)、按 addr 快速查账。// 验证代码里的典型类型组合
typedef enum logic [2:0] {OP_WRITE, OP_READ, OP_BURST} op_e; // 枚举:操作码
typedef struct packed { // 打包结构体:一条总线请求
logic [31:0] addr;
logic [7:0] len;
op_e op;
} req_t;
class pkt;
req_t fixed_req; // struct 当普通成员
int frame[]; // 动态数组:整帧数据
int q[$]; // 队列:等待比对的响应
bit [31:0] model [bit [31:0]]; // 关联数组:稀疏参考存储器
endclass
| 类型 | 一句话记忆 | 典型用例 |
|---|---|---|
| logic | 单驱动信号万能写法 | 端口、内部信号、testbench 信号(默认选它) |
| enum | 带名字、带检查的常量集 | 状态机 state、操作码、phase 名 |
| typedef | 类型别名 | word_t、req_t,让位宽语义化 |
| struct packed | 字段打包成一根总线 | 协议帧描述、事务字段打包(可综合) |
| 动态数组 [] | 运行时定长、可重建 | 按需分配的帧缓冲 |
| 队列 [$] | 头尾进出、自动伸缩 | scoreboard 期望队列、参考 FIFO |
| 关联数组 [k] | 稀疏哈希表 | 稀疏存储器模型、按地址索引的比对 |
if (aa.exists(k)) 直接读不存在的 key,得到默认 0 却不自知。是什么:SV 的 class 是纯软件概念(只在验证代码中存在,不可综合):字段描述数据、方法(function/task)描述行为,通过 new() 在堆上创建对象,变量里存的其实是句柄(handle,类似 C 的指针)。三大特性:封装(字段+方法打包)、继承(extends)、多态(virtual 方法)。
为什么验证需要 OOP:验证的本质是"生成事务 → 驱动 DUT → 比对响应"。事务(transaction)、比对器、驱动器都是"有数据有行为"的实体,用类封装后才能:一个类多处复用、派生新类扩展字段而不改老代码、用父类句柄统一管理不同子类对象——这正是 UVM 全部组件都是类的根本原因。
怎么做——先记住四个关键机制:
function new(string name="t"); super.new(name); endfunction,分配内存 + 初始化;有父类必先调 super.new()。virtual 后,子类同名方法会覆盖它;通过父类句柄调用时,执行的是对象实际类型(子类)的版本——不加 virtual 则按句柄类型(父类)静态绑定,多态失效。t2 = t1; 复制的是句柄(两个名字指向同一个对象),不是拷贝对象;拷贝要用 copy()/clone()。== 和 === 对类句柄比较的都是"是否指向同一对象"(身份比较):区别在于一侧为 null 时 == 结果是 x,而 === 恒为 0/1;比较对象内容要用 compare() 方法。// 一个完整的数据事务类:激励的"最小单元"
class my_txn;
// —— 字段 ——
rand bit [31:0] addr; // rand:参与随机化的字段
rand bit [31:0] wdata;
rand bit [3:0] len;
bit [31:0] rdata; // 响应数据(DUT 返回,不随机)
bit err;
constraint c_legal {
addr[1:0] == 2'b00; // 4 字节对齐
len inside {[1:8]};
}
// —— 构造:new() 分配内存并初始化 ——
function new(bit [31:0] addr = 0);
this.addr = addr; // this 区分成员与参数
endfunction
// —— 行为:virtual 让子类可覆盖,实现多态 ——
virtual function string show();
return $sformatf("addr=%h wdata=%h len=%0d err=%0d", addr, wdata, len, err);
endfunction
virtual function bit compare(my_txn rhs); // 内容比较(== 只比句柄)
return (addr == rhs.addr) && (wdata == rhs.wdata) && (len == rhs.len);
endfunction
endclass
// 继承:突发事务 = 通用事务 + 突发字段
class my_burst_txn extends my_txn;
rand bit [2:0] burst; // 新增字段
function new(bit [31:0] addr = 0);
super.new(addr); // 先完成父类初始化
endfunction
function string show(); // 覆盖(override)父类方法
return {super.show(), $sformatf(" burst=%0d", burst)};
endfunction
endclass
// 多态:父类句柄指向子类对象,调用的是子类版本
my_txn t; // 父类句柄
my_burst_txn b = new(32'h1000); // 子类对象
t = b; // 句柄赋值:复制句柄,不复制对象
$display("%s", t.show()); // → 调 my_burst_txn::show()(show 是 virtual)
$display("%0d", (t == b)); // → 1:同一个对象;内容比较请用 t.compare(b)
是什么:把对象字段标成 rand(每次随机)或 randc(循环遍历,不重复),再用 constraint 块描述合法值域与字段间关系,调用 obj.randomize() 时,求解器自动找一个满足所有约束的值。这是"约束随机验证(CRV)"的语言基础:定向用例只能测工程师想到的场景,约束随机能批量撞出想不到的角落。
怎么做——掌握五个常用写法:
class txn;
rand bit [31:0] addr;
randc bit [2:0] mode; // randc:8 个值轮着来,不重复取完再循环
rand bit [7:0] len;
bit [31:0] rdata; // 非 rand:人工/响应赋值
constraint c_len { len inside {[1:16]}; } // inside:集合/范围
constraint c_dist { addr dist { 32'h0 := 10, // dist:加权随机
[32'h1000:32'h1FFF] := 5,
32'hFFFF_FFFF :/ 40 }; } // :/ 权重被区间均分
constraint c_align { len < 8 -> addr[2:0] == 3'd0; } // -> 蕴含:len 小则强制对齐
constraint c_solve { solve mode before addr; } // 指定求解顺序
endclass
txn t = new();
assert(t.randomize()) // ★ 必须检查返回值!
else $fatal(1, "randomize failed"); // 求解失败返回 0
assert(t.randomize() with { addr == 32'h1234; }) // inline:临时追加约束
else $fatal(1, "inline randomize failed");
randc 位宽过大、约束互相矛盾都会让求解时间爆炸甚至失败。c.constraint_mode(0) 关闭约束缩小范围;③rand_mode(0) 冻结可疑变量看是否可行;④常见根因清单:约束互相矛盾、约束引用了 null 句柄成员、给非 rand 变量加了约束、出现"x 值参与求解"(字段未初始化为 4 态 x)。with 写成覆盖而非追加约束(它是与类内约束求交集,交集为空就失败);③约束里用了 4 态未初始化信号,求解器把 x 当约束的一部分,行为不可移植;④soft constraint(软约束)与普通约束的取舍没搞清——冲突时软约束让位,硬约束冲突直接失败。是什么:interface 是一组信号的具名打包:把一条总线的 valid/ready/data 等信号在接口里声明一次,DUT 和 testbench 都引用同一个接口实例。它还能带两类"附件"——modport(给不同角色提供不同方向的视图)和 clocking block(定义"在时钟沿之前多久采样、之后多久驱动")。
为什么比 Verilog 端口列表强:①Verilog 里改一个信号名要同时改 DUT、TB、多层例化的端口连接;interface 只改一处。②modport 让方向错误在编译期暴露(DUT 侧想驱动 input 会直接报错)。③clocking block 解决 TB 与 DUT 的采样竞争:testbench 是零延时的软件环境,直接在时钟沿驱动/采样会产生竞态;clocking 规定"沿前 1step 采样、沿后 #1 驱动",激励和采样都发生在"安全的时刻"。④类(class)是动态的、不能直接引用静态层次信号,必须通过 virtual interface 才能摸到接口——这是 7.1 config_db 的核心用途。
// Verilog 写法:每个模块重复声明一整套端口,改一个信号要动 N 处
module dut (input clk, input valid, input [31:0] data, output ready, /*...*/);
module tb (output reg valid, output reg [31:0] data, input ready, /*...*/);
// SV 接口:信号打包一次,所有模块共享同一个实例
interface bus_if (input logic clk, rst_n);
logic valid;
logic [31:0] addr;
logic [31:0] wdata;
logic ready;
clocking drv_cb @(posedge clk); // clocking block:driver 用它驱动
default input #1step output #1ns; // 采样:沿前 1 个 time step;驱动:沿后 1ns
output valid, addr, wdata; // driver 的驱动方向
input ready; // driver 的采样方向
endclocking
modport dut (input clk, rst_n, valid, addr, wdata, output ready); // DUT 视角
modport tb (output valid, addr, wdata, input clk, rst_n, ready); // TB 视角
endinterface
// 例化与连接:一根接口"线缆"接到两端
// bus_if u_if (clk, rst_n); → dut u_dut (u_if.dut); testbench 用 u_if / u_if.drv_cb
vif.valid <= 1; 不经 clocking block——高概率采样竞争,波形"看起来对"但偶发错拍;②modport 方向写反,DUT 把 ready 当输出;③virtual interface 未通过 config_db 传入就访问,null 句柄崩溃;④接口在 TB 里例化了两个实例却想让 DUT/TB 共享——接口必须同名同例化才互通。是什么:testbench 天生是"多线程程序":同时发激励、收响应、记覆盖率、掐超时。SV 提供四件套:fork-join(线程并行/汇合)、mailbox(线程间传数据的队列)、semaphore(互斥钥匙)、event(同步触发)。UVM 里的 sequencer/analysis 本质上就是这些原语的封装升级。
| 写法 | 父线程行为 | 典型场景 |
|---|---|---|
| fork … join | 等全部子线程结束才继续 | 多路激励并行发完,统一收尾 |
| fork … join_any | 任一子线程结束就继续 | 激励线程 + 超时看门狗,谁先完成谁触发下一步 |
| fork … join_none | 不等,立即继续(子线程在首个阻塞点才真正启动) | testbench 顶部同时拉起 driver/monitor/coverage 线程 |
| disable fork / wait fork | 杀掉未完成子线程 / 等所有子线程结束 | join_any 后清场;退出前确保子线程跑完 |
// fork-join_any + disable fork:经典"看门狗"结构
initial begin
fork
send_all_pkts(); // 线程 1:发激励
begin : watchdog // 线程 2:超时报警
#10us;
$display("ERROR: timeout!");
end
join_any // 任一结束就往下走
disable fork; // 关掉剩下的线程(超时后停发激励 / 发完后关看门狗)
end
// mailbox:generator 与 driver 之间的"传送带"(纯 SV TB 的经典用法)
mailbox #(my_txn) mb = new();
// generator 线程: mb.put(t); // 传事务;容量设上限后,满则阻塞
// driver 线程: mb.get(t); // 空则阻塞等待;try_get 空则立即返回 0(非阻塞)
// semaphore:多线程互斥共享一个资源(如存储器端口)
semaphore mem_key = new(1); // 1 把钥匙
task mem_access();
mem_key.get(); // 拿钥匙,没有就等(互斥进入)
... 访问共享存储 ...
mem_key.put(); // 还钥匙
endtask
// event:阶段同步("复位完成"通知所有线程)
event rst_done;
initial begin ... 执行复位序列 ... ->rst_done; end // -> 触发
initial begin @rst_done; ... end // 等触发(错过则一直等)
initial begin wait(rst_done.triggered); ... end // triggered 状态可查询,不怕错过
| 机制 | 本质 | 选型口诀 |
|---|---|---|
| mailbox #(T) | 参数化消息队列(put/get 阻塞,try_* 非阻塞) | 传数据:生产者→消费者 |
| semaphore | 计数钥匙,get/put | 抢资源:互斥、限流 |
| event | 一次性触发/等待 | 等事件:阶段同步、条件通知 |
| fork-join 族 | 并行与汇合控制 | 排线程:并行、看门狗、清场 |
是什么:UVM(Universal Verification Methodology,通用验证方法学)是一套用 SystemVerilog 写成的开源类库 + 运行框架:它提供 uvm_component/uvm_sequence 等基类(类库)、phase 调度与 factory/config_db 等机制(框架),并约定了一套组织 testbench 的标准结构(方法学)。它由 Accellera 维护,已标准化为 IEEE 1800.2-2020(1800 语言标准中抽出 UVM 部分单独成册),官方参考实现 UVM 2020-3.2 可从 Accellera 或 GitHub(accelera-official/uvm-core)下载。
为什么需要方法学——没有 UVM 的世界:每人手搓一套 testbench 结构,换个项目重写一遍;激励全靠定向写,覆盖率全凭感觉;A 公司的经验到 B 公司归零。现代验证的三大支柱正对应 UVM 的三个设计目标:
怎么做——建立两个正确认知:①UVM 不是新语言,它就是一堆 SV 类,看懂类的继承层次(uvm_object 与 uvm_component 两大家族)比背宏更重要;②不要一上来背 API,先理解 8.2 节"最小环境"的骨架,再逐个机制(工厂/组件树/三角/TLM/config_db/RAL/phase)往里加。血统上 UVM 继承自 OVM 与 VMM 两代方法学,2011 年由 Accellera 推出 1.0,如今 1800.2 时代版本号形如 2020-3.2。
是什么:工厂(factory)是 UVM 的"对象制造局":每个类用注册宏(`uvm_component_utils / `uvm_object_utils)在工厂登记"类型名 ↔ 类型"的对照表;之后所有组件/对象都通过 Type::type_id::create(...) 创建。创建时工厂先查表:有没有 override(替换类型)?有就造替换者,没有才造原类型。
为什么必须走工厂:①override 是核心价值——测试需要换掉环境里的某个组件(如注入错误的 driver、扩展版 scoreboard)时,用 set_type_override 一行替换,env/test 代码零修改;若用 new,类型在编译期写死,override 无从谈起。②create 时把 parent 句柄传入,UVM 才能把对象挂进组件树、纳入 phase 调度。③按字符串创建也让"用例脚本化配置"成为可能。
class my_driver extends uvm_driver #(my_txn);
`uvm_component_utils(my_driver) // 注册:component 家族用这个宏
...
endclass
class my_txn extends uvm_sequence_item;
`uvm_object_utils(my_txn) // 注册:object 家族(transaction/sequence)用这个
...
endclass
// 创建:一律走工厂(create),不要 new
drv = my_driver::type_id::create("drv", this); // 参数:实例名 + 父组件句柄
req = my_txn ::type_id::create("req");
// override:工厂的杀手锏 —— test 里一行替换全局类型
my_driver::type_id::set_type_override(my_driver_v2::get_type()); // 类型替换
// 也可只替换某个实例:factory.set_inst_override(...);(实例级 override)
是什么:UVM testbench 是一棵组件树:uvm_component 派生的组件按 parent-child 关系挂树,顶层是 uvm_test_top(run_test 创建),下面依次 env → agent → driver/sequencer/monitor,scoreboard/coverage 挂在 env。树有两个用途:①phase 按树调度(7.3 节);②config_db 按"路径"寻址(7.1 节)——每个组件的全名就是它在这棵树上的路径。
| 组件(基类) | 一句话职责 |
|---|---|
| my_test(uvm_test) | 顶层用例:决定配置(is_active、override、默认 sequence)与验证目标 |
| my_env(uvm_env) | 容器:把 agent + scoreboard + coverage 组装成可整体复用的环境 |
| my_agent(uvm_agent) | 一路接口的打包:active 含 driver+sequencer+monitor;passive 仅 monitor |
| my_sequencer(uvm_sequencer) | 激励仲裁器:多条 sequence 的事务流排队、仲裁后发给 driver |
| my_driver(uvm_driver) | 翻译官:把事务(transaction)翻译成引脚级时序波形打进接口 |
| my_monitor(uvm_monitor) | 旁路观察者:被动采样引脚,还原成事务,经 analysis_port 广播 |
| my_scoreboard(uvm_scoreboard) | 裁判:参考模型 + 数据比对,发现不一致立刻报错 |
| my_seq / my_txn(uvm_sequence / uvm_sequence_item) | 激励剧本 / 数据包 —— 属于 object 家族,不在组件树上 |
active 与 passive 两种模式:agent 用 uvm_active_passive_enum is_active 配置。active 模式创建 driver+sequencer+monitor,完整地"发激励 + 采样";passive 模式只例化 monitor,用于:只读不写的观测通道(如性能统计)、chip 级环境里复用 block 级 monitor 做检查、多个 agent 共享一路激励等。复用的意义在于:monitor 永远都在,scoreboard/coverage 在任何模式下都能接上它。
是什么:这是 UVM 最核心的协作结构。sequence(uvm_sequence 派生,object)是"激励剧本":它 create 一件件事务,按剧情随机并交给 sequencer;sequencer(uvm_sequencer,component)是"剧场调度":缓存并仲裁多条剧本的事务流;driver(uvm_driver,component)是"演员":从 sequencer 领事务,演成引脚波形。三者通过 seq_item_port / seq_item_export 握手。
怎么做——背下六步握手:sequence 侧 start_item(req) 申请发送权(阻塞直到被授权)→ req.randomize() 随机 → finish_item(req) 交付(阻塞直到 driver 完成);driver 侧 get_next_item(req) 领事务(没有就阻塞等)→ 驱动引脚 → item_done() 报告完成。注意:driver 里不需要 new 事务,req 是 uvm_driver 自带的成员。
// —— sequence:只管"生成什么激励",不管"怎么打到引脚" ——
class my_seq extends uvm_sequence #(my_txn);
`uvm_object_utils(my_seq)
task body();
repeat (10) begin
req = my_txn::type_id::create("req");
start_item(req); // ① 申请发送权(等仲裁)
assert(req.randomize() with { len inside {[1:16]}; }); // ② 随机
finish_item(req); // ③ 交付(等 driver item_done)
end
endtask
endclass
// —— driver:只管"把事务演成波形",不管"激励是什么剧情" ——
class my_driver extends uvm_driver #(my_txn);
`uvm_component_utils(my_driver)
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req); // ④ 领事务(空则阻塞)
drive_pins(req); // ⑤ 按协议驱动引脚(若干拍)
seq_item_port.item_done(); // ⑥ 报告完成,sequencer 放行下一个
end
endtask
endclass
// —— sequence 挂到 sequencer 的两种方式 ——
// 方式一:test 的 run_phase 里直接 start(时序可控,常配合 objection)
task my_test::run_phase(uvm_phase phase);
my_seq seq;
phase.raise_objection(this);
seq = my_seq::type_id::create("seq");
seq.start(env.agt.sqr); // 参数:挂到哪个 sequencer
phase.drop_objection(this);
endtask
// 方式二:config_db 挂"默认 sequence"(由 main_phase 自动启动,回归批跑常用)
uvm_config_db #(uvm_object_wrapper)::set(this, "env.agt.sqr.main_phase",
"default_sequence", my_seq::type_id::get());
是什么:TLM(Transaction Level Modeling)是 UVM 组件间传事务的标准接口,核心是三件套:port(发起方/生产者持有)、export(中转方)、imp(终点,真正实现 write()/put() 等回调)。验证平台里用得最多的是analysis 族广播:monitor 持有 analysis_port,每次采到一个事务就 ap.write(t),所有连接上来的订阅者(scoreboard、coverage collector)同时收到——生产者完全不知道也不关心有几个订阅者。
// —— 生产者:monitor 声明 analysis_port 并广播 ——
class my_monitor extends uvm_monitor;
uvm_analysis_port #(my_txn) ap; // 广播口:可连多个订阅者
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
// ... run_phase 里采样后:
// my_txn t = my_txn::type_id::create("t");
// t.addr = vif.addr; t.wdata = vif.wdata;
// ap.write(t); // 一次广播,所有订阅者同时收到
endclass
// —— 消费者:scoreboard 用 analysis_imp 作为终点,自己实现 write() ——
class my_scoreboard extends uvm_scoreboard;
uvm_analysis_imp #(my_txn, my_scoreboard) item_imp; // imp 回指本组件
function new(string name, uvm_component parent);
super.new(name, parent);
item_imp = new("item_imp", this); // 第二参数告诉 imp:write 由我实现
endfunction
function void write(my_txn t); // imp 的回调(必须是 function)
`uvm_info("SCB", $sformatf("recv addr=%h data=%h", t.addr, t.wdata), UVM_LOW)
endfunction
endclass
// —— 装配:env 的 connect_phase 里连接(port 连 imp),一个 port 可连多个 imp ——
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
agt.mon.ap.connect(scb.item_imp); // port → imp
agt.mon.ap.connect(cov.item_imp); // 同一 port 再连 coverage,天然广播
endfunction
| 端口类型 | 角色 | 可以 connect 到 | 典型例子 |
|---|---|---|---|
| port | 发起方(生产者) | 同层兄弟组件的 export / imp;本组件或父组件的 port(把子接口向上穿透) | mon.ap、drv.seq_item_port |
| export | 中转/汇合 | imp;子组件或本组件的 export(穿透) | sqr.seq_item_export、sb.analysis_export |
| imp | 终点(实现回调) | 不可再 connect | sb 的 write()、cov 的 write() |
mon.ap.connect(this.ap) 把 monitor 的接口暴露给上层)。是什么:uvm_config_db #(T) 是一个全局"键值配置数据库":高层组件(test)在 build_phase 里 set(写入),低层组件在 build_phase 里 get(读取)。参数四元组是 (context 上下文, inst_name 实例路径, field 字段名, value 值)——最终寻址键 = context 的全名 + inst_name + field。
为什么需要它:把"配置什么"(用例视角)和"怎么用配置"(组件实现)分开:同一个 agent 不改一行代码,靠 set 就能切成 active/passive;虚接口(virtual interface)从静态层次"递进"动态类世界的唯一正道就是 config_db;不同 test 复用同一 env,各自注入不同激励/覆盖开关。
// —— set:写在 test(或上层组件)的 build_phase ——
function void my_test::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db #(virtual bus_if)::set(this, "env.agt.drv", "vif", u_if); // 虚接口
uvm_config_db #(uvm_active_passive_enum)::set(this, "env.agt", "is_active", UVM_ACTIVE);
uvm_config_db #(int)::set(this, "env", "num_masters", 2);
endfunction
// —— get:写在组件自己的 build_phase(此时上层已 set 完:build 自顶向下) ——
function void my_driver::build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual bus_if)::get(this, "", "vif", vif)) // inst_name 空串 = 自己
`uvm_fatal("NOVIF", "vif 没拿到:set 的路径与组件全名对得上吗?")
endfunction
| 场景 | set 写法(set 的 context=this,即 test) | get 写法(组件内) |
|---|---|---|
| 虚接口传入 driver | set(this, "env.agt.drv", "vif", u_if) | get(this, "", "vif", vif) |
| agent 切 active/passive | set(this, "env.agt", "is_active", UVM_PASSIVE) | get(this, "", "is_active", is_active) |
| env 级参数(通道数等) | set(this, "env", "num_masters", 2) | get(this, "", "num_masters", n) |
| 通配符批量配置 | set(this, "env.agt*", "en_cov", 1) | 各组件照常 get |
| 挂在某个 phase 的默认 sequence | set(this, "env.agt.sqr.main_phase", "default_sequence", my_seq::type_id::get()) | 由框架在 main_phase 自动 get |
get(this, "", field, val)——"" 表示"取发给我自己全名的那个配置"。是什么:RAL(Register Abstraction Layer,寄存器抽象层)是 UVM 里对 DUT 寄存器的软件镜像模型:寄存器 → uvm_reg,位域 → uvm_reg_field,一批寄存器 → uvm_reg_block,访问入口/地址映射 → uvm_reg_map。模型建成后,验证代码用 rm.ctrl.write/read/mirror/update 等高层操作访问寄存器,RAL 负责翻译成总线事务(经 adapter 交给 sequencer)。
为什么寄存器验证要用 RAL:①复用:写一次"使能→读状态"的序列,任何 block/chip 上都能跑;地址、位域、复位值都在模型里,换地址不改测试;②自动化:mirror/predict 内建"期望值账本",读写后自动比对,位冲(bit-bash)、复位值检查等经典序列可直接套用;③前后门统一:同一行代码换个参数就在前门(真实总线)与后门(直插内部)之间切换;④寄存器模型通常由 Excel/IP-XACT 脚本生成,与 spec 天然一致。
// —— 建模:一个 32 位控制寄存器 ——
class my_ctrl_reg extends uvm_reg;
rand uvm_reg_field enable; // bit 0
rand uvm_reg_field mode; // bit [2:1]
`uvm_object_utils(my_ctrl_reg)
function new(string name = "my_ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE); // 32 位寄存器
endfunction
function void build();
enable = new("enable");
enable.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 1); // 父,位宽,低位,属性,易失?,复位值,...
mode = new("mode");
mode.configure(this, 2, 1, "RW", 0, 2'b00, 1, 1, 1);
endfunction
endclass
// —— 组块 + 地址映射 + 接上 sequencer 与 adapter ——
class my_reg_block extends uvm_reg_block;
my_ctrl_reg ctrl;
function new(string name = "my_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
function void build();
ctrl = my_ctrl_reg::type_id::create("ctrl");
ctrl.build(); ctrl.configure(this);
default_map = create_map("map", 'h0000, 4, UVM_LITTLE_ENDIAN); // 基址,总线宽 4B
default_map.add_reg(ctrl, 'h00, "RW"); // 偏移与读写属性
lock_model(); // 锁定模型
endfunction
endclass
// —— 使用:env 里接总线,test 里当"账本"用 ——
rm.build(); rm.lock_model();
rm.default_map.set_sequencer(env.agt.sqr, reg2bus_adapter); // 前门通路:map→sequencer
rm.default_map.set_auto_predict(1); // 自动镜像预测
rm.ctrl.write(status, 32'h0000_0007); // 前门写:走真实总线
rm.ctrl.read(status, rdata); // 前门读:自动与镜像比对
rm.ctrl.poke(status, 32'h0000_0001); // 后门写:直插内部状态
rm.ctrl.mirror(status, UVM_CHECK); // 读回 DUT 与模型核对
| 访问方式 | 路径 | 特点 | 典型用途 |
|---|---|---|---|
| 前门 frontdoor | map 翻译地址 → adapter 变总线事务 → sequencer/driver 真实读写 | 慢,但真实走协议,顺带验证总线与译码 | 绝大多数功能验证 |
| 后门 backdoor | peek/poke 按层次路径(或 DPI)直接改 DUT 内部 | 零时间完成,绕过总线逻辑 | 快速预置状态、大容量表项初始化、与前门结果对照排查 |
是什么:UVM 把仿真生命周期切成一系列 phase(阶段),框架按固定顺序在整棵组件树上调度:先所有组件依次完成 build,再依次 connect……组件只需覆写自己关心的 phase(function void build_phase(uvm_phase phase); / task run_phase(...))。
| phase | 类型 | 方向 | 干什么 |
|---|---|---|---|
| build | function | 自顶向下 | create 子组件、set/get config_db |
| connect | function | 自底向上 | 连接 TLM port/export、挂 sequencer 与 adapter |
| end_of_elaboration | function | 自底向上 | 树已完整:检查连接、print_topology 打印拓扑 |
| start_of_simulation | function | 自底向上 | 仿真前最后准备(打印配置、加载存储) |
| run / main 等任务 phase | task | 整棵树并行 | 激励与采样;objection 控制结束时间 |
| extract → check → report | function | 自底向上 | 收集结果 → 检查断言/计数 → 打印报告 |
| final | function | — | 收尾(关文件等) |
为什么 build 自顶向下、connect 自底向上?①build 先父后子:子组件是父组件在 build 里 create 出来的(不先有父就没有人 create 子),且父在 build 里 set 的 config_db,恰好能在子 build 里 get 到——顺序保证了"配置先于使用者存在"。②connect 先子后内再外:connect 要引用子组件的端口(此时都已建好),且父级"向外暴露"的穿透连接必须等内部先连好才有意义。③run 类任务 phase 在全树并行执行(driver/monitor/sequence 同时跑),用 raise_objection/drop_objection 告诉框架"还有活没干完",最后一个 objection 放下仿真才结束。
是什么:UVM 统一了打印:所有消息经 report server 管控,统一格式(时间 / 组件全名 / 消息 ID / 内容)、统一计数与开关。四条宏对应四级严重度:`uvm_info(ID, msg, verbosity)、`uvm_warning、`uvm_error(计数不停表)、`uvm_fatal(立即终止)。
verbosity(冗余度)怎么工作:info 消息自带一个"重要度"标签;仿真时全局设一个冗余度阈值(默认 UVM_MEDIUM),消息重要度数值 ≤ 阈值才打印。于是同一份代码:日常回归静默跑,排查问题把阈值调到 HIGH/FULL,海量细节日志就涌出来——不用改代码、不用重编译,命令行 +UVM_VERBOSITY=UVM_HIGH 即可。消息 ID 是过滤钥匙:可对特定组件/ID 单独提高或降低冗余度(如 set_report_id_verbosity),或用 report catcher 拦截改写特定消息。
// 用法:ID 大写短名(方便过滤),内容可用 $sformatf 拼变量,最后一个参数是冗余度
`uvm_info("DRV", $sformatf("drive addr=%h wdata=%h", t.addr, t.wdata), UVM_MEDIUM)
`uvm_info("SCB", "tx/rx matched", UVM_LOW)
`uvm_warning("DRV", "ready 拉高超过 100 拍")
`uvm_error("SCB", $sformatf("数据不匹配 exp=%h act=%h", exp, act)) // 计数,不停止
`uvm_fatal("ENV", "vif 为 null,环境无法继续") // 立即结束仿真
// 命令行:不改代码调日志量
// ./simv +UVM_TESTNAME=my_test +UVM_VERBOSITY=UVM_HIGH
| 冗余度 | 含义 | 典型用途 |
|---|---|---|
| UVM_NONE | 永远打印(等同关闭 verbosity 过滤) | 致命/关键结果 |
| UVM_LOW | 默认阈值下也打印 | 每个事务一条的结果级日志 |
| UVM_MEDIUM | 默认阈值,打印边界在此 | 常规过程信息 |
| UVM_HIGH | 调高阈值才打印 | 握手、字段级细节 |
| UVM_FULL / UVM_DEBUG | 全量/调试 | 深挖协议逐拍行为 |
怎么做:把前面 11 个知识点装配成一个可运行整体——interface + transaction + sequence + driver + monitor + scoreboard + agent + env + test + top,共 10 块拼图。它验证一个"发事务→驱动接口→采样还原→打印核对"的闭环;换掉接口信号与 drive_pins(),它就是任何协议验证平台的起点(下一页 13 将在这副骨架上补全 APB、覆盖率与 RAL)。建议亲手在 EDA Playground 或仿真器上跑通一次。
`include "uvm_macros.svh"
import uvm_pkg::*;
// ========== 1. 接口:DUT 与 TB 的握手信号 ==========
interface bus_if (input logic clk, rst_n);
logic valid;
logic [31:0] addr;
logic [31:0] wdata;
logic ready;
clocking drv_cb @(posedge clk);
default input #1step output #1ns;
output valid, addr, wdata; // driver 驱动
input ready; // driver 采样
endclocking
endinterface
// ========== 2. transaction:数据包(object 家族) ==========
class my_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] wdata;
`uvm_object_utils_begin(my_txn)
`uvm_field_int(addr, UVM_ALL_ON) // field 宏:免费获得 copy/print/compare
`uvm_field_int(wdata, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "my_txn"); super.new(name); endfunction
endclass
// ========== 3. sequence:激励剧本 ==========
class my_seq extends uvm_sequence #(my_txn);
`uvm_object_utils(my_seq)
function new(string name = "my_seq"); super.new(name); endfunction
task body();
repeat (10) begin
req = my_txn::type_id::create("req");
start_item(req);
assert(req.randomize() with { addr < 32'h1000; });
finish_item(req);
end
endtask
endclass
// ========== 4. driver:事务 → 引脚波形 ==========
class my_driver extends uvm_driver #(my_txn);
`uvm_component_utils(my_driver)
virtual bus_if vif;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual bus_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "bus_if 未通过 config_db 传入")
endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req); // 领事务
drive_one(req); // 演成波形
seq_item_port.item_done(); // 归还
end
endtask
task drive_one(my_txn t);
vif.drv_cb.valid <= 1'b1;
vif.drv_cb.addr <= t.addr;
vif.drv_cb.wdata <= t.wdata;
do @(vif.drv_cb); while (!vif.drv_cb.ready); // 等待 DUT 握手
vif.drv_cb.valid <= 1'b0;
endtask
endclass
// ========== 5. monitor:被动采样,还原事务并广播 ==========
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
virtual bus_if vif;
uvm_analysis_port #(my_txn) ap; // TLM 广播口
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual bus_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "bus_if 未通过 config_db 传入")
endfunction
task run_phase(uvm_phase phase);
my_txn t;
forever begin
@(vif.drv_cb);
if (vif.valid === 1'b1) begin
t = my_txn::type_id::create("t");
t.addr = vif.addr;
t.wdata = vif.wdata;
`uvm_info("MON", $sformatf("saw addr=%h data=%h", t.addr, t.wdata), UVM_HIGH)
ap.write(t); // 广播:scoreboard/coverage 同时收到
end
end
endtask
endclass
// ========== 6. scoreboard:订阅比对(imp 终点) ==========
class my_scoreboard extends uvm_scoreboard;
`uvm_component_utils(my_scoreboard)
uvm_analysis_imp #(my_txn, my_scoreboard) item_imp;
function new(string name, uvm_component parent);
super.new(name, parent);
item_imp = new("item_imp", this);
endfunction
function void write(my_txn t);
`uvm_info("SCB", $sformatf("recv addr=%h data=%h", t.addr, t.wdata), UVM_LOW)
// 真实环境:这里查关联数组参考模型,不匹配就 `uvm_error
endfunction
endclass
// ========== 7. agent:active/passive 一路接口的打包 ==========
typedef uvm_sequencer #(my_txn) my_sequencer; // 参数化 sequencer 起个名
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
my_sequencer sqr;
my_monitor mon;
uvm_active_passive_enum is_active = UVM_ACTIVE; // 可被 config_db 覆盖
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
if (is_active == UVM_ACTIVE) begin
drv = my_driver ::type_id::create("drv", this);
sqr = my_sequencer::type_id::create("sqr", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export); // port → export
endfunction
endclass
// ========== 8. env:容器与装配 ==========
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent agt;
my_scoreboard scb;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt = my_agent ::type_id::create("agt", this);
scb = my_scoreboard::type_id::create("scb", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
agt.mon.ap.connect(scb.item_imp); // port → imp(广播)
endfunction
endclass
// ========== 9. test:配置 + 启动激励 ==========
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_env env;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
my_seq seq;
phase.raise_objection(this); // 告诉框架:还有活没干完
seq = my_seq::type_id::create("seq");
seq.start(env.agt.sqr); // sequence 挂上 sequencer
#200; // 留出尾部排水时间
phase.drop_objection(this); // 放手,允许仿真结束
endtask
endclass
// ========== 10. top:例化接口与 DUT,启动框架 ==========
module tb_top;
logic clk = 0, rst_n = 0;
always #5 clk = ~clk; // 100MHz
bus_if u_if (clk, rst_n);
// dut u_dut (.clk(clk), .rst_n(rst_n), .valid(u_if.valid), ...); // 本例省略 DUT
initial begin
rst_n = 0; #23 rst_n = 1; // 复位
end
initial begin
uvm_config_db #(virtual bus_if)::set(null, "*", "vif", u_if); // 虚接口注入
run_test("my_test"); // 启动 UVM,也可 run_test() + +UVM_TESTNAME
end
endmodule
运行方式(VCS 示意,Xcelium/Questa 参数名不同):
vcs -sverilog -ntb_opts uvm-1.2 tb_top.sv -o simv
./simv +UVM_TESTNAME=my_test +UVM_VERBOSITY=UVM_MEDIUM
Type::type_id::create 而不是 new 创建,最主要的原因是?以下资源均经人工核实可访问(2026-09);链接按"官方 → 教程 → 开源代码 → 视频 → 中文社区 → 书籍"排序,建议配合本页第 1~8 节对照使用。