3.1 词法约定(Lexical Conventions)
词法约定是任何编程语言最基础的"书写规则"------包括空白符怎么用、注释怎么写、数字怎么表示、名字怎么起。Verilog的语法和C语言非常相似,有C语言基础的同学会感觉很亲切。
3.1.1 空白符(White Space)
空白符包括: 空格(␣)、制表符(Tab键 → \t)、换行符(Enter键 → \n)。
规则: Verilog编译器在编译时会忽略空白符,它们仅用于分隔标识符,提高代码可读性。
// 以下三种写法在Verilog编译器看来是完全等价的:
// 写法1:紧凑(不推荐,可读性差)
module M(a,b,c);output c;input a,b;and(c,a,b);endmodule
// 写法2:有空格和换行(推荐)
module M(a, b, c);
output c;
input a, b;
and (c, a, b);
endmodule
// 写法3:大量空白(编译器也会忽略)
module M ( a , b , c ) ;
output c ;
input a , b ;
and ( c , a , b ) ;
endmodule
特别注意 :字符串(用双引号括起来的内容)中的空白符不会被忽略 。"Hello World" 和 "HelloWorld" 是两个不同的字符串。
3.1.2 注释(Comments)
注释是写给"人"看的,编译器会完全忽略。Verilog支持两种注释风格:

// ============================================================
// 文件名:full_adder.v
// 功能:一位全加器
// 作者:张三
// 日期:2024年1月1日
// ============================================================
module full_adder(sum, c_out, a, b, c_in);
// ----- 端口声明 -----
output sum, c_out; // sum是和,c_out是进位输出
input a, b, c_in; // a和b是加数,c_in是进位输入
/*
* 以下是内部逻辑实现
* 使用两个异或门和两个与门加一个或门
*/
wire s1, c1, c2; // 内部连线
xor (s1, a, b); // s1 = a ^ b
and (c1, a, b); // c1 = a & b
xor (sum, s1, c_in); // sum = s1 ^ c_in
and (c2, s1, c_in); // c2 = s1 & c_in
or (c_out, c2, c1); // c_out = c2 | c1
endmodule
重要限制 :多行注释 /* ... */ 不允许嵌套!
/* 这是 /* 嵌套注释 */ 错误! */ // ❌ 编译报错
/* 这是 // 合法注释 */ // ✅ 单行注释可以嵌套在多行注释中
3.1.3 操作符(Operators)
Verilog的操作符按操作数的个数分为三类:

// 三种操作符的对比
reg a, b, c, d, sel;
initial begin
a = 1'b1;
b = 1'b0;
c = ~a; // 单目:c = 0
d = a & b; // 双目:d = 0
y = sel ? a : b; // 三目:如果sel=1则y=a,否则y=b
end
3.1.4 数字声明(Number Specification)
Verilog中数字的表示方法非常灵活,这是初学者需要重点掌握的内容。
① 指明位数的数字(Full Specification)
语法格式: <位宽>'<基数格式><数值>

// 指明位数的数字示例
4'b1111 // 4位二进制数 1111(十进制15)
8'hA5 // 8位十六进制数 A5(十进制165)
12'd255 // 12位十进制数 255(二进制 000011111111)
16'o1234 // 16位八进制数 1234
② 不指明位数的数字(Implicit Specification)
如果不指定位宽,Verilog会使用默认位宽(通常为32位,具体取决于仿真器和平台):
23456 // 32位十进制数 23456
'hC3 // 32位十六进制数 C3(高位补0)
'o21 // 32位八进制数 21
不指明位数的数字在拼接操作中是非法的! 因为拼接操作必须知道每个操作数的确切位宽。
③ X 和 Z 值
Verilog用两个特殊符号来表示不确定状态 和高阻状态,这是硬件描述语言区别于普通编程语言的重要特征:

