第 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
endmodulemodule mux2 (...):声明一个名叫mux2的模块,括号里是端口列表。- 每个端口都有方向:
input(只读,外部驱动进来)、output(只写,模块驱动出去);还有一种inout,用于双向信号(比如真实的 I/O 引脚),本系列不会深入用到。 - 端口的类型(这里是
logic)会在"数据类型"一章详细展开。
有了模块定义之后,例化(instantiate)就是"创建一个这个模块的实例",最清晰的写法是按名字连接每个端口:
mux2 dut (
.sel(sel),
.a(a),
.b(b),
.y(y)
);mux2 是模块名,dut 是这次例化给它起的实例名(一个模块可以被例化多次,每次用不同的实例名区分)。.端口名(连接的信号) 这种写法叫按名连接,明确写出端口名和信号名的对应关系,不用去记端口列表的顺序——比另一种"按位置对应"的写法更不容易出错,也是本系列后面会一直采用的风格。
数字字面量:<位宽>'<进制><数值>
1'b0、8'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 则用于"错误严重到没必要继续跑下去"的场景,会直接终止仿真。
给模块一些可配置的参数:parameter、localparam 与 #(...)
有时候希望同一个模块能在不同场景下配置成不同的宽度或深度(比如一个通用的 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=8、DEPTH=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 (...); 这行例化代码,#(...) 里的内容起什么作用?