Siemens S7 STL Memory-Indirect Addressing with AR1/AR2

David Krause12 min read
S7-300SiemensTutorial / How-to
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

Statement List (STL) on the S7-300/S7-400 supports two flavors of indirect addressing: memory-indirect and register-indirect addressing. The first variant uses two 32-bit accumulators (ACCU 1 / ACCU 2) to hold a pointer, while the second uses the dedicated address registers AR1 and AR2. The two registers are identical in capability; STL simply reserves them as the default address registers for register-indirect operations such as L DBB[AR1,P#630.0], L MB[AR2,P#160.0], and T PQB[AR1,P#0.0].

The pointer that AR1/AR2 carry is not a plain byte index. It is a packed 32-bit value where the lower three bits encode the bit number (0–7) and bits 3–18 encode the byte offset (0–65535). This is why you must shift a byte value left by three (SLW 3) before loading it into an address register — and why a value of 248 inside AR1 decodes to P#31.0 rather than P#248.0.

This article decodes that format, walks through the exact operations used to map an AS-i CP342-2's peripheral I/Q into a memory mirror (MB140–MB175), and provides a verification procedure you can use in STEP 7 V5.x or TIA Portal with a classic S7-300 CPU.

Platform note: S7-1500 CPUs do not have AR1/AR2 in the same way. TIA Portal V20 documents the equivalent indirect addressing for the S7-1500 in Indirect addressing in STL (S7-1500), where the tag-relative pointer (P#"DB".Static_1) replaces the address-register model. The byte-bit packing rules described below still apply to the lower 24 bits of the pointer for the S7-1200/1500 variants.

Prerequisites

  • STEP 7 V5.5 SP2 (or higher) for classic S7-300/400 STL editing, or TIA Portal V13.1+ for projects migrated to V13.1+ — see the Siemens STEP 7 Professional V13.1 entry on indirect addressing.
  • A CPU that supports indirect STL: 31x, 31xC, 31xT, 41x, or 41xH. CPUs 312, 312C and lower omit some indirect forms; AR1/AR2 indirect is supported on every S7-300/400 CPU.
  • Knowledge of the basic STL operations: L (load), T (transfer), SLW (shift left word), LAR1 / LAR2 (load AR1/AR2), TAR1 / TAR2 (transfer AR1/AR2 to ACCU).
  • For the worked example: two CP342-2 AS-i masters, configured at logical base addresses PI/PQ 120…135 and PI/PQ 140…155, and a free memory area at MB140…MB175.

Pointer Format Specification

A 32-bit pointer used in AR1/AR2 has the layout shown below. The Siemens convention is:

Bit:   31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
       [--- must be 0 for area-internal ---] [----- area / ID -----] [-------------- byte --------------] [bit]
       0  0  0  0  0  0  0  0  0  0  0  0  0  0  Y  Y  Y  Y  Y  Y  Y  Y  Y  Y  Y  Y  Y  Y  Y  x  x  x
  • Bit 0–2 (xxx) — bit address, 0…7. Always 0 for byte-oriented transfers.
  • Bit 3–18 (YYYYY…YYYYY) — byte offset, 0…65535. This is the value that the operand is fetched from or written to, relative to its area.
  • Bit 19–30 — area identifier (must be 0 for area-internal pointers, otherwise a cross-area pointer with bits selecting I, Q, M, DB, PI/PQ, etc.).
  • Bit 31 — must be 0 for area-internal pointers (the kind AR1/AR2 use with P# notation in the offset field).

For area-internal pointers the upper 13 bits are zero and the lower 19 bits hold the byte-bit address. That is why the maximum byte offset with P# notation is 65535.7.

Decoding the 248 Example

When the discussion reports a pointer of 248 = 11111000 binary, the breakdown is:

248 decimal  =  0000 0000 0000 0000 0000 0000 1111 1000  binary
Bit 0–2      =  000  → bit offset 0
Bit 3–7      =  11111  → lower 5 bits of byte offset = 31
Bit 8–18     =  00000000000  → upper 11 bits of byte offset = 0
Byte offset  =  00000000000_11111  =  31
Result       =  P#31.0

The trailing two zeros in 11111000 are the bit address; the leading five ones encode the byte address (31). So 248 decodes to P#31.0, not P#248.0. This is the single most common source of confusion when reading code that uses SLW 3.

SLW 3: Converting a Byte Index to a Pointer

Because the bit address occupies the lowest 3 bits of the pointer, a plain byte number must be shifted left by 3 to slot into the byte-offset field. The SLW 3 instruction does exactly that — it multiplies the 16-bit ACCU1-L by 8, which is the inverse of the decoding you saw above.

      L   #Temp2            // byte index, e.g. 31
      SLW 3                 // 31 * 8 = 248 = P#31.0
      LAR1                  // load AR1 with the pointer

Worked examples:

Source value (byte) SLW 3 result (decimal) Binary (bits 0–18) Decoded pointer
0 0 …00000_000 P#0.0
1 8 …00000_001_000 P#1.0
15 120 …00000_1111_000 P#15.0
31 248 …00000_11111_000 P#31.0
120 960 …00000_1111000_000 P#120.0
140 1120 …00001_0001100_000 P#140.0
175 1400 …00001_0101111_000 P#175.0

Use the SRW 3 (shift right word by 3) instruction when you need to read the byte value back out of an address register — divide by 8.

AR1/AR2 Address Registers

The S7-300/400 CPU provides two 32-bit registers, AR1 and AR2, used exclusively for pointer arithmetic and indirect addressing. There is no documented speed or feature difference between them; the convention in Siemens libraries is to use AR1 for the primary pointer and AR2 for the secondary.

Key operations:

STL Effect
LAR1 Load AR1 from ACCU1 (32-bit pointer)
LAR1 P#120.0 Load AR1 with the constant pointer P#120.0 (= 960)
LAR1 AR2 Copy AR2 into AR1
TAR1 Transfer AR1 to ACCU1 (preserves ACCU2)
+AR1 Add ACCU1 to AR1 (pointer addition)
-AR1 Subtract ACCU1 from AR1
CAR Exchange AR1 and AR2

AR1 is also the implicit pointer used by block-call operations (CALL FB1, DB100 loads the instance pointer into AR1 and restores it on return), so be aware of AR1's life-cycle when writing FB code.

Memory-Indirect Addressing Syntax

The full syntax for an area-internal, register-indirect operand in STL is:

<operand>[AR<n>, <P#offset>]

The CPU adds the byte-bit offset from the address register and the constant P# offset, and the result selects the operand within the area specified by the prefix. A non-zero P# offset simply shifts the start of the iteration. The pointer is area-internal — it cannot cross between, say, M and DB — so the prefix (DBB, MB, PQB, …) governs which area is addressed.

Three forms occur in practice:

Syntax Description Example
[AR1] AR1 alone (no constant) L MW[AR1]
[AR1, P#0.0] AR1 with explicit zero offset T PQB[AR1, P#0.0]
[AR1, P#630.0] AR1 plus constant byte/bit offset L DBB[AR1, P#630.0]

Code Walkthrough: AS-i CP342-2 Mapping

The source code supplied to the forum post is the typical AS-i/CP342-2 mapping block. The CP342-2 occupies two logical I/O slots: PI/PQ 120–135 and PI/PQ 140–155, and the program's job is to mirror the I area into MB140–MB175 and copy the Q area back out. The relevant STL lines are:

      L   DBB[AR1, P#630.0]   // load a DB byte at (AR1_byte + 630)
      L   MB [AR2, P#160.0]   // load a memory byte at (AR2_byte + 160)
      T   PQB[AR1, P#0.0]     // write to a PQ byte at (AR1_byte + 0)

The constant 630 is the precomputed pointer-encoded base of the configured DB mapping table, and 160 is the offset that turns a P#0.0 base pointer into MB140 (140 + 20 in some configurations; the exact number depends on how the FB walks the table). What matters for understanding is the mechanism:

  1. AR1 is loaded elsewhere with the pointer to the first row of the DB mapping table (typically P#0.0 + 8 × index).
  2. AR2 carries the current MB index, pre-shifted by 3 so that [AR2, P#160.0] adds the correct 160-byte bias.
  3. The block reads the mapping table out of the DB, looks up the destination MB, merges any logic, and writes the result back to the PQ area at the CP342-2 base address.

If you strip out the AS-i context, the same three lines are a textbook indirect copy: read DB → read M → write to PQ, with both index pointers held in AR1/AR2.

Step-by-Step Pointer Construction

The following FC is the minimal pattern that demonstrates the entire mechanism without the AS-i overhead. Drop it into OB1 on an S7-300 with a CPU 314 or higher.

  1. Create shared DB. Define a data block DB100 with 64 bytes (ARRAY[0..63] OF BYTE). The DB holds the source image.
  2. Create local variables in FC200.
VAR
    Index : INT;        // 0..63 loop counter
    PtrDB : DWORD;      // pointer into DB100 (byte offset, shifted)
    PtrMB : DWORD;      // pointer into MB100 (byte offset, shifted)
END_VAR
  1. FC200 body (STL):
      L   0
      T   #Index
      L   0
      T   #PtrDB
      L   0
      T   #PtrMB

LOOP: L   #Index
      SLW 3                // byte * 8 = pointer
      T   #PtrDB
      L   #Index
      +   100              // start of MB100 mirror
      SLW 3
      T   #PtrMB

      L   DBB[#PtrDB]      // load source byte from DB100
      T   MB [#PtrMB]      // mirror into MB100

      L   #Index
      L   1
      +I
      T   #Index
      L   64
      <I                   // continue while Index < 64
      JC  LOOP
      BE

When Index = 0, #PtrDB = 0, and the first iteration copies DB100.DBB0 → MB100. When Index = 63, #PtrDB = 504 and the final iteration copies DB100.DBB63 → MB163. The mirror occupies MB100…MB163 — 64 bytes, one per DB byte.

Verification

Before trusting the loop, verify each step in STEP 7 Monitor/Modify or with a VAT (variable table). Use the following checks:

Step Action Expected result
1 Open VAT, monitor AR1 and AR2 after LAR1 P#120.0 AR1 = W#16#3C0 (= 960 dec)
2 Monitor #PtrDB after SLW 3 with Index = 31 #PtrDB = W#16#F8 (= 248 dec = P#31.0)
3 Force DB100.DBB0 = 16#AA, run loop, monitor MB100 MB100 = 16#AA
4 Force DB100.DBB63 = 16#55, run loop, monitor MB163 MB163 = 16#55
5 Run loop with cross-area pointer test: LAR1 P#0.0, then T PQB[AR1, P#120.0] after loading ACCU1 with 16#01 First output LED on the CP342-2 slave lights

If step 2 fails to show 248, suspect that the previous instruction left garbage in ACCU1. STL always uses ACCU1 for arithmetic; clear or load explicitly before every shift.

Common Errors and Diagnostics

Symptom Probable cause Diagnostic / fix
CPU goes to STOP with SF LED on, diagnostic buffer: "Area length error when reading" Pointer points past the end of the area (e.g., AR1 = P#200.0 on a 32-byte DB) Bound the loop counter against the area length before SLW 3
Always reads MB0 regardless of AR1 Forgot to apply SLW 3; AR1 = 31 is interpreted as bit offset 31 — only bits 0–2 are used, so result = P#0.7 Insert SLW 3 between the byte load and the LAR1
Values are correct for inputs but outputs are wrong by 1 byte Mixing area-internal and cross-area pointers (e.g., using P#120.0 from PI as if it were a DB pointer) Reset bit 31 to 0 and clear the area bits 19–30; build the pointer from scratch with L P#120.0
CP342-2 outputs flicker during the loop Peripheral outputs are written by the loop, then overwritten by the next OB1 cycle before the AS-i master has time to refresh Insert TPB on the AS-i ready bit, or use a sync OB (OB101 for warm restart, OB102 for cold restart) to delay the initial pass
Intermittent wrong value only on bit 7 of one byte Forgot to clear the bit-offset portion of the pointer; the bit offset of 7 is being added on top of the byte shift Mask with UW W#16#FFF8 before LAR1 to clear bits 0–2

Performance and Best Practice

On the S7-300, a single LAR1 + indirect read takes ~2 µs longer than a direct operand access. Loops written this way run well up to a few hundred iterations per OB1; for table sizes beyond a few KB, move the data with SFC 20 BLKMOV (which uses internal pointer pairs and is significantly faster) or with SFC 81 UBLKMOV for consistency over multiple OB1 cycles.

Document the pointer offset constant in the block's COMMENT — for example, the constant 630 in L DBB[AR1, P#630.0] becomes legible as "630 = P#630.0 base pointer to AS-i slave descriptor DB" in the symbol table. This single line of documentation prevents the next engineer (or you, six months later) from trying to "correct" the offset to a real byte number.

Migration tip: If the project later moves to an S7-1500 in TIA Portal, the equivalent of LAR1 + [AR1, P#x.y] is P#"MyDB".MyArray[i] in SCL, or P#[DB].Static_1 in STL. The byte-bit packing convention is identical for the lower 19 bits, but TIA Portal does not expose AR1/AR2 as freely — pointer arithmetic must be done on a DWORD tag and then re-cast with P# at the use site.

Frequently Asked Questions

What is the difference between an area-internal and a cross-area pointer?

An area-internal pointer has bit 31 = 0 and bits 19–30 = 0; the upper 13 bits are reserved, and the lower 19 bits hold the byte-bit address relative to the operand's own area. A cross-area pointer sets bits 19–30 to identify the destination area (I, Q, M, DB, PI, PQ) and sets bit 31 = 1. AR1/AR2 can hold either form, but the P# offset notation in [AR1, P#x.y] only works with area-internal pointers.

Why do I need to shift left by 3 (SLW 3) before loading a pointer?

The byte offset occupies bits 3–18 of the 32-bit pointer, with bits 0–2 reserved for the bit address. Multiplying a plain byte number by 8 slots the value into the correct bit position. Forgetting the shift makes the CPU interpret the number as a bit address, which is the most common bug in hand-rolled pointer code.

Can AR1 and AR2 be used interchangeably in STL?

Yes, mechanically. Convention reserves AR1 as the primary pointer (it is also used by the CPU for instance-DB pointers during FB calls) and AR2 as the secondary. The CAR instruction swaps them in one cycle if needed.

How do I know whether my pointer is bit- or byte-aligned?

Inspect the lower three bits of the value in AR1/AR2. If bits 0–2 are all zero, the pointer is byte-aligned (P#x.0). If any of those bits are 1, the pointer is bit-offset within the byte — common in bit-flip loops over I or Q areas.

Why does PQB[AR1, P#0.0] not add any offset?

The constant P#0.0 is a zero offset — the CPU still adds the byte-bit portion of AR1 to the PQ area's base address, but no constant is added on top. The P#0.0 form is preferred over [AR1] alone when you want the code's intent (a base-anchored write) to be explicit at the call site.

Back to blog