X-Propagation
See how an unreset flop hides from two-state and four-state simulation, how formal turns it into a free value the solver picks, and what this engine does with an explicit x.
- Lesson
- F5.5
- Time
- 15 min
- Runs on
- SymbiYosys
- Check
- 4 questions
A mode register had no reset, because firmware was meant to write it before enabling the block. Every RTL test passed, and on silicon the flop powered up as 3 often enough to enable the block early. The gate-level run had shown X everywhere, and the team had spent two weeks blaming the netlist.
Measured with smtbmc yices at depth 20. The flop has no reset, so its first value is the solver's choice: 3. Sampled at step 2, reported at 3.
Three Ways to Read an Unknown
Read the diagram as text
- mode[3:0], no reset: four-state sim: X. Two-state sim: 0. Formal: any value, chosen per run
- mode == 4'd3: four-state: X. Two-state: 0. Formal: 1 if the solver chose 3
- en = armed && (mode == 3): four-state: X once armed
- if (en) in a consumer: four-state: X counts as false, the else branch runs
- gate-level simulation: the X spreads through the netlist: pessimism
- silicon: the flop powers up 0 or 1 per bit; sometimes 3
- mode[3:0], no reset → mode == 4'd3
- mode == 4'd3 → en = armed && (mode == 3)
- en = armed && (mode == 3) → if (en) in a consumer: optimism
- en = armed && (mode == 3) → gate-level simulation
- mode[3:0], no reset → silicon
Neither simulation says what the hardware does. Formal has no X: it treats the unknown as a value it may pick, and picks the one that breaks the rule.
In four-state simulation the flop starts as X. The compare gives X, and an if on an X takes the else branch: the RTL hides the unknown, which is X-optimism. Gate-level simulation spreads the X instead, which is pessimism. A two-state simulator starts the flop at 0 and the bug never appears.
Formal is two-valued. A flop with no reset and no initial value is a free choice in the first step (F1.6). The solver picks mode = 3, and the enable fires before any firmware write: FAIL at reported step 3.
A Sampler Against the Full Set
| Run | Initial value of mode | Early enables |
|---|---|---|
| Two-state simulation, default | every bit 0 | 0 in 10000 cycles |
| Random initial values, seed 1 | random, per seed | 0 |
| Random initial values, seed 2 | random, per seed | 0 |
| Random initial values, seed 3 | random, per seed | 0 |
| Random initial values, seeds 1 to 100 | random, per seed | found by 11 of 100 seeds |
| Formal, depth 20 | all sixteen values at once | FAIL at reported step 3 |
Verilator 5.055 devel with --x-initial unique, run with +verilator+rand+reset+2 and a seed. A random start is a sampler: about one value in sixteen breaks the rule. Formal is the full set.
The two-state run printed 0 early enables in 10000 cycles, and so did seeds 1, 2 and 3. Over 100 seeds, 11 started mode at 3. A random initial value is one draw per run; formal took all sixteen in one run of 491 ms.
Two Fixes, and the x You Write
| Change | Measured result |
|---|---|
reset mode to 0 (FIX_RESET) | BMC PASS to depth 20; UNKNOWN at k = 4 (base case passed, induction step failed); Prove PASS (abc pdr) |
no reset, if ($initstate) assume(mode == 4'd0) | BMC PASS to depth 20 |
assign dbg = 1'bx; with assert(!dbg) | FAIL at reported step 2 |
Mode bmc at depth 20 unless named, smtbmc yices, reset assumed in step 0.
The reset fixes the design. The assumption fixes only the proof: it says firmware always writes first, which is now a promise someone must keep and a line for the sign-off review. The k-induction attempt needs a helper or PDR, because an unreachable state with mode at 3 and no write looks fine for four steps.
An explicit x becomes a free value too. With SBY's default script the solver's model gains $anyseq cells where the RTL wrote 1'bx (2 here, none without it), so the assert on dbg fails at once. Commercial X-propagation apps model more than this; this page claims only what the open engine does.
Asked in Interviews
| Question | Answer |
|---|---|
| What is X-propagation checking? | Finding unknowns (unreset flops, x assignments) that can reach an output or a control point, by treating them as values a solver may choose. |
| Why do RTL simulations miss it? | X-optimism in if and case takes a branch, and a two-state simulator starts at one chosen value. |
| What is the fix? | A reset, or a documented assumption that firmware writes first, reviewed at sign-off. |
Try It
The rule in SVA
assert property (@(posedge clk) disable iff (rst) en |-> fw_seen);
fw_seen is a bench flop set by the first firmware write.
As this engine runs it
always @(posedge clk) begin
if ($initstate) begin assume(rst); end else begin assume(!rst); end
end
reg fw_seen=0; // firmware has written mode at least once (a write during reset counts too)
always @(posedge clk) if (fw_wr) fw_seen <= 1;
always @(posedge clk) if (!rst && en) assert(fw_seen);
A write during reset counts too, so the rule cannot blame the reset step.
[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top xcmp
[files]
design.svRun the simulation twin
Run the Verilator bench: 0 early enables. Which initial value did it use?
Run formal
Run as it is: FAIL at reported step 3. Read
modeat step 0 and look for a firmware write.Add the reset
Put
rstin themodebranch and run: BMC PASS to depth 20.Assume instead
Remove the reset and add
if ($initstate) assume(mode == 4'd0);: BMC PASS to depth 20. Where should that line be reviewed?Write an x
Add
assign dbg = 1'bx;andassert(!dbg): FAIL at reported step 2.
design.sv
props.sv
Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.
Inputs are free unless constrained. Run to find a counterexample or check a proof.
Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.
SBY summary and log
Check Yourself
Remember
- Four-state simulation hides an unknown in
if(optimism) or floods the netlist with it (pessimism); neither says what silicon does. - Formal is two-valued: an unreset flop is a free initial value and the solver picks the one that breaks the rule.
- A random initial value is one draw per run; formal checks all values in one run.
- Fix with a reset, or with an initial-state assumption that goes on the sign-off list. An explicit x is a free value too.