One-Hot FSM Lock-Up and Button Debounce in a Guard Interlock Controller
A machine-guard interlock controller is a 12-state one-hot FSM in an FPGA clocked at 50 MHz. The register is reg [11:0] state;, decoded with a case (1'b1) inside an always @(posedge clk) block that ends with state <= next_state; and a registered drive output K1_EN that energises the guard-lock solenoid. The case lists the 12 legal states and has one default: ; branch. A 'request open' push-button arrives asynchronously on a 3 m unshielded cable from a panel; the design samples it directly with the 50 MHz clock and uses a level-sensitive 'press detected' flag to start a 2 s unlock timer.
Field returns show two symptoms. On a few units the machine stops responding to 'request open' while K1_EN stays energised, and configuration readback reports state = 12'b0000_0000_0000. Separately, operators regularly get a double unlock request from a single press, and this is repeatable on a particular panel, not random. There is no parity or scrubbing on the configuration memory and no external watchdog.
1. Explain how the FSM can end in 12'b0 and stay there with K1_EN still asserted, given that a default branch exists.
Read the worked answer
Encoding. 12 one-hot bits give 4096 encodings, of which only 12 are legal. Any single event upset that clears the set bit maps a legal state to 12'b0; an upset that sets another bit maps it to a two-bit state. One-hot has no inherent error detection, so nothing flags the illegal value.
Why it never recovers. The default: ; branch assigns nothing. In a clocked always block that means next_state holds its previous value (inferred latch/hold), so the machine re-enters the same unmatched case every cycle: state 0 is a stable trap, not a transient.
Why the solenoid stays on. Because no case item matches, none of the assignments to K1_EN execute either; a registered output simply retains its last value, which was the energised value from the run state. The failure is therefore fail-dangerous: the guard lock is released-looking but the drive is still enabled.
Fix. default: next_state = S_FAULT; with S_FAULT explicitly de-energising K1_EN, next_state = S_SAFE after one cycle, and an alarm. Better still: drive K1_EN from a separate block with a default of 0, add a watchdog kicked only in legal states, and consider parity-protected or duplicated-inverted encoding to make zero unreachable by a single bit flip.
2. Design the input conditioning for the push-button and justify why the double request is not a metastability problem.
Read the worked answer
Diagnosis first. A repeatable double trigger on one specific panel with a mechanical contact points to contact bounce and cable pickup, not metastability: metastability is a probabilistic, rare, loading-dependent effect, whereas bounce is systematic and will reproduce on every press.
Conditioning chain.
- RC filter at the connector (e.g. 10 kOhm / 100 nF, tau = 1 ms) plus a Schmitt-trigger buffer, to slow edges and reject 3 m of coupled noise.
- Two-flop synchroniser on the 50 MHz clock to remove setup/hold violation. A one-stage synchroniser leaves the first flop metastable for a full clock period; adding the second raises MTBF by many orders of magnitude (typically thousands of years per synchroniser at 50 MHz).
- Debounce: divide the clock to 1 kHz and require 8 consecutive identical samples (8 ms) before accepting a level - comfortably longer than a 5 ms bounce. Note that an 8 ms window at 50 MHz needs 400 000 counts, so a 16-bit counter is not sufficient; use 19+ bits or the divided clock.
- Edge-detect the debounced level to produce a single-cycle event, and re-arm the 'press detected' flag only after the button is seen released for the same debounce window.
3. How would you verify that the design cannot lock up in an illegal state after the fixes?
Read the worked answer
Simulation alone is not sufficient because the illegal state space is large, so use a mixture:
- Fault-injection sweep. For each of the 4096 encodings, force
stateat time zero in simulation and assert that (a) the machine reachesS_SAFEwithin two clock cycles, (b)K1_ENis de-asserted within two cycles, and (c) a fault flag is raised. This is exhaustive and runs in seconds. - Formal properties. Assert the invariants
state != 12'b0after reset release and$onehot(state) || state == S_FAULT; use a reachability check from reset to prove no path into an illegal state exists in normal operation, and prove recovery from every illegal state as a cover property. - Gate-level re-run. Re-run the state-sweep test on the post-synthesis netlist. An empty
defaultis exactly the construct that synthesis can optimise into a hold or a don't-care, so the RTL result must be confirmed at gate level with the real timing. - Output-level check. Add a safety assertion that
K1_ENmay only be high whilestateis in the enumerated 'run' set, and prove it formally - this catches the registration behaviour that let the solenoid stay energised.