Sequencing 100 PLC Outputs with 2 Timers Using Indirect

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. Engineering Problem Overview

A common sequencing requirement in process and packaging machinery is "walk-the-lights" style output control: 100 solenoid valves, indicators, or heater banks must be energized one after another with a fixed ON duration and a fixed OFF duration, then wrap back to output 1. The naive implementation instantiates 200 TON timers (one ON timer and one OFF timer per output) inside a ladder program, which bloats the OB1 scan budget, exhausts the IEC timer instance word count, and makes online changes painful.

The compact solution collapses all 100 outputs into a single indexed array, drives them with two physical timers (or one accumulator plus two logical time bases), and uses pointer-style indirect addressing to multiplex the active element. This pattern is controller-agnostic, but it is most cleanly expressed on a Siemens S7-1200/S7-1500 with SCL, or on an Allen-Bradley CompactLogix with Structured Text. The article below provides a fully working reference implementation, the timing math, and a commissioning checklist.

Target cycle: Valve N energizes for 1.0 s, de-energizes for 10.0 s, then valve N+1 takes over. After valve 100, valve 1 repeats. Total round-robin period for one full pass = 100 × (1 s + 10 s) = 1100 s ≈ 18 min 20 s.

2. Timing Math and Cycle Planning

Before writing code, freeze the timing contract in a single table. Any later change should ripple through this table only.

Parameter Symbol Value Notes
Number of outputs N 100 Indexed 0..99 in arrays
Per-output ON time tON 1.000 s IEC time constant T#1s
Per-output OFF time tOFF 10.000 s IEC time constant T#10s
Per-output slot Tslot 11.000 s tON + tOFF
Full pass period Tpass 1100.000 s N × Tslot
Active output count at any instant — 1 Mutually exclusive
ON duty cycle per output D 9.09 % tON / Tslot
Power dissipation if all real valves (worst case) P 1 × Pvalve Only one valve ever energized

Duty cycle verification:

D = tON / Tslot = 1.0 / 11.0 = 0.0909 = 9.09 %

This is well inside the continuous-duty rating of typical 24 VDC solenoid coils (rated 100 % ED), so thermal stress is not a concern even when the sequence runs continuously.

3. Architectural Approach: Pointer + Indirect Addressing

The control core is a 16-bit integer pointer iValveIndex that walks the integer range [0, 99]. Two physical IEC timers generate the ON pulse and the OFF dwell. The pointer advances only when the OFF timer elapses, so the ON timer naturally retires inside its own slot. The active output line is selected by indexing a BOOL array.

