XSim 仿真假死:未驱动信号恒为 X 导致 wait 永不苏醒的定位与修复
更新时间:2026-11-10。本篇属于 FPGA 入门排错线,只讨论 XSim 批处理下一个非常具体的现象:
run all之后仿真看起来挂死,没有报错、没有崩溃,直到 testbench 自带的看门狗把它强行收尾。关于 RTL 仿真里 X 的四类来源与 VCS 定位法,仿真波形里信号变 X 怎么排查 给的是方法学全景;门级网表阶段的乐观/悲观 xprop 语义见 门级仿真 X 传播:乐观与悲观语义。本篇不重复 X 的来源分类,只盯着「未驱动的 reg 恒为 X,wait永远等不到」这一条故障链,工具链固定为 Vivado 2017.4 的 XSim。
首屏答案:仿真没有真的死锁,而是 testbench 里的使能信号 start 自始至终没有任何赋值,Verilog 四值逻辑中未驱动的 reg 保持初始值 X。wait (start === 1'b1) 等待的是条件由假变真的事件,可 X 不会自己跳变,条件永远不成立,initial 进程永久挂起;DUT 内部同样因为 start === 1'b1 恒为假而停在复位值 0。最后是 500 ns 的看门狗进程调 $finish 收场,日志里只留下一行 watchdog: start=x is_x=1 state=0。修复方法很朴素:复位期间把 start 驱动成确定的 0,再由独立的 initial 块按时钟沿给出一个周期的脉冲,wait 在 25 ns 正常苏醒,仿真 75 ns 计划内收尾。
本文要回答的问题
run all之后仿真不报错也不退出,为什么说是「假死」而不是工具挂了?- 未驱动的 reg 为什么恒为 X,X 态为什么不能唤醒
wait,也触发不了@(posedge ...)? ==和===遇到 X 时结果有什么差别,为什么本例 DUT 里用===反而帮了排查的忙?- 批处理模式没有波形窗口,靠什么手段拿到「信号此刻是 X」的证据?
$isunknown在 Verilog 模式下为什么编译不过? - 修复为什么是「复位期先给确定电平」而不是随便找个地方赋个 1?
先给一张分区概念卡,把现象、根因、定位手段和修复方向放在同一张图里,后面的章节逐区展开。
读这张卡抓住左右之分:左边两区是「仿真为什么不动」,右边两区是「怎么证实、怎么修」。最容易走偏的做法是看见 state=0 就回头查计数器逻辑——本例 DUT 一行都没错,它只是忠实地响应了一个永远无效的使能。
一、现象:run all 不收尾,500 ns 看门狗兜底
实验 DUT 是一个由外部 start 选通的 4 位计数器:复位时 state 清零,复位释放后只有 start === 1'b1 的时钟沿才递增。testbench 的主激励进程在释放复位后用 wait 等启动脉冲,等到了才打印 GO 并延时结束;另一个 initial 块是 500 ns 看门狗,专门对付「仿真不知道为什么停不住」的局面。坏版 testbench 的关键段落如下:
// tb_xhang_bad.v:start 声明了,但全文没有任何赋值
reg start; // no initial value, no assignment anywhere -> X
initial begin
clk = 1'b0;
rst_n = 1'b0;
#13 rst_n = 1'b1;
wait (start === 1'b1);
$display("GO observed at %0t ns, state=%0d", $time, state);
#50 $finish;
end
initial begin
#500;
$display("watchdog: start=%b is_x=%0d state=%0d at %0t ns",
start, (start === 1'bx), state, $time);
$finish;
end按 XSim 命令行三连 跑完 xvlog.bat dut_xhang.v tb_xhang_bad.v、xelab.bat tb_xhang_bad,xsim 加载快照执行 run all。编译细化全部干净,快照正常构建,仿真却在运行阶段「挂住」:没有任何错误输出,GO 那行始终不打印,最后只等来看门狗。
这段输出有两个锚点。第 39 行 watchdog: start=x is_x=1 state=0 at 500000 ns 是看门狗自己的格式化打印:%b 把 start 打成字符 x,is_x=1 说明 (start === 1'bx) 判定为真,计数器输出 state 停在复位值 0。第 40 行 $finish called at time : 500 ns : File ".../tb_xhang_bad.v" Line 32 表明结束动作来自 testbench 第 32 行——正是看门狗块里的 $finish,而不是主激励进程第 25 行那个计划内的收尾。换句话说,仿真不是崩溃,是主进程永久挂起后被备用进程「拉了电闸」。这也是批处理脚本里必须放看门狗的理由:没有 GUI 可以人工点停止,一旦真出现挂起,没有看门狗的 run all 会一直占着仿真进程。
二、机理:X 不是电平跳变,wait 与沿事件都唤醒不了
要理解为什么 wait 永不苏醒,先回到 Verilog 的四值逻辑与事件语义。Verilog 的每个 bit 有 0、1、X、Z 四种取值,X 表示「未知」——仿真器不知道它是 0 还是 1。reg 型变量的初始值就是 X,在被首次赋值之前一直保持。本例的 start 全文没有任何过程赋值或连续赋值驱动,所以它不是「刚开始是 X、后来变了」,而是从时间 0 到仿真结束恒为 X。
wait (cond) 是电平敏感语句:条件在进入时已为真就立即继续,否则进程挂起,直到条件表达式的值发生变化且新值为真才被唤醒。注意这里的关键词是「值发生变化」。仿真器的调度靠事件驱动,一个恒为 X 的信号不产生任何值变化事件,等待它的进程连被重新评估的机会都没有;即使被重新评估,X === 1'b1 的结果也是确定的假(见下表),依旧醒不过来。同理,@(posedge start) 也等不到:上升沿定义为 0/X/Z 到 1 的跳变,一个从不跳变的 X 不存在任何沿。
== 与 === 对 X 的处理是本节另一个必须分清的点,1 位场景的真值表如下:
| 表达式 A op B | == 逻辑相等结果 | === 全等结果 |
|---|---|---|
1'bx == 1'b1 / === | X(不确定) | 0 |
1'bx == 1'b0 / === | X(不确定) | 0 |
1'bx == 1'bx / === | X(不确定) | 1 |
1'b1 == 1'b1 / === | 1 | 1 |
1'b0 == 1'b0 / === | 1 | 1 |
差别一句话概括:== 只要任一端含 X 或 Z,结果就可能是 X,表示「比不出确定答案」;=== 按四值逐位严格比较,X 等于 X、X 不等于 1,结果永远是确定的 0 或 1。传统 Verilog 语义里,if 和 wait 只把确定的 1 当真,X 与 0 一样走假分支。于是本例出现了一组「看似矛盾、实则自洽」的行为:DUT 里 else if (start === 1'b1) 恒为假,计数器一次也不使能,state 保持 0;testbench 里同一个条件让 wait 永久挂起。如果当初写的是 wait (start == 1'b1),表达式的值是 X 而非确定假,行为更隐晦,排查反而更难——这也是涉及使能、握手的判断推荐 === 的原因。X 在组合逻辑里的扩散规律与四值语义基础,信号变 X 排查 有更系统的梳理,可以对照阅读。
三、定位:看门狗打印加 X 判定,以及一个方言坑
批处理模式下没有波形窗口可看,定位「谁是 X」全靠 testbench 自己留证据。本例的做法是两层:看门狗超时后用 $display 同时打印嫌疑信号的 %b 值、专门的 X 判定位和下游输出,一行日志同时回答「是不是 X、确认手段是什么、DUT 受了什么影响」。判定 X 的写法这里有个真实的坑。
这个实验的首跑版本(作业 v17a)里,看门狗用的是 SystemVerilog 系统函数 $isunknown(start),结果 xvlog 在 Verilog 模式下直接拒绝编译:
ERROR: [VRFC 10-2696] system call isunknown not allowed in this dialect. Use SystemVerilog mode
ERROR: [VRFC 10-1040] ... module ignored第一条错误说明 $isunknown 属于 SystemVerilog 方言,.v 文件按 Verilog-2001 解析时不允许使用;第二条连带导致整个模块被忽略。模块没编译进库,后续 xelab 自然报 [XSIM 43-3225] Cannot find design unit,快照建不出来——同一个设计单元缺失错误的通用排查路径见 XSim 找不到设计单元(XSIM 43-3225)。出路有两条:把文件改成 .sv 走 SystemVerilog 模式,或者换成 Verilog-2001 本来就合法的四值比较。本例选后者,(start === 1'bx) 与 $isunknown 在 1 位信号上等价,不依赖文件后缀,改完即编译通过,这才有了上一节那份运行日志。
整体排查动线如下图,颜色按角色区分:红是入口现象与根因,蓝是排查动作,橙是修复动作,绿是复验闭环,紫是防复发沉淀。
读这张图抓住 judgex 到 event 的转折:日志只能告诉你「信号是 X」,真正定根因还要回源码回答「它为什么一直是 X」。本例源码里 start 只有声明行、没有任何赋值,证据链就此闭合。反过来,如果看门狗打印出的是确定的 0,那方向就完全不同——要查的是激励时序而不是缺驱动,两类问题在第一行日志上就能分叉。
四、修复:复位期给确定电平,再按沿给脉冲
修复只动 testbench,DUT 保持原样。改动有两处,缺一不可。第一处是在主激励进程的复位序列里加 start = 1'b0,让信号从时间 0 起就处于确定电平,上电 X 被消灭在复位期;第二处是新增一个独立 initial 块,等复位释放后再等两个时钟沿,然后把 start 拉高一个完整时钟周期再拉低,形成一个干净的单周期启动脉冲:
// tb_xhang_fixed.v:复位期确定电平 + 独立激励块给脉冲
initial begin
clk = 1'b0;
rst_n = 1'b0;
start = 1'b0; // 复位期即驱动为确定 0
#13 rst_n = 1'b1;
wait (start === 1'b1);
$display("GO observed at %0t ns, state=%0d", $time, state);
#50 $finish;
end
initial begin
@(posedge rst_n);
repeat (2) @(posedge clk);
start = 1'b1; // 单周期启动脉冲
@(posedge clk);
start = 1'b0;
end为什么强调「复位期先给 0」而不是图省事直接在某个时刻赋 1?因为 wait 和 DUT 的使能判断都需要一个从确定值到确定值的跳变:先有稳定的 0,后来的 0→1 才是仿真器眼里真实的上升沿事件,挂起的进程才会被重新评估并唤醒。跳过这一步、让信号从 X 直接赋 1,虽然多数仿真器也能唤醒进程,但 X→1 的起始相位不干净,对接计数器、握手类 DUT 时容易在沿采样上留下歧义。脉冲由独立 initial 块生成也有讲究:激励生成与等待判断分属两个进程,testbench 的可读性更好,脉冲宽度直接由时钟沿量化,改频率时不会失配。
五、验证:25 ns 苏醒,75 ns 计划内收尾
用修复版 testbench 重跑同一套三连:xvlog.bat dut_xhang.v tb_xhang_fixed.v、xelab.bat tb_xhang_fixed、xsim.bat tb_xhang_fixed -t xsim_run.tcl。仿真行为与坏版形成干净对照。
第 39 行 GO observed at 25000 ns, state=0 说明 wait 在 25 ns 苏醒:复位在 13 ns 释放,之后两个时钟沿(15 ns、25 ns)对齐,激励块在 25 ns 沿后把 start 拉高,等待进程随即恢复;此刻是首个使能沿之前,state 仍为 0,与计数器时序吻合。第 40 行 $finish called at time : 75 ns : File ".../tb_xhang_fixed.v" Line 27 表明收尾来自主激励块第 27 行的 #50 $finish(25 ns 加 50 ns 等于 75 ns),是计划内动作;500 ns 的看门狗全程没有触发,日志里找不到 watchdog 行。两份日志并排,修复是否成立看三个锚点即可:GO 行出现、finish 时间从 500 ns 变为 75 ns、finish 行号从看门狗的第 32 行变为主进程的第 27 行。
六、预防清单
- 激励 reg 声明即配初值:testbench 里所有要驱动 DUT 的 reg,在复位序列中统一赋确定电平,不留「声明了回头再驱动」的空窗;时钟与复位本身更要在时间 0 就赋值。
- 批处理 testbench 标配看门狗:每个等待类 testbench 放一个独立 initial 块,超时打印全部嫌疑信号的当前值后
$finish,让挂死以「有证据的收尾」收场,而不是占住进程等人工干预。 - 使能与握手判断用
===:===对 X 给出确定的 0/1,条件真假不依赖仿真器对 X 分支的处理,排查时日志行为可预测;需要判定 X 时 Verilog 模式写(sig === 1'bx),SystemVerilog 模式才可用$isunknown,混用前先确认文件后缀与方言。 - 脉冲沿时钟沿生成:跨进程的启动、握手激励用
@(posedge clk)量化宽度,避免#23这类与时钟周期无关的裸延时,复位释放后空两拍再发激励,给 DUT 一个干净的起始相位。 - 挂死先查激励再查 DUT:看到看门狗日志里信号为 X,方向是回 testbench 找缺失的赋值;同系列的运行期故障 XSim 断言失败:$fatal 仿真提前终止的 Fatal 行定位与边界修复 则相反——DUT 行为正确、错在 testbench 的检查边界,两篇对照可以建立「先分清是激励问题还是检查问题」的分诊习惯。
实验环境
- 机器/系统:Windows 节点(64 位),Vivado 工程语境为 xc7a35tcpg236-1
- 工具版本:Vivado Simulator v2017.4(SW Build 2086221,
xvlog.bat/xelab.bat/xsim.bat) - 日期:2026-11-10
- 完整命令(问题版本):
xvlog.bat dut_xhang.v tb_xhang_bad.v;xelab.bat tb_xhang_bad;xsim.bat tb_xhang_bad -t xsim_run.tcl - 完整命令(修复版本):
xvlog.bat dut_xhang.v tb_xhang_fixed.v;xelab.bat tb_xhang_fixed;xsim.bat tb_xhang_fixed -t xsim_run.tcl - 批处理脚本
xsim_run.tcl只有run all与quit两行,跑完即退,两份日志均由命令行重定向留存。
代码获取:完整复现工程(dut_xhang.v、tb_xhang_bad.v、tb_xhang_fixed.v、xsim_run.tcl 与两份实测日志)在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-xsim-x-propagation-hang,装好含 XSim 的 Vivado 后按 README 分别跑两组 xvlog、xelab、xsim 三连,即可复现 500 ns 看门狗收尾与 75 ns 正常收尾的对照。
这一篇在知识体系里的位置
本篇是 FPGA 入门排错线中 XSim 运行期故障的一员,从两份真机日志出发讲「未驱动信号恒 X 导致等待语句永不苏醒」这一具体故障链。X 态的方法学全景——未初始化寄存器、多驱动冲突、数组越界、跨模块引用四类来源与 VCS 定位法——在 仿真波形里信号变 X 怎么排查;综合后门级网表阶段的 X 放大与掩盖、乐观/悲观 xprop 语义在 门级仿真 X 传播:乐观与悲观语义,两篇与本篇构成「RTL 方法学—门级语义—XSim 具体故障」的三层关系。同工具链的上游编译细化故障见 XSim 找不到设计单元(XSIM 43-3225),同类运行期故障(检查器而非激励出错)见 XSim 断言失败:$fatal 仿真提前终止的 Fatal 行定位与边界修复。
总结
XSim 仿真假死不是工具挂死,而是事件驱动机制下的必然结果:未驱动的 reg 保持四值逻辑初值 X,恒 X 不产生任何值变化事件,wait 等不到条件变真、@(posedge ...) 等不到沿,主激励进程永久挂起,最后由看门狗在 500 ns 调 $finish 收场,日志锚点是 watchdog: start=x is_x=1 state=0。定位上,批处理模式靠看门狗 $display 打印嫌疑信号与下游输出,Verilog 方言用 (sig === 1'bx) 判 X——$isunknown 是 SystemVerilog 系统函数,Verilog 模式会触发 VRFC 10-2696 并连带 43-3225;机理上要分清 == 遇 X 返回不确定值、=== 按四值严格比较给出确定 0/1。修复只动 testbench:复位期把 start 驱动为确定 0,再由独立 initial 块按时钟沿给单周期脉冲,wait 在 25 ns 苏醒、仿真 75 ns 计划内收尾,看门狗不再触发。预防上守住三条:激励 reg 声明即配初值、批处理 testbench 标配带证据打印的看门狗、使能握手判断一律用 ===。