XSim 断言失败:$fatal 仿真提前终止的 Fatal 行定位与边界修复
更新时间:2026-11-10。本篇属于 FPGA 入门排错线,讲 XSim 运行阶段 testbench 自检任务
$fatal触发后,仿真在预期时间之前提前收官的一类问题。它和 XSim 找不到设计单元(XSIM 43-3225) 是同一命令行三连流程上不同阶段的故障:43-3225 断在 xelab,连快照都建不出来;本篇的场景里 xvlog、xelab 全部通过,快照正常加载,是仿真跑起来之后被自己写的检查器叫停的。
首屏答案:日志里出现 Fatal: ... 并紧跟一行 $finish called at time : 115 ns,意思是 testbench 里的某个 $fatal 系统任务被执行,XSim 当场终止仿真——这不是工具崩溃,而是你写的检查逻辑主动判定「设计行为不符合预期」。本例的真相却相反:DUT 是 4 位使能计数器,合法取值天然在 0..15 之间回绕,检查器却把合法上界错写成 9,计数到 10 的那一拍触发 $fatal(1, "bound check failed: cnt=%0d exceeded 9", cnt)。把断言边界改成 4 位总线的真实范围 cnt > 15 后,断言不再触发,仿真跑到 212 ns 由 testbench 的 $finish 正常收尾。
本文要回答的问题
- 日志里的
Fatal:行是谁打印的,为什么仿真没跑完就退出了? $fatal、$error、$warning三个系统任务在 XSim 里的输出形态和退出行为有什么差别?- Fatal 行下面的 Time、Process、File 三要素分别怎么读,怎样靠它们反推出 testbench 的出错位置?
- 计数器在 10 就被叫停,为什么说错的是断言而不是 DUT?断言边界应该怎么对位宽?
- 为什么同样的工程首跑可能连断言都跑不到,先被 xelab 的 timescale 报错拦下?
先给一张分诊漏斗,本篇后面的章节就是沿着它逐层展开:从「仿真提前结束」这个表面症状,一路收敛到「断言上界与总线位宽不匹配」这一个根因。
读这张漏斗抓住一个方向:仿真提前终止只是症状,Fatal 消息里的数值才是入口,真正要动手的是最底层那条「边界与位宽不匹配」。如果看到 cnt=10 就回头去给计数器加限幅逻辑,等于把正确的 DUT 改成配合错误断言的样子,方向恰好反了。
一、现象:115 ns 的 Fatal 让仿真提前收官
实验 DUT 是一个带异步复位的 4 位使能计数器,输出 cnt 声明为 reg [3:0],使能有效时每个时钟沿加一,计到 15 后下一拍自然回绕到 0,这是 4 位总线的硬件本性。testbench 在时钟沿里挂了一个边界检查器,写法如下:
// tb_assert_bad.v:问题检查器
always @(posedge clk) begin
if (rst_n && cnt >= 4'd10)
$fatal(1, "bound check failed: cnt=%0d exceeded 9", cnt);
end
initial begin
#12 rst_n = 1;
#200 $finish;
end检查器的意图是「计数不许超过 9」,但这个上界是硬编码拍脑袋写的,和 DUT 的 4 位位宽没有任何推导关系。按 XSim 命令行三连 跑完 xvlog、xelab,xsim 加载快照执行 run all,仿真没有语法错误、没有细化错误,却在中途停下:
读这段输出抓三行。第 39 行 Fatal: bound check failed: cnt=10 exceeded 9 是 $fatal 的格式化消息原样输出,触发值 10 和消息文本都来自 testbench 第 18 行那行代码本身。第 40 行是 XSim 自动追加的定位三要素:时间 115 ns、进程名 /tb_assert_bad/Always16_2、源文件 tb_assert_bad.v。第 41 行 $finish called at time : 115 ns 说明仿真进程是被 finish 类动作正常结束的,而不是崩溃退出——区别在于这个 finish 是 $fatal 替你调的,testbench initial 块里计划在 212 ns 执行的 $finish 根本没有机会运行。
为什么恰好是 115 ns?时钟周期 10 ns,复位在 12 ns 释放,之后每个上升沿计数加一:15 ns 变为 1、25 ns 变为 2,依此类推,105 ns 那拍 cnt 经非阻塞赋值更新为 10,但同一沿上检查器读到的还是更新前的旧值 9;到下一个上升沿 115 ns,检查器读到稳定的 cnt=10,条件 cnt >= 10 成立,$fatal 触发。时间点与日志逐拍对得上,这也是验证 Fatal 行可信度的第一步。
二、机理:$fatal、$error、$warning 在 XSim 里的差别
Verilog-2005 起提供了四个严重性递增的报告系统任务:$info、$warning、$error、$fatal。它们共用同一套格式化字符串语法,差别只在输出前缀、是否计数以及是否终止仿真。在 XSim 2017.4 下的实际行为整理如下:
| 系统任务 | 输出前缀 | 是否计入 error 统计 | 是否终止仿真 | 典型用途 |
|---|---|---|---|---|
$warning(...) | Warning: | 否 | 不终止,继续跑 | 提示可疑但允许的情形 |
$error(...) | ERROR: | 是 | 不终止,跑完统一报失败 | 检查点失败但还要收集后续现象 |
$fatal(n, ...) | Fatal: | 是 | 立即终止,等价于调 $finish | 继续运行已无意义的硬失败 |
$info(...) | INFO: | 否 | 不终止 | 普通进度与诊断信息 |
三个细节值得记住。第一,$fatal 的第一个参数是 finish 编号(本例的 1),它影响退出码与诊断级别,但无论传 0、1 还是 2,$fatal 都会终止仿真,不要以为传 0 就只是提示。第二,$error 与 $warning 打印后仿真继续,适合「一次跑收集多个失败点」的回归场景;$fatal 适合 DUT 已经偏离前提、后续波形没有分析价值的场景,比如协议状态机进了非法态。第三,四个任务的消息文本都是开发者自己写的格式化字符串,XSim 只负责前缀、时间与定位——所以 Fatal 行里的 exceeded 9 不是工具的判断,而是写检查器的人留下的措辞,它可能本身就是错的,本例正是如此。
Python 侧的同类对照可以看 cocotb TestFailed / AssertionError 断言失败报错读法速查:cocotb 用异常冒泡终止测试,XSim 用系统任务终止仿真,定位思路都是「先读消息里的实际值,再回到断言代码核对期望值」。
三、定位:Time、Process、File 三要素反推出错位置
Fatal 行下面那行定位信息是本篇最值钱的一行,三个字段各有分工:
- Time: 115 ns:触发时刻。配合时钟周期和复位释放时间,可以反推是第几拍、DUT 当时应该输出什么,判断触发值是否真的非法。
- Process: /tb_assert_bad/Always16_2:触发进程的层次化名字。斜杠前是模块实例路径,说明失败发生在 testbench 自身而不是 DUT 内部;
Always16对应 testbench 第 16 行开始的那个 always 块,后缀编号是 XSim 给同模块内进程的序号。看到进程名落在 tb 里,方向就明确了——先怀疑检查器,再怀疑 DUT。 - File: .../tb_assert_bad.v:源文件绝对路径。批处理日志里路径可能很长,配合进程名里的行号提示,直接在该文件第 16 行附近找到
$fatal调用即可。
完整的排查动线如下图,颜色按角色区分:红是入口现象与根因,蓝是排查动作,橙是修复动作,绿是复验闭环,紫是防复发沉淀。
读这张图抓住 check 这个判断点:定位到断言代码后不要急着改,先回答「触发值在 DUT 的合法取值空间里吗」。本例 cnt 是 4 位向量,硬件上只能表示 0..15,计到 15 后回绕 0 是 Verilog 向量加法的标准行为;cnt=10 不但合法,而且是复位后必然经过的值。于是责任归属反转:DUT 完全正确,是检查器的期望值脱离了位宽现实。工程里快速核对的办法是把信号声明和断言条件放一起看——只要断言里出现的常数不能从声明位宽推出来,就是可疑硬编码。
四、修复:让断言边界与总线位宽对齐
修复只动 testbench,DUT 一行不改。把检查条件从「不许超过 9」改成「超出 4 位总线的表示能力才报错」。对 4 位向量而言,cnt > 15 在正常回绕下永远不会成立,断言变成一道真正的兜底防线:
// tb_assert_fixed.v:修复后的检查器
always @(posedge clk) begin
if (rst_n && cnt > 4'd15)
$fatal(1, "out of 4-bit range: cnt=%0d", cnt);
end这里顺手纠正原检查器的两个写法问题。其一,条件用 >= 4'd10 而消息写 exceeded 9,条件与文案各说各话,排查时容易误判边界到底是几;修复版条件与消息统一围绕 4 位范围表述。其二,更稳妥的工程写法是让边界从同一个 parameter 推导,位宽调整时断言自动跟随,彻底消灭「改了位宽忘了改断言」:
parameter integer WIDTH = 4;
wire [WIDTH-1:0] cnt;
// 上界随位宽推导:WIDTH 改为 8 时,检查边界自动变成 255
always @(posedge clk) begin
if (rst_n && cnt > (2**WIDTH - 1))
$fatal(1, "out of range: cnt=%0d", cnt);
end判断该改断言还是该改 DUT 的标准只有一个:以信号声明的位宽和协议规定的取值空间为准。如果设计文档明确要求计数器在 9 之后归零或停住,那才是 DUT 缺限幅逻辑;本例没有这类需求,4 位回绕就是规格本身,因此修断言。
五、验证:212 ns 正常 $finish,Fatal 行消失
用修复后的 testbench 重跑同一套三连,xvlog 与 xelab 输出不变,xsim 阶段的差别集中在结尾:没有 Fatal: 行,计数器在整个运行期间自由经过 10、15 并回绕,最终由 initial 块的 #200 $finish 收尾。
这张终端卡的验证锚点是第 39 行:$finish called at time : 212 ns : File ".../tb_assert_fixed.v" Line 23。212 ns 正好等于 initial 块里 #12 与 #200 两段延迟之和,Line 23 正是 testbench 里计划内的 $finish 语句——收尾时间、收尾位置都与 testbench 意图逐字对上。与坏版相比,差异只有两处:115 ns 的 Fatal 三要素行消失,结束时间从 115 ns 变为 212 ns。把两次日志的结尾并排看,断言修复是否生效不需要猜,看这两个锚点即可。
六、常见坑与预防清单
常见坑:缺 timescale 时断言根本跑不到
这个实验首跑还踩到一个前置坑:如果 DUT 或 testbench 文件漏掉 `timescale 编译指令,xelab 阶段会先被 ERROR: [XSIM 43-4099] ... doesn't have a timescale 拦下,快照建不出来,xsim 无从运行,写得再好的断言也没有执行机会。也就是说,排查 $fatal 问题的前提是三连的前两步已经干净通过;看到 43-4099 时不要在断言逻辑里浪费时间,先去补 timescale,完整机理与「只给一半文件补为什么不行」见 XSIM 43-4099:模块缺 timescale 报错排查与修复。本工程两个 testbench 与 DUT 都统一写了 `timescale 1ns/1ps,所以首跑修好这个问题后才采集到上文两份日志。
预防清单
- 断言边界只从位宽或协议推导:禁止裸写魔数上界;用
parameter WIDTH配合2**WIDTH - 1表达上界,位宽一改,断言自动跟随。 - 条件与消息文案保持一致:
>= 10就不要在消息里写exceeded 9,消息里带上实际值与合法范围,让 Fatal 行本身就能说明问题。 - 按失败性质选报告任务:要一次回归收集多个失败点用
$error,状态机进非法态等继续运行无意义的场景才用$fatal,避免断言一触发就看不到后续现象。 - 先过三连前两步再查断言:xelab 报 43-4099、43-3225 之类错误时,仿真根本没启动,先修编译细化问题,再谈断言行为。
- 复验看两个锚点:Fatal 行消失且
$finish的时间与行号落在 testbench 计划位置,两个条件同时满足才算修复,只看「进程退出码为 0」会漏掉提前终止。 - 断言纳入评审与 lint:code review 时把每条断言的期望值与信号声明位宽对照检查,有条件的工程用 Verilator 等工具先跑一轮宽度与越界 lint,把边界错误挡在 Vivado 之前。
实验环境
- 机器/系统:Windows 节点(64 位)
- 工具版本:Vivado Simulator v2017.4(SW Build 2086221,
xvlog.bat/xelab.bat/xsim.bat) - 日期:2026-11-10
- 完整命令(问题版本):
xvlog.bat dut_counter.v tb_assert_bad.v;xelab.bat tb_assert_bad;xsim.bat tb_assert_bad -t xsim_run.tcl - 完整命令(修复版本):
xvlog.bat dut_counter.v tb_assert_fixed.v;xelab.bat tb_assert_fixed;xsim.bat tb_assert_fixed -t xsim_run.tcl - 批处理脚本
xsim_run.tcl内容只有run all与quit两行,跑完即退,日志由命令行重定向留存。
代码获取:完整复现工程(dut_counter.v、tb_assert_bad.v、tb_assert_fixed.v、xsim_run.tcl 与两份实测日志)在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-xsim-assertion-failure,装好 Vivado 2017.4(或任意含 XSim 的版本)后按 README 跑两组 xvlog、xelab、xsim 三连即可复现 115 ns 提前终止与 212 ns 正常收尾的对照。
这一篇在知识体系里的位置
本篇是 FPGA 入门排错线中 XSim 运行期故障的一员,从两份真机日志出发讲「检查器边界与 DUT 位宽脱节」导致的断言失败。同流程上游的两类故障分别是:xelab 找不到顶层的 XSim 找不到设计单元(XSIM 43-3225),模块缺时间精度的 XSIM 43-4099 timescale 报错排查;testbench 的基本写法与自检任务入门见 Testbench 与仿真。跨语言的断言失败对照见 cocotb TestFailed / AssertionError 报错读法速查,综合侧「仿真与硬件语义分叉」的姊妹案例见 Synth 8-324 位选越界排查——两篇共享同一条教训:凡是脱离信号声明位宽的常数,迟早会在仿真或综合里还债。
总结
XSim 日志里的 Fatal: 行不是工具报错,而是 testbench 的 $fatal 系统任务主动终止仿真,其下 Time、Process、File 三要素分别给出触发时刻、出错 always 块与源文件,是反推定位的直接线索;$fatal 与只打印不终止的 $error、$warning 的核心差别就是立即结束仿真。本例中 4 位计数器合法范围本就是 0..15,cnt=10 是必然经过的合法值,真正出错的是把上界硬编码为 9 的检查器,把边界改为随位宽推导的 cnt > 15(参数化写法 2**WIDTH - 1)后,Fatal 行消失、仿真在 212 ns 由计划内的 $finish 正常收尾;排查时先确认 xvlog、xelab 干净通过(小心缺 timescale 的 43-4099 让断言根本跑不到),复验盯住「Fatal 消失 + 收尾时间与行号吻合」两个锚点,断言边界永远从信号位宽和协议规格推导,不写无源魔数。