Verilog HDL 学习笔记(九)| 第 9 章 实用建模技术

9.1 过程连续赋值(Procedural Continuous Assignment)

一、什么是过程连续赋值?

前面学过的过程赋值 (在always/initial块中用=<=)是"一次性"的------执行到这条语句时赋值一次,之后变量保持该值直到下一次被赋值。

过程连续赋值 则不同:它允许在一段时间内 ,将表达式的值持续地 驱动到寄存器或线网上。它会改写(覆盖) 该变量上原有的赋值,直到被显式取消。

核心区别:

对比维度 普通过程赋值 过程连续赋值
执行方式 执行到该语句时赋值一次 持续驱动,直到被取消
能否被覆盖 可以被后续赋值覆盖 覆盖其他赋值,直到被取消
适用场景 常规逻辑描述 异步复位、调试强制赋值

注意 :过程连续赋值只能在 initialalways 块内部使用 ,不能像 assign 那样写在模块顶层。

二、assigndeassign(第一类)

assigndeassign 用于改写寄存器(reg) 上的赋值。

语法:

复制代码
assign <寄存器> = <表达式>;
deassign <寄存器>;

示例:带异步复位的D触发器

复制代码
module edge_dff(q, qbar, d, clk, reset);
    output q, qbar;
    input d, clk, reset;
    reg q, qbar;

    // 时钟下降沿触发:正常传输数据
    always @(negedge clk) begin
        q = d;
        qbar = ~d;
    end

    // reset 变化时:用过程连续赋值强制覆盖 q 和 qbar
    always @(reset) begin
        if (reset) begin
            assign q = 1'b0;      // 强制 q = 0
            assign qbar = 1'b1;   // 强制 qbar = 1
        end else begin
            deassign q;           // 取消强制,让正常赋值生效
            deassign qbar;
        end
    end
endmodule

解读:

1. 模块声明与端口
复制代码
module edge_dff(q, qbar, d, clk, reset);
    output q, qbar;
    input d, clk, reset;
    reg q, qbar;
  • 定义了一个 D 触发器,带有互补输出 qqbar,以及数据输入 d、时钟 clk 和复位信号 reset

  • 因为 qqbar 都在 always 块中被赋值,所以它们被声明为 reg 类型。

2. 正常的数据传输(时钟沿触发)
复制代码
    // 时钟下降沿触发:正常传输数据
    always @(negedge clk) begin
        q = d;
        qbar = ~d;
    end
  • 这是一个时序逻辑块 。当 clk 出现下降沿(negedge)时,它会将输入 d 的值赋给输出 q,并将 d 的反码赋给 qbar

  • 注意这里的赋值符号 = :在时序逻辑中通常应该使用非阻塞赋值 <=,但这里使用了阻塞赋值 =。在单触发器且无多条赋值冲突的情况下,仿真结果通常也是正确的,但这属于不规范写法,容易引发竞争冒险。

3. 复位逻辑(过程连续赋值)
复制代码
    // reset 变化时:用过程连续赋值强制覆盖 q 和 qbar
    always @(reset) begin
        if (reset) begin
            assign q = 1'b0;      // 强制 q = 0
            assign qbar = 1'b1;   // 强制 qbar = 1
        end else begin
            deassign q;           // 取消强制,让正常赋值生效
            deassign qbar;
        end
    end

这是本段代码最核心的讲解点。

  • assign q = 1'b0; :这里的 assign 不是模块外的连续赋值,而是位于 always 块内部的过程连续赋值(Procedural Continuous Assignment) 。它的作用是强制锁定 q 的值。只要这个 assign 生效,无论时钟怎么跳变,q 都永远是 0,直到被 deassign 解除。

  • deassign q; :当 reset 变为 0 时,解除对 q 的强制锁定。解除后,q 的值就恢复到由第一个 always 块(negedge clk)来正常控制。

  • 优先级 :在同一时刻,assign 的优先级高于普通的 always 赋值,所以只要 reset 为高,时钟沿就会被完全屏蔽。

4,优缺点分析

优点:

  1. 语法巧妙,逻辑清晰 :利用 assign/deassign,完美地实现了"复位优先级高于时钟沿"的逻辑。这在早期的 Verilog 仿真模型(尤其是总线模型)中很常见。

  2. 仿真速度快:这种写法在仿真器中能非常直接地控制变量的值。

