SV timescale 精度坑:timeunit/timeprecision 继承与延迟失真
更新时间:2026-09-28。这是验证入门长尾篇。基础测试平台结构见 SystemVerilog 测试平台基础,时钟采样与竞争消除见 clocking block 与测试平台时序同步;本篇只聚焦一个容易被忽略的角落:仿真时间单位和精度是怎么一层层继承、又怎么在不经意间把你的
#1.5ns悄悄砍成#1ns的。
本文要回答的问题
`timescale 1ns/1ps里前后两个数到底谁是单位、谁是精度?- 模块没写 timescale 时,它的时间单位从哪儿继承?顺序错了会怎样?
timeunit/timeprecision声明与`timescale宏谁优先级高?#1.5延迟被截断成#1、$time打印出整数,这些坑怎么一眼识别和根治?
一、先分清:时间单位与时间精度是两回事
SystemVerilog 的时间体系由两个独立参数构成:
- timeunit(时间单位):模块里一个裸数字
#1代表多长的物理时间。 - timeprecision(时间精度):仿真器内部时间轴的最小刻度,也叫 resolution。所有延迟值都会被舍入到这个刻度的整数倍。
经典宏写法 `timescale 1ns/1ps:斜杠前是单位(1ns),斜杠后是精度(1ps)。也就是说 #1 = 1ns,#1.5ns 可以精确表示(= 1500 个 1ps 刻度),而 #1.0005ns(0.5ps)会被舍入——超出精度的部分被截断或四舍五入(具体行为各仿真器略有差异,LRM 规定向最近精度舍入)。
关键纪律:精度永远只能比单位更细或相等。`timescale 1ps/1ns 这种「单位比精度细」的写法是非法的,大多数仿真器会直接报错或按 1ps/1ps 处理。

