ladder logic

plc exercise

sequencer

Car Wash PLC Exercise: Full Ladder Logic

‌
Car wash PLC ladder logic exercise diagram showing sensor inputs, state machine, and output devices

A car wash is one of the best teaching machines in automation. It has a clear start condition, a fixed sequence of timed steps, physical interlocks (is the car in position? is the bay door closed?), and a clean end state. It forces you to think about state management, TON timers chained together, and what happens if a sensor fails mid-cycle. Better than any toy exercise.

This post builds a complete, runnable car wash sequencer in Studio 5000 Ladder Logic. You'll get the full I/O list, the state machine logic, the timer rungs, and the interlocks. If you want to adapt it for CODESYS or GX Works 3, the structure maps directly, only the tag syntax changes.

Car Wash System Description

This is a single-bay automatic car wash. The sequence is: detect vehicle, close entry gate, pre-soak spray, scrub brush pass forward, rinse spray, blower pass, open exit gate. The vehicle does not move. The wash gantry travels back and forth on rails. Keeping the vehicle stationary is a deliberate design choice that makes the limit switch logic simple to reason about.

I/O List

Tag NameTypeDescription
VehicleDetectDIPhotoeye, TRUE when vehicle in bay
GantryAtHomeDILimit switch, gantry at start (entry) end
GantryAtEndDILimit switch, gantry at exit end
StartBtnDIOperator pushbutton, momentary
EStopDINC E-stop contact, FALSE = emergency
EntryGate_ClosedDIReed switch, gate fully closed
ExitGate_ClosedDIReed switch, gate fully closed
EntryGate_SolDOOpen/close entry gate solenoid
ExitGate_SolDOOpen/close exit gate solenoid
GantryFwdDODrive gantry toward exit end
GantryRevDODrive gantry toward home end
PreSoak_SolDOPre-soak spray valve
Rinse_SolDORinse spray valve
Brush_MotorDOScrub brush motor contactor
Blower_MotorDOAir blower motor contactor
CycleActive_LampDOAmber in-progress lamp
Car wash I/O list for the Studio 5000 exercise

State Machine: The Right Way to Sequence This

You could code this as a pile of interlocked rungs, but you'll regret it by state 3. Use an integer tag called WashState and one rung per state transition. Each state has a number, a set of outputs it activates, and a condition that advances to the next state. This pattern is sometimes called a sequential function chart in code, and it scales.

StateNameActive OutputsAdvance Condition
0IdleNoneVehicleDetect AND StartBtn AND EStop
1Gate CloseEntryGate_SolEntryGate_Closed AND GantryAtHome
2Pre-SoakPreSoak_Sol, GantryFwdSoak_Timer.DN (8 s)
3Scrub ForwardBrush_Motor, GantryFwdGantryAtEnd
4Rinse ForwardRinse_Sol, GantryFwdRinse_Fwd_Timer.DN (10 s) AND GantryAtEnd
5Blower ForwardBlower_Motor, GantryFwdGantryAtEnd
6Gantry ReturnGantryRevGantryAtHome
7Exit Gate OpenExitGate_SolExit_Timer.DN (4 s)
8CompleteCycleActive_Lamp offXIO(VehicleDetect) -> State 0
Car wash state machine. WashState drives all outputs.
Always make State 0 your idle/safe state. On power-up, WashState defaults to 0 and nothing moves. That one habit prevents a lot of embarrassing surprises at commissioning.

State Transition Rungs

The pattern for each transition rung is: EQU(WashState, N) AND [advance condition] then MOV(N+1, WashState). Below is the structured text equivalent, which is actually more readable for a sequencer this size. After the ST block, you'll find the ladder rung for the scrub step because that's the one with the motor interlock people always get wrong.

CarWash_Sequencer.st
// Car Wash Sequencer - Studio 5000 Structured Text (Add-On Instruction or routine)
// All timers pre-declared: Soak_Timer, Rinse_Fwd_Timer, Exit_Timer (TIMER data type)