缺点(极其致命):

  1. 完全不可综合FPGA 和 ASIC 综合工具(如 Vivado, Quartus)不支持 assigndeassign 语句。 如果你试图用这段代码去生成实际电路,综合工具会直接报错。

  2. 不符合现代 RTL 规范 :现代硬件设计(RTL)严格要求时序逻辑中只能使用 posedge clknegedge clk 作为触发条件,复位信号(posedge resetnegedge reset)也必须写在同一个 always 块的敏感列表中。

  3. 容易引发仿真与综合不一致:由于上述语法不可综合,在仿真器里跑得很完美,但实际做出来芯片后发现功能完全不同。

重要提示assign/deassign 结构现在被认为是一种糟糕的编码风格 ,建议在 Verilog 代码中不要使用 这种形式的赋值语句。现代推荐的做法是使用标准的 always @(posedge reset or negedge clk) 写法(如第7章所学)。

三、forcerelease(第二类)

forcerelease 是更强大的版本------它们既可以改写寄存器,也可以改写线网(wire)

典型应用场景:交互式调试

  • 在仿真过程中,强制某些信号为特定值,观察对其他信号的影响

  • 建议只用在 Testbench 或调试语句中,不要在设计模块内部使用

语法:

复制代码
force <信号> = <值>;
release <信号>;

示例:在 Testbench 中强制信号

复制代码
module stimulus;
    reg clk, reset;
    wire [3:0] q;

    counter uut(q, clk, reset);

    initial begin
        // 正常仿真
        clk = 0;
        reset = 1;
        #10 reset = 0;
        #100;

        // 调试:强制 q 为特定值
        force uut.q = 4'b1010;    // 强制计数器的输出为 1010
        #20;
        release uut.q;            // 释放强制,恢复正常的计数器输出

        #100 $finish;
    end
endmodule

force 在寄存器和线网上的行为差异:

被 force 的对象 行为
寄存器(reg) 释放后,变量保持被强制时的值,直到下一次被正常赋值改变
线网(wire) 释放后,立即恢复为驱动源(如 assign 语句)所确定的值

核心提醒force/release 主要用于调试 。正常的 RTL 设计中,不要使用这些语句,它们会让代码行为变得难以预测

在实际工程中,这通常用于以下场景:

  1. 故障注入(Fault Injection) :当你需要测试一个系统在"突然收到异常数据"时的反应时,可以用 force 强行制造一个异常状态。

  2. 硬件调试(Debugging) :当仿真跑飞了,你想看看如果硬性改变某个寄存器状态,系统会怎么走,就可以暂停仿真(利用 $stop),然后用 force 修改信号,继续仿真。

  3. 绕过复杂的初始化过程 :有些 IP 核初始化需要成千上万个周期,为了快速测试后面的逻辑,直接 force 到一个就绪状态。

9.2 改写(覆盖)参数(Overriding Parameters)

一、为什么要改写参数?

在模块中使用 parameter 定义常量(如数据位宽、延迟值),可以让模块更通用。但不同的使用场景可能需要不同的参数值------比如同一个 FIFO 模块,在项目A中需要深度 16,在项目B中需要深度 32。

Verilog 提供了两种方法在实例化时改写参数值

二、方法一:用 defparam 语句改写(不推荐)

defparam 通过层次路径名直接修改低层模块的参数。

复制代码
// ---- 定义带参数的模块 ----
module hello_world;
    parameter id_num = 0;
    initial
        $display("Displaying hello_world id number = %d", id_num);
endmodule

// ---- 顶层模块用 defparam 改写参数 ----
module top;
    // 实例化两个 hello_world 模块
    hello_world w1();
    hello_world w2();

    // 通过层次路径名改写参数
    defparam w1.id_num = 1;
    defparam w2.id_num = 2;
endmodule

仿真输出:

复制代码
Displaying hello_world id number = 1
Displaying hello_world id number = 2

带多个参数时的写法:

复制代码
module bus_master;
    parameter delay1 = 2;
    parameter delay2 = 3;
    parameter delay3 = 7;
endmodule

module top;
    bus_master b1();
    bus_master b2();

    // 一行改写多个参数
    defparam b1.delay1 = 4, b1.delay2 = 5, b1.delay3 = 6;
    defparam b2.delay1 = 9, b2.delay2 = 4;  // delay3 保持默认值 7
endmodule

重要警告defparam 结构现在被认为是一种糟糕的编码风格 ,建议在 Verilog 代码中使用替代形式 。而且 defparam 在大多数综合工具中不可综合,仅用于仿真。

三、方法二:实例化时传递参数(推荐!)

在实例化模块时,用 #(参数值列表) 直接传递新参数。

按顺序传递:

复制代码
module bus_master;
    parameter delay1 = 2;
    parameter delay2 = 3;
    parameter delay3 = 7;
endmodule

