4. SystemVerilog基础:数据类型、过程块、接口、断言、随机化
各位好,咱们今天聊聊SystemVerilog。说实话,我刚从Verilog转到SV那会儿,心里还挺抵触的——觉得Verilog够用了,干嘛要学新东西?后来被项目逼着用,才发现真香。SV不是一门新语言,它是对Verilog的增强和扩展。你想想看,Verilog做验证就像用螺丝刀拧螺丝,而SV给你配了把电动螺丝刀。
4.1 数据类型:从wire/reg到logic和自定义结构
先说说数据类型。Verilog里最让人头疼的就是wire和reg的区分——组合逻辑用assign就得wire,时序逻辑用always就得reg。SV引入了一个叫logic的类型,它把两者统一了。
核心要点:在SV中,logic可以替代绝大多数wire和reg的场景。但注意,它不能用于多驱动(比如双向总线),那种情况还得用wire。
我个人习惯是:90%的信号都用logic声明。只有遇到inout端口或者多个驱动源时,才切回wire。这样代码清爽很多。
// Verilog老写法
wire clk;
reg [31:0] data;
reg valid;
// SV新写法
logic clk;
logic [31:0] data;
logic valid;
SV还引入了typedef和struct,这个太实用了。我在项目中遇到过一个大模块,有几十个控制信号,每次端口列表都写到手软。后来用struct一包,清爽多了。
typedef struct packed {
logic valid;
logic [7:0] addr;
logic [31:0] data;
logic rw;
} bus_trans_t;
bus_trans_t master_req; // 一个变量搞定
还有枚举类型,这个在状态机里特别好用。以前用parameter定义状态,仿真波形里看到的全是数字,调试时得对着表查。用枚举后,波形直接显示状态名,爽。
typedef enum logic [1:0] {
IDLE = 2'b00,
READ = 2'b01,
WRITE = 2'b10,
WAIT = 2'b11
} state_t;
小技巧:枚举类型默认是int,如果你要控制位宽,记得像我上面那样显式声明logic [1:0]。不然综合时可能会多出不必要的触发器。
4.2 过程块:always_comb、always_ff、always_latch
Verilog里一个always块打天下,组合逻辑、时序逻辑、锁存器全靠你写代码的风格来区分。SV把它拆成了三个明确的过程块。
| 过程块 | 用途 | 敏感列表 |
|---|---|---|
always_comb |
组合逻辑 | 自动推断 |
always_ff |
时序逻辑(触发器) | 需指定时钟沿 |
always_latch |
锁存器 | 自动推断 |
为什么要有这三个?说白了,就是让工具帮你检查。比如你用always_comb,工具会检查你是不是漏写了赋值——如果某个分支没覆盖到,它会报warning。我用always_ff时,如果敏感列表写错了,工具直接报error。
// 组合逻辑
always_comb begin
if (sel)
out = a;
else
out = b;
end
// 时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= '0;
else
q <= d;
end
注意:千万别在always_comb里用阻塞赋值以外的写法。我曾经见过有人在always_comb里写<=,仿真能过,但综合出来的电路跟你想象的不一样。嗯,那哥们后来改bug改了一整天。
4.3 接口:告别冗长的端口列表
接口(interface)是SV里我最喜欢的特性之一。你想想看,一个AXI总线有多少信号?地址、数据、握手、ID、突发长度...写端口列表能写一页纸。用interface,一个名字搞定。
interface axi_if;
logic aclk;
logic aresetn;
logic [31:0] awaddr;
logic [2:0] awprot;
logic awvalid;
logic awready;
// ... 还有几十个信号
endinterface
// 模块声明
module axi_slave (
axi_if.slave bus
);
// 直接用 bus.awaddr, bus.awvalid 等
endmodule
接口还支持modport,用来定义不同角色的视角。比如master看到的信号方向和slave是反的。modport就是干这个的。
interface axi_if;
// 信号声明...
modport master (
output awaddr, awvalid,
input awready
// ...
);
modport slave (
input awaddr, awvalid,
output awready
// ...
);
endinterface
我在项目中遇到过一个问题:多个master共享一个slave,每个master的接口信号都要单独例化。用interface配合数组,代码量直接减半。
4.4 断言:让bug无处遁形
断言(assertion)说白了就是给设计加个监控。你告诉工具:「这个信号应该永远这样」,如果它不这样了,工具就报错。断言分两种:立即断言和并发断言。
立即断言:写在过程块里,像if语句一样执行。
always_ff @(posedge clk) begin
if (valid) begin
assert (data !== 'x)
else $error("data is X when valid is high!");
end
end
并发断言:基于时钟周期,可以描述跨周期的时序关系。
// 握手协议:valid拉高后,ready必须在3个周期内拉高
property handshake;
@(posedge clk)
valid |-> ##[1:3] ready;
endproperty
assert_handshake: assert property (handshake);
我的经验:断言一定要在写RTL的时候就加,别等验证工程师来催你。我见过太多项目,验证发现bug后,设计才补断言——那时候已经晚了。断言是设计者的护身符,不是验证者的负担。
4.5 随机化:验证的核武器
随机化是SV验证的灵魂。说白了,就是让工具自动生成各种合法的输入组合,帮你找到那些你没想到的边界情况。
class packet;
rand logic [7:0] addr;
rand logic [31:0] data;
rand bit burst;
// 约束:地址范围
constraint addr_range {
addr inside {[0:16], [32:48]};
}
// 约束:burst模式下数据不能为0
constraint burst_data {
burst -> (data != 0);
}
endclass
packet pkt = new();
repeat (100) begin
assert(pkt.randomize());
// 用pkt.addr, pkt.data驱动DUT
end
随机化最牛的地方在于约束。你可以告诉工具:「给我随机生成数据,但要满足这些条件」。工具会自动在约束空间里找解。如果约束冲突了,randomize()会返回0,你得检查约束是不是写矛盾了。
避坑指南:我曾经写过一条约束addr > 0 && addr < 0,想当然地以为能生成非零地址。结果randomize一直失败,查了半天才发现自己写了个永远不可能满足的约束。嗯,从那以后我写约束都会先跑个空跑测试。
随机化配合覆盖率,就是验证的黄金搭档。你随机生成一万个包,然后看覆盖率——哪些分支没跑到?哪些状态没跳到?再针对性地加约束,把覆盖率补上去。
好了,这一章的内容就这些。数据类型让你写代码更简洁,过程块让意图更明确,接口让连接更清爽,断言让bug无处藏,随机化让验证更高效。这五个东西,是SV验证的基石。下一章咱们聊聊面向对象编程——class、继承、多态,那才是SV真正的大招。