SystemVerilog 基础

第 5 章 · 共 16 章

更多数据类型:2 态与 4 态、枚举、结构体与 typedef

理解 2 态与 4 态数据类型的区别与取舍,用 enum 给状态命名,用 struct 打包相关字段,用 typedef 让类型名字有意义。

上一章的 logic(以及 wirereg)都是4 态类型:除了 01,还能表示未知态 x 和高阻态 z。这一章继续往前走:先弄清楚 2 态类型和 4 态类型该怎么选,再学会用 enumstructtypedef 把数据组织得更清晰,而不是散落一堆孤立的变量。

2 态与 4 态数据类型

SystemVerilog 里的整数类型分成两大阵营:

  • 4 态类型logicregwireinteger 等,每一位可以是 01x(未知)、z(高阻)。
  • 2 态类型bitbyteshortintintlongint 等,每一位只能是 01,没有 x/z
类型位宽是否 4 态
logic / reg声明时指定(默认 1 位)
wire声明时指定(默认 1 位)
integer32 位
bit声明时指定(默认 1 位)
byte8 位
shortint16 位
int32 位
longint64 位

两者最直观的差别是未初始化时的默认值

module default_values_demo;
  logic   l;  // 4 态:logic,1 位
  integer n;  // 4 态:integer,32 位
  bit     b;  // 2 态:bit,1 位
  int     i;  // 2 态:int,32 位
 
  initial begin
    $display("l = %b", l);  // x
    $display("n = %b", n);  // 32 个 x
    $display("b = %b", b);  // 0
    $display("i = %b", i);  // 32 个 0
  end
endmodule

这个差别不只是打印好不好看的问题:4 态类型的 x 能帮你在仿真里发现"这个信号其实没被正确驱动/初始化"这类真实的设计或验证 bug;而 2 态类型未初始化时直接是 0,会把这类问题悄悄"合理化"掉。所以一个常见的经验法则是:RTL 里表示硬件信号,优先用 4 态类型(logic);testbench 里做计数器、循环变量、ID 这类纯软件用途的变量,用 2 态类型(int/byte 等)更高效,也更符合它们本来就该有确定初值的语义

enum:给状态命名

用裸整数表示状态可读性很差——单看 state == 2'd1 这一行,根本看不出它对应哪个状态,通常还得回头翻文档才能确认。enum 把这些"魔法数字"换成有意义的名字:

typedef enum logic [1:0] {
  IDLE,
  RUN,
  DONE
} state_e;
 
state_e cur_state;
 
initial begin
  cur_state = IDLE;
  $display("current state: %s", cur_state.name());
end

几个要点:

  • typedef enum ... state_e; 同时做了两件事:定义了一个枚举类型,并用 typedef 给它起了名字 state_e(关于 typedef 本身,下一节详细说)。
  • logic [1:0] 指定了枚举的底层表示宽度和 4 态特性——如果不写,默认底层类型是 int(2 态,32 位);在描述硬件状态机时,通常会显式指定一个刚好够用的位宽。
  • 每个枚举变量自带一个 .name() 方法,能直接返回当前值对应的名字字符串,调试打印($display)时比裸数字直观得多。

struct:把相关字段打包在一起

当几个变量总是一起出现、一起传递时(比如一次总线传输的地址、数据、有效位),把它们分别声明成三个独立变量既啰嗦又容易传错顺序。struct 把它们打包成一个整体:

// packed struct:连续的比特位布局,可综合,能直接当端口/总线类型使用
typedef struct packed {
  logic [7:0] addr;
  logic [7:0] data;
  logic       valid;
} bus_pkt_t;
 
bus_pkt_t pkt;
 
initial begin
  pkt.addr  = 8'h10;
  pkt.data  = 8'hAB;
  pkt.valid = 1'b1;
end

SystemVerilog 的 struct 分两种:

  • packed struct(如上例):所有字段紧密排列成一段连续的比特位,整体可以当成一个向量使用,也可以直接用作模块端口类型——适合描述会变成真实硬件信号的数据(比如总线事务)。
  • unpacked struct:字段之间没有强制的连续位布局,可以混用不同宽度、不同种类的类型,更接近软件里的结构体,通常用在 testbench 这类不需要综合的场景:
