Menu

Verilog initial 块:在仿真开始时运行初始化代码

initial 块和 always 有什么不同,为什么它只存在于仿真中,以及它的常见用法:施加激励、设置波形、打印日志表头。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

只执行一次的过程块

initial begin ... end 是 always 的兄弟。区别在于:initial 从时间 0 开始恰好执行一次,然后就结束了。它没有敏感列表,也不会循环。

它存在的理由只有一个:设置测试平台。设置初始值、开始施加激励、打开日志文件、在固定的仿真时间后调用 $finish,这些都是你希望在运行开始时按已知顺序执行一次的事情。

一步步看看仿真器做了什么:

  1. 时间推进到 0。
  2. initial 块开始执行。第一个 $display 输出。
  3. data 被设为 8'hA5。
  4. #10 让仿真时间前进 10 个单位。
  5. 第二个 $display 输出。
  6. $finish 结束仿真。

这个块只执行了一次,没有第二遍。

标准的测试平台骨架

你将来写的几乎每个测试平台,都会用到这样一个 initial 块:

看看它的结构,这就是你每次都会用到的测试平台模板:

  1. 打开 VCD 转储文件($dumpfile、$dumpvars),在 Dumpfile 与 VCD 中讲解。
  2. 设置初始值并保持复位几个周期。
  3. 施加激励:一系列变化,中间穿插 # 延迟。
  4. $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 起并发运行,随着时间推进,仿真器会交错执行它们的语句。

Coddy 编程语言插图

和 Coddy 一起学编程

开始