Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Digital Design & Verification (Verilog, SystemVerilog, VLSI)  Module 4 – Finite-state machines in RTL

FSM coding styles in SystemVerilog

Code Moore and Mealy state machines in SystemVerilog with typed enums, pick one- or two-process style, register outputs, and recover from illegal codes.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Write Moore and Mealy outputs of a state machine typed with an enum
  • Choose between the two-process and one-process styles for a machine
  • Prevent output glitches with registered outputs, and weigh what each choice costs
  • Recover safely from illegal state codes and from resets

Before you start

What you’ll learn · Before you start

What you will learn

  • Write Moore and Mealy outputs of a state machine typed with an enum
  • Choose between the two-process and one-process styles for a machine
  • Prevent output glitches with registered outputs, and weigh what each choice costs
  • Recover safely from illegal state codes and from resets

Before you start

On this page

A keypad lock written as a state machine matched an independent Python model on all 18,307 cycles of a seeded test, in three builds and on two simulators. Its decoded unlock output still read 1 for 2 time units after a wrong code, once its four state bits reached the decoder at slightly different times, and a build without a default branch stopped answering for good after its register was set to code 12. Correct, clean and safe are three properties of a state machine, and the way it is written decides each of them.

You will code Moore and Mealy outputs from a typed enum, choose between the two-process and one-process styles, register outputs so that they cannot glitch, and lead every code back to a known state. Icarus Verilog 13.0 and Verilator 5.052 ran the simulations, Yosys 0.69 found the machines in the RTL, and the lock’s model runs in your browser.

A state machine is a register, its next-state logic and its outputs

The lock opens for the four digits 5 9 1 3. It keeps reading a wrong code to its fourth digit, so the keypad never shows which digit was wrong, and the third wrong code in a row locks the keypad for 8 cycles. A finite-state machine (FSM): Logic that is always in one of a fixed set of states, held in a state register, and moves to its next state at each clock edge according to its present state and its inputs. holds that history in a few flip-flops: nine named states, IDLE, OK1 to OK3 while the code is right so far, BAD1 to BAD3 once a digit was wrong, then OPEN and LOCKOUT, with two counters beside them, for the wrong codes in a row and the cycles left in OPEN or LOCKOUT.

State On a key Without a key
IDLE, OK1, OK2 the next OK state for the right digit, else the next BAD state stays
OK3 OPEN for the right digit, else a failure stays
BAD1, BAD2 the next BAD state, whatever the digit stays
BAD3 a failure, whatever the digit stays
OPEN, LOCKOUT ignored IDLE after 3 or 8 cycles

A failure goes back to IDLE and counts one more wrong code, or goes to LOCKOUT on the third in a row. door_lock.sv writes the machine in three blocks. The state register is an always_ff that loads state_q from state_d at each rising edge, after a reset to IDLE. The next-state logic: The combinational part of a state machine that works out the next state from the present state and the inputs. In two-process style it is an always_comb that begins by keeping the present state. is an always_comb that begins with state_d = state_q, “stay”, and then lets the present state’s branch decide, so no branch can forget to stay and no latch appears. The output logic turns the state into outputs. The state is a typed enumerated type (enum): A type whose values are a list of named constants, such as typedef enum logic [1:0] {IDLE, RUN, DONE} state_t. The tools refuse a plain number assigned to it without a cast, and name() prints a value's label. More in “Structs, enums, unions and interfaces”, state_t: the tools refuse a plain number where a state belongs, and logs print IDLE instead of 0.

