第 5 章 · 共 16 章
更多数据类型:2 态与 4 态、枚举、结构体与 typedef
理解 2 态与 4 态数据类型的区别与取舍,用 enum 给状态命名,用 struct 打包相关字段,用 typedef 让类型名字有意义。
上一章的 logic(以及 wire、reg)都是4 态类型:除了 0、1,还能表示未知态 x 和高阻态 z。这一章继续往前走:先弄清楚 2 态类型和 4 态类型该怎么选,再学会用 enum、struct、typedef 把数据组织得更清晰,而不是散落一堆孤立的变量。
2 态与 4 态数据类型
SystemVerilog 里的整数类型分成两大阵营:
- 4 态类型:
logic、reg、wire、integer等,每一位可以是0、1、x(未知)、z(高阻)。 - 2 态类型:
bit、byte、shortint、int、longint等,每一位只能是0或1,没有x/z。
| 类型 | 位宽 | 是否 4 态 |
|---|---|---|
logic / reg | 声明时指定(默认 1 位) | 是 |
wire | 声明时指定(默认 1 位) | 是 |
integer | 32 位 | 是 |
bit | 声明时指定(默认 1 位) | 否 |
byte | 8 位 | 否 |
shortint | 16 位 | 否 |
int | 32 位 | 否 |
longint | 64 位 | 否 |
两者最直观的差别是未初始化时的默认值:
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;
endSystemVerilog 的 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;
endtypedef:给类型起名字
前面两节其实已经在用 typedef 了——它的作用就是给一个类型起一个新名字,之后就可以像内置类型一样使用这个名字,而不用每次都重复写完整的类型定义。除了给 enum/struct 命名,typedef 也可以直接用在简单类型上:
typedef logic [7:0] byte_t;
byte_t data_in, data_out;好处很直接:一是可读性(byte_t 比 logic [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!");
endstring 变量可以直接用 ==/!= 比较内容(不需要像 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());
endlog_entry 既清楚地表达了"一条记录包含哪些字段",又因为 status 是 enum 类型,打印出来的是 PKT_OK 而不是裸数字 0——这就是 enum/struct/typedef 组合起来带来的可读性提升。
小结
- 4 态类型(
logic、reg、integer)能表示x/z,适合描述硬件信号;2 态类型(bit、int、byte等)没有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 类型,以下哪种说法正确?