12'h13x // 12位十六进制数,最低4位为x(不确定)
6'hx // 6位十六进制数,全部6位为x(即 6'bxxxxxx)
32'bz // 32位全部为z(高阻态)
4'b10?? // ? 是 z 的另一种表示,等价于 4'b10zz
位宽扩展规则:
-
如果最高位是 0、x 或 z,Verilog会分别使用 0、x 或 z填充更高的位
-
如果最高位是 1,Verilog会使用 0 填充更高的位
4'b1010 // 就是 1010
8'bz // 扩展为 8'bzzzzzzzz(全部用z填充)
8'b1 // 扩展为 8'b00000001(最高位是1,用0填充)
④ 负数和下划线
-6'd3 // 6位二进制补码形式存储的 -3
4'd-2 // ❌ 非法!负号必须放在位宽前面
12'b1111_0000_1010 // 下划线只用于提高可读性,编译器会忽略
// 等价于 12'b111100001010
3.1.5 字符串(Strings)
字符串是用双引号括起来的字符序列,必须写在一行内(不能跨行)。
"Hello Verilog World" // 合法的字符串
"a / b" // 合法的字符串
"Hello // ❌ 非法!字符串不能跨行
World"
字符串中可以用转义字符表示特殊符号:

// 字符串中的特殊字符
$display("Hello\nWorld"); // 输出两行:Hello 换行 World
$display("50%% discount"); // 输出:50% discount
3.1.6 标识符(Identifiers)和关键字(Keywords)
标识符 是程序员给变量、模块、信号等起的名字。关键字 是Verilog语言预留的、有特殊含义的标识符(全部为小写)。

// 合法的标识符
reg clk; // 时钟信号
reg [7:0] data_bus; // 数据总线
wire _internal; // 以下划线开头(合法)
wire addr_1; // 包含数字(合法,但不在首位)
// 非法的标识符
reg 1st_signal; // ❌ 以数字开头
reg data bus; // ❌ 包含空格
reg always; // ❌ always 是关键字
// 区分大小写的例子
reg CLK; // 这是 CLK(大写)
reg clk; // 这是 clk(小写)
// 两者是完全不同的信号!
3.1.7 转义标识符(Escaped Identifiers)
如果实在想用关键字作为标识符(虽然不推荐),或者标识符中包含特殊字符,可以用反斜线 \ 开头:
\data_bus // 转义标识符
\*mult* // 包含特殊字符
\always // 使用关键字作为标识符(不推荐!)
转义标识符以反斜线开始,以空白符结束。反斜线和空白符之间的所有字符都被当作标识符的一部分。
3.2 数据类型(Data Types)
数据类型是Verilog描述硬件的基础。理解每种数据类型的用途,是写出正确代码的前提。
3.2.1 值的种类(Value Set)
Verilog采用四值逻辑来模拟实际电路中的信号状态:

除了逻辑值,Verilog还使用强度值来解决多个驱动源之间的竞争问题:

竞争规则 :如果两个信号驱动同一个线网,强度高的信号胜出 。如果强度相同,结果为 x(不确定)。
// 强度示例(不要求初学者掌握,了解即可)
// 如果 strong1 和 weak0 同时驱动同一个线网,strong1 胜出
// 如果 strong1 和 strong0 同时驱动,结果为 x
3.2.2 线网(Nets)
线网(Net) 代表硬件电路中的物理连线,用于连接各个元件。它由驱动源(如门输出、模块输出)持续驱动。
// 最常用的线网类型:wire
wire a; // 声明一个1位的wire
wire [7:0] bus; // 声明一个8位的wire向量(总线)
线网的特性:

Verilog中的线网类型(wire是最常用的):

// wire的声明和使用
wire data; // 声明一个wire
assign data = a & b; // 用assign语句驱动wire
// supply0和supply1的用法
supply0 gnd; // 声明一个地线
supply1 vdd; // 声明一个电源线
assign a = vdd; // a 恒为 1
assign b = gnd; // b 恒为 0
3.2.3 寄存器(Registers)
寄存器(reg) 代表一个数据存储单元 ,可以保持值直到被重新赋值。注意: Verilog中的 reg 不一定是硬件中的触发器(Flip-Flop),它只是一个"能保持值的变量"。

reg reset; // 声明一个1位的reg
reg [7:0] count; // 声明一个8位的reg向量
initial begin
reset = 1'b1; // 初始化为1
#100 reset = 1'b0; // 100个时间单位后变为0
end
带符号寄存器(用于有符号数运算):
reg signed [63:0] m; // 64位带符号寄存器
integer i; // 32位带符号整数(见下节)
3.2.4 向量(Vectors)
线网和寄存器都可以声明为向量(位宽大于1)。如果没有指定位宽,默认为标量(1位)。
wire a; // 标量(1位),等价于 wire [0:0] a;
wire [7:0] bus; // 8位向量总线,bus[7]是最高位(MSB)
wire [31:0] busA, busB, busC; // 三条32位总线
reg clock; // 标量(1位)
reg [0:40] virtual_addr; // 41位向量,第0位是最高位
注意 :[7:0] 表示最高位是7,最低位是0。[0:7] 也是合法的 ,表示第0位是最高位。但习惯上使用 [n:0] 的形式(高位在前)。
向量域选择(Part Select)
可以选取向量的某一位或某几位:
wire [7:0] bus;
bus[7] // 第7位(最高位)
bus[2:0] // 最低3位:bus[2]、bus[1]、bus[0]
bus[0:2] // ❌ 非法!高位必须在左边
可变的向量域选择(Verilog-2001新增):
reg [255:0] data1; // data1[255]是最高位
reg [7:0] byte;
// [起始位 +: 宽度]:从起始位开始递增
byte = data1[31 +: 8]; // 相当于 data1[31:24]
// [起始位 -: 宽度]:从起始位开始递减
byte = data1[31 -: 8]; // 相当于 data1[31:24]
// 应用:用循环遍历向量的所有位
for (j = 0; j < 32; j = j + 1)
byte = data1[(j * 8) +: 8]; // 依次取 data1[7:0], [15:8], [23:16]...
3.2.5 整数、实数和时间寄存器
除了 reg 类型,Verilog还提供了几种专用的寄存器数据类型,用于特定场景。
① 整数(Integer)

integer counter; // 32位有符号整数
integer i, j, k; // 声明多个整数
initial begin
counter = -1; // 可以存储负数
for (i = 0; i < 16; i = i + 1) begin
// 循环体内代码
end
end
reg vs integer :reg 是无符号数,integer 是有符号数。如果需要处理负数,用 integer。
② 实数(Real)

real delta;
real frequency;
initial begin
delta = 2.13; // 十进制小数
frequency = 3e6; // 科学计数法:3,000,000
// 如果赋给整数,会取整:
integer i = delta; // i = 2(2.13取整)
end
③ 时间寄存器(Time)

time save_time; // 64位时间变量
initial begin
save_time = $time; // 保存当前仿真时间
$display("Current time = %t", save_time);
end
3.2.6 数组(Arrays)
Verilog允许声明 reg、integer、time、real 等类型的一维或多维数组。
integer count[0:7]; // 8个整数的数组
reg bool[31:0]; // 32个1位寄存器的数组
time chk_point[1:100]; // 100个时间变量的数组
reg [4:0] port_id[0:7]; // 8个5位寄存器的数组
integer matrix[4:0][0:255]; // 二维整数数组
数组 vs 向量的区别(常混淆!):

