【个人向】Verilog基础:模块代码、Testbench详解与仿真图

文章简介

个人学习总结2 - Verilog最基本模块与Testbench仿真
个人向!!!
参考:数字逻辑基础与Verilog设计(英文原书第3版)McGrawHill Education
参考:机械工业出版社中文翻译版 - 国外电子与电气工程技术丛书
软件:Quartus_II,Vivado,ChatGPT
此文章包括Verilog基本模块代码,Testbench代码,Tb语句讲解,仿真结果与分析!
个人NTU在读,欢迎指正


基本的语法见上一篇文章 :)
状态机电路、Tb及仿真详见下一篇文章,本篇篇幅太长了 :)


目录

文章简介

目录

1. 组合逻辑基础模块

1.1. 4to1MUX多路选择器

1.2. 译码器Decoder(拓展)

拓展 - 7段译码器​编辑

1.3. 编码器Encoder(拓展)

拓展 - 优先级编码器priority

1.4. D锁存器latch

2. 时序逻辑基础模块

2.1. D触发器DFF

2.2. 寄存器register

2.3. 移位寄存器shift register

2.4. !!!计数器counter

2.5. 计数器拓展-2位从0-59计数

X. 由于篇幅太长,10000字了

!状态机FSM



1. 组合逻辑基础模块

1.1. 4to1MUX多路选择器

- 4个输入,使用2bit的选择信号S来决定谁作为输出。
- 第一种写法,使用if / else,这种写法综合出来比较复杂

module test1
(
    input [2:0] w0,  //这里使用3bit的待选输入信号,后面TB看着清晰点
    input [2:0] w1,
    input [2:0] w2,
    input [2:0] w3,
    input [1:0] S,   //选择信号S
    output reg [2:0] Y  //输出Y
);
    always@(*)
    begin
        if(S == 0)   //S等于几就选w几
            Y = w0;
        else if(S == 1)
            Y = w1;
        else if(S == 2)
            Y = w2;
        else
            Y = w3;
    end
endmodule

- 第二种写法,使用case,这种方法综合出来直接是多bit输入的MUX,比较简单

module test1
(
    input [2:0] w0, w1, w2, w3,
    input [1:0] S,
    output reg [2:0] Y
);
    always@(*)
    begin
        case(S)
            0: Y = w0;
            1: Y = w1;
            2: Y = w2;
            3: Y = w3;
        endcase
    end
endmodule

- Testbench如下

