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 PASSEDThe log shows five independent
PASSlines: 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 livefloorcounter anddoor_openagainst the floor it requested, any wrong movement or a door opening at the wrong floor would printFAIL. The finalALL TESTS PASSEDconfirms 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 endmodulePASS: 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 PASSEDThe log proves the direction policy, not just reachability. The first
PASSconfirms 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 oftargetagainst the currentfloor. 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
pendingregister 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
aboveandbelow: combinational flags computed by scanning the pending vector for any request strictly above or below the current floor. The machine flipsdir_upat 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, andmoving_downdepend 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 separatealways @(*)block straight from the state register; in Example 2door_openis 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 combinationalalways @(*)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
localparamconstants; 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
forloop overNFto computeabove/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
NFand widen the request vector. For Example 1 you would widenfloorand the priority-encoder fortarget. 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 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