Verilog 中的“时间”是什么
Verilog 没有内置的“秒”的概念。仿真器推进的是“时间单位”,也就是一个个任意的整数刻度,你的代码用 #N 来等待 N 个刻度。把这些刻度映射到真实时间的,正是 `timescale 指令。
`timescale 1ns / 1ps
module test;
initial begin
$display("t = %0t", $time); // 0
#5;
$display("t = %0t", $time); // 5 ns
#1.5;
$display("t = %0t", $time); // 6500 ps (or 6.5 ns)
$finish;
end
endmodule
两个参数:
- 单位(第一个数):
#1代表多长时间。1ns表示一个刻度是 1 纳秒。 - 精度(第二个数):仿真在这个单位内记录时间的精细程度。
1ps表示小数延迟会四舍五入到皮秒。
精度不能比单位更粗(1ps / 1ns 是非法的)。常见选择:
1ns / 1ps:事实上的标准。一切以纳秒计,任何门延迟都有亚纳秒精度。1ps / 1ps:模拟极快的电路,或者所有延迟都小于 1 纳秒时使用。1us / 1ns:用于慢速、嵌入式量级的仿真(UART 字节时间、慢速协议)。
写在哪里
`timescale 是一条编译器指令,不是模块级的语法结构。它写在文件的最顶部,在任何 module 之前:
`timescale 1ns / 1ps
module foo(...);
// ...
endmodule
它的作用范围是“按编译顺序从此处开始,直到下一条 `timescale 或编译结束”。这有个不易察觉的后果:没有显式写 `timescale 的文件,会沿用前一个编译的文件所声明的值,而这取决于编译器读取文件的顺序。避免这种意外的办法是:在每个文件顶部都写上 `timescale。
这些文档里的浏览器编辑器会为你设置一个默认的 timescale(通常是 1ns / 1ps),所以前面文档的示例没有声明也能正常运行。在真实项目中,你应该显式写出来。
实战中的 #delay
在 `timescale 1ns / 1ps 之后:
#5 // wait 5 ns
#100 // wait 100 ns
#1.5 // wait 1.5 ns (precision allows it)
#0.001 // wait 1 ps (just barely above precision)
#0 // zero-delay; useful for ordering events at the same time
在 initial 或 always 块中,#N 会让过程流程暂停 N 个时间单位。仿真器暂停的只是当前这个块(其他并发的块继续运行),延迟结束后再恢复。
你可以在赋值语句前加上延迟:
#10 a = 1; // wait 10 ns, then assign
data <= #2 new_value; // schedule the non-blocking assignment 2 ns from now
第一种写法是 testbench 的常用手法。第二种(带延迟的非阻塞赋值)用于门级仿真,模拟传播延迟。
生成时钟
经典写法:
`timescale 1ns / 1ps
module test;
reg clk = 0;
always #5 clk = ~clk;
// ...
endmodule
在 1ns / 1ps 的 timescale 下,#5 就是 5 ns。时钟每 5 ns 翻转一次,周期为 10 ns,也就是 100 MHz 的时钟。要改变频率,就改变延迟:
| 半周期 | 周期 | 频率 |
|---|---|---|
#1 | 2 ns | 500 MHz |
#2.5 | 5 ns | 200 MHz |
#5 | 10 ns | 100 MHz |
#10 | 20 ns | 50 MHz |
#25 | 50 ns | 20 MHz |
#50 | 100 ns | 10 MHz |
如果想要小数的半周期(#2.5),精度必须支持它:1ps 的精度可以精确到 0.001 ns,任何合理的频率都没问题。
多个文件混用 timescale
真实的设计由很多文件组成,它们可以声明不同的 timescale。仿真器用每个文件自己的 timescale 来解释该文件中的延迟。如果 module_a.v 写的是 `timescale 1ns / 1ps 并使用 #5,那就是 5 ns;如果 module_b.v 写的是 `timescale 1us / 1ns 并使用 #5,那就是 5 us。
这一点通常感觉不到,因为仿真器始终呈现一条全局时间轴,但它意味着两个文件中同样的 #N 可能代表天差地别的时长。解决办法:选定一种 timescale(业界标准是 1ns / 1ps),写在每个文件顶部,不要混用。
延迟不可综合
关键一点:#delay 只为仿真而存在。综合工具读到:
// In synthesizable RTL - WRONG
always @(posedge clk) begin
out <= #2 in;
end
…之后,要么忽略 #2(大多数工具),要么拒绝这种写法(更严格的 lint 工具)。真实硬件的时序由时钟和门电路的传播延迟决定,两者在源代码中都看不到。
规则是:只在 testbench 中使用 #。可综合的 RTL 中没有 # 延迟。如果你发现自己想在可综合代码里加延迟,你真正需要的是一个随时钟递减的计数器,真实硬件就是这样“等待”的。
$time 与 $realtime
读取当前仿真时间有两种方式:
$time返回一个 64 位整数,以当前 timescale 的单位计。$realtime返回一个real,同样以当前 timescale 的单位计,但保留完整精度。
在 testbench 中记录日志时,$time 几乎总是够用。只有当打印语句需要比一个刻度更细的精度时,才用 $realtime。
实用建议
- 在每个文件顶部都声明
`timescale。1ns / 1ps是稳妥的默认值。 - 只在 testbench 中使用
#delay。 把“可综合代码中没有它”当作一条固定规则。 - 让时钟周期与目标频率一致。 如果你仿真的是 50 MHz 的设计,就用 20 ns 的时钟周期:周期不一致可能掩盖对时序敏感的 bug。
- 按周期计数的激励,用
@(posedge clk)代替#。 时钟周期改变时它依然可靠。
接下来
到这里,你已经看完了这套 Verilog 教程的全部文档:从语言基础(wire 与 reg、模块、运算符),到过程块和控制流,再到同步设计和状态机,最后是证明这一切都能正常工作的 testbench 工具。是时候动手做点东西了:这些文档旁边的在线编辑器用的正是我们一路使用的同一个仿真器,随时可以运行你写出的任何模块。
常见问题
Verilog 中的 `timescale 1ns / 1ps 是什么意思?
它告诉仿真器:“这个文件里的一个时间单位是 1 纳秒,时间以皮秒精度记录。”有了这条指令,#5 等待 5 ns,#1.5 等待 1.5 ns(四舍五入到皮秒),$time 以纳秒为单位报告。第一个数是时间单位,第二个数是时间精度。
每个 Verilog 文件都需要写 `timescale 吗?
最佳做法是:需要。这条指令的作用范围到下一条 `timescale 或编译结束为止,所以没有写它的文件会沿用前一个编译的文件所声明的值,这会让时序在不同构建之间变得不确定。在每个源文件顶部写上 `timescale 1ns / 1ps(最常见的约定),就永远不会遇到意外。
Verilog 中的 #5 是什么意思?
#5 让仿真时间前进 5 个时间单位。单位来自当前生效的 `timescale 指令。在 `timescale 1ns / 1ps 下,#5 是 5 纳秒;在 `timescale 1us / 1ns 下,#5 是 5 微秒。这个数可以是小数:只要精度比单位更细,#1.5 就能用。
Verilog 中的 #delay 可以综合吗?
不可以。#delay 只影响仿真,综合工具会忽略或拒绝它。真实硬件的时序来自时钟信号和门电路的传播延迟,而不是 # 语句。在 testbench 中可以随意使用 #,但绝不要把它写进可综合的 RTL。