只执行一次的过程块
initial begin ... end 是 always 的兄弟。区别在于:initial 从时间 0 开始恰好执行一次,然后就结束了。它没有敏感列表,也不会循环。
它存在的理由只有一个:设置测试平台。设置初始值、开始施加激励、打开日志文件、在固定的仿真时间后调用 $finish,这些都是你希望在运行开始时按已知顺序执行一次的事情。
一步步看看仿真器做了什么:
- 时间推进到 0。
initial块开始执行。第一个$display输出。data被设为8'hA5。#10让仿真时间前进 10 个单位。- 第二个
$display输出。 $finish结束仿真。
这个块只执行了一次,没有第二遍。
标准的测试平台骨架
你将来写的几乎每个测试平台,都会用到这样一个 initial 块:
看看它的结构,这就是你每次都会用到的测试平台模板:
- 打开 VCD 转储文件(
$dumpfile、$dumpvars),在 Dumpfile 与 VCD 中讲解。 - 设置初始值并保持复位几个周期。
- 施加激励:一系列变化,中间穿插
#延迟。 $finish,干净地结束仿真。
就这些。你会看到的每个 Verilog 测试,包括厂商示例中的那些,都遵循这个模式。
多个 initial 块是并行的
同一个模块中的多个 initial 块都在时间 0 同时启动。它们各自在自己的时间片中执行自己的语句:
仿真器在时间 0 同时启动两个块。块 B 的第一个 $display 立即执行。块 A 等待 5 个单位后才输出。块 B 在时间 2 再次输出。块 A 的第二次输出发生在时间 15。块 B 的 $finish 在时间 22 结束仿真。
把设置、时钟生成和激励拆分到不同的 initial 块中是一种常见风格:每个块都很短,只做一件事。
在声明中直接初始化
一种常见的简洁写法:在声明 reg 时直接初始化。大多数仿真器对它的处理和 initial 一样:
reg clk = 0;
reg reset = 1;
reg [7:0] count = 0;
这等价于:
reg clk;
reg reset;
reg [7:0] count;
initial begin
clk = 0;
reset = 1;
count = 0;
end
测试平台中常见的是简洁写法:初始值紧挨着声明,一眼就能看到。
initial 只用于仿真
综合工具会忽略 initial 块。真实的硬件没有运行初始化代码的“零时刻”:它有上电、复位信号,以及由这些事件带来的配置。如果你需要寄存器在真实硬件中从一个已知状态开始,就在 always @(posedge clk) 中用复位信号驱动它:
always @(posedge clk) begin
if (reset) state <= IDLE;
else state <= next_state;
end
这个 if (reset) 分支就是 initial state = IDLE 的可综合等价写法。“如何在真实硬件上初始化寄存器?”答案就是复位。
有些 FPGA 流程确实接受受限形式的 initial 来设置寄存器的复位值(例如 Xilinx 的工具),但这是各厂商自己的扩展。不要在可移植的代码中依赖它。
initial 中常见的写法
除了标准的测试平台骨架,还有几种常见写法:
定时结束
initial begin
#1000 $finish; // safety net: kill the sim after 1000 time units
end
一个单独的 initial,唯一的任务就是给仿真设一个硬性的时间上限。即使你的主激励块因为等待一个永远不会到来的信号而卡死,这个块也会触发并结束运行。
设置波形
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, test); // dump everything under the `test` scope
end
这两行告诉仿真器把 test 作用域中的每个信号都写入一个 VCD 文件。没有它们,就没有波形。
初始化存储器内容
reg [7:0] memory [0:255];
initial begin
$readmemh("image.hex", memory);
end
$readmemh 把一个十六进制格式的文件加载到数组中。在 CPU 测试平台中用来预先加载指令存储器。仅用于仿真。
常见错误
用 initial 编写可综合逻辑。 它无法综合。请改用复位信号。
忘记 $finish。 没有它,仿真器会一直运行,直到被别的东西停下(默认的时间上限、手动中断等)。做个快速测试倒无所谓;在回归测试脚本中,一定要写 $finish。
激励赋值之间忘了加 #delay。 如果写成 a = 0; b = 1; 而没有延迟,两者会发生在同一个仿真时间,DUT 可能会同时看到它们,而不是把它们当作两个独立的事件。在不同的激励事件之间插入 #1 或更长的延迟。
试图在 initial 中驱动 wire。 规则和 always 一样:只有 reg 才是合法的赋值目标。
接下来
你已经见过两种过程块了。下一篇文档讲的是 Verilog 初学者最容易混淆的话题:阻塞与非阻塞赋值。知道什么时候用 =、什么时候用 <=,决定了你得到的是一个正常工作的触发器,还是一团竞争条件的乱麻。
常见问题
Verilog 中的 initial 块是什么?
initial begin ... end 是一种过程块,在仿真开始时(时间 0)恰好执行一次。它是设置测试平台状态的标准位置:初始化信号、打开日志文件、调用 $dumpfile/$dumpvars、施加激励,最后用 $finish 结束仿真。一个模块中可以同时有多个 initial 块,它们都在时间 0 并行启动。
Verilog 中 initial 和 always 有什么区别?
initial 在时间 0 执行一次,然后结束。always 会永远重复执行:它有敏感列表,只要列表中的信号发生变化就会被唤醒。initial 几乎只用在测试平台中。always 则是测试平台和可综合 RTL 的主力。
initial 块可以综合吗?
在标准 Verilog 中不行。综合工具会忽略 initial 块,因为真实的硬件没有一个运行初始化代码的“时间零点”。有些 FPGA 工具链接受一种受限的形式来设置寄存器的复位值,但一般情况下它只用于仿真。把 initial 块留在测试平台里;可综合逻辑的初始化请用复位信号。
一个 Verilog 模块中可以有多个 initial 块吗?
可以。每个 initial 块都从时间 0 开始,各自独立地执行到结束。把设置工作拆分到多个块中是测试平台的常见写法:一个块生成时钟,一个块施加激励,一个块转储波形。它们从时间 0 起并发运行,随着时间推进,仿真器会交错执行它们的语句。