S7-200 Timer Subroutine Parameter Limitation Workarounds

David Krause17 min read
S7-200SiemensTroubleshooting
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 Overview: Timers as Subroutine Parameters in S7-200

The SIMATIC S7-200 family of micro-PLCs (CPU 221, 222, 224, 224XP, 226) imposes a hard architectural constraint that catches nearly every programmer migrating from S7-300/400 or who simply attempts to modularize code into subroutines (SBR_0, SBR_1, …). Timers in S7-200 — whether TON (on-delay), TOF (off-delay), or TONR (retentive on-delay) — cannot be passed as IN, OUT, or IN_OUT parameters to a subroutine (SBR). The STEP 7 Micro/WIN compiler rejects such declarations and the online help explicitly enumerates which data types are legal: BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, and STRING.

This restriction trips up two distinct but related design patterns:

  1. Passing a timer operand (e.g., T37) as a parameter to a subroutine so the routine can read/modify its current value and bit. This is legal in S7-300/400 (and in the S7-1200/1500 multi-instance world) but is not legal in S7-200.
  2. Re-using the same timer number in two independent places (main routine and subroutine, or two subroutines) to drive unrelated logic. Even though the call sites are not "parameters," re-using a global timer in this way creates a single shared time-keeping element that both code paths read and write, leading to unpredictable behavior. Effective independence between the two usages is impossible on the S7-200.

This article documents the root cause, the precise rule set enforced by Micro/WIN, the contrast with S7-300/400 parameter passing, and the engineering workarounds that keep modular S7-200 code clean, deterministic, and free of race conditions.

Root Cause: How S7-200 Subroutine Parameters Are Encoded

When STEP 7 Micro/WIN compiles a subroutine call (CALL SBR_0, IN1, OUT1), it generates a parameter list whose entries are limited to fixed-width, value-passed data types. The CPU's interpreter engine for S7-200 (the same instruction set that powers all CPU 22x models) does not implement a symbolic reference to a timer word — it works on direct, absolute timer addresses in the variable-memory map.

The S7-200 variable-memory area is structured as follows for timers:

Area Suffix Read/Write Bit Resolution Word Resolution
Process-image timers (T) T0 … T255 (CPU 224/224XP/226) R/W (bit), R (word current) 1 bit per timer 16-bit current value (count of time-base ticks)
Retentive area timers (T) Power-flow retentive subset R/W 1 bit per timer Backed up on power loss

Each timer instruction in the program body — TON T37, 50 — is a direct reference into this area. The "timer" is not an object with an address-of operator; it is a region of the variable memory. Micro/WIN's subroutine parameter engine, by contrast, emits a 16-bit aligned slot for each declared parameter. There is no compiler path that takes a timer identifier and re-encodes it as a 16-bit slot, because a timer element exposes two coordinated pieces of state (the bit and the 16-bit current value) that the call frame cannot model atomically.

The complete list of legal subroutine parameter types in S7-200 is therefore limited to the elementary scalar and string types that map cleanly to a single CPU register width:

Data Type Width Allowed as IN Allowed as OUT Allowed as IN_OUT
BOOL 1 bit Yes Yes Yes
BYTE 8 bits Yes Yes Yes
WORD 16 bits Yes Yes Yes
DWORD 32 bits Yes Yes Yes
INT 16 bits signed Yes Yes Yes
DINT 32 bits signed Yes Yes Yes
REAL 32 bits IEEE-754 Yes Yes Yes
STRING Variable (max 255) Yes No No
TIMER (T) — No No No
COUNTER (C) — No No Yes (with care)
Note: The COUNTER (C) type is similarly restricted in most S7-200 CPU firmware revisions; treat any counter-as-parameter pattern as undefined behavior unless verified on the specific firmware build (e.g., V2.40 of CPU 224XP).

Why the S7-300/400 Pattern Cannot Be Translated Directly

The S7-300 and S7-400 families — and, in a different form, the S7-1200/1500 — implement "call-by-reference" semantics for FB/FC parameters through their multi-instance DB, instance-DB, or TEMP variable machinery. In those platforms the caller's operand for a parameter of type TIMER is just a 16-bit pointer (or two bytes in the case of S7-300/400) into the IEC timer data block; the called block reads/writes through that pointer, so passing the same timer operand from multiple call sites is a well-defined (if not always useful) operation.

