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 PASSED

The 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 with done high at the end. ALL TESTS PASSED therefore proves both the correct stage ordering and the correct per-stage actuator pattern.

This variant adds a pause input and a door_lock output. 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
endmodule
PASS: reached WASH stage
PASS: pause froze state/timer, motor off, door locked
PASS: resumed and reached DONE with door unlocked
ALL TESTS PASSED

The testbench runs the machine into the WASH stage, snapshots the state and timer, then holds pause for 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-bit localparam set 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 load bus and the clocked block latches it into timer. 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 only state. This is what makes it a Moore machine: water_valve is high in FILL and RINSE, motor in WASH, RINSE and SPIN, drain in SPIN, and done in DONE. Because the outputs do not depend on start or 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 pause request must not lose the washer's place, so when it is asserted during an active stage the clocked block simply re-writes state and timer with their current values, freezing the cycle. Resuming continues the exact same countdown.

Safety is handled in the output block: the interlock cuts water_valve, motor and drain whenever pause is high, but it deliberately keeps door_lock asserted. 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 each always @(*) 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 default arm 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 load value 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, localparam state 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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD