4. SystemVerilog基础(上):数据类型、数组、结构体、枚举、过程语句

各位同学,欢迎来到第四章。从这一章开始,我们正式进入SystemVerilog的世界。

说实话,我刚从Verilog转到SystemVerilog时,第一反应是:「这玩意儿也太香了吧!」 以前写验证环境,光是定义数据类型、连线、写状态机,就能把人逼疯。SystemVerilog把这些痛点全解决了。

今天我们先啃下前半部分:数据类型、数组、结构体、枚举,还有两个最重要的过程语句——always_comb和always_ff。

核心思想:SystemVerilog不是一门新语言,它是Verilog的超级增强版。你已有的Verilog知识全部有效,只是多了很多「偷懒」的工具。

SystemVerilog基础(上) 数据类型 数组 结构体 枚举 过程语句 状态机、FSM定义 always_comb / always_ff 定宽/动态/关联/队列

4.1 数据类型:从wire/reg到logic

先问大家一个问题:Verilog里wire和reg的区别,你当年背了多久?

我记得刚学Verilog时,导师跟我说:「wire用于连续赋值,reg用于过程赋值。」我点点头,然后写代码时还是经常搞混。明明都是存一个值,为什么要分两种?

SystemVerilog的设计者显然也受够了。他们引入了一个新类型——logic。

// Verilog时代
wire       clk;
reg  [7:0] data;
assign data = 8'hFF;  // 等等,data是reg,不能用assign?

// SystemVerilog时代
logic       clk;       // 可以是wire,也可以是reg
logic [7:0] data;
assign data = 8'hFF;   // 没问题!
always_ff @(posedge clk) data <= 8'hAA;  // 也没问题!

我的建议:从今天起,忘掉wire和reg。在验证环境里,90%的场景用logic就够了。只有当你需要多驱动源(比如双向总线)时,才回头用wire。

除了logic,SystemVerilog还引入了双状态数据类型:bit、int、byte等。它们只有0和1,没有X和Z。为什么需要这个?

我在项目中遇到过一个问题:仿真跑了一整天,结果因为某个信号莫名其妙变成了X,整个验证环境崩了。后来发现,那个信号根本不需要X态——它只是用来计数的。从那以后,所有计数器、索引变量,我全用int或bit。

int          loop_count;   // 循环计数,不需要X态
bit [3:0]    state;        // 状态编码,也不需要X态
logic [31:0] address;      // 地址总线,可能有X,用logic

4.2 数组:定宽、动态、关联、队列

数组这块,SystemVerilog可以说是「鸟枪换炮」。以前Verilog只有定宽数组,现在四种数组任你选。

4.2.1 定宽数组

这个大家熟,就是Verilog的老朋友。不过SystemVerilog给了个新语法——合并数组和非合并数组。

// 合并数组:所有元素连续存放,像一个向量
logic [7:0] [3:0] mem_merged;   // 8个4bit元素,连续存放

// 非合并数组:每个元素独立存放
logic [3:0] mem_unmerged [7:0]; // 8个4bit元素,独立存放

嗯,这里要注意:合并数组可以整体赋值、整体比较,非合并数组不行。我一般只在建模寄存器文件时用合并数组,其他场景用非合并。

4.2.2 动态数组

这个是我最喜欢的。以前写验证环境,数组大小必须在编译前确定。现在?运行时想多大就多大。

int dyn[];          // 声明一个动态数组
dyn = new[100];     // 分配100个元素
dyn = new[200](dyn);// 重新分配为200个,保留前100个的值
dyn.delete();       // 清空

避坑指南:我曾经在循环里反复new动态数组,结果仿真越跑越慢。后来才发现,每次new都会分配新内存,旧内存虽然会被回收,但频繁分配/释放会影响性能。建议:如果能预估最大大小,一次性分配到位。

4.2.3 关联数组

说白了就是哈希表。当你需要按名字或特殊索引查找数据时,用它准没错。

// 按名字查找
string name_to_addr [string];
name_to_addr["uart"] = "0x1000";
name_to_addr["spi"]  = "0x2000";

// 遍历
foreach (name_to_addr[name]) begin
  $display("%s -> %s", name, name_to_addr[name]);
end

4.2.4 队列

队列是动态数组的升级版。它支持在头部和尾部快速插入/删除。

int q[$];       // 声明一个队列
q.push_back(1); // 尾部插入
q.push_front(2);// 头部插入
q.pop_back();   // 尾部弹出
q.pop_front();  // 头部弹出

你想想看,如果你用动态数组实现队列,每次头部插入都要移动所有元素,多麻烦。队列就是为这种场景设计的。

4.3 结构体:把相关信号打包

以前写Verilog,如果有个AXI接口,你得定义几十个wire信号。每次例化都要一个个连,手都酸了。

SystemVerilog的结构体就是来解决这个问题的。

// 定义一个AXI地址通道的结构体
typedef struct packed {
  logic [31:0] addr;
  logic [3:0]  id;
  logic [7:0]  len;
  logic [2:0]  size;
  logic [1:0]  burst;
} axi_addr_t;

// 使用
axi_addr_t addr_chan;
addr_chan.addr  = 32'h1000;
addr_chan.len   = 8'h0F;
addr_chan.burst = 2'b01;

关键点:struct packed会把所有成员按bit连续打包,保证内存布局和硬件一致。如果你只是做软件建模,用struct(不加packed)就行,更灵活。

我在项目中习惯把整个接口协议定义成一个结构体。比如AXI的五个通道,每个通道一个结构体。这样模块之间的连线瞬间从几十根变成几根。代码可读性提升不止一个档次。

4.4 枚举:让状态机不再神秘

枚举,说白了就是给数字起个名字。但别小看这个功能,它能让你的代码自文档化。

// 定义状态
typedef enum logic [2:0] {
  IDLE    = 3'b000,
  READ    = 3'b001,
  WRITE   = 3'b010,
  WAIT    = 3'b011,
  DONE    = 3'b100
} state_t;

state_t current_state, next_state;

为什么我推荐用枚举?因为仿真器会帮你检查。如果你给current_state赋了一个不在枚举列表里的值,仿真器会报错。这比你自己手写3'b101然后祈祷不出错要靠谱得多。

我的习惯:枚举值一定要显式指定编码。虽然不指定也能用,但指定了以后,综合工具会按你的编码方式实现,不会自作主张优化成独热码或其他格式。

4.5 过程语句:always_comb和always_ff

终于到了过程语句。这部分我要多说两句,因为太多人在这里翻车了。

4.5.1 always_comb:组合逻辑的正确打开方式

Verilog时代,我们写组合逻辑用always @(*)。但有个坑:如果敏感列表写漏了,仿真和综合结果就不一致。

always_comb完美解决了这个问题。它自动推断敏感列表,而且仿真器会检查你是否在块内写了锁存器。

// 老方法
always @(*) begin
  if (sel) 
    out = a;
  else if (!sel)
    out = b;
  // 如果sel是X,out保持原值?综合会生成锁存器!
end

// 新方法
always_comb begin
  if (sel)
    out = a;
  else
    out = b;
  // 所有分支都覆盖了,没问题
end

避坑指南:我曾经在always_comb里写了一个for循环,循环里对同一个变量多次赋值。仿真没问题,但综合工具报错。后来发现,always_comb要求每个变量在每个仿真时刻只被赋值一次。如果你需要循环赋值,用always加automatic。

4.5.2 always_ff:时序逻辑的标准化

always_ff是专门用来描述触发器的。它的语法强制你指定时钟沿和复位方式。

// 标准写法
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    q <= 0;
  else
    q <= d;
end

// 或者只带时钟沿(异步复位单独写)
always_ff @(posedge clk) begin
  q <= d;
end

你可能会问:这和always @(posedge clk)有什么区别?

区别在于:always_ff会检查你是否在块内使用了阻塞赋值(=)。如果你不小心写了q = d,仿真器会报错。这个检查在大型项目中特别有用——我见过太多因为阻塞/非阻塞混用导致的仿真与综合不一致的bug了。

4.6 小结

今天的内容不少,但都是基本功。总结一下:

  • 数据类型:用logic代替wire/reg,用int/bit代替不需要X态的场景
  • 数组:定宽数组打基础,动态数组灵活,关联数组查表,队列做缓冲
  • 结构体:把相关信号打包,接口连线瞬间清爽
  • 枚举:状态机、命令码,用枚举代替魔法数字
  • 过程语句:组合逻辑用always_comb,时序逻辑用always_ff

这些知识点,每一个我都在实际项目中踩过坑、填过坑。希望你们能站在我的肩膀上,少走弯路。

可可精品资料库