CASE WashState OF

    0: // Idle
        IF VehicleDetect AND StartBtn AND EStop THEN
            WashState := 1;
        END_IF;

    1: // Close entry gate
        EntryGate_Sol := TRUE;
        IF EntryGate_Closed AND GantryAtHome THEN
            WashState := 2;
        END_IF;

    2: // Pre-soak (timed, gantry creeps forward at low speed)
        PreSoak_Sol := TRUE;
        GantryFwd   := TRUE;
        // TON Soak_Timer, preset 8000 ms
        IF Soak_Timer.DN THEN
            WashState := 3;
        END_IF;

    3: // Scrub brush forward pass
        Brush_Motor := TRUE;
        GantryFwd   := TRUE;
        IF GantryAtEnd THEN
            WashState := 4;
        END_IF;

    4: // Rinse forward (hold at end for 10 s)
        Rinse_Sol := TRUE;
        IF Rinse_Fwd_Timer.DN THEN
            WashState := 5;
        END_IF;

    5: // Blower forward pass
        Blower_Motor := TRUE;
        GantryFwd    := TRUE;
        IF GantryAtEnd THEN
            WashState := 6;
        END_IF;

    6: // Return gantry to home
        GantryRev := TRUE;
        IF GantryAtHome THEN
            WashState := 7;
        END_IF;

    7: // Open exit gate
        ExitGate_Sol := TRUE;
        // TON Exit_Timer, preset 4000 ms
        IF Exit_Timer.DN THEN
            WashState := 8;
        END_IF;

    8: // Wait for vehicle to leave
        IF NOT VehicleDetect THEN
            ExitGate_Sol := FALSE;
            WashState    := 0;
        END_IF;

END_CASE;

// CycleActive lamp: on whenever not idle
CycleActive_Lamp := (WashState > 0) AND (WashState < 8);

// E-stop: force state 0 and kill all motion outputs
IF NOT EStop THEN
    WashState    := 0;
    GantryFwd    := FALSE;
    GantryRev    := FALSE;
    Brush_Motor  := FALSE;
    Blower_Motor := FALSE;
    PreSoak_Sol  := FALSE;
    Rinse_Sol    := FALSE;
END_IF;

Ladder Logic: Scrub Step with Motor Interlock

Here's the ladder rung for State 3 in pure Ladder Diagram. This is the one that trips people up because you need to confirm the gate is actually closed before you energise the brush motor. Notice the EQU contact compares WashState to the constant 3. In Studio 5000 that's an EQU instruction on the rung, treated here as a named contact for readability.

Car Wash: Scrub Step Activation with Gate Interlock (Studio 5000). Ladder logic (5 rungs): Rung 0: examine if State3_Active is on (XIC), then examine if EntryGate_Closed is on (XIC), then examine if ExitGate_Closed is on (XIC), then examine if Brush_FaultLatch is off (XIO), then energize output Brush_Motor (OTE). Rung 1: examine if State3_Active is on (XIC), then examine if EntryGate_Closed is on (XIC), then examine if ExitGate_Closed is on (XIC), then examine if Brush_FaultLatch is off (XIO), then energize output GantryFwd (OTE). Rung 2: examine if Brush_Motor is on (XIC), then examine if Brush_FaultLatch is off (XIO), then TON on Brush_StartTimeout. Rung 3: examine if Brush_StartTimeout.DN is on (XIC), then examine if Brush_Running_FB is off (XIO), then examine if Brush_Fault_OS is on (XIC), then latch output Brush_FaultLatch (OTL). Rung 4: examine if Brush_FaultLatch is on (XIC), then examine if HMI_FaultAck is on (XIC), then examine if Brush_Motor is off (XIO), then unlatch output Brush_FaultLatch (OTU). State3_Active is a BOOL derived from EQU(WashState,3). Both gates must confirm closed before the brush motor energises. A 5-second TON watches for a motor run feedback signal (Brush_Running_FB from a current relay or auxiliary contact). If feedback never arrives, the fault latches and the operator must acknowledge before the cycle can restart. GantryFwd is only allowed when the brush is actually running, preventing a dry gantry travel.

Car Wash: Scrub Step Activation with Gate Interlock (Studio 5000)Ladder logic
Toggle inputs
Rung 0
Ladder logic rung: examine if State3_Active is on (XIC), then examine if EntryGate_Closed is on (XIC), then examine if ExitGate_Closed is on (XIC), then examine if Brush_FaultLatch is off (XIO), then energize output Brush_Motor (OTE) examine if State3_Active is on (XIC), then examine if EntryGate_Closed is on (XIC), then examine if ExitGate_Closed is on (XIC), then examine if Brush_FaultLatch is off (XIO), then energize output Brush_Motor (OTE) XIC State3_Active State3_Active State3_Active XIC EntryGate_Closed EntryGate_Closed EntryGate_Closed XIC ExitGate_Closed ExitGate_Closed ExitGate_Closed XIO Brush_FaultLatch Brush_FaultLatch Brush_FaultLatch OTE Brush_Motor Brush_Motor Brush_Motor
Rung 1
Ladder logic rung: examine if State3_Active is on (XIC), then examine if EntryGate_Closed is on (XIC), then examine if ExitGate_Closed is on (XIC), then examine if Brush_FaultLatch is off (XIO), then energize output GantryFwd (OTE) examine if State3_Active is on (XIC), then examine if EntryGate_Closed is on (XIC), then examine if ExitGate_Closed is on (XIC), then examine if Brush_FaultLatch is off (XIO), then energize output GantryFwd (OTE) XIC State3_Active State3_Active State3_Active XIC EntryGate_Closed EntryGate_Closed EntryGate_Closed XIC ExitGate_Closed ExitGate_Closed ExitGate_Closed XIO Brush_FaultLatch Brush_FaultLatch Brush_FaultLatch OTE GantryFwd GantryFwd GantryFwd
Rung 2
Ladder logic rung: examine if Brush_Motor is on (XIC), then examine if Brush_FaultLatch is off (XIO), then TON on Brush_StartTimeout examine if Brush_Motor is on (XIC), then examine if Brush_FaultLatch is off (XIO), then TON on Brush_StartTimeout XIC Brush_Motor Brush_Motor Brush_Motor XIO Brush_FaultLatch Brush_FaultLatch Brush_FaultLatch TON Brush_StartTimeout T#5s 0 TONTimerBrush_StartTimeoutBrush_StartTimeoutPresetT#5sT#5sAccum00
Rung 3
Ladder logic rung: examine if Brush_StartTimeout.DN is on (XIC), then examine if Brush_Running_FB is off (XIO), then examine if Brush_Fault_OS is on (XIC), then latch output Brush_FaultLatch (OTL) examine if Brush_StartTimeout.DN is on (XIC), then examine if Brush_Running_FB is off (XIO), then examine if Brush_Fault_OS is on (XIC), then latch output Brush_FaultLatch (OTL) XIC Brush_StartTimeout.DN Brush_StartTimeout.DN Brush_StartTimeout.DN XIO Brush_Running_FB Brush_Running_FB Brush_Running_FB OSR Brush_Fault_OS Brush_Fault_OS Brush_Fault_OS OSR OTL Brush_FaultLatch Brush_FaultLatch Brush_FaultLatch L
Rung 4
Ladder logic rung: examine if Brush_FaultLatch is on (XIC), then examine if HMI_FaultAck is on (XIC), then examine if Brush_Motor is off (XIO), then unlatch output Brush_FaultLatch (OTU) examine if Brush_FaultLatch is on (XIC), then examine if HMI_FaultAck is on (XIC), then examine if Brush_Motor is off (XIO), then unlatch output Brush_FaultLatch (OTU) XIC Brush_FaultLatch Brush_FaultLatch Brush_FaultLatch XIC HMI_FaultAck HMI_FaultAck HMI_FaultAck XIO Brush_Motor Brush_Motor Brush_Motor OTU Brush_FaultLatch Brush_FaultLatch Brush_FaultLatch U
energizedTip: click a contact in the diagram to flip its bit.
State3_Active is a BOOL derived from EQU(WashState,3). Both gates must confirm closed before the brush motor energises. A 5-second TON watches for a motor run feedback signal (Brush_Running_FB from a current relay or auxiliary contact). If feedback never arrives, the fault latches and the operator must acknowledge before the cycle can restart. GantryFwd is only allowed when the brush is actually running, preventing a dry gantry travel.

Timer Wiring for the Timed States

States 2, 4 and 7 are timer-gated. Each needs a TON instruction that resets when the state is not active, otherwise the timer carries its accumulated value into the next cycle and the first pre-soak lasts zero seconds. The cleanest way: gate the TON enable contact with the state BOOL, so when WashState moves away from 2, the TON input goes FALSE and the accumulator resets automatically.

Timer reset gotcha. If you write XIC(AlwaysOnBit)TON(Soak_Timer,8000,0) and forget to gate it with the state condition, Soak_Timer keeps accumulating in every state. By the time you reach State 2, it's already done. Gate every timer with its state BOOL, and that problem disappears.