S7-200 has no equivalent. There is no instance DB, no multi-instance, no formal "block parameter" type system beyond the eight scalar/string types above. The closest analog is the indirect addressing capability via &VBx / *VD in pointer operations, but the S7-200 instruction set does not expose an indirect TON operand — you cannot write TON *VD2000, +50 to time a variable timer number. This is a structural limitation of the CPU 22x instruction set, not a compiler bug.

Workaround 1 — Use Distinct Timer Numbers Per Call Site

The cleanest, most maintainable solution is to allocate a unique timer number to every timer usage in the program. S7-200 CPUs provide the following timer pool:

CPU Model Number of Timers (T0…Txxx) Max Concurrent Active
CPU 221 T0 … T255 (256) 256
CPU 222 T0 … T255 (256) 256
CPU 224 T0 … T255 (256) 256
CPU 224XP / 224XPsi T0 … T255 (256) 256
CPU 226 T0 … T255 (256) 256

For 99% of real applications 256 timers is more than enough. Reserve a number range per subroutine and document the convention in the symbol table. Example: a debounce routine uses T40–T49, a pump-seal routine uses T50–T59, and so on.

Example: Subroutine That Owns Its Own Timer

Subroutine SBR_0 implements a 5-second off-delay. The timer is declared in the subroutine body itself; the caller passes only the enable input (BOOL) and the done output (BOOL).

// SBR_0: Off-delay (5 s) — self-contained timer
// Caller: CALL SBR_0, I0.0, Q0.0

NETWORK 1 // Ladder / STL equivalent
LD       I0.0          // Enable input (BOOL parameter)
TOF      T40, +500     // 5.0 s off-delay at 10 ms time base
// STL form
NETWORK 1
LD       I0.0
TOF      T40, +500
NETWORK 2
LD       T40           // Read the timer's done bit
=       Q0.0          // Drive the BOOL output parameter

The caller never sees the timer. If the same subroutine is called twice with the same enable logic, the two calls will share timer T40 and behave unpredictably — the second call pattern solves that with the workarounds below.

Workaround 2 — Pass Time-Base Ticks and Reset Logic Explicitly

If the call site needs to time multiple independent events using the same subroutine, allocate a unique timer number per call and pass it as part of a "timer handle" structure. Although S7-200 cannot pass a timer type, you can pass a WORD representing the current tick count plus a discrete BOOL for the timer's running bit. The subroutine drives the timer; the caller polls the result. This is functionally equivalent to a call-by-reference timer parameter on S7-300/400:

// Caller: Main OB1
NETWORK 1
LD       I0.0           // Enable 1
CALL     SBR_0, VB100,  // Timer 1 handle (current count)
         V100.0,        // Timer 1 done bit (BOOL)
         +200,          // Preset ticks
         I0.0           // Enable input
NETWORK 2
LD       I0.1           // Enable 2 — independent
CALL     SBR_0, VB102,  // Timer 2 handle (current count)
         V102.0,        // Timer 2 done bit (BOOL)
         +200,          // Preset ticks
         I0.1           // Enable input
// SBR_0: Self-decoding timer (uses internal T41)
NETWORK 1
LD       #EN_IN         // BOOL IN parameter
=       L20.0          // Latch for the TON enable
NETWORK 2
LD       L20.0
TON     T41, #PRESET   // Local timer, preset supplied by caller
NETWORK 3
LD       T41
=       #DONE_OUT      // BOOL OUT parameter
NETWORK 4
LD       T41
MOVW    T41, *#HANDLE  // *HANDLE is indirect (operator in Micro/WIN) — see note
Important: The S7-200 does not support indirect operands on the TON/TOF/TONR instruction. The MOVW from T41 into a caller-supplied VW location must be performed with an explicit MOVW into the V-memory word you have already allocated to that call site. The "#HANDLE" pseudo-code above is illustrative; the real implementation must use direct V addresses. See Workaround 4 for the closest practical equivalent.

Workaround 3 — Encapsulate Each Call Site Behind a Wrapper FB-Style Subroutine

