Menu

Verilog x 和 z 值:未知态与高阻态详解

Verilog 信号有四种可能的值,而不是两种。本文讲清 x(未知)和 z(高阻)在仿真中的真正含义,以及如何调试它们。

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

二值逻辑与四值逻辑

在软件中,一个位是 0 或 1。在 Verilog 中,一个位可以是四种值之一:

  • 0:导线被驱动为低电平。
  • 1:导线被驱动为高电平。
  • x:导线的值未知,仿真器无法确定。
  • z:导线处于高阻状态,没有任何东西在驱动它。

之所以有这个四值模型,是因为硬件也有同样的问题。一根真实的导线可以接低电平、接高电平、处于不确定状态(被两个互相冲突的源驱动),或者悬空(完全没有驱动)。仿真器必须对这四种情况都建模才有用。

x 是怎么出现的

运行下面的代码,看看输出:

a 声明了但从未写入,所以它一直是 x。计算 x + 5 得到 x:任何与未知值进行的算术运算,结果都是未知的。输出中 aaaa 的位置显示的全是 x。

设计中 x 的常见来源:

  • 声明了却从未复位的 reg(大多数可综合的 Verilog 会用显式复位来清零它们)。
  • 没有 default 的 case 语句,遇到了没有任何分支匹配的输入值。
  • 重构之后失去了唯一驱动源的 wire。
  • if/else 链中,一个分支给某个信号赋了值,另一个分支却没有(没有覆盖到的情况会锁存出 x)。
  • 读取超出向量或数组末尾的位置。

x 的传播:一点点 x 就能毁掉一切

x 最麻烦的地方在于它会传播。操作数中只要有一个 x 位,整个结果就会变成 x:

注意 0 & x 是 0(与 0 相与总是 0),1 | x 是 1(与 1 相或总是 1)。仿真器逐位进行悲观估计,但仍然遵守这些恒等式。算术运算和比较运算就没这么宽容了。

这就是为什么单个未初始化的寄存器就能让整条输出总线变成 xxxx。从任何一个 x 往回追,你都能找到源头。

z 是怎么出现的

z 是一根没有任何东西驱动的导线的值:

这段代码里有两种情况:

  • floating 只是声明了,从未被驱动,默认值就是 z。
  • data_out 是有意设计的三态输出。当 enable 为低时,输出被显式释放为 z。总线驱动器就是这样“放手”,让另一个驱动器接管的。

在内部逻辑上,z 几乎总是错的。在双向引脚或共享总线上,z 恰恰是对的。

用 == 与 === 比较

普通的相等运算符 == 在任一操作数含有 x 或 z 位时都会返回 x:

===(以及它的搭档 !==)会逐位严格比较,包括 x 和 z。在 testbench 中需要检测 x/z 或与之比较时,都应该使用它。=== 不可综合,但在 testbench 的 initial 块里这无关紧要。

系统函数 $isunknown(expr) 是询问“这个表达式里有没有 x 或 z 位?”最简洁的方式:有就返回 1,没有就返回 0。

把 x 用作有意的无关项

一种有争议但合法的写法:在状态机的 default 分支中使用 'x,告诉综合工具“这个状态不可达,可以随意优化”:

case (state)
    IDLE:    next_state = go ? RUNNING : IDLE;
    RUNNING: next_state = done ? IDLE : RUNNING;
    default: next_state = 'x;   // unreachable
endcase

综合工具可以利用这个 x 来合并状态、减少门数。在仿真中,如果你的推理有误,default 分支确实被执行到了,你会看到 x 从 next_state 传播出去,bug 立刻就会暴露。

只有在你仔细想清楚 default 分支是否真的不可达之后,才这样用。如果没有想清楚,就把 default 设成一个安全的状态。

常用的调试方法

你正盯着一张满是 x 的波形图。按以下步骤来:

  1. 找到最早出现的 x。 在波形上沿时间往回看。最早变成 x 的信号离源头最近。
  2. 找到它的驱动源。 打开源代码。是什么在给这个信号赋值?一个 assign?一个 always 块?
  3. 检查驱动源的输入。 如果驱动源的右侧表达式中有 x,那么传播只是在正常工作,bug 在更上游。
  4. 如果驱动源的输入都正常,却产生了 x,说明驱动源本身不完整。 比如 case 缺少 default,if 缺少 else,寄存器缺少复位。

大多数满屏是 x 的 bug,最终都可以归结为以下之一:缺少复位、缺少 default,或者某个子模块没有连接。

接下来

现在你已经掌握了完整的数据类型知识:wire/reg、向量、参数、数字字面量,以及四值逻辑模型。下一章开始用这些知识构建表达式:各种运算符,包括那些在软件中没有意义的位级运算符。

常见问题

Verilog 中的 x 是什么意思?

x 是未知值。一个为 x 的信号可能是 0,也可能是 1,仿真器无法确定。它会出现在这些情况中:信号没有被驱动、两个驱动源互相冲突、寄存器在复位之前被读取,或者任何本来会悄悄传播的未定义行为。把 x 当作 bug 的信号,它几乎从来不是你想要的结果。

Verilog 中的 z 是什么意思?

z 是高阻值:这根导线完全没有被驱动。它是三态输出(数据总线、双向引脚)的合法状态,但在内部信号上,z 通常是个错误,意思是“这里什么都没连”。除了明确的三态输出使能之外,综合工具会拒绝大多数 z 的用法。

为什么我的 Verilog 输出是 xxxx?

几乎总是因为某个信号没有被任何东西驱动,或者某个运算把另一个信号的 x 传播了过来。倒着往回查:哪个信号是 x?什么在驱动它?驱动源是否在工作?常见的原因有:case 语句缺少 default 分支、寄存器没有复位,以及重构之后丢失了驱动源的 wire。

Verilog 中如何检查 x 或 z?

使用 === 运算符,它会逐位精确比较,包括 x 和 z。当 a 真的是 x 时,a === 1'bx 为真。普通的 == 在任一操作数含有 x 位时都会返回 x,所以 a == 1'bx 永远得不到你想要的答案。还有 $isunknown(a),它返回一个干净的布尔值。

Verilog 中可以把 x 赋值为默认值吗?

可以,这是 case 语句中一种有意为之的技巧:default: out = 'x; 是在告诉综合工具“我保证这种情况永远不会发生,你可以随意优化”。代价是,如果仿真中真的发生了,x 会传播开来,你就会看到这个 bug。只有在你已经证明 default 分支不可达时才这样用,不要把它当作省略分支的办法。

Coddy 编程语言插图

和 Coddy 一起学编程

开始