Skip to content

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

Rung 0: ──[XIO B:1/6]──(OTL B:0/0)──
Rung 1: ──[XIO B:1/6]──(OTL B:1/6)──

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:

  1. 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.
  2. XIC with 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 (Touch1PulseTouch4Pulse) 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

Rung 10: ──┤ B:1/1 ├──┐
           ┤ B:1/2 ├──┼──(OTE B:1/0)──
           ┤ B:1/3 ├──┤
           ┤ B:1/4 ├──┘

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:

  1. Add WaitDigit5 (B:0/8) and Digit5 (N:5) symbols — shift MatchCount to N:6
  2. Copy Rungs 20–22 pattern for the new state (WaitDigit4 → WaitDigit5 → CheckCode)
  3. Add a 5th EQU/ADD rung in the comparison section
  4. Change the final EQU in Rung 28 from N:5 == 4 to N: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