3. SystemVerilog基础(上):数据类型、操作符、过程块
各位同学,咱们今天开始啃SystemVerilog这块硬骨头。说实话,很多做验证的朋友一开始都觉得SV语法多、杂、乱,但只要你把数据类型、操作符和过程块这三个地基打牢了,后面写起testbench来会顺手很多。我个人习惯把这部分叫做「验证工程师的ABC」,今天咱们先聊上半部分。
3.1 数据类型:从Verilog到SV的进化
先说说数据类型。Verilog时代,我们用的无非就是wire和reg。但到了SV,数据类型丰富了很多。为什么?因为验证需要更精确的数据描述。
3.1.1 四值逻辑 vs 二值逻辑
SV里最核心的一个概念就是四值逻辑和二值逻辑的区分。四值逻辑就是0、1、X、Z,这跟Verilog一样。二值逻辑只有0和1,没有X和Z。
我在项目中遇到过一个问题:某个模块的输入信号在仿真时出现了X态,但用bit类型去采样,X被自动转成了0,结果bug没抓到。从那以后,我给自己定了个规矩——
核心原则:RTL级信号用四值逻辑(logic),验证环境中的参考模型用二值逻辑(bit、int)。
| 类型 | 取值 | 默认值 | 典型用途 |
|---|---|---|---|
| logic | 0,1,X,Z | X | DUT接口信号 |
| bit | 0,1 | 0 | 验证环境内部变量 |
| int | 32位有符号 | 0 | 循环计数、索引 |
| byte | 8位有符号 | 0 | 小范围数据 |
3.1.2 定宽数组与动态数组
数组这块,SV比Verilog强太多了。Verilog只有定宽数组,SV引入了动态数组、关联数组和队列。
// 定宽数组 - 编译时确定大小
logic [7:0] mem [0:255];
// 动态数组 - 运行时确定大小
int dyn_arr[];
dyn_arr = new[100]; // 分配100个元素
// 队列 - 类似C++的deque
int q[$];
q.push_back(10);
q.push_front(5);
我个人最喜欢用队列。为什么?因为它可以动态增长,而且支持前后插入。写scoreboard的时候,队列几乎是标配。
小技巧:用队列做FIFO模型时,记得用q.pop_front()和q.push_back(),这样代码读起来就像在描述一个真实的FIFO行为。
3.1.3 结构体与联合体
SV支持struct和union,这让我们可以把相关的信号打包在一起。比如一个AXI总线的地址、数据、控制信号,用struct包起来,代码整洁很多。
typedef struct packed {
logic valid;
logic [31:0] addr;
logic [31:0] data;
logic [3:0] id;
} axi_trans_t;
axi_trans_t txn;
txn.valid = 1'b1;
txn.addr = 32'h1000;
嗯,这里要注意:packed struct可以按位操作,unpacked struct不行。做协议解析时,packed struct特别有用。
3.2 操作符:不只是加减乘除
SV的操作符大部分跟C语言类似,但有几个是硬件验证特有的,我重点说说。
3.2.1 位操作与归约操作
位操作大家都会,&、|、^这些。但归约操作很多人容易搞混。
logic [3:0] a = 4'b1010;
// 归约操作 - 对一个向量的所有位进行操作
logic and_all = &a; // 1 & 0 & 1 & 0 = 0
logic or_all = |a; // 1 | 0 | 1 | 0 = 1
logic xor_all = ^a; // 1 ^ 0 ^ 1 ^ 0 = 0
我曾经在写地址译码器时,用归约操作判断地址是否全0,结果因为X态传播导致仿真挂了。后来我加了个if条件判断X态,才把问题解决。
避坑指南:归约操作遇到X或Z时,结果也是X。如果你在写断言或覆盖率收集,记得先排除X态。
3.2.2 流操作符
流操作符{>>}和{<<}是SV特有的,用于位序转换。做协议处理时特别有用。
logic [7:0] byte_stream [4];
logic [31:0] word;
// 将4个字节打包成一个32位字
word = {>>{byte_stream}}; // 大端序
// 将一个32位字拆成4个字节
{>>{byte_stream}} = word;
你想想看,如果没有流操作符,你得写一堆for循环去移位拼接,多麻烦。
3.3 过程块:always_comb与always_ff
这部分是SV对Verilog最大的改进之一。Verilog的always @(*)和always @(posedge clk)虽然能用,但不够精确。SV引入了always_comb、always_ff和always_latch,让意图更明确。
3.3.1 always_comb:组合逻辑专用
always_comb用于描述组合逻辑。它比always @(*)好在哪?
- 自动敏感列表:不需要手动列信号
- 仿真时自动执行一次:避免初始值问题
- 工具可以检查是否有锁存器被推断
always_comb begin
if (sel) begin
out = a;
end else begin
out = b;
end
end
注意:always_comb里必须对所有输出赋值。如果某个分支漏了赋值,工具会报warning,提示可能产生了锁存器。
我的习惯:写always_comb时,先给所有输出赋默认值,再用if/case覆盖特殊情况。这样永远不会漏。
3.3.2 always_ff:时序逻辑专用
always_ff用于描述时序逻辑,比如触发器、计数器。
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= '0;
end else if (en) begin
cnt <= cnt + 1;
end
end
这里有个细节:always_ff的敏感列表必须包含时钟和异步复位。如果你写的是同步复位,敏感列表里只放时钟就行。
我记得有一次,一个同事在always_ff里用了阻塞赋值=,结果仿真波形全是乱的。排查了半天才发现是赋值方式用错了。时序逻辑一定要用非阻塞赋值<=,这是铁律。
3.3.3 always_latch:锁存器专用
always_latch用得少,但偶尔会碰到。比如地址锁存器。
always_latch begin
if (latch_en) begin
q <= d;
end
end
说实话,我建议尽量少用锁存器。在验证环境里,锁存器容易引起仿真和综合不一致的问题。能用always_comb或always_ff解决的,就别用latch。
3.4 知识体系总览
为了让大家对本章内容有个整体认识,我画了一张图。
3.5 实战建议
说了这么多,最后给几条实战建议:
- 数据类型选择:DUT接口用logic,验证环境内部用bit或int。别混用,否则仿真效率会下降。
- 数组优先用队列:除非你知道确切大小,否则队列比动态数组灵活。
- 过程块用对:组合逻辑用always_comb,时序逻辑用always_ff。别再用老式的always @(*)。
- 赋值方式:组合逻辑用=,时序逻辑用<=。混用的话,仿真结果会让你怀疑人生。
最后说一句:SV的语法虽然多,但常用的就这些。先把数据类型、操作符、过程块搞明白,后面学接口、类、随机化会轻松很多。别贪多,嚼不烂。
公众号:蓝海资料掘金营,微信deep3321