2. SystemVerilog基础:数据类型、操作符、过程块、接口与模块

各位同学,咱们今天聊聊SystemVerilog的基础。说实话,很多刚入行的朋友觉得这玩意儿就是Verilog的升级版,学学语法就完事了。但我在项目里摸爬滚打这么多年,发现真正用好它的人真不多。来,咱们一个一个掰扯清楚。

2.1 数据类型:从reg到logic的进化

先说说数据类型。我记得刚入行那会儿,公司还在用Verilog-95,那时候最头疼的就是reg和wire的区分。你写个组合逻辑,用reg吧,心里犯嘀咕——这玩意儿不是寄存器吗?用wire吧,又怕综合出来不对。

SystemVerilog直接给你整了个logic类型。说白了,它就是个万能类型。你写组合逻辑用它,写时序逻辑也用它。我现在的习惯是:只要不是多驱动场景,一律用logic。

核心数据类型一览:
类型 说明 我的建议
logic 四值逻辑(0,1,X,Z) 日常首选,替代reg和wire
bit 二值逻辑(0,1) 验证环境用,RTL慎用
int/byte 有符号整型 循环变量、计数器
string 字符串类型 打印信息、文件路径
小技巧: 验证环境里我习惯用bit和int,仿真速度快。RTL设计里老老实实用logic,避免X态传播问题。

2.2 操作符:你可能会忽略的细节

操作符这块,大家都会用。但我想重点提两个:缩减操作符和流操作符。

缩减操作符长这样:&data、|data、^data。它能把一个向量缩减成1位。比如你想判断一个32位总线是否全为0,写if (~|data)比写if (data == 32'b0)简洁多了。

流操作符{>>{}}和{<<{}},说白了就是比特序重排。我在做DMA验证时遇到过一个问题:CPU写数据到内存,字节序搞反了。用流操作符一行代码搞定,省得写一堆for循环。

// 流操作符示例:比特序反转
logic [7:0] data_in = 8'hA5;
logic [7:0] data_out;

// 按位反转
data_out = {<<{data_in}};  // 结果是8'hA5 -> 8'hA5? 不对,是8'h5A

// 按字节反转
data_out = {>>{data_in}};  // 结果还是8'hA5,因为只有1个字节
注意: 流操作符的优先级比较低。我曾经在代码里写data_out = {<<{data_in}} + 1;,结果综合出来完全不对。记住加括号:data_out = ({<<{data_in}}) + 1;

2.3 过程块:always_comb、always_ff、always_latch

嗯,这里要重点说说。Verilog时代就一个always,靠敏感列表区分组合和时序。SystemVerilog给了三个专用块:

  • always_comb:组合逻辑专用。工具会自动推导敏感列表,你少写一个信号它也能发现。
  • always_ff:时序逻辑专用。必须指定时钟沿和复位。
  • always_latch:锁存器专用。说实话,我很少用这个——锁存器在ASIC里是个坑。

我个人习惯是:RTL代码里只用always_comb和always_ff。为什么?因为工具会帮你检查。比如你在always_comb里漏了赋值,工具直接报warning,省得你debug到半夜。

// 好的写法
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    cnt <= '0;
  else if (en)
    cnt <= cnt + 1;
end

// 坏的写法(Verilog风格)
always @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    cnt <= '0;
  else if (en)
    cnt <= cnt + 1;
end
避坑指南: 我曾经在always_comb里写了#1延时,结果仿真和综合结果不一致。记住:always_comb里不允许用延时,这是语法规定。

2.4 接口:告别冗长的端口列表

接口(interface)是我最喜欢的SystemVerilog特性之一。你想想看,一个AHB总线有多少信号?地址、数据、控制、响应...少说20根。每个模块都要把这些信号列一遍,写起来烦,读起来更烦。

接口就是把这些信号打包成一个整体。我举个例子:

interface ahb_if (input logic hclk, input logic hrst_n);
  logic [31:0] haddr;
  logic [31:0] hwdata;
  logic [31:0] hrdata;
  logic        hwrite;
  logic [1:0]  htrans;
  logic        hready;
  
  // 可以定义modport
  modport master (
    output haddr, hwdata, hwrite, htrans,
    input  hrdata, hready
  );
  
  modport slave (
    input  haddr, hwdata, hwrite, htrans,
    output hrdata, hready
  );
endinterface

有了接口,模块例化就清爽多了:

module ahb_master (
  ahb_if.master bus
);
  // 直接用 bus.haddr、bus.hwdata
endmodule
我的经验: 接口里一定要用modport。没有modport的接口,就像没有目录的书——信息都在,但找起来费劲。另外,接口里可以放断言和覆盖率收集代码,这个后面章节会细讲。

2.5 模块:从黑盒到白盒

模块(module)是Verilog就有的概念,但SystemVerilog给它加了新玩法。比如你可以用import语句引入包,用parameter做参数化设计。

我重点说说参数化模块。做验证的都知道,DUT的位宽经常变。今天32位,明天64位。如果你写死了,改起来想哭。

module fifo #(
  parameter DEPTH = 16,
  parameter WIDTH = 8
)(
  input  logic             clk,
  input  logic             rst_n,
  input  logic             wr_en,
  input  logic [WIDTH-1:0] wr_data,
  input  logic             rd_en,
  output logic [WIDTH-1:0] rd_data,
  output logic             full,
  output logic             empty
);
  // 内部实现
endmodule

例化的时候直接改参数:

fifo #(.DEPTH(32), .WIDTH(16)) u_fifo (
  .clk(clk),
  .rst_n(rst_n),
  // ...
);
注意: 参数化虽然好用,但别过度。我见过有人把FIFO深度、宽度、甚至时钟周期都做成参数,结果代码可读性极差。适度参数化,过度就是灾难。

2.6 知识体系总览

说了这么多,咱们用一张图把关系理清楚:

SystemVerilog基础:知识体系 SystemVerilog基础 数据类型 logic / bit / int string / enum / struct 四值 vs 二值 操作符 缩减操作符 & | ^ 流操作符 << >> 优先级陷阱 过程块 always_comb always_ff always_latch(慎用) 接口 (interface) 信号打包 / modport 减少端口列表 可内嵌断言/覆盖率 模块 (module) 参数化设计 import包 / 层次化 黑盒 vs 白盒 掌握这些,RTL和验证都能写

这张图把咱们今天讲的内容串起来了。你看,数据类型是砖瓦,操作符是工具,过程块是施工规范,接口和模块是设计蓝图。缺了哪一块,房子都盖不结实。

好了,今天就到这儿。记住我说的:多用logic、少用reg;接口一定要加modport;过程块选对类型。这些习惯养成了,后面写UVM、做覆盖率都会顺手很多。

可可精品资料库