9.1 过程连续赋值(Procedural Continuous Assignment)
一、什么是过程连续赋值?
前面学过的过程赋值 (在always/initial块中用=或<=)是"一次性"的------执行到这条语句时赋值一次,之后变量保持该值直到下一次被赋值。
过程连续赋值 则不同:它允许在一段时间内 ,将表达式的值持续地 驱动到寄存器或线网上。它会改写(覆盖) 该变量上原有的赋值,直到被显式取消。
核心区别:
| 对比维度 | 普通过程赋值 | 过程连续赋值 |
|---|---|---|
| 执行方式 | 执行到该语句时赋值一次 | 持续驱动,直到被取消 |
| 能否被覆盖 | 可以被后续赋值覆盖 | 覆盖其他赋值,直到被取消 |
| 适用场景 | 常规逻辑描述 | 异步复位、调试强制赋值 |
注意 :过程连续赋值只能在 initial 或 always 块内部使用 ,不能像 assign 那样写在模块顶层。
二、assign 和 deassign(第一类)
assign 和 deassign 用于改写寄存器(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 触发器,带有互补输出
q和qbar,以及数据输入d、时钟clk和复位信号reset。 -
因为
q和qbar都在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,优缺点分析
优点:
-
语法巧妙,逻辑清晰 :利用
assign/deassign,完美地实现了"复位优先级高于时钟沿"的逻辑。这在早期的 Verilog 仿真模型(尤其是总线模型)中很常见。 -
仿真速度快:这种写法在仿真器中能非常直接地控制变量的值。
缺点(极其致命):
-
完全不可综合 :FPGA 和 ASIC 综合工具(如 Vivado, Quartus)不支持
assign和deassign语句。 如果你试图用这段代码去生成实际电路,综合工具会直接报错。 -
不符合现代 RTL 规范 :现代硬件设计(RTL)严格要求时序逻辑中只能使用
posedge clk或negedge clk作为触发条件,复位信号(posedge reset或negedge reset)也必须写在同一个always块的敏感列表中。 -
容易引发仿真与综合不一致:由于上述语法不可综合,在仿真器里跑得很完美,但实际做出来芯片后发现功能完全不同。
重要提示 :assign/deassign 结构现在被认为是一种糟糕的编码风格 ,建议在 Verilog 代码中不要使用 这种形式的赋值语句。现代推荐的做法是使用标准的 always @(posedge reset or negedge clk) 写法(如第7章所学)。
三、force 和 release(第二类)
force 和 release 是更强大的版本------它们既可以改写寄存器,也可以改写线网(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 设计中,不要使用这些语句,它们会让代码行为变得难以预测
在实际工程中,这通常用于以下场景:
-
故障注入(Fault Injection) :当你需要测试一个系统在"突然收到异常数据"时的反应时,可以用
force强行制造一个异常状态。 -
硬件调试(Debugging) :当仿真跑飞了,你想看看如果硬性改变某个寄存器状态,系统会怎么走,就可以暂停仿真(利用
$stop),然后用force修改信号,继续仿真。 -
绕过复杂的初始化过程 :有些 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 提供了 ifdef、ifndef、else、elsif、endif 等编译指令。
基本语法:
`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 开始存放后面的数据 -
数据用空格分隔
-
可以包含
x和z -
未初始化的地址默认值为
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 表示无限层级)