Create a small "wrapper" subroutine for every logical timer instance. Each wrapper owns its own T number. The application logic calls the wrapper rather than the generic timer routine. This is the S7-200-friendly approximation of multi-instance OOP from S7-1500:

// SBR_1: Pump A seal-off delay wrapper
NETWORK 1
LD       I0.0          // Pump A run
TOF      T50, +300     // 3 s seal-off
NETWORK 2
LD       T50
=       Q0.0          // Seal-off valve

// SBR_2: Pump B seal-off delay wrapper
NETWORK 1
LD       I0.1          // Pump B run
TOF      T51, +300     // 3 s seal-off (independent)
NETWORK 2
LD       T51
=       Q0.1          // Seal-off valve

The code is larger but every timer is fully independent. Adding a third pump means adding SBR_3 with T52. The cost is symbolic-table discipline; the benefit is a program that is easy to commission and impossible to break by re-using a timer number.

Workaround 4 — Use V-Memory "Software Timers" Driven by an Interrupt

For very large numbers of timer objects (hundreds), the 256-timer hardware pool can be exhausted. The standard S7-200 technique is to build a software timer array in V memory, updated by a timed interrupt (SMB34 / SMB35 interval register). Each "software timer" is a 16-bit WORD that the ISR decrements or increments, and a parallel BOOL array holds the done bit. This pattern also fits the "pass a handle" idea from Workaround 2 cleanly, because the handle is just a pointer (in V memory) to a WORD and a BOOL.

Minimum ingredients:

  1. Configure timed interrupt 0 (INT_0) for a 10 ms or 100 ms period via SMB34 (0–255 ms).
  2. Attach INT_0 to a global "tick" event with ATCH INT_0, 10.
  3. Enable interrupts globally with ENI.
  4. Inside INT_0, decrement every active software timer's tick count and set its done bit when it reaches zero.
  5. Expose each "software timer" to the application as a WORD (current ticks) and a BOOL (done), both stored in V memory, both legal as subroutine parameters.
// INT_0: 100 ms tick — software-timer engine
// (Use the S7-200 interrupt routine, attached to event 10 = timed INT 0)

NETWORK 1  // Decrement timer slot 0 (VW2000 / V2000.0 done bit)
LDW>=  VW2000, +1     // If current >= 1
DECW   VW2000          //   decrement
NETWORK 2
LDW<=  VW2000, +0     // If current == 0
S      V2000.0, 1      //   set done bit
NETWORK 3  // Repeat for slot 1, 2, 3, ... up to N
// (Use a pointer loop for production code)

With this engine in place, "passing a software timer to a subroutine" is just passing the WORD current-value address and the BOOL done-bit address. The subroutine reads/writes them as normal parameters, and the ISR keeps the time-base accurate independent of the main scan.

Workaround 5 — Promote to S7-1200/1500 or STEP 7 in TIA Portal

If the application genuinely requires hundreds of modular, instance-based timer objects, the right answer is hardware migration, not code tricks. S7-1200 (firmware V4.0 and later) and S7-1500 expose the IEC_TIMER / TP / TON / TOF / TONR data type as a multi-instance-capable system data type. In TIA Portal you can declare a multi-instance DB inside a function block, and each FB call owns its own timer — the original design pattern from S7-300/400 is preserved cleanly.

Migration triggers include:

  • More than 32 distinct timer-based routines in a single project.
  • Need to pass timer objects across FB boundaries.
  • Need for retentive behavior across CPU restart with selective reset.
  • Need for IEC 61131-3 compliance (e.g., for functional safety with F-CPU).

Comparison Table: S7-200 vs S7-300/400 vs S7-1200/1500 Timer Passing

Capability S7-200 (CPU 22x) S7-300/400 S7-1200/1500 (TIA)
Hardware timer pool 256 2048 (S7-300) / variable (S7-400) Limited only by work memory
Pass timer as FB/FC parameter No Yes (TIMER type) Yes (IEC_TIMER / TP / TON / TOF / TONR data type)
Multi-instance timers No Yes (instance DB) Yes (multi-instance DB)
Retentive on power loss TONR (configurable subset) Yes (per instance) Yes (per instance, optional)
Indirect TON operand No No (use DB) Yes (array of IEC_TIMER)
Param type BOOL / BYTE / WORD / DWORD / INT / DINT / REAL / STRING Yes Yes (plus more) Yes (plus more, including STRUCT and ARRAY)

