Verilog HDL学习笔记(三)| 第3章 基本概念(上)——词法约定与数据类型

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 integerreg 是无符号数,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允许声明 regintegertimereal 等类型的一维或多维数组

复制代码
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)RAMROM 建模。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语言中的标准库函数(如 printfscanf)。

① 显示任务 ------ $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!
========================================
相关推荐
Hotchip_MEMS1 小时前
MS2202AB-M15E输出驱动能力:直连MCU GPIO无需三极管
人工智能·笔记·物联网·电脑·制造
Privasa-隐私实验室1 小时前
跨端技术选型|Flutter搭建隐私加密App,安卓与iOS双端差异适配实战
android·笔记·安全·flutter·ios·隐私安全·aes-256
tju新生代魔迷1 小时前
Verilog HDL 学习笔记(九)| 第 9 章 实用建模技术
笔记·学习
凯尔萨厮1 小时前
Java学习笔记十三(Stream与Lambda)
笔记·学习
爱三盖浇饭2 小时前
挑战从零开始学习Kafka2: Kafka 常见问题解决方案
学习·kafka
dtq04243 小时前
数据结构 - 线性表 - 双向链表
c语言·数据结构·学习
M78佐菲9 小时前
Linux学习笔记:TCP协议
linux·笔记·学习·tcp/ip·算法
噜~噜~噜~10 小时前
操作系统笔记-2.3.5.1.1生产者-消费者问题
笔记·操作系统
yi.Ist10 小时前
数据定义语言-DDL操作
数据库·学习·mysql·oracle·大海豚