S7 PLC Loop vs Inline Code: Scan Time Optimization Guide

David Krause12 min read
Best PracticesSiemensTIA Portal
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

Overview: The Loop vs Inline Trade-off in S7 PLCs

On Siemens S7-300, S7-400, S7-1200, and S7-1500 controllers, every line of STEP 7 or SCL code costs execution time. The decision between a FOR/WHILE loop with parameterized index versus repeated in-line calls with absolute addresses is one of the most consequential choices a programmer makes for scan time. The rule of thumb is counterintuitive for engineers used to high-level languages: loops save program memory but almost always cost more scan time than equivalent in-line code, because every iteration pays a fixed overhead for counter management, branch evaluation, and—on the S7 architecture—indirect addressing through AR1/AR2 address registers. A loop is a control flow construct that allows code to be executed repeatedly, and on a PLC that construct is implemented without hardware caching, so every fetch is observable in the instruction timing.

This article quantifies the overhead, walks through a 64-motor example, and gives a decision matrix for selecting the right pattern on real S7 hardware. All instruction-time references point to the official Siemens Programming and Operating Manual for the relevant CPU family.

Prerequisites: S7 Scan Time Fundamentals

Before picking a control flow pattern, the engineer must understand the three scan components the S7 CPU reports in Module Information > Online > Diagnostic Buffer and via SFC 78/79:

Component Source Typical Range (S7-1500) Typical Range (S7-1200)
OB1 cycle time Main cyclic OB 1–20 ms 1–50 ms
Process image update PI/PQ update at OB start/end 200–800 µs 150–600 µs
Communication share PG/OP, S7 comm, OPC UA 5–30% of cycle 5–40% of cycle

The S7-1500 CPU manual (SIMATIC S7-1500 CPU 1518-4 PN/DP manual, entry ID 109751826) lists instruction execution times down to 10 ns for bit operations. Loop and indirect-addressing primitives fall in the 100 ns to 4 µs range, so a poorly written loop can dominate a fast cycle.

How a Loop Executes on an S7 CPU

Whether you write the loop in STL, SCL, or ladder, the compiled code on an S7-300/400 uses the address registers AR1 and AR2 plus the DBW/DIW indirect-addressing modes. On an S7-1500, the same constructs are compiled to native machine code but still pay a branch-prediction penalty. The minimum per-iteration overhead is:

  1. Load loop counter from local data (L LW x) — typically 60–100 ns on S7-1500.
  2. Compare against limit (>=I or =I) — 30–60 ns.
  3. Conditional jump (JC/JCN) — 30–60 ns; pipelines flush on taken branch.
  4. Decrement counter and store (DEC, T LW x) — 60–120 ns.
  5. Indirect addressing setup when the loop body uses indexed operands: LAR1 P##DB or similar — 200–400 ns on S7-300/400, 60–120 ns on S7-1500.
  6. Indexed load/store (L DBW [AR1,P#0.0]) — 200–600 ns depending on operand width.

Sum of fixed overhead: ~400–900 ns per iteration on S7-1500, ~1.0–2.5 µs per iteration on S7-300/400, before the loop body itself runs. A body that does a few bit operations and a single data move may execute in 300 ns; the loop scaffolding more than doubles the cost.

How In-line Code Executes

In-line code uses absolute addresses (e.g., L DB101.DBW 0, L DB101.DBW 2) that the compiler resolves at compile time. On both S7-300/400 and S7-1500, this produces a flat sequence of fixed-length instructions with no indirect addressing and no per-iteration branch. The CPU's instruction prefetch and the absence of a taken branch mean each line is fetched once per scan and executed back-to-back with no pipeline stall.

The trade-off is straightforward:

  • Pros: Predictable timing, easy to single-step in monitor, easy to set breakpoints, no address-register contamination across FC calls.
  • Cons: Larger program memory footprint, multiple code-change touchpoints, harder to scale to N > 16 items.

Quantifying the Cost: A Worked Example

Consider 5 lines of code repeated 5 times. In-line the body costs exactly 5 × 5 = 25 instruction lines per scan, all fixed-address. A loop with a counter, end test, decrement, and indirect addressing adds roughly 5–8 extra lines of scaffolding. The compiled loop body executes (5 + 5) × 5 = 50 effective line-units per scan (5 lines of body × 5 iterations, plus 5 lines of loop control executed 5 times). On a representative S7-315-2 PN/DP at 200 ns per typical instruction, the in-line version consumes ~5 µs and the loop version ~10 µs. The loop is ~2× slower for this case.

Now reverse the scenario. Suppose the body is 50 lines (motor start/stop with interlock, scaling, alarm evaluation) repeated 64 times. In-line code is 50 × 64 = 3,200 lines. A loop with the same body is 50 (body) + 6 (scaffolding) = 56 lines executed 64 times = 3,584 effective line-units, only ~12% slower per scan, while consuming ~95% less program memory and allowing a single edit point for bug fixes. At 200 ns per line, 3,584 lines = ~717 µs extra in the loop case, an acceptable cost for 64 instances of motor logic.

Scenario In-line lines per scan Loop lines per scan Loop penalty Memory saved by loop
5 body × 5 reps 25 50 ~2.0× None
10 body × 10 reps 100 160 ~1.6× ~40%
50 body × 64 reps 3,200 3,584 ~1.12× ~95%
200 body × 32 reps 6,400 6,592 ~1.03× ~98%

The crossover sits between repetition counts of 4 and 8: below 4, in-line is the clear winner; above 8 with a non-trivial body, the loop's per-iteration overhead amortizes below 10%.

When In-line Code Wins

  • Repetition count ≤ 4: the loop scaffolding is more expensive than the saved lines.
  • Time-critical subroutines: high-speed packaging, cam profiling, fast interrupts (OB35/OB82 time) where every microsecond matters.
  • Variable iteration cost: when each pass does conditional work, loops add branch overhead inside the body.
  • Commissioning pressure: in-line code is single-steppable and breakpoint-friendly; a faulted motor instance can be debugged without disturbing others.

When a Loop Wins

  • Repetition count ≥ 8 with a body of ≥ 10 lines.
  • Variable N from configuration: the count comes from a recipe, machine option, or HMI-set parameter. In-line code would require 64 conditional calls (IF N>1 THEN Call FCMotor(DB:=1); ... IF N>64 THEN Call FCMotor(DB:=64);) which the loop collapses to one parameterized call.
  • Memory-constrained S7-1200 CPUs: program memory is tighter than work memory; loops preserve program area at the cost of a few microseconds of scan.
  • Single edit point: a bug fix in the body takes effect for all instances automatically.

SCL Patterns and Their Costs

The same trade-off exists in SCL (Structured Control Language). A FOR i := 1 TO n DO ... END_FOR; compiles to a counted loop. The relevant SCL constructs and their relative costs on S7-1500:

SCL Construct Per-iteration overhead (S7-1500) Notes
FOR i := 1 TO n DO ~80 ns Counter in local; integer compare.
WHILE cond DO ~120 ns Condition evaluated every iteration.
REPEAT ... UNTIL cond ~100 ns Body executes at least once.
Indexed arr[i] +40–80 ns vs fixed index Bounds check on SCL arrays.
CASE dispatch (SCL) +200 ns per case Compiled jump table.

For SCL on S7-1500, the SCL compiler in TIA Portal can sometimes unroll short FOR loops when the trip count is a compile-time constant. Verify by inspecting the generated STL under Project > Compile > Show generated code. Reference the SCL programming manual (SIMATIC S7-1200/1500 SCL manual, entry ID 109751926) for the exact rules.

Practical Pattern: 64 Motor Example

The 64-motor example from the field is a textbook case for loops. In SCL on S7-1500:

FUNCTION_BLOCK FB_MotorArray
VAR_INPUT
  NumberOfMotors : INT;  // 1..64 from HMI/recipe
END_VAR
VAR
  i : INT;
END_VAR

FOR i := 1 TO NumberOfMotors DO
   FCMotorSingle(DBAccess := i);   // FC uses ANY-pointer or DB-number parameter
END_FOR;

The corresponding in-line pattern is unworkable at 64 instances and slower below 4 instances. The loop runs in ~64 × (1.2 µs body + 0.4 µs scaffolding) = ~103 µs at 1.6 µs per iteration. A flat in-line version would force 64 conditional calls, each adding a compare (~30 ns) plus the call overhead (~150 ns), totaling ~12 µs of test overhead alone before the body runs — and the body must still be unrolled to 64 copies in program memory.

Caution on indirect addressing: when the FC parameterizes its DB number, the S7-300/400 generates code that loads the DB number into DB register, opens the DB, then accesses offsets. Each OPN DB [index] costs ~1.5 µs on S7-315. On S7-1500, the same pattern is ~150 ns because the DB number can be passed in an ANY pointer that the CPU resolves in microcode. Always profile both CPU generations before committing to a pattern.

Optimization Techniques When You Must Loop

  1. Hoist invariants out of the loop. Compute constants, fetch HMI scaling factors, and load pointers before the FOR.
  2. Use local temp variables for the loop counter; never read the counter from a DB inside the body.
  3. Pre-decrement and branch in STL: L L#0; T #i; L #n; JU LOOP; LOOP: L #i; +1; T #i; L #n; <I; JC body; JU end; body: ... ; L #i; L 1; -I; T #i; JU LOOP; end: NOP 0; trades clarity for a few hundred ns per iteration.
  4. Unroll the loop manually for small N. A 2-iteration loop in-line is rarely faster than 2 in-line bodies; the compiler usually handles this, but verify with the SCL compiler's optimization report.
  5. Avoid ANY pointers inside the loop body on S7-300/400. Pass the DB number as INT, use OPN DI [#dbNum], and access offsets with AR1-based addressing.
  6. Move the loop to a lower-priority OB. OB35 (cyclic interrupt) at 100 ms is a common target for non-time-critical batching logic, leaving OB1 free for fast I/O.

Optimization Techniques When You Must In-line

  1. Use symbolic addresses with the same DB instance — the compiler resolves them once.
  2. Group related bits into a single DWORD and operate on the whole word with UW/OW/XW to halve the instruction count.
  3. Combine consecutive A/O ladder rungs into a single network — each network boundary costs a few ns on the S7-300/400.
  4. Replace L / T pairs with direct assignment (MOV on S7-1500) where the compiler allows.

Verification: Profiling the Choice on Real Hardware

After implementing either pattern, follow this verification procedure on the running CPU:

  1. Open TIA Portal, connect online to the target device, and navigate to Online > Diagnostics > Cycle Time.
  2. Note OB1 min, max, and current cycle time. Record under steady-state load (no HMI polling spikes).
  3. Use the trace function (Project tree > Traces) to record OB1_PI_SERVICE and OB1_CYCL_TIME from the Time system clock over 60 seconds.
  4. For a focused measurement, add a RD_SYS_T timestamp call at the start and end of the candidate routine and store the delta in a real-time-monitorable tag.
  5. Compare against the spec — most S7-1500 applications target OB1 < 20 ms; S7-1200 typically < 50 ms. The Siemens S7-1500 CPU manual lists the per-instruction timings used in the cycle budget.

Troubleshooting Matrix

Symptom Likely Cause Fix
Scan time spikes when N motors changes Loop trip count rises; OPN DB executed N times Pre-open the DB in OB1, or move logic to a cyclic OB at fixed rate
OB1 cycle drifts upward over time Memory leak from POKE in loop, or unbounded WHILE Bound all loops with explicit trip count; use SFC 87 C_DIAG to inspect DBs
Indirect address faults (SF LED, BF) AR1/AR2 not saved across FC calls in loop Save AR1/AR2 at loop FC entry, restore at exit; on S7-1500, prefer temporary tags
HMI update slows when changing motor count Loop recomputes every scan, not every 100 ms Move scaling into OB35 or use SFC 26/27 (UPDAT_PI / UPDAT_PO) on a slower rate
Compiler generates unexpected code SCL optimizer disabled in project settings Enable Compile > SCL > Optimization in TIA Portal; rebuild; inspect generated STL

Decision Flowchart

  1. Is the repetition count a compile-time constant < 4? → In-line.
  2. Is the body ≤ 5 lines? → In-line.
  3. Is the body executed in OB35, OB82, or a ≤ 1 ms cyclic interrupt? → In-line if count < 16, else profile the loop.
  4. Is N read from a configuration/recipe? → Loop with parameterized FC call.
  5. Is N ≥ 8 and body ≥ 10 lines? → Loop, hoist invariants, verify with cycle-time trace.
  6. Is memory consumption the binding constraint? → Loop.
  7. Is commissioning speed the binding constraint and N < 8? → In-line for breakpoint clarity.

Field-Proven Cautions

No CPU cache means predictable timing. S7 instruction times published by Siemens assume every fetch is from main memory. There is no L1 cache that would mask the cost of a taken branch or a complex addressing mode. Code that "looks slow" in STL is exactly that slow at runtime.
AR1/AR2 leakage. On S7-300/400, every FC that uses indirect addressing must save AR1 and AR2 on entry and restore on exit. A loop that calls 64 FCs in a row propagates the address-register state; a bug in one FC's save/restore can corrupt the next FC's data access and produce a fault that is hard to attribute.
STEP 7 SCL vs TIA Portal SCL. Optimization behavior differs between STEP 7 V5.x and TIA Portal SCL. Always profile on the target firmware; do not assume instruction counts carry over between versions. Reference the current TIA Portal help under Programming and Operating Manual > SCL.

Frequently Asked Questions

Does a FOR loop in SCL on S7-1500 ever get unrolled automatically?

Yes, when the trip count is a compile-time constant and the SCL optimizer is enabled in TIA Portal project settings, the compiler may unroll small loops. Inspect the generated STL to confirm, and measure OB1 cycle time before and after the change.

Is indirect addressing the only loop overhead, or does the counter also cost cycles?

The counter, decrement, compare, and branch together cost ~200–400 ns per iteration on S7-1500 and ~1.0–2.0 µs on S7-300. Indirect addressing adds another ~60–600 ns depending on operand width. Both contribute; the counter is usually the larger share on S7-1500.

Can I use a loop for safety-critical logic on a fail-safe CPU?

Yes, but F-CPU execution times are longer and the safety cycle has a fixed budget. Profile the loop on the F-CPU using the F-runtime group's diagnostic data. Keep loop trip counts small and avoid ANY pointers in safety code.

How do I measure the scan-time cost of one specific routine?

Wrap the routine with RD_SYS_T calls, subtract the two timestamps, and store the delta in an HMI-visible tag. For sub-microsecond resolution, repeat the routine 1000 times in a loop and divide by 1000.

Does the S7-1500 cache instructions like a PC CPU does?

No. Siemens instruction timing is specified without cache effects. Treat every instruction as fetch-from-memory for timing purposes. See the S7-1500 CPU manual entry ID 109751826 for the published instruction times.

Back to blog