// unpacked struct:更自由的字段组合,常用于验证代码
typedef struct {
  int addr_width;
  int data_width;
} bus_cfg_t;
 
bus_cfg_t cfg;
 
initial begin
  cfg.addr_width = 32;
  cfg.data_width = 64;
end

typedef:给类型起名字

前面两节其实已经在用 typedef 了——它的作用就是给一个类型起一个新名字,之后就可以像内置类型一样使用这个名字,而不用每次都重复写完整的类型定义。除了给 enum/struct 命名,typedef 也可以直接用在简单类型上:

typedef logic [7:0] byte_t;
 
byte_t data_in, data_out;

好处很直接:一是可读性byte_tlogic [7:0] 更能表达意图);二是一致性——如果以后需要把这个类型的位宽从 8 位改成 16 位,只需要改 typedef 这一行,所有用到 byte_t 的地方自动跟着变,不用满项目搜索替换。

string:变长字符串类型

前面几节的类型都是固定位宽的整数家族。验证代码里还经常需要文本——事务的名字、日志信息、报错内容——这正是 string 类型的用途。它是一个可变长度的字符串类型,用法很接近其他高级语言里的字符串,不需要像 C 语言那样手动管理长度或以 \0 结尾:

string name;
string msg;
 
initial begin
  name = "packet0";
  msg  = "unexpected value";
 
  $display("name = %s, length = %0d", name, name.len());
 
  if (name == "packet0")
    $display("name matches!");
end

string 变量可以直接用 ==/!= 比较内容(不需要像 C 那样调用 strcmp),.len() 方法返回当前字符串的长度,赋值新的字符串时长度会自动跟着变化,不需要预先声明容量。和 class(第 10 章会讲到)一样,string 是纯粹的验证语言特性——不可综合,只会出现在 testbench 代码里,常见于事务的标签、配置信息、断言失败时的报错文案等场景。

组合起来:enum + struct + typedef

把这三样放在一起,就能写出既紧凑又可读的数据结构,这在后面验证相关的章节里会经常出现:

typedef enum logic [1:0] {
  PKT_OK,
  PKT_ERR,
  PKT_TIMEOUT
} pkt_status_e;
 
typedef struct {
  int          addr;
  int          data;
  pkt_status_e status;
} pkt_record_t;
 
pkt_record_t log_entry;
 
initial begin
  log_entry.addr   = 32'h1000;
  log_entry.data   = 32'hdead_beef;
  log_entry.status = PKT_OK;
  $display("addr=%0h data=%0h status=%s",
           log_entry.addr, log_entry.data, log_entry.status.name());
end

log_entry 既清楚地表达了"一条记录包含哪些字段",又因为 statusenum 类型,打印出来的是 PKT_OK 而不是裸数字 0——这就是 enum/struct/typedef 组合起来带来的可读性提升。

小结

  • 4 态类型(logicreginteger)能表示 x/z,适合描述硬件信号;2 态类型(bitintbyte 等)没有 x/z,更适合 testbench 里的计数器、循环变量等纯软件用途。
  • 未初始化时,4 态变量默认是 x,2 态变量默认是 0——这个差异会直接影响你能不能在仿真里发现"忘记初始化"这类 bug。
  • enum 把裸数字换成有名字的状态,还自带 .name() 方法方便调试打印。
  • struct 把相关字段打包成一个整体;packed struct 是连续比特位、可综合,unpacked struct 更自由、常用于验证代码。
  • typedef 给类型起名字,提升可读性,也让以后修改类型定义只需要改一处。
  • string 是可变长度的字符串类型,支持 ==/!= 直接比较内容、.len() 取长度,是纯粹的验证语言特性,不可综合。

一个未被初始化或驱动的 4 态变量(如 logic),仿真中它的默认值是什么?

关于 packed struct 与 unpacked struct,以下哪种说法是正确的?

给一个枚举变量调用哪个内置方法,可以直接得到它当前值对应的名字字符串(不带括号,仅方法名)?

关于 SystemVerilog 的 string 类型,以下哪种说法正确?