Verification: Confirming a Clean Build

After applying any of the workarounds above, run these checks in STEP 7 Micro/WIN (V4.0 SP9 or later is recommended for CPU 224XP/226 firmware compatibility):

  1. Compile the project (PLC > Compile or Ctrl+F9). The status bar must show "0 errors, 0 warnings."
  2. Cross-reference every timer number with Edit > Find > Cross Reference or symbol-table double-click. Every T number used must appear in exactly one network. Re-used timer numbers are an automatic defect.
  3. Open the Symbol Table and verify each T number has a unique, descriptive name. Reject anonymous timers.
  4. Download to the CPU and place it in RUN. Watch the LED behavior: on CPU 224 and above, the SF (system fault) LED must be off and the RUN LED must be solid green.
  5. Force each timer in turn (via status chart or the Force dialog) to confirm the bit toggles, the current value increments, and the done output reflects it after the preset period.
  6. Use the Program Status view (Debug > Program Status) to step through the subroutine and confirm that the timer used in the body is the one referenced in the cross-reference report.

Troubleshooting Matrix

Symptom Likely Cause Diagnostic Fix
Micro/WIN compile error: "Invalid parameter type" on a subroutine declaration Attempted to use a TIMER or COUNTER as a parameter Open the SBR_? Local Variable table; check the Type column for T or C Remove the timer parameter; allocate a unique T number inside the subroutine
Two unrelated routines fire simultaneously when only one enable is active Both routines use the same global timer number Cross-reference the timer number; count the network hits Allocate a unique timer number to each routine
Done bit never goes true, but current value increments Preset value exceeds 32767 (signed INT limit) when the time-base math overflows Read the current value; check the preset type Use a smaller preset + faster time base, or build a long-period software timer
Subroutine runs but produces no output Subroutine was declared but never called, or CALL missing in main Cross-reference SBR_? Add a CALL SBR_? to OB1 with the correct parameter list
SF LED on, RUN LED off, diagnostic buffer: "Indirect addressing error" Pointer arithmetic on a V-memory handle is wrong Open PLC > Information > Diagnostic Buffer Audit every &VBx / *VD usage; never use indirect on a TON operand
Timer counts but does not reset when enable goes false TONR used where TON was intended, or TOF where TON was intended Inspect the instruction in the network Substitute the correct timer type
Code migrated from S7-300/400 fails to compile TIMER/COUNTER parameters used Search the SBR local variable table for type T or C Apply Workaround 1–4 above, or migrate to S7-1200/1500

Engineering Notes and Caveats

  • Time-base math: S7-200 timer presets are integers interpreted as multiples of the time base. TON T37, +500 with a 10 ms time base gives a 5 000 ms delay. TON T32 and T96 are 1 ms timers; T33–T36 and T97–T100 are 10 ms; the rest are 100 ms. Re-using a fast time-base timer (T32) for a long delay will overflow the 16-bit current value at 32 767 × 1 ms ≈ 32.7 s. Choose the time base deliberately.
  • Scan-dependence of done bit: The timer's done bit updates at the end of the scan cycle in which the current value reaches the preset. If a subroutine uses a TON and immediately reads the done bit in the same scan that fired it, the bit will still be 0. Always allow at least one scan between the start of timing and the first read of the done bit — or use the timer's current value comparison (LDD>= T37, +500) directly.
  • Retentive behavior: On power loss, TONR retains its current value, but TON and TOF do not. If the application demands power-loss-resilient timing on the S7-200, store the current value to a retentive V region (VB14 to VB2047, depending on CPU) and restore it on first scan with SM0.1.
  • Subroutine call nesting: S7-200 supports a maximum nesting depth of 8 subroutine calls. Wrapper-heavy designs (Workaround 3) must stay within this limit or the CPU will throw a "nesting stack overflow" fault.
  • String parameter length: STRING parameters occupy one byte per character plus a length byte. A 50-character string uses 51 bytes of the parameter frame. Keep STRING use inside subroutines modest to avoid frame-size surprises.
  • No symbolic timer arrays: Unlike S7-1200, S7-200 has no ARRAY[0..N] OF TIMER type. If you need indexed timer access, build the V-memory software-timer engine described in Workaround 4. The hardware timer pool is fixed at 256 and is not extensible.