module top;
    // 按参数声明顺序传递:delay1=4, delay2=5, delay3=6
    bus_master #(4, 5, 6) b1();

    // 只传递前两个,第三个保持默认值 7
    bus_master #(9, 4) b2();
endmodule

按名字传递(推荐!更清晰):

复制代码
module top;
    // 按名字传递:只改 delay2 和 delay3,delay1 保持默认
    bus_master #(.delay2(4), .delay3(7)) b3();
endmodule

建议:按名字传递参数是最佳实践,因为:

  • 顺序无关,不容易出错

  • 可以只改写部分参数,其他保持默认

  • 代码可读性更强,一看就知道改的是哪个参数

ANSI C 风格的参数声明(更简洁):

复制代码
// 在模块声明时直接指定参数
module hello_world #(parameter id_num = 0) (
    input clk,
    output reg [7:0] data
);
    initial
        $display("Displaying hello_world id number = %d", id_num);
endmodule

// 实例化时传递参数
module top;
    hello_world #(.id_num(1)) w1();
    hello_world #(.id_num(2)) w2();
endmodule

9.3 条件编译和执行(Conditional Compilation and Execution)

一、为什么需要条件编译?

在工程实践中,一套代码往往需要在不同环境下使用:

  • 仿真环境:需要详细的调试信息输出

  • 综合环境:不需要调试信息,要尽可能精简

  • 不同芯片平台:可能需要不同的实现方式

如果每次都手动注释/取消注释代码,不仅麻烦还容易出错。条件编译就是为了解决这个问题。

二、条件编译指令

Verilog 提供了 ifdefifndefelseelsifendif 等编译指令。

基本语法:

复制代码
`ifdef <宏名>
    // 如果宏已定义,编译这部分代码
`elsif <宏名2>
    // 如果宏2已定义,编译这部分代码
`else
    // 如果以上宏都未定义,编译这部分代码
`endif

示例1:选择不同的运算模块

复制代码
// ---- 在文件开头选择要使用的运算 ----
`define OP_AND        // 使用与运算
// `define OP_OR      // 注释掉,不使用或运算
// `define OP_XOR     // 注释掉,不使用异或运算

module op_test(
    input [3:0] in1,
    input [3:0] in2,
    output [3:0] out
);

    `ifdef OP_AND
        assign out = in1 & in2;      // 编译与运算
    `endif

    `ifdef OP_OR
        assign out = in1 | in2;      // 不编译(OP_OR未定义)
    `endif

    `ifdef OP_XOR
        assign out = in1 ^ in2;      // 不编译(OP_XOR未定义)
    `endif

endmodule

综合效果 :只会综合出与运算电路,或运算和异或运算的电路根本不会生成,从而节省芯片面积

示例2:仿真 vs 综合的不同配置

复制代码
module my_design #(parameter DEPTH = 256) (
    input clk,
    input reset,
    input [7:0] data_in,
    output reg [7:0] data_out
);

    `ifdef SIMULATION
        // 仿真模式:使用行为模型(仿真速度快)
        always @(posedge clk) begin
            if (reset)
                data_out <= 8'b0;
            else
                data_out <= data_in;
        end
    `else
        // 综合模式:使用门级模型(精确但慢)
        wire [7:0] q;
        DFF dff_inst[7:0] (.q(q), .d(data_in), .clk(clk), .reset(reset));
        assign data_out = q;
    `endif

endmodule

三、ifndef------检查宏是否定义

复制代码
module top;

    `ifndef IGNORE_B5
        bus_master b5();    // 如果 IGNORE_B5 未定义,则实例化 b5
    `endif

    `ifdef ADD_B2
        bus_master b2();    // 如果 ADD_B2 已定义,则实例化 b2
    `elsif ADD_B3
        bus_master b3();    // 如果 ADD_B3 已定义,则实例化 b3
    `else
        bus_master b4();    // 默认情况
    `endif

endmodule

四、条件执行($test$plusargs

条件编译在编译时 决定哪些代码被包含,而条件执行在仿真运行时决定哪些代码被执行。

复制代码
module test;

    initial begin
        // 检查运行时是否传入了 "DISPLAY_VAR" 参数
        if ($test$plusargs("DISPLAY_VAR"))
            $display("Display = %b", {a, b, c});
        else
            $display("No Display");
    end

endmodule

运行方式:

复制代码
# 带参数运行,显示变量
> vsim test +DISPLAY_VAR

# 不带参数运行,不显示
> vsim test

$value$plusargs------获取参数的值:

复制代码
module test;
    reg [8*128-1:0] test_string;
    integer clk_period;

    initial begin
        // 从命令行获取测试文件名
        if ($value$plusargs("testname=%s", test_string))
            $readmemh(test_string, vectors);   // 读取指定的测试向量文件
        else
            $display("Test name option not specified");

        // 从命令行获取时钟周期
        if ($value$plusargs("clk_t=%d", clk_period))
            forever #(clk_period/2) clk = ~clk;
        else
            $display("Clock period not specified");
    end
endmodule

运行方式:

复制代码
> vsim test +testname=test1.vec +clk_t=10

9.4 时间尺度(timescale

一、为什么需要 timescale

不同模块可能需要不同的时间单位:

  • 一个模块的延迟是 1μs

  • 另一个模块的延迟是 100ns

如果都用同一个时间单位,要么写很大的数字(100ns 写成 0.1μs),要么精度不够。timescale 让每个模块可以有自己的时间基准。

二、基本语法

复制代码
`timescale <时间单位> / <时间精度>

