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:
-
No time elapses inside a FOR loop. OB1 is called cyclically; the loop iterates 10 times in microseconds, calls
S_ODT20 times with the sameTIMER_X, and exits. TheQoutput is never observed long enough for the 1 s time constant to expire. -
Single timer number, double assignment.
S_ODTis called twice onTIMER_Xin the same scan with conflictingSandRinputs. The timer is loaded, then immediately reset;BInever accumulates beyond zero. -
S_ODT semantics are not idempotent. Each
S_ODTcall writes the currentTVinto 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.
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.
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_ODTis still available in TIA V15+ but is internally remapped to an FB with an instance DB. PreferTON/TOFfor 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
-
Online monitor with VAT. Open a Variable Table and force
START = TRUE,TRIP_CAUSE = TRUE,RPM = 100. WatchBRAKEtoggle at 1 s/2 s for exactly 30 s. The cycle counterCVmust hit 10 andDONEmust latch. -
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. - 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.
- 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.
-
Stop conditions. With
STARTdropping to FALSE, all outputs must reset within one scan, and a freshSTARTrising 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_ODTtwice in the same scan with the sameT_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_VARinstances, 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
BRAKEdrives 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.