读这张图时抓住一条线:所有 # 延迟最终都要落到「精度刻度的整数倍」上。刻度是 1ps 时,1.5ns 能整除,安然无恙;1.0005ns 除不尽,就会被舍入。坑不在单位,在精度。
二、继承顺序:没写 timescale 的模块跟谁走
`timescale 是编译器指令,不是模块声明——它从出现的位置开始生效,影响其后编译的所有模块,直到被下一个 `timescale 覆盖。于是一个没有自己声明时间的模块,它的单位/精度取决于它在编译命令行里的位置,这是所有 timescale 事故的总根源:
| 场景 | 模块实际用到的时间设置 | 风险 |
|---|---|---|
文件顶部写了 `timescale | 自己声明的 | 无,推荐 |
| 没写,前面编译的文件有 | 继承上一个文件的 | 编译顺序一换全乱 |
| 全工程都没写 | 仿真器默认(常见 1ns/1ns) | 默认精度 1ns,亚纳秒延迟全被截断 |
模块内写 timeunit/timeprecision | 模块内声明优先 | 无,最推荐 |
第三条尤其阴:如果你给 DUT 写的 #0.1ns 建立时间检查跑在一个默认 1ns/1ns 精度的仿真里,0.1ns 会被舍入成 0——检查形同虚设,而且不报任何警告。
部分仿真器支持命令行 -timescale=1ns/1ps 作为「兜底值」,只作用于那些自己没声明的模块,这个选项值得常驻 makefile,相当于给全工程上了一道精度地板。
三、timeunit/timeprecision:模块内声明的现代化写法
SystemVerilog 在模块(或 interface、package、program、clocking 块)内部引入了一对声明:
module my_monitor;
timeunit 1ns; // 本模块 #1 = 1ns
timeprecision 1ps; // 本模块精度 1ps
task automatic sample();
#1.5; // 精确 1.5ns,不依赖任何外部宏
...
endtask
endmodule优先级规则一句话记住:模块内显式声明 > `timescale 宏 > 命令行兜底 > 仿真器默认。timeunit 声明跟着模块走,与文件编译顺序无关,天然免疫第二节说的继承事故。新写的验证组件(monitor、coverage、checker)一律用这对声明,把宏留给不得已的老代码。
两个细节容易踩:
timeunit 1ns;与timeunit 1ns/1ps;都合法,后者把单位精度一句话写完,等价于分两句。- 声明必须出现在模块内任何其他声明之前(除了 package import),写晚了编译器直接报语法错误——这反倒是好事,逼你把时间声明放在最显眼的位置。
四、真实坑例:延迟被截断与打印失真
坑一:#1.5 变成 #1
老 TB 里一段代码 `timescale 1ns/1ns,后人加了个 #0.5; 想做半个纳秒的偏移。精度是 1ns,0.5ns 四舍五入成 1ns 或截断成 0(依仿真器),波形上看偏移要么消失要么翻倍,而且这种错误 lint 不查、编译不报。判据:凡是 # 后面带小数点的延迟,先问当前精度容不容得下它。
坑二:$time 打印出「错误的整数」
$time 返回的是按 timeunit 取整后的 64 位整数,不是真实时间。单位 1ns、当前时刻 1.6ns 时,$display("%0t", $time) 打出来的是 2(四舍五入)。要拿真实小数时间,用 $realtime(返回 real,单位仍是 timeunit):
$display("int time = %0t", $time); // 2 ← 被取整
$display("real time = %0t", $realtime); // 1.6 ← 真实值scoreboard 里用 $time 计算两事件的间隔做时序检查,差值小于 1 个单位时永远得 0,检查直接失效——这是 $time 混用最典型的翻车姿势。凡是涉及亚单位间隔的比较,一律 $realtime。
坑三:DUT 与 TB 精度不一致
DUT 是老 RTL,1ns/1ns;TB 是新写的,1ns/1ps。仿真器会取全工程最细精度做全局时间轴,各模块再各自换算。功能通常没错,但性能直接跳水——时间轴细了 1000 倍,事件调度开销陡增。统一精度的方法见下一节的对照图。

这张图把继承链画成了一根「宏接力棒」:B、D 的命运攥在前一个被编译的文件手里,A、C 一旦换序或被拆分编译,B、D 的时间尺度跟着漂移;只有 E 自带声明、岿然不动。工程纪律就是从这张图直接推出来的三条。
五、根治纪律与排错清单
| 现象 | 根因 | 处理 |
|---|---|---|
| 小数延迟波形上消失/变整 | 精度太粗,延迟被舍入 | 提高 timeprecision;用 timeunit 1ns/1ps; |
$time 打印全是整数、间隔为 0 | $time 按单位取整 | 改用 $realtime |
| 换编译顺序后时序全乱 | 模块靠宏继承,顺序敏感 | 每个文件自带声明,杜绝裸模块 |
| 亚纳秒检查从不触发 | 默认精度 1ns,检查值被截成 0 | 命令行加 -timescale=1ns/1ps 兜底 |
| 仿真速度莫名慢一个量级 | 全工程被最细精度拖慢 | 统一各模块精度,别让 1ps 传染给不需要的模块 |
| package 里的函数延时不定 | package 无 timeunit,跟编译顺序走 | package 内同样写 timeunit/timeprecision |
这一篇在知识体系里的位置
- 上游基础:SystemVerilog 测试平台基础 搭建平台骨架,本篇补它没展开的时间语义。
- 时序采样:clocking block 与测试平台时序同步 解决沿上采样竞争,与本篇的时间精度是同一枚硬币的两面。
- 门级延伸:门级仿真、SDF 反标与 X 传播 里 SDF 反标的亚纳秒延迟对 timescale 精度最敏感,读完本篇再看那一篇会更顺。
总结
SV timescale 精度坑的本质是「单位决定 #1 多长、精度决定延迟能切多细,两者沿编译单元向后继承」:裸模块跟谁编译跟谁走,#0.5 被精度截断、$time 被单位取整、亚纳秒检查被默认值清零都不报警;根治只有三条纪律——每个文件自带时间声明、新代码用模块内 timeunit/timeprecision、命令行 -timescale 做兜底,再把 $realtime 替掉所有亚单位间隔的 $time 比较。