Problem Overview: CPU STOP on Indexed Array Access in STL
When you try to write a bit into a static ARRAY OF BOOL inside a Function Block (FB) using a runtime-supplied index, the S7-300/S7-400 CPU will drop into STOP if you reach for the most obvious STL operand. The symptom is consistent: the PLC goes into STOP the moment the network executes, and the diagnostic buffer reports an illegal area pointer or an unknown operand area. The cause is a misuse of the global data operand DBX where the instance data operand DIX is required.
The original code that triggers the STOP looks like this:
LAR2 P##Index_Value // load address of the input variable into AR2
SET // RLO = 1
= DBX [AR2, P#2.0] // attempt to set a bit in the *current* DB
The PLC goes to STOP because DBX is resolved against the DB register pair DB1 (open global DB). If no global DB is open at the moment the network executes - which is the normal case inside an FB - the operand area is undefined and the CPU raises an addressing error. S7-300/S7-400 reference manuals describe this in the Statement List (STL) for S7-300 and S7-400 Programming reference manual under the area-internal and area-crossing addressing chapters.
DBX out of a global DB (DB1, DB100, etc.) and paste it into a multi-instance or single-instance FB. Siemens does not generate a warning at compile time - the error only surfaces at the first execution of the affected network.Root Cause: DBX versus DIX, DB1 versus DB2
Every S7-300/S7-400 CPU maintains two parallel address registers for the current data context:
| Register | Mnemonic prefix | Resolves to | Typical use |
|---|---|---|---|
| DB1 |
DBX, DBB, DBW, DBD
|
Open global data block | Global DBs (DB100, DB200, ...) |
| DB2 |
DIX, DIB, DIW, DID
|
Open instance data block | Instance DB of the executing FB |
Inside an FB, the runtime opens the instance DB at FB entry and closes it at FB exit. AR2 is automatically pre-loaded with the address of the start of the instance data area of the current FB. This is why indirect access patterns inside FBs almost always take the form [AR2, P#x.y] or DIX[AR1, P#x.y] - the AR1/AR2 register pair is the canonical scratch register for indirect addressing on S7-300/400.
The two offending lines therefore become correct when you swap the operand area and the address register:
LAR2 P##Index_Value
SET
= DIX [AR2, P#2.0] // writes the bit into the *instance* DB
The editor will then display the symbolic name of the static that lives at that offset, e.g. SENSOR[0]. If you only change the DBX to DIX but leave AR2 pointing at a local temp or input, the symbolic view disappears and you are back to pointer arithmetic on your own.
[AR2, P#x.y] accesses for every static, input, output, in-out, and temp reference. Clobbering AR2 corrupts all of them and produces the dreaded "very unexpected results" with no diagnostic buffer entry. If you need a second pointer, use AR1.Register-Indirect Addressing with AR1 and AR2
Indirect addressing in STL on the S7-300/400 family is documented in two flavors in the STL for S7-300/S7-400 Programming manual:
-
Memory-indirect addressing - the address is held in a marker, DB, or DI word/double word (e.g.
DBW[MD0]). -
Register-indirect area-internal addressing - the address is held in AR1 or AR2 and combined with a constant pointer (e.g.
DIX [AR1, P#2.0]).
For an array access where the index is computed at runtime, register-indirect addressing with AR1 is the cleanest pattern. The recipe is:
- Compute the byte offset of the indexed element from the start of the array (index × element size in bits, divided by 8 for the byte offset).
- Load the address of the array's first element into AR1. Because the array is static, the easiest way is to use the compiler-generated pointer
P##ArrayNameand add the FB-internal base pointer (AR2) to it. - Add the computed byte offset to AR1.
- Use
DIX [AR1, P#0.0]for the actual access. The constantP#0.0is a stub because AR1 already contains the full byte/bit address.
This pattern is exactly what the SCL compiler emits when you write a simple indexed assignment (see the SCL section below), so once you understand it you can read the generated STL without fear.
Computing the Byte Offset for the Variable Index
For an array element the byte offset from the start of the array is given by:
byte_offset = (index - lower_bound) * element_size_bytes + dim2_offset + dim3_offset
For a one-dimensional ARRAY[0..50] OF BOOL, this collapses to byte_offset = index * (1 bit / 8) = index / 8. Because the index is an INT and we want a DINT-compatible pointer arithmetic result, the typical STL snippet uses three double-word operations:
| Step | STL | Purpose |
|---|---|---|
| Subtract lower bound |
L L#0-D
|
Normalize index to zero-based; L#0 if the array starts at 0, otherwise the lower bound. |
| Multiply by element size (in bits) |
L L#1*D
|
1 bit for BOOL, 8 bits for BYTE, 16 bits for INT, 32 bits for DINT/REAL, 64 bits for LREAL. |
| Add 2-D offset |
L L#0+D
|
Computed from the second index; usually 0 in a one-dimensional case. |
| Add 3-D offset |
L L#0+D
|
Computed from the third index; usually 0 in a one-dimensional case. |
| Add instance base |
TAR2+D
|
AR2 holds the FB's instance data area base; this turns the relative pointer into a full DI-area pointer. |
| Store to AR1 | LAR1 |
AR1 is now the address of the indexed element inside the instance DB. |
For a ARRAY[0..50] OF INT, the multiplier is 16, not 1. For a ARRAY[0..50] OF REAL, the multiplier is 32. The same pattern works for STRING, where you also need to add the 2-byte header offset; in practice, however, you would not runtime-index a STRING array in STL - you would use SCL or a parameterized FB.
STL Implementation: Step-by-Step Code
Putting it all together, here is the canonical STL implementation for a variable-index write into a static BOOL array inside an FB with an instance DB:
FUNCTION_BLOCK FB 1
TITLE = Variable-index bit set
VERSION : 0.1
VAR_INPUT
Index_Value : INT; // 0..50 expected
END_VAR
VAR
Sensor : ARRAY [0 .. 50] OF BOOL;
END_VAR
BEGIN
NETWORK
TITLE = Indexed bit set in instance DB
TAR2 ; AR2 (DI base) -> ACCU1
LAR1 P##Sensor ; pointer to start of Sensor array (DB-relative) -> AR1
+AR1 ; AR1 = AR1 + AR2 = absolute DI address of Sensor[0]
L #Index_Value; runtime index
+AR1 ; AR1 += index (works for BOOL because each element is 1 byte at the bit level via P#0.0)
SET
= DIX [AR1, P#0.0]
END_FUNCTION_BLOCK
The pointer arithmetic above is correct for BOOL only because each BOOL occupies one bit, and the addition of an INT to a byte-granular pointer is implicitly scaled by the operand width when the access uses DIX [AR1, P#0.0]. For wider types the explicit L#n *D pattern is mandatory:
TAR2
LAR1 P##Sensor
L P#0.0 // alternative: use P# to do the byte math
+AR1 // (some editors prefer this form)
L #Index_Value
ITD // INT -> DINT for the multiply
L L#0
-D // lower-bound normalization, here 0
L L#1
*D // BOOL = 1 bit
L L#0
+D // 2-D offset (none)
L L#0
+D // 3-D offset (none)
+AR1 // add to base pointer in AR1
SET
= DIX [AR1, P#0.0]
The form generated by the SCL compiler is identical in structure - see the comparison below.
SCL Alternative and Compiler-Generated STL
If you have the SCL package installed, the same function block is dramatically shorter and the compiler will produce STL that you can copy verbatim into a pure-STL block:
FUNCTION_BLOCK FB10
VAR_INPUT
Index: INT;
END_VAR
VAR
Sensor : ARRAY[0..50] OF BOOL;
END_VAR
BEGIN
Sensor[Index] := TRUE;
END_FUNCTION_BLOCK
The STL that the SCL compiler emits for the assignment is:
SET
SAVE
= L 0.1
L #Index
ITD
L L#0
-D
L L#1
*D
L L#0
+D
TAR2
+D
LAR1
= DIX [AR1, P#2.0]
SAVE
BE
Notice the offsets: P##Sensor in our hand-written code resolved to P#0.0 for the first element, but in this generated STL the final constant is P#2.0. That is because the SCL compiler packed the BOOL array starting at byte offset 2 inside the instance data area - the first two bytes are the saved RLO bits L 0.1 and the FB status word, and the array begins immediately after. Always inspect the compiler's output with Project > Compile > STL if you are porting a generated snippet back into a hand-written block, otherwise the offset will be wrong by a few bytes and you will write into the wrong static.
BLKMOV Caveat: Why the Block Move Is the Wrong Tool Here
A common shortcut attempt is to use BLKMOV (SFC 20) to copy the index into the sensor array, since both are 16-bit-wide operands. The call would look like:
CALL "BLKMOV"
SRCBLK := #Index_Value
RET_VAL := #Return_Value
DSTBLK := #Sensor
This appears to work, but it copies the 16 bits of Index_Value as raw memory into the first 16 bits of the sensor array. Because the value 8 stored in an INT is W#16#0008 (binary ...0000 0000 0000 1000), the copy lands bit 3 in bit position 3 of byte 3 of the array, i.e. Sensor[3.3], not Sensor[8]. The two interpretations are:
| Index_Value (decimal) | Bit pattern | Bit lit by BLKMOV | Bit you actually wanted |
|---|---|---|---|
| 8 | ...0000 1000 | Sensor[3.3] | Sensor[8] |
| 11 | ...0000 1011 | Sensor[2.0], Sensor[2.1], Sensor[3.3] | Sensor[11] |
| 65535 | ...1111 1111 1111 1111 | Sensor[0..15] | Sensor[65535] (out of range) |
Use BLKMOV only when the source and destination are the same element width and the index is being copied as data, not interpreted as an array position.
Bounds Checking and Safety Patterns
STL will not bounds-check the index for you. If Index_Value is negative or larger than 50, the resulting pointer will land somewhere inside the instance DB that you did not intend - possibly on a different static, possibly on the FB's own local stack pointer, possibly on the area identifier. Add explicit bounds checking:
NETWORK
TITLE = Bounds check 0..50
L #Index_Value
L 0
<I // Index < 0 ?
JC ERR // jump if RLO = 1 (in S7-300 STL)
L #Index_Value
L 50
>I // Index > 50 ?
JC ERR
// ... pointer arithmetic and bit set as above ...
JU CONT
ERR: SET
= #Index_Error
CONT: NOP 0
On S7-400, the comparison operators <I and >I set the RLO when the condition is true; on S7-300 the polarity is reversed and you must invert with NOT or use <>I for "less than or equal" patterns. Verify against the STL for S7-300 and S7-400 Programming reference for the exact CPU type.
Multi-Dimensional Array Offsets
For a two-dimensional array such as Sensor : ARRAY[0..7, 0..3] OF BOOL, the byte offset from the start of the array is:
offset_bits = (i - i_lo) * (j_hi - j_lo + 1) + (j - j_lo)
which in STL is built as two consecutive multiply-and-add blocks. The first block computes the i-axis contribution and the second block adds the j-axis contribution. The third +D block in the canonical pattern above is reserved for a k-axis if you ever need a three-dimensional array. The SCL compiler emits exactly this pattern, which is why reverse-engineered SCL output is such a useful template for hand-written STL.
S7-1200 and S7-1500 Indirect Addressing
On the S7-1200 and S7-1500 families the addressing model is different: there is no DB1/DB2 register pair in the same sense, and the legacy STL operand forms DBX/DIX with [AR1, P#x.y] have been replaced by symbolic, slice-aware access and the PEEK/POKE variants in the extended STL. The TIA Portal V20 Indirect Addressing in STL (S7-1200/S7-1500) topic describes the modern equivalent: Memory-indirect addressing and Register-indirect area-internal addressing with the legacy AR1/AR2 are still available, but the recommended pattern is to use Variant / DB_ANY and the PEEK / POKE instructions. If you are migrating an FB that uses the S7-300/400 pattern shown above to a 1500, plan to refactor to symbolic access or to SCL rather than trying to port the AR1/AR2 recipe verbatim.
Verification and Commissioning Steps
- Compile the FB in STEP 7 V5.x or in TIA Portal. The compiler should not emit any warnings about unknown operand areas.
- Open the instance DB in the watch table and confirm that the starting byte of the
Sensorarray is at the offset shown by the compiled STL's finalP#x.yconstant. If the constant isP#2.0, the array begins at byte 2 of the instance data area; if it isP#0.0, the array is the first static. - Force
Index_Value = 0, execute one scan, and verify in the watch table thatSensor[0]isTRUE(i.e. byte 0 bit 0 of the array is 1) and that all other bits are unchanged. - Repeat with
Index_Value = 7,15, and50. Each must light exactly one bit at the correct position. - Force
Index_Value = -1and51. The CPU must not enter STOP; the bounds-check error flag must be set. - From the HMI or the watch table, verify that the online help in the SiePortal STL reference matches the addressing model in use - the Help on STL program SiePortal entry is a useful pointer to community-maintained examples, but always cross-check against the printed reference manual.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| CPU goes to STOP on first scan |
DBX used instead of DIX with no global DB open |
Replace DBX with DIX, or open the desired global DB with OPN first. |
| Correct bit gets set, but the bit just before it is also set | Element width mismatch (used L#1 *D for an INT array) | Set the multiplier to the bit width of the element: 8 for BYTE, 16 for INT/WORD, 32 for DINT/DWORD/REAL, 64 for LREAL. |
| Bit is set in the wrong static, not in the array | Offset constant from the SCL compiler does not match the FB layout after a hand edit | Recompile the SCL source and copy the new P#x.y constant, or use LAR1 P##Sensor to let the compiler resolve the offset. |
| Works in STL, fails after switching CPU to a 1500 | AR1/AR2 register-indirect addressing is not the recommended pattern on S7-1500 | Refactor to SCL, or use PEEK / POKE per the TIA Portal V20 reference. |
| AR2 is corrupted after a network that uses the new access | Code wrote to AR2 instead of AR1 | Reserve AR1 for pointer arithmetic inside FBs; never touch AR2. |
| Bit is set in the wrong byte inside the array | Lower-bound normalization not performed for arrays declared as [L..H] with L != 0 | Subtract the lower bound with L L#L / -D before multiplying. |
Why does the S7 CPU go to STOP when I use DBX inside an FB?
DBX is resolved against the open global DB (DB1 register). Inside an FB the runtime opens the instance DB but does not open a global DB, so the operand area is undefined and the CPU raises an addressing error on the first execution. Use DIX (instance data) to write into the instance DB, or use OPN DBnnn to explicitly open the global DB first.
Can I just write SENSOR[#Index_Value] in STL like I do in SCL?
No. STEP 7 STL on S7-300/S7-400 only accepts literal integer constants as array indices. Variable indices require register-indirect addressing with AR1/AR2, or a rewrite in SCL. The SCL compiler internally generates the AR1/AR2 code shown above.
What multiplier do I use for non-BOOL arrays?
Use the bit width of the element before the *D instruction: 1 for BOOL, 8 for BYTE, 16 for INT/WORD, 32 for DINT/DWORD/REAL, 64 for LREAL. Load the constant as L#n into ACCU1 immediately before the *D.
Why does BLKMOV with the index light the wrong bit in my BOOL array?
BLKMOV copies the raw bit pattern of the source into the destination. A value of 8 in an INT is binary 0000 0000 0000 1000, so BLKMOV writes bit 3 in byte 3 of the array (Sensor[3.3]) instead of bit 8 (Sensor[8]). Use the pointer-arithmetic pattern, not BLKMOV, for indexed array access.
Is the same code valid on S7-1200 and S7-1500 CPUs?
Register-indirect area-internal addressing with AR1/AR2 is still available in the legacy STL on S7-1500, but it is not the recommended pattern. For new code, prefer SCL or the PEEK/POKE instructions described in the TIA Portal V20 indirect addressing reference. Legacy code that runs on S7-300/S7-400 should be reviewed and refactored during a 1500 migration.