Elevator Controller FSM in Verilog: State Machine with Code

An elevator controller is one of the most popular finite state machine (FSM) design problems in digital design and VLSI interviews, because it packs request latching, direction decisions, and clean Moore outputs into a single, intuitive machine. This page gives you working elevator controller Verilog code for a small 4-floor building: the FSM senses which floors have pending requests, moves the cab up or down toward them, and opens the door on arrival.

Both examples below are complete, self-contained Verilog-2005 modules with self-checking testbenches that were compiled and simulated with Icarus Verilog. The first is the clearest idiomatic FSM; the second is a parameterized SCAN (“elevator algorithm”) variant that serves all requests in one direction before reversing.

This is the classic three-process FSM: a latch for floor requests, combinational next-state and target-selection logic, and a sequential block that advances the floor counter. The door-open output is a pure Moore output decoded from the S_DOOR state. The testbench issues five request sequences and checks that the cab reaches each target and asserts door_open at exactly that floor.

module elevator4 (
    input  wire       clk,
    input  wire       rst,       // sync active-high reset
    input  wire [3:0] req,       // per-floor request buttons (level-latched)
    output reg  [1:0] floor,     // current floor 0..3
    output reg        door_open, // Moore output: asserted while servicing
    output reg        moving_up,
    output reg        moving_down
);
    localparam S_IDLE = 2'd0,
               S_UP   = 2'd1,
               S_DOWN = 2'd2,
               S_DOOR = 2'd3;

    reg [1:0] state, nstate;
    reg [3:0] pending;          // latched requests
    reg [1:0] target;

    // request latch + clear on service
    always @(posedge clk) begin
        if (rst)
            pending <= 4'b0;
        else begin
            pending <= pending | req;            // latch new presses
            if (state == S_DOOR)
                pending[floor] <= 1'b0;          // clear the served floor
        end
    end

    // pick lowest pending floor as target (simple priority)
    always @(*) begin
        if      (pending[0]) target = 2'd0;
        else if (pending[1]) target = 2'd1;
        else if (pending[2]) target = 2'd2;
        else if (pending[3]) target = 2'd3;
        else                 target = floor;
    end

    // next-state logic
    always @(*) begin
        nstate = state;
        case (state)
            S_IDLE: begin
                if (|pending) begin
                    if (target > floor)      nstate = S_UP;
                    else if (target < floor) nstate = S_DOWN;
                    else                     nstate = S_DOOR;
                end
            end
            S_UP:   nstate = (floor == target) ? S_DOOR : S_UP;
            S_DOWN: nstate = (floor == target) ? S_DOOR : S_DOWN;
            S_DOOR: nstate = S_IDLE;
            default: nstate = S_IDLE;
        endcase
    end

    // state register + floor counter
    always @(posedge clk) begin
        if (rst) begin
            state <= S_IDLE;
            floor <= 2'd0;
        end else begin
            state <= nstate;
            if (state == S_UP   && floor != target) floor <= floor + 2'd1;
            if (state == S_DOWN && floor != target) floor <= floor - 2'd1;
        end
    end

    // Moore outputs
    always @(*) begin
        door_open   = (state == S_DOOR);
        moving_up   = (state == S_UP);
        moving_down = (state == S_DOWN);
    end
endmodule

