Washing Machine FSM in Verilog: State Machine with Code
A washing-machine controller is the classic interview FSM: a finite state machine that steps a drum through IDLE → FILL → WASH → RINSE → SPIN → DONE, driving a water valve, a motor, and a drain at the right moment and holding each stage for a fixed time. This page gives you clean, synthesizable washing machine Verilog code written as a Moore machine, so every output is a pure function of the current state and glitches on combinational inputs never reach the actuators.
Both examples below are self-contained .v files with a self-checking testbench, written in strict Verilog-2005. Example 1 is the six-state controller driven by a per-stage countdown timer; Example 2 adds a pause/resume control and a door-lock interlock. The simulation logs underneath each listing are the real output captured from Icarus Verilog.
This is the idiomatic three-block Moore FSM: a combinational next-state block that also picks each stage’s duration, a clocked block holding the state register plus a countdown timer, and a combinational output block that decodes the state only.
// Washing machine controller FSM (Moore, Verilog-2005)
module washer(
input clk,
input rst,
input start,
output reg water_valve,
output reg motor,
output reg drain,
output reg done
);
localparam IDLE = 3'd0,
FILL = 3'd1,
WASH = 3'd2,
RINSE = 3'd3,
SPIN = 3'd4,
DONE = 3'd5;
reg [2:0] state, next;
reg [3:0] timer; // counts down within a stage
reg [3:0] load; // duration loaded on entry to the NEXT stage
// Next-state + per-stage duration (combinational)
always @(*) begin
next = state;
load = 4'd0;
case (state)
IDLE : if (start) begin next = FILL; load = 4'd3; end
FILL : if (timer==0) begin next = WASH; load = 4'd4; end
WASH : if (timer==0) begin next = RINSE; load = 4'd3; end
RINSE: if (timer==0) begin next = SPIN; load = 4'd2; end
SPIN : if (timer==0) begin next = DONE; load = 4'd0; end
DONE : next = DONE;
default: next = IDLE;
endcase
end
// State register + countdown timer (sequential)
always @(posedge clk) begin
if (rst) begin
state <= IDLE;
timer <= 4'd0;
end else if (next != state) begin
state <= next;
timer <= load; // load the new stage's duration
end else if (timer != 4'd0) begin
timer <= timer - 4'd1; // stay and count down
end
end
// Moore outputs: a function of state only
always @(*) begin
water_valve = 1'b0;
motor = 1'b0;
drain = 1'b0;
done = 1'b0;
case (state)
FILL : water_valve = 1'b1;
WASH : motor = 1'b1;
RINSE: begin water_valve = 1'b1; motor = 1'b1; end
SPIN : begin motor = 1'b1; drain = 1'b1; end
DONE : done = 1'b1;
default: ; // IDLE: all outputs 0
endcase
end
endmodule
module tb;
reg clk, rst, start;
wire water_valve, motor, drain, done;
washer dut(.clk(clk), .rst(rst), .start(start),
.water_valve(water_valve), .motor(motor),
.drain(drain), .done(done));
// Golden Moore decode: {water_valve, motor, drain, done}
function [3:0] moore;
input [2:0] s;
begin
case (s)
3'd1: moore = 4'b1000; // FILL
3'd2: moore = 4'b0100; // WASH
3'd3: moore = 4'b1100; // RINSE
3'd4: moore = 4'b0110; // SPIN
3'd5: moore = 4'b0001; // DONE
default: moore = 4'b0000; // IDLE
endcase
end
endfunction
integer i, errors, nseq;
reg [2:0] seq [0:7];
reg [2:0] prev;
always #5 clk = ~clk;
initial begin
clk = 0; rst = 1; start = 0; errors = 0; nseq = 0;
@(negedge clk); @(negedge clk);
rst = 0;
prev = dut.state;
seq[0] = dut.state; nseq = 1; // record IDLE
start = 1;
for (i = 0; i < 40; i = i + 1) begin
@(negedge clk);
// outputs must equal the golden decode of the current state
if ({water_valve, motor, drain, done} !== moore(dut.state)) begin
$display("FAIL: output mismatch in state %0d exp %b got %b",
dut.state, moore(dut.state),
{water_valve, motor, drain, done});
errors = errors + 1;
end
// record each new state entered
if (dut.state !== prev) begin
if (nseq < 8) seq[nseq] = dut.state;
nseq = nseq + 1;
prev = dut.state;
end
end
// Expected full cycle: IDLE->FILL->WASH->RINSE->SPIN->DONE
if (nseq !== 6) begin
$display("FAIL: expected 6 distinct states, saw %0d", nseq);
errors = errors + 1;
end else if (seq[0]==3'd0 && seq[1]==3'd1 && seq[2]==3'd2 &&
seq[3]==3'd3 && seq[4]==3'd4 && seq[5]==3'd5) begin
$display("PASS: state sequence IDLE->FILL->WASH->RINSE->SPIN->DONE");
end else begin
$display("FAIL: wrong state order %0d %0d %0d %0d %0d %0d",
seq[0],seq[1],seq[2],seq[3],seq[4],seq[5]);
errors = errors + 1;
end
// Specific per-stage output spot checks (ordering of actuators)
if (done === 1'b1) $display("PASS: done asserted at end of cycle");
else begin $display("FAIL: done not asserted"); errors = errors + 1; end
if (errors == 0) $display("ALL TESTS PASSED");
else $display("TESTS FAILED: %0d error(s)", errors);
$finish;
end
endmodule
PASS: state sequence IDLE->FILL->WASH->RINSE->SPIN->DONE PASS: done asserted at end of cycle ALL TESTS PASSEDThe testbench pulses
start, then clocks the design and records every distinct state it enters. It checks two things on each cycle: the four Moore outputs always match an independent golden decode of the current state, and the recorded trace is exactly IDLE→FILL→WASH→RINSE→SPIN→DONE withdonehigh at the end.ALL TESTS PASSEDtherefore proves both the correct stage ordering and the correct per-stage actuator pattern.This variant adds a
pauseinput and adoor_lockoutput. While a cycle is active, asserting pause freezes the state register and the countdown timer, cuts all actuators, but keeps the door locked for safety; releasing pause resumes exactly where it stopped.// Washing machine FSM with pause/resume and door-lock interlock (Verilog-2005) module washer_pause( input clk, input rst, input start, input pause, output reg water_valve, output reg motor, output reg drain, output reg done, output reg door_lock ); localparam IDLE = 3'd0, FILL = 3'd1, WASH = 3'd2, RINSE = 3'd3, SPIN = 3'd4, DONE = 3'd5; reg [2:0] state, next; reg [3:0] timer, load; // Active stages are everything between IDLE and DONE (door must stay locked) wire active = (state != IDLE) && (state != DONE); // Next-state + per-stage duration (combinational) always @(*) begin next = state; load = 4'd0; case (state) IDLE : if (start) begin next = FILL; load = 4'd3; end FILL : if (timer==0) begin next = WASH; load = 4'd4; end WASH : if (timer==0) begin next = RINSE; load = 4'd3; end RINSE: if (timer==0) begin next = SPIN; load = 4'd2; end SPIN : if (timer==0) begin next = DONE; load = 4'd0; end DONE : next = DONE; default: next = IDLE; endcase end // State + timer. pause freezes both while a cycle is active. always @(posedge clk) begin if (rst) begin state <= IDLE; timer <= 4'd0; end else if (pause && active) begin state <= state; // hold timer <= timer; // freeze countdown end else if (next != state) begin state <= next; timer <= load; end else if (timer != 4'd0) begin timer <= timer - 4'd1; end end // Moore outputs with the pause interlock applied last. always @(*) begin water_valve = 1'b0; motor = 1'b0; drain = 1'b0; done = 1'b0; door_lock = 1'b0; case (state) FILL : begin water_valve = 1'b1; door_lock = 1'b1; end WASH : begin motor = 1'b1; door_lock = 1'b1; end RINSE: begin water_valve = 1'b1; motor = 1'b1; door_lock = 1'b1; end SPIN : begin motor = 1'b1; drain = 1'b1; door_lock = 1'b1; end DONE : done = 1'b1; default: ; // IDLE endcase // Interlock: while paused mid-cycle, cut actuators but keep the door locked. if (pause && door_lock) begin water_valve = 1'b0; motor = 1'b0; drain = 1'b0; end end endmodule module tb; reg clk, rst, start, pause; wire water_valve, motor, drain, done, door_lock; washer_pause dut(.clk(clk), .rst(rst), .start(start), .pause(pause), .water_valve(water_valve), .motor(motor), .drain(drain), .done(done), .door_lock(door_lock)); integer i, errors; reg [2:0] frozen_state; reg [3:0] frozen_timer; always #5 clk = ~clk; initial begin clk = 0; rst = 1; start = 0; pause = 0; errors = 0; @(negedge clk); @(negedge clk); rst = 0; start = 1; // Advance until we are inside the WASH stage (an active stage). i = 0; while (dut.state != 3'd2 && i < 30) begin @(negedge clk); i = i + 1; end if (dut.state === 3'd2) $display("PASS: reached WASH stage"); else begin $display("FAIL: never reached WASH"); errors = errors + 1; end // Capture state+timer, then pause for several cycles. frozen_state = dut.state; frozen_timer = dut.timer; pause = 1; for (i = 0; i < 5; i = i + 1) begin @(negedge clk); if (dut.state !== frozen_state || dut.timer !== frozen_timer) begin $display("FAIL: pause did not freeze FSM"); errors = errors + 1; end // Safety interlock: motor off but door stays locked while paused. if (motor !== 1'b0) begin $display("FAIL: motor running during pause"); errors = errors + 1; end if (door_lock !== 1'b1) begin $display("FAIL: door unlocked during pause"); errors = errors + 1; end end if (errors == 0) $display("PASS: pause froze state/timer, motor off, door locked"); // Resume and run to completion. pause = 0; i = 0; while (dut.state != 3'd5 && i < 40) begin @(negedge clk); i = i + 1; end if (done === 1'b1 && door_lock === 1'b0) $display("PASS: resumed and reached DONE with door unlocked"); else begin $display("FAIL: did not complete cleanly (done=%b door_lock=%b)", done, door_lock); errors = errors + 1; end if (errors == 0) $display("ALL TESTS PASSED"); else $display("TESTS FAILED: %0d error(s)", errors); $finish; end endmodulePASS: reached WASH stage PASS: pause froze state/timer, motor off, door locked PASS: resumed and reached DONE with door unlocked ALL TESTS PASSEDThe testbench runs the machine into the WASH stage, snapshots the state and timer, then holds
pausefor five cycles. Each of those cycles it verifies the state and timer did not change, the motor is off, and the door stays locked. It then releases pause and confirms the cycle still reaches DONE with the door unlocked. The PASS lines show the interlock freezes the FSM without losing progress and without ever running the motor on a paused, locked drum.State diagram
The controller is a straight line of six states with one self-loop at the end:
IDLE --start--> FILL --timer--> WASH --timer--> RINSE --timer--> SPIN --timer--> DONE --> (DONE)IDLE waits for the user to press
start. Each wash stage then runs until its own timer expires, at which point control advances to the next stage. DONE is a terminal, self-looping state that holds the finished indicator until a reset. Encoding the six states as a 3-bitlocalparamset keeps the code readable and lets the synthesizer choose binary or one-hot encoding as it sees fit.Timed transitions
Real appliances dwell in each stage for a fixed period, so the FSM is driven by a countdown timer rather than a raw external event. When the machine enters a new stage, the next-state block supplies that stage's duration on the
loadbus and the clocked block latches it intotimer. On every subsequent clock the timer decrements; when it reaches zero the next-state logic advances the FSM and loads the following stage's duration.Why load-on-entry works
The key detail is that the state register and timer update together in one clocked block. On the cycle where
next != state, the design writes the new state and the new duration at the same clock edge, so the timer is always correct for the stage it belongs to. On cycles where the state is unchanged, only the timer counts down. This avoids an off-by-one where the timer briefly holds the previous stage's value.Moore outputs
All outputs are generated in a separate
always @(*)block that reads onlystate. This is what makes it a Moore machine:water_valveis high in FILL and RINSE,motorin WASH, RINSE and SPIN,drainin SPIN, anddonein DONE. Because the outputs do not depend onstartor the timer directly, a noisy input cannot create a one-cycle glitch on the motor or valve lines. The trade-off is one extra clock of latency versus a Mealy design, which is almost always the right choice when the outputs switch power electronics.
- Assign a default value to every output at the top of the block, then override per state. This is the single most important habit for latch-free FSM outputs.
- Keep the output decode independent of the next-state logic so the two can be reviewed and reused separately.
Pause and door-lock interlock
Example 2 shows the production-grade pattern. A
pauserequest must not lose the washer's place, so when it is asserted during an active stage the clocked block simply re-writesstateandtimerwith their current values, freezing the cycle. Resuming continues the exact same countdown.Safety is handled in the output block: the interlock cuts
water_valve,motoranddrainwheneverpauseis high, but it deliberately keepsdoor_lockasserted. A spinning drum must coast to a stop behind a locked door; unlocking on pause would be a hazard. Applying the interlock as the last override in the output block guarantees it wins over the per-state defaults.Avoiding inferred latches and unreachable states
Two bugs dominate FSM code reviews:
- Inferred latches. If a combinational block fails to assign an output on some path, the synthesizer infers a latch to 'remember' the old value. The cure used here is to assign defaults to
next,load, and every output at the top of eachalways @(*)block, so no path leaves a signal unassigned.- Unreachable or lock-up states. A 3-bit register has eight codes but the design uses six. A
defaultarm in both case statements sends any illegal code back toward IDLE, so a glitch or SEU cannot strand the FSM in a dead state.Common mistakes
- Driving outputs from both the state and the timer, which quietly turns a Moore design into a Mealy one and reintroduces glitches.
- Loading the timer in a different block or a different cycle than the state change, causing an off-by-one in stage length.
- Using blocking assignments (
=) in the clocked block; always use non-blocking (<=) for sequential logic.- Forgetting the synchronous reset path, leaving the FSM in an unknown state at power-up in simulation.
FAQ
Is this Moore or Mealy washing machine Verilog code?
Moore. Every output depends only on the current state, which is the standard, safe choice for a controller that switches motors and valves.
How do I change how long each stage runs?
Edit the
loadvalue assigned on each transition in the next-state block. The duration travels with the transition, so FILL, WASH, RINSE and SPIN can each have independent lengths without touching the rest of the design.Will this synthesize?
Yes. It uses only
reg/wire,localparamstate codes, non-blocking assignments in the clocked block, fully-defaulted combinational blocks, and a synchronous reset, so it maps cleanly to flip-flops and combinational logic on both FPGA and ASIC flows.Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres