Incrementing Indirect Bit Addressing in STEP 7 STL

David Krause19 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

Incrementing Indirect Bit Addressing in STEP 7 STL

Walking a single output through a contiguous range of BOOL bits (M0.0, M0.1, M0.2, …) is a recurring commissioning task on every Siemens S7-300 and S7-400 project written in Statement List (STL). A natural first attempt is to load an address from a marker double-word, increment the address register by P#0.1 (one bit), and write the result back. That approach almost always fails the first time it is built because the increment runs every scan cycle, the pointer walks two or more bits per press, or the reset is ignored. This reference documents three production-ready patterns, explains the underlying pointer format, and compares the same logic on S7-1200 and S7-1500.

Overview

The S7-300/400 CPU family exposes two 32-bit address registers, AR1 and AR2, that hold an internal pointer in the format P#Byte.Bit. Combined with the instructions LAR1 (load AR1), TAR1 (transfer AR1), and the arithmetic operations +AR1 / -AR1 / +AR2 / -AR2, you can build indexed access to any bit, byte, word, or double-word inside the process image, the bit-memory area, or the data blocks. The full instruction set is described in chapter 4 of the Siemens Industry Online Support reference "Programming and Operating Manual STEP 7 V5.5 – Basic Information".

Indirect bit access via = M[AR1,P#0.0] is the foundation of sequencer, multiplexer, and fault-queue logic. A typical application is a one-of-N selector that turns on exactly one of 16, 32, or 64 lamps in a turnstile indicator panel. The source code reviewed in this article attempts that exact pattern, but trips over three classic STL pitfalls. The sections below show how to correct each one.

Prerequisites

  • STEP 7 V5.5 SP2/SP3/SP4 (or TIA Portal V13–V15 with installed S7-300/S7-400 support package).
  • S7-300 CPU 31x (any variant) or S7-400 CPU 41x with firmware ≥ V2.0. CPU 312 IFM (order number 6ES7 312-1AF00-0AB0) has a reduced instruction set that does not support the LOOP instruction; use a CPU 312C, 313, 314, or 315 instead if you plan to use LOOP.
  • PLCSIM V5.4 SP5 or higher for offline verification, or a real rack with one digital input wired to the increment request (I 0.0) and one wired to the reset (I 0.1).
  • STL source editor and the Monitor/Modify function. The "Operands" view of the monitor is required to see AR1, AR2, MD, and MW values in real time.
  • Symbol table or DB tags for the pointer (MD 100) and the index (MW 110). Naming them with a prefix such as ptrSeq and iSeq helps the next engineer read the code.
Note: S7-1200 (CPU 1211C–1217C) and S7-1500 (CPU 15xx) do not expose AR1/AR2 to the user. They use the POKE_BOOL / PEEK_BOOL SCL instructions or, in TIA Portal V14+, the __SET_BIT / __RESET_BIT instructions, or simply an ARRAY[0..n] OF BOOL in a global data block. See the cross-reference section below for a migration example.

Address Register Architecture

Each S7-300/400 CPU has two 32-bit address registers. They are the only CPU registers that can hold a memory pointer, and they are also used internally by the operating system. The rules below must be followed in every block that touches them:

  • AR1 is the user pointer of choice. It is preserved across calls to standard library FBs and is overwritten only by the operating system during certain SFC calls. The default in the S7-300 instruction list treats AR1 as the primary pointer.
  • AR2 is the parameter-pointer for FB calls. Every time a function block (FB) with instance-DI access is called, AR2 is loaded with the instance data-block start address and the FB's TAR2 / LAR2 operations refer to that instance. If you intend to use AR2 for your own pointer, you must save and restore it with PUSH / POP (the stack) or with a temporary DWORD.
  • AR1 and AR2 are 32 bits wide and are interpreted as an area-internal byte offset when the area identifier is zero. When the area identifier is non-zero (e.g. 83H for bit memory), the high byte specifies the area.
  • Some SFCs overwrite AR1 unconditionally, including SFC 20 (BLKMOV), SFC 21 (FILL), SFC 22 (CREAT_DB), SFC 24 (TEST_DB), SFC 25 (COMPRESS), and SFC 81 (USTRING). The PID blocks FB 41 (CONT_C), FB 42 (CONT_S), and FB 43 (PULSEGEN) also touch AR1 and AR2. Save and restore them around any such call.

Pointer Format: P#Byte.Bit

An S7-300/400 pointer is a 32-bit word that contains three fields. The structure is documented in section 4.5 of the S7-300 Automation System reference manual:

Bits 31–24 Bits 23–16 Bits 15–3 Bits 2–0
Area ID (00 = P, 81 = I, 82 = Q, 83 = M, 84 = DBX, 85 = DIX, 86 = L, 87 = V) Byte address (high byte) Byte address (low 13 bits) Bit offset 0–7

The constant P#0.1 therefore represents the pointer to bit 1 of byte 0. Loaded with the area ID 83H, it resolves to M0.1. The constant P#2.0 with area ID 83H targets byte 2 bit 0, i.e. M2.0. Incrementing by P#0.1 walks one bit at a time. Incrementing by P#1.0 walks one byte at a time. Incrementing by P#2.0 jumps two bytes (16 bits) in a single instruction. The full set of valid area identifiers and the rules for crossing area boundaries are in chapter 6 of the STEP 7 reference.

Memory Area Identifiers

Identifier Area ID (hex) Typical use Size on CPU 315-2 PN/DP
P (periphery) 00 Direct I/O access (P# or PIB/PQW) depends on configuration
I (input image) 81 Process input image (PII) 128 bytes
Q (output image) 82 Process output image (PIQ) 128 bytes
M (bit memory) 83 Merker (scratchpad flags) 2048 bytes
DBX (data bit) 84 Bits inside a global or instance DB up to 64 KB per DB
DIX (instance DI bit) 85 Bits inside an instance DB up to 64 KB per DB
L (local stack) 86 Temporary variables inside a block 16 KB per OB/FB/FC
V (preceding area) 87 Cross-area pointer (used by ANY) n/a

The default area ID for a marker pointer loaded via LAR1 MD 100 is 83H (bit memory) because the area is taken from the instruction context, in this case the M operand. To be explicit, use the constant LAR1 P#M 0.0.

Diagnosing the Original Bug

The source program reads:

NETWORK 1
A     "run"               // steps for increment
LAR1  MD 100
+AR1  P#0.1              // increment
TAR1  MD 100
=     M [MD 100]

NETWORK 2
A     M    1.2           // reset
LAR1  P#2.0
TAR1  MD 100

Three compounding problems cause the symptom "increments only once and the bit pattern does not change":

  1. No edge detection on the increment input. The bit "run" is true for the entire duration of the input, so the increment runs every PLC cycle (typically 10–50 ms). The output therefore appears to chase itself one bit per cycle rather than one bit per press. The pointer visibly moves in the monitor, but the eye cannot detect a single-bit change at 50 ms intervals on a slow-reacting load.
  2. MD 100 is read and written in the same network. Even with edge detection, the read of MD 100 and the write back to MD 100 must be ordered so that the increment uses the previous value, not the just-written one. Using a temporary accumulator or two separate networks solves this.
  3. Reset and increment are in different networks but use the same pointer variable. When the reset input is level-triggered and the increment is not, the reset can fire on the same cycle as the increment, overwriting the incremented pointer. The reset branch should be moved above the increment branch and edge-triggered as well.
Tip: Open the STL editor in Monitor mode and toggle the "Operands" view to see AR1 in real time. AR1 shows as a 32-bit hex word. Setting it to DW#16#83000008, for example, corresponds to P#M 0.1 (area ID 83, byte 0, bit 1). Setting it to DW#16#83000010 corresponds to P#M 2.0 (byte 2 × 8 = 0x10, bit 0).

Solution 1 — Rising-Edge Detection with FP and JCN

This is the standard pattern for a single increment per request. The FP <bit> instruction requires a flag bit (edge memory) to latch the previous state of the input. The flag must not be used by any other logic in the project.

NETWORK 1 — Reset (level-triggered)
A     M      1.2                // reset
JCN   end1
LAR1  P#2.0                       // initialise pointer to M2.0
TAR1  MD   100
end1: NOP   0

NETWORK 2 — Increment (edge-triggered)
A     M      1.1                // steps for increment
FP    M      1.0                // edge memory bit
JCN   end2
=     M [MD 100]                // set the bit pointed to by MD100
L     MD   100                  // load pointer
INC   1                         // add one pointer unit
T     MD   100                  // write back
end2: NOP   0

Notes on this implementation:

  • INC 1 adds 1 to ACCU1. Because the pointer is a 32-bit word, the STEP 7 assembler must generate the 32-bit INC D form. STEP 7 detects the operand width from the destination of the next instruction (T MD 100), so the 32-bit form is selected automatically. To force the width explicitly, write L MD 100 first.
  • The reset branch initialises the pointer unconditionally on the cycle that M 1.2 is true. If the increment edge fires on the same cycle, it will increment the freshly-loaded P#2.0 to P#2.1, not back to P#2.0. To make the reset dominate, place it in its own network above the increment and add a short overlap mask (SPB) or use a one-cycle inhibit flag.
  • = M[MD 100] sets the bit, but does not reset any other bit. If you need a one-of-N pattern (only one bit on at a time), you must also reset the previous bit. The cleanest way is to maintain a second pointer MD 104 for the "previously set" position and write a reset to that bit before the new set.

Solution 2 — LOOP Instruction

When the requirement is to walk N consecutive bits in a single pass (for example write 10 bits from M10.0 to M10.7 and M11.0/M11.1), the LOOP instruction is more compact. LOOP decrements ACCU1-L by 1 and jumps to the label if the result is non-zero.

NETWORK 1 — LOOP walk of 10 bits
L     10                       // number of bits
NEXT: T     LW    0               // loop counter in local word 0
A     "run"                      // request
FP    M     1.0                   // optional edge
JCN   END
LAR1  MD   100                    // load pointer
+AR1  P#0.1                      // advance one bit
TAR1  MD   100
=     M [MD 100]
L     LW    0                    // reload counter
LOOP  NEXT                       // decrement and jump if non-zero
END:  NOP   0

Watch-outs for the LOOP pattern:

  • LOOP is a 16-bit instruction. If you need more than 32 767 iterations, you must chain two LOOP blocks or use a counter word (Z / ZW).
  • LOOP decrements ACCU1-L only, leaving ACCU1-H untouched. If your routine depends on the high word, restore it before the LOOP line.
  • The REPT label style used in the source code is from the Siemens Graph 7 / HiGraph 7 editor. In a pure STL block, use the conventional NEXT: label and place it on its own line.
  • LW 0 is a local word inside the OB1 temporary area. If you move the code into an FB, change it to a static tag in the instance DB so that the counter survives a multi-scan loop.

Solution 3 — Direct Integer Counter with Pointer Conversion

If the index is conceptually a number (0, 1, 2, …) and the pointer is a derived value, a third pattern is to keep the index in an INT / DINT word and convert it to a pointer on demand. This is more readable when the index is also used elsewhere, for example shown on an HMI tag.

NETWORK 1 — Reset (edge-triggered)
A     M     1.2                // reset
FP    M     1.3                // edge memory
JCN   noRes
L     0
T     MW   110                   // index = 0
T     MD   100                   // pointer = P#M 0.0
noRes:NOP 0

NETWORK 2 — Step (edge-triggered)
A     M     1.1                // step
FP    M     1.0
JCN   noStep
=     M [MD 100]                // set current bit
L     MW   110
L     1
+I                              // index += 1
T     MW   110
L     MD   100
L     P#0.1
+D                              // pointer += P#0.1
T     MD   100
noStep: NOP 0

This style separates "where I am" (MW 110) from "what the address is" (MD 100). It is the recommended pattern for any non-trivial project because the HMI tag and the PLC tag are decoupled. The HMI shows the index 0, 1, 2, … while the PLC keeps the pointer in a separate DWORD.

Cross-Reference: S7-1200 and S7-1500

On S7-1200 and S7-1500 (TIA Portal V13 and newer), the address registers AR1/AR2 are not visible to the user. The recommended pattern is an ARRAY[0..n] OF BOOL in a global DB and a FOR loop in SCL. The S7-1200 programmable controller product page lists the SCL POKE_BOOL and PEEK_BOOL instructions for legacy compatibility.

Capability S7-300/400 (STEP 7 V5.5) S7-1200/1500 (TIA Portal)
AR1 / AR2 address registers Yes, 2 × 32-bit No (replaced by tag arrays and PEEK/POKE)
Indexed bit access = M[AR1,P#0.0] "M_BitArray"[i] (Array of Bool) or POKE_BOOL
Looping LOOP (16-bit) or counter FOR … TO … DO in SCL
Edge detection FP / FN Same operators; also R_TRIG / F_TRIG function blocks
Pointer arithmetic +AR1 P#0.1 Integer index i + 1
Pointer area IDs 81, 82, 83, 84, 85, 86, 87 Implicit (managed by compiler)

The TIA Portal SCL version of the 16-step sequencer is approximately one third of the STL length because the FOR loop and the ARRAY index replace the explicit pointer arithmetic. The trade-off is the inability to address memory that is not part of a declared tag; any "wild" pointer arithmetic is rejected by the compiler.

Best Practices

  1. Use one network per phase. Loading the pointer, setting the bit, incrementing, and writing back belong to separate networks. Mixing them inside one network is the most common cause of "increments only once" symptoms.
  2. Save AR1 / AR2 around SFC / SFB calls. The operating system overwrites them inside SFCs 20, 21, 22, 24, 25, 81 and inside the PID blocks FB 41–FB 43. The pattern is PUSH; CALL SFC 20 (...); POP; TAR1 MD 100.
  3. Watch the pointer wrap. The bit-memory area is 0–32 767 bits (M0.0 to M4095.7) on most S7-300 CPUs. Incrementing past M4095.7 silently rolls into the I/O area or wraps. Add a bounds check (JC OVERFLOW) before +AR1 P#0.1.
  4. Declare MD 100 as a DWORD in a DB, not a bit-memory MD. Bit-memory pointers are lost on a warm restart. A DB-based pointer survives a stop/run transition and is retentive if the tag is marked RETAIN.
  5. Document the area ID in the symbol. Naming a tag ptrSeq : DWORD := DW#16#83000000 immediately tells the next engineer that it points into the bit-memory area.
  6. Edge-trigger every step input. Use FP for the increment, FP for the reset, and a mutual-exclusion mask to ensure both edges cannot fire on the same cycle.
  7. Avoid LAR1 P#0.0 without an area prefix. The compiler will not warn, but the area is taken from the operand and may surprise the next reader.

Troubleshooting Matrix

Symptom Most likely cause Fix
Output stays on a single bit No edge detection on increment input Add FP as in Solution 1
Pointer advances two or more bits per press Both +AR1 and INC used in the same network Pick one increment method per network
Reset does nothing MD 100 overwritten before LAR1 P#2.0 Put the reset branch in its own network above the increment branch
SF LED on, OB 121 / OB 122 called Pointer crosses area boundary into non-existent memory Add bounds check; see "Watch the pointer wrap" above
Bit pattern reverses direction Decrement (-AR1) used by mistake Replace with +AR1 P#0.1 or change sign of the constant
Works in PLCSIM, fails on real CPU Real CPU has different M area size (e.g. 512 bytes on CPU 312 IFM) Check CPU technical data for M area limit
Bit flickers between two adjacent bits Two increment inputs are wired to the same tag Audit the symbol table for duplicate references
Compiles but runs OB 121 in cycle 1 MD 100 is uninitialised on cold start Initialise in OB 100 (warm restart) or with a default value in the DB
Pointer valid in monitor but = M[MD 100] does nothing AR1 was overwritten by an SFC earlier in the cycle Re-load LAR1 MD 100 immediately before the assignment

Extended Example: 16-Step Sequencer in OB1

The following is a complete, copy-paste ready OB1 in STL that walks 16 bits from M20.0 to M21.7 with edge-triggered step, level-triggered reset, and a watchdog against runaway scans. It is the production version of the three solutions above and is suitable for a turnstile indicator or a 16-station status panel.

NETWORK 1 — Reset (level-triggered)
A     M     1.2                       // reset
JCN   n01
LAR1  P#M 20.0                        // initial pointer
TAR1  MD   100                        // save in MD 100
L     0
T     MW   110                        // clear index
n01:  NOP   0

NETWORK 2 — Step (edge-triggered)
A     M     1.1                       // step request
FP    M     1.0                       // rising edge
JCN   n02
=     M [MD 100]                      // set current bit
L     MD   100
L     P#0.1
+D
T     MD   100                        // pointer += P#0.1
L     MW   110
L     1
+I
T     MW   110                        // index += 1
L     16
<I                                   // index < 16 ?
JC    n02                             // yes, continue
LAR1  P#M 20.0                        // wrap to start
TAR1  MD   100
L     0
T     MW   110
n02:  NOP   0

The watchdog at the end (compare index with 16 and wrap) prevents the pointer from drifting past M21.7 into M22.0 if a sensor produces more than 16 edges in a second. Without it, the sequencer would silently start writing to the next row of markers and corrupt the HMI faceplate.

Verification in PLCSIM

  1. Open OB1 in Monitor mode and create a Variable Table (VAT) with the following rows: MW 100, MD 100, AR1, MW 110, M 1.0, M 1.1, M 1.2.
  2. Force M 1.2 = 1 for one cycle. Confirm AR1 = DW#16#830000A0 (P#M 20.0 → area 83, byte 20 × 8 = 0xA0, bit 0).
  3. Pulse M 1.1 once. Confirm M 20.0 = 1 and that AR1 = DW#16#830000A1 (P#M 20.1).
  4. Pulse M 1.1 a second time. Confirm M 20.1 = 1 and AR1 = DW#16#830000A2 (P#M 20.2).
  5. Continue for 16 pulses. After the 16th pulse, confirm that AR1 wraps back to DW#16#830000A0 and M 20.0 is set again.
  6. If the CPU is in STOP with an OB 121 error, the diagnostic buffer will report "Area length error" with the byte offset that was being read. The complete list of diagnostic events is in the Siemens Industry Online Support portal under the CPU's diagnostic entry ID.

Diagnostic Buffer Codes

The most common diagnostic events raised by the patterns in this article are summarised in the table below. The event IDs are from the S7-300 / S7-400 system diagnostics manual and apply to firmware ≥ V2.0.

Event ID (hex) Meaning Likely cause Remedy
2521 OB 121 programming error – area length error Pointer refers outside the configured M / DB / I area Add bounds check; reduce step count
2522 OB 121 – area write error (read-only DB opened with OPN incorrectly) Attempted write to a read-only data block Check DB access mode; use OPN DI for instance DBs
2523 OB 121 – area conflict between block and operand area Pointer format is malformed (area ID wrong) Re-build the pointer with explicit LAR1 P#M 0.0
3942 OB 121 – DB not loaded Pointer references a DB that is not on the CPU Check the hardware configuration and download the DB
2520 OB 121 – nesting depth overflow Recursive FB call via pointer Replace recursion with a FOR loop

Safety and Restart Considerations

The patterns above are intended for non-safety sequencer logic. For safety-related applications up to SIL 2 / Cat 3, the F-CPU (e.g. CPU 315F-2 PN/DP, 6ES7 315-2FJ14-0AB0) and the F-library (S7 F Systems) must be used. Standard FP / +AR1 patterns are not accepted by the F-runtime.

For warm restart, initialise the pointer in OB 100:

NETWORK 1 — Initialise on warm restart (OB 100)
LAR1  P#M 0.0
TAR1  DB20.DBD 0                    // store in a retentive DB
L     0
T     DB20.DBW 4                    // clear index

Marker data is lost on STOP→RUN. A pointer in MD 100 therefore resets to 0 (which is the bit-memory area ID 0, not bit-memory bit 0.0) and the first cycle may either set an unrelated bit or trigger an OB 121. The fix is to write the pointer into a data block tag that is marked RETAIN, then load it back into AR1 at the top of OB 1.

FAQ

Why does MD 100 increment only once and then stop advancing?

Because the increment runs every scan cycle while the input is true, the same bit is written, set, then the pointer moves on before the next cycle reads it. Use edge detection (FP) or a dedicated counter and write the increment in its own network above the assignment.

What is the difference between +AR1 P#0.1 and INC 1 on a pointer?

+AR1 P#0.1 adds a 32-bit pointer-format constant (one bit) to the address register and is the correct way to advance an AR1-based pointer. INC 1 adds 1 to the integer interpretation of the accumulator; it works only if the pointer is loaded as an INT/DINT and the area ID is zero. Mixing the two in the same network will skip bits.

How do I reset the pointer on a CPU restart?

Store the pointer in a data block (DB) tag, not in a marker double-word. Marker data is lost on STOP→RUN. In OB 100 (warm restart), write LAR1 P#M 0.0; TAR1 DBx.DBDy once at the top of the OB, and load the same tag back into AR1 at the top of OB 1.

Can I use AR2 instead of AR1 for the pointer?

Yes, but AR2 is used by the operating system during FB calls and by SFCs that take ANY pointers. Save and restore AR2 with PUSH / POP around any block call, or use AR1 throughout to avoid conflicts. The Siemens Industry Online Support knowledge base lists every SFC that touches AR2.

Does the same code work on an S7-1200 or S7-1500?

No. S7-1200 and S7-1500 do not expose AR1/AR2 to the user. The recommended pattern is an ARRAY[0..n] OF BOOL in a global data block, accessed by index. TIA Portal SCL offers FOR ... TO ... DO for looping and __SET_BIT / __RESET_BIT for explicit bit access.

Why does PLCSIM accept the program but the real CPU goes to STOP?

PLCSIM allows any pointer value without an area check. The real CPU enforces the area length and raises OB 121 with event ID 2521 ("Area length error"). Add a bounds check before +AR1 and load the pointer with an explicit area prefix.

Can I increment the pointer by more than one bit per cycle?

Yes. Replace +AR1 P#0.1 with +AR1 P#0.3 (three bits) or +AR1 P#1.0 (one byte). The bit offset in the constant is the increment. Always verify the new address on the monitor; jumps of more than 7 bits will wrap the byte and change the effective area crossing behaviour.

Back to blog