4-Digit Code Lock¶
Example file: examples/code_lock.plcproj Difficulty: Intermediate Key instructions: XIC/XIO with OSR decorator, MOV, ADD, EQU, OTL/OTU, TON
Overview¶
This example implements a 4-digit combination lock using four touch buttons. The operator presses buttons in sequence; when the last digit is entered the logic compares the sequence against a stored secret code and lights a green or red LED for two seconds before resetting.
The design demonstrates several important PLC programming patterns:
- One-shot (OSR) edge detection — each button press is captured as a single-scan pulse regardless of how long the button is held
- State machine using latch/unlatch bits — only one state bit is active at a time, making transitions explicit and predictable
- Input lockout during output — prevents further input while the result LED is displayed
- First-scan initialization — uses a normally-closed contact on an unlatched bit to run startup code exactly once
The secret code is 4-2-3-1 (press Touch4, Touch2, Touch3, Touch1 in that order).
I/O Map¶
Inputs¶
| Address | Symbol | Description |
|---|---|---|
| I:0/4 | Touch1 | Touch button 1 |
| I:0/5 | Touch2 | Touch button 2 |
| I:0/6 | Touch3 | Touch button 3 |
| I:0/7 | Touch4 | Touch button 4 |
Outputs¶
| Address | Symbol | Description |
|---|---|---|
| O:0/6 | LED3 | Red LED — wrong code |
| O:0/7 | LED4 | Green LED — correct code |
Internal Bits¶
| Address | Symbol | Description |
|---|---|---|
| B:0/0 | WaitDigit1 | State: waiting for the 1st button press |
| B:0/1 | WaitDigit2 | State: waiting for the 2nd button press |
| B:0/2 | WaitDigit3 | State: waiting for the 3rd button press |
| B:0/3 | WaitDigit4 | State: waiting for the 4th button press |
| B:0/4 | CheckCode | State: evaluate the entered sequence |
| B:0/5 | ShowCorrect | State: displaying the correct-code LED |
| B:0/6 | ShowWrong | State: displaying the wrong-code LED |
| B:0/7 | Lockout | Inhibits all touch input during LED display |
| B:1/0 | AnyPress | High for one scan when any button is pressed |
| B:1/1 | Touch1Pulse | One-shot pulse for Touch1 |
| B:1/2 | Touch2Pulse | One-shot pulse for Touch2 |
| B:1/3 | Touch3Pulse | One-shot pulse for Touch3 |
| B:1/4 | Touch4Pulse | One-shot pulse for Touch4 |
| B:1/5 | CodeMatch | All four digits matched the secret code |
| B:1/6 | InitDone | First-scan initialization completed |
Integer Registers¶
| Address | Symbol | Description |
|---|---|---|
| N:0 | Digit1 | First entered digit (1–4) |
| N:1 | Digit2 | Second entered digit (1–4) |
| N:2 | Digit3 | Third entered digit (1–4) |
| N:3 | Digit4 | Fourth entered digit (1–4) |
| N:4 | PressedButton | Which button was pressed this scan (1–4) |
| N:5 | MatchCount | Number of digits that matched the code |
Timers¶
| Address | Symbol | Preset | Description |
|---|---|---|---|
| T:0 | CorrectTimer | 2000 ms | Holds the green LED on for 2 seconds |
| T:1 | WrongTimer | 2000 ms | Holds the red LED on for 2 seconds |
State Machine Overview¶
The program uses seven mutually exclusive state bits (B:0/0–B:0/6). Exactly one is active at any time:
┌──────────────┐ any press ┌──────────────┐ any press ┌──────────────┐ any press ┌──────────────┐
│ WaitDigit1 ├────────────►│ WaitDigit2 ├────────────►│ WaitDigit3 ├────────────►│ WaitDigit4 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
│ any press
▼
┌──────────────┐
┌──────────►│ CheckCode │
│ └──────┬───────┘
│ │
│ ┌───────────┴───────────┐
│ │ │
│ match=4 match<4
│ │ │
│ ▼ ▼
│ ┌──────────────┐ ┌──────────────┐
│ │ ShowCorrect │ │ ShowWrong │
│ │ (LED4, 2s) │ │ (LED3, 2s) │
│ └──────┬───────┘ └──────┬───────┘
│ │ timer done │ timer done
└────────┴──────────────────┘
(reset to WaitDigit1)
Ladder Logic — Rung by Rung¶
Rungs 0–1: First-Scan Initialization¶
B:1/6 (InitDone) starts at 0 (off), so XIO B:1/6 is TRUE on the very first scan only. Rung 0 latches WaitDigit1 (the starting state), and Rung 1 immediately latches InitDone so neither rung fires again.
Why use XIO on InitDone instead of a first-scan contact? PiPLC doesn't have a dedicated first-scan coil, but a normally-closed contact on an unlatched bit achieves exactly the same effect — it's TRUE until you latch it, after which it's FALSE forever.
Rungs 2–5: Edge Detection (One-Shot per Button)¶
Rung 2: ──[XIO B:0/7]──[XIC I:0/4 ⬆OSR]──(OTE B:1/1)── ← Touch1 pulse
Rung 3: ──[XIO B:0/7]──[XIC I:0/5 ⬆OSR]──(OTE B:1/2)── ← Touch2 pulse
Rung 4: ──[XIO B:0/7]──[XIC I:0/6 ⬆OSR]──(OTE B:1/3)── ← Touch3 pulse
Rung 5: ──[XIO B:0/7]──[XIC I:0/7 ⬆OSR]──(OTE B:1/4)── ← Touch4 pulse
Each rung uses two conditions in series:
XIO B:0/7(Lockout) — blocks all input while a result LED is displayed. The Lockout bit is set when CheckCode transitions to ShowCorrect or ShowWrong.XICwith OSR decorator — the one-shot rising (OSR) decorator makes the contact TRUE for exactly one scan on the button's rising edge, no matter how long it stays pressed.
The result is a clean single-scan pulse bit (Touch1Pulse…Touch4Pulse) per button press.
Rungs 6–9: Map Button to Number¶
Rung 6: ──[XIC B:1/1]──[MOV 1 → N:4]── ← Touch1 → 1
Rung 7: ──[XIC B:1/2]──[MOV 2 → N:4]── ← Touch2 → 2
Rung 8: ──[XIC B:1/3]──[MOV 3 → N:4]── ← Touch3 → 3
Rung 9: ──[XIC B:1/4]──[MOV 4 → N:4]── ← Touch4 → 4
These rungs convert whichever pulse bit is active into an integer value stored in PressedButton (N:4). Because the pulse bits are mutually exclusive (only one button can produce an OSR edge per scan), at most one MOV fires per scan.
Rung 10: AnyPress Consolidation¶
A parallel branch produces AnyPress (B:1/0) whenever any touch pulse is active. The state machine rungs check AnyPress rather than each pulse individually, keeping each state transition to two conditions (current state + button pressed).
Rungs 11–22: State Machine — Capture 4 Digits¶
Each state uses three rungs: store the pressed value, latch the next state, unlatch the current state. The unlatch always comes last so the store and latch rungs can still see the active state bit on the same scan.
State 1 — WaitDigit1 (Rungs 11–13):
Rung 11: ──[XIC B:0/0]──[XIC B:1/0]──[MOV N:4 → N:0]── ← store as Digit1
Rung 12: ──[XIC B:0/0]──[XIC B:1/0]──(OTL B:0/1)── ← enter WaitDigit2
Rung 13: ──[XIC B:0/0]──[XIC B:1/0]──(OTU B:0/0)── ← leave WaitDigit1
State 2 — WaitDigit2 (Rungs 14–16): Same pattern, stores to Digit2, transitions to WaitDigit3.
State 3 — WaitDigit3 (Rungs 17–19): Stores to Digit3, transitions to WaitDigit4.
State 4 — WaitDigit4 (Rungs 20–22): Stores to Digit4, transitions to CheckCode instead of another WaitDigit state.
Why latch the next state before unlatching the current one? If you cleared the current state first, the rungs above would no longer see it as active and wouldn't fire. By latching next and unlatching last within the same scan, all three actions are guaranteed to execute together — an atomic state transition.
Rungs 23–32: Code Comparison¶
The secret code is 4-2-3-1 (Digit1=4, Digit2=2, Digit3=3, Digit4=1).
Rung 23: ──[XIC B:0/4]──[MOV 0 → N:5]── ← reset MatchCount
Rung 24: ──[XIC B:0/4]──[EQU N:0 == 4]──[ADD N:5+1 → N:5]──
Rung 25: ──[XIC B:0/4]──[EQU N:1 == 2]──[ADD N:5+1 → N:5]──
Rung 26: ──[XIC B:0/4]──[EQU N:2 == 3]──[ADD N:5+1 → N:5]──
Rung 27: ──[XIC B:0/4]──[EQU N:3 == 1]──[ADD N:5+1 → N:5]──
Rung 28: ──[XIC B:0/4]──[EQU N:5 == 4]──(OTL B:1/5)── ← all 4 matched → CodeMatch
Rung 29: ──[XIC B:0/4]──[XIC B:1/5]──(OTL B:0/5)── ← correct → ShowCorrect
Rung 30: ──[XIC B:0/4]──[XIO B:1/5]──(OTL B:0/6)── ← wrong → ShowWrong
Rung 31: ──[XIC B:0/4]──(OTL B:0/7)── ← engage Lockout
Rung 32: ──[XIC B:0/4]──(OTU B:0/4)── ← leave CheckCode
The comparison uses an accumulating match counter: each digit is compared independently and MatchCount is incremented for each match. Only when all four match (MatchCount == 4) is CodeMatch latched. This approach is easy to extend — changing the code or the number of digits only requires editing the comparison rungs.
Rung 31 sets Lockout before Rung 32 clears CheckCode, so the very next scan the edge detection rungs (2–5) are already blocked.
Rungs 33–38: ShowCorrect — Green LED for 2 Seconds¶
Rung 33: ──[XIC B:0/5]──[TON T:0 2000ms]──
Rung 34: ──[XIC B:0/5]──(OTE O:0/7)── ← LED4 on
Rung 35: ──[XIC B:0/5]──[XIC T:0.DN]──(OTU B:1/5)── ← clear CodeMatch
Rung 36: ──[XIC B:0/5]──[XIC T:0.DN]──(OTU B:0/7)── ← release Lockout
Rung 37: ──[XIC B:0/5]──[XIC T:0.DN]──(OTL B:0/0)── ← back to WaitDigit1
Rung 38: ──[XIC B:0/5]──[XIC T:0.DN]──(OTU B:0/5)── ← leave ShowCorrect (last)
While ShowCorrect is active, the TON timer accumulates. LED4 is energized by a plain OTE (not latched) so it turns off automatically when the state clears. When the timer completes (T:0.DN), the state clears in this order: clear CodeMatch, release Lockout, latch WaitDigit1, then finally clear ShowCorrect — ensuring WaitDigit1 is set before ShowCorrect is cleared.
Rungs 39–43: ShowWrong — Red LED for 2 Seconds¶
Rung 39: ──[XIC B:0/6]──[TON T:1 2000ms]──
Rung 40: ──[XIC B:0/6]──(OTE O:0/6)── ← LED3 on
Rung 41: ──[XIC B:0/6]──[XIC T:1.DN]──(OTU B:0/7)── ← release Lockout
Rung 42: ──[XIC B:0/6]──[XIC T:1.DN]──(OTL B:0/0)── ← back to WaitDigit1
Rung 43: ──[XIC B:0/6]──[XIC T:1.DN]──(OTU B:0/6)── ← leave ShowWrong (last)
Identical structure to ShowCorrect but uses T:1 and LED3. No CodeMatch flag to clear because it was never set (the wrong-code path branches from XIO B:1/5).
Execution Timing¶
Scan: 1 2 ... N N+1 N+2 ... N+2001
───────────────────────────────────────────────────
Touch4: ▲─────────────────────────────────────────────────
Touch4Pulse: TRUE FALSE ... FALSE
WaitDigit1: TRUE FALSE ... FALSE
WaitDigit2: FALSE TRUE ... TRUE FALSE
...
WaitDigit4: (becomes TRUE after 3 more presses)
CheckCode: (1 scan only — clears itself at end of scan)
ShowCorrect: FALSE FALSE ... FALSE TRUE TRUE ... FALSE
T:0.ACC: 0 0 ... 0 0 1 ... 2000 → DN
LED4 (O:0/7):FALSE FALSE ... FALSE FALSE TRUE ... FALSE
The CheckCode state exists for only one scan — it is set by WaitDigit4 and cleared by Rung 32 in the same scan execution sequence.
Customization Ideas¶
Change the Secret Code¶
Edit the four EQU comparison values in Rungs 24–27:
Rung 24: EQU N:0 == X ← 1st digit (1–4)
Rung 25: EQU N:1 == X ← 2nd digit (1–4)
Rung 26: EQU N:2 == X ← 3rd digit (1–4)
Rung 27: EQU N:3 == X ← 4th digit (1–4)
Extend to More Digits¶
To add a 5th digit:
- Add
WaitDigit5 (B:0/8)andDigit5 (N:5)symbols — shiftMatchCounttoN:6 - Copy Rungs 20–22 pattern for the new state (WaitDigit4 → WaitDigit5 → CheckCode)
- Add a 5th EQU/ADD rung in the comparison section
- Change the final EQU in Rung 28 from
N:5 == 4toN:6 == 5
Add an Attempt Counter¶
Track failed attempts and lock the panel after three wrong codes:
; Add after Rung 30 (ShowWrong latch):
──[XIC B:0/6]──[ADD N:7 + 1 → N:7]── ← increment attempts
──[XIC B:0/6]──[GEQ N:7 >= 3]──(OTL B:2/0)── ← latch PermanentLock
Then add XIO B:2/0 in series with XIO B:0/7 on Rungs 2–5 to block input even after the LED timer expires.
Change the LED Display Duration¶
Edit the preset values on the TON instructions:
- Rung 33 (
T:0) — correct code display time - Rung 39 (
T:1) — wrong code display time
Values are in milliseconds: 1000 = 1 second, 5000 = 5 seconds.
Concepts Demonstrated¶
| Concept | Where |
|---|---|
| First-scan initialization with XIO | Rungs 0–1 |
| One-shot rising edge (OSR decorator) | Rungs 2–5 |
| Input lockout during output | Rung 31, XIO B:0/7 in Rungs 2–5 |
| State machine with OTL/OTU | Rungs 11–32 |
| Atomic state transition (latch next, unlatch last) | Rungs 11–13 (and each state group) |
| Accumulating match counter | Rungs 23–28 |
| TON timer for timed output | Rungs 33–43 |
| Parallel branch (any of N conditions) | Rung 10 |