Overview of AR1/AR2 in Siemens S7-300/400
Every S7-300 and S7-400 CPU exposes two 32-bit address registers, AR1 and AR2, that act as the base pointer for memory-indirect and register-indirect STL operations. Instructions such as L MW [AR1, P#0.0], T DB[AR1].DBW0, A I [AR1, P#0.0], or = Q [AR1, P#0.0] dereference a memory location by combining the address register with an optional 16-bit constant offset (P#x.y).
AR1 is the general-purpose pointer register. AR2 is reserved by Siemens convention for the second pointer passed into FBs through DI/DO (instance data block), and by certain system functions. The CPU does not enforce this convention at runtime, but FB source code generated by STEP 7 follows it, and reassigning AR2 inside an FB can corrupt the implicit AR2-based parameter passing done by the caller's environment.
The STL instruction set gives engineers +AR1 and +AR2 (16-bit signed addition to the address register) and the LAR1/LAR2/TAR1/TAR2 load/transfer instructions. Notably absent is any -AR1 or -AR2 instruction. Decrementing the pointer requires either loading a negative constant into the accumulator and using +AR1, or pulling AR1 into the accumulator, doing integer math, and writing it back with LAR1. This technical reference documents each technique with STL examples, the bit-level pointer layout that determines when they work, and the edge cases that surface as field bugs.
Bit-Level Pointer Layout in AR1
Before manipulating AR1 you must understand the bit layout of the 32-bit pointer it stores. Bit 31 selects between two formats:
| Format | Bit 31 | Bits 30-24 | Bits 23-16 | Bits 15-3 | Bits 2-0 |
|---|---|---|---|---|---|
| Area-internal | 0 | 0000000 | 00000000 | Byte offset (0-8191) | Bit offset (0-7) |
| Area-crossing | 1 | 00000 bbb (area ID) | DB number or byte offset high | Byte offset (0-8191) | Bit offset (0-7) |
Area ID (bbb in bits 26-24 of an area-crossing pointer):
| bbb | Area | Typical Use |
|---|---|---|
| 000 | P | Periphery (direct I/O) |
| 001 | I | Process input image |
| 010 | Q | Process output image |
| 011 | M | Bit memory |
| 100 | DB | Data block |
| 101 | DI | Instance DB |
| 110 | L | Local / temp |
| 111 | V | Pre-V4 designation (legacy) |
This layout drives a critical limitation of +AR1: the instruction only manipulates the low 16 bits of AR1 (the byte offset + bit offset). It never touches the high 16 bits, so it cannot move a pointer across area boundaries or change the DB number of a DB pointer. A subtraction implemented via +AR1 therefore behaves identically to an intra-area arithmetic operation on the offset portion of the pointer.
The +AR1 Instruction Variants
STL exposes three syntactic forms of +AR1. All three perform the same 16-bit signed add on the low word of AR1; only the source operand differs.
| Form | Effect | Typical Use |
|---|---|---|
+AR1 (no operand) |
AR1 = AR1 + ACCU1-L (16-bit signed) | Add a precomputed dynamic offset |
+AR1 P#x.y |
AR1 = AR1 + (x*8 + y) bits | Add a constant compile-time offset |
+AR1 <int> |
AR1 = AR1 + <int> (16-bit signed) | Add a small integer literal |
The constant forms are syntactic sugar; the S7 compiler/loader encodes P#x.y or the integer as a 16-bit value in ACCU1-L before issuing the no-operand +AR1 micro-operation. The hardware semantics are identical. The signed 16-bit range is -32768 to +32767. Because the pointer's low 16 bits encode byte+bit offset (byte in bits 15-3, bit in bits 2-0), the practical maximum offset per +AR1 is +8191 bytes + 7 bits forward or -8192 bytes backward, well within 16-bit signed range. The range limit does not bite in normal loop work because pointer walks of more than 16 KB are rare.
Why Siemens Does Not Provide a -AR1 Instruction
No -AR1 microcode exists on any S7-300 or S7-400 CPU. The +AR1 instruction predates the S7 line and was inherited from the S5-135U/155U family, which already had a similar limitation. Siemens has consistently declined to add a dedicated subtract instruction on the grounds that loading a negative constant and using +AR1 produces identical results with no cycle-time penalty.
+AR1 with a constant operand consumes one extra word of MC7 code. Using LAR1 to load a precomputed pointer and then +AR1 with no operand sometimes compiles to a single MC7 instruction. Profile your hot loop in the PLC's Block Last Invocation Time diagnostic if cycle budget matters.The trade-off is real but small. Most automation code calls +AR1 rarely (per loop iteration, not per scan), so the missing -AR1 is a documentation nuisance rather than a runtime problem.
Workaround 1: Negative Constant with +AR1
The shortest path to AR1 -= N is loading -N into ACCU1-L and executing +AR1. STL accepts a negative integer literal in the L instruction:
// Subtract 10 bytes (80 bits) from AR1
L -80 // ACCU1-L = 16#FFF6 = -80 (signed)
+AR1 // AR1 = AR1 + (-80) bits = AR1 - 10 bytes
For symbolic offsets, precompute the bit count:
// Subtract pointer P#10.0 (= 10 bytes = 80 bits)
L -80 // 16-bit constant load; lower word = -80
+AR1 // AR1 -= 10 bytes
P#x.y pointer, load -(x*8 + y). Misalignment off by one bit is the most common source of "I am reading the wrong word" bugs in legacy AR1 code.Workaround 2: TAR1/LAR1 Round-Trip
When the value to subtract is computed at runtime (loop counter, parameter, or pointer arithmetic result) and cannot be expressed as a literal, route AR1 through the accumulator:
// AR1 := AR1 - MD100 (DINT, byte count)
TAR1 // ACCU1-L = current AR1 (low 16 bits)
L MD100 // ACCU1-L = byte count to subtract (positive value)
NEGI // ACCU1-L = -byte count (32-bit signed negate)
+AR1 // AR1 += ACCU1-L (low 16 bits) = AR1 - byte count
If you need to subtract a pointer value (P#) rather than a raw integer, the cleanest sequence is:
// AR1 := AR1 - P#4.0 (subtract 5 bytes worth of bits)
TAR1 // ACCU1 = AR1
L P#4.0 // ACCU1 = P#4.0 (40 = 0x0028)
NEGI // ACCU1 = -40 (0xFFFFFFD8 in 32-bit, 0xFFD8 in low word)
+AR1 // AR1 = AR1 + (-40) = AR1 - 5 bytes
This pattern keeps the symbolic P# in the source, which is more readable than a magic bit count, and works for any pointer-size operand from P#0.0 up to P#8191.7.
Note: the mnemonic CAR1 sometimes appears in legacy comments and informal notes; the correct instruction is TAR1 (Transfer AR1 to ACCU1). LAR1 is its inverse (Load ACCU1 to AR1). CAR is not a valid S7 instruction; treat it as TAR1.
Workaround 3: Full Pointer Reload with LAR1
If you want to set AR1 to an absolute pointer rather than walk it, just load the new pointer directly. No TAR1 step is required:
// AR1 := P#100.0 (offset 100 bytes into the current area)
LAR1 P#100.0 // 32-bit pointer load; AR1 fully overwritten
// AR1 := P#DB10.DBX0.0 (specific DB and offset)
LAR1 P#DB10.DBX0.0
This is not a decrement, but it is the answer in many real situations where the original question is "I want AR1 to point at byte N-K given it currently points at byte N." When N-K is known at compile time, reload the absolute pointer instead of subtracting. This avoids any accumulator overhead and produces a single MC7 instruction in most cases.
Application: AR1 Walking in FBs with ANY-Pointer Parameters
FBs declared with a VARIANT or ANY input parameter receive a 10-byte descriptor describing a source area. The classic pattern reads the source area byte-by-byte using AR1 as the walking pointer:
// Inside an FB (called via instance DB DI):
// Input: srcAny : ANY
// Temp: bytesLeft : INT, srcDB : WORD
LAR1 P##srcAny // AR1 -> srcAny parameter in DI
L W [AR1,P#4.0] // load byte count from ANY body (bytes 4-5)
T #bytesLeft
L W [AR1,P#2.0] // load DB number from ANY body (bytes 2-3)
T #srcDB
L D [AR1,P#6.0] // load area-crossing pointer from ANY body (bytes 6-9)
LAR1 // AR1 now points to first source byte
To walk backwards through the source (e.g., for a reverse memcpy), apply Workaround 1 or 2 to AR1 inside the loop:
loop_back:
L MW 100 // ACCU1-L = bytes processed so far
NEGI // negate to make it negative
+AR1 // AR1 walks backward by that many bits
// ... read byte at new AR1 ...
L DBB [AR1, P#0.0]
Because AR1 holds an area-crossing pointer at this point, the +AR1 only modifies the byte/bit offset portion. The DB number and area ID are preserved, so the dereference L DB[AR1].DBB0 still resolves to the correct data block.
ANY Pointer 10-Byte Layout
The 10-byte ANY structure used for parameter passing has a fixed layout. Knowing it lets you decompose the pointer loaded from srcAny:
| Byte Offset | Width | Content |
|---|---|---|
| 0-1 | WORD | ID: 16#10 = ANY (01=BYTE, 02=WORD, 03=DWORD, 04=REAL, 05=COUNTER, 06=TIMER) |
| 2-3 | WORD | DB number (0 if not DB area) |
| 4-5 | WORD | Byte count (length of pointed area) |
| 6-9 | DWORD | Area-crossing pointer (bit 31=1) to first byte |
The 4-byte pointer at offset 6-9 follows the area-crossing format from earlier. Loading it with L D [AR1, P#6.0] puts the full 32-bit value into ACCU1, ready for LAR1 or further decomposition with SLD/SRD rotates. VARIANT pointers (S7-1500) replace the 10-byte ANY with a structured handle plus an 8-byte pointer; the working principle is the same but the offsets differ. See the Siemens Industry Online Support entry on VARIANT pointer layout when migrating an FB from ANY to VARIANT.
Loop Pattern: Byte-by-Byte Copy
A common production code pattern copies N bytes from a source ANY to a destination ANY using AR1/AR2 as the walking pointers. The skeleton looks like:
// FC "ANY_COPY" : VOID
// IN: src : ANY; dst : ANY; len : INT
LAR1 P##src
L W [AR1,P#4.0] // src byte count
T #srcLen
LAR1 P##dst
L W [AR1,P#4.0] // dst byte count
T #dstLen
L #len
L #srcLen
<I // IF len > srcLen THEN error
JC err_short
LAR1 P##src
L D [AR1,P#6.0] // src area-crossing pointer
LAR1
LAR2 P##dst
L D [AR2,P#6.0] // dst area-crossing pointer
LAR2
L #len
NEXT: T #i
L DBB [AR1, P#0.0] // read source
T DBB [AR2, P#0.0] // write destination
+AR1 P#1.0 // src += 1 byte
+AR2 P#1.0 // dst += 1 byte
L #i
LOOP NEXT // DEC #i; IF #i <> 0 THEN NEXT
To reverse the copy direction (fill dst from end to start), swap the loop direction and apply +AR1 with a negative offset:
// Inside the reversed loop body:
L -8 // -1 byte in bits
+AR1 // src walks backward
L -8
+AR2 // dst walks backward
System Function Alternatives
For block moves larger than a few hundred bytes, replace the manual loop with SFC20 (BLKMOV) or SFC21 (FILL). SFC20 copies a source area to a destination area using any-pointer semantics internally, with cycle times on the order of 20 microseconds per kilobyte on a CPU 315-2 and 5 microseconds per kilobyte on a CPU 417-4. The AR1-based manual loop is appropriate when SFC20 cannot be used (overlapping source/destination, or mid-loop transformation of the data).
| SFC | Name | Use Case | AR1 Required? |
|---|---|---|---|
| SFC20 | BLKMOV | Non-overlapping block copy | No (uses internal pointers) |
| SFC21 | FILL | Initialize block with pattern byte | No |
| SFC81 | UBLKMOV | Uninterruptible block copy | No |
| Manual AR1 loop | -- | Overlap or in-loop transformation | Yes |
STL +AR1 vs SCL PEEK/POKE
For new code on S7-1200/1500, Siemens recommends SCL with symbolic tags over STL with explicit AR1 walking. The PEEK and POKE intrinsics provide a near-equivalent capability without exposing the address register:
| Capability | STL +AR1 | SCL PEEK/POKE |
|---|---|---|
| Read byte at runtime offset | L DBB [AR1, P#0.0] |
PEEK_BOOL(area := 16#83, db := 10, byteOffset := i) |
| Write byte at runtime offset | T DBB [AR1, P#0.0] |
POKE_BOOL(area := 16#83, db := 10, byteOffset := i, value := TRUE) |
| Read 32-bit value at runtime offset | L D [AR1, P#0.0] |
PEEK_DWORD(area := 16#84, db := 10, byteOffset := i*4) |
| Cycle time per access | 0.04-1.2 microseconds (CPU dependent) | 3-6 microseconds (FCALL overhead) |
| Bounds checking | None at runtime | None at runtime |
| Area-crossing pointer support | Native via LAR1 | Implicit via area + db parameters |
PEEK/POKE trades 2-4x slower execution for symbolic clarity and easier debugging. Inside a 10 ms OB1 cycle on a CPU 1516, a 1000-iteration PEEK loop completes in 6 ms; the equivalent STL loop completes in 1 ms. Choose based on whether the block is scan-rate critical or a maintenance utility.
CPU Cycle Time Notes
Cycle cost per +AR1 or LAR1 instruction varies by CPU generation:
| CPU | Family | +AR1 (no operand) | +AR1 P#x.y | LAR1 |
|---|---|---|---|---|
| CPU 312 IFM | S7-300 (older) | 1.2 microseconds | 1.4 microseconds | 1.5 microseconds |
| CPU 315-2 DP | S7-300 | 0.5 microseconds | 0.7 microseconds | 0.6 microseconds |
| CPU 319-3 PN/DP | S7-300 | 0.04 microseconds | 0.05 microseconds | 0.05 microseconds |
| CPU 412-2 | S7-400 | 0.06 microseconds | 0.08 microseconds | 0.08 microseconds |
| CPU 416-3 | S7-400 | 0.03 microseconds | 0.04 microseconds | 0.04 microseconds |
| CPU 1516-3 PN | S7-1500 | 0.01 microseconds (non-optimized) | 0.012 microseconds | 0.01 microseconds |
These figures come from the Instruction List reference manuals for the respective CPU families, available through the Siemens Industry Online Support portal under CPU-specific operating instructions. The CPU 1516 numbers assume non-optimized block compilation; optimized blocks reject the instructions outright. For cycle-critical code, prefer the no-operand +AR1 form (operands pre-loaded into ACCU1).
Edge Cases and Pitfalls
The workarounds above silently misbehave in five situations that surface as field bugs:
-
Underflow on area-internal pointer: If AR1 points at byte 0 bit 0 and you
+AR1 -1, the low 16 bits wrap to 0xFFFF (8191 bytes, 7 bits forward). For memory (M) or DB areas this points at a valid but unintended byte; for I/Q areas it points outside the process image and the read returns 0 silently. Always bounds-check before walking backward. -
Area-crossing pointer with non-zero DB number:
+AR1only touches bits 0-15. If youLAR1 P#DB10.DBX0.0then+AR1 -80, AR1 still references DB 10, just 10 bytes earlier. This is usually what you want, but it does mean you cannot use+AR1to step across to a different DB. -
Bit offset asymmetry:
P#10.0 - P#0.7isP#9.1, notP#10.0(one byte, one bit forward). If you naively doL P#10.0; NEGI; +AR1starting from aP#0.7pointer, you end up atP#9.1. Verify with a watched pointer in VAT if bit-precise alignment matters. - DB number greater than 255 on S7-300: S7-300 CPUs only accept DB numbers 1-255 in most models. An ANY body carrying DB number 300 loads successfully, but dereferencing it inside the CPU raises OB121 (programming error). Inspect the DB number word before walking.
-
Optimized blocks on S7-1500: The
+AR1/LAR1/TAR1instructions exist on S7-1500 but only inside blocks compiled with Standard or non-optimized access. Optimized blocks forbid the symbolic pointer arithmetic in source code; you must use SCL slice notation or AT-view overlays. See the Siemens Industry Online Support portal for the official guidance on migrating from non-optimized to optimized block access.
S7-1200 and S7-1500 Considerations
S7-1200 (since firmware V2.0) and S7-1500 retain the AR1/AR2 registers and the +AR1 instruction, but Siemens has steadily migrated new code to symbolic addressing and tag-absolute access. In TIA Portal V15 and later, pointer arithmetic is supported only in STL source files inside FBs/FCs compiled with non-optimized access. Optimized blocks (the default for new S7-1200/1500 projects) reject +AR1, LAR1 P##Symbol, and TAR1 with a compile error.
For new S7-1500 code, prefer:
-
ARRAY indexing with a loop counter:
"db".data[i] := "db".data[i-1];in SCL compiles to bounds-checked, optimized machine code and avoids the AR1 register entirely. -
AT view overlays: Declare a TEMP variable as
AT %DB1.DBX0.0 BYTEand index into it with a static index. The compiler generates the pointer arithmetic for you. -
PEEK/POKE on SCL: The PEEK (read) and POKE (write) intrinsics accept a pointer DWORD and a byte offset. They are the SCL equivalent of
+AR1without the STL.
Legacy S7-300/400 STL code that depends on AR1 walking still runs unmodified on an S7-1500 if the host FB is set to non-optimized access. Migration to optimized blocks requires rewriting the AR1 logic into the patterns above.
Verification and Diagnostics
After patching a block, verify the pointer math in three steps before deploying to a running process:
- Static STL watch table: Open the affected FB in STEP 7, right-click the AR1 dereference line, and Monitor/Modify. The status display shows the 32-bit AR1 in hex. Walk through one full loop iteration manually by toggling the loop index.
- Cross-reference the pointer byte offset: In the VAT, monitor both the modified AR1 and the operand it dereferences. Confirm the absolute byte addresses match what your algorithm expects. Common mismatch: P# (in bits) vs. DWORD (in bytes); one is 8x the other.
- OB121 / OB122 stress test: Wrap the AR1 walk in a loop that goes two iterations past the legal boundary. If OB121 fires (programming error, area violation) your bounds check is wrong; if OB122 fires (I/O access error) you walked into the process image gap.
For field commissioning, capture the AR1 value at the point of failure by writing it to a retentative MD before triggering the buggy dereference. The CPU fault buffer entry then shows both the failing instruction and the AR1 value, which usually pinpoints the underflow in seconds.
FAQ
Why does Siemens not provide a -AR1 instruction in S7-300/400?
The +AR1 micro-operation predates S7 and was carried over from the S5 family. A negative operand on +AR1 produces the identical result, so Siemens has not added a dedicated subtract instruction. The official line is documented in the STEP 7 STL reference manual under address-register instructions.
Can I do AR1 := AR1 - P#x.y in a single STL statement?
No single instruction exists. The shortest sequence is L P#x.y; NEGI; +AR1;. This preserves the symbolic offset in source and avoids manual bit-count arithmetic. The three-line form runs in approximately 3 microseconds on a CPU 315-2 DP and 1 microsecond on a CPU 416-3.
Does +AR1 with a negative value change the DB number of an area-crossing pointer?
No. +AR1 only manipulates bits 0-15 of AR1. For an area-crossing pointer, that means the byte offset and bit offset only; bits 16-31 (area ID and DB number) are untouched. To change the DB number, reload AR1 with LAR1 P#DBxx.DBXy.z.
What happens if AR1 underflows below byte 0 bit 0?
The low 16 bits wrap to 0xFFFF, pointing to byte 8191 bit 7 of the same area. For M and DB areas this reads or writes a valid but unintended location; for I/Q it points outside the process image and the read returns 0 silently or, on some CPUs, raises OB122. Always bounds-check before walking backward.
Can I use +AR1 inside an optimized S7-1500 block?
No. Optimized blocks in TIA Portal reject pointer-register instructions at compile time. Either set the block to non-optimized access for legacy compatibility, or rewrite the loop using ARRAY indexing or AT-view overlays. The Siemens Industry Online Support portal covers the migration path under the keyword "optimized block access."