Siemens SCL Pulse Pattern: 1s ON, 2s OFF, 10 Cycles

David Krause14 min read
S7-300SiemensTutorial / 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

Problem Statement

A common SCL task on a Siemens S7-300 / S7-400 CPU is to generate a deterministic pulse train: 1 second ON, 2 seconds OFF, repeated ten times, gated by a trip condition (TRIP_CAUSE) and a speed threshold (RPM <= 200). The naive approach wraps two S_ODT (on-delay) calls in a FOR loop, but the loop terminates inside a single OB1 scan, so no time ever elapses. This article explains why the FOR-loop pattern fails, then gives three working SCL solutions for STEP 7 V5.x and the TIA Portal, with full code, parameter tables, and commissioning checks.

Why the FOR Loop With S_ODT Fails

The original failing snippet looks like this:

IF TRIP_CAUSE AND RPM <= 200 THEN
    FOR X := 1 TO 10 BY 1 DO
        T_VAL := S_ODT(T_NO := TIMER_X, S := TRUE,  TV := T#1S, BI := biVal, R := FALSE, Q := BRAKE);
        T_VAL := S_ODT(T_NO := TIMER_X, S := FALSE, TV := T#2S, BI := biVal, R := TRUE,  Q := BRAKE);
    END_FOR;
ELSE
    BRAKE := FALSE;
END_IF;

Three distinct defects make this logic unusable:

  1. No time elapses inside a FOR loop. OB1 is called cyclically; the loop iterates 10 times in microseconds, calls S_ODT 20 times with the same TIMER_X, and exits. The Q output is never observed long enough for the 1 s time constant to expire.
  2. Single timer number, double assignment. S_ODT is called twice on TIMER_X in the same scan with conflicting S and R inputs. The timer is loaded, then immediately reset; BI never accumulates beyond zero.
  3. S_ODT semantics are not idempotent. Each S_ODT call writes the current TV into the timer's accumulator. Resetting on the next call wipes any progress. The block is intended to be evaluated once per scan against persistent timer memory.
Rule of thumb: SCL time-based code must be scan-coherent. The PLC must see the same S input TRUE for as many scans as the time base requires, then see R TRUE to clear. FOR loops are for iterating over arrays, not for timekeeping.

Understanding S_ODT and Scan-Coherent Timing

S_ODT (assign on-delay timer, IEC 61131-3) stores its state in a system timer word assigned by T_NO. The instance must be unique per call site. STEP 7 reserves timer words T0..T255 in S7-300/400; the TIA Portal replaces these with IEC timer DBs but keeps the same calling convention.

Input Type Meaning
T_NO TIMER System timer word (S7-300/400) or IEC_Timer instance (TIA)
S BOOL Start input; rising edge loads TV into the accumulator
TV TIME / S5TIME Preset time (e.g. T#1S, T#2S)
R BOOL Reset; clears accumulator and Q
Q BOOL TRUE when accumulator reaches TV (time elapsed)
BI WORD / S5TIME Current time value, BCD-encoded in classic STEP 7
ET TIME Elapsed time, useful in TIA

Every S7 scan, the CPU updates the time base for active timers. Therefore a pulse generator must rely on the persistent Q state across many OB1 calls. Three robust strategies are shown below.

Solution 1: CPU Clock Memory With Edge-Detection Counter

The most resource-efficient method exploits the CPU's configurable clock memory bits. Enable clock memory in HW Config (CPU Properties > Cycle/Clock Memory) and assign a byte, typically MB0. The 8 bits then toggle at fixed periods:

Bit Period Frequency Use Case
M0.0 0.10 s 10 Hz Fast blink
M0.1 0.20 s 5 Hz Indicator
M0.2 0.50 s 2 Hz Half-second
M0.3 1.00 s 1 Hz Heartbeat
M0.4 2.00 s 0.5 Hz Long blink
M0.5 4.00 s 0.25 Hz Slow blink
M0.6 8.00 s 0.125 Hz Watchdog
M0.7 16.0 s 0.0625 Hz Cycle / shift

Reference the Siemens S7-300 CPU 31x manual for the exact clock-memory byte address; it is set per project. The counter increments on the rising edge of a chosen clock bit. Edge detection is mandatory: if you sample the bit directly, the count would advance by one every scan while the bit is high, not one per pulse.

Working SCL (STEP 7 V5.x, OB1):

ORGANIZATION_BLOCK OB1
TITLE = 'PULSE_1S_ON_2S_OFF_X10'
VERSION : '1.0'
VAR_TEMP
    info : ARRAY[0..19] OF BYTE;
END_VAR
BEGIN
    // M0.3 is the 1 Hz clock bit; "TWO" is its rising-edge flag
    IF START = TRUE THEN
        // Edge detection: count only on 0->1 transition of TWO
        IF TWO = TRUE AND FLANK1 = FALSE THEN
            ZAHL := ZAHL + 1;
        END_IF;
        FLANK1 := TWO;

        // 1s ON = ZAHL 0..1 (2 ticks at 0.5 s base -- see below),
        // 2s OFF = ZAHL 2..5, then reset. Adjust per chosen clock.
        IF (ZAHL >= 0) AND (ZAHL < 2) THEN
            OUT := TRUE;
        ELSE
            OUT := FALSE;
        END_IF;

        IF ZAHL > 5 THEN
            ZAHL := 0;
        END_IF;
    ELSE
        ZAHL   := 0;
        FLANK1 := FALSE;
        OUT    := FALSE;
    END_IF;
END_ORGANIZATION_BLOCK

The same idea scales to n ON cycles and m OFF cycles by changing the ZAHL thresholds. To get exactly 1 s ON / 2 s OFF from a 0.5 s base, run the OB at priority 1 and select M0.2 (0.5 s). For ten full cycles (30 s total), the counter must reach 60; the reset threshold becomes ZAHL > 59 and the ON window is ZAHL < 2.

FLANK1 is not a decorative variable. Without it, ZAHL increments once per OB1 call for the entire half-second the clock bit is high -- hundreds of counts, not one. Always pair a clock bit with explicit edge detection.

Solution 2: Cascaded On-Delay Timers

If clock memory is disabled or you need a period that is not in the standard 8-bit table, build the pattern from two on-delay timers in series. The first timer triggers the OFF interval; its done bit starts the ON timer, and a counter stops the chain after ten cycles.

FUNCTION_BLOCK FB100
TITLE  = 'PULSE_1S_ON_2S_OFF'
VERSION: '1.0'
VAR_INPUT
    GO        : BOOL;   // master enable (TRIP_CAUSE AND RPM <= 200)
END_VAR
VAR_OUTPUT
    BRAKE     : BOOL;   // 1 s ON output
    CYCLES    : INT;    // completed cycles, 0..10
    DONE      : BOOL;   // TRUE when 10 cycles complete
END_VAR
VAR
    T_ON      : S_ODT;  // 1 s ON timer
    T_OFF     : S_ODT;  // 2 s OFF timer
    T_ON_INST : S_ODT_DB;  // multi-instance container (TIA)
    T_OFF_INST: S_ODT_DB;
    C_CYCLES  : CTU;    // up-counter, preset 10
    STATE     : INT;    // 0=idle, 1=ON, 2=OFF
END_VAR
BEGIN
    IF NOT GO THEN
        // hard reset of all elements
        T_ON(S := FALSE, R := TRUE,  TV := T#1S);
        T_OFF(S := FALSE, R := TRUE, TV := T#2S);
        C_CYCLES(CU := FALSE, R := TRUE);
        STATE  := 0;
        BRAKE  := FALSE;
        CYCLES := 0;
        DONE   := TRUE;
        RETURN;
    END_IF;

    // one-shot to start the first cycle
    IF STATE = 0 THEN
        STATE  := 1;
        BRAKE  := TRUE;
        T_ON(S := TRUE, R := FALSE, TV := T#1S);
    END_IF;

    IF STATE = 1 THEN
        T_ON(S := TRUE, R := FALSE, TV := T#1S);
        IF T_ON.Q THEN        // 1 s elapsed
            T_ON(S := FALSE, R := TRUE, TV := T#1S);
            STATE := 2;
            BRAKE := FALSE;
            T_OFF(S := TRUE, R := FALSE, TV := T#2S);
        END_IF;
    ELSIF STATE = 2 THEN
        T_OFF(S := TRUE, R := FALSE, TV := T#2S);
        IF T_OFF.Q THEN       // 2 s elapsed
            T_OFF(S := FALSE, R := TRUE, TV := T#2S);
            C_CYCLES(CU := TRUE, R := FALSE);
            CYCLES := C_CYCLES.CV;
            IF C_CYCLES.Q THEN
                STATE := 0;
                DONE  := TRUE;
            ELSE
                STATE := 1;
                BRAKE := TRUE;
                T_ON(S := TRUE, R := FALSE, TV := T#1S);
            END_IF;
        END_IF;
    END_IF;
END_FUNCTION_BLOCK

This pattern uses two distinct timer instances (T0, T1 in S7-300/400; multi-instances in TIA), which is the only correct way to cascade on-delays. The counter C_CYCLES terminates the chain after ten ON pulses.

State BRAKE Active timer Exit condition Next state
0 idle FALSE none GO rising 1
1 ON TRUE T_ON (1 s) T_ON.Q 2
2 OFF FALSE T_OFF (2 s) T_OFF.Q + counter < 10 1
2 OFF FALSE T_OFF (2 s) T_OFF.Q + counter = 10 0

Solution 3: SCL State Machine With IEC Timer FB

For TIA Portal projects, prefer the IEC timer function blocks (IEC_Timer_0_0, TON, TOF) inside an FB with static instances. They are re-entrant and do not consume the global T0..T255 pool, which is essential for libraries that ship across projects.

FUNCTION_BLOCK "PULSE_GEN"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
    i_go        : BOOL;
    i_t_on      : TIME := T#1S;
    i_t_off     : TIME := T#2S;
    i_cycles_n  : INT  := 10;
END_VAR
VAR_OUTPUT
    q_pulse     : BOOL;
    q_done      : BOOL;
    q_cycle_cv  : INT;
END_VAR
VAR
    ton_on      : TON;          // IEC on-delay
    tof_off     : TOF;          // off-delay for the silent gap
    edge_go     : BOOL;
    state       : INT;          // 0..2 as above
    cv          : INT;
END_VAR
VAR_TEMP
    info        : ARRAY[0..19] OF BYTE;
END_VAR
BEGIN
    // edge-detect GO to (re)start the pulse train
    IF i_go AND NOT edge_go THEN
        state := 1;
        cv    := 0;
        q_done:= FALSE;
    END_IF;
    edge_go := i_go;

    IF NOT i_go THEN
        state  := 0;
        cv     := 0;
        q_pulse:= FALSE;
        q_done := TRUE;
    ELSIF state = 1 THEN
        ton_on(IN := TRUE, PT := i_t_on);
        q_pulse := NOT ton_on.Q;        // TRUE while timing
        IF ton_on.Q THEN
            ton_on(IN := FALSE);
            state := 2;
        END_IF;
    ELSIF state = 2 THEN
        tof_off(IN := FALSE, PT := i_t_off);
        q_pulse := FALSE;
        IF tof_off.Q THEN              // 2 s expired
            cv := cv + 1;
            q_cycle_cv := cv;
            IF cv >= i_cycles_n THEN
                state  := 0;
                q_done := TRUE;
            ELSE
                state  := 1;
            END_IF;
        END_IF;
    END_IF;
END_FUNCTION_BLOCK

Notes on TIA Portal migration:

  • S_ODT is still available in TIA V15+ but is internally remapped to an FB with an instance DB. Prefer TON/TOF for new code.
  • Mark the FB as optimized (S7-1200/1500). On S7-300/400, multi-instances are still required to stay within the limited DB/FB range.
  • Clock memory byte defaults to MB 0 in TIA Portal; it can be reassigned per CPU in Properties > System and clock memory.

Indirect Timer Addressing in SCL

Because timer words are a flat pool (T0..T255), STEP 7 does not allow symbolic access through an array. However, indirect calls are possible with the WORD_TO_BLOCK_DB / BLKMOV pattern, or with the array-of-timer FBs introduced in TIA. A more practical alternative is to instantiate an S_ODT as a multi-instance inside an FB; the compiler then generates the timer word automatically.

FUNCTION_BLOCK FB200
VAR
    timers : ARRAY[1..10] OF S_ODT;   // 10 multi-instance on-delays
END_VAR
VAR_TEMP
    i : INT;
END_VAR
BEGIN
    FOR i := 1 TO 10 DO
        timers[i](S := (i = current_step),
                  R := (i <> current_step),
                  TV := T#1S);
    END_FOR;
END_FUNCTION_BLOCK

This indirect pattern works only when the loop is bounded by a state variable that changes slowly across scans, not by a tight FOR counter. Each iteration is still scan-coherent: every timers[i] is updated once per OB1 cycle, so the IEC timer engine can advance it.

Hardware and Software Prerequisites

Item Requirement
CPU S7-300 (e.g. 315-2 PN/DP, 317-2) or S7-400; S7-1200/1500 for TIA IEC timers
Firmware STEP 7 V5.5 SP4+ for S_ODT semantics; TIA V15.1+ for optimized access
Clock memory Enabled in HW Config, byte typically MB0; verify in CPU online > Module Information
OB1 cycle time Must be < clock bit period (e.g. < 100 ms for M0.0). On 1 Hz use, a 50 ms cycle is safe.
Counter resource One CTU (Z0..Z255) or one INT variable; well within budget
Timer resource S7-300: 256 timers total; S7-400: 256 per CPU rack. Two timers per pulse generator is negligible.

Verification and Commissioning

  1. Online monitor with VAT. Open a Variable Table and force START = TRUE, TRIP_CAUSE = TRUE, RPM = 100. Watch BRAKE toggle at 1 s/2 s for exactly 30 s. The cycle counter CV must hit 10 and DONE must latch.
  2. Trace with S7-PLCSIM or PLCSIM Advanced. In TIA, drop a trace on q_pulse, ton_on.Q, cv; configure a 60 s recording and verify the duty cycle is 1:2 with 10 rising edges.
  3. OB1 cycle time check. In online diagnostics, confirm OB1 runtime is below the smallest clock period used. For M0.3 (1 s) at least 5x margin: OB1 < 200 ms.
  4. Edge detection sanity. Add a comment: FLANK1 captures TWO between scans. If ZAHL jumps by more than 1 on a single rising edge, edge detection is broken.
  5. Stop conditions. With START dropping to FALSE, all outputs must reset within one scan, and a fresh START rising edge must restart the 10-cycle chain from cycle 1.

Troubleshooting Matrix

Symptom Likely cause Fix
BRAKE never goes high Clock memory not enabled, or wrong byte Re-assign clock memory byte in HW Config, download, watch MB0 in VAT
BRAKE latches on, no OFF period S_ODT R input never TRUE; ON timer never reset Confirm second S_ODT call sets R := TRUE; check that T_OFF has a unique T_NO
BRAKE toggles too fast No edge detection on clock bit; counter advances once per scan Add FLANK1 capture-and-compare pattern
Pattern runs 5 cycles instead of 10 Reset threshold on ZAHL or CV is too low Set reset condition to CV >= n and exit before re-entering state 1
OB1 cycle time > 100 ms Heavy communication blocks, or background OB priority issue Move to OB35 (cyclic interrupt, 100 ms typical) and only run pulse logic there
BI value shows odd BCD digits S5TIME base mismatch; TIA vs classic STEP 7 In TIA prefer ET (TIME) over BI; in classic verify W#16#... literals are not used for TV
Second invocation overwrites first timer Same T_NO used in two S_ODT calls Switch to multi-instance pattern or use unique timer words T0..T1, T2..T3, etc.
BRAKE stays high when START drops Reset branch not executed because IF NOT GO is missing Add explicit reset branch that forces BRAKE := FALSE, clears timers, clears counter
IEC_Timer_0_0 compile error in TIA Optimized access mismatch with old library Re-import from "Timers" under Basic Instructions; verify block version >= 1.0

Best Practices and Field Notes

  • Avoid FOR loops for timekeeping. Use them only for batch operations on arrays (sum, max, search). For time-based patterns, prefer IEC timers or clock memory.
  • One timer per logical role. Never call S_ODT twice in the same scan with the same T_NO; the second call is not a state transition, it is a write conflict.
  • Edge detection is not optional. Clock bits are 50% duty-cycle; without an edge flag your counter will count OB1 scans, not pulses.
  • Multi-instance for libraries. When shipping an FB across projects, declare timers and counters as VAR ... END_VAR instances, not global T/Z symbols. This keeps the block portable and re-entrant.
  • Time bases in OB35. For high-precision pulse trains, run the state machine in a cyclic interrupt OB (e.g. OB35 at 100 ms) rather than OB1. The cycle jitter is bounded by the interrupt period.
  • Safety note. If BRAKE drives a real mechanical brake, route the output through a safety relay or F-CPU F-output. The SCL pattern above is logic-only and has no SIL classification; consult IEC 62061 / ISO 13849-1 for the safety function.

Glossary

Term Definition
SCL Structured Control Language, Siemens implementation of ST (IEC 61131-3)
S_ODT Assign on-delay timer; legacy S7-300/400 system timer
Clock memory Peripheral byte whose bits toggle at fixed periods, set in HW Config
Edge detection Boolean pattern detecting 0-to-1 transitions across one scan
Multi-instance Static instance of a block declared inside another FB; conserves DB numbers
OB1 Main cyclic organization block, default 150 ms priority on S7-300
OB35 Cyclic interrupt OB, default 100 ms period

Why does a FOR loop with S_ODT not produce a real 1 s pulse?

A FOR loop completes in microseconds inside a single OB1 scan. S_ODT is a per-scan update block; if the PLC never sees the same S=TRUE input across hundreds of scans, the timer's accumulator never reaches TV and Q never goes high. Use a state machine or clock memory instead.

Can I use the same timer number for two S_ODT calls in different parts of the program?

No. A timer word (T0..T255) is a single global resource; assigning it to two blocks causes undefined behavior because each call overwrites the previous accumulator. Use multi-instance FBs (TIA) or distinct T_NO values (classic STEP 7).

What is the difference between S_ODT and IEC TON in TIA Portal?

S_ODT is the legacy call that maps to a system timer word. IEC TON is an FB with a static instance and uses the IEC 61131-3 PT/ET interface. TIA Portal S7-1200/1500 prefer TON; S7-300/400 accept both, and STEP 7 V5.x code with S_ODT migrates automatically.

How do I pick a clock memory bit for a 1 s pulse?

Enable clock memory in HW Config. The default byte MB0 gives M0.0 = 0.1 s, M0.1 = 0.2 s, M0.2 = 0.5 s, M0.3 = 1.0 s. For a 1 s ON / 2 s OFF pattern, M0.2 (0.5 s) is ideal because both periods are integer multiples of the base.

Do I need a counter if I already have a FOR loop from 1 to 10?

Yes. A FOR loop is not a counter; it does not remember state across scans. Use a CTU IEC counter or a static INT variable incremented on a clock-bit edge. The FOR loop can then iterate over an array of ten pulse profiles if needed.

Can the pattern run in OB35 instead of OB1?

Recommended for high-precision or fast patterns. Configure OB35 with a 100 ms period and run the state machine there. OB1 then only needs to read the BRAKE output and handle the trip-condition logic.

Back to blog