第二讲:SystemVerilog基础回顾——数据类型、操作符、过程块、任务与函数
各位同学,欢迎来到第二讲。今天咱们聊聊SystemVerilog的基础语法。说实话,很多老工程师觉得这些太简单,但我见过太多人因为基础不牢,写出来的激励又臭又长,调试起来想砸电脑。
我个人习惯是,不管多忙,每半年都会翻一遍基础语法。为什么?因为基础决定了你的代码能写多稳。好,咱们开始。
2.1 数据类型——别再用reg和wire打架了
刚学Verilog那会儿,我总搞不清什么时候用reg,什么时候用wire。后来SystemVerilog引入了logic,这个问题就简单了。
核心原则:能用logic的地方,就别纠结reg和wire。logic可以驱动,也可以被驱动,综合工具会自动判断。
但注意,logic不能用于多驱动场景(比如双向总线)。那种情况你得用wire。
// 推荐写法
logic [7:0] data_bus; // 单驱动,随便用
wire [7:0] shared_bus; // 多驱动,必须用wire
// 别这么写
reg [7:0] old_style; // 没必要,除非你怀旧
我在项目中遇到过一位同事,整个模块全用reg,结果综合时报了一堆警告。其实换成logic就清净了。
2.1.1 两态与四态类型
SystemVerilog里,bit是两态(0,1),logic是四态(0,1,X,Z)。
| 类型 | 状态数 | 默认值 | 适用场景 |
|---|---|---|---|
| bit | 2 | 0 | 纯功能验证,不关心X/Z |
| logic | 4 | X | RTL设计、需要X传播检查 |
| reg | 4 | X | 兼容老代码 |
我的建议:写测试激励时,能用bit就用bit。仿真跑得快,内存占用小。但如果你要检查X态传播,那就用logic。
2.1.2 数组与结构体
我记得刚学SystemVerilog时,看到结构体眼前一亮——终于能把一堆信号打包了。
// 定义一个结构体
typedef struct packed {
logic [7:0] addr;
logic [31:0] data;
logic valid;
} packet_t;
packet_t pkt;
pkt.addr = 8'hFF;
pkt.data = 32'h1234_5678;
pkt.valid = 1'b1;
你想想看,如果没有结构体,你得写三个单独的变量,传参时还得一个个列出来。有了结构体,一个变量搞定一切。
2.2 操作符——别小看这些符号
操作符谁都会用,但有些细节不注意,debug能让你怀疑人生。
2.2.1 算术操作符
加减乘除没什么好说的。但要注意,除法在仿真里很慢,综合出来更慢。我一般只在测试激励里用除法,RTL里能避免就避免。
2.2.2 逻辑操作符 vs 位操作符
这个坑我踩过。&&和&的区别,说白了就是:
- &&:逻辑与,结果只有0或1
- &:位与,结果按位运算
logic [3:0] a = 4'b1010;
logic [3:0] b = 4'b1100;
logic result1 = a && b; // 结果:1(因为a和b都不为0)
logic [3:0] result2 = a & b; // 结果:4'b1000
我曾经在条件判断里写了个if (a & b),本意是判断a和b都不为0,结果变成了按位与。仿真跑了两天才发现,气得我直接改了代码规范——条件判断里禁止用位操作符。
2.2.3 拼接与复制操作符
拼接操作符{}太好用了。我写地址生成时经常这么干:
logic [7:0] high = 8'hAB;
logic [7:0] low = 8'hCD;
logic [15:0] addr = {high, low}; // 16'hABCD
// 复制操作符
logic [7:0] pattern = {4{2'b10}}; // 8'b10101010
嗯,这里要注意:复制操作符外面的大括号不能省。我见过有人写{4{2'b10}},结果综合出来完全不对。
2.3 过程块——always和initial的恩怨情仇
过程块是硬件描述语言的核心。说白了,就是告诉仿真器什么时候干活。
2.3.1 initial块
initial块只执行一次。测试激励里,我一般用它来做初始化:
initial begin
clk = 0;
rst_n = 0;
#100 rst_n = 1;
#1000 $finish;
end
注意,initial块不可综合。所以RTL里别用,否则综合工具会报错。
2.3.2 always块
always块是RTL的主力。但SystemVerilog里,我推荐用always_comb和always_ff,而不是裸always。
// 组合逻辑
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不会报这些。
我在项目中遇到过,有人用always @(*)写组合逻辑,结果敏感列表里漏了一个信号,仿真和综合结果不一致。换成always_comb后,工具直接报warning,问题当场解决。
2.4 任务与函数——把重复代码收起来
写测试激励时,最烦的就是重复代码。任务和函数就是用来解决这个问题的。
2.4.1 函数(function)
函数返回一个值,不能包含时序控制(比如#delay、@event)。适合做纯计算:
function automatic logic [31:0] crc32(input logic [7:0] data);
// CRC计算逻辑
return crc_result;
endfunction
// 调用
logic [31:0] crc = crc32(rx_data);
注意那个automatic关键字。如果不加,函数里的变量是静态的,递归调用时会出问题。我一般习惯加上,省心。
2.4.2 任务(task)
任务可以有时序控制,可以返回多个值(通过output参数)。适合做总线读写这类操作:
task automatic read_reg(input logic [7:0] addr, output logic [31:0] data);
@(posedge clk);
addr_bus <= addr;
@(posedge clk);
data = data_bus;
endtask
// 调用
logic [31:0] val;
read_reg(8'h10, val);
我的习惯:纯计算用函数,有时序操作用任务。别混着用,否则代码可读性会变差。
2.4.3 任务与函数的区别
| 特性 | 函数 | 任务 |
|---|---|---|
| 返回值 | 有(一个) | 无(通过output返回多个) |
| 时序控制 | 不允许 | 允许 |
| 调用方式 | 表达式 | 语句 |
| 综合 | 可综合(有限制) | 不可综合 |
你想想看,如果你要写一个SPI读操作,里面有时钟沿等待,那就必须用任务。函数搞不定这个。
2.5 知识体系总览
下面这张图,是我自己总结的本章知识结构。建议你保存下来,写代码时对照着看。
好了,这一讲的内容就到这里。数据类型、操作符、过程块、任务与函数,这四个东西是SystemVerilog的基石。你写测试激励时,80%的代码都在跟它们打交道。
记住:基础不牢,地动山摇。别急着学高大上的UVM,先把这些基础玩明白。
可可精品资料库