module tb;
    reg        clk, rst;
    reg  [3:0] req;
    wire [1:0] floor;
    wire       door_open, moving_up, moving_down;
    integer    cyc;
    integer    errors;

    elevator4 dut(.clk(clk), .rst(rst), .req(req),
                  .floor(floor), .door_open(door_open),
                  .moving_up(moving_up), .moving_down(moving_down));

    always #5 clk = ~clk;

    // drive one request and wait until the door opens, self-check
    task service;
        input [1:0] want;
        begin
            @(negedge clk);
            req = (4'b0001 << want);  // press button for 'want'
            @(negedge clk);
            req = 4'b0000;            // release (request already latched)
            cyc = 0;
            while (!(door_open && floor == want) && cyc < 40) begin
                @(negedge clk);
                cyc = cyc + 1;
            end
            if (door_open && floor == want)
                $display("PASS: reached floor %0d, door_open=%b", floor, door_open);
            else begin
                $display("FAIL: target %0d not served (floor=%0d door=%b)", want, floor, door_open);
                errors = errors + 1;
            end
            // door must not be open at any other floor during this cycle
            @(negedge clk);
        end
    endtask

    initial begin
        clk = 0; rst = 1; req = 0; errors = 0;
        @(negedge clk); @(negedge clk);
        rst = 0;

        service(2'd3);  // go up to top
        service(2'd0);  // come back down
        service(2'd2);  // up to middle
        service(2'd1);  // down one
        service(2'd1);  // same floor request -> immediate door

        if (errors == 0)
            $display("ALL TESTS PASSED");
        else
            $display("TESTS FAILED: %0d", errors);
        $finish;
    end
endmodule
PASS: reached floor 3, door_open=1
PASS: reached floor 0, door_open=1
PASS: reached floor 2, door_open=1
PASS: reached floor 1, door_open=1
PASS: reached floor 1, door_open=1
ALL TESTS PASSED

The log shows five independent PASS lines: the cab is driven up to floor 3, back down to 0, up to 2, down to 1, and a same-floor request that opens the door immediately. Because the testbench compares the live floor counter and door_open against the floor it requested, any wrong movement or a door opening at the wrong floor would print FAIL. The final ALL TESTS PASSED confirms every target was serviced correctly.

This variant implements the SCAN ("elevator algorithm") direction policy and is parameterized by the number of floors (NF). Instead of jumping to the nearest request, it keeps travelling in one direction, stopping at every pending floor in order, and only reverses when nothing remains ahead. The testbench presses floors 1 and 3 together and verifies they are serviced in ascending order, then forces a downward reversal.

// SCAN ("elevator algorithm") controller: keep going in one direction,
// opening the door at every pending floor, until none remain ahead,
// then reverse. Parameterized number of floors.
module elevator_scan #(
    parameter NF = 4                  // number of floors (power-of-two friendly)
)(
    input  wire                 clk,
    input  wire                 rst,
    input  wire [NF-1:0]        req,  // level-latched floor requests
    output reg  [31:0]          floor,
    output reg                  door_open,
    output reg                  dir_up // current scan direction
);
    localparam S_IDLE = 2'd0, S_MOVE = 2'd1, S_DOOR = 2'd2;

    reg [1:0]      state;
    reg [NF-1:0]   pending;
    integer        i;
    reg            above, below;      // pending requests above/below current floor

    // are there pending requests strictly above / below current floor,
    // and is the current floor itself pending?
    always @(*) begin
        above = 1'b0;
        below = 1'b0;
        for (i = 0; i < NF; i = i + 1) begin
            if (pending[i] && i > floor) above = 1'b1;
            if (pending[i] && i < floor) below = 1'b1;
        end
    end

    always @(posedge clk) begin
        if (rst) begin
            state     <= S_IDLE;
            floor     <= 32'd0;
            dir_up    <= 1'b1;
            door_open <= 1'b0;
            pending   <= {NF{1'b0}};
        end else begin
            pending <= pending | req;          // latch
            door_open <= 1'b0;
            case (state)
                S_IDLE: begin
                    if (pending[floor]) state <= S_DOOR;
                    else if (above)  begin dir_up <= 1'b1; state <= S_MOVE; end
                    else if (below)  begin dir_up <= 1'b0; state <= S_MOVE; end
                end
                S_MOVE: begin
                    if (dir_up) floor <= floor + 32'd1;
                    else        floor <= floor - 32'd1;
                    state <= S_IDLE;           // re-evaluate at the new floor
                end
                S_DOOR: begin
                    door_open      <= 1'b1;    // Moore: pulse while serving
                    pending[floor] <= 1'b0;    // clear served floor
                    // decide whether to keep direction or flip
                    if (dir_up && !above && below)      dir_up <= 1'b0;
                    else if (!dir_up && !below && above) dir_up <= 1'b1;
                    state <= S_IDLE;
                end
                default: state <= S_IDLE;
            endcase
        end
    end
endmodule

module tb;
    localparam NF = 4;
    reg              clk, rst;
    reg  [NF-1:0]    req;
    wire [31:0]      floor;
    wire             door_open, dir_up;

    integer k, guard, errors;
    reg [NF-1:0] served;     // floors whose door we observed open

    elevator_scan #(.NF(NF)) dut(.clk(clk), .rst(rst), .req(req),
                                 .floor(floor), .door_open(door_open),
                                 .dir_up(dir_up));
    always #5 clk = ~clk;

    // record every floor at which the door opens
    always @(posedge clk)
        if (door_open) served[floor[1:0]] <= 1'b1;

    initial begin
        clk = 0; rst = 1; req = 0; errors = 0; served = 0;
        @(negedge clk); @(negedge clk);
        rst = 0;

        // Press floors 1 and 3 together while sitting at 0.
        // SCAN must serve 1 THEN 3 going up (ascending order), not 3 then 1.
        @(negedge clk);
        req = 4'b1010;          // floors 3 and 1
        @(negedge clk);
        req = 4'b0000;

        // run and check ascending service order: 1 served before 3
        guard = 0;
        begin : wait1
            while (guard < 60) begin
                @(negedge clk);
                guard = guard + 1;
                if (door_open && floor == 32'd1) begin
                    if (served[3]) begin
                        $display("FAIL: floor 3 served before floor 1 (bad SCAN order)");
                        errors = errors + 1;
                    end else
                        $display("PASS: floor 1 served first (ascending), served[3]=%b", served[3]);
                    disable wait1;
                end
            end
            if (guard >= 60) begin
                $display("FAIL: floor 1 never served");
                errors = errors + 1;
            end
        end

        // let it finish serving floor 3
        guard = 0;
        while (!(served[3]) && guard < 60) begin @(negedge clk); guard = guard + 1; end
        if (served[1] && served[3])
            $display("PASS: both requested floors serviced (1 and 3)");
        else begin
            $display("FAIL: not all floors serviced served=%b", served);
            errors = errors + 1;
        end

        // Now request a lower floor to force a direction reversal (down scan).
        served = 0;
        @(negedge clk);
        req = 4'b0001;          // floor 0, below current top
        @(negedge clk);
        req = 4'b0000;
        guard = 0;
        while (!(door_open && floor == 32'd0) && guard < 60) begin
            @(negedge clk); guard = guard + 1;
        end
        if (door_open && floor == 32'd0)
            $display("PASS: reversed direction and served floor 0, dir_up=%b", dir_up);
        else begin
            $display("FAIL: floor 0 not served after reversal");
            errors = errors + 1;
        end

        if (errors == 0) $display("ALL TESTS PASSED");
        else             $display("TESTS FAILED: %0d", errors);
        $finish;
    end
endmodule
PASS: floor 1 served first (ascending), served[3]=0
PASS: both requested floors serviced (1 and 3)
PASS: reversed direction and served floor 0, dir_up=0
ALL TESTS PASSED

The log proves the direction policy, not just reachability. The first PASS confirms floor 1 opens its door before floor 3 (correct ascending SCAN order) rather than serving the nearer-to-top request first. The second confirms both requested floors were serviced, and the third confirms the cab reversed direction (dir_up=0) to serve floor 0 below it. A naive nearest-request controller would fail the ordering check.

State diagram

The idiomatic 4-floor controller uses four states:

  • IDLE — cab is parked; if any request is pending it chooses a target and decides to move up, move down, or (if already there) open the door.
  • MOVING_UP / MOVING_DOWN — the floor counter increments or decrements once per clock until it equals the target.
  • DOOR_OPEN — the cab has arrived; the door asserts for one service cycle and the served floor is cleared from the pending set, then the machine returns to IDLE to look for the next request.

Transitions are driven by two things: whether any request is pending (|pending) and the comparison of target against the current floor. Keeping the counter and the state in the same clocked block makes the timing easy to reason about: the cab moves exactly one floor per clock edge while in a MOVING state.

Request latching

Floor buttons are momentary — a passenger presses once, but the request must persist until served. The controller keeps a pending register and ORs in new presses every cycle (pending <= pending | req). The bit for a floor is cleared only when that floor is actually serviced in the DOOR state. This is the single most common source of bugs in elevator code: if you forget to latch, a request that arrives for one cycle is lost; if you forget to clear, the cab oscillates on a floor forever.

In the examples the testbench presses a button for one cycle and then releases it, specifically to prove the latch works — the cab still travels to a floor whose button is no longer held.

Direction policy and the SCAN idea

Example 1 uses a simple lowest-floor-first priority: among pending requests it picks the lowest-numbered floor as the target. That is easy to verify but can be unfair — a request just behind the cab's direction of travel waits for a full round trip.

Example 2 implements SCAN, the algorithm real elevators approximate. The cab continues in its current direction, opening the door at every pending floor it passes, and reverses only when there are no more requests ahead of it. The key signals are above and below: combinational flags computed by scanning the pending vector for any request strictly above or below the current floor. The machine flips dir_up at a stop only when nothing remains in the current direction but something waits in the other.

Why SCAN is fairer

SCAN bounds the worst-case wait to roughly two full sweeps of the building, whereas nearest-request scheduling can starve a floor indefinitely under a steady stream of closer requests. This trade-off between latency and fairness is exactly the kind of discussion interviewers look for.

Moore outputs

Both designs use Moore outputs: door_open, moving_up, and moving_down depend only on the current state, not directly on the inputs. This is the safe choice for an elevator because the outputs drive physical actuators (a door motor, a hoist motor) — you never want a glitch on a combinational input to momentarily open a door mid-travel. In Example 1 the outputs are decoded in a separate always @(*) block straight from the state register; in Example 2 door_open is registered so it emits a clean one-cycle pulse at each stop.

Mealy outputs (which also depend on inputs) could save a state or a cycle, but they expose the actuators to input glitches and make the timing harder to close. For a controller like this, Moore is the correct engineering answer.

Common mistakes and avoiding inferred latches

  • Inferred latches in combinational blocks. In the always @(*) next-state and target logic, every output must be assigned on every path. Both examples set a default first (nstate = state;, target = floor;) so no branch can leave a signal unassigned — that is what prevents the synthesizer from inferring an unwanted latch.
  • Forgetting to clear a served request — the cab parks on a floor and keeps re-opening the door. Clear pending[floor] in the DOOR state.
  • Blocking vs non-blocking assignments. Use non-blocking (<=) for all state and counter updates inside clocked blocks, and blocking (=) only in combinational always @(*) blocks. Mixing them is a classic cause of simulation-vs-synthesis mismatch.
  • Counter under/overflow. Only increment when floor != target (Example 1) or when a request genuinely exists ahead (Example 2), so the counter never walks past the top or bottom floor.

Synthesis notes

Both modules are fully synthesizable RTL. A few practical points:

  • The state encoding uses localparam constants; most tools will re-encode (one-hot or gray) during synthesis unless you constrain it. One-hot is often best for FPGAs.
  • Example 2 uses a for loop over NF to compute above/below. This is a static, bounded loop that unrolls into a small priority network at elaboration time — it is not a runtime loop and is fully synthesizable.
  • The reset is synchronous and active-high; match it to your target library's preferred reset style if needed.
  • For a real building you would register the button inputs through a two-flop synchronizer, since passengers press asynchronously to the elevator clock.

FAQ

Is this a Mealy or Moore machine?

Both examples are Moore machines — the door and motor outputs are decoded from the state only, which is the safe choice for driving physical actuators.

How do I extend it to more floors?

Example 2 is already parameterized: change NF and widen the request vector. For Example 1 you would widen floor and the priority-encoder for target. The SCAN version scales more cleanly because its logic is written as a loop over all floors.

Why does the door output pulse for only one cycle?

The service state lasts one clock in these models to keep the self-checking deterministic. In a real design you would hold DOOR_OPEN for a programmable door-timer count before returning to IDLE.

How is the code verified?

Each example ships with a self-checking testbench that drives fixed (non-random) stimulus and compares the live outputs against a golden expectation, printing PASS/FAIL. The code was compiled and simulated under Icarus Verilog in strict Verilog-2005 mode.

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