`timescale 1ns / 1ps  //时间单位 / 精度
module tb_test1;
    reg [2:0] w0_test, w1_test, w2_test, w3_test;  //将输入声明为reg
    reg [1:0] S;
    wire [2:0] Y;   //输出声明为wire

    // Instantiate the design under test (DUT) 实例化连接
    test1 dut (
        .w0(w0_test),  // Connect w0_test to w0 input of DUT
        .w1(w1_test),
        .w2(w2_test),
        .w3(w3_test),
        .S(S),
        .Y(Y)
    );

    // Test测试
    initial 
    begin
        // Apply initial values to the reg signals
        w0_test = 4;  // Now w0_test is assigned a value
        w1_test = 5;
        w2_test = 6;
        w3_test = 7;
        S = 2'b00; // S = 0, expect Y = w0_test

        // Display header
        $display("S\tw0\tw1\tw2\tw3\tY");
        $monitor("%b\t%b\t%b\t%b\t%b\t%b", S, w0_test, w1_test, w2_test, w3_test, Y);

        // Apply different values for S
        #10 S = 2'b01; // S = 1, expect Y = w1_test
        #10 S = 2'b10; // S = 2, expect Y = w2_test
        #10 S = 2'b11; // S = 3, expect Y = w3_test

        #30 $finish;  //30ns后结束仿真
    end
endmodule

超详细的Testbench解释
1. timescale

- timescale 指令用于指定仿真的时间单位和精度,它是可选的,但通常在testbench和模块文件的顶部添加,以确保仿真时间的一致性和精确度。
- 1ns表示仿真时间的单位为纳秒;1ps表示时间的精度为皮秒。
- 如果改成1ns / 1ns,
2. 模块声明
- 还是用“module”和“endmodule”来声明测试模块,名称通常会加上“_tb”来表示是tb模块。
- 记得直接在名称后面加上“;”分号。
3. 输入输出声明
- Tb中不需要显式地声明输入和输出,测试嘛,实例化连接到原模块模拟一下就行。
- reg:用于表示可以在仿真过程中赋值的信号,通常用于输入信号,表示它们在仿真中可以存储和改变。“_test”这里表示tb中的测试信号,和原模块做区分。
- wire:用于表示通过组合逻辑传输的信号,通常用于输出信号,它的值由模块或其他逻辑驱动。
4. 实例化连接
- 正常的模块调用,把原模块调用到tb里,在tb中模块实例化是必须的,其的目的是将设计单元DUT嵌入到tb中,使其可以进行仿真验证。
5. 测试部分
- 由于是组合逻辑验证,每种情况跑一次就行,所以用了initial。
- 给所有的输入进行赋值,作为一开始的情况,这里直接给S赋值为0,所以一开始就会选择w0_test
- "$display"是Verilog中用于打印消息到仿真输出(控制台)的系统任务,它的作用类似于“printf”函数,在仿真过程中输出文本或变量的值。
- display不写其实不影响tb的仿真,它的意义主要体现在输出格式化调试便捷性上,它用来打印头部或者一些标识性的消息,在tb中用来在仿真开始时打印一行标题,这样你就可以清楚地知道每一列代表的信号是什么。(左图没有display,右图有display)(Tcl控制台截图)

- "S\tw0\tw1\tw2\tw3\tY"是字符串,表示输出的文本格式,"\t"是制表符"Tab",用于在输出中加入空格,使得每个变量名之间有一定的间隔,使输出更加整齐,查看输出结果时,变量名将以表格形式排布。这部分就和python差不多,“ ”内输入什么就打印出什么。
- "$monitor"是Verilog 中用于实时监控信号的系统任务,用来实时监控并输出信号的值,它的行为会随着信号的变化而实时更新,display只执行一次,而monitor会持续监视指定的信号,一旦这些信号的值发生变化,它就会重新输出最新的信号值。
- monitor会一直执行,直到最大仿真时间,或者"$finish"被调用表示结束。(机考题出现)
- "%b\t%b\t%b\t%b\t%b\t%b"是格式字符串,表示输出的格式,“%b”表示二进制形式,“&d”则表示十进制,“\t”同样为制表符,使输出格式对齐。
- 格式完了用逗号隔开S, w0_test, w1_test, w2_test, w3_test, Y表示要输出的信号,当这些信号的值发生变化时,monitor会自动打印这些信号的新值。
- 如果改成"%d\t%b\t%b\t%b\t%b\t%b",则第一个"S"的控制台输出呈现十进制,如下图:

- #10表示延迟10ns,完了不要任何符号,tb中语句延迟10ns后S变化为1,10ns后变为2,10ns后变为3,以此来测试多路选择器,图中Y依次输出w0_test、w1_test、w2_test和w3_test的值。
- "$finish"表示仿真结束,我故意延迟了30ns,所以看到“S = 3”持续了30ns,延迟时间可以修改。
呼……!好!每一行都讲了,进入下个模块!

1.2. 译码器Decoder(拓展)

- 输入二进制数,表示几就让输出的第几bit为1。

module decoder_2to4 (
    input [1:0] A,     // 2-bit input
    output reg [3:0] Y // 4-bit output
)
    always @ (A) 
    begin
        case (A)
            2'b00: Y = 4'b0001;
            2'b01: Y = 4'b0010;
            2'b10: Y = 4'b0100;
            2'b11: Y = 4'b1000;
            default: Y = 4'b0000; // 默认情况下输出 0000
        endcase
    end
endmodule
module tb_test1;
    // Declare the reg types for the inputs
    reg [1:0] A_test;
    wire [3:0] Y_test;

    // DUT实例化
    test1 dut 
    (
        .A(A_test),  
        .Y(Y_test)
    );

    // Test sequence
    initial begin
        // Display header
        $display("A\tY");
        $monitor("%b\t%b", A_test, Y_test);
        
        // Apply initial values to the reg signals
        A_test = 2'b00;   //Y = 0001
        // Apply different values for A_test
        #10 A_test = 2'b01; // A_test = 1, expect Y = 0010
        #10 A_test = 2'b10; // A_test = 2, expect Y = 0100
        #10 A_test = 2'b11; // A_test = 3, expect Y = 1000
        #10 $finish;  
    end
endmodule

拓展 - 7段译码器

module seg7 
(
    input [3:0] bcd,      // 输入 4 位 BCD
    output reg [6:0] leds  // 7 段 LED 显示
);
    always @(*)
        case (bcd)           //abcdefg
            4'b0000: leds = 7'b1111110; // 0
            4'b0001: leds = 7'b0110000; // 1
            4'b0010: leds = 7'b1101101; // 2
            4'b0011: leds = 7'b1111001; // 3
            4'b0100: leds = 7'b0110011; // 4
            4'b0101: leds = 7'b1011011; // 5
            4'b0110: leds = 7'b1011111; // 6
            4'b0111: leds = 7'b1110000; // 7
            4'b1000: leds = 7'b1111111; // 8
            4'b1001: leds = 7'b1111011; // 9
            default: leds = 7'b0000000; // 错误或不支持的 BCD 输入,显示空白
        endcase
endmodule

1.3. 编码器Encoder(拓展)

- 与译码器相反

module encoder (
    input [7:0] Data,
    output reg [2:0] Code
); 
    always @(*) 
    begin
        case (Data)
            8'b00000001: Code = 3'b000;
            8'b00000010: Code = 3'b001;
            8'b00000100: Code = 3'b010;
            8'b00001000: Code = 3'b011;
            8'b00010000: Code = 3'b100;
            8'b00100000: Code = 3'b101;
            8'b01000000: Code = 3'b110;
            8'b10000000: Code = 3'b111;
            default: Code = 3'b000; 
        endcase
    end
endmodule
module tb_test1;
    // Signal declarations
    reg [7:0] Data;
    wire [2:0] Code;
    // Instantiate the encoder module
    test1 dut 
    (
        .Data(Data),  //输入
        .Code(Code)   //输出
    );
    initial 
    begin
        // Monitor the outputs
        $display("Data\tCode");
        $monitor("%b\t%b", Data, Code);
        // Test cases
        Data = 8'b00000001;
        #10 Data = 8'b00000010;
        #10 Data = 8'b00000100;
        #10 Data = 8'b00001000;
        #10 Data = 8'b00010000; 
        #10 Data = 8'b00100000;
        #10 Data = 8'b01000000; 
        #10 Data = 8'b10000000;
        #10 Data = 8'b11111111;
        // Finish simulation
        #10 $finish;
    end
endmodule

拓展 - 优先级编码器priority

- 比起普通的编码器多了一个有效信号“v”,有效信号用于区分是否存在有效输入,在case里时v=1,不在时v=0.

module test1 (
    input [3:0] D,       // 4-bit 输入
    output reg [1:0] Y,  // 2-bit 输出
    output reg V         // 有效位 (Valid) 输出
);
    always @(*) 
    begin
        V = 1;
        casex (D) // 使用 casex 表示支持模糊匹配(x 代表 don't care)
            4'b1xxx: Y = 2'b11; // D[3] 优先级最高
            4'b01xx: Y = 2'b10; // D[2]
            4'b001x: Y = 2'b01; // D[1]
            4'b0001: Y = 2'b00; // D[0] 优先级最低
            default: begin
                Y = 2'b00; // 默认输出
                V = 0;
            end
        endcase
    end
endmodule 
module tb_test1;
    reg [3:0] D;       // 4-bit 输入
    wire [1:0] Y;      // 2-bit 输出
    wire V;            // 有效信号输出

    // 实例化4-2优先级编码器
    test1 dut (
        .D(D),
        .Y(Y),
        .V(V)
    );
    initial 
    begin
        $display("Time\tD\tY\tV");
        $monitor("time %0t\tD=%b\tY=%b\tV=%b", $time, D, Y, V);

        // 测试用例
        D = 4'b0000; #10; // 无效输入
        D = 4'b0001; #10; // D[0] 高
        D = 4'b0010; #10; // D[1] 高
        D = 4'b0100; #10; // D[2] 高
        D = 4'b1000; #10; // D[3] 高
        D = 4'b1100; #10; // D[3] 和 D[2] 高 (优先级:D[3])
        D = 4'b1010; #10; // D[3] 和 D[1] 高 (优先级:D[3])
        D = 4'b0111;  // D[2]、D[1]、D[0] 高 (优先级:D[2])
        #10 $finish;
    end
endmodule


- 这里在display和monitor中加入了时间“time”,是 Verilog 提供的一个内置系统函数,用于返回当前仿真的时间
- “%0t”用于显示时间,将"$time"转化为可读输出,同时monitor中还可以添加文字,使得Tcl控制台的结果打印更清晰,如下图:

1.4. D锁存器latch

- 是迈向时序电路的重要环节,它用到了时间“clk”,但是仍然还是组合逻辑,在时钟为1时输出等于输入值。

module test1
(
    input D, clk,
    output reg Q
);
    always @(*)
        if(clk)
            Q = D;
endmodule
module tb_test1;
    reg D, clk;       // 4-bit 输入
    wire Q;           // 有效信号输出

    // 实例化4-2优先级编码器
    test1 dut (
        .D(D),
        .clk(clk),
        .Q(Q)
    );

    initial 
    begin
        $display("Time\tD\tclk\tQ");
        $monitor("time %0t\tD=%b\tclk=%b\tQ=%b", $time, D, clk, Q);

        // 测试用例
        D = 0; clk = 0; #10;
        D = 1; clk = 1; #10;
        D = 1; clk = 0; #10;
        D = 0; clk = 1; 
        #10 $finish;
    end
endmodule

2. 时序逻辑基础模块

2.1. D触发器DFF

- DFF正式走进时序逻辑,使用"clk"作为时钟,"rst_n"作为复位信号。

module test1 (
    input clk,         // 时钟信号
    input rst_n,       // 同步复位信号
    input D,           // D 输入
    output reg Q       // Q 输出
);
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n) 
            Q <= 1'b0;  // 复位时输出为0
        else
            Q <= D;     // 在时钟上升沿更新 Q
    end
endmodule
module tb_test1;
    reg clk, rst_n, D; // 时钟信号
    wire Q;            // Q 输出
    // 实例化 D 触发器
    test1 dut (
        .clk(clk),
        .rst_n(rst_n),
        .D(D),
        .Q(Q)
    );
    // 生成时钟信号
    initial 
    begin
        clk = 0;
        forever #5 clk = ~clk;  // 时钟周期为10时间单位
    end
    // 测试逻辑
    initial 
    begin
        $display("Time\tclk\trst_n\tD\tQ");
        $monitor("%0t\t%b\t%b\t%b\t%b", $time, clk, rst_n, D, Q);
        // 初始化信号
        rst_n = 0; D = 0; #10; // 初始复位
        rst_n = 1; D = 0; #10; // 释放复位,D=0
        D = 1; #10;             // 设置D=1,观察Q的变化
        D = 0; #10;             // 设置D=0,观察Q的变化
        rst_n = 0; #10;         // 再次复位
        rst_n = 1; D = 1; #10;  // 释放复位,D=1
        $finish;                // 结束仿真
    end
endmodule

- 输入的时钟信号仍然使用"reg"类型,在测试中会不断循环。
- 比组合逻辑更多一部分,即时钟信号生成,通过多加一个initial来实现,这里就会用到设计中不会使用的“forever”关键字,让初始clk=0,每5ns,时钟就翻转一次,实现10ns的时钟周期。
- 后面的测试逻辑不变。

2.2. 寄存器register

- 寄存器逻辑很简单,每当时钟到来时,将输入的内容存到reg中。

module test1
(
    input [3:0] D,
    input clk, rst_n,
    output reg [3:0] Q
);
    always@(posedge clk or negedge rst_n)
    begin
        if(!rst_n)
            Q <= 0;
        else
            Q <= D;
    end
endmodule
module tb_test1;
    reg clk, rst_n; // 时钟信号
    reg [3:0] D;
    wire [3:0] Q;            // Q 输出
    // 实例化 D 触发器
    test1 dut (
        .clk(clk),
        .rst_n(rst_n),
        .D(D),
        .Q(Q)
    );
    // 生成时钟信号
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 时钟周期为10时间单位
    end
    // 测试逻辑
    initial 
    begin
        $display("Time\tclk\trst_n\tD\tQ");
        $monitor("%0t\t%b\t%b\t%b\t%b", $time, clk, rst_n, D, Q);
        // 初始化信号
        rst_n = 0; D = 0; #10; // 初始复位
        rst_n = 0; D = 1; #10; // 释放复位,D=0
        rst_n = 1; D = 1; #10;             // 设置D=1,观察Q的变化
        D = 2; #10;             // 设置D=1,观察Q的变化
        D = 3; #10;             // 设置D=0,观察Q的变化
        D = 4; #10;
        D = 5; #10;
        D = 6; #10;
        D = 7; #10;
        $finish;                // 结束仿真
    end
endmodule

2.3. 移位寄存器shift register

- 考虑简单的逻辑移位寄存器,信号有clk、L(置数)、R(要置入的数)、w(单bit输入)、Q(输出)

module test1
(
    input [3:0] R,
    input L, w, clk,
    output reg [3:0] Q
);
    always@(posedge clk) 
    begin
        if(L)
            Q <= R;
        else
        begin
            Q[3] <= w;     //向右移位
            Q[2] <= Q[3];
            Q[1] <= Q[2];
            Q[0] <= Q[1];
        end
    end
endmodule
module tb_test1;
    reg [3:0] R;       // 并行加载输入
    reg L, w, clk;     // 加载控制信号,w输入,时钟
    wire [3:0] Q;      // 输出信号
    // 实例化移位寄存器
    test1 dut (
        .R(R),
        .L(L),
        .w(w),
        .clk(clk),
        .Q(Q)
    );
    // 生成时钟信号
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 时钟周期为10时间单位
    end

    // 测试逻辑
    initial begin
        $display("Time\tclk\tL\tR\tw\tQ");
        $monitor("%0t\t%b\t%b\t%b\t%b\t%b", $time, clk, L, R, w, Q);

        // 初始化信号
        R = 4'b1010; L = 0; w = 0; #10; // 初始化,移位模式
        L = 1; #10;                     // 加载模式,加载R到Q
        L = 0; w = 1; #10;             // 移位模式,w=1
        w = 0; #10;                    // 移位模式,w=0
        w = 1; #10;                    // 移位模式,w=1
        L = 1; R = 4'b1100; #10;       // 加载新值到R
        L = 0; w = 0; #10;             // 返回移位模式
        $finish;                      // 结束仿真
    end
endmodule

2.4. !!!计数器counter

- 加计数和减计数一样,所以这里写个加/减可控计数器

module test1 
(
    input clk, rst_n, up_down,
    output reg [3:0] count
);
    always@(posedge clk or negedge rst_n)
    begin
        if(!rst_n)
            count <= 0;
        else
            count <= count + (up_down ? 1 : -1);  //1时加计数,0时减计数
    end
endmodule
module tb_test1;
    reg clk, rst_n, up_down;     // 加载控制信号
    wire [3:0] count;      // 输出信号
    // 实例化移位寄存器
    test1 dut (
        .up_down(up_down),
        .rst_n(rst_n),
        .clk(clk),
        .count(count)
    );
    // 生成时钟信号
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 时钟周期为10时间单位
    end

    // 测试逻辑
    initial begin
        $display("Time\tclk\trst_n\tup_down\tcount");
        $monitor("%0t\t%b\t%b\t%b\t%b", $time, clk, rst_n, up_down, count);

        // 初始化信号
        rst_n = 0; up_down = 0; #10;  //复位
        rst_n = 1; up_down = 1; #10;  //开始加计数
        #140;                         //持续计到15
        up_down = 0; #155;            //变成减计数并计到0
        $finish;                      // 结束仿真
    end
endmodule

2.5. 计数器拓展-2位从0-59计数

- 使用1个输出从0到多大都可以,只要bit数够,但是这里是两位数,即十位个位进行技术。
- 这是常见的数字时钟模块的基础,一定要掌握。
- 往后可以拓展更多位,百位千位的计数,实现4个7段数码管显示完整时间。

module test1(
    input clk,
    input rst_n,            // 复位信号
    output reg [3:0] ones,  // 个位
    output reg [2:0] tens   // 十位 (最大值 5,3位表示)
);
    // 个位计数器
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)  // 复位时
            ones <= 4'b0000;  // 个位清零
        else if (ones == 9)  // 当个位达到 9 时
            ones <= 4'b0000;  // 重置个位为 0
        else
            ones <= ones + 1;  // 个位加 1
    end
    // 十位计数器
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)  // 复位时
            tens <= 3'b000;  // 十位清零
        else if (ones == 9 && tens < 5)  // 当个位达到 9 且十位小于 5 时
            tens <= tens + 1;  // 十位加 1
        else if (ones == 9 && tens == 5)  // 当个位达到 9 且十位达到 5 时
            tens <= 3'b000;  // 十位重置为 0
    end
endmodule
module tb_test1;
    reg clk, rst_n;  //输入
    wire [3:0] ones;  //输出个位
    wire [2:0] tens;  //输出十位
    // Instantiate实例化
    test1 dut (
        .clk(clk),
        .rst_n(rst_n),
        .ones(ones),
        .tens(tens)
    );   
    initial begin  // Clock generation (50 MHz clock, period = 20 ns)
        clk = 0;
        forever #10 clk = ~clk; // 20 ns 时钟周期d
    end
    // Test stimulus
    initial begin
        // Initialize inputs
        rst_n = 0; #30; rst_n =1; //刚开始复位保持30ns
        #1500 $finish; // 延长仿真时间到 1500 ns
    end
    // Monitor outputs
    initial begin
        $monitor("Time: %0t | rst_n: %b | ones: %b | tens: %b", 
                 $time, rst_n, ones, tens);
    end
endmodule

X. 由于篇幅太长,10000字了

!状态机FSM

- 状态机篇幅比较长,且稍微有些进阶(相比起本文,本文实在是太基础了)
- 所以放到下篇文章讲,有Moore型和Mealy型两种!

!恭喜
- 至此已经完成了Verilog基础模块的代码编写、Testbench编写以及Vivado仿真结果。
- 已经可以根据知识编写更大的模块了。


后面下一篇文章会总结常见的状态机电路,同样是最基础的部分,包括TB。
但不包括分频,异步复位同步释放、跨时钟域,FIFO等等那些啊!
机考笔试题后面也会总结!

最后再次声明,此部分为个人向,仅为自己的总结,不断充实自己的小作品集!

感谢各位!!!

--- END ---



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值