SystemVerilog 基础

第 2 章 · 共 16 章

基础语法:模块、字面量与打印输出

学会读懂并写出模块的声明与例化、SystemVerilog 的数字字面量格式、$display/$sformatf 与消息严重程度、parameter/localparam 参数化,以及三目(条件)运算符——这些是后面每一章示例代码都会默认你已经认识的“连接组织”。

上一章的 mux2 例子已经用到了不少语法:模块的声明和例化、1'b0 这样的字面量、$display 里的 %0b,还有 sel ? b : a 这个问号写法——但都还没有正式解释过。这一章把这些基础语法一次性讲清楚,后面每一章的示例代码都会默认你已经认识它们。

模块:声明、端口与例化

一个模块用 module...endmodule 包裹起来,中间是端口列表和内部逻辑:

module mux2 (
  input  logic sel,
  input  logic a,
  input  logic b,
  output logic y
);
 
  always_comb begin
    y = sel ? b : a;
  end
 
endmodule
  • module mux2 (...):声明一个名叫 mux2 的模块,括号里是端口列表
  • 每个端口都有方向:input(只读,外部驱动进来)、output(只写,模块驱动出去);还有一种 inout,用于双向信号(比如真实的 I/O 引脚),本系列不会深入用到。
  • 端口的类型(这里是 logic)会在"数据类型"一章详细展开。

有了模块定义之后,例化(instantiate)就是"创建一个这个模块的实例",最清晰的写法是按名字连接每个端口:

mux2 dut (
  .sel(sel),
  .a(a),
  .b(b),
  .y(y)
);

mux2 是模块名,dut 是这次例化给它起的实例名(一个模块可以被例化多次,每次用不同的实例名区分)。.端口名(连接的信号) 这种写法叫按名连接,明确写出端口名和信号名的对应关系,不用去记端口列表的顺序——比另一种"按位置对应"的写法更不容易出错,也是本系列后面会一直采用的风格。

数字字面量:<位宽>'<进制><数值>

1'b08'hFF 这样的写法是 SystemVerilog 的数字字面量语法,格式是:

<位宽>'<进制><数值>
  • <位宽>:这个字面量占多少位;省略时默认按 32 位处理。
  • <进制>b/B(二进制)、o/O(八进制)、d/D(十进制,默认进制)、h/H(十六进制)。
  • <数值>:对应进制下的数字;对 4 态类型来说,还可以用 x/X(未知)、z/Z(高阻)。
4'b1010        // 4 位,二进制 1010,等于十进制 10
8'hFF          // 8 位,十六进制 FF,等于十进制 255
8'd10          // 8 位,十进制 10
1'bx           // 1 位,未知态(只有 4 态类型才有意义)
32'h1000_2000  // 数字中的下划线只是为了可读性,会被忽略

_ 可以随意插入数字之间(通常用来分隔字节,像上面 1000_2000 那样),纯粹是为了可读性,不影响字面量的实际数值。如果省略位宽和进制直接写一个数字(比如 10),会被当成 32 位有符号十进制数处理。

$display 与格式说明符

$display 用来在仿真时打印一行信息,用法类似其他语言里的格式化打印:

$display("sel=%0b -> y=%0b (expected %0b)", sel, y, a);

字符串里的 %X 是格式说明符,告诉 $display 按什么进制打印后面对应的参数:

说明符含义
%b二进制
%d十进制
%h十六进制
%s字符串
%t仿真时间

前面加个 0(比如 %0d%0b)表示"不做固定宽度的前导填充,按实际需要的宽度打印"——%d 会按信号的位宽固定填充空格/零,而 %0d 只打印刚好够用的位数,看起来更紧凑,这也是本系列示例里几乎都用 %0d/%0b/%0h 而不是不带 0 版本的原因。$display 每次调用会自动换行;如果不想换行,可以用 $write(本系列不会用到)。

$sformatf 与消息级别:$display$warning$error$fatal

$display 直接把格式化好的字符串打印出来;$sformatf 用法完全一样,但不打印,而是把格式化结果作为字符串返回,方便先拼好消息内容,再决定怎么处理它:

string msg;
msg = $sformatf("addr=%0h data=%0h", 8'h20, 8'hFF);
$display(msg);  // 效果等同于直接 $display("addr=%0h data=%0h", 8'h20, 8'hFF);

除了 $display,SystemVerilog 还提供三个按严重程度分级的打印任务,后面章节的验证代码会经常用到:

