Finite state machines control almost everything in a chip: bus interfaces, protocol engines, power controllers. Their bugs are typically in the corners: a transition that forgets to clear a timer, a state that can never be left, a state that can never be reached. This article lists the standard properties for state machines and uses them on a request controller with a timeout, whose timer is not cleared when a new request starts.

9 min read | Intermediate

What You'll Learn

  • Write the standard properties of a state machine: legal states, transitions, outputs, no deadlock
  • Check every state for reachability with covers
  • Find a timer that is not reinitialized on a transition
  • Prove a state machine property with k-induction

Standard FSM Properties

  • Legal states: the state register only holds defined encodings. With a 2-bit register and 3 states, the fourth encoding must never appear.
  • Legal transitions: each state goes only to its allowed successors, under the right conditions.
  • Outputs per state: outputs match the state, such as busy exactly in WAIT.
  • No deadlock: the machine cannot get stuck, which in a bounded check means a limit on how long it stays in a waiting state.
  • Reachability: every state and every transition can happen, shown with covers.

The Design: a Request Controller

On req, the controller goes to WAIT and waits for ack. If no acknowledge comes within 4 cycles, it goes to ERR until software clears it:


module req_fsm (input clk, input rst, input req, input ack, input clr,
                output busy, output err, output reg [1:0] state);
  localparam IDLE = 2'd0, WAIT = 2'd1, ERR = 2'd2;
  reg [2:0] t;
  assign busy = (state == WAIT);
  assign err  = (state == ERR);
  always @(posedge clk)
    if (rst) begin
      state <= IDLE; t <= 3'd0;
    end else case (state)
      IDLE: if (req) state <= WAIT;                     // bug: t is not cleared here
      WAIT: if (ack)            begin state <= IDLE; end
            else if (t == 3'd3) begin state <= ERR;  t <= 3'd0; end
            else                      t <= t + 3'd1;
      ERR:  if (clr) state <= IDLE;
      default: state <= IDLE;
    endcase
endmodule

The timer t is cleared on reset and on the timeout, but not when an acknowledge ends the wait. The next request then starts with a timer that is already running.

The Properties


a_legal:   assert property (@(posedge clk) disable iff (rst) state != 2'd3);
a_timeout: assert property (@(posedge clk) disable iff (rst)
                            state == IDLE && req ##1 (state == WAIT && !ack) [*4] |=> state == ERR);
a_early:   assert property (@(posedge clk) disable iff (rst) $rose(err) |-> $past(state == WAIT, 4));

a_timeout says that four cycles of waiting without an acknowledge lead to ERR. a_early says that ERR is never entered without four cycles of WAIT before it. In the lab form, a helper counter w measures the time spent in WAIT:


`ifdef FORMAL
  reg init = 1'b1;
  always @(posedge clk) init <= 1'b0;
  always @(*) if (init) assume (rst);
  // legal states only
  always @(*) if (!init) assert (state != 2'd3);
  // helper: how long the machine has been waiting
  reg [3:0] w = 4'd0;
  always @(posedge clk) w <= (rst || state != WAIT) ? 4'd0 : w + 4'd1;
  // ERR only after 4 cycles of waiting without ack
  always @(posedge clk) if (!init && !$past(rst) && err && !$past(err)) assert ($past(w) == 4'd3);
  // never stuck in WAIT: at most 4 cycles
  always @(*) if (!init) assert (w <= 4'd4);
`endif

The run fails at step 9:

The first request is acknowledged in its second cycle of waiting, which leaves t at 1. The second request starts with t = 1, so the controller reaches ERR after only 3 cycles of waiting. A slow memory that answers in the fourth cycle would be reported as failed. The fix clears the timer when a request is accepted: IDLE: if (req) begin state <= WAIT; t <= 3'd0; end.

Quick Check

Q: Why does the bug need two requests to show?

Show Answer

After reset, t is 0, so the first request times out correctly. Only a wait that ends with an acknowledge leaves t non-zero, and only the next request suffers from it.

Reachability

For every state, a cover shows that it can be reached, and the trace shows how. In the lab, the meant-to-fail form assert (!err) fails at step 6 on the fixed design: a request in step 1 and four cycles without an acknowledge. A state machine with an unreachable state either has dead logic or a bug that blocks the path to it.

A Proof

The fixed controller passes in prove mode with k-induction at depth 6, and with the PDR engine. A small state machine with a short timer is a typical case where a full proof is cheap, so there is no reason to stop at a bounded result.

Quick Check

Q: The state register has 2 bits and the machine uses 3 states. What should happen if the fourth encoding ever appears, for example after a radiation upset?

Show Answer

The machine should recover, which is what the default: state <= IDLE; branch does. Formal cannot reach that state from reset, so the recovery is checked separately, for example with a start-anywhere run.

Running It in ChipVerify Lab

The example below is the buggy controller with the properties above. It fails at step 9 within the free depth.

Common Mistakes

Mistake #1: Clearing a timer on only some of the transitions

A timer or counter tied to a state must be initialized on every path into that state. Check each transition, or better, clear it on entry.

Mistake #2: Writing properties that copy the case statement

"In WAIT with t == 3, go to ERR" restates the RTL and shares its bug. Describe the time spent in a state with an independent helper, as w does.

Mistake #3: Skipping the reachability covers

An error state that can never be reached makes every property about it vacuous. Cover every state.

Practice Exercise

Challenge: Fix the Timer and Show ERR Is Reachable

Open the example and run it. Fix the controller. Then add a meant-to-fail check that err never goes high, and read the trace it produces.

Hint

The meant-to-fail check is always @(*) if (!init) assert (!err);. Its counterexample is a trace into ERR.

Solution

With the fix, the properties pass. The meant-to-fail check fails at step 6: a request in step 1 moves the controller to WAIT in step 2, no acknowledge comes in steps 2 to 5, and the controller is in ERR in step 6. That is exactly four cycles of waiting, as specified.

Open the ready-made example in the lab and run the formal check from the formal panel. The free tier allows a depth of up to 10 cycles, and PRO allows up to 100.

Summary

  • State machines need properties for legal states, legal transitions, outputs, bounded waiting and reachability.
  • A helper counter for the time spent in a state is independent of the RTL's own timer and catches its mistakes.
  • A timer that was not cleared on a new request made the controller time out after 3 cycles instead of 4.
  • Covers, or meant-to-fail checks in the lab, show that every state can be reached.
  • Small state machines are easy to prove completely; do it.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.