合法值: 时间单位和时间精度必须是 1、10 或 100 的倍数。

合法值 含义
1ns 1纳秒
10ns 10纳秒
100ns 100纳秒
1us 1微秒
10us 10微秒
100us 100微秒
1ms 1毫秒
10ms 10毫秒
100ms 100毫秒
1s 1秒

三、示例对比

复制代码
// ---- 模块1:时间单位 100ns,精度 1ns ----
`timescale 100ns / 1ns
module dummy1;
    reg toggle;
    initial toggle = 1'b0;
    // 这里的 5 个时间单位 = 500ns = 0.5μs
    always #5 begin
        toggle = ~toggle;
        $display("%0t, In module dummy1 toggle = %b", $time, toggle);
    end
endmodule

// ---- 模块2:时间单位 1μs,精度 10ns ----
`timescale 1us / 10ns
module dummy2;
    reg toggle;
    initial toggle = 1'b0;
    // 这里的 5 个时间单位 = 5μs = 5000ns
    always #5 begin
        toggle = ~toggle;
        $display("%0t, In module dummy2 toggle = %b", $time, toggle);
    end
endmodule

仿真输出分析:

  • dummy1 每 500ns 翻转一次

  • dummy2 每 5μs 翻转一次

  • dummy1 翻转 10 次,dummy2 才翻转 1 次

💡 $time 的单位$time 返回的时间值是以调用该任务的模块的参考时间单位 为准的。不同模块的 $time 返回值单位不同

9.5 常用的系统任务

