Vending Machine FSM in Verilog: State Machine with Code

A vending machine is the textbook finite state machine (FSM): it accepts coins, tracks a running total, and dispenses a product once enough money has been inserted. This page gives you clean, synthesizable vending machine Verilog code for a classic 20-cent machine that takes 5-cent and 10-cent coins, asserts a dispense signal when the total reaches 20, and returns change on overpayment.

Both examples below are self-checking and were compiled and run under Icarus Verilog. You get a idiomatic three-block Moore design first, then a parameterized accumulator-based Mealy variant that scales to any price. The patterns here are exactly what interviewers probe when they ask you to “design a vending machine FSM in Verilog.”

This is the canonical Moore-style vending machine: four states (0, 5, 10, 15 cents) with separate next-state logic, output logic, and a clocked state register. The dispense pulse is one clock wide, and change asserts only when the final coin overshoots 20 to 25.

// Moore vending machine: product costs 20, coins 5 (coin=01) and 10 (coin=10).
// States track accumulated money 0,5,10,15. On reaching >=20, dispense=1 for
// one cycle; change=1 if the last coin pushed total to 25 (overpay by 5).
module vending(
    input        clk,
    input        rst,       // synchronous active-high reset
    input  [1:0] coin,      // 00=none, 01=5c, 10=10c
    output reg   dispense,
    output reg   change
);
    localparam S0  = 2'd0;  // 0 cents
    localparam S5  = 2'd1;  // 5 cents
    localparam S10 = 2'd2;  // 10 cents
    localparam S15 = 2'd3;  // 15 cents

    reg [1:0] state, next;

    // Next-state logic (Moore): purely combinational
    always @(*) begin
        next = state;
        case (state)
            S0:  if (coin==2'b01) next = S5;  else if (coin==2'b10) next = S10; else next = S0;
            S5:  if (coin==2'b01) next = S10; else if (coin==2'b10) next = S15; else next = S5;
            S10: if (coin==2'b01) next = S15; else if (coin==2'b10) next = S0;  else next = S10; // 10+10=20 -> dispense, back to 0
            S15: if (coin==2'b01) next = S0;  else if (coin==2'b10) next = S0;  else next = S15; // 15+5=20, 15+10=25
            default: next = S0;
        endcase
    end

    // Output logic (Moore outputs depend on current state AND the arriving coin
    // in the reach cycle; implemented as a decode so pulses are one cycle wide).
    always @(*) begin
        dispense = 1'b0;
        change   = 1'b0;
        // dispense asserts in the cycle a coin brings the running total to >=20
        if (state==S10 && coin==2'b10) begin dispense = 1'b1; change = 1'b0; end       // 20
        else if (state==S15 && coin==2'b01) begin dispense = 1'b1; change = 1'b0; end  // 20
        else if (state==S15 && coin==2'b10) begin dispense = 1'b1; change = 1'b1; end  // 25 -> 5 change
    end

    always @(posedge clk) begin
        if (rst) state <= S0;
        else     state <= next;
    end
endmodule

module tb;
    reg clk, rst;
    reg [1:0] coin;
    wire dispense, change;
    integer errors;

    vending dut(.clk(clk), .rst(rst), .coin(coin), .dispense(dispense), .change(change));

    always #5 clk = ~clk;

    // drive one coin for exactly one clock, sample outputs just before the edge
    task feed;
        input [1:0] c;
        input exp_d;
        input exp_c;
        begin
            coin = c;
            #1; // let combinational outputs settle after coin changes
            if (dispense !== exp_d || change !== exp_c) begin
                errors = errors + 1;
                $display("FAIL coin=%b dispense=%b(exp %b) change=%b(exp %b)", c, dispense, exp_d, change, exp_c);
            end else begin
                $display("PASS coin=%b dispense=%b change=%b", c, dispense, change);
            end
            @(posedge clk); // register advances
            #1;
        end
    endtask

    initial begin
        clk = 0; rst = 1; coin = 2'b00; errors = 0;
        @(posedge clk); #1; rst = 0;

        // Sequence A: 5,5,5,5 -> dispense exactly on the 4th coin (20), no change
        $display("-- Seq A: 5,5,5,5 --");
        feed(2'b01,0,0);
        feed(2'b01,0,0);
        feed(2'b01,0,0);
        feed(2'b01,1,0);

        // Sequence B: 10,10 -> dispense on 2nd coin, no change
        $display("-- Seq B: 10,10 --");
        feed(2'b10,0,0);
        feed(2'b10,1,0);

        // Sequence C: 10,5,10 -> 10,15,25 dispense+change on 3rd coin
        $display("-- Seq C: 10,5,10 --");
        feed(2'b10,0,0);
        feed(2'b01,0,0);
        feed(2'b10,1,1);

        // Sequence D: 5,10,5 -> 5,15,20 dispense no change
        $display("-- Seq D: 5,10,5 --");
        feed(2'b01,0,0);
        feed(2'b10,0,0);
        feed(2'b01,1,0);

        if (errors==0) $display("ALL TESTS PASSED");
        else $display("TESTS FAILED: %0d", errors);
        $finish;
    end
endmodule
-- Seq A: 5,5,5,5 --
PASS coin=01 dispense=0 change=0
PASS coin=01 dispense=0 change=0
PASS coin=01 dispense=0 change=0
PASS coin=01 dispense=1 change=0
-- Seq B: 10,10 --
PASS coin=10 dispense=0 change=0
PASS coin=10 dispense=1 change=0
-- Seq C: 10,5,10 --
PASS coin=10 dispense=0 change=0
PASS coin=01 dispense=0 change=0
PASS coin=10 dispense=1 change=1
-- Seq D: 5,10,5 --
PASS coin=01 dispense=0 change=0
PASS coin=10 dispense=0 change=0
PASS coin=01 dispense=1 change=0
ALL TESTS PASSED

The simulation log drives four coin sequences (5-5-5-5, 10-10, 10-5-10, 5-10-5) and self-checks the outputs against the expected result on every coin. Seeing dispense=1 fire exactly once per completed sale, and change=1 only on the 25-cent path, proves the state transitions and output decode are correct.

Instead of enumerating states, this variant keeps a running total in an accumulator register and parameterizes the price and coin values. Outputs are Mealy (they depend on the current total and the arriving coin), so dispense and the numeric change_amt appear in the same cycle the coin lands.

// Parameterized vending machine with a Mealy-style output.
// An accumulator register holds the running total. Coins are given as a 2-bit
// code (00 none, 01 = COIN_A, 10 = COIN_B). When an arriving coin makes the
// total reach or exceed PRICE, dispense pulses high the same cycle (Mealy),
// change_amt reports the overpay, and the accumulator resets for the next sale.
module vending_param #(
    parameter PRICE  = 20,
    parameter COIN_A = 5,
    parameter COIN_B = 10,
    parameter W      = 6     // accumulator width, holds 0..PRICE+max_coin
)(
    input              clk,
    input              rst,
    input      [1:0]   coin,
    output reg         dispense,
    output reg [W-1:0] change_amt
);
    reg [W-1:0] total;
    reg [W-1:0] value;     // decoded coin value (combinational)
    reg [W-1:0] sum;       // total + value (combinational)

    always @(*) begin
        case (coin)
            2'b01:   value = COIN_A[W-1:0];
            2'b10:   value = COIN_B[W-1:0];
            default: value = {W{1'b0}};
        endcase
        sum = total + value;
        // Mealy: outputs are a function of state (total) and input (coin)
        if (sum >= PRICE[W-1:0]) begin
            dispense   = 1'b1;
            change_amt = sum - PRICE[W-1:0];
        end else begin
            dispense   = 1'b0;
            change_amt = {W{1'b0}};
        end
    end

    always @(posedge clk) begin
        if (rst)
            total <= {W{1'b0}};
        else if (sum >= PRICE[W-1:0])
            total <= {W{1'b0}};        // sale complete, start fresh
        else
            total <= sum;              // accumulate
    end
endmodule

module tb;
    reg clk, rst;
    reg [1:0] coin;
    wire dispense;
    wire [5:0] change_amt;
    integer errors, i, model_total, cval;
    reg [1:0] seq [0:7];

    // Golden model mirrors the DUT contract
    task check;
        input [1:0] c;
        reg exp_d;
        reg [5:0] exp_change;
        integer s;
        begin
            case (c)
                2'b01:   cval = 5;
                2'b10:   cval = 10;
                default: cval = 0;
            endcase
            s = model_total + cval;
            coin = c;
            #1;
            if (s >= 20) begin exp_d = 1'b1; exp_change = s - 20; model_total = 0; end
            else         begin exp_d = 1'b0; exp_change = 0;       model_total = s; end
            if (dispense !== exp_d || change_amt !== exp_change) begin
                errors = errors + 1;
                $display("FAIL coin=%b d=%b(exp %b) chg=%0d(exp %0d)", c, dispense, exp_d, change_amt, exp_change);
            end else begin
                $display("PASS coin=%b d=%b chg=%0d", c, dispense, change_amt);
            end
            @(posedge clk); #1;
        end
    endtask

    vending_param dut(.clk(clk), .rst(rst), .coin(coin), .dispense(dispense), .change_amt(change_amt));
    always #5 clk = ~clk;

    initial begin
        clk=0; rst=1; coin=2'b00; errors=0; model_total=0;
        @(posedge clk); #1; rst=0;

        // Deterministic sweep: exercise exact-change and overpay paths
        $display("-- 5,5,5,5 (exact 20) --");
        check(2'b01); check(2'b01); check(2'b01); check(2'b01);
        $display("-- 10,10 (exact 20) --");
        check(2'b10); check(2'b10);
        $display("-- 10,5,10 (25 -> change 5) --");
        check(2'b10); check(2'b01); check(2'b10);
        $display("-- 5,5,10 (20 exact) --");
        check(2'b01); check(2'b01); check(2'b10);
        $display("-- 10,10 then idle then 10,10 --");
        check(2'b10); check(2'b10); check(2'b00); check(2'b10); check(2'b10);

        if (errors==0) $display("ALL TESTS PASSED");
        else $display("TESTS FAILED: %0d", errors);
        $finish;
    end
endmodule
-- 5,5,5,5 (exact 20) --
PASS coin=01 d=0 chg=0
PASS coin=01 d=0 chg=0
PASS coin=01 d=0 chg=0
PASS coin=01 d=1 chg=0
-- 10,10 (exact 20) --
PASS coin=10 d=0 chg=0
PASS coin=10 d=1 chg=0
-- 10,5,10 (25 -> change 5) --
PASS coin=10 d=0 chg=0
PASS coin=01 d=0 chg=0
PASS coin=10 d=1 chg=5
-- 5,5,10 (20 exact) --
PASS coin=01 d=0 chg=0
PASS coin=01 d=0 chg=0
PASS coin=10 d=1 chg=0
-- 10,10 then idle then 10,10 --
PASS coin=10 d=0 chg=0
PASS coin=10 d=1 chg=0
PASS coin=00 d=0 chg=0
PASS coin=10 d=0 chg=0
PASS coin=10 d=1 chg=0
ALL TESTS PASSED

The testbench runs a deterministic sweep covering exact-payment and overpayment paths, including an idle cycle and a back-to-back second sale, and checks every output against an independent golden model computed in the testbench. The reported chg=5 on the 10-5-10 sequence confirms the change arithmetic, and the clean re-arm after each sale confirms the accumulator resets correctly.

State diagram of the vending machine FSM

The Moore design uses four states that encode how much money has been inserted toward the 20-cent product:

  • S0 (0c) → +5 goes to S5, +10 goes to S10
  • S5 (5c) → +5 goes to S10, +10 goes to S15
  • S10 (10c) → +5 goes to S15, +10 reaches 20 so it dispenses and returns to S0
  • S15 (15c) → +5 reaches 20 (dispense), +10 reaches 25 (dispense + change), both return to S0

The three "reach" edges (S10+10, S15+5, S15+10) are the ones that assert dispense. Only the S15+10 edge, which overshoots to 25, also asserts change. Because the machine returns to S0 after every sale, the FSM is cyclic with no dead ends.

Moore vs Mealy for a vending machine

The two examples illustrate the fundamental FSM trade-off:

Moore

Outputs depend only on the current state. In a pure Moore machine the dispense signal would be a property of a dedicated "DISPENSE" state, making it glitch-free and easy to time, but adding one clock of latency. Example 1 uses four money-states and decodes the dispense pulse from the state-plus-incoming-coin so the pulse stays one cycle wide; this is a common practical compromise taught as "Moore-style."

Mealy

Outputs depend on the current state and the current input, so the response appears in the same cycle the triggering coin arrives, as in Example 2. Mealy machines usually need fewer states (the accumulator version needs no explicit state enumeration at all) but their outputs can glitch if inputs are not clean, and they are more sensitive to combinational timing.

For interviews: say Moore is safer and simpler to time; Mealy is faster-reacting and often smaller.

Reset handling

Both designs use a synchronous, active-high reset that forces the money back to zero (S0 / total=0) on the next clock edge. Synchronous reset keeps the reset signal inside the normal timing analysis and avoids the recovery/removal issues of asynchronous reset, which matters on FPGA fabrics that register the reset anyway.

If your target needs asynchronous reset, change the sequential block to always @(posedge clk or posedge rst) and test rst first. Whichever you choose, apply the same reset style consistently across the whole design — mixing sync and async reset on registers in the same clock domain is a classic source of hard-to-find bugs.

Common FSM mistakes

  • Inferred latches. If a combinational always @(*) block does not assign the next-state or every output on every path, the synthesizer infers a latch. Both examples default next = state and clear dispense/change at the top of the block so every branch is covered.
  • Missing default case. Always include a default in the state case to define behavior for unreachable or illegal encodings; this also lets the FSM recover from an upset state rather than locking up.
  • Multi-cycle output pulses. A dispense that stays high for several cycles can dispense multiple products. Decode the pulse so it is exactly one clock wide, and return to S0 immediately after the sale.
  • Mixing blocking and non-blocking assignments. Use non-blocking (<=) in clocked blocks and blocking (=) in combinational blocks. Crossing them causes simulation/synthesis mismatches.
  • Unreachable states. With a 2-bit register you have four encodings and all four are used here; if you add states, make sure each is reachable and that the default maps stray codes back to a safe state.

Synthesis notes

The Moore version synthesizes to a 2-bit state register plus a small amount of combinational logic — tiny and fast. Let the tool choose the state encoding (binary vs one-hot); on FPGAs one-hot is often preferred and most tools do this automatically for FSMs they recognize.

The parameterized version infers an adder and a W-bit accumulator. Keep W wide enough to hold PRICE plus the largest coin so the sum comparison never wraps. Because price and coin values are parameters, you can retarget the same RTL to a 25-cent or 50-cent machine without touching the logic — a good demonstration of parameterized, reusable design.

FAQ

Can I add a coin-return / cancel input? Yes — add an input that forces the state back to S0 (Moore) or clears total (accumulator) and optionally pulses a refund amount equal to the money collected.

Why not use $random in the testbench? Reference pages need deterministic, reproducible logs, so these testbenches use fixed coin vectors checked against a golden model. Random stimulus is great for coverage but is layered on top, not a substitute for directed self-checks.

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