Designing a Six-State Elevator Controller for Siemens S7 PLCs

David Krause13 min read
HMI ProgrammingSiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

1. Problem Overview: Single-Car Elevator Control

A single-car elevator is one of the canonical PLC training problems because it exercises the full set of scan-cycle disciplines in a small footprint: input debouncing of mechanical pushbuttons, latched request storage, deterministic state transitions, directional arbitration between conflicting floor calls, and safety interlocks (gate, limit, overspeed) that cannot be allowed to misfire. The control problem is to take a set of floor-request pushbuttons (FR1..FRn) and car-call pushbuttons (CC1..CCn) and produce the coil signals that drive the car motor up, the motor down, the gate motor close, the gate motor open, and the indicator lamps.

For a Siemens S7-300 or S7-400 controller programmed with STEP 7 V5.x (the environment that exposes STL and LAD side by side), the natural decomposition is one organisation block (OB1) that calls a chain of functions (FC) or function blocks (FB). Each function implements exactly one state. The point that trips most first-time builders is the ordering of those states: the state that captures new requests has to run first, not last, regardless of which numeric label is assigned to it on the problem sheet.

This article uses a generic 6-floor single-car configuration. Scale all I/O counts linearly when extending to 8, 12, or 16 floors. Safety-critical hardware paths (door locks, final limits, buffer switches) must remain hardwired per EN 81-20:2014 / EN 81-50:2014, never implemented solely in software.

2. Why the "Waiting" State Must Be State 1, Not State 6

A PLC executes OB1 once per scan. If the request-capture logic is placed at the end of the cycle (or in a state numbered "6" that is only entered after the car has stopped), the program will miss every pushbutton press that occurs while the car is idle. The car will sit motionless even though FR and CC latches are being energised in the field.

Place the request-capture state as the first rung group in OB1. Numerically call it State 1. The number is not cosmetic: it is the execution order.

Bad Practice Correct Practice
Label request capture "State 6" but place it as the last subroutine in OB1 Label it State 1 and place it as the first subroutine in OB1
Number states by perceived importance ("waiting is last") Number states by execution order in the scan
Re-enter request capture only inside a "stopped" branch Re-enter request capture every scan via the call chain
Combine request capture with gate-close logic in one FC Split request capture (FC1) from motion/gate logic (FC2..FC6)

The same principle applies regardless of whether you code in LAD, FBD, or STL: ordering of network execution equals state ordering. There is no implicit re-ordering by the compiler.

3. The Six Required States

For a single-car passenger or car-park elevator the problem statement typically provides six state names. The minimum required set, with execution order, is:

  1. State 1 - Waiting / Catch Floor and Car Requests. Read FR1..FRn and CC1..CCn, latch them into a request word, and remain here until a transition condition (current floor equals requested floor, gate closed, no further outstanding requests) is satisfied.
  2. State 2 - Direction Decision. Compare current floor to the next outstanding request and pick UP, DOWN, or stay. Tie-break when floor is between two requests by scanning the request word upward from the current position.
  3. State 3 - Moving UP (sub-state 3a) or Moving DOWN (sub-state 3b). Energise the up or down contactor, increment or decrement the floor-position word on each floor-mark sensor pulse, and stop when the target floor is reached.
  4. State 4 - Car Gate Closing. Drive the gate-close output with a 3-second on-timer, verify the gate-closed limit switch within 5 seconds, otherwise raise a door-obstruction fault (real elevator controllers must reverse and reopen on obstruction per EN 81-20 §5.3.13).
  5. State 5 - Car Stop / Dwell. Hold the car at floor for a configured dwell time (1.0 to 3.0 seconds typical for passenger elevators, 0.5 seconds for car-park/shuttle elevators).
  6. State 6 - Car Gate Opening. Drive the gate-open output until the gate-open limit is reached, then clear the request bit for the current floor and return to State 1.

States 3, 4, 5, 6 form a loop. State 1 is the entry point from cold start, from a fault-clear, and after every gate-open completion.

4. Program Architecture: OB1, OB100, and Six FCs

STEP 7 V5.x provides three relevant block types for this application:

Block Purpose Example Number
OB1 Main cyclic program. Calls the state FCs in order 1..6. OB1
OB100 Warm restart. Resets all request latches, sets current_floor := 1, sets state := 1. OB100
FC1 State 1: Catch floor and car requests. FC1
FC2 State 2: Direction decision. FC2
FC3 State 3: Motion (UP/DOWN). FC3
FC4 State 4: Gate close. FC4
FC5 State 5: Stop / dwell. FC5
FC6 State 6: Gate open. FC6
DB1 Shared instance data: request_word (DWORD), current_floor (INT), target_floor (INT), direction (BOOL), state (INT), dwell_timer (S5TIME). DB1

