Siemens Step 7 STL: Loading Symbolic Pointers into AR1
Overview
When a Step 7 STL program walks a complex DB structure (an array of UDTs, nested STRUCTs, a multi-axis data block) and accesses elements through the address registers AR1 and AR2, the engineer quickly hits a hard compiler limitation: LAR1 / LAR2 cannot accept a fully qualified symbolic name. The CPU rejects the syntax L "Machines".ETN[0].Task.Speed.X_Axe / LAR1 at compile time, because the compiler cannot fold a multi-level symbolic expression (especially one with a runtime index) into the 32-bit area-internal pointer that LAR1 consumes.
This reference explains exactly why the limitation exists, what the compiler does accept, and three production-tested patterns used in the field to work around it:
- Pass the structure (or its starting address) into an FB as an
IN_OUTparameter, load that parameter's pointer withP##, and combine it with a per-element offset. - Copy the symbolic source DB into a generic working DB with
BLKMOV(SFC20) and walk the working copy with absolute offsets. - Migrate the indexed loop to SCL, where
"Machines".ETN[i].Task.Speed.X_Axeis a legal expression that the compiler resolves to a pointer automatically.
Each pattern has different trade-offs in work-memory footprint, scan-time cost, and how brittle the code becomes when the schema changes. The article closes with verification steps, a troubleshooting matrix, and a short FAQ.
Why Symbolic Addresses Cannot Be Loaded Directly Into AR1
Step 7 STL compiles the symbol table at download time. The compiler maps every symbolic name to a fixed (DB number, byte offset, bit offset) triple and stores that mapping offline. The online STL editor, however, only generates machine code for the instructions you see in the source listing. There is no STL opcode of the form:
L "Machines".ETN[0].Task.Speed.X_Axe
LAR1
The L instruction only loads constants, accumulators, address-register contents, or pointer literals of the form P#byte.bit. The compiler has no way to fold a three-level symbolic path into a P#... literal at the call site, because the path may contain runtime indexes (here [0], but [i] for the next loop iteration). For runtime indexes the address has to be computed at execution time, which is precisely the job of LAR1 plus pointer arithmetic.
The reverse direction works fine: T DBW [AR1, P#0.0] is legal because the operand is a memory identifier with an indexed form, and STL has explicit memory-indirect syntax. The forward direction is the one that fails.
For the same reason, the following constructs are also illegal and the compiler will raise an "Invalid operand" or "Unknown instruction" error during the build:
L "Machines".ETN[0].Task.Speed.X_Axe
TAR1 "Machines".ETN[0].Task.Speed.X_Axe // not an opcode
LAR1 "Machines".ETN[0].Task.Speed.X_Axe // not an opcode
L P##<FB parameter> / LAR1, and it only accepts FB/FC parameters and OB temporaries. It does not accept a global DB tag.STL Address Registers in One Page
The S7-300/400 CPU provides two 32-bit address registers, AR1 and AR2. They are loaded by LAR1 / LAR2, saved by TAR1 / TAR2, exchanged by CAR, and incremented by +AR1 / +AR2. The 32-bit value held in an address register is an area-internal pointer with this layout:
| Bit range | Content |
|---|---|
| 31..24 | Area code. 10001xxx = data block (DB or DI), 10000xxx = instance DB. Other encodings exist for cross-area pointers. |
| 23..16 | Byte address within the area (0..65535). |
| 15..3 | Always zero. |
| 2..0 | Bit address within the byte (0..7). |
The pointer's "zero" representation is P#0.0, which is the byte-and-bit offset 0 inside whatever area the next memory instruction uses (DB if a DB is open, DI otherwise). Indirect memory access uses this syntax:
| Operand | Meaning |
|---|---|
DBW [AR1, P#0.0] |
Word at DB + AR1 + 0.0 |
DBB [AR2, P#10.0] |
Byte at DB + AR2 + 10.0 |
DIX [AR1, P#2.0] |
Word at instance DB + AR1 + 2.0 |
DBD [AR1, P#4.0] |
Double word at DB + AR1 + 4.0 |
LW [AR1, P#4.0] |
Local word at L-stack + AR1 + 4.0 |
MB [AR1, P#0.0] |
Bit-memory byte + AR1 + 0.0 |
The constant in the square brackets is the "intra-area offset"; the CPU adds it at runtime to AR1 and accesses the resulting byte in the area specified by the operand (DBW = data block, DIX = instance DB, etc.). The CPU checks that the resulting byte is inside the loaded area, otherwise it raises an area-length error.
For more detail, see the SIMATIC S7-300 CPU reference manual and the "Programming with STEP 7 STL" manual on the Siemens Industry Online Support portal.
Pattern 1 — Pass the Structure into an FB, Then P## the Parameter
The only legal way to get a symbolic pointer into AR1 at runtime is to use P##, and P## only accepts an FB parameter (IN, OUT, IN_OUT, STAT, TEMP) or an OB temporary. Globally declared DB tags are not accepted as P## operands.
FB Declaration
FUNCTION_BLOCK FB 17
VAR_IN_OUT
pData : STRUCT // receives a pointer to the structure base
X_Axe : INT;
Y_Axe : INT;
END_STRUCT;
END_VAR
VAR_INPUT
iIndex : INT; // runtime element index (when array base is passed)
END_VAR
VAR_TEMP
dwPtrRet : DWORD;
END_VAR
BEGIN
NETWORK
TITLE = Load parameter pointer into AR1 and add the per-element offset
L P##pData; // 6-byte pointer to the parameter slot
LAR1 ;
L iIndex; // runtime index i
ITD ; // INT -> DINT
SLD 5; // *32 bytes per element (UDT element size)
+AR1 ; // AR1 now points to element i
L 30; // value to write
T DBW [AR1, P#0.0]; // indirect write
END_FUNCTION_BLOCK
Call Site
CALL FB 17 , DB 100
pData := "Machines".ETN[0].Task.Speed
iIndex := #i
Key Rules
- The parameter must be typed
POINTER,ANY, or a UDT/STRUCT. For complex parameters the compiler acceptsP##of the parameter and returns a pointer to its base. -
L P##<param>produces a 6-byte constant in the FB's static area; load it into ACC1 and transfer to AR1 withLAR1. - The element size must be known to the FB so it can multiply the runtime index. If the elements are a UDT of 32 bytes (declared in the DB), the FB hard-codes
SLD 5(= *32). If the UDT size changes later, recompile the FB, or useBLKMOVto learn the element size at runtime (Pattern 2). -
VAR_IN_OUTis preferred overVAR_INPUTfor the data structure:VAR_INPUTof a complex parameter is passed by value (the FB has its own copy), so modifying the parameter slot does not affect the source tag.
Why Only FBs
The compiler can produce a P## reference for an FB parameter because the FB is its own scope: the parameter address is known relative to the FB's DI block. For OB temporaries the same applies relative to the L stack. For an arbitrary global DB tag, the compiler would have to insert an instruction sequence that fetches the tag's DB number and byte offset from the symbol table at runtime, which STL does not implement. SCL does implement it; that is the core of Pattern 3.
Pattern 2 — Copy the Symbolic Structure into a Generic DB with BLKMOV
When the structure is large and the FB-pass approach becomes awkward, or when the loop must stay in pure STL, declare a second DB with the same UDT and copy the live data into it once per scan, then walk the copy with absolute offsets.
Step-by-Step
- Declare UDT100 with the same structure as
Machines.ETN. - Declare DB200 with one element of UDT100 named
ETN(this is the working copy). - In OB1, before the indexed access, copy the symbolic block with BLKMOV:
CALL "BLKMOV" // SFC20 SRCBLK := "Machines".ETN[0] RET_VAL:= MW 200 DSTBLK := "Machines-General".ETN - After BLKMOV returns 0 in RET_VAL, AR1 can point into
Machines-Generalwith absolute offsets:L P#82.0 LAR1 L 30 T DBW [AR1, P#0.0]
Pros and Cons
- (+) Pure STL, no SCL compiler required.
- (+) The walking loop is identical to "classic" STL indirect programming.
- (-) The copy doubles the work memory for that structure.
- (-) The offsets are brittle: if you change the UDT, every
P#82.0,P#4.0, etc. has to be re-derived. - (-) BLKMOV costs scan time; for large structures on a slow CPU this matters.
SFC20 BLKMOV Parameters
| Parameter | Declaration | Meaning |
|---|---|---|
| SRCBLK | INPUT (ANY) | Source area (DB, M, I, Q, L, etc.) |
| RET_VAL | OUTPUT (INT) | Return code; 0 = OK, negative = error |
| DSTBLK | OUTPUT (ANY) | Destination area |
Source and destination must NOT overlap. Both ANY pointers must be fully resolvable (DB number + byte offset + length). Typical RET_VAL error codes for BLKMOV include:
| RET_VAL | Meaning |
|---|---|
| 0 | No error |
| 8091 | Source area exceeds the data area boundary |
| 8092 | Destination area exceeds the data area boundary |
| 80A1 / 80A2 | Source or destination ANY pointer alignment error |
| 80B1 / 80B2 | Source or destination area not loaded (DB not opened, M area not configured) |
SFC20 is documented in the Siemens "System Software for S7-300/400 System and Standard Functions" reference.
Pattern 3 — Migrate to SCL
SCL treats symbolic names as first-class expressions, including array indexing with a runtime variable. The same loop the original STL snippet was trying to express becomes:
FOR i := 1 TO 32 DO
"Machines".ETN[i].Task.Speed.X_Axe := 30;
"Machines".ETN[i].Task.Speed.Y_Axe := 30;
END_FOR;
No address register, no P##, no BLKMOV. The SCL compiler emits the same AR1/AR2 sequence you would have written by hand, but it tracks the offsets through the symbol table automatically. This is the most maintainable option for any non-trivial indexed access.
Caveats When Migrating
- The data block used as the destination must be a global DB, not an instance DB of an FB used with
CALLfrom SCL inside the same FB. Use a separate global DB. - Ensure the SCL source has
ACCESSofDB(default for global DBs). For instance DBs, the keyword isSTRUCT/END_STRUCTinside the FB declaration. - Set the compiler option "Generate reference data for SCL" if you want cross-references after build.
- SCL code that the compiler cannot optimize into a single AR1 sequence is slower than hand-written STL for tight loops. Profile with the SCL
RUNTIMEmeasurement if scan time is critical.
Combining Patterns
In real projects the patterns are combined. A typical layout:
- One UDT holds the per-element layout.
- One global DB holds the array of UDTs (the "production" data).
- One FB receives the array base pointer via
IN_OUTand exposesRead(i)/Write(i, value)methods that hide the address-register arithmetic. - STL code that needs indexed access calls the FB; STL code that touches a single known index uses absolute offsets from the FB parameter pointer.
- An SCL block (e.g., an OB or an FC) drives the loops.
This keeps the UDT as the single source of truth for the structure layout: changing the UDT propagates automatically through the FB and the SCL blocks. Only the absolute offsets in any remaining "raw" STL have to be re-validated.
Verification
After implementing either pattern, verify on the CPU with the following checklist:
- Download the project and place the CPU in
RUN. Watch the diagnostic buffer for SF events. - Open the symbol table and double-click the FB; confirm the instance DB has been created with the expected size.
- Use
Monitor/Modifyon the destination DB. After the write, the value at the indexed element should equal what the STL wrote. - Force a breakpoint inside the FB at the
T DIB [AR1, P#0.0]line. Step through and inspect AR1 in the STL monitor; it should display asP#xx.xwherexxequals the symbol's byte offset. - Run a CPU
Memory Resetand re-download if AR1 looks like garbage on the first scan (some FBs have init logic in the static area that must run first). - In
PLC -> Module Information -> Diagnostic Buffer, confirm no area-length or area-cross errors during the BLKMOV call.
If the value still does not appear at the expected offset, the most likely cause is that the pointer loaded by P## is a pointer to the parameter slot inside the FB's instance DB rather than to the source tag. Re-check the parameter declaration (POINTER, ANY, or typed STRUCT) and ensure the call site passes the actual tag, not a constant.
Common Errors and Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
Compiler error "Unknown instruction" on LAR1 <symbol>
|
STL has no opcode for symbolic LAR1 | Use Pattern 1 (FB + P##), Pattern 2 (BLKMOV), or Pattern 3 (SCL) |
| CPU SF after BLKMOV with RET_VAL = 8091 | Source or destination ANY pointer points outside the DB | Check that SRCBLK and DSTBLK reference existing DB numbers and lengths |
| CPU SF with "Area length error" on indirect access | The intra-area offset crosses the DB boundary | Reduce the element index or extend the destination area |
| Written value appears at the wrong byte offset | Element-size constant hard-coded in the FB no longer matches the UDT size | Re-derive the shift count from sizeof(UDT) or move to SCL |
AR1 holds P#0.0 after L P##p
|
The FB parameter is declared VAR_IN (by value) |
Change to VAR_IN_OUT or VAR_TEMP so the address is meaningful |
| CPU SF "DB not loaded" | The destination DB is not opened before the indirect write | Add OPN for the correct DB |
| CPU SF on first scan after FB instance creation | Pointer parameter is uninitialised until the first call sets it | Initialise the parameter slot in the FB's startup logic or use a flag to skip the first scan |
Performance Notes
-
L P##<param>costs effectively zero at runtime — it is a 6-byte literal embedded in the FB code. - Indirect memory access (
T DBW [AR1, P#0.0]) on S7-300/400 takes one micro-cycle per access, identical to direct symbolic access. - BLKMOV is implemented in firmware and runs at bus speed. For a 1 KB structure it costs roughly 100 µs on a CPU 315-2 PN/DP. For larger structures or slower CPUs, prefer Pattern 1 or Pattern 3.
- Pattern 1 keeps the work-memory footprint low (just the FB instance DB) but requires manual offset tracking inside the FB. Pattern 2 doubles the work memory footprint for the structure. Pattern 3 keeps footprint low but can produce longer machine code for nested access.
FAQ
Can I write LAR1 with a fully symbolic DB tag in Step 7 STL?
No. The STL instruction LAR1 only accepts a constant P# literal, another address register, or the contents of the accumulator. A symbolic path like "Machines".ETN[0].Task.Speed.X_Axe is not a legal operand. Pass the structure into an FB and use L P##<param> / LAR1, copy the data with BLKMOV into a generic DB, or rewrite the loop in SCL.
What is the difference between P## and a regular pointer load?
L P##<parameter> loads the 6-byte intra-area pointer (or a pointer to the parameter slot for complex parameters) into ACC1, which you then transfer to AR1 with LAR1. It is the only STL way to get a symbolic address into an address register at runtime. It is legal only for FB/FC parameters and OB temporaries; it is illegal for global DB tags.
Why does BLKMOV work while LAR1 does not?
BLKMOV (SFC20) accepts an ANY pointer parameter, which the compiler builds from any symbolic expression including indexed tags. SFC20 runs in firmware and uses the ANY pointer directly. LAR1 is an STL instruction with a fixed opcode grammar; it cannot consume a symbolic expression.
Is there an STL instruction equivalent to P## for global DBs?
No. For global DB tags you must either copy the data with BLKMOV into a generic DB and access it with absolute offsets, or migrate the loop to SCL. Some programmers use a setup FB that stores the tag's DB number and byte offset in a static DWORD and then loads it with L / LAR1, but that requires manual offset tracking.
Does this also apply to S7-1500 STL?
S7-1500 supports a limited STL subset and still does not allow symbolic operands on LAR1. The same three patterns (FB pass-by-reference, BLKMOV, SCL) apply, with the simplification that on S7-1500 you can also use optimized block access and ARRAY[*] (flexible array bounds) to keep the loop entirely symbolic without explicit pointer arithmetic.