一、文件输出($fopen$fdisplay$fmonitor$fclose

1. 打开文件($fopen
复制代码
integer handle1, handle2, handle3;

initial begin
    handle1 = $fopen("file1.out");   // 返回 32'h0000_0002(第1位置1)
    handle2 = $fopen("file2.out");   // 返回 32'h0000_0004(第2位置1)
    handle3 = $fopen("file3.out");   // 返回 32'h0000_0008(第3位置1)
end

多通道描述符$fopen 返回一个 32 位的值,每一位代表一个文件通道。标准输出(通道0)的位0始终为1。

2. 写入文件($fdisplay$fmonitor
复制代码
integer desc1, desc2, desc3;

initial begin
    // 写入文件 file1.out 和标准输出
    desc1 = handle1 | 1;    // 位0 和 位1 都置1
    $fdisplay(desc1, "Display 1");

    // 写入文件 file1.out 和 file2.out
    desc2 = handle1 | handle2;
    $fdisplay(desc2, "Display 2");

    // 只写入文件 file3.out
    desc3 = handle3;
    $fdisplay(desc3, "Display 3");
end

文件输出任务对照表:

屏幕输出任务 文件输出任务 功能
$display $fdisplay 打印并换行
$write $fwrite 打印不换行
$monitor $fmonitor 持续监视并打印
$strobe $fstrobe 选通打印(同一时刻最后执行)
3. 关闭文件($fclose
复制代码
$fclose(handle1);   // 关闭文件,不能再写入

二、显示层次(%m

$display 等任务中使用 %m 格式符,可以显示当前所在的完整层次路径

复制代码
module M;
    initial
        $display("Displaying in %m");
endmodule

module top;
    M m1();
    M m2();
    M m3();
endmodule

仿真输出:

复制代码
Displaying in top.m1
Displaying in top.m2
Displaying in top.m3

%m 可以显示模块实例、任务、函数和命名块的完整层次路径,在调试多个实例时非常有用。

三、选通显示($strobe

$strobe$display 非常相似,唯一的区别是:$strobe 在同一时刻的所有其他赋值语句执行完成后才执行

复制代码
always @(posedge clock) begin
    a = b;
    c = d;
end

// 使用 $strobe:在 a 和 c 更新完成后才打印
always @(posedge clock)
    $strobe("Displaying a = %b, c = %b", a, c);

// 如果使用 $display:可能在 a 和 c 更新之前就打印,显示旧值

何时使用 :当需要在时钟边沿之后显示更新后的 信号值时,用 $strobe 而不是 $display

四、随机数生成($random

$random 是 Verilog 提供的随机数生成系统任务,返回一个 32 位有符号整数

基本用法:

复制代码
integer r_seed;
reg [31:0] addr;

initial begin
    // 不指定种子:每次仿真产生不同的随机序列
    addr = $random;

    // 指定种子:每次仿真产生相同的随机序列(便于调试)
    addr = $random(r_seed);
end

生成指定范围的随机数:

复制代码
reg [23:0] rand1, rand2;

// 生成 -59 到 +59 的随机数(有符号)
rand1 = $random % 60;

// 生成 0 到 59 的随机数(无符号,用拼接强制为正)
rand2 = {$random} % 60;

五、用数据文件初始化存储器($readmemb / $readmemh

$readmemb$readmemh 用于从外部文件读取数据来初始化存储器。

语法:

复制代码
$readmemb("<文件名>", <存储器名>);
$readmemb("<文件名>", <存储器名>, <起始地址>);
$readmemb("<文件名>", <存储器名>, <起始地址>, <结束地址>);
// $readmemh 的语法完全相同,只是读取十六进制数据

示例:

复制代码
module test;
    reg [7:0] memory[0:7];    // 8 个 8 位存储单元
    integer i;

    initial begin
        // 从文件 init.dat 读取数据初始化存储器
        $readmemb("init.dat", memory);

        // 显示初始化后的内容
        for (i = 0; i < 8; i = i + 1)
            $display("Memory [%0d] = %b", i, memory[i]);
    end
endmodule

数据文件 init.dat 格式:

复制代码
@002 11111111 01010101 00000000 10101010
@006 1111zzzz 00001111
  • @002 表示从地址 2 开始存放后面的数据

  • 数据用空格分隔

  • 可以包含 xz

  • 未初始化的地址默认值为 x

仿真输出:

复制代码
Memory [0] = xxxxxxxx
Memory [1] = xxxxxxxx
Memory [2] = 11111111
Memory [3] = 01010101
Memory [4] = 00000000
Memory [5] = 10101010
Memory [6] = 1111zzzz
Memory [7] = 00001111

六、值变转储文件(VCD)

VCD(Value Change Dump)文件记录了仿真过程中信号值的变化,可以用后处理工具查看波形。

主要系统任务:

任务 功能
$dumpfile("文件名") 指定 VCD 输出文件名
$dumpvars 选择要转储的信号
$dumpon 开始转储
$dumpoff 停止转储
$dumpall 生成检查点,转储所有当前值

示例:

复制代码
initial begin
    $dumpfile("myfile.dmp");              // 指定输出文件
    $dumpvars(0, top);                    // 转储 top 模块及其下所有层级的信号
    #100000 $dumpoff;                     // 100000 时间单位后停止转储
end

$dumpvars 的层级控制:

复制代码
$dumpvars;                // 转储设计中的所有信号
$dumpvars(1, top);        // 只转储 top 模块第一层的信号(不含子模块内部)
$dumpvars(2, top.m1);     // 转储 top.m1 模块以下两层的信号
$dumpvars(0, top.m1);     // 转储 top.m1 模块以下所有层的信号(0 表示无限层级)
相关推荐
凯尔萨厮18 分钟前
Java学习笔记十三(Stream与Lambda)
笔记·学习
爱三盖浇饭1 小时前
挑战从零开始学习Kafka2: Kafka 常见问题解决方案
学习·kafka
dtq04242 小时前
数据结构 - 线性表 - 双向链表
c语言·数据结构·学习
M78佐菲8 小时前
Linux学习笔记:TCP协议
linux·笔记·学习·tcp/ip·算法
噜~噜~噜~9 小时前
操作系统笔记-2.3.5.1.1生产者-消费者问题
笔记·操作系统
yi.Ist9 小时前
数据定义语言-DDL操作
数据库·学习·mysql·oracle·大海豚
骐骥113 小时前
学习:javaScriptProxy和registerJavaScriptProxy有什么区别,能注册多少个对象
学习·鸿蒙
Huathy-雨落江南,浮生若梦13 小时前
Caddy 学习笔记 - 反向代理与 IP 封禁
笔记·学习·tcp/ip
谢白羽14 小时前
SGLang的AWQ量化笔记
笔记·python·sglang