3. SystemVerilog基础回顾:数据类型、接口、模块、进程与同步
好,咱们正式开始搭建控制器验证平台之前,我得先带你过一遍SystemVerilog的核心基础。你可能会想:「这些我早就会了,还用讲?」嗯,我理解。但说实话,我在项目里见过太多人栽在基础细节上——比如接口信号方向搞反、进程同步漏了触发、数据类型不匹配导致仿真崩溃。所以这一章,咱们不搞花架子,就扎扎实实把地基打牢。
3.1 数据类型:从reg到logic,再到自定义结构
我记得刚入行那会儿,大家还在纠结reg和wire的区别。现在好了,SystemVerilog直接给了我们logic类型。说白了,logic就是reg的升级版——它既能当寄存器用,也能当连线用。你想想看,是不是省心多了?
核心原则:在验证平台中,90%的信号我都用logic声明。只有需要多驱动源(比如双向总线)时,才用wire。
除了logic,还有几个常用的数据类型:
bit:两值逻辑,适合做状态标志。我在项目中常用它来标记「是否完成」这类简单信号。int:32位有符号整数。注意,它默认是有符号的!我曾经踩过坑——用int做计数器,结果负数一路减下去,仿真直接崩了。byte:8位,适合存数据包里的字段。string:动态字符串,调试打印时特别好用。
再来说说自定义类型。我个人习惯把验证平台里的「事务」定义成结构体:
typedef struct packed {
bit valid;
logic [7:0] addr;
logic [31:0] data;
bit rw; // 1: read, 0: write
} transaction_t;
这样写的好处是,你一眼就能看出一个事务长什么样。而且packed关键字让它可以按位操作,方便打包成比特流。
小技巧:定义结构体时,尽量把位宽写清楚。我见过有人用logic [7:0]和logic [0:7]混着用,结果仿真波形里地址顺序全反了。统一用[高:低],别给自己挖坑。
3.2 接口:把信号捆在一起
接口(interface)是SystemVerilog里我最喜欢的特性之一。你想想,一个AXI总线有多少根信号?地址、数据、握手、响应……如果每个信号都单独拉出来连线,代码能写到让你怀疑人生。
接口就是把这些信号「捆」成一个整体。比如:
interface axi_if (input clk, input rst_n);
logic [31:0] awaddr;
logic awvalid;
logic awready;
logic [31:0] wdata;
logic wvalid;
logic wready;
logic [1:0] bresp;
logic bvalid;
logic bready;
// ... 还有ar、r通道,这里省略
endinterface
然后你在模块里直接传这个接口:
module axi_slave (axi_if bus);
always_ff @(posedge bus.clk or negedge bus.rst_n) begin
if (!bus.rst_n) begin
bus.awready <= 1'b0;
end else begin
bus.awready <= bus.awvalid && !bus.awready;
end
end
endmodule
你看,代码是不是清爽多了?而且接口里可以加modport来定义方向:
interface axi_if;
// ... 信号声明 ...
modport master (output awaddr, awvalid,
input awready, ...);
modport slave (input awaddr, awvalid,
output awready, ...);
endinterface
注意:接口里的信号方向由modport控制,但实际驱动关系还得靠模块内部逻辑保证。我曾经见过有人把master和slave的modport搞反,仿真结果全是X态。排查了一整天,最后发现是方向写反了——这种低级错误,真的让人哭笑不得。
3.3 模块:验证平台的「积木块」
模块(module)是Verilog时代就有的概念,但SystemVerilog给它加了点新料。比如always_comb、always_ff、always_latch——这三个关键字能帮你明确代码意图。
always_comb:组合逻辑。仿真器会检查你是不是漏了敏感列表。always_ff:时序逻辑。必须带时钟沿和复位。always_latch:锁存器。说实话,我尽量不用它——锁存器在验证平台里容易出问题。
举个例子,一个简单的计数器模块:
module counter (
input logic clk,
input logic rst_n,
input logic en,
output logic [7:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'h00;
else if (en)
count <= count + 1'b1;
end
endmodule
在验证平台里,模块通常用来封装DUT(待测设计)或者一些简单的BFM(总线功能模型)。我个人习惯把每个模块的接口都用interface传进去,这样顶层连线就变得特别简单。
3.4 进程与同步:让仿真「动」起来
验证平台的核心就是并发。你想想,DUT在跑,驱动在发激励,监视器在抓数据,计分板在比对——这些都得同时进行。SystemVerilog用initial和always来创建进程,但真正让它们协同工作的,是同步机制。
3.4.1 进程创建
最常用的就是initial块:
initial begin
// 初始化信号
reset();
// 发送激励
send_packet();
// 等待结果
wait_for_response();
end
但注意,initial块里的语句是顺序执行的。如果你想让多个事情同时跑,就得用fork...join:
initial begin
fork
// 进程1:驱动时钟
forever #5 clk = ~clk;
// 进程2:发送激励
begin
reset();
send_packet();
end
// 进程3:监视输出
monitor_output();
join
end
fork...join有三种变体:
| 类型 | 行为 | 我用在哪儿 |
|---|---|---|
join |
所有子进程结束才继续 | 需要等所有任务完成后再做下一步 |
join_any |
任意一个子进程结束就继续 | 超时检测,比如等第一个响应 |
join_none |
不等待,立即继续 | 启动后台监视器,不阻塞主流程 |
我的经验:在验证平台里,join_none用得最多。比如启动一个监视器进程,让它一直在后台跑,主流程继续发激励。但要注意——join_none启动的进程如果没处理好,可能会在仿真结束时还挂着。我习惯在final块里加个disable fork来清理。
3.4.2 同步:事件、信号量、信箱
进程之间怎么通信?SystemVerilog给了我们三件套:
- 事件(event):最轻量的同步方式。一个进程触发,另一个进程等待。
- 信号量(semaphore):控制对共享资源的访问。比如多个驱动都想用同一个总线,就得靠信号量来仲裁。
- 信箱(mailbox):传递数据。生产者往信箱里放,消费者从信箱里取。
举个例子,用事件同步两个进程:
event done;
initial begin : driver
// 发送激励
drive_packet();
-> done; // 触发事件
end
initial begin : monitor
@done; // 等待事件
check_result();
end
这里要注意,@done是边沿敏感的——如果事件在等待之前就已经触发了,那就永远等不到了。我遇到过这种bug,排查了好久才发现是时序问题。后来我改用wait (done.triggered),它是电平敏感的,更安全。
避坑指南:我曾经在项目里用信箱传递事务,结果发现消费者总是取不到数据。查了半天,原来是信箱满了——生产者放得太快,消费者处理不过来。解决方案有两个:要么加大信箱深度,要么加个流控。我个人推荐后者,因为深度再大也有满的时候。
3.5 知识体系总览
说了这么多,咱们用一张图来总结一下本章的核心内容。这张图展示了SystemVerilog基础在验证平台中的位置和作用:
这张图把咱们讲的内容串起来了。你看,数据类型是「血肉」,接口是「骨架」,模块是「器官」,进程与同步是「神经系统」——缺了哪个,验证平台都跑不起来。
好了,这一章就到这里。记住,基础不牢,地动山摇。下一章咱们开始动手搭建第一个验证组件,到时候这些知识全都会用上。
公众号:蓝海资料掘金营,微信deep3321