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
LOOPinstruction; use a CPU 312C, 313, 314, or 315 instead if you plan to useLOOP. - 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 asptrSeqandiSeqhelps the next engineer read the code.
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/LAR2operations refer to that instance. If you intend to use AR2 for your own pointer, you must save and restore it withPUSH/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.
83Hfor 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":
-
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. - 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.
- 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.
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 1adds 1 to ACCU1. Because the pointer is a 32-bit word, the STEP 7 assembler must generate the 32-bitINC Dform. 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, writeL MD 100first. - The reset branch initialises the pointer unconditionally on the cycle that
M 1.2is true. If the increment edge fires on the same cycle, it will increment the freshly-loadedP#2.0toP#2.1, not back toP#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 pointerMD 104for 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
LOOPline. - The
REPTlabel style used in the source code is from the Siemens Graph 7 / HiGraph 7 editor. In a pure STL block, use the conventionalNEXT:label and place it on its own line. -
LW 0is 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
- 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.
-
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. -
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. -
Declare MD 100 as a
DWORDin a DB, not a bit-memoryMD. 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 markedRETAIN. -
Document the area ID in the symbol. Naming a tag
ptrSeq : DWORD := DW#16#83000000immediately tells the next engineer that it points into the bit-memory area. -
Edge-trigger every step input. Use
FPfor the increment,FPfor the reset, and a mutual-exclusion mask to ensure both edges cannot fire on the same cycle. -
Avoid
LAR1 P#0.0without 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
- 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. - Force
M 1.2 = 1for one cycle. ConfirmAR1 = DW#16#830000A0(P#M 20.0 → area 83, byte 20 × 8 = 0xA0, bit 0). - Pulse
M 1.1once. ConfirmM 20.0 = 1and thatAR1 = DW#16#830000A1(P#M 20.1). - Pulse
M 1.1a second time. ConfirmM 20.1 = 1andAR1 = DW#16#830000A2(P#M 20.2). - Continue for 16 pulses. After the 16th pulse, confirm that
AR1wraps back toDW#16#830000A0andM 20.0is set again. - 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.