第二讲: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)。

类型状态数默认值适用场景
bit20纯功能验证,不关心X/Z
logic4XRTL设计、需要X传播检查
reg4X兼容老代码

我的建议:写测试激励时,能用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基础回顾知识体系 SystemVerilog基础 数据类型 logic / bit / reg 数组 / 结构体 两态 vs 四态 操作符 算术 / 逻辑 / 位 拼接与复制 条件判断陷阱 过程块 initial / always always_comb / always_ff 可综合 vs 不可综合 任务与函数 function(纯计算) task(有时序) automatic关键字 核心原则: 能用logic不用reg/wire | 能用always_comb不用always 下一站:面向对象编程(OOP)

好了,这一讲的内容就到这里。数据类型、操作符、过程块、任务与函数,这四个东西是SystemVerilog的基石。你写测试激励时,80%的代码都在跟它们打交道。

记住:基础不牢,地动山摇。别急着学高大上的UVM,先把这些基础玩明白。

可可精品资料库