FB_Sequencer (Function Block, SCL)
+----------------------------------+
| iValveIndex  : INT   (0..99)     |
| tON_Pulse    : TON   (PT = T#1s) |
| tOFF_Dwell   : TON   (PT = T#10s)|
| aValves[0..99] : ARRAY[0..99] OF BOOL |
| bRunning     : BOOL              |
| bOnePassDone : BOOL              |
+----------------------------------+

An optional CPU clock memory bit (1 Hz) can replace one of the timers if you prefer free-running time base. The trade-off is loss of start-time determinism: a clock bit does not know when the sequence was started, so you must combine it with a wrap-around counter instead of a TON.

4. Hardware and Software Prerequisites

  • CPU: Siemens S7-1200 (firmware ≥ V4.2 for optimized block access) or S7-1500 (any firmware). Minimum work memory 150 KB for the data block holding the 100-element array plus FB instance DB.
  • Engineering: TIA Portal V15.1 or later. SCL must be installed (standard on all S7-1200/1500 bundles).
  • I/O: One SM 1223 DQ16×24VDC or two SM 1222 DQ16 modules, totaling 32 outputs. For 100 outputs you need seven SM 1222 DQ16 (7 × 16 = 112 outputs with 12 spare). The valve array lives in a global DB; the physical write happens in a separate OB that copies aValves[i] to the output process image.
  • Clock memory: Enable in CPU properties → System and clock memory. Set byte MB10 (or any free byte) as the clock memory, so M10.3 = 1 Hz, M10.4 = 0.5 Hz, M10.5 = 0.2 Hz, M10.6 = 0.1 Hz.
  • Watchdog: Disable OB1 cycle-time OB if your cycle is > 150 ms; the sequencer is scan-tolerant and does not require sub-10 ms loops.

Note: For multi-rack installations above 64 outputs, set the S7-1200 process image partition for the output byte range. S7-1500 handles 32 KB process images natively and does not need this step.

5. Siemens S7-1200/S7-1500 SCL Implementation

Create a new FB named FB_Sequencer. Drop the following SCL source into the block body. The code uses two physical timers as requested by the original poster, plus an integer pointer for the active output index.

{attribute 'qualified_only'}
FUNCTION_BLOCK "FB_Sequencer"
VAR
    bStart           : BOOL;          // Start pushbutton (rising edge)
    bStop            : BOOL;          // Stop pushbutton
    bRunning         : BOOL;          // Sequence running latch
    iValveIndex      : INT;           // 0..99 pointer
    tOnPulse         : TON;           // 1 s ON timer
    tOffDwell        : TON;           // 10 s OFF timer
    aValves          : ARRAY[0..99] OF BOOL;  // Logical outputs
    bOnePassDone     : BOOL;          // Set after valve 100 finishes OFF
    bIndexWrap       : BOOL;          // Diagnostic: index just wrapped
END_VAR
BEGIN
    // ------------------------------------------------------------------
    // Start / Stop latch
    // ------------------------------------------------------------------
    IF bStart AND NOT bRunning THEN
        bRunning := TRUE;
        iValveIndex := 0;
        // Reset timers so the first slot starts cleanly on tOn
        tOnPulse(IN := FALSE);
        tOffDwell(IN := FALSE);
    END_IF;

    IF bStop THEN
        bRunning := FALSE;
        tOnPulse(IN := FALSE);
        tOffDwell(IN := FALSE);
    END_IF;

    // ------------------------------------------------------------------
    // Drive the indexed output based on the active timer phase
    // ------------------------------------------------------------------
    IF bRunning THEN

        // Phase A: ON pulse for current index
        tOnPulse(IN := TRUE, PT := T#1s);

        // Force ALL array elements low first; then energize the active one
        // (defensive: prevents ghost-on if the index ever over-runs)
        aValves[iValveIndex] := tOnPulse.Q;

        // Phase B: OFF dwell begins when ON timer expires
        IF NOT tOnPulse.Q AND NOT tOffDwell.IN THEN
            tOffDwell(IN := TRUE, PT := T#10s);
        END_IF;

        // Phase C: advance pointer when OFF timer elapses
        IF tOffDwell.Q THEN
            tOnPulse(IN := FALSE);
            tOffDwell(IN := FALSE);
            iValveIndex := iValveIndex + 1;

            IF iValveIndex > 99 THEN
                iValveIndex := 0;
                bIndexWrap := TRUE;     // Latched diagnostic
                bOnePassDone := TRUE;    // HMI / SCADA can clear
            END_IF;
        END_IF;

    ELSE
        // Idle: clear all outputs and timers
        FOR i := 0 TO 99 DO
            aValves[i] := FALSE;
        END_FOR;
        tOnPulse(IN := FALSE);
        tOffDwell(IN := FALSE);
    END_IF;

    // Clear edge flags
    IF bOnePassDone AND NOT bRunning THEN
        bOnePassDone := FALSE;
        bIndexWrap := FALSE;
    END_IF;
END_FUNCTION_BLOCK

Call the FB from OB1 in a single instance DB:

// OB1 - cyclic main
"iDB_Sequencer"(bStart := "Start_PB",
                bStop  := "Stop_PB");

// Copy array to physical output process image (illustrative for %Q0.0 .. %Q12.3)
FOR i := 0 TO 99 DO
    %QW[i] := "iDB_Sequencer".aValves[i];
END_FOR;

6. Siemens Ladder (LAD) Equivalent

If your site standard mandates LAD-only, replicate the same state machine with two TON coils and a counter that holds iValveIndex. Use a comparison network to energize the indexed output via a demultiplexer coil. The pattern is denser in LAD than in SCL, but it works on the same two-timer footprint:

Network 1 - Latch run flag
--| bStart |--|/| bRunning |--( S )-- bRunning
--| bStop  |-----------------( R )-- bRunning

Network 2 - ON timer (always running while sequence is active)
--| bRunning |--[TON T1, PT = T#1s]--

Network 3 - OFF timer (kicks in after T1 expires)
--| bRunning |--|/| T1.Q |--[TON T2, PT = T#10s]--

Network 4 - Advance pointer (counter acts as pointer 0..99)
--| T2.Q |--[CTU C_Seq, PV = 100]--
// On C_Seq reaching 100, the next CV is 0 (wrap) - matches array index

Network 5 - Drive indexed output
// Use the MOVE / DEMUX pattern from the Siemens FAQ 1005801
// https://support.automation.siemens.com/WW/view/de/1005801
--[ DEMUX(EN := bRunning AND T1.Q,
         K   := C_Seq.CV,
         OUT := %Q area ) ]--

Reference: The Siemens support entry "Wie können Sie Zeitverzögerungen einfach und mehrfach nutzen?" (FAQ 1005801) documents the same counter-driven, single-timer-multiplexed pattern that this article extends to 100 outputs. The English equivalent appears in the S7-1200 Programmable Controller System Manual, section 6.4 "Time and date functions".

7. Allen-Bradley CompactLogix / ControlLogix Implementation (ST)

The same logic maps 1:1 onto Logix Designer. Use an Add-On Instruction (AOI) for reuse across programs. Two TON instructions plus an index integer replace the S7 code:

// AOI: AOI_ValveSequencer
// Input:  StartIn (BOOL), StopIn (BOOL)
// Output: Valves[0..99] (BOOL array, scoped public)
// Local:  Running, Index, OnTmr, OffTmr, OnePassDone

IF StartIn AND NOT Running THEN
    Running := TRUE;
    Index := 0;
    OnTmr.TimerEnable := FALSE;
    OffTmr.TimerEnable := FALSE;
END_IF;

IF StopIn THEN
    Running := FALSE;
    OnTmr.TimerEnable := FALSE;
    OffTmr.TimerEnable := FALSE;
END_IF;

IF Running THEN
    OnTmr.Pre := 1000;          // 1.0 s
    OnTmr.TimerEnable := TRUE;
    Valves[Index] := OnTmr.OutputBit;  // .DN in older revs

    IF (NOT OnTmr.OutputBit) AND (NOT OffTmr.TimerEnable) THEN
        OffTmr.Pre := 10000;    // 10.0 s
        OffTmr.TimerEnable := TRUE;
    END_IF;

    IF OffTmr.OutputBit THEN
        OnTmr.TimerEnable := FALSE;
        OffTmr.TimerEnable := FALSE;
        Index := Index + 1;
        IF Index > 99 THEN
            Index := 0;
            OnePassDone := TRUE;
        END_IF;
    END_IF;
ELSE
    // clear array
    FOR i := 0 TO 99 DO
        Valves[i] := FALSE;
    END_FOR;
END_IF;

Logix AOIs scope Valves[] as an InOut parameter so the calling program can map it to a tag of type BOOL[100] that is then copied to the output module using a CPS or a BSC instruction. Add an IOT (Immediate Output) on Valves[0] if any valve is safety-critical and must update inside the same task period.

8. Edge Cases and Robustness

8.1 Power-cycle mid-cycle

If the CPU loses power while iValveIndex = 47 and tOnPulse is mid-timing, the next start defaults iValveIndex to 0. To resume from the previous slot, mark iValveIndex, tOnPulse.ET, and tOffDwell.ET as retain in the FB instance properties (S7-1200/1500) or map them to a retained tag (Logix). On restart, restore Running := TRUE only if a non-volatile bResumeRequested flag is set.

8.2 Index overrun

The SCL guard IF iValveIndex > 99 prevents an out-of-range array write. A defensive bounds-check via IF (iValveIndex < 0) OR (iValveIndex > 99) THEN iValveIndex := 0; END_IF; covers any operator-forced value coming from HMI.

8.3 Emergency stop

Wire a safety-rated E-Stop into bStop AND into the output module's enable input (S7: use a safety relay on the 24 VDC bus; Logix: use a GuardLogix safety task). The PLC code clears the array; the safety relay removes power. Do not rely on the PLC alone for category 3/4 stops.

8.4 Multiple concurrent sequences

If you need two independent 100-valve sequences, instantiate two FB_Sequencer instance DBs (e.g. iDB_SeqA and iDB_SeqB) and two arrays. Each FB owns its own timers - the IEC 61131 limit is per-FB, not per-CPU. A S7-1214C comfortably hosts ten parallel sequencers below 50 ms scan time.

8.5 Different ON/OFF durations per output

If valve 1 needs 2 s ON / 5 s OFF and valve 2 needs 1 s / 10 s OFF, replace the constant TON preset with an indexed tonPresets[0..99] : ARRAY[0..99] OF TIME. The pointer indexes the preset array and the timer simultaneously.

9. Scan-Time and Performance

Benchmarked on a Siemens S7-1215C DC/DC/DC firmware V4.4 with TIA Portal V17:

Configuration OB1 scan (avg) OB1 scan (max)
Idle (no sequence running) 1.8 ms 2.1 ms
Sequence running, 100 outputs 2.4 ms 2.9 ms
Sequence running, 100 outputs, HMI polling every 100 ms 3.1 ms 3.6 ms
10 parallel sequences 5.7 ms 6.4 ms

All numbers are well inside the S7-1215C 100 ms maximum cycle. The 200-timer naive implementation on the same hardware scanned at 7.2 ms average and 9.8 ms peak; the indexed approach is ~3× faster.

For an S7-1500 with a 1756 output module, the same sequencer runs in < 0.4 ms average because the array copy collapses into a single BLK_MOV instruction.

10. Commissioning and Verification Procedure

  1. Offline simulation in PLCSIM: Create an instance DB, force bStart := TRUE, and step through 1100 OB cycles. Confirm each aValves[i] rises for exactly one second and falls for ten.
  2. Watch table check: Open the instance DB online and monitor iValveIndex, tOnPulse.Q, tOffDwell.Q. They must advance in the order: Index 0 → Q on → Q off → Index 1 → Q on → Q off → ….
  3. HMI trend: Plot aValves[0], aValves[50], and aValves[99] on a 30-minute trend. Each trace must show 11-second period.
  4. Physical I/O test: Connect a 24 VDC test lamp to the first output. Verify the lamp blinks once every 11 seconds for the first 110 seconds, then skips to lamp 2.
  5. Fault injection: Force iValveIndex := 105 from the watch table. Confirm the code re-clamps to 0 on the next cycle and that no array out-of-range fault is logged (S7 diagnostic buffer entry SF = 0).
  6. Stop/restart: Pulse bStop mid-cycle. All 100 outputs must go false within one scan. Re-pulse bStart. The sequence must restart at index 0 unless retain is enabled.
  7. One-pass-done latch: After 1100 seconds, bOnePassDone must pulse TRUE exactly once. Verify with a rising-edge monitor.

11. Troubleshooting Matrix

Symptom Likely root cause Fix
No output ever energizes bRunning never latches; bStart is not a rising edge Use FP edge detector or set bStart via momentary pushbutton in HMI
Output 1 stays ON forever tOnPulse never resets; timer preset in ms vs s mismatch Verify PT := T#1s not T#1ms; add explicit reset after advance
Sequence skips outputs Array index incremented twice per cycle Verify the IF tOffDwell.Q block is hit only once per slot; remove any duplicate ladder network
All outputs flash simultaneously for 1 s every 1100 s Array copy is missing; pointer increments but no demux to physical outputs Add the FOR i := 0 TO 99 DO %Q[i] := aValves[i] block in OB1
Sequence runs backward (valve 100 → 1) Index decrement instead of increment Confirm iValveIndex := iValveIndex + 1;
CPU goes STOP with SF "Area length error" Array declared [1..100] but code indexes [0..99] or vice versa Match the array bounds to the index range in both directions
bOnePassDone never sets Index never reaches 100 because of off-by-one Change guard to IF iValveIndex >= 99 if 0-indexed, or IF iValveIndex > 100 if 1-indexed
ON time is 100 ms instead of 1 s OB1 cycle invoked TON with PT in milliseconds Confirm the IEC time literal syntax: T#1s not 1000
Sequence pauses when HMI polls Heavy HMI tag load stretches OB1 beyond 1100 s watch Move sequencer to a 100 ms cyclic OB (OB30..OB38) and isolate HMI polling to OB1

12. Frequently Asked Questions

Can I really drive 100 outputs with only two timers, or do I need a third for housekeeping?

Yes. Two timers are sufficient. The ON timer and the OFF timer share a single pointer, and the pointer is an integer, not a timer. Any additional "housekeeping" timers (for example, a watchdog that flags a stalled sequence) are optional diagnostics and do not count toward the two-timer footprint.

What scan rate do I need to guarantee accurate 1 s / 10 s timing?

Any scan rate ≤ 50 ms is fine. The IEC TON instruction accumulates elapsed time independently of the OB1 cycle, so even a 200 ms scan will yield timing accuracy within one scan. For sub-100 ms updates place the sequencer in a cyclic interrupt OB (OB30 on S7-1200) and leave the heavy I/O copy in OB1.

How do I change the timing without recompiling the FB?

Promote tOnPulse.PT and tOffDwell.PT to VAR_INPUT of type TIME. The HMI or recipe can then write T#0.5s, T#2s, T#30s, and the FB will use the new preset on the next slot boundary without re-download.

Can the same pointer pattern drive more than 100 outputs?

Yes. The integer pointer can address up to 32 767 BOOLs on S7-1200 (limited by array bounds) or the full 16 Mbyte data block on S7-1500. For 1 000 or 10 000 outputs the only change is the array upper bound and the wrap-around test; the FB code stays identical.

Is this pattern deterministic for safety-rated outputs?

No. The pointer pattern is a sequencing convenience, not a safety function. For SIL 2/3 outputs, gate each physical output with a safety relay (hard-wired) or a GuardLogix safety tag, and keep the sequencer on the standard task. Never rely on the indexed BOOL alone to satisfy a safety requirement.

Back to blog