In ladder, add a rung like this for State 2:

State2_Timer.ladder
// Rung: Pre-soak timer (active only in State 2)
// State2_Active = EQU(WashState, 2)
XIC(State2_Active) TON(Soak_Timer, 8000, 0)

// Transition rung: advance to State 3 when timer done
XIC(State2_Active) XIC(Soak_Timer.DN) MOV(3, WashState)

Gantry Direction Interlock: The One You Cannot Skip

GantryFwd and GantryRev must never be TRUE at the same time or you'll smoke a drive and possibly damage the gantry mechanics. Add a hardware interlock at the drive level if the drive supports it, and add a software interlock in the PLC as a second layer. The software rung is just XIO(GantryRev) in series with GantryFwd, and XIO(GantryFwd) in series with GantryRev. Cheap insurance.

Gantry direction interlock ladder diagram preventing GantryFwd and GantryRev from energising simultaneously
Mutual direction interlock: each output has a normally-closed contact from the other, preventing simultaneous forward and reverse commands.

Exercises to Take This Further

Once the basic sequence runs, try these to build real competence:

  1. Add a wash cycle counter (CTU) that increments each time State 8 completes, resets via HMI, and triggers a maintenance alarm at 500 cycles. See the PLC Counter Instructions guide if you need a CTU refresher.
  2. Add a VehicleDetect debounce timer: require the photoeye to be TRUE for 2 continuous seconds before allowing a cycle start. This stops a shopping trolley blowing through from triggering a wash.
  3. Add a fault state (State 9): if the gantry doesn't reach the end limit within 30 seconds, latch a fault, kill all motion, and force a manual reset.
  4. Replace the fixed timer presets with DINT tags mapped to HMI numeric entries so the operator can tune soak and rinse times without a program change.
  5. Convert the CASE statement to SFC in CODESYS and compare the two approaches. The SFC version is more visual but the CASE statement is easier to version-control in plain text.

Common Mistakes in Car Wash PLC Logic

  • Forgetting to turn off outputs when leaving a state. In the CASE statement approach, outputs are written every scan inside their state. If you leave a state, the output naturally goes FALSE because nothing writes it TRUE. This is the advantage of the CASE pattern over OTL/OTU latches for sequencer outputs.
  • Energising the brush motor before confirming the gate is closed. Always check physical feedback (the reed switch), not just the solenoid command.
  • Using a single limit switch for both gantry home and gantry end. Use two separate switches. A single switch mounted wrong gives you no directional information.
  • Not testing the E-stop path at commissioning. Trip the E-stop in every state, confirm the gantry stops and the state resets to 0. Sounds obvious; gets skipped constantly.
  • Leaving water spray on when the gantry stops. If GantryFwd goes FALSE mid-spray due to a fault, the spray valve must also close. Gate the spray outputs with GantryFwd or add an explicit spray shutoff in the E-stop and fault rungs.

Adapting to Other Platforms

The CASE statement above is valid IEC 61131-3 Structured Text, so it runs in CODESYS, TwinCAT, Sysmac Studio and TIA Portal with minor syntax tweaks. In TIA Portal the TIMER data type becomes IEC_TIMER and the preset uses TIME# literals (T#8s). In GX Works 3 you'd use the SFC editor directly since Mitsubishi's SFC implementation is excellent. The state machine concept is identical across all of them.

If you're coming from a project like the PLC Bottle Filling Machine exercise, you'll notice the structure is the same: state integer, transition conditions, output map. The car wash is more complex because the gantry moves in two directions and multiple states share the same physical output (GantryFwd appears in States 2, 3, 4 and 5). That shared-output pattern is where beginners write duplicate logic and then wonder why the gantry moves when it shouldn't.

Keep your output rungs in a separate section from your state transition rungs. State transitions write to WashState. Output rungs read WashState and drive physical I/O. Mixing the two in one rung makes troubleshooting painful.

What You Should Be Able to Do After This

Build this in simulation first. Studio 5000 Logix Designer lets you force I/O tags offline. Simulate GantryAtEnd by forcing it TRUE after you see GantryFwd go TRUE. Step through every state. Then break it: force EStop FALSE in State 4 and verify the brush motor de-energises immediately. If your logic handles that cleanly, you've written sequencer logic that'll hold up in the field.

Related Blogs