OB1 call sequence (LAD, Networks 1..6):

NETWORK 1    // State 1 - Catch Requests
   CALL  FC1   // EN is automatic, no conditions

NETWORK 2    // State 2 - Direction Decision
   U     DB1.State_OK          // latched true once FC1 has read at least one scan
   CALL  FC2

NETWORK 3    // State 3 - Motion
   U     DB1.Motion_Enable
   CALL  FC3

NETWORK 4    // State 4 - Gate Close
   U     DB1.Close_Enable
   CALL  FC4

NETWORK 5    // State 5 - Dwell
   U     DB1.Dwell_Enable
   CALL  FC5

NETWORK 6    // State 6 - Gate Open
   U     DB1.Open_Enable
   CALL  FC6

Note that the request-capture FC1 has no enable contact - it executes every scan. All later FCs are gated by a state-flag bit set inside FC1. This guarantees the scan order: capture first, then act.

5. State 1 Implementation: Catch Floor and Car Requests (FC1)

FC1 has three responsibilities: debounce the pushbuttons, latch the requests, and set the transition flag that hands control to FC2.

LAD network for FR (floor request) capture, repeated per floor n:

NETWORK 1    // FR3 (floor 3 hall-call UP button)
   U     E 0.3          // raw input, FR3
   UN    DB1.FR3_ack    // acknowledge from gate-open state
   S     DB1.FR3        // set latch bit in request word

NETWORK 2    // Car call CC3
   U     E 1.3
   UN    DB1.CC3_ack
   S     DB1.CC3

NETWORK 3    // Aggregate request_word (DWORD bits 0..15)
   L     DB1.FR1
   L     DB1.FR2
   ...
   OW
   T     DB1.RequestWord  // 16-bit consolidated request map

Equivalent STL (shorter, useful when the problem specifies STL):

NETWORK 1
   U     E 0.3
   UN    DB1.FR3_ack
   S     DB1.FR3

NETWORK 10
   L     0
   U     DB1.FR1
   =     DB1.RequestWord.%X0
   U     DB1.FR2
   =     DB1.RequestWord.%X1
   ...

FC1 transition logic (at end of FC1):

// If any request is pending AND current_gate_closed AND no fault
   L     DB1.RequestWord
   L     0
   <>I
   U     E 2.0        // gate-closed limit switch
   U     DB1.NoFault
   S     DB1.State_OK
   S     DB1.Motion_Enable

6. State 2 Implementation: Direction Decision (FC2)

Direction arbitration must avoid two classic bugs: (a) reversing direction at the top or bottom floor on a stale request, and (b) "hunting" between two requests on adjacent floors. The deterministic rule used in most elevator controllers is:

  1. If current_floor equals highest outstanding request and current_floor equals lowest outstanding request, no motion is needed - go directly to State 4 (close gate) then State 6 (open gate) then back to State 1.
  2. Else if target_floor is undefined or has been satisfied, scan RequestWord upward from current_floor. The first bit found above current_floor becomes the new target - direction is UP.
  3. Else if no bits are set above current_floor, scan downward. The first bit found below current_floor becomes the new target - direction is DOWN.

STL scan routine (compact form):

// Scan upward
   L     DB1.current_floor        // INT
   T     DB1.scan_index
UP1: L     DB1.scan_index
   L     6                        // number of floors
   >I                            // scan_index > 6 ?
   JC    UP_DONE
   L     DB1.RequestWord
   L     DB1.scan_index           // index into bit position
   SRW   1                        // shift right by index
   A     DB1.scan_index
   ...
// (implementation depends on STEP 7 version; SCL is much cleaner:
//   FOR i := current_floor+1 TO n DO
//     IF testbit(RequestWord, i) THEN target := i; dir := UP; RETURN; END_IF;
//   END_FOR;
// )
For readability and maintainability in production code, implement FC2 in SCL (Structured Control Language) rather than STL. SCL is supported in STEP 7 V5.x SP1 and later and converts to STL automatically.

7. State 3 Implementation: Motion (FC3)

FC3 drives the up or down contactor and counts floor-position pulses. The motion termination condition is:

current_floor == target_floor AND motion_stable (no pulse change for > 200 ms)