// 数组的访问
count[5] = 0; // 访问第5个元素
port_id[3] = 5'b01010; // 访问第3个元素,赋5位值
matrix[1][0] = 33559; // 访问二维数组的(1,0)位置
// ❌ 非法操作
port_id = 0; // 不能对整个数组赋值
matrix[1] = 0; // 不能对整个行赋值(二维数组)
3.2.7 存储器(Memory)
在数字电路仿真中,经常需要对寄存器文件(Register File) 、RAM 和 ROM 建模。Verilog使用寄存器的一维数组来表示存储器。
reg mem1bit[0:1023]; // 1K × 1位的存储器
reg [7:0] membyte[0:1023]; // 1K × 8位的存储器(字节寻址)
// 访问存储器中的特定字
membyte[511] = 8'hA5; // 向地址511写入数据
data = membyte[511]; // 从地址511读取数据
存储器 = 数组 :reg [7:0] mem[0:1023] 就是一个1024个元素的数组,每个元素是8位宽。
3.2.8 参数(Parameters)
parameter 用于定义常量 ,在编译时确定,运行时不能修改。但每个模块实例的参数值可以被重载(覆盖),这是参数最重要的用途。
parameter port_id = 5; // 常数5
parameter cache_line_width = 256; // 常数256
parameter signed [15:0] WIDTH; // 带符号的16位参数
// 局部参数(不可被重载)
localparam state1 = 4'b0001;
localparam state2 = 4'b0010;
为什么要用参数?