Field-Proven Pattern: Modular Subroutine Library

The most maintainable S7-200 programs structure every reusable behavior as a self-contained subroutine with a small, fixed parameter signature. Example structure for a typical machine:

Subroutine Inputs Outputs Owns Timers
SBR_0 — Debounce (rising) BOOL Raw BOOL Filtered T40
SBR_1 — Debounce (falling) BOOL Raw BOOL Filtered T41
SBR_2 — One-shot (rising edge) BOOL In BOOL Pulse (none)
SBR_3 — On-delay BOOL In, INT Preset, INT TimeBase BOOL Out T50–T57 (1 per call site)
SBR_4 — Off-delay BOOL In, INT Preset, INT TimeBase BOOL Out T60–T67 (1 per call site)
SBR_5 — Pulse train BOOL In, INT OnT, INT OffT BOOL Out T70, T71
SBR_6 — Pump seal-off wrapper BOOL Run, INT SealTime BOOL SealValve T80, T81, T82 (3 pumps)

Every subroutine is responsible for its own timer allocation. The main routine (OB1) reads as a flat sequence of CALLs and never touches a T address directly. This pattern survives firmware updates, programmer handovers, and ten-year maintenance cycles.

Summary

S7-200 subroutine parameters are restricted to BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, and STRING. The S7-200 instruction set does not expose the timer identifier as a passable data type, and the CPU does not support indirect addressing of TON/TOF/TONR operands. The effective engineering solutions are:

  1. Use a unique timer number per call site (preferred for small to mid-scale projects).
  2. Pass a V-memory handle (WORD + BOOL pair) and a self-contained software-timer engine (preferred for high-timer-count projects).
  3. Wrap each call site in a dedicated subroutine that owns its own timer.
  4. Migrate to S7-1200/1500 (TIA Portal) when the application genuinely needs hundreds of modular, instance-based timers.

Re-using the same T number in two unrelated code paths is a guaranteed race condition and must never ship. Allocate timers per call site, document them in the symbol table, and verify with cross-reference tools after every code change.

Can I pass a timer (T37) as a parameter to an S7-200 subroutine (SBR)?

No. The STEP 7 Micro/WIN compiler does not allow TIMER as a parameter type. The legal subroutine parameter types in S7-200 are BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, and STRING. Use a unique T number inside the subroutine body or pass a V-memory handle for indirect access.

How many timers can an S7-200 CPU have simultaneously?

All CPU 22x models (CPU 221, 222, 224, 224XP, 224XPsi, 226) provide 256 timers, T0 through T255. The timer time base is 1 ms for T32 and T96, 10 ms for T33–T36 and T97–T100, and 100 ms for the rest. TONR retains its current value through power loss; TON and TOF do not.

Is the same timer number usable in both the main program and a subroutine?

Technically yes — the compiler allows it — but it is functionally wrong. Both code paths will read and write the same bit and current value, producing unpredictable behavior. The robust pattern is one unique T number per call site, enforced via cross-reference checks.

Why does S7-300/400 allow timer parameters but S7-200 does not?

S7-300/400 uses call-by-reference semantics for FB/FC parameters, with the timer data stored in instance DBs. S7-200 has no instance DB mechanism, no pointer-to-timer instruction, and the Micro/WIN parameter engine supports only eight fixed-width scalar and string types. It is an architectural difference, not a firmware bug.

How do I implement hundreds of timers in S7-200?

Build a software-timer engine in V memory driven by a timed interrupt (SMB34 for INT_0, period 5–255 ms). Each software timer is a WORD (current tick count) plus a BOOL (done bit), both storable in V memory and both legal as subroutine parameters. This decouples the timer count from the 256-timer hardware limit.

What is the maximum subroutine nesting depth in S7-200?

8 nested subroutine calls. Designs that wrap every timer in its own subroutine must respect this limit or the CPU will fault with a stack overflow. Use software-timer engines or migration to S7-1200/1500 for deeply nested architectures.

Back to blog