LAD skeleton:

NETWORK 1
   U     DB1.direction_up
   U     DB1.Motion_Enable
   UN    DB1.top_limit
   S     DB1.coil_up           // output to up contactor
   R     DB1.coil_down

NETWORK 2
   U     DB1.direction_down
   U     DB1.Motion_Enable
   UN    DB1.bottom_limit
   S     DB1.coil_down
   R     DB1.coil_up

NETWORK 3    // Floor pulse counter
   U     E 3.0                 // floor-mark sensor pulse
   UN    DB1.pulse_seen
   S     DB1.pulse_seen
   U     DB1.direction_up
   L     DB1.current_floor
   +     1
   T     DB1.current_floor
   JU    END
   U     DB1.direction_down
   L     DB1.current_floor
   +     -1
   T     DB1.current_floor
END: NOP 0

NETWORK 4    // Arrival check
   L     DB1.current_floor
   L     DB1.target_floor
   ==I
   U     DB1.pulse_stable      // 200 ms no-change timer done
   S     DB1.Arrived
   R     DB1.Motion_Enable
   S     DB1.Close_Enable

8. States 4, 5, 6: Close, Dwell, Open (FC4, FC5, FC6)

FC4 (Close):

   U     DB1.Close_Enable
   S     DB1.coil_gate_close
   UN    E 2.0              // gate-closed limit
   L     S5T#5S              // 5-second timeout
   SD    DB1.close_timer
   U     DB1.close_timer    // timer done, gate did not close
   S     DB1.Fault_DoorObstruction
   U     E 2.0              // gate closed OK
   R     DB1.coil_gate_close
   R     DB1.Close_Enable
   S     DB1.Dwell_Enable

FC5 (Dwell) - timer-only block:

   U     DB1.Dwell_Enable
   L     S5T#2S              // 2-second dwell, configurable per car spec
   SE    DB1.dwell_timer
   U     DB1.dwell_timer
   R     DB1.Dwell_Enable
   S     DB1.Open_Enable

FC6 (Open):

   U     DB1.Open_Enable
   S     DB1.coil_gate_open
   U     E 2.1              // gate-open limit
   R     DB1.coil_gate_open
   R     DB1.Open_Enable
   R     DB1.Arrived

// Clear request bit for the floor we just arrived at
   L     DB1.current_floor
   L     1
   -I
   SRD   1                  // align to bit index (depends on bit ordering)
   ... // clear FR_current and CC_current

// Reset state machine to State 1
   SET
   R     DB1.State_OK

Control returns to OB1, which then re-enters FC1 on the next network. The cycle repeats.

9. STL to LAD Translation Notes

When source logic exists in STL and must be ported to LAD (for example, an instructor-supplied reference or a legacy upgrade), apply these rules:

STL Construct LAD Equivalent
U E 0.0 Normally-open contact ---| E0.0 |---
UN E 0.0 Normally-closed contact ---|/ E0.0 |---
S DB1.FR1 Set coil ---( S )--- with bit DB1.FR1
R DB1.FR1 Reset coil ---( R )--- with bit DB1.FR1
L +5 / T MW10 Move box: EN moves +5 to MW10
L MW10 / L MW12 / +I / T MW14 Add box: MW10 + MW12 -> MW14
L MW10 / L MW12 / >I / JC NEXT Compare box GT_I (MW10, MW12), jump label NEXT if RLO=1
CALL FC1 Box: FC1 (no parameters needed for void FC)

In LAD, parameter passing to FCs uses the IN/OUT/IN_OUT pin strip on the box. STL uses the stack notation. For an FC with no parameters, both forms reduce to a single CALL statement.

Reference: Siemens STEP 7 V5.x Programming and Operating Manual and the S7-300 CPU 31xC and CPU 31x Reference Manual for the full instruction list.

10. Commissioning and Verification

Verification is run in this order. Each step must pass before the next:

  1. OB100 cold-start test: Power-cycle the CPU. Confirm DB1.current_floor = 1, DB1.RequestWord = 0, DB1.state = 1, all coil outputs de-energised. Watch table: VAT_1.
  2. Single-request test: Press FR3 from floor 1 with no car calls. Car should travel up, stop at floor 3, gate close, dwell, gate open, return to State 1.
  3. Multi-request test: Press FR5, FR2, CC3 in random order from floor 1. Car should service them in floor order (3 -> 5 -> 2 or 2 -> 3 -> 5 depending on direction-arbitration rule).
  4. Direction reversal at limits: With car at floor 1 and FR1 latched, request FR6. Car should service FR1 first then FR6. Confirm no spurious "down" command fires when already at bottom.
  5. Door obstruction test: Block the gate during FC4. After 5-second timer, DB1.Fault_DoorObstruction must set. Car must not move. Reset via OB100 or a dedicated fault-reset input.
  6. Power-loss recovery: Trip power mid-motion. On OB100 restart, confirm current_floor restores from absolute floor-mark sensor, not from a volatile counter.

