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.
%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.
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:
-
Absolute bit assignment:
= DB4.DBX 34.0— the parser resolves the bit to a real bit address at compile time. -
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).
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.
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).
-
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
#bor#cnt. -
Single-pass online trace (PLC required): Open the FC in Monitor/Modify. Force
M 10.1 = 1. WatchMW 0count down from 16 to 0, and watchDB4.DBW 34settle to the expected value. If MW 0 jumps back to a high value, defect D1 is still present. -
Bit-pattern unit test: Force
#in = W#16#0001. ExpectDB4.DBX 34.0 = 1and every other bit in DB4.DBX 34.0..35.7 to be 0. Force#in = W#16#8000. ExpectDB4.DBX 35.7 = 1. Force#in = W#16#FFFF. Expect all 16 destination bits set. -
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 (expectP#DBX 34.0), pass 1 (expectP#DBX 34.1), pass 8 (expectP#DBX 35.0), pass 15 (expectP#DBX 35.7). If AR1 reverts toP#DBX 34.0on every pass, defect D2 is still present. - 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.
-
PLCSIM regression run (no hardware risk): Load the project into S7-PLCSIM, scan the FC for 10 000 cycles with randomized
#invalues, 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.
-
Initialize pointers once, before the loop label. A
LAR1orLAR2inside a loop body almost always means the address register is being re-anchored each pass, and any+AR1inside the body becomes a no-op. -
Use a TEMP variable for the loop counter, not a global MW.
VAR_TEMP #lcnt : WORDkeeps the counter in the local stack where it cannot collide with watch-dog or HMI data. Never use odd addresses for#lcnt. -
Use even byte addresses for MW/MD. A word at
MW noccupiesMB nandMB n+1; a double-word atMD noccupiesMB n..MB n+3. Pin temporaries toMW 0, MW 2, MW 4, ...andMD 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. -
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. - 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.
-
Never combine a bit-mirror loop with a
JC exitfirst-match jump. The two operations look almost identical in STL and a code-reviewer can confuse them. Add a// PURPOSE: mirror or find-first-setcomment on the FC header. -
Test with a known input pattern first. Force
W#16#0001,W#16#8000,W#16#5555,W#16#AAAA, andW#16#FFFFin 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:
- SIMATIC S7-300 CPU 31xC and CPU 31x Operating Instructions — hardware reference and instruction timing for the LOOP opcode on S7-300 CPUs.
- SIMATIC S7-400 Automation System S7-400 System and Programming Manual — description of AR1/AR2, DB context, and the OPN DB / OPN DI instructions.
-
STEP 7 V5.x STL and SCL Programming Manual — canonical syntax for the LOOP instruction, the LAR1/LAR2 indirect addressing forms, and the
= [AR1, P#0.0]indirect assignment. -
S7-300/400 SCL Programming Manual — reference for the indexed
DBX[byte].(bit)syntax used in the SCL version of the bit-mirror function.
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.