door_lock.sv: a keypad lock in two-process style SV · hdl/door_lock.sv
// A keypad door lock. The four digits of Code unlock it for OpenTicks cycles. A wrong code is still read to its
// fourth digit, so the keypad never shows which digit was wrong, and the third wrong code in a row locks the keypad
// for LockTicks cycles. Two-process style: the next state and the Mealy output in always_comb, the registers in
// always_ff, then the output logic.
module door_lock #(
  parameter logic [15:0] Code              = 16'h5913,  // one digit per four bits, the first digit on top
  parameter logic [3:0]  OpenTicks         = 4'd3,
  parameter logic [3:0]  LockTicks         = 4'd8,
  parameter bit          RegisteredOutputs = 1'b0       // 1: every output from a flip-flop
) (
  input  logic       clk_i,
  input  logic       rst_ni,
  input  logic       key_valid_i,  // a key press, for one cycle
  input  logic [3:0] key_i,        // its digit, 0 to 9
  output logic       accept_o,     // Mealy: the key of this cycle is taken
  output logic       unlock_o,     // Moore: the door is unlocked
  output logic       lockout_o     // Moore: keys are ignored for now
);
  typedef enum logic [3:0] {IDLE, OK1, OK2, OK3, BAD1, BAD2, BAD3, OPEN, LOCKOUT} state_t;

  state_t     state_q, state_d;
  logic [1:0] fails_q, fails_d;    // wrong codes in a row
  logic [3:0] timer_q, timer_d;    // cycles left in OPEN or LOCKOUT
  logic       accept_d;            // the key of this cycle is taken
  logic       wrong;               // the fourth digit of a wrong code is being taken

  // Next-state logic: start from "stay", then let the present state's branch decide.
  always_comb begin
    state_d  = state_q;
    fails_d  = fails_q;
    timer_d  = timer_q;
    accept_d = 1'b0;
    wrong    = 1'b0;
    unique case (state_q)
      IDLE: if (key_valid_i) begin
        accept_d = 1'b1;
        if (key_i == Code[15:12]) state_d = OK1;
        else                      state_d = BAD1;
      end
      OK1: if (key_valid_i) begin
        accept_d = 1'b1;
        if (key_i == Code[11:8]) state_d = OK2;
        else                     state_d = BAD2;
      end
      OK2: if (key_valid_i) begin
        accept_d = 1'b1;
        if (key_i == Code[7:4]) state_d = OK3;
        else                    state_d = BAD3;
      end
      OK3: if (key_valid_i) begin
        accept_d = 1'b1;
        if (key_i == Code[3:0]) begin
          state_d = OPEN;
          fails_d = '0;
          timer_d = OpenTicks - 4'd1;
        end else begin
          wrong = 1'b1;
        end
      end
      BAD1: if (key_valid_i) begin
        accept_d = 1'b1;
        state_d  = BAD2;
      end
      BAD2: if (key_valid_i) begin
        accept_d = 1'b1;
        state_d  = BAD3;
      end
      BAD3: if (key_valid_i) begin
        accept_d = 1'b1;
        wrong    = 1'b1;
      end
      OPEN, LOCKOUT: begin         // keys are ignored until the timer runs out
        if (timer_q == '0) state_d = IDLE;
        else               timer_d = timer_q - 4'd1;
      end
`ifndef NO_RECOVERY
      default: state_d = IDLE;     // codes 9 to 15 name no state: back to IDLE
`endif
    endcase
    if (wrong) begin
      if (fails_q == 2'd2) begin   // the third wrong code in a row
        state_d = LOCKOUT;
        fails_d = '0;
        timer_d = LockTicks - 4'd1;
      end else begin
        state_d = IDLE;
        fails_d = fails_q + 2'd1;
      end
    end
  end

  // The registers: reset values, then the values the next-state logic chose.
  always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
      state_q <= IDLE;
      fails_q <= '0;
      timer_q <= '0;
    end else begin
      state_q <= state_d;
      fails_q <= fails_d;
      timer_q <= timer_d;
    end
  end

  // Output logic: the Moore outputs decoded from the state and the Mealy output passed on, or, with
  // RegisteredOutputs, all three loaded into flip-flops from the next-state logic's values.
  if (!RegisteredOutputs) begin : g_decoded
    assign unlock_o  = (state_q == OPEN);
    assign lockout_o = (state_q == LOCKOUT);
    assign accept_o  = accept_d;
  end else begin : g_registered
    logic unlock_q, lockout_q, accept_q;
    always_ff @(posedge clk_i or negedge rst_ni) begin
      if (!rst_ni) begin
        unlock_q  <= 1'b0;
        lockout_q <= 1'b0;
        accept_q  <= 1'b0;
      end else begin
        unlock_q  <= (state_d == OPEN);     // from the next state: no cycle lost
        lockout_q <= (state_d == LOCKOUT);
        accept_q  <= accept_d;              // a Mealy output: one cycle later
      end
    end
    assign unlock_o  = unlock_q;
    assign lockout_o = lockout_q;
    assign accept_o  = accept_q;
  end
endmodule

Step through the cycle in which the fourth digit arrives, block by block:

Interactive · step by step The three blocks of the lock through one clock cycle Step through the cycle in which the fourth digit arrives and watch which block answers when.
The three blocks of the lock through one clock cycle, step 7 of 7. Showing next-state logic (OPEN: count down), key_valid_i, key_i (3, valid), state register (state_q = OPEN), output logic (unlock_o = 1), accept_o, unlock_o. In focus: next-state logic.state_dnext-state logicOPEN: count downkey_valid_i, key_i3, validstate registerstate_q = OPENoutput logicunlock_o = 1accept_o, unlock_ostate_q

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Before the edge. The register holds OK3, so the lock has the first three digits of the code. The output logic decodes unlock_o = 0 from that state.
  2. A key arrives. Just after a falling edge, key_valid_i goes to 1 with key_i = 3, the fourth digit. Nothing is stored yet; the inputs simply arrive.
  3. The next state. The next-state logic starts from state_d = state_q, then the OK3 branch sees the right digit and chooses OPEN. The register still holds OK3.
  4. The Mealy output. The same branch set accept_d to 1, and the output logic passes it straight on. accept_o is 1 in this very cycle, because it reads the key.
  5. The clock edge. At the rising edge the register loads state_d and state_q becomes OPEN. This is the only moment at which the state changes.
  6. The Moore output. unlock_o is decoded from state_q alone, so it turns 1 only now, a cycle after the key. The key has gone, so accept_o is 0 again.
  7. Round again. state_q also feeds the next-state logic, which now runs the OPEN branch. It counts three cycles down, then chooses IDLE.

The order is the design: the present state and the inputs go in, the next state comes out, and the clock edge makes it the present state. lock_model.py states the same specification another way. It keeps the digits of the attempt in a list and compares the whole code once four are in, with no OK or BAD states at all, which makes it an independent reference model: An independent model of what a design must do, written from its specification, that a testbench compares the design's outputs against. More in “How chips are designed and verified” for the RTL. Change KEYS, for example to three wrong codes in a row, and run it:

The lock's model, cycle by cycle Python · model/lock_model.py
"""A model of door_lock.sv, written from its specification rather than from its code: it keeps the digits of the
attempt in a list and compares the whole code once four are in, with no OK or BAD states at all.

Run as it is, it prints what the lock does, cycle by cycle, with the keys in KEYS. With --vectors FILE it writes the
test vectors of tb_door_lock.sv instead: 2,000 key sequences from a seeded generator, one line per clock cycle, with
the outputs the model expects in that cycle.
"""
import random
import sys

CODE = (5, 9, 1, 3)
OPEN_TICKS = 3   # cycles the door stays unlocked
LOCK_TICKS = 8   # cycles the keypad ignores keys after the third wrong code in a row

# One key press per entry, or None for a cycle without a key. Change them and run again.
KEYS = [5, 9, None, 1, 3, None, None, None, 7, 7, 7, 7]


class Lock:
    def __init__(self):
        self.reset()

    def reset(self):
        self.taken = []        # digits of the attempt so far
        self.mode = "entry"    # entry, open or lockout
        self.fails = 0         # wrong codes in a row
        self.timer = 0         # cycles left in open or lockout

    def outputs(self, key_valid):
        """accept needs this cycle's input (Mealy); unlock and lockout need only the state (Moore)."""
        accept = key_valid and self.mode == "entry"
        return int(accept), int(self.mode == "open"), int(self.mode == "lockout")

    def clock(self, key_valid, key):
        """What one rising clock edge does."""
        if self.mode != "entry":
            if self.timer == 0:
                self.mode = "entry"
            else:
                self.timer -= 1
            return
        if not key_valid:
            return
        self.taken.append(key)
        if len(self.taken) < 4:
            return
        right = tuple(self.taken) == CODE
        self.taken = []
        if right:
            self.mode, self.fails, self.timer = "open", 0, OPEN_TICKS - 1
        elif self.fails == 2:
            self.mode, self.fails, self.timer = "lockout", 0, LOCK_TICKS - 1
        else:
            self.fails += 1


def trace(keys):
    lock = Lock()
    print("cycle  key  accept  unlock  lockout")
    for cycle, key in enumerate(keys):
        accept, unlock, lockout = lock.outputs(key is not None)
        shown = "-" if key is None else str(key)
        print(f"{cycle:5}  {shown:>3}  {accept:6}  {unlock:6}  {lockout:7}")
        lock.clock(key is not None, key)
    print(f"after {len(keys)} cycles: mode {lock.mode}, wrong codes in a row {lock.fails}, digits of the next code {len(lock.taken)}")


def vectors(path, seed=1, sequences=2000):
    """Inputs and expected outputs, one cycle per line: rst_n, key_valid, key, accept, unlock, lockout."""
    rng = random.Random(seed)
    lock = Lock()
    lines = []
    count = {"opened": 0, "locked out": 0, "resets": 0}

    def cycle(rst_n, key_valid, key):
        if not rst_n:
            lock.reset()
        accept, unlock, lockout = lock.outputs(key_valid)
        lines.append((rst_n << 8) | (key_valid << 7) | (key << 3) | (accept << 2) | (unlock << 1) | lockout)
        before = lock.mode
        if rst_n:
            lock.clock(key_valid, key)
        if before == "entry" and lock.mode != "entry":
            count["opened" if lock.mode == "open" else "locked out"] += 1

    for _ in range(sequences):
        while lock.mode != "entry" and rng.random() < 0.9:
            cycle(1, 0, rng.randrange(16))             # most people wait while the door is open or locked
        code = CODE if rng.random() < 0.3 else tuple(rng.randrange(10) for _ in range(4))
        reset_before = rng.randrange(4) if rng.random() < 0.02 else None
        for i, digit in enumerate(code):
            for _ in range(rng.randrange(3)):
                cycle(1, 0, rng.randrange(16))         # no key: the key bus holds anything
            if i == reset_before:
                cycle(0, 0, 0)
                count["resets"] += 1
            cycle(1, 1, digit)
    with open(path, "w") as f:
        f.write("".join(f"{v:03x}\n" for v in lines))
    print(f"{sequences} key sequences, seed {seed}: {len(lines)} cycles, opened {count['opened']} times, "
          f"locked out {count['locked out']} times, {count['resets']} resets")


if __name__ == "__main__":
    if len(sys.argv) == 3 and sys.argv[1] == "--vectors":
        vectors(sys.argv[2])
    else:
        trace(KEYS)

Output

cycle  key  accept  unlock  lockout
    0    5       1       0        0
    1    9       1       0        0
    2    -       0       0        0
    3    1       1       0        0
    4    3       1       0        0
    5    -       0       1        0
    6    -       0       1        0
    7    -       0       1        0
    8    7       1       0        0
    9    7       1       0        0
   10    7       1       0        0
   11    7       1       0        0
after 12 cycles: mode entry, wrong codes in a row 1, digits of the next code 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lock_model.py

accept was 1 in every cycle with a key, unlock was 1 for the three cycles after the fourth digit, and 7 7 7 7 ended as one wrong code in a row. accept answers in the key’s own cycle while unlock waits for the next one, and that difference has a name.

Checkpoint

Check: The three blocks of a state machine

  1. Question 1 of 2 Why does the always_comb of door_lock.sv begin with state_d = state_q?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: So that every path that chooses no state keeps the present one, and no latch appears

    A combinational block that leaves a value unassigned on some path builds a latch. Starting with "stay" gives state_d a value on every path, so each branch needs to name only the states it moves to.

  2. Question 2 of 2 What does typing state_q as the enum state_t buy the lock?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The tools refuse a plain number where a state belongs, and logs print IDLE instead of 0

    The enum checks the source and names the values in every log. The hardware is still four flip-flops, which is why the machine needs a way back from the codes no name has.

A Moore output waits for the edge; a Mealy output answers at once

A Moore machine: A state machine whose outputs depend only on its present state, so they change only after a clock edge has changed the state. output depends on the state alone: unlock_o is state_q == OPEN, so it can change only after a clock edge has changed the state. A Mealy machine: A state machine with outputs that depend on its inputs as well as its state, so they can answer in the same cycle as an input, and follow every change of that input, glitches included. output depends on the inputs as well: accept_o, the keypad’s “key taken” signal, is 1 when a key is valid in any state that reads keys, so it answers in the key’s own cycle. tb_moore_mealy.sv keys in the right code, one key every other cycle, prints each cycle just before its rising edge, and ends with a pulse on key_valid that starts and ends between two edges:

The right code, then a pulse on key_valid

hdl/tb_moore_mealy.sv

// The right code, one key every other cycle, printed just before each rising edge: accept_o (Mealy) answers in the
// key's own cycle, unlock_o (Moore) only after the edge that takes the fourth digit. Then a pulse on key_valid that
// starts and ends between two edges, as a noisy keypad wire might give, reaches accept_o and nothing else.
module tb_moore_mealy;
  logic       clk = 1'b0, rst_n = 1'b0, key_valid = 1'b0;
  logic [3:0] key = '0;
  logic       accept, unlock, lockout;

  door_lock u_lock (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
                    .accept_o(accept), .unlock_o(unlock), .lockout_o(lockout));

  always #5 clk = ~clk;                        // rising edges at 5, 15, 25 ...

  // One cycle: inputs just after a falling edge, the line printed just before the next rising edge.
  task automatic cycle(input logic valid, input logic [3:0] digit);
    key_valid = valid;
    key       = digit;
    #4 if (valid) $display("t=%3d  key %0d  state %-4s  accept %b  unlock %b", $time, digit,
                           u_lock.state_q.name(), accept, unlock);
       else       $display("t=%3d  key -  state %-4s  accept %b  unlock %b", $time,
                           u_lock.state_q.name(), accept, unlock);
    @(negedge clk);
  endtask

  initial begin
    repeat (2) @(negedge clk);
    rst_n = 1'b1;
    cycle(1, 5); cycle(0, 0); cycle(1, 9); cycle(0, 0); cycle(1, 1); cycle(0, 0); cycle(1, 3);
    repeat (4) cycle(0, 0);
    key = 4'd5;                                // the falling edge at 130; the next rising edge is at 135
    #1 key_valid = 1'b1;
    #1 $display("t=%3d  pulse on key_valid: accept %b  unlock %b", $time, accept, unlock);
    #1 key_valid = 1'b0;
    #1 $display("t=%3d  pulse over:         accept %b  unlock %b", $time, accept, unlock);
    @(posedge clk);
    #1 $display("t=%3d  after the edge: state %s", $time, u_lock.state_q.name());
    $finish;
  end
endmodule

Commands · hdl/mealy_session.py

from session import run

# The right code, then a pulse on key_valid between two edges, printed just before each rising edge.
run("""
iverilog -g2012 -Wall -o mealy.vvp tb_moore_mealy.sv door_lock.sv
vvp -n mealy.vvp
""")

Output

# Icarus Verilog 13.0 (stable)
$ iverilog -g2012 -Wall -o mealy.vvp tb_moore_mealy.sv door_lock.sv
door_lock.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
$ vvp -n mealy.vvp
t= 24  key 5  state IDLE  accept 1  unlock 0
t= 34  key -  state OK1   accept 0  unlock 0
t= 44  key 9  state OK1   accept 1  unlock 0
t= 54  key -  state OK2   accept 0  unlock 0
t= 64  key 1  state OK2   accept 1  unlock 0
t= 74  key -  state OK3   accept 0  unlock 0
t= 84  key 3  state OK3   accept 1  unlock 0
t= 94  key -  state OPEN  accept 0  unlock 1
t=104  key -  state OPEN  accept 0  unlock 1
t=114  key -  state OPEN  accept 0  unlock 1
t=124  key -  state IDLE  accept 0  unlock 0
t=132  pulse on key_valid: accept 1  unlock 0
t=134  pulse over:         accept 0  unlock 0
t=136  after the edge: state IDLE
tb_moore_mealy.sv:37: $finish called at 136 (1s)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mealy_session.py

Icarus Verilog’s note that it ignores the unique qualifier is about the checks it skips, not about the result. The trace shows the two timings. accept was 1 at t = 24 with the key 5 still on the inputs. The fourth key was there at t = 84, yet unlock stayed 0 until t = 94: the edge at 85 made the state OPEN, and only then could a Moore output follow. Step through the run:

Interactive · message by message When each output of the lock answers Step through the right code and the pulse after it; the times are the ones the run printed.
When each output of the lock answers, step 8 of 8. Messages so far: Keypad to door_lock: key 5; door_lock to Keypad: accept_o = 1; Keypad to door_lock: 9, then 1; Keypad to door_lock: key 3; door_lock to Door latch: unlock_o = 1; door_lock to Door latch: unlock_o = 0; Keypad to door_lock: 2-unit pulse; door_lock to Keypad: accept_o pulses.Keypadkey_valid_i, key_idoor_lockstate_qDoor latchunlock_okey 5accept_o = 19, then 1key 3unlock_o = 1unlock_o = 02-unit pulseaccept_o pulsesunlock_o stays 0

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. First key. At t = 24 the key 5 is on the inputs and the state is IDLE. accept_o is a Mealy output, so it is already 1 in the key's own cycle.
  2. Accepted at once. The keypad sees accept_o = 1 before the clock edge at 25, the same cycle. At that edge the state becomes OK1.
  3. Two more keys. The keys 9 and 1 go the same way, two cycles apart, and take the state through OK2 to OK3.
  4. The fourth key. At t = 84 the key 3 arrives. accept_o is 1 again at once, but unlock_o is still 0, because the state is OK3 until the edge.
  5. The Moore output. The edge at 85 makes the state OPEN, and at t = 94 unlock_o is 1. A Moore output follows the state, so it comes one cycle after the key.
  6. Locked again. OPEN lasts three cycles; at t = 124 the state is IDLE and unlock_o is 0 again.
  7. A pulse between edges. At t = 131 key_valid rises for 2 time units, between the edges at 125 and 135, as a noisy wire might.
  8. The Mealy output follows. accept_o follows the input and pulses with it, at t = 132. The edge at 135 sees key_valid = 0, so the state stays IDLE and unlock_o never moves.

The pulse at the end is the price of answering at once. accept_o followed key_valid up and down between the edges at 125 and 135, while the edge at 135 saw key_valid at 0, so the state stayed IDLE. A flip-flop on the same clock never sees such a pulse, because it samples only at edges; a buzzer driver, a pin or a block on another clock does.

Moore unless the answer must come in the same cycle

Start with Moore outputs: they change once per edge and depend on nothing outside the machine. Make an output Mealy only when it must answer an input in that input’s own cycle, as accept_o does.

Checkpoint

Check: Moore and Mealy outputs

  1. Question 1 of 2 In the trace, the fourth key was on the inputs at t = 84. When did unlock_o first read 1, and why then?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: At t = 94, after the edge at 85 made the state OPEN, because a Moore output follows the state

    The key was taken at the edge at 85, which loaded OPEN; the first line after it, t = 94, shows unlock 1. A Moore output is always a cycle behind the input that moved the state.

  2. Question 2 of 2 A 2-unit pulse on key_valid starts and ends between two rising edges, while the lock is in IDLE. What changes?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: accept_o pulses with it; the state stays IDLE and unlock_o does not move

    A Mealy output passes on every change of its inputs, glitches included. Flip-flops on the same clock never see such a pulse; a pin, a latch or another clock domain can.

A registered output cannot glitch

A Moore output is clean in RTL, but in silicon state_q == OPEN is a decoder of four bits, and the four bits reach it at slightly different times after each edge. tb_glitch.sv models that with delays on the state bits: bit 0 arrives 3 time units after the edge and bits 3 to 1 after 1. Beside that lock it runs a second one built with RegisteredOutputs = 1, whose three outputs come from flip-flops loaded from the next-state logic’s values, and it keys in a wrong code, then the right one, then the pulse:

Decoded and registered outputs, side by side

hdl/tb_glitch.sv

// The lock with decoded outputs and the same lock with registered outputs, side by side. A decoder in silicon sees
// the four state bits through wires of unequal delay: here bit 0 arrives 3 time units after the clock edge and bits 3
// to 1 after 1. A wrong code (4 9 1 3) and then the right one (5 9 1 3) are keyed in, and a pulse on key_valid
// between two edges ends the run. Every change of an output between two clock edges is printed.
module tb_glitch;
  logic       clk = 1'b0, rst_n = 1'b0, key_valid = 1'b0;
  logic [3:0] key = '0;
  logic       dec_accept, dec_unlock, dec_lockout, reg_accept, reg_unlock, reg_lockout;

  door_lock #(.RegisteredOutputs(1'b0)) u_dec (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
      .accept_o(dec_accept), .unlock_o(dec_unlock), .lockout_o(dec_lockout));
  door_lock #(.RegisteredOutputs(1'b1)) u_reg (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
      .accept_o(reg_accept), .unlock_o(reg_unlock), .lockout_o(reg_lockout));

  always #5 clk = ~clk;                        // rising edges at 5, 15, 25 ...

  // The decoded unlock, as a decoder at the end of four wires of different delay computes it.
  logic [3:0] seen;
  logic       seen_unlock;
  assign #3 seen[0]   = u_dec.state_q[0];
  assign #1 seen[3:1] = u_dec.state_q[3:1];
  assign seen_unlock = (seen == 4'b0111);         // OPEN, the eighth name of the enum

  always @(seen_unlock) if (rst_n)
    $display("t=%3d  decoded unlock, delayed wires: %b   (state %s)", $time, seen_unlock, u_dec.state_q.name());
  always @(reg_unlock) if (rst_n)
    $display("t=%3d  registered unlock:             %b   (state %s)", $time, reg_unlock, u_reg.state_q.name());

  task automatic press(input logic [3:0] digit);
    key_valid = 1'b1;
    key       = digit;
    @(negedge clk);
    key_valid = 1'b0;
    @(negedge clk);
  endtask

  initial begin
    repeat (2) @(negedge clk);
    rst_n = 1'b1;
    press(4); press(9); press(1); press(3);    // wrong first digit: IDLE BAD1 BAD2 BAD3, then IDLE
    press(5); press(9); press(1); press(3);    // the right code: OPEN for three cycles
    repeat (4) @(negedge clk);
    key = 4'd5;
    #1 key_valid = 1'b1;
    #1 $display("t=%3d  pulse on key_valid: decoded accept %b, registered accept %b", $time, dec_accept, reg_accept);
    #1 key_valid = 1'b0;
    #1 $display("t=%3d  pulse over:         decoded accept %b, registered accept %b", $time, dec_accept, reg_accept);
    @(negedge clk);
    $display("t=%3d  done: decoded state %s, registered state %s", $time, u_dec.state_q.name(),
             u_reg.state_q.name());
    $finish;
  end
endmodule

Commands · hdl/glitch_session.py

from session import run

# The decoded and the registered lock side by side: a wrong code, the right code, and a pulse on key_valid.
run("""
iverilog -g2012 -Wall -o glitch.vvp tb_glitch.sv door_lock.sv
vvp -n glitch.vvp
""")

Output

# Icarus Verilog 13.0 (stable)
$ iverilog -g2012 -Wall -o glitch.vvp tb_glitch.sv door_lock.sv
door_lock.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
door_lock.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
$ vvp -n glitch.vvp
t= 66  decoded unlock, delayed wires: 1   (state BAD3)
t= 68  decoded unlock, delayed wires: 0   (state BAD3)
t=165  registered unlock:             1   (state OPEN)
t=166  decoded unlock, delayed wires: 1   (state OPEN)
t=195  registered unlock:             0   (state IDLE)
t=196  decoded unlock, delayed wires: 0   (state IDLE)
t=222  pulse on key_valid: decoded accept 1, registered accept 0
t=224  pulse over:         decoded accept 0, registered accept 0
t=230  done: decoded state IDLE, registered state IDLE
tb_glitch.sv:51: $finish called at 230 (1s)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 glitch_session.py

The decoded unlock read 1 from t = 66 to t = 68, in state BAD3, after a wrong code. The enum’s codes put BAD2 at 0101 and BAD3 at 0110, so on that edge bit 1 rose while bit 0 fell, and for 2 time units the decoder saw 0111, the code of OPEN. That short pulse is a glitch: A short, unwanted pulse on the output of combinational logic while its inputs change at slightly different times. It is over before the next clock edge, so it harms only logic that uses the signal without that clock. More in “Counters, LFSRs, PWM and clock enables”. Step through the edge:

Interactive · step by step A decoded output glitching after a wrong code Step through one clock edge of the wrong code and compare the decoded unlock with the registered one.
A decoded output glitching after a wrong code, step 6 of 6. Showing state_q (BAD3 0110), next-state logic, wires of unequal delay (bit 0 arrived too), unlock_q flip-flop (holds 0), decoder state == OPEN (sees 0110), unlock outputs (decoded 0, registered 0). In focus: unlock_q flip-flop, unlock outputs.state_qBAD3 0110next-state logicstate_d = BAD3wires of unequal delaybit 0 arrived toounlock_q flip-flopholds 0decoder state == OPENsees 0110unlock outputsdecoded 0, registered 0

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Two digits in. The lock has taken 4 and 9 of a wrong code, so state_q is BAD2, code 0101. The decoder sees 0101 and says 0.
  2. The next state. The third key is 1, and the next-state logic has chosen BAD3, code 0110. state_d == OPEN is false, so the flip-flop's input is 0.
  3. The edge at t = 65. The register loads 0110. Inside it, bit 1 rises and bit 0 falls at the same moment, and the flip-flop loads its 0.
  4. t = 66. Bits 3 to 1 have reached the decoder and bit 0 has not, so it sees 0111, the code of OPEN, and its output goes to 1.
  5. t = 68. Bit 0 arrives, the decoder sees 0110 and says 0 again. The decoded unlock was 1 for 2 time units; nothing in the RTL ever was OPEN.
  6. The registered output. The flip-flop changes only at an edge, so its output never moved. At the right code it rose at t = 165, one unit before the decoder's.

A registered output: An output taken straight from a flip-flop, so it changes once per clock edge and cannot glitch. Loaded from the decode of the next state, it keeps the timing of a decoded Moore output. cannot do that: unlock_q is a flip-flop loaded at each edge with state_d == OPEN, the next state’s decode, so it changes once per edge. It rose at t = 165, at the very edge that made the state OPEN, one unit before the decoded unlock, and the registered accept stayed 0 through the pulse on key_valid. Loaded from the next state, a registered Moore output keeps its timing:

Registering the present state's decode

Wrong: unlock_q <= (state_q == OPEN). That flip-flop takes OPEN one edge after the state does, so the output is a cycle late. Right: unlock_q <= (state_d == OPEN), which takes it at the same edge as the state.

A registered Mealy output cannot keep its timing: accept_q is accept_d one edge later, so it comes a cycle after its key. The extra flip-flops have a price too, which the next section counts.

Key idea

Load an output’s flip-flop with the decode of the next state: it changes in the same cycle as the decoded output would, and it cannot glitch, because a flip-flop changes only at an edge.

Checkpoint

Check: Registered outputs

  1. Question 1 of 2 Why did the decoded unlock read 1 for 2 time units after the wrong code 4 9 1 3?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: On the edge from BAD2, 0101, to BAD3, 0110, the new bit 1 reached the decoder before the new bit 0, so it saw 0111, the code of OPEN

    A decoder of several state bits can glitch whenever more than one bit changes on an edge and their arrival times differ, even though both states it moves between are innocent.

  2. Question 2 of 2 The registered lock loads unlock_q <= (state_d == OPEN). Compared with the decoded unlock_o, what does it cost?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A flip-flop and its logic, but no cycle; it rose at t = 165, at the edge that made the state OPEN

    Loading an output from the next state gives it the timing of a decoded Moore output and the cleanness of a flip-flop. A registered Mealy output is different: accept_q came a cycle after its key.

Two processes or one: the same machine, the same model

door_lock.sv is in the two-process style: Writing a block as one always_comb that computes each register's next value, the _d signals, from the registers and the inputs, and one always_ff that loads those values into the registers, the _q signals, at the clock edge. More in “RTL coding style that scales”: the next state is decided in always_comb and stored in always_ff. door_lock_1p.sv is the same lock in the one-process style: Writing a state machine as a single always_ff that decides the next state and stores it in the same statement; the outputs it assigns become flip-flops, set and cleared on the arcs that need them.: a single always_ff decides the next state and stores it in the same statement. Its outputs become flip-flops of their own, set on every arc into OPEN or LOCKOUT and cleared on every arc out, and its failure branch is written twice, once in OK3 and once in BAD3. accept_o has to stay outside the clocked block, in an always_comb of its own, because it answers in the key’s own cycle.

The model of the first section writes 2,000 key sequences from a seeded generator, with keys during OPEN and LOCKOUT that must be ignored and a few resets in the middle of a code, together with the outputs it expects in every cycle. tb_door_lock.sv plays them into the three builds, the two-process lock, the same with registered outputs and the one-process lock, on both simulators:

Three builds of the lock against 2,000 seeded key sequences

hdl/door_lock_1p.sv

// The keypad lock of door_lock.sv in one-process style: one always_ff decides the next state and stores it, and the
// Moore outputs are flip-flops of their own, set on every arc into their state and cleared on every arc out of it.
// The failure branch is written twice, once for each state that takes the fourth digit of a wrong code. accept_o
// answers in the key's own cycle, so it cannot come from the clocked block.
module door_lock_1p #(
  parameter logic [15:0] Code      = 16'h5913,
  parameter logic [3:0]  OpenTicks = 4'd3,
  parameter logic [3:0]  LockTicks = 4'd8
) (
  input  logic       clk_i,
  input  logic       rst_ni,
  input  logic       key_valid_i,
  input  logic [3:0] key_i,
  output logic       accept_o,
  output logic       unlock_o,
  output logic       lockout_o
);
  typedef enum logic [3:0] {IDLE, OK1, OK2, OK3, BAD1, BAD2, BAD3, OPEN, LOCKOUT} state_t;

  state_t     state_q;
  logic [1:0] fails_q;
  logic [3:0] timer_q;
  logic       unlock_q, lockout_q;

  always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
      state_q   <= IDLE;
      fails_q   <= '0;
      timer_q   <= '0;
      unlock_q  <= 1'b0;
      lockout_q <= 1'b0;
    end else begin
      unique case (state_q)
        IDLE: if (key_valid_i) begin
          if (key_i == Code[15:12]) state_q <= OK1;
          else                      state_q <= BAD1;
        end
        OK1: if (key_valid_i) begin
          if (key_i == Code[11:8]) state_q <= OK2;
          else                     state_q <= BAD2;
        end
        OK2: if (key_valid_i) begin
          if (key_i == Code[7:4]) state_q <= OK3;
          else                    state_q <= BAD3;
        end
        OK3: if (key_valid_i) begin
          if (key_i == Code[3:0]) begin
            state_q  <= OPEN;
            unlock_q <= 1'b1;
            fails_q  <= '0;
            timer_q  <= OpenTicks - 4'd1;
          end else if (fails_q == 2'd2) begin   // the third wrong code in a row
            state_q   <= LOCKOUT;
            lockout_q <= 1'b1;
            fails_q   <= '0;
            timer_q   <= LockTicks - 4'd1;
          end else begin
            state_q <= IDLE;
            fails_q <= fails_q + 2'd1;
          end
        end
        BAD1: if (key_valid_i) state_q <= BAD2;
        BAD2: if (key_valid_i) state_q <= BAD3;
        BAD3: if (key_valid_i) begin
          if (fails_q == 2'd2) begin
            state_q   <= LOCKOUT;
            lockout_q <= 1'b1;
            fails_q   <= '0;
            timer_q   <= LockTicks - 4'd1;
          end else begin
            state_q <= IDLE;
            fails_q <= fails_q + 2'd1;
          end
        end
        OPEN, LOCKOUT: begin
          if (timer_q == '0) begin
            state_q   <= IDLE;
            unlock_q  <= 1'b0;
            lockout_q <= 1'b0;
          end else begin
            timer_q <= timer_q - 4'd1;
          end
        end
        default: begin               // codes 9 to 15 name no state
          state_q   <= IDLE;
          unlock_q  <= 1'b0;
          lockout_q <= 1'b0;
        end
      endcase
    end
  end

  // The Mealy output, outside the clocked block: the key of this cycle is taken in any state that reads keys.
  always_comb begin
    unique case (state_q)
      IDLE, OK1, OK2, OK3, BAD1, BAD2, BAD3: accept_o = key_valid_i;
      default:                               accept_o = 1'b0;
    endcase
  end

  assign unlock_o  = unlock_q;
  assign lockout_o = lockout_q;
endmodule

hdl/tb_door_lock.sv

// Plays the model's vectors into three builds of the lock, one line per clock cycle: the two-process lock, the same
// lock with registered outputs, and the one-process lock. It counts the cycles in which a build's outputs differ from
// what the model expects. A registered accept_o comes one cycle late, so it is compared with the model's accept of the
// cycle before.
module tb_door_lock;
  logic       clk = 1'b0, rst_n = 1'b0, key_valid = 1'b0;
  logic [3:0] key = '0;
  logic [2:0] two, reg_out, one;               // accept, unlock, lockout of each build
  logic [8:0] v;                               // rst_n, key_valid, key, then the expected accept, unlock, lockout
  logic       last_accept = 1'b0;
  int         fd, cycles = 0, keys = 0, bad_two = 0, bad_reg = 0, bad_one = 0;

  door_lock                          u_two (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
                                            .accept_o(two[2]), .unlock_o(two[1]), .lockout_o(two[0]));
  door_lock #(.RegisteredOutputs(1)) u_reg (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
                                            .accept_o(reg_out[2]), .unlock_o(reg_out[1]), .lockout_o(reg_out[0]));
  door_lock_1p                       u_one (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
                                            .accept_o(one[2]), .unlock_o(one[1]), .lockout_o(one[0]));

  always #5 clk = ~clk;                        // rising edges at 5, 15, 25 ...

  initial begin
    fd = $fopen("vectors.hex", "r");
    repeat (2) @(negedge clk);                 // power-on reset over two edges
    while ($fscanf(fd, "%h\n", v) == 1) begin
      {rst_n, key_valid, key} = v[8:3];        // new inputs just after a falling edge
      #4;                                      // compare just before the next rising edge
      if (two !== v[2:0]) bad_two++;
      if (one !== v[2:0]) bad_one++;
      if (reg_out !== {rst_n && last_accept, v[1:0]}) bad_reg++;
      last_accept = v[2];
      cycles++;
      keys += key_valid;
      @(negedge clk);
    end
    $display("%0d cycles, %0d key presses; cycles that differ from the model:", cycles, keys);
    $display("  two-process %0d, registered outputs %0d, one-process %0d", bad_two, bad_reg, bad_one);
    $finish;
  end
endmodule

Commands · hdl/model_session.py

from session import run

# The model writes the vectors of 2,000 key sequences; the three builds of the lock run them on Icarus Verilog,
# then on Verilator. lock_model.py here is the same file as ../model/lock_model.py, which the page runs in the browser:
# a session runs in a copy of its own folder.
run("""
python3 lock_model.py --vectors vectors.hex
iverilog -g2012 -Wall -o sim.vvp tb_door_lock.sv door_lock.sv door_lock_1p.sv
vvp -n sim.vvp
verilator --binary -j 0 --quiet --top-module tb_door_lock tb_door_lock.sv door_lock.sv door_lock_1p.sv
obj_dir/Vtb_door_lock +verilator+quiet
""")

Output

# Icarus Verilog 13.0 (stable) · Verilator 5.052
$ python3 lock_model.py --vectors vectors.hex
2000 key sequences, seed 1: 18307 cycles, opened 370 times, locked out 367 times, 32 resets
$ iverilog -g2012 -Wall -o sim.vvp tb_door_lock.sv door_lock.sv door_lock_1p.sv
door_lock.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
door_lock.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
door_lock_1p.sv:33: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
door_lock_1p.sv:95: vvp.tgt sorry: Case unique/unique0 qualities are ignored.
$ vvp -n sim.vvp
18307 cycles, 8000 key presses; cycles that differ from the model:
  two-process 0, registered outputs 0, one-process 0
tb_door_lock.sv:38: $finish called at 183090 (1s)
$ verilator --binary -j 0 --quiet --top-module tb_door_lock tb_door_lock.sv door_lock.sv door_lock_1p.sv
$ obj_dir/Vtb_door_lock +verilator+quiet
18307 cycles, 8000 key presses; cycles that differ from the model:
  two-process 0, registered outputs 0, one-process 0
- tb_door_lock.sv:38: Verilog $finish

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 model_session.py

All three matched the model in all 18,307 cycles and 8,000 key presses, on Icarus Verilog and on Verilator, the registered build compared with the model’s accept of the cycle before. The lock opened 370 times and locked out 367 times. Synthesis tells the styles apart where simulation cannot:

What Yosys finds in each style, and what each build costs Python · hdl/fsm_session.py
from session import run

# What Yosys finds in each style: its FSM passes on their own, then a whole synthesis of each build with its cell
# counts. grep keeps the lines about the machine and the totals.
run("""
yosys -q -p "read_verilog -sv door_lock.sv; proc; opt -nodffe -nosdff; tee -q -o two_fsm.txt fsm -nomap"
yosys -q -p "read_verilog -sv door_lock_1p.sv; proc; opt -nodffe -nosdff; tee -q -o one_fsm.txt fsm -nomap"
grep -H -E "Found FSM|reset state|encoding to|state bits" two_fsm.txt one_fsm.txt
yosys -q -p "read_verilog -sv door_lock.sv; synth -top door_lock; tee -q -o two.txt stat"
yosys -q -p "read_verilog -sv door_lock.sv; chparam -set RegisteredOutputs 1 door_lock; synth -top door_lock; tee -q -o reg.txt stat"
yosys -q -p "read_verilog -sv door_lock_1p.sv; synth -top door_lock_1p; tee -q -o one.txt stat"
grep -H -E "cells|DFF" two.txt reg.txt one.txt
""")

Output

# Yosys 0.69
$ yosys -q -p "read_verilog -sv door_lock.sv; proc; opt -nodffe -nosdff; tee -q -o two_fsm.txt fsm -nomap"
$ yosys -q -p "read_verilog -sv door_lock_1p.sv; proc; opt -nodffe -nosdff; tee -q -o one_fsm.txt fsm -nomap"
$ grep -H -E "Found FSM|reset state|encoding to|state bits" two_fsm.txt one_fsm.txt
two_fsm.txt:Found FSM state register door_lock.state_q.
two_fsm.txt:  found reset state: 4'0000 (from async reset)
two_fsm.txt:  mapping auto encoding to `one-hot` for this FSM.
two_fsm.txt:  Number of state bits:       9
one_fsm.txt:Found FSM state register door_lock_1p.state_q.
one_fsm.txt:  found reset state: 4'0000 (from async reset)
one_fsm.txt:  mapping auto encoding to `one-hot` for this FSM.
one_fsm.txt:  Number of state bits:       9
$ yosys -q -p "read_verilog -sv door_lock.sv; synth -top door_lock; tee -q -o two.txt stat"
$ yosys -q -p "read_verilog -sv door_lock.sv; chparam -set RegisteredOutputs 1 door_lock; synth -top door_lock; tee -q -o reg.txt stat"
$ yosys -q -p "read_verilog -sv door_lock_1p.sv; synth -top door_lock_1p; tee -q -o one.txt stat"
$ grep -H -E "cells|DFF" two.txt reg.txt one.txt
two.txt:      100 cells
two.txt:        2   $_DFFE_PN0P_
two.txt:       12   $_DFF_PN0_
two.txt:        1   $_DFF_PN1_
reg.txt:      122 cells
reg.txt:        2   $_DFFE_PN0P_
reg.txt:       15   $_DFF_PN0_
reg.txt:        1   $_DFF_PN1_
one.txt:      114 cells
one.txt:        8   $_DFFE_PN0P_
one.txt:        8   $_DFF_PN0_
one.txt:        1   $_DFF_PN1_

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fsm_session.py

Yosys found the machine in both styles, with IDLE, 0000, as its reset state, and rebuilt it with one flip-flop per state, 9 in all, which the encoding lesson of this module measures. The two-process lock took 100 cells and 15 flip-flops, the registered build 122 and 18, the one-process lock 114 and 17: two more flip-flops for its outputs. The table answers back when you tell it about your outputs:

Interactive · answer and compare Which way to write this machine? Answer two questions about your outputs; the table marks the styles that fit and says why.
Must an output answer in the same cycle as the input that causes it?
Does an output drive something that reacts at once, such as a pin, a latch or another clock?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to write the lock, against what the runs of this lesson showed
Style Glitch-free outputs Answer in the same cycle Cells and flip-flops Few places to slip
Two-process, decoded Outputs come from: state_q, and the keys for accept_o Poor decoded unlock read 1 for 2 units after a wrong code Good accept_o in the key's own cycle Good 100 cells, 15 flip-flops Good every output decided in one place
Two-process, registered Outputs come from: flip-flops loaded from state_d Good every output from a flip-flop Poor accept_o one cycle after the key Mixed 122 cells, 18 flip-flops Good outputs still decided in one place
One-process Outputs come from: flip-flops, plus a block for accept_o Mixed unlock and lockout from flip-flops, accept_o decoded Mixed accept_o needs a block of its own Mixed 114 cells, 17 flip-flops Poor set and clear each output on every arc; failure branch twice
How the answers decide
  • “Must an output answer in the same cycle as the input that causes it?” Yes, like accept_o: rules out Two-process, registered. A registered accept_o arrives a cycle after the key; keep that one output combinational, as the other two do.
  • “Does an output drive something that reacts at once, such as a pin, a latch or another clock?” Yes, it leaves the clock domain: rules out Two-process, decoded and adds a caveat to One-process. A decoded output can pulse while the state bits settle; a pin or a latch sees the pulse. Drive it from a flip-flop.
  • “Must an output answer in the same cycle as the input that causes it?” No, a cycle later is fine and “Does an output drive something that reacts at once, such as a pin, a latch or another clock?” No, only flip-flops on this clock read it: adds a caveat to Two-process, registered and One-process. When only flip-flops on this clock read the outputs, a pulse between edges is never seen, and the extra flip-flops buy nothing.

In short: two-process with decoded outputs is the smallest and keeps every decision in one place; registering the outputs costs a flip-flop each and removes the glitches; the one-process style registers its Moore outputs by construction, at the cost of setting and clearing each of them on every arc.

Checkpoint

Check: Two-process and one-process styles

  1. Question 1 of 2 Why does door_lock_1p.sv compute accept_o in an always_comb of its own, outside its always_ff?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: accept_o must answer in the key's own cycle, and anything assigned in always_ff is a flip-flop that changes only at the next edge

    The one-process style registers everything it assigns. A Mealy output therefore always needs combinational logic next to the clocked block.

  2. Question 2 of 2 Why check the three builds against lock_model.py rather than against a copy of the RTL's own branches?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The model keeps the digits in a list and has no OK or BAD states, so a slip in the RTL's branches is not copied into it

    An independent reference model catches the mistakes a copy of the design would repeat. Here all three builds matched it in all 18,307 cycles.

Every code needs a way back to a known state

state_q has 4 bits for 9 states, so codes 9 to 15 name no state: they are illegal state: A code of a state register that names no state, such as codes 9 to 15 of a 4-bit register that holds nine states. The register can still reach one after an upset or at power-up, so a safe machine leads every such code back to a known state.. The RTL never assigns one, but the register can still hold one, after a particle or a power glitch flips a bit, after a timing failure, or at power-up before the reset arrives. tb_illegal.sv forces code 12 into the register for one clock cycle, releases it, then keys in the right code. It runs door_lock.sv as it is, and once more built with -DNO_RECOVERY, which leaves out the line default: state_d = IDLE;:

Code 12 in the state register, with and without a default branch

hdl/tb_illegal.sv

// Puts the lock's state register into code 12, which names no state, as an upset or a power glitch could, and prints
// the state for the next cycles. Built once as door_lock.sv is, and once with -DNO_RECOVERY, which leaves out its
// default branch. Then the right code is keyed in, to see whether the lock still opens.
module tb_illegal;
  logic       clk = 1'b0, rst_n = 1'b0, key_valid = 1'b0;
  logic [3:0] key = '0;
  logic       accept, unlock, lockout;

  door_lock u_lock (.clk_i(clk), .rst_ni(rst_n), .key_valid_i(key_valid), .key_i(key),
                    .accept_o(accept), .unlock_o(unlock), .lockout_o(lockout));

  always #5 clk = ~clk;                        // rising edges at 5, 15, 25 ...

  task automatic show(input string what);
    string name = u_lock.state_q.name();
    if (name == "") name = "no name";
    $display("t=%3d  state code %0d (%s), unlock %b: %s", $time, u_lock.state_q, name, unlock, what);
  endtask

  task automatic press(input logic [3:0] digit);
    key_valid = 1'b1;
    key       = digit;
    @(negedge clk);
    key_valid = 1'b0;
    @(negedge clk);
  endtask

  initial begin
    repeat (2) @(negedge clk);
    rst_n = 1'b1;
    @(negedge clk);
    show("before");
    force u_lock.state_q = type(u_lock.state_q)'(4'd12);   // no name of the enum has code 12
    @(negedge clk);
    release u_lock.state_q;
    show("forced to 12, released");
    repeat (3) begin
      @(negedge clk);
      show("one edge later");
    end
    press(5); press(9); press(1); press(3);
    show("after keying 5 9 1 3");
    $finish;
  end
endmodule

Commands · hdl/illegal_session.py

from session import run

# The lock forced into code 12, built as door_lock.sv is and once more without its default branch. The testbench's
# force and release write the state register, so Verilator's MULTIDRIVEN warning is turned off for these builds.
run("""
verilator --binary -j 0 --quiet -Wno-MULTIDRIVEN --top-module tb_illegal -Mdir safe tb_illegal.sv door_lock.sv
safe/Vtb_illegal +verilator+quiet
verilator --binary -j 0 --quiet -Wno-MULTIDRIVEN -DNO_RECOVERY --top-module tb_illegal -Mdir trap tb_illegal.sv door_lock.sv
trap/Vtb_illegal +verilator+quiet
""")

Output

# Verilator 5.052
$ verilator --binary -j 0 --quiet -Wno-MULTIDRIVEN --top-module tb_illegal -Mdir safe tb_illegal.sv door_lock.sv
$ safe/Vtb_illegal +verilator+quiet
t= 30  state code 0 (IDLE), unlock 0: before
t= 40  state code 12 (no name), unlock 0: forced to 12, released
t= 50  state code 0 (IDLE), unlock 0: one edge later
t= 60  state code 0 (IDLE), unlock 0: one edge later
t= 70  state code 0 (IDLE), unlock 0: one edge later
t=150  state code 7 (OPEN), unlock 1: after keying 5 9 1 3
- tb_illegal.sv:43: Verilog $finish
$ verilator --binary -j 0 --quiet -Wno-MULTIDRIVEN -DNO_RECOVERY --top-module tb_illegal -Mdir trap tb_illegal.sv door_lock.sv
$ trap/Vtb_illegal +verilator+quiet
t= 30  state code 0 (IDLE), unlock 0: before
t= 40  state code 12 (no name), unlock 0: forced to 12, released
t= 50  state code 12 (no name), unlock 0: one edge later
t= 60  state code 12 (no name), unlock 0: one edge later
t= 70  state code 12 (no name), unlock 0: one edge later
t=150  state code 12 (no name), unlock 0: after keying 5 9 1 3
- tb_illegal.sv:43: Verilog $finish

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 illegal_session.py

With the default branch the lock was back in IDLE one edge later, and the code 5 9 1 3 opened it. Without it the register stayed at 12 for good and the right code did nothing: no branch matched 12, so the opening state_d = state_q kept it. The line that prevents latches had turned an illegal code into a trap. Neither build printed a word about the code that names no state: unique is a promise that the tools may check, nothing checked it here, and the default branch is the recovery. The asynchronous reset also leads every code to IDLE, but only when something pulls it.

Key idea

Give the case of every state machine a default branch that goes to a known state: state_d = state_q alone keeps an illegal code for ever, as the lock without its default kept code 12.

Synthesis can still undo this: when Yosys gives the states new codes, as it did above, the codes 9 to 15 no longer exist in the netlist, and the netlist’s own unused codes have no branch at all. The last lesson of this module reproduces that and keeps the recovery.

Checkpoint

Check: Illegal codes and recovery

  1. Question 1 of 2 Built without its default branch, why did the lock stay at code 12 for good?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No branch matched 12, so the opening state_d = state_q kept it at every edge

    Without a default the case does nothing for an unnamed code, and "stay" keeps it. With the default the lock went to IDLE at the next edge.

  2. Question 2 of 2 The asynchronous reset also takes every code to IDLE. Why does the lock still need its default branch?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The default branch recovers at the next edge on its own; the reset works only when something asserts it

    Recovery should not depend on someone noticing. The default branch makes every code lead to IDLE at the next edge, while the reset remains the way to start from a known state.

Practise: a level-crossing controller

The exercise is a Moore machine of its own: the gate controller of a railway level crossing, with a sensor on each side of the road, a gate with two end switches, a timeout on the gate and a fault state, written as a Python model. Its sample tests drive trains from both sides, a gate that never arrives, a stuck sensor and a state value the controller does not know.

Exercise · Medium · Python

A level-crossing gate controller as a Moore machine

Write the controller of a railway level crossing as a Moore machine in Python. A train sensor sits on each side of the road, some distance away: west and east, each 1 while any part of a train is over it. The gate has a motor and two end switches.

  • Crossing() starts in "UP", with the road open.
  • outputs() returns (down, up, lights, alarm): run the gate motor down, run it up, flash the road lights, sound the fault alarm. It reads the state alone. A state it does not know gives the outputs of "FAULT".
  • clock(west, east, gate_down, gate_up) is one rising clock edge, with the inputs of the cycle before it.

| State | Outputs | At a clock edge | | --- | --- | --- | | "UP" | 0, 0, 0, 0 | "LOWERING" when either sensor is 1; the far side is the other one (east when both are 1) | | "LOWERING" | 1, 0, 1, 0 | "DOWN" when gate_down is 1; otherwise "FAULT" on its 8th edge here | | "DOWN" | 0, 0, 1, 0 | "RAISING" once the far sensor has been 1 and both sensors are 0 | | "RAISING" | 0, 1, 1, 0 | "LOWERING" when either sensor is 1; else "UP" when gate_up is 1; else "FAULT" on its 8th edge here | | "FAULT" | 0, 0, 1, 1 | stays |

Two rules hold in every state. A sensor that has been 1 for 50 edges in a row is stuck, and the edge that makes it 50 goes to "FAULT". Any other value of state goes to "FAULT" at the next edge. The attributes of the starter are there for the far side, the timer and the run of each sensor.

Starter code · crossing.py

class Crossing:
    """The level-crossing controller: a Moore machine, one clock() per clock cycle."""

    def __init__(self):
        self.state = "UP"
        self.far = None          # the sensor the train will leave by: "west" or "east"
        self.seen_far = False    # the far sensor has been 1 since the gate came down
        self.timer = 0           # edges spent in LOWERING or RAISING
        self.high = {"west": 0, "east": 0}   # edges each sensor has been 1 in a row

    def outputs(self):
        """(down, up, lights, alarm), from the state alone."""
        return (0, 0, 0, 0)

    def clock(self, west, east, gate_down, gate_up):
        """One rising clock edge: the next state from the present state and the inputs of this cycle."""
        pass
The sample tests · test_crossing.py
from crossing import Crossing

UP, LOWERING, DOWN, RAISING, FAULT = (0, 0, 0, 0), (1, 0, 1, 0), (0, 0, 1, 0), (0, 1, 1, 0), (0, 0, 1, 1)


def run(c, cycles, west=0, east=0, gate_down=0, gate_up=0):
    """Clocks the controller with the same inputs for some cycles and returns its outputs after the last edge."""
    for _ in range(cycles):
        c.clock(west, east, gate_down, gate_up)
    return c.outputs()


def train_down(c, near):
    """A train reaches the near sensor and the gate comes down."""
    side = {"west": 1} if near == "west" else {"east": 1}
    assert run(c, 1, **side) == LOWERING
    assert run(c, 1, gate_down=1, **side) == DOWN


def test_train_from_west():
    """lowers on the west sensor, waits for the east one to rise and fall, then raises"""
    c = Crossing()
    assert c.outputs() == UP
    train_down(c, "west")
    assert run(c, 3) == DOWN
    assert run(c, 2, east=1) == DOWN
    assert run(c, 1) == RAISING
    assert run(c, 2) == RAISING
    assert run(c, 1, gate_up=1) == UP


def test_train_from_east():
    """treats west as the far side for a train from the east, and ignores the near sensor falling"""
    c = Crossing()
    train_down(c, "east")
    assert run(c, 4) == DOWN
    assert run(c, 3, west=1) == DOWN
    assert run(c, 1) == RAISING


def test_slow_gate_faults():
    """goes to FAULT on the 8th edge in LOWERING without gate_down, and stays there"""
    c = Crossing()
    assert run(c, 1, west=1) == LOWERING
    assert run(c, 7) == LOWERING
    assert run(c, 1) == FAULT
    assert run(c, 3, gate_down=1, gate_up=1) == FAULT


def test_gate_down_on_the_last_edge():
    """still goes DOWN when gate_down comes on the 8th edge in LOWERING"""
    c = Crossing()
    run(c, 1, west=1)
    run(c, 7)
    assert run(c, 1, gate_down=1) == DOWN


def test_stuck_sensor():
    """goes to FAULT when a sensor has been 1 for 50 edges in a row, not before"""
    c = Crossing()
    train_down(c, "west")
    assert run(c, 47, west=1) == DOWN
    assert run(c, 1, west=1) == FAULT


def test_second_train_while_raising():
    """lowers again when a sensor rises while the gate is going up, and stops on a raise that takes too long"""
    c = Crossing()
    train_down(c, "west")
    run(c, 1, east=1)
    assert run(c, 1) == RAISING
    assert run(c, 1, east=1) == LOWERING
    assert run(c, 1, gate_down=1) == DOWN
    run(c, 1, west=1)
    assert run(c, 1) == RAISING
    assert run(c, 7) == RAISING
    assert run(c, 1) == FAULT


def test_unknown_state():
    """treats a state value it does not know as a fault, in its outputs and at the next edge"""
    c = Crossing()
    c.state = "HALF_OPEN"
    assert c.outputs() == FAULT
    assert run(c, 1) == FAULT
    assert c.state == "FAULT"
A hint

Write clock as one if per state, each deciding only for its own state, with "stay" as what happens when no rule fires. Update the two sensor runs first, before the per-state rules. Set the timer to 0 when you enter LOWERING or RAISING, and add one on every edge spent there.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

The self-check covers the blocks of a machine, Moore and Mealy outputs, registered outputs, the two styles and recovery. It starts with the idea you have answered least well and returns to every miss:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which block of door_lock.sv decides the next state?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: The always_comb, which computes state_d from state_q and the inputs

    Two-process style: the next state in always_comb, the register in always_ff, the outputs after them.

  2. Question 2 of 10 A next-state block without the opening state_d = state_q leaves state_d unassigned when OK2 sees no key. What does synthesis build?

    Choose one answer.

    A hint

    Think about what a combinational block must do with a value it does not assign.

    Show the answer: Question 2 of 10

    Answer: A latch, to hold state_d's old value on that path

    Starting from "stay" gives every path a value. Without it, the paths that forget to assign build a latch.

  3. Question 3 of 10 Which output of the lock is a Mealy output?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: accept_o

    An output that reads an input is Mealy; one that reads only the state is Moore.

  4. Question 4 of 10 A designer wants unlock_o to rise in the fourth key's own cycle. What must change, and what does the change risk?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: unlock_o must also decode OK3 with a valid right key, which makes it Mealy, so it follows key_i and key_valid between edges, glitches included

    Answering in the input's own cycle and depending only on the state cannot both hold. An early unlock is a Mealy output, with a Mealy output's glitches.

  5. Question 5 of 10 The registered lock loads accept_q <= accept_d. When does accept_q answer a key?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: In the cycle after the key

    Registering a Mealy output removes its glitches and costs the cycle it saved.

  6. Question 6 of 10 A state machine's output drives the enable pin of an LED driver chip directly. Decoded or registered, and from which state?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Registered, loaded from the next state's decode, so the pin changes once per edge and on time

    Anything outside the clock's reach should be driven from a flip-flop. Loading it with the next state's decode costs a flip-flop and no cycle.

  7. Question 7 of 10 In the one-process style, where is the next state decided?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: In the same always_ff that stores it

    One process decides and stores; its assigned outputs are flip-flops too, which is why it registers its Moore outputs by construction.

  8. Question 8 of 10 Which build of the lock took the most cells, and why?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: The registered two-process build, 122 cells, for its three extra flip-flops and the logic in front of them

    Behaviour and cost are separate questions: all three matched the model, and they differed by 22 cells.

  9. Question 9 of 10 A state register of 4 bits holds 9 named states. How many of its codes name no state?

    Type a number.

    Show the answer: Question 9 of 10

    Answer: 7 codes

    Four bits give 16 codes; 9 have names, so 16 − 9 = 7 do not, codes 9 to 15 for the lock.

  10. Question 10 of 10 Yosys rebuilt the lock with one flip-flop per state, 9 in all. How many codes of that 9-bit register name no state?

    Type a number.

    A hint

    A one-flip-flop-per-state code has exactly one bit set.

    Show the answer: Question 10 of 10

    Answer: 503 codes

    Nine bits give 512 codes, and only the 9 with exactly one bit set name a state: 512 − 9 = 503 have none, and the RTL's default branch was written for codes 9 to 15 of the old register, not for these.

Interview questions

Warm-up (fresher): what is the difference between a Moore and a Mealy output, and when would you choose each?

A Moore output depends only on the state, so it changes once per clock edge, after the state does. A Mealy output depends on the state and the inputs, so it can answer in the same cycle as an input, and it follows every change of that input, glitches included. In this lesson’s lock, unlock_o is Moore and rose at t = 94, a cycle after the fourth key at t = 84, while accept_o is Mealy and was 1 at once, and also pulsed with a 2-unit pulse on key_valid that no edge saw. Choose Moore by default, since it is clean and its timing is simple, and Mealy when an answer must come in the input’s own cycle and only flip-flops on the same clock read it. Registering a Mealy output removes its glitches and costs the cycle it saved.

The harder questions are about outputs that misbehave on a board, a default branch called dead code, and a lockout that a reset clears. Decide how you would begin, then compare with the first step given for each:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A controller drives a board's buzzer straight from a decode of its 3-bit binary state register. On the board the buzzer chirps during some moves between two quiet states, yet no RTL simulation ever enters the buzzer state there. Explain what happens, and what you would change in the RTL and in the verification.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    In a design review a colleague calls the default branch of your state machine's case dead code: the state is a typed enum and every name has an item, so the default can never run. How do you answer, and what would you check in the synthesised netlist?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A keypad lock clears its count of wrong codes on reset, and its reset comes from the power-on circuit. How could someone get around the lockout, and how would you redesign the count and the reset so that the lockout holds?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What three blocks make up a state machine in this track's style?

    A state register in always_ff, next-state logic in always_comb that begins with state_d = state_q so that every path stays by default, and output logic that turns the state into outputs.

  2. How does a Moore output differ from a Mealy output?

    A Moore output depends on the state alone, so it changes only after a clock edge. A Mealy output also depends on the inputs, so it answers in the input's own cycle and follows every change of that input, short pulses included.

  3. Why did the lock's decoded unlock output read 1 for 2 time units after a wrong code?

    Between BAD2, 0101, and BAD3, 0110, bit 1 rose while bit 0 fell, and the bits reached the decoder at different times, so for 2 units it saw 0111, the code of OPEN.

  4. How is a Moore output registered without making it a cycle late?

    Load its flip-flop with the decode of the next state, unlock_q <= (state_d == OPEN). Loading it from state_q == OPEN takes OPEN one edge after the state does.

  5. What does the one-process style cost compared with the two-process style?

    Its outputs become flip-flops that are set on every arc into a state and cleared on every arc out, and shared branches are written twice; the lock took 114 cells against 100.

  6. What happened when code 12 was forced into the lock without a default branch?

    No branch matched 12, so state_d = state_q kept the code for good and the right digits did nothing. With default: state_d = IDLE; the lock was back in IDLE one edge later.

  7. How can a state register come to hold a code that names no state?

    A particle or a power glitch can flip a bit, a timing failure can load a wrong value, and the register can power up with any value before the reset arrives.

Key takeaways

  • A state machine is a state register in always_ff, next-state logic in always_comb that starts with state_d = state_q, and output logic, with the state typed as an enum.
  • A Moore output follows the state, so unlock_o came a cycle after the fourth key; a Mealy output follows the inputs, so accept_o answered at once and also passed on a 2-unit pulse that no edge saw.
  • A decoded output can glitch when several state bits change, as unlock did for 2 units between BAD2 and BAD3; loading a flip-flop with the next state’s decode removes the glitch without losing a cycle.
  • The two-process, registered and one-process locks all matched the model in 18,307 cycles; they cost 100, 122 and 114 cells, and the one-process style sets and clears its outputs on every arc.
  • Codes 9 to 15 of the lock’s register name no state; with a default branch the lock returned from code 12 in one edge, without one it stayed there for good.

Terms in this lesson

finite-state machine (FSM)
Logic that is always in one of a fixed set of states, held in a state register, and moves to its next state at each clock edge according to its present state and its inputs.
illegal state
A code of a state register that names no state, such as codes 9 to 15 of a 4-bit register that holds nine states. The register can still reach one after an upset or at power-up, so a safe machine leads every such code back to a known state.
Mealy machine
A state machine with outputs that depend on its inputs as well as its state, so they can answer in the same cycle as an input, and follow every change of that input, glitches included.
Moore machine
A state machine whose outputs depend only on its present state, so they change only after a clock edge has changed the state.
next-state logic
The combinational part of a state machine that works out the next state from the present state and the inputs. In two-process style it is an always_comb that begins by keeping the present state.
one-process style
Writing a state machine as a single always_ff that decides the next state and stores it in the same statement; the outputs it assigns become flip-flops, set and cleared on the arcs that need them.
registered output
An output taken straight from a flip-flop, so it changes once per clock edge and cannot glitch. Loaded from the decode of the next state, it keeps the timing of a decoded Moore output.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.