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.
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.
// 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:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
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:
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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- 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.
- Two more keys. The keys 9 and 1 go the same way, two cycles apart, and take the state through OK2 to OK3.
- 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.
- 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.
- Locked again. OPEN lasts three cycles; at t = 124 the state is IDLE and unlock_o is 0 again.
- 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.
- 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
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:
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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:
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:
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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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;:
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
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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.
-
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?
-
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?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
- IEEE 1800-2023: IEEE Standard for SystemVerilog, Unified Hardware Design, Specification, and Verification Language (IEEE Standards Association)
- Icarus Verilog documentation: iverilog command line flags (Icarus Verilog)
- Verilator documentation: verilator arguments (--binary, -D, -Mdir, -Wno) (Verilator)
- Verilator documentation: warnings (MULTIDRIVEN) (Verilator)
- Yosys command reference: FSM handling (fsm, fsm_detect, fsm_recode) (YosysHQ)
Report a problem with this lesson
Kept only in this browser. Your Learn progress