3.2.9 字符串(Strings as Registers)
字符串可以存储在 reg 类型的变量中,每个字符占用8位(1字节)。
reg [8*18:1] string_value; // 18字节(足够存储 "Hello Verilog World")
initial begin
string_value = "Hello Verilog World";
// 存储时:字符串占 18×8 = 144 位
end
规则:
-
如果寄存器宽度 > 字符串长度:高位补0
-
如果寄存器宽度 < 字符串长度:截去最左边的高位
💡 为什么不直接用
reg [143:0]? 使用8*n:1的形式让宽度计算更清晰,可读性更好。
3.3 系统任务和编译指令
系统任务(System Tasks)和编译指令(Compiler Directives)是Verilog提供的"内置工具函数"。系统任务以 $ 开头,编译指令以 `````(反引号)开头。
3.3.1 系统任务(System Tasks)
系统任务是Verilog预定义的函数,用于执行输入输出、仿真控制、调试辅助 等操作。它们类似于C语言中的标准库函数(如 printf、scanf)。
① 显示任务 ------ $display
$display 是Verilog中最常用的输出命令,用于在仿真控制台打印信息,类似于C语言的 printf。
语法:
$display(p1, p2, p3, ..., pn);
参数说明:
-
参数可以是字符串 (用双引号括起来)或变量/信号名
-
字符串中的
%是格式说明符,用于控制变量的显示格式// 基本用法
display("Hello Verilog"); // 输出:Hello Verilog display("The value is %d", count); // 输出:The value is 5// 多个变量
$display("a = %b, b = %b, sum = %b", a, b, sum);
格式说明符一览表:

// 综合示例
reg [3:0] a = 4'b1010;
reg [7:0] b = 8'hA5;
reg [31:0] c = 42;
string name = "Counter";
$display("a = %b (二进制), %d (十进制)", a, a); // a = 1010 (二进制), 10 (十进制)
$display("b = %h (十六进制)", b); // b = A5 (十六进制)
$display("c = %d (十进制), %h (十六进制)", c, c); // c = 42 (十进制), 2A (十六进制)
$display("Module: %m", ); // Module: top.stimulus
自动换行 :$display 会在输出末尾自动添加换行符 。如果不想换行,使用 $write。
② 不换行输出 ------ $write
$write 的用法与 $display 完全一样,唯一的区别是不自动换行。
// 对比
$display("Hello"); // 输出 "Hello" 并换行
$display("World"); // 输出 "World" 并换行
// 结果:
// Hello
// World
$write("Hello"); // 输出 "Hello" 不换行
$write("World"); // 输出 "World" 不换行
// 结果:HelloWorld
实际应用: 用于格式化输出表格或连续打印信息。
// 打印表头
$write("Time\t"); // 制表符分隔
$write("Value\n"); // 最后换行
// 打印数据
$write("%0d\t", $time);
$write("%b\n", count);
💡 %0d 中的 0 表示不填充空格(紧凑格式),输出 5 而不是 5(前面有空格)。
③ 监视任务 ------ $monitor
$monitor 是Verilog中最强大的调试工具之一。它会持续监视指定的信号,每当任何一个信号发生变化时,自动打印所有参数的值。
语法:
$monitor(p1, p2, p3, ..., pn);
特点:
-
只需调用一次,在整个仿真过程中持续有效
-
任何被监视的信号发生变化时,自动输出
-
同一时刻只能有一个
$monitor有效(后调用的会覆盖之前的)// 监视时钟和计数器
monitor(time, " clk = %b, reset = %b, q = %d", clk, reset, q);
// 每当 clk、reset 或 q 变化时,自动打印// 输出示例:
// 0 clk = 0, reset = 1, q = 0
// 10 clk = 1, reset = 1, q = 0
// 20 clk = 0, reset = 0, q = 1
// 30 clk = 1, reset = 0, q = 1
$monitor 的控制开关:

initial begin
$monitor($time, " q = %d", q);
#100 $monitoroff; // 前100个时间单位监视
#200 $monitoron; // 300个时间单位后重新开启
end
$display vs $monitor:

// 对比示例
initial begin
// 每10个时间单位手动打印一次
forever #10 $display($time, " q = %d", q);
// 自动监视,q变化时打印(更高效)
$monitor($time, " q = %d", q);
end
④ 仿真控制任务 ------ $stop 和 $finish

initial begin
#100 $stop; // 100个时间单位后暂停,查看信号状态
#200 $finish; // 再经过200个时间单位后结束仿真
end
💡 $stop 在交互式仿真中特别有用------你可以暂停仿真,查看当前所有信号的值,然后决定是否继续。
系统任务实战:完整的Testbench示例
module stimulus;
reg clk, reset;
wire [3:0] q;
// 实例化被测试模块
ripple_carry_counter r1(q, clk, reset);
// ----- 时钟发生器 -----
initial begin
clk = 1'b0;
forever #5 clk = ~clk; // 周期10ns
end
// ----- 测试流程 -----
initial begin
// 第1阶段:复位
$display("===== Starting Test =====");
$display("Applying reset...");
reset = 1'b1;
#20 reset = 1'b0;
$display("Reset released at time %0t", $time);
// 第2阶段:正常计数
#100;
$display("Counting for 100 time units");
// 第3阶段:再次复位
#50;
$display("Applying reset again...");
reset = 1'b1;
#10 reset = 1'b0;
// 第4阶段:结束
#50;
$display("===== Test Complete =====");
$finish;
end
// ----- 监视输出 -----
initial begin
$monitor($time, " clk=%b reset=%b q=%d", clk, reset, q);
end
endmodule
仿真输出:
===== Starting Test =====
Applying reset...
0 clk=0 reset=1 q=x
5 clk=1 reset=1 q=x
10 clk=0 reset=1 q=x
15 clk=1 reset=1 q=x
20 clk=0 reset=0 q=0
Reset released at time 20
25 clk=1 reset=0 q=0
30 clk=0 reset=0 q=1
35 clk=1 reset=0 q=1
40 clk=0 reset=0 q=2
...
Counting for 100 time units
...
Applying reset again...
...
===== Test Complete =====
3.3.2 编译指令(Compiler Directives)
编译指令以 `````(反引号) 开头,在编译阶段 生效,用于控制编译器的行为。它们类似于C语言中的 #define、#include、#ifdef 等预处理指令。
⚠️ 注意 :反引号 ````` 和单引号 ' 是不同的!很多初学者会把 ```define`` 写成 'define,导致编译错误。
① define ------ 文本宏定义
```define`` 用于定义文本宏,在编译时进行文本替换。这是Verilog中最常用的编译指令。
语法:
`define <宏名> <替换文本>
使用:
`define WORD_SIZE 32 // 定义宏
reg [`WORD_SIZE-1:0] data; // 展开为 reg [31:0] data;
`define S $stop; // 定义别名
`define WIDTH 8
// 常用定义示例
`define TRUE 1'b1
`define FALSE 1'b0
`define VDD 1'b1
`define GND 1'b0
define 的典型应用:
// 1. 定义常量
`define DATA_WIDTH 32
`define ADDR_WIDTH 16
reg [`DATA_WIDTH-1:0] data_bus;
reg [`ADDR_WIDTH-1:0] addr_bus;
// 2. 定义状态机状态编码
`define IDLE 3'b000
`define READ 3'b001
`define WRITE 3'b010
`define WAIT 3'b011
// 3. 定义测试参数
`define CLK_PERIOD 10
`define RESET_TIME 20
`define SIM_TIME 1000
注意事项:
-
宏定义不受作用域限制,一旦定义在整个文件中都有效
-
宏定义通常写在文件开头
-
宏名通常用大写,便于识别
② include ------ 文件包含
```include`` 用于在编译时将另一个Verilog文件的内容插入到当前文件中。
语法:
`include "文件名"
典型应用:
创建一个头文件 defines.v,存放公共定义:
// ========== defines.v ==========
`define DATA_WIDTH 32
`define ADDR_WIDTH 16
`define TRUE 1'b1
`define FALSE 1'b0
`define IDLE 3'b000
`define READ 3'b001
`define WRITE 3'b010
在主文件中包含头文件:
// ========== top_module.v ==========
`include "defines.v"
module top_module;
reg [`DATA_WIDTH-1:0] data; // 使用宏定义
reg [`ADDR_WIDTH-1:0] addr;
always @(posedge clk) begin
if (state == `IDLE) begin
// 状态机逻辑
end
end
endmodule
实际工程中的建议:
-
把所有的
define集中放在一个或多个头文件中 -
使用
include在需要的地方引入 -
这样修改参数只需改一处,所有文件同步更新
③ ifdef / else / endif ------ 条件编译
条件编译允许你根据是否定义了某个宏,有选择地编译某段代码。
语法:
`ifdef <宏名>
// 如果宏已定义,编译这部分代码
`else
// 如果宏未定义,编译这部分代码
`endif
典型应用:
`define DEBUG // 注释掉这一行可以关闭调试信息
module test;
initial begin
`ifdef DEBUG
$display("Debug: simulation started");
$monitor($time, " q = %d", q);
`else
// 不输出调试信息
`endif
end
endmodule
多分支条件编译(Verilog-2001):
`define SIM_MODE 1
`ifdef SIM_MODE
// 仿真模式:使用行为模型
memory_behavioral u_mem(...);
`elsif SYNTH_MODE
// 综合模式:使用门级模型
memory_gate u_mem(...);
`else
// 默认模式
memory_default u_mem(...);
`endif
实际工程案例:
// ========== project_config.vh ==========
`define FPGA // 当前目标平台是FPGA
// `define ASIC // 如果改为ASIC,注释掉上面这行
// ========== design.v ==========
`include "project_config.vh"
module my_design(...);
`ifdef FPGA
// FPGA版本:使用PLL
pll u_pll(...);
`else
// ASIC版本:使用外部时钟
// 不使用PLL
`endif
endmodule
④ timescale ------ 时间尺度
timescale 用于指定仿真时间单位 和时间精度 。这在仿真中非常重要,它决定了 #10 到底代表多长时间。
语法:
`timescale <时间单位> / <时间精度>
合法值: 时间单位和时间精度必须是 1、10 或 100 的倍数。

示例:
`timescale 1ns / 1ps // 时间单位1ns,精度1ps
#10 // 延迟10ns
`timescale 10ns / 1ns // 时间单位10ns,精度1ns
#10 // 延迟100ns
`timescale 1us / 10ns // 时间单位1us,精度10ns
#5.5 // 延迟5.5us(精度10ns,实际为5.50us)
时间精度(Time Precision)的作用:
时间精度决定了延迟值取整的精度:
`timescale 1ns / 100ps
#5.45 // 实际延迟 = 5.4ns(精度为100ps,四舍五入到最近的100ps倍数)
#5.567 // 实际延迟 = 5.6ns
$time 的单位 :$time 返回的时间值是以当前模块的时间单位 为基准的,不同模块的 timescale 不同,$time 的返回值也可能不同。
`timescale 10ns / 1ns
module M1;
initial begin
#5 $display($time); // 输出5(单位10ns,即50ns)
end
endmodule
`timescale 1ns / 1ps
module M2;
initial begin
#5 $display($time); // 输出5(单位1ns,即5ns)
end
endmodule
完整学习示例:带注释的计数器仿真
// ============================================================
// 文件名:counter_with_testbench.v
// 功能:四位计数器 + 完整的Testbench
// 说明:综合了本章所有的系统任务和编译指令
// ============================================================
`timescale 1ns / 10ps // 时间单位1ns,精度10ps
`define CLK_PERIOD 20 // 时钟周期20ns
`define RESET_TIME 15 // 复位持续时间15ns
`define SIM_TIME 200 // 仿真总时长200ns
`define DEBUG // 开启调试模式
// ========== 设计模块 ==========
module counter_4bit(q, clk, reset);
output reg [3:0] q;
input clk, reset;
// 同步复位计数器
always @(posedge clk) begin
if (reset)
q <= 4'b0;
else
q <= q + 1'b1;
end
endmodule
// ========== 测试模块 ==========
module testbench;
reg clk, reset;
wire [3:0] q;
// 实例化设计模块
counter_4bit uut(q, clk, reset);
// ----- 时钟发生器 -----
initial begin
clk = 1'b0;
forever #(`CLK_PERIOD/2) clk = ~clk;
end
// ----- 测试流程 -----
initial begin
$display("========================================");
$display(" 4-bit Counter Testbench");
$display("========================================");
$display("Clock Period = %0d ns", `CLK_PERIOD);
$display("Reset Time = %0d ns", `RESET_TIME);
$display("Sim Time = %0d ns", `SIM_TIME);
$display("========================================\n");
`ifdef DEBUG
$display("[DEBUG] Starting simulation at time %0t", $time);
`endif
// 应用复位
reset = 1'b1;
#(`RESET_TIME) reset = 1'b0;
`ifdef DEBUG
$display("[DEBUG] Reset released at time %0t", $time);
`endif
// 等待计数
#(`SIM_TIME - `RESET_TIME);
`ifdef DEBUG
$display("[DEBUG] Simulation complete at time %0t", $time);
`endif
$display("\n========================================");
$display(" Simulation Complete!");
$display("========================================");
$finish;
end
// ----- 监视输出 -----
initial begin
$monitor("%0t ns: clk=%b reset=%b q=%d", $time, clk, reset, q);
end
// ----- 条件调试输出 -----
initial begin
`ifdef DEBUG
#1 $display("[DEBUG] Initial q value = %d", q);
#(`RESET_TIME + 5) $display("[DEBUG] First count should appear soon...");
#(`CLK_PERIOD) $display("[DEBUG] Count value = %d", q);
#(`CLK_PERIOD * 4) $display("[DEBUG] After 5 counts: q = %d", q);
#(`CLK_PERIOD * 10) $display("[DEBUG] After 15 counts: q = %d", q);
`endif
end
endmodule
仿真输出(部分):
========================================
4-bit Counter Testbench
========================================
Clock Period = 20 ns
Reset Time = 15 ns
Sim Time = 200 ns
========================================
[DEBUG] Starting simulation at time 0
[DEBUG] Initial q value = x
0 ns: clk=0 reset=1 q=x
5 ns: clk=1 reset=1 q=x
10 ns: clk=0 reset=1 q=x
15 ns: clk=1 reset=0 q=0
[DEBUG] Reset released at time 15000
[DEBUG] First count should appear soon...
20 ns: clk=0 reset=0 q=0
25 ns: clk=1 reset=0 q=0
30 ns: clk=0 reset=0 q=1
[DEBUG] Count value = 1
35 ns: clk=1 reset=0 q=1
40 ns: clk=0 reset=0 q=2
45 ns: clk=1 reset=0 q=2
50 ns: clk=0 reset=0 q=3
55 ns: clk=1 reset=0 q=3
...
[DEBUG] After 5 counts: q = 5
...
[DEBUG] After 15 counts: q = 15
...
[DEBUG] Simulation complete at time 200000
========================================
Simulation Complete!
========================================