Recommended monitoring: open VAT_1 with the following tags visible - DB1.current_floor, DB1.target_floor, DB1.RequestWord (HEX), DB1.direction_up, DB1.direction_down, DB1.coil_up, DB1.coil_down, DB1.coil_gate_close, DB1.coil_gate_open, DB1.State_OK, DB1.Arrived, DB1.Fault_DoorObstruction.

11. Common Pitfalls and Field-Proven Corrections

Symptom Likely Root Cause Correction
Car never moves even though pushbuttons are pressed Request-capture FC placed after motion FC in OB1 Move FC1 to Network 1 of OB1; remove any enable contact on FC1 call
Car moves on first press but ignores subsequent presses Request latches cleared too early (in motion block, not in gate-open block) Clear FR/CC bits only in FC6 after gate-open limit is reached
Car oscillates between two floors Direction arbitration picks wrong target due to unsigned compare on signed floor index Use signed INT compare; clamp scan_index to range 1..n
Gate reverses immediately on close Light curtain or safety-edge input not honoured Wire the obstruction input to DB1.coil_gate_close via NC contact in FC4
Floor count drifts after power cycle Floor position stored in volatile flag byte Use a non-retentive DB and re-initialise from absolute sensor in OB100
Car skips floors at high speed Floor-mark sensor pulse shorter than scan time Latch the pulse via S/R and clear on next scan; add 50 ms input filter in HW config
Program uploads but never runs in LAD view (only STL works) Source written with non-graphable constructs (e.g., indirect memory access inside a network) Use symbolic addressing and avoid cross-network labels; convert to SCL where possible
Per EN 81-20:2014 and ASME A17.1/CSA B44 (Safety Code for Elevators and Escalators), the final-limit, buffer, and emergency-stop functions must be implemented in hardware with the PLC acting as a supervisory layer only. Do not rely on the PLC alone to stop the car at the terminal floors.

12. Glossary of Symbols Used in This Article

Symbol Meaning
E x.y Digital input, byte x bit y (STEP 7 German notation; equivalent to I x.y in international manuals)
A x.y Digital output (Ausgang)
M x.y Merker (flag) bit
DB1.name Data bit/word inside instance DB1
FR_n Floor request bit for floor n (hall call)
CC_n Car call bit for floor n (in-car call)
S5T#2S Siemens 5-time format, 2 seconds
FC Function (no memory)
FB Function block (instance memory)
OB Organisation block (cyclic, interrupt, restart)

Frequently Asked Questions

Why does my elevator program ignore button presses when the car is idle?

The state that reads and latches floor and car requests must be the first state executed in the scan cycle, not the last. Move the request-capture FC to Network 1 of OB1 and ensure no enable contact gates its CALL - it must run every scan.

How many states does a single-car elevator really need?

Six states cover a minimal passenger or car-park elevator: Wait/Catch, Direction Decide, Move (UP or DOWN), Gate Close, Stop/Dwell, Gate Open. Each becomes one FC or one network block in OB1.

Can I implement the request-capture logic in STL and translate it to LAD?

Yes. STL constructs U, UN, S, R, L, T, +I, >I, and CALL map directly to LAD contacts, coils, compare boxes, and FC call boxes. Prefer SCL over STL when scan-direction logic or loops are required, because SCL translates to cleaner LAD after compilation.

Where should I store the current floor so it survives a power cycle?

Store current_floor in a non-retentive instance DB and re-initialise it from the absolute floor-mark sensor in OB100 (warm restart). For battery-backed retention, mark the relevant DB tag as retentive in the CPU properties under STEP 7 HW Config.

Which safety functions must remain hardwired and outside the PLC?

Final-limit switches, buffer switches, emergency stop, and the door-lock interlock must be hardwired per EN 81-20:2014 and ASME A17.1/CSA B44. The PLC may supervise but cannot be the sole means of compliance. Wire these inputs to the contactor holding circuits in series with the PLC outputs.

Back to blog