任务严重程度是否终止仿真
$display无(普通信息)
$warning警告
$error错误否(但会被大多数仿真器统计为失败)
$fatal致命错误是——内部会调用 $finish 立即停止仿真
if (y != expected) begin
  $error("mismatch: expected=%0b actual=%0b", expected, y);
end

$error/$warning 不会自动停止仿真——它们的作用是打上严重程度的标记,方便仿真日志和覆盖率工具统计"跑完这次仿真到底有没有失败",而不是像 $display 那样只是普通输出。$fatal 则用于"错误严重到没必要继续跑下去"的场景,会直接终止仿真。

给模块一些可配置的参数:parameterlocalparam#(...)

有时候希望同一个模块能在不同场景下配置成不同的宽度或深度(比如一个通用的 FIFO,宽度可以是 8 位也可以是 32 位),又不想为每种配置写一个新模块。parameter 就是为此设计的——声明一个可以在例化时覆盖的编译期常量:

module fifo #(
  parameter int WIDTH = 8,
  parameter int DEPTH = 16
) (
  input  logic [WIDTH-1:0] data_in,
  output logic [WIDTH-1:0] data_out
);
  localparam int ADDR_BITS = $clog2(DEPTH);  // 由 DEPTH 推导,不能单独覆盖
  logic [WIDTH-1:0] mem [DEPTH];
  // ...
endmodule

例化时用 #(...) 覆盖参数值,写法和端口的按名连接很像:

fifo #(.WIDTH(32), .DEPTH(64)) my_fifo (.data_in(din), .data_out(dout));

不写 #(...) 就使用声明时的默认值(WIDTH=8DEPTH=16)。parameter 可以被 #(...) 覆盖;localparam 则是只读的派生常量(比如根据 DEPTH 算出寻址需要的位数),不能在例化时覆盖。这个系列后面会看到 class 也能用类似的语法参数化(比如 mailbox #(int)),用来表示"这个类可以装什么类型的数据"——那部分留到讲完 class 之后再展开。

三目(条件)运算符 ?:

mux2 里的 y = sel ? b : a; 用的是三目运算符(也叫条件运算符),语法是:

条件 ? 条件为真时的值 : 条件为假时的值

sel ? b : a 的意思是:"如果 sel 为真,结果是 b;否则结果是 a"——这正是一个二选一多路选择器(mux)应该做的事,也是为什么这一行代码就能完整描述 mux2 的行为。三目运算符在 SystemVerilog 里非常常用,本系列后面几乎每一章都会看到它。

关于时间单位的简单说明

#10 这样的延时语句会让仿真时间往前推进 10 个"时间单位"。一个时间单位具体对应多长的真实时间,由 `timescale 这样的设置决定,下一章会专门展开——现在只需要知道 #N 表示"让仿真时间前进 N 个单位,再继续往下执行"就够了,后面遇到延时和时钟时会反复用到同样的写法。

小结

  • 模块用 module...endmodule 声明,端口有 input/output(还有较少用到的 inout)方向;例化时用 .端口名(信号名) 按名字连接,比按位置连接更清晰、不容易出错。
  • 数字字面量的格式是 <位宽>'<进制><数值>,比如 8'hFF;省略位宽默认按 32 位处理,省略进制默认是十进制。
  • $display 里的 %b/%d/%h/%s/%t 是格式说明符;前面加 0(如 %0d)表示不做固定宽度填充,按实际需要的宽度打印。
  • $sformatf$display 用法一样,但返回格式化后的字符串而不是直接打印;$display/$warning/$error/$fatal 按严重程度分级,$fatal 会终止仿真,其余不会。
  • parameter(可被 #(...) 覆盖)和 localparam(只读派生常量)让同一个模块能在例化时配置成不同的宽度/深度等参数。
  • 三目运算符 条件 ? 真值 : 假值 是"二选一"的简写,mux2 的核心逻辑就是靠它描述的。
  • #N 让仿真时间推进 N 个时间单位,具体时长由 `timescale 之类的设置决定,下一章会专门展开。

在例化语句 mux2 dut (.sel(sel), .a(a), .b(b), .y(y)); 里,dut 和 mux2 分别是什么?

字面量 8'hFF 表示的是什么?

表达式 sel ? b : a 的含义是什么?

$display 里的 %0d 和 %d 相比,主要区别是什么?

$error 和 $display 相比,主要区别是什么?

fifo #(.WIDTH(32), .DEPTH(64)) my_fifo (...); 这行例化代码,#(...) 里的内容起什么作用?