Resolving Siemens S7 STL Loop Failures in Bit-Mirror Code

David Krause19 min read
HMI ProgrammingSiemensTroubleshooting
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 Summary

Analysis of the source STL reveals five distinct defects in the body of the loop, all of which must be removed for the bit-mirror function to operate deterministically. This article walks through each defect, supplies a corrected STL version, a more compact STL that uses SLW (shift-left-word), and a clean SCL replacement. It concludes with a verification procedure, best-practice checklist, and a troubleshooting matrix mapping each symptom to its underlying cause.

Scope: This article targets STEP 7 Classic (V5.x) on S7-300 and S7-400 CPUs. S7-1200/S7-1500 use a different bit/byte/word addressing model (%MW, AT construct) and a different accumulator concept; the STL and the fixes below do not apply to TIA Portal symbolic STL on those platforms. For SCL source generation, the same FC may be reused on any S7-300/400/1500 CPU that supports SCL.

Original STL Program (As Posted)

The user's program reads the input word into a temporary, opens DB4, loads the bit-mask value 1 into temporary #a, loads the loop counter 15 into a double-word literal, and then enters a labeled loop. Inside the loop it ANDs the input word with the current mask, compares the result against the mask itself, and writes the boolean comparison to a DB bit addressed indirectly through AR1. The mask is then doubled and the loop is re-entered until the counter reaches zero.

// Posted STL, annotated with defect call-outs
A   M 10.1            // enable flag from process
OPN DB 4              // open target data block
JNB f                 // skip entire FC if M10.1 = 0
L   1                 // initial bit-mask = 2#0000_0000_0000_0001
T   #a                // a = 1
L   L#15              // loop counter = 15

acb:T   MW 0          // D1: redundant T to MW 0 inside loop body
L   #in               // load input word
L   #a                // load current mask
AW                    // word AND -> result in ACCU1
T   #b                // b = in AND a
LAR1 P#DBX 34.0       // D2: AR1 reloaded on EVERY iteration
L   #b                // load b
L   #a                // load a
==I                   // b == a ?
=   DBX [AR1,P#0.0]   // D3: invalid syntax for indirect DB bit write
JC  exit              // jump out on first match (early exit)
+AR1 P#0.1            // D4: AR1 is about to be reset by LAR1 above
L   #a
L   2
*I                    // a = a * 2
T   #a                // next mask
T   MW 5              // D5: odd-word write overlaps MB5/MB6
T   MW 7              // D5: odd-word write overlaps MB7/MB8
L   MW 0
DEC  1
T   MW 0
L   MW 0
LOOP acb              // repeat while ACCU1-L != 0

exit:S   M 10.0        // done flag
f:  BEU               // block end unconditional

Before diagnosing, note that the program is logically intended to perform a bit-mirror, not a bit-count. Each pass writes a 1 to the destination bit if the corresponding source bit is set, otherwise writes a 0. The conditional early exit on first match (JC exit) is the most damaging semantic defect because it terminates the loop on the first non-zero bit found.

Root-Cause Matrix

The table below summarizes the five defects, the symptom each one produces, and the corrective action required. All five must be fixed for the function to mirror the full 16 bits in one pass.

ID Defect Observed Symptom Fix
D1 T MW 0 at top of loop body Loop counter area (MW 0) overwritten each pass; visual debugging misleading Remove the T to MW 0; let LOOP manage ACCU1-L
D2 LAR1 P#DBX 34.0 inside the loop All 16 bit writes land on DBX 34.0 Move LAR1 initialization before the loop label
D3 = DBX [AR1,P#0.0] indirect write syntax Compiler error or write to wrong memory area Use = [AR1,P#0.0] with OPN DB 4 active
D4 JC exit early exit on first 1-bit Loop stops after bit 0; only DBX 34.0 (or 34.1) is written Remove the conditional jump; let the loop finish 16 iterations
D5 Odd-word writes to MW5 and MW7 MB5/MB6 and MB7/MB8 corrupted, possibly breaking flags, timers, or counters mapped there Remove debug T to MW5/MW7; if a temporary is needed, declare a TEMP word (e.g. #dbg)

Defect D1 — Manual Counter Store Inside the Loop

The LOOP instruction in S7-300/S7-400 STL decrements the value in ACCU1-L (low word of accumulator 1) by 1 and jumps to the specified label if the result is non-zero. The standard pattern is:

L   loop_count          // load initial counter
label: loop_body
L   counter_word         // only needed if loop body overwrites ACCU1
DEC  1                      // optional — LOOP already decrements
T   counter_word
L   counter_word
LOOP label             // decrement and branch

The user's first instruction inside the label is T MW 0. This transfer stores whatever sits in ACCU1 into MW 0. On the very first pass the accumulator contains 15 (from the preceding L L#15), so the net effect is a no-op for the counter. On subsequent passes, however, the accumulator still contains the counter (since LOOP just decremented it and left the value in ACCU1-L). The T is therefore redundant and merely clobbers a memory word that may be used elsewhere in the FC, OB, or by a watch instance in the engineering tool.

Recommendation: Remove T MW 0 from the top of the loop. If you need the current counter value visible online, declare a VAR_TEMP word (for example #lcnt : WORD) and transfer ACCU1 into it once after LOOP with T #lcnt — that way you do not pollute global memory and you avoid a write inside the hot path.

Defect D2 — AR1 Reset on Every Iteration

The LAR1 P#DBX 34.0 instruction is placed inside the loop body, after the AND-mask compare. On each pass it overwrites the address register with the constant pointer P#DBX 34.0. The subsequent +AR1 P#0.1 increments AR1 to P#DBX 34.1, but because the next iteration of the loop immediately re-executes LAR1 P#DBX 34.0, the increment is lost.

The result is that all 16 iterations write to DBX 34.0. The first 1-bit the loop encounters will be reflected there; the remaining 15 bits are silently discarded. If you watch DB4 in the VAT or in a watch table, you will see DBX 34.0 toggle based on the first non-zero bit of #in while DBX 34.1 through DBX 34.7, DBX 35.0 through DBX 35.7 stay at zero.

Fix: Move LAR1 P#DBX 34.0 to the section that runs once, before the loop label. The increment +AR1 P#0.1 then advances AR1 through the destination area correctly:

... enable / OPN DB 4 / load mask / load counter
LAR1 P#DBX 34.0       // <-- one-time initialization
acb:L   #in
L   #a
AW
T   #b
L   #b
L   #a
==I
=   [AR1,P#0.0]       // indirect DB-bit write (D3 fix)
+AR1 P#0.1            // <-- now meaningful; advances to next DBX bit
L   #a
L   2
*I
T   #a
L   MW 0
DEC  1
T   MW 0
L   MW 0
LOOP acb

Defect D3 — Indirect DB-Bit Write Syntax

STL distinguishes two forms of bit assignment:

  1. Absolute bit assignment: = DB4.DBX 34.0 — the parser resolves the bit to a real bit address at compile time.
  2. Indirect bit assignment: = [AR1, P#0.0] — the parser emits an "assign to bit via AR1" opcode; the bit number is added to the byte address held in AR1 at runtime.

Writing = DBX [AR1, P#0.0] is not a recognized combination. The parser either rejects it with "Invalid operand or incorrect operand type" or, on older STEP 7 versions, accepts it and silently produces a write to the wrong area. The correct syntax for an indirect write into the currently opened DB is exactly = [AR1, P#0.0] with OPN DB 4 still active. The DB context is inherited from the most recent OPN DB (or from OPN DI for instance DBs).

Common confusion: LAR1 P#DBX 34.0 and = [AR1, P#0.0] look asymmetric — one mentions DBX, the other does not. The reason is that P#DBX 34.0 is a pointer constant that combines the area identifier (DBX, MB, IB, QB, etc.) with a byte.bit offset, while [AR1, P#0.0] is an address expression that reuses whatever area identifier is already encoded in the pointer held in AR1. They are consistent if you initialize AR1 with the matching area.

Defect D4 — Conditional Early Exit

The JC exit instruction is meant to short-circuit the loop once a match is found. The problem is the loop's intent: a bit-mirror must write to all 16 destination bits, including the zeros. If the user is only trying to count set bits, the conditional exit is correct and the mask comparison is wrong. The two implementations are easy to confuse because they look similar.

Operation Loop body core Loop exit
Bit-mirror (copy bits 1:1) = [AR1, P#0.0] with ==I result Natural end after 16 iterations
Bit-count (count set bits) Increment a counter when bit is set No early exit; complete 16 iterations
First-set bit (find lowest 1) Store mask/index when bit is set JC exit after first match

Remove JC exit if the goal is a full bit-mirror. The S M 10.0 done flag will then be set after the 16th pass, not after the first non-zero bit.

Defect D5 — Odd-Word Memory Overlap (MW5, MW7)

On S7-300 and S7-400, every word address MWn is composed of the two adjacent bytes MBn (low) and MB(n+1) (high). When n is odd, the word straddles two byte addresses, and writing to MWn can collide with other data structures that are pinned to those bytes. The PLC firmware does not warn about this; the corruption is silent.

Word Low byte High byte Collision risk in posted program
MW 0 MB 0 MB 1 Loop counter; will be overwritten if a separate flag is mapped to MB0/MB1
MW 5 MB 5 MB 6 Used as a scratch write — collides with anything the user has mapped there
MW 7 MB 7 MB 8 Same; both odd word writes are unsafe in a multi-developer project

Fix: Delete the T MW 5 and T MW 7 instructions. They do not contribute to the bit-mirror logic and almost certainly originated as a debugging aid that was never removed. If you need a debug value, declare a VAR_TEMP — for example #dbg : WORD — and transfer ACCU1 into it with a single T #dbg. Temporary variables live in the local stack of the calling block and cannot collide with global flags.

Corrected STL Implementation

The following FC body is a clean, deterministic bit-mirror of #in into DB4 starting at DBX 34.0. It is functionally equivalent to the SCL solution proposed in the thread, but expressed in pure STL for engineers who must stay in STL because of project coding standards or because the project predates SCL.

// FC 1 — Bit-mirror input word to DB4.DBX 34.0..DBX 35.7
// Input:  #in    WORD   — source word (16 bits to mirror)
// Output: M 10.0 BOOL   — done flag (set when mirror completes)

A   M 10.1            // enable
OPN DB 4
JNB f                 // skip on enable = 0

L   1                 // initial mask = 2#0000_0000_0000_0001
T   #a                // #a = mask
L   16                // loop counter (16 iterations)
LAR1 P#DBX 34.0       // one-time pointer init

acb:L   #in
L   #a
AW                    // b = in AND a
T   #b
L   #b
L   #a
==I                   // is the isolated bit == 1 ?
=   [AR1, P#0.0]      // indirect bit write into currently opened DB
+AR1 P#0.1            // advance destination pointer by one bit
L   #a
L   2
*I                    // shift mask left by one bit
T   #a
L   MW 0              // reload counter (body overwrites ACCU1)
DEC  1                // manual decrement (the explicit form for clarity)
T   MW 0
L   MW 0
LOOP acb              // decrement ACCU1-L, branch if != 0

exit:S   M 10.0        // set done flag after 16 successful passes
f:  BEU

Counter starts at 16, not 15, because the loop iterates 16 times (bit 0 through bit 15). The ==I comparison produces a 1 if the isolated bit equals the mask (i.e. the bit was set in #in) and a 0 otherwise. The result is written directly to the addressed DBX bit through the open DB context, so DBX 34.0 receives the value of bit 0 of #in, DBX 34.1 receives bit 1, and so on, up to DBX 35.7 receiving bit 15.

Optimized STL Using SLW (Shift Left Word)

// Bit-mirror using SLW
A   M 10.1
OPN DB 4
JNB f

L   #in               // load source
LAR1 P#DBX 34.0       // init destination pointer
L   16                // counter

lp: T   #cnt           // save counter
     L   #in
     L   1             // isolate bit 15 (after 15 shifts) ... see note
     AW
     L   0
     ==I               // is isolated bit == 0 ?
     JCN one           // if non-zero, bit was set
     CLR               // bit was clear -> write 0
     =   [AR1, P#0.0]
     JU  cont
one: SET               // bit was set -> write 1
     =   [AR1, P#0.0]
cont: NOP 0
     L   #in
     SLW 1             // shift source left by 1, lowest bit filled with 0
     T   #in
     +AR1 P#0.1
     L   #cnt
     LOOP lp

exit:S   M 10.0
f:  BEU

Bit-ordering note: The version above mirrors the source such that bit 0 of #in lands in DBX 34.0, bit 1 in DBX 34.1, and so on, because the source is shifted leftward and the lowest bit is read first. If you want bit 15 of #in in DBX 34.0, rotate the loop body to test bit 15 first (use mask 2#1000_0000_0000_0000) and shift right with SRW instead. Always confirm bit ordering with the process requirement, not from the code.

SCL Implementation (Preferred)

SCL is the recommended approach for any bit-mirror, copy, or index-based operation on an S7-300/400. The compiler emits the LOOP/AR1/AW sequence for you, names the variables, and produces a watch-friendly structure. The SCL version of the same function is much shorter and eliminates every defect above by construction.

FUNCTION FC1 : VOID
// Bit-mirror: copy each bit of #in to DB4.DBX 34.0..35.7
VAR_INPUT
  in : WORD;     // source word (16 bits to mirror)
END_VAR
VAR_OUTPUT
  done : BOOL;   // set after 16 bits have been mirrored
END_VAR
VAR_TEMP
  i   : INT;     // loop index
  b   : BOOL;    // working bit
END_VAR

BEGIN
  OPN DB 4;                    // target DB
  FOR i := 0 TO 15 BY 1 DO
    b := (in AND (1 << i)) <> 0;     // test bit i
    DBX[34 + (i DIV 8)].(i MOD 8) := b;  // write to DBX 34+i
  END_FOR;
  done := TRUE;
END_FUNCTION

The SCL FOR loop is functionally identical to the manual STL LOOP construct, but the compiler generates correct AR1/AR2 setup, automatic ACCU management, and the byte/bit decomposition. The DBX[byte_offset].(bit_offset) indexed syntax is the SCL way of writing the indirect DB-bit access that STL expresses as [AR1, P#0.0]. The same FC compiles and runs on S7-300 (from firmware V3.x with the optional SCL package), S7-400, S7-1200, and S7-1500 with no changes.

Engineer field-note: Some legacy S7-300 CPUs (CPU 312, CPU 312C, CPU 313, CPU 314) ship without the SCL optional package. In that case, the FC body must be STL. The SCL source above will fail to compile on those CPUs and STEP 7 will report "SCL compiler not installed". Confirm the CPU's SIMATIC S7-300 product support page for the SCL option availability before writing SCL code in a project.

Verification Procedure

After deploying the corrected STL or SCL code, perform the following checks before running the machine. The tests are ordered from cheapest (online monitor only) to most expensive (I/O loop-back).

  1. Static syntax check (no PLC required): Compile the FC/OB in STEP 7. Corrected STL must compile without warnings. SCL must compile without the message "uninitialized variable" for #b or #cnt.
  2. Single-pass online trace (PLC required): Open the FC in Monitor/Modify. Force M 10.1 = 1. Watch MW 0 count down from 16 to 0, and watch DB4.DBW 34 settle to the expected value. If MW 0 jumps back to a high value, defect D1 is still present.
  3. Bit-pattern unit test: Force #in = W#16#0001. Expect DB4.DBX 34.0 = 1 and every other bit in DB4.DBX 34.0..35.7 to be 0. Force #in = W#16#8000. Expect DB4.DBX 35.7 = 1. Force #in = W#16#FFFF. Expect all 16 destination bits set.
  4. AR1 integrity test: Place a breakpoint (or use single-step in S7-PLCSIM) on the = [AR1, P#0.0] instruction. Read AR1 on pass 0 (expect P#DBX 34.0), pass 1 (expect P#DBX 34.1), pass 8 (expect P#DBX 35.0), pass 15 (expect P#DBX 35.7). If AR1 reverts to P#DBX 34.0 on every pass, defect D2 is still present.
  5. Memory collision test: Force a known pattern into MB 0, MB 1, MB 5, MB 6, MB 7, and MB 8 before the FC runs. Execute the FC. Verify those bytes are unchanged after the call. If MB 5/MB 6 or MB 7/MB 8 changed, defect D5 is still present.
  6. PLCSIM regression run (no hardware risk): Load the project into S7-PLCSIM, scan the FC for 10 000 cycles with randomized #in values, and compare the post-call contents of DB4.DBX 34.0..35.7 against the expected mirror byte-for-byte. See the SIMATIC S7-PLCSIM V5.x manual for cycle-time configuration.

Best Practices for S7 STL Loops

The defects above are extremely common in shop-floor STL code. The following rules, applied at code-review time, prevent them from being merged into a project.

  1. Initialize pointers once, before the loop label. A LAR1 or LAR2 inside a loop body almost always means the address register is being re-anchored each pass, and any +AR1 inside the body becomes a no-op.
  2. Use a TEMP variable for the loop counter, not a global MW. VAR_TEMP #lcnt : WORD keeps the counter in the local stack where it cannot collide with watch-dog or HMI data. Never use odd addresses for #lcnt.
  3. Use even byte addresses for MW/MD. A word at MW n occupies MB n and MB n+1; a double-word at MD n occupies MB n..MB n+3. Pin temporaries to MW 0, MW 2, MW 4, ... and MD 0, MD 4, MD 8, .... If you must use an odd address, document the overlap and add a comment line // MW5 overlaps MB5/MB6 — do not reuse.
  4. For indirect bit writes, the syntax is = [AR1, P#0.0] with the appropriate OPN DB / OPN DI in effect. Never write = DBX[AR1,P#0.0] — it is not a legal combination.
  5. Choose the right control structure. Use the LOOP instruction only for short, fixed-iteration counter scans. For dynamic-length scans, variable stride, or nested indexing, use SCL — the compiler-generated STL is smaller and safer than a hand-written one.
  6. Never combine a bit-mirror loop with a JC exit first-match jump. The two operations look almost identical in STL and a code-reviewer can confuse them. Add a // PURPOSE: mirror or find-first-set comment on the FC header.
  7. Test with a known input pattern first. Force W#16#0001, W#16#8000, W#16#5555, W#16#AAAA, and W#16#FFFF in order. Each one has a known expected destination pattern; if any pattern fails, the defect is reproducible.

Troubleshooting Matrix

Use this matrix when an STL bit-mirror loop does not behave as expected. Walk the table top-to-bottom; the first row that matches the observed behavior identifies the defect class and the corrective action.

Observed behavior Likely defect Corrective action
Loop body never executes; CPU goes to STOP with "OB not loaded" FC called from OB1 but FC is missing; or JNB f skips because M 10.1 = 0 Force M 10.1 = 1, confirm FC is in the S7 program as a called block, recompile
Only DBX 34.0 changes; remaining bits in DBX 34..35 stay 0 D2 — AR1 reset inside loop Move LAR1 P#DBX 34.0 above the loop label
Loop stops after first 1-bit; destination contains exactly one 1 D4 — JC exit early termination Remove JC exit if the goal is a full mirror
Compiler error "Invalid operand" on the bit assignment D3 — invalid = DBX [AR1,P#0.0] syntax Rewrite as = [AR1, P#0.0] with OPN DB 4 active
Random flags set after FC call; HMI shows wrong values D5 — MW5/MW7 odd-word writes clobber flags Remove T MW 5 / T MW 7; use VAR_TEMP instead
Loop counter visible in MW 0, but value never decreases D1 — T MW 0 at top of loop rewrites counter Remove T MW 0 from the loop body
Loop runs 16 times but writes are mirrored incorrectly (bit 0 lands in DBX 35.7, etc.) Bit-ordering or byte-vs-bit byte offset error in LAR1 / SLW choice Confirm whether mask is shifted left (bit 0 first) or right (bit 15 first) and which DBX receives bit 0
CPU goes to STOP with "Area length error" on the indirect write AR1 points outside the opened DB; pointer arithmetic carried past DB end Reduce loop count to 16 and confirm DB4 has at least 2 bytes available at offset 34

Reference Documentation

Engineers reproducing the fix on other Siemens platforms should consult the following primary documentation:

FAQ

Why does my STL loop keep writing all 16 bits to DBX 34.0 instead of DBX 34.0..35.7?

The address register AR1 is being reloaded with LAR1 P#DBX 34.0 on every iteration, so the +AR1 P#0.1 increment inside the loop is immediately overwritten. Move the LAR1 P#DBX 34.0 instruction to a position that runs once, before the loop label.

What is the correct syntax for an indirect bit write into a data block in STL?

Use = [AR1, P#0.0] after an OPN DB <n> (or OPN DI <n>) has opened the target block. The form = DBX [AR1, P#0.0] is not a legal combination; DBX is part of the pointer constant used to initialize AR1, not part of the assignment.

Should I use the LOOP instruction or a FOR loop in SCL for a 16-bit bit-mirror?

Use SCL FOR i := 0 TO 15 whenever the target CPU supports SCL. The compiler emits the same AR1/LOOP sequence but manages ACCU1, ACCU2, and AR1/AR2 correctly, and produces a watchable loop index i. Reserve hand-written STL with the LOOP instruction for CPUs without the SCL optional package, such as older CPU 312/313/314 units.

Why do engineers warn against odd addresses like MW5 or MW7?

On S7-300/S7-400, every word MW n occupies bytes MB n and MB n+1. If n is odd, the word straddles two byte addresses that may be in use by other data blocks, flags, timers, or counters. A write to MW5 will corrupt MB5 and MB6 silently. Use MW0, MW2, MW4, ... and prefer VAR_TEMP for loop-local scratch values.

Is it safe to use the conditional jump JC exit inside a bit-mirror loop?

No — a bit-mirror must write a 0 and a 1 to every destination bit. A conditional exit on the first match will leave the remaining destination bits at their default value. The conditional jump is appropriate only for a find-first-set or bit-count operation, not for a mirror. Remove the JC exit for a mirror and let the loop run all 16 iterations.

How can I test the bit-mirror function without connecting real I/O?

Use S7-PLCSIM V5.x to load the project into a simulated CPU, force a known value into the source word, and read the destination DBW 34 in a watch table. Test with the patterns W#16#0001, W#16#8000, W#16#5555, W#16#AAAA, and W#16#FFFF; each one has a unique expected destination pattern that exposes a single defect class.

Back to blog