这里写自定义目录标题
- class 基础
- 如何在class内部使用 class外部的队列和reg变量?
- 什么是多态?
- clocking 块
- 子类如何调用父类的同名函数?
- Virtual
- Virtual Method(虚方法)
- Virtual Class(抽象类)—— 不能实例化,只当模板
- pure virtual function/task 纯虚函数
- Virtual Interface
- fatal(0/1/2,...)/ error(...)/ warning(...)/ info(...) /display(...)
- 随机变量(rand/ randc)
- randomize()
- constraint约束块
- 运行时控制:constraint_mode()& rand_mode()
- pre_randomize()/ post_randomize()回调
- 非class场景下:std::randomize()
- 继承有什么用处?
- 多继承和多层继承
- cast(目的句柄,原句柄) 等价 目的句柄 = 原句柄 ;
- 抽象类
- 回调
- 蓝图
- with
- 定位 / 过滤方法 —— with必选,表达式是布尔条件
- 缩减方法
- 排序方法(Ordering Methods)—— with可选
- 最小值 / 最大值 / 去重(可选 with)
全部由AI生成。
class 基础
类的new()函数,类外直接传递参数给类。
类的extern函数使用了类内部的parameter,在类外书写找不到parameter,可以使用类名::PARxx指定parameter来源。
函数的参数的默认值不允许在extern函数指定,要在函数声明时指定。
class transaction #(int WIDTH=8,int DEPTH=10) ; bit [31:0] addr,crc,data;//变量=属性;函数/任务 = 方法; static int cnt = 0 ;//静态变量,只在这个类的所有对象内可见有效。 // function new(bit [31:0] a ,bit [31:0] b ,bit [31:0] c='1 ) ;//new函数不能有返回值,可以有参数 addr = a ; crc = b ; data = c ; cnt++ ; endfunction // function void display ; $display("addr=%0h,crc=%0h,data=%0h,cnt=%0d",addr,crc,data,cnt); //this访问类的一级变量 $display("this.cnt=%0d",this.cnt); endfunction //静态方法只能操作静态变量。 static function void static_display ; $display("cnt=%0d",cnt); endfunction //函数定义在类外 extern function void display_crc () ; //task任务 task display_addr() ; $display("addr=%0d",addr); endtask // endclass function void transaction::display_crc ; $display("crc=%0h",crc); endfunction initial begin transaction tr ;//类的句柄 tr = null transaction tr1 ;//类的句柄 tr = null tr = new(1,2) ;//类的实例(对象);参数优先从左到右,c使用默认=FFFFFFFF tr.crc = 32'h1234 ;//改变类的变量值 tr.display() ; tr.display_crc() ; tr.display_addr() ; // tr1 = tr ;//只是赋值了句柄,对象没有复制,tr1和tr指向同一个对象。 // tr = null ;//没有句柄指向的对象会被系统回收。 tr1 = null ; //通过类名来访问静态变量 $display("transaction.cnt=%0d",transaction::cnt); end //默认赋值是浅拷贝(仅复制句柄),需自定义copy()方法实现深拷贝。如何在class内部使用 class外部的队列和reg变量?
类(class)不能直接访问模块或接口外部的信号(如 reg、队列 queue),必须通过引用传递、虚接口(virtual interface)或配置对象等方式显式连接。这是验证平台(testbench)设计的基本原则,用于保证封装性和可重用性。
- 使用虚接口(Virtual Interface)
这是 UVM/验证环境中标准做法。将外部变量封装在 interface中,类通过 virtual interface访问。
优点:解耦、可重用、符合 UVM 规范
interface ext_if; reg ext_reg; // reg 变量 int ext_queue[$]; // 队列 endinterface class my_driver; virtual ext_if vif; // 虚接口句柄 function new(virtual ext_if vif); this.vif = vif; endfunction task drive(); vif.ext_reg = 1'b1; vif.ext_queue.push_back(100); $display("queue size = %0d", vif.ext_queue.size()); endtask endclass module top; ext_if ext_if0(); initial begin my_driver drv = new(ext_if0); drv.drive(); end endmodule- 通过 ref 参数直接传递(简单场景)
class my_class; task process(ref int q[$], ref reg r); q.push_back(42); r = 1'b0; endtask endclass int my_q[$]; reg my_r; my_class c = new(); c.process(my_q, my_r);- 使用配置对象(Configuration Object)
适用于多个类共享同一组外部变量。
class cfg_obj; int ext_queue[$]; reg ext_reg; endclass class my_agent; cfg_obj cfg; function new(cfg_obj cfg); this.cfg = cfg; endfunction endclass什么是多态?
- 基于继承关系,不同子类可以对父类的同一个方法做不同的实现,调用的时候可以用父类类型的引用统一接收不同子类的对象,执行时会自动走到对应子类的实现。
- 父类不加 virtual时,方法调用看「句柄的类型」;加了 virtual后,方法调用看「句柄所指对象的真实类型」。
- Polymorphism = 继承(Inheritance)+ 虚方法(virtual)+ 向上转型(Upcasting:父类句柄指向子类对象)
- 把不同子类塞进同一个"父类类型"的容器/参数里,统一对待。
- 父类句柄 = 子类对象 ; 默认情况:如果父类和子类有同名函数,只会调用父类的同名函数。(调用哪个方法,取决于「句柄的声明类型」)。痛点:你明明塞进去的是子类对象,却调不到子类的行为。只改一行:把父类方法标记为 virtual,就能解决痛点。
- 父类句柄能调子类覆盖的 virtual方法,但如果那个子类有父类没有的新方法,光靠父类句柄调不到,因为父类它"看不见"。
- 父类句柄能接子类对象,但反过来不行。
- 子类写不写 virtual都能多态,但写上可读性好。
- 参数列表必须匹配(类型、个数、顺序)否则就不是"覆盖"而是"重载了一个新方法"
////////////////////////////////////////////////////////////////// // 基类:抽象协议——只定义"所有事务必须能 display / process" ////////////////////////////////////////////////////////////////// class Transaction; bit [31:0] addr; virtual function void display(); $display("[TXN] addr=0x%h", addr); endfunction virtual task process(); // 默认空实现 / 也可做成 pure virtual 逼子类必须写 #1; endtask endclass ////////////////////////////////////////////////////////////////// // 子类 1:读事务 ////////////////////////////////////////////////////////////////// class ReadTxn extends Transaction; bit [3:0] burst; virtual function void display(); $display("[READ] addr=0x%h burst=%0d", addr, burst); endfunction virtual task process(); $display("[READ] processing read @0x%h x%0d", addr, burst); #10; endtask endclass ////////////////////////////////////////////////////////////////// // 子类 2:写事务 ////////////////////////////////////////////////////////////////// class WriteTxn extends Transaction; bit [31:0] data; virtual function void display(); $display("[WRITE] addr=0x%h data=0x%h", addr, data); endfunction virtual task process(); $display("[WRITE] processing write @0x%h := 0x%h", addr, data); #5; endtask endclass ////////////////////////////////////////////////////////////////// // 一段"泛型"代码:它只知道 Transaction,不关心具体类型 ////////////////////////////////////////////////////////////////// task run_all(Transaction txn_q[$]); foreach (txn_q[i]) begin txn_q[i].display(); // ← 多态:各自走各自的 display() txn_q[i].process(); // ← 多态:各自走各自的 process() end endtask ////////////////////////////////////////////////////////////////// // top ////////////////////////////////////////////////////////////////// module tb; initial begin Transaction txn_q[$]; ReadTxn rd = new(); rd.addr = 32'h1000; rd.burst = 4; WriteTxn wr = new(); wr.addr = 32'h2000; wr.data = 32'hDEAD; txn_q.push_back(rd); // Packet 句柄 ← ReadTxn 对象 ✓ txn_q.push_back(wr); // Packet 句柄 ← WriteTxn 对象 ✓ run_all(txn_q); end endmoduleclocking 块
- DUT与testbench之间通过interface接口交互。故clocking块在接口内使用。
- 使用clocking块的目的是:解决delta-cycle 竞争问题。RTL 的 always @(posedge clk)采样发生在 时钟沿的那个时刻。如果你的 testbench 也在同一时刻(posedge clk)用 <=驱动信号,那么:DUT 采样到的可能是 旧值,也可能是 新值。
- clocking block 怎么解决的?
clocking block做的事情本质上是给信号的采样和驱动各加一个明确的"时间偏移(skew)",把它们从"恰好在时钟沿那一刻"挪开:
clocking 块名 @(时钟事件); default input #输入偏移 output #输出偏移; input 信号1, 信号2; // TB的input 从这些信号"采样"(只读) output 信号3, 信号4; // TB的output 向这些信号"驱动"(只写) inout 信号5; // 双向 endclocking //下面是常见用法:clocking 管时序,modport 管方向/视图,两者嵌套组合: //modport把 clocking包进去 interface bus_if(input logic clk); logic [31:0] addr; logic valid; logic ready; // ---- clocking blocks ---- clocking cb_drv @(posedge clk); default input #1step output #0; output addr, valid; input ready; endclocking clocking cb_mon @(posedge clk); default input #1step output #0; input addr, valid, ready; endclocking // ---- modports ---- modport DRIVER ( clocking cb_drv, input clk ); modport MONITOR( clocking cb_mon, input clk ); // DUT 侧如果不用 clocking(RTL通常不用),可以给普通方向视图: modport DUT ( input clk, addr, valid, output ready ); endinterface使用时:
class / program / testbench走 DRIVER或 MONITOR(带 clocking),通过 vif.cb_drv.xxx访问
RTL module(DUT)走 DUT视图(普通 input/output,不用 clocking,因为 RTL 的 always 块自己管时钟)
TB使用:不建议@(posedge vif.clk)裸写。建议@(vif.cb_drv)走 clocking
唯一适合用 vif.clk的场景:复位阶段(此时时钟可能在跑但你还没进入正常协议节拍)、超时检测、或调试打印——这些本来就不是 clocking 管的事。
子类如何调用父类的同名函数?
- 在子类内部使用super.xxx()
- super只能在子类的方法体内使用。
- 只能上一层级,不能 super.super.method(),多继承也不支持,所以不存在"跨两层调爷爷"的合法写法。
- 构造函数 new()的特例:super.new()
- 子类 不能直接用 super.new()的形式"调用父类同名函数"来注入逻辑,而是有专门规则:
- 规则 1:如果父类 new()不带参数,则SystemVerilog 会自动在子类的new()开头插入 super.new(),你可以写也可以不写。
- 规则 2:如果父类 new()有参数,你必须显式在子类的new()开头的第一行调用 super.new(xxx)。
- 子类 不能直接用 super.new()的形式"调用父类同名函数"来注入逻辑,而是有专门规则:
Virtual
告诉仿真器:这个方法别急着在编译时定死,留到运行时根据实际对象类型再找。
Virtual Method(虚方法)
- 只需在基类声明 virtual
一旦基类的某个 method 是 virtual,它在整个继承链中 自动保持 virtual,子类同名方法不必重复写 virtual(但写了也不错,增强可读性)。 - 签名必须一致
参数个数、类型、返回类型必须匹配,否则编译报错。 - super.method() 仍可显式调用父类版本
即使 virtual,子类内部也可以用 super.speak()调父类实现。
Virtual Class(抽象类)—— 不能实例化,只当模板
- virtual class PacketBase;
- 通过 virtual class声明一个 抽象类:它不能被 new()实例化,只能被 extends,用来定义子类 必须遵守的接口骨架。
pure virtual function/task 纯虚函数
- 举例:pure virtual function void pack(ref byte pkt[]);
- 只有原型,没有函数体,task 同样支持。
- 放在 virtual class内部(虽语法上不一定强制配对,但语义上就是这样用)。
- 任何非抽象子类必须提供全部 pure virtual 方法的实现,否则编译报错。
Virtual Interface
- 理解矛盾的根源:interface(硬件)与 class(软件)生活在两个世界。
- Virtual Interface 是什么:本质是 指向 interface 实例的句柄(指针)
- class 通过 virtual interface句柄来操控真实 interface 的信号。
fatal(0/1/2,…)/ error(…)/ warning(…)/ info(…) /display(…)
…都遵循 $display风格
fatal(0/1/2,…)与 finish(0/1/2)同款“诊断层级”:
0/1/2(0=最少诊断、2=最多统计类信息)。
它决定 fatal结尾隐式调用 finish时的“report level”。
随机变量(rand/ randc)
- 随机变量只能定义在 class内,用 rand或 randc标记成员变量,然后通过调用对象的 randomize()方法来触发随机赋值。
- 约束(constraint)则用来把"纯随机"变成"合法的随机"。
- 随机变量不能直接在 module/initial 块里声明,必须包在 class 里。
- rand是普通随机:每次独立摇骰子,可能连续两次相同。
- randc是周期随机:每个值出现一遍后才重复(像洗牌发牌)。
- randc的"全遍历"开销随位宽指数增长,所以 randc一般只用于小位宽(如 bit [3:0])。大范围≥ 16bit 就用 rand + constraint替代。
randomize()
- randomize()返回 bit:1=成功,0=失败(约束无解)→ 一定要用 assert或 if检查!
if (!t.randomize()) $fatal(0, "randomize failed");- 内嵌额外约束 with { }(最常用技巧之一)
with {}里的约束和类内约束共同生效,如果冲突 → randomize 返回 0(无解)。
// 临时追加约束,不改类定义 assert(t.randomize() with { addr == 32'h2000; // 强制固定地址 data > 8; });- 只随机化部分变量
t.randomize(addr); // 只随机 addr,其余保持不变(但仍须满足约束)- soft约束 — 可被 with{}安全覆盖的"默认偏好"
class Transaction; rand bit [7:0] len; constraint c_len { soft len inside {[1:10]}; } // 默认值偏好 endclass // 使用时: t.randomize() with { len == 32; }; // OK!覆盖了 soft 约束 // 如果不加 soft,上面就会冲突失败constraint约束块
- 每个表达式(xx;)至少有一个rand、randc类型的随机变量。
- constraint是声明式的(非顺序执行),所有表达式同时生效。
- 比较 & 范围:
constraint c_range { //约束块 len >= 1;//表达式1 len <= 64;//表达式2 addr[1:0] == 0; // 字对齐 }- inside 属于某集合 / 区间:
constraint c_set { burst_size inside {1, 2, 4, 8}; // 离散值集合 addr inside {[32'h0000 : 32'hFFFF]}; // 连续区间 !(addr inside {[32'hDEAD : 32'hDEAF]}); // 取反:排除一段 }- dist— 权重分布(控制出现概率)
constraint c_dist { // := 每个值各占指定权重 cmd dist { RD := 70, WR := 30 }; //RD 的权重是 70 // :/ 权重平均分摊到区间内每个值 len dist { [1:10] :/ 60, //60 除以 10 个值,每个值权重 = 6 [11:64] :/ 40 }; }- 条件 / 蕴含约束:->(不是 if-else!)
A -> B意思是 “A 为假时 B 随便,A 为真时 B 必须成立”
constraint c_cond { // 如果是读操作 → 地址必须对齐 (cmd == RD) -> (addr[1:0] == 0); //这和 if(A) B不一样 ->是约束求解器理解的双向声明关系。 // 如果 burst 模式 → len 必须是 4 的倍数 burst_en -> (len % 4 == 0); }- unique— 一组变量互不相同
rand bit [3:0] a, b, c; constraint c_uniq { unique {a, b, c}; // a ≠ b ≠ c }- foreach约束数组元素
rand byte payload[]; constraint c_pld { payload.size() inside {[4:16]}; // 动态数组长度随机 } constraint c_pld_val { foreach (payload[i]) payload[i] != 0; // 每个元素非零 }运行时控制:constraint_mode()& rand_mode()
这在不同测试用例复用同一个 transaction 类时极其有用——不同 test 只需开关约束,不用改类。
Transaction t = new(); // ---- 开关约束 ---- t.c_len.constraint_mode(0); // 禁用某个约束块 t.c_len.constraint_mode(1); // 启用 t.constraint_mode(0); // 禁用所有约束 // ---- 开关变量的随机性 ---- t.addr.rand_mode(0); // addr 不再随机(固定当前值) t.addr.rand_mode(1); // 恢复随机pre_randomize()/ post_randomize()回调
randomize()的内部调用顺序:pre_randomize()→ 求解约束赋值 → post_randomize()
class EthFrame; rand bit [47:0] dest; rand byte payload[]; bit [31:0] fcs; constraint c_size { payload.size() inside {[46:1500]}; } function void pre_randomize(); // 随机化前:可以设置动态上下界、清状态等 endfunction function void post_randomize(); // 随机化后:根据随机结果计算派生字段 fcs = calc_crc(dest, payload); endfunction endclass非class场景下:std::randomize()
当你 不在 class 里,也想做一次带约束的快速随机:
bit [7:0] a, b; if (std::randomize(a, b) with { a > b; a < 20; }) begin $display("a=%0d b=%0d", a, b); end以及系统函数(无约束,纯随机值):
val = $urandom_range(3, 10); // 返回 3~10 均匀随机
val = $urandom(); // 32-bit 无符号随机
抽象、封装、继承、多态
继承有什么用处?
- 代码复用:把"共性"抽出来只写一次。公共逻辑放基类,改一处就全生效;子类只聚焦自己的差异化字段/行为。
- 继承让你能在不碰已有、已验证通过的基类的前提下扩展出新类型。
- 多态(Polymorphism)—— 同一段代码处理多种包类型。这是继承在 SV 验证里最有杀伤力的价值。配合 virtual方法,基类句柄可以指向任意子类对象。这意味着:Driver / Monitor / Scoreboard 写成通用的,所有衍生 transaction 都能无缝接入,不需要 if/else判断类型。
- 框架骨架 UVM 的 component / object 层级关系全靠继承建立
多继承和多层继承
多继承(一个类同时继承多个父类):SystemVerilog 不支持
多层继承/单继承链(A extends B extends C):SystemVerilog 支持,有祖父类概念
cast(目的句柄,原句柄) 等价 目的句柄 = 原句柄 ;
父句柄可以指向子对象,也可指向父对象。
子句柄只能指向子对象,不能指向父对象。
因此用cast()检查是否合法。
不允许 直接子句柄 = 父句柄,允许cast(子句柄,父句柄),当父句柄指向的是子对象返回成功,父对象返回失败。
抽象类
在 SystemVerilog 中,抽象类(Abstract Class)是一种不能被实例化的类,专门用来作为基类定义接口规范,强制子类实现特定行为。它是实现多态和框架设计的核心工具。
核心思想:“我只定义你要做什么(What),不关心怎么做(How)。”
- 使用 virtual class声明
- 至少包含一个纯虚方法(pure virtual function/task)
- 不能直接 new()创建对象,抽象类可以有new(),可以被子类new()创建对象。
- 必须由子类 extends并实现所有纯虚方法后才能使用
回调
- 回调 = 组件开发者在关键位置留一个"钩子(hook)",组件使用者不改动原代码,就能把自定义逻辑"挂"进去执行。
- 回调模式(Callback):加/换 局部行为片段。回调改变的是 流程行为(注入、监控、过滤),不是 transaction 类型本身。钩子位置Driver/Monitor 的 执行流程关键点(pre/post)
// 回调:不关心 packet 是什么类型,只关心"发送前后夹一刀" class Driver; DriverCallback cb_q[$]; task drive(Transaction tr); foreach (cb_q[i]) cb_q[i].pre_send(tr); // ← hook transmit(tr); foreach (cb_q[i]) cb_q[i].post_send(tr); // ← hook endtask endclass // Test 层:注册回调 = 插入行为 err_cb = new(); err_cb.rate = 5; drv.register(err_cb);蓝图
- 在类内定义1个父类的句柄,这个句柄叫做蓝图。
- 实际上就是1个中间句柄,方便上层的类代码按需替换这个句柄指向的对象,而不需要改变底层的代码,故也叫做钩子。
- 要求:蓝图指向的对象有深度copy的方法。
- 蓝图模式:换整个对象类(及其字段、约束、方法),钩子位置Generator 的 对象创建点
// Generator → mailbox → Driver → DUT // 蓝图:决定 mailbox 里放的是什么"类型"的 transaction class Generator; Transaction blueprint = new(); // 基类句柄 task run(int n); Transaction tr; repeat(n) begin assert(blueprint.randomize()); tr = blueprint.clone(); gen2drv.put(tr); end endtask endclass // Test 层:换蓝图 = 换产出类型 generator.blueprint = new(BadPacket); // 换成坏包模板with
- with的执行 = 数组方法控制循环负责遍历数组 → 每轮每走到一个元素把 item绑到当前元素 → 求 with表达式 → 按方法语义消费这个结果(过滤 / 累积 / 比较键)。
定位 / 过滤方法 —— with必选,表达式是布尔条件
- 这类方法用 with里的条件做过滤,返回值是一个队列,定位方法返回值永远是队列,哪怕只匹配到一个,也要用队列接。
| 方法 | 返回值类型 | 作用 |
|---|---|---|
find() | queue of T | 返回所有满足条件的元素 |
find_index() | queue of int | 返回所有满足条件的索引(非关联数组时类型为int) |
find_first() | queue of T | 返回第一个满足条件的元素 |
find_first_index() | queue of int | 返回第一个满足条件的索引 |
find_last() | queue of T | 返回最后一个满足条件的元素 |
find_last_index() | queue of int | 返回最后一个满足条件的索引 |
int d[] = '{9, 1, 8, 3, 4, 4}; int tq[$], ti[$]; tq = d.find with (item > 3); // '{9, 8, 4, 4} ti = d.find_index with (item > 3); // '{0, 2, 4, 5} tq = d.find_first with (item == 4); // '{4} ti = d.find_first_index with (item == 8); // '{2} tq = d.find_last with (item == 4); // '{4} ti = d.find_last_index with (item == 4); // '{5}缩减方法
(Reduction Methods)—— with可选
- 不加 with—— 直接算。
- 加 with—— 对每个元素做变换后再缩减。
| 方法 | 作用 | 返回值类型 |
|---|---|---|
sum() | 求和 | 与元素同类型(注意位宽!) |
product() | 求积 | 与元素同类型 |
and() | 按位与 | 与元素同类型 |
or() | 按位或 | 与元素同类型 |
xor() | 按位异或 | 与元素同类型 |
int d[] = '{9, 1, 8, 3, 4, 4}; int cnt, tot; cnt = d.sum with (item > 7); // 2 ← 是"个数",不是"和" tot = d.sum with ((item > 7) * item); // 17 ← 9+8 tot = d.sum with (item < 8 ? item : 0); // 12 ← 1+3+4+4排序方法(Ordering Methods)—— with可选
| 方法 | 作用 | 能否带 with |
|---|---|---|
sort() | 原地升序 | 可选(指定排序键) |
rsort() | 原地降序 | 可选(指定排序键) |
reverse() | 原地反转 | 不支持 |
shuffle() | 原地随机打乱 | 不支持 |
int arr[] = '{5, 2, 8, 1, 9}; arr.sort(); // '{1, 2, 5, 8, 9} arr.rsort(); // '{9, 8, 5, 2, 1} // 结构体数组按某字段排 typedef struct packed { int id; int score; } Stu; Stu class_list[$] = '{ '{1,85}, '{2,92}, '{3,78} }; class_list.sort(s) with (s.score); // 按 score 升序sort/rsort/reverse/shuffle原地修改原数组,定位方法则返回新队列不改原数组。
最小值 / 最大值 / 去重(可选 with)
| 方法 | 作用 | with效果 |
|---|---|---|
min() | 返回最小元素(队列形式) | 不支持 |
max() | 返回最大元素(队列形式) | 不支持 |
unique() | 去除重复值,返回队列 | 加 with 可按表达式键值去重 |
unique_index() | 返回去重后元素在原数组的索引队列 | 加 with 可按键值去重 |
int f[6] = '{1,6,2,6,8,6}; int tq[$]; tq = f.min(); // '{1} tq = f.max(); // '{8} tq = f.unique(); // '{1, 6, 2, 8} // 按"奇/偶"分组去重(只保留每种奇偶性的首次出现) tq = f.unique(x) with (x & 1);