X-Propagation in Practice
Section 8.3 was about timing. Section 8.4 is about bugs, starting with the one Module 6 mentioned in passing: X spreading through a design like dye in water. Time to chase one down.
Where X comes from
X in Verilog means unknown: could be 0, could be 1, the simulator does not know. It is not a simulator bug; it is the simulator being honest about real uncertainty. Three sources account for nearly every X you will chase.
Uninitialised registers. Before reset, before any assignment, a reg in simulation starts at X, deliberately, because on silicon its power-on value is unpredictable, and starting at X is the honest representation rather than pretending it has a value hardware does not guarantee.
Unconnected or floating ports. A port left unconnected at instantiation, or a bus with some bits driven and others left dangling, reads as X on every unconnected bit.
Explicit X in RTL. Code like default: result = 'x; from the case article in Section 5.3, deliberately marking a path as “this should not happen, and if it does I want the simulator to tell me loudly” rather than picking a value that would hide the problem.
Why X spreads instead of resolving
This is what makes X dangerous rather than merely inconvenient: most operators propagate X rather than guessing. 1'bx & 1'b1 is 1'bx, because the simulator has no way to know which real value the X stands for and refuses to guess. The same holds for OR, addition, comparison, nearly every operator. One uninitialised bit feeding one gate gives an X output; that feeds the next gate, which also gives X; and the contamination spreads through every piece of logic downstream. The boundary of “everything this X has touched” only grows until something intervenes.
The last step in Figure 1 is the one that turns a nuisance into a real problem. If X reaches an FSM’s state register, the state itself becomes X, and an FSM in state X cannot evaluate any of its case branches, since X == IDLE is itself X, neither true nor false. The state machine locks up in an undefined condition and every consumer of its outputs inherits the corruption.
The trace-back technique
Chasing an X to its origin is a repeatable process, not luck.
- Find the earliest point in simulation time where the X first appears. Open the waveform (Section 6.5) and scan backward from wherever you noticed the symptom, a wrong output or a stuck FSM, to the first transition to X. This is almost never the moment you noticed; by the time an X is visible at an output it may have originated many cycles and many logic levels earlier.
- At that first X transition, identify what drives the signal. Trace backward one level at a time: which register or expression is responsible for this signal’s value at this moment?
- Repeat, following the X upstream, until you reach a genuine source: an uninitialised register that was never reset, an unconnected port, or a deliberate
'xdefault. That is the root cause; everything downstream was correctly propagating a problem that started there.
// A classic case: this register has no reset path at all
reg [7:0] counter; // starts at X, and NOTHING in this module ever fixes that
always @(posedge clk) begin
counter <= counter + 1; // X + 1 = X, forever: this counter can NEVER recover on its own
end
Note the trap: counter is not X once, it is X forever, because X + 1 is X and nothing in this code can ever produce a real value from it. This is the failure Section 5.5 warned about when it insisted every register needs a reset path. A register with no reset can start at X and have no way to leave.
A tempting but wrong instinct is to add an X-clearing hack downstream, something like if (result === 8'hxx) result = 8'h00;, to make a testbench stop reporting X-related failures. That fixes nothing. It hides the uninitialised register behind a value that looks clean in simulation and has no relationship to what silicon will do, since silicon has no X state, only some specific unpredictable voltage. Always trace back to the real source.
Practice Exercise
A testbench reports that an FSM’s state reads X from 45 ns, though the design’s reset is correctly asserted and released well before that. Describe the trace-back steps.
Solution
First confirm the state register itself has a correct reset path. If it does, as stated, the X is not coming from the state register’s own reset logic, so it must be arriving through the next-state logic feeding it. Trace backward from state to the next_state expression and check every signal it reads: FSM inputs and internal signals. If one of them is X at or before 45 ns, follow it upstream the same way: a register with its own missing reset, an unconnected port from another module, or an intentional 'x default in another block’s case statement firing under conditions the design did not expect. Continue one signal and one clock edge at a time until you reach a signal that is the first to go X with no earlier X feeding it. That signal’s own driver is the root cause; everything downstream, including the FSM state, was correctly propagating it.
X is the simulator honestly representing uncertainty, from an uninitialised register, an unconnected port or a deliberate 'x. Because nearly every operator propagates it, one uninitialised signal can spread through a design, and if it reaches a state register the machine sticks. The fix is never to clear an X where it surfaces but to trace back, one driver at a time, to the source, which is almost always a missing reset or an unconnected port. In the next article we revisit latch inference, not the mechanism, which Section 5.2 covered, but how to find one that has already got into a design.