STEP 7 STL Indirect Addressing with M[DBD] and AR1/AR2

David Krause19 min read
HMI ProgrammingSiemensTutorial / 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: Why Indirect Addressing in STEP 7 STL

Indirect addressing lets one STL instruction sequence operate on a memory location whose address is computed at runtime rather than fixed at compile time. In Siemens STEP 7 V5.x for the S7-300 and S7-400 families, this technique is essential for parameterised recipes, indexed loops, batch handlers, and any logic that must walk through arrays whose bounds are not known at edit time. The classic form of the construct is the memory-indirect expression A M [DBD 140] or its register-indirect sibling A M [AR1, P#0.0].

The trade-off is that the operand to the right of the square brackets is no longer a constant symbol but a pointer, and the rules for how that pointer is interpreted depend on the address identifier, the data type, and whether you are crossing area boundaries (DB, M, I, Q, L) or staying inside one area. Misinterpreting the pointer format is the single most common reason an indirect read returns the wrong bit or triggers an area-length error during runtime. A general introduction to addressing modes is available on the Wikipedia addressing-mode reference; the rest of this document drills into the Siemens-specific format that STEP 7 STL expects.

This reference walks through both pointer families, shows the bit/byte arithmetic that connects a value such as 1234 to the actual operand M154.2, and demonstrates the SLD 3 conversion used when you need to move a byte address into address register AR1 or AR2. Programming and operating details for STEP 7 are documented in the official Siemens Industry Online Support portal.

Pointer Anatomy: Byte, Bit, and Area Codes

Before dissecting a real instruction it helps to lay out the bits of a STEP 7 pointer. A 32-bit pointer is divided into two fields:

  • Byte number (24 bits, bits 3-31): unsigned offset inside the selected area.
  • Bit number (3 bits, bits 0-2): value 0-7, the bit selector inside that byte.

When the high byte of the 32-bit value is non-zero, the pointer also carries an area code in bits 24-31. The codes STEP 7 recognises are:

Area code (hex) Area Used in instruction form
0x00 Default (area-internal) M[..], DBX[..], I[..], Q[..], L[..]
0x81 Inputs (I / PI) Area-crossing pointer on input area
0x82 Outputs (Q / PQ) Area-crossing pointer on output area
0x83 Locals (L) Area-crossing pointer on temp area
0x84 Data block (DB) Area-crossing pointer opened by OPN DB
0x85 Instance DB (DI) Multi-instance area-crossing pointer

The decimal value the CPU actually stores in a DBD for a bit operand is therefore 8 * byte + bit. This is the relationship every STL programmer eventually commits to muscle memory.

Memory-Indirect Addressing: M [DBD] Explained

The mnemonic M [DBD 140] parses as: read the 32-bit value stored in data-block double-word DBD140 (inside the currently opened DB), treat that value as a bit address inside the memory area, and logically AND the bit at that location with the RLO. A minimal example lifted from the original question:

OPN   DB   103          // Open DB 103; subsequent DBW/DBD/DBX use it
A     M    [DBD 140]    // AND M bit whose address is stored in DB103.DBD140
=     M    206.0        // Latch the result to M206.0
A     M    [DBD 384]    // Same idea, second pointer slot in DB103
=     M    206.2
OPN   DB   104          // Switch active DB
A     M    [DBD 20]     // Read pointer from DB104.DBD20 and test the M bit
=     M    206.1

Three points are easy to miss on a first reading:

  1. The expression inside the brackets is always evaluated as a bit address when the operand outside the brackets is bit-typed (M, I, Q, DBX). This is why DBD140 = 1234 resolves to M154.2 and not to M1234.0 as the byte-oriented reader may expect.
  2. The data-block operand (DBD) names a byte offset inside the currently open DB. The CPU does not interpret DBD140 as a pointer to an absolute DB number; it is a 32-bit slot in the data block opened by the most recent OPN DB (or a multi-instance DB opened by CDB). Any subsequent OPN retargets every following DBD[..] reference.
  3. The result = M 206.0 writes to a fixed bit. The indirect address influences only the source of the read, not the destination.
Field note: An S7 will not reject an out-of-range bit address until the statement is executed. If the computed value is, say, 99999, you will get an area-length error (OB121) instead of a clear compile-time message. Always clamp the pointer to its legal range before the read.

Decoding the DBD Bit Address Math

Siemens packs byte and bit information into a single 32-bit value in the form P#byte.bit. For a bit operand the lowest three bits encode bit (0-7) and the remaining bits encode the unsigned byte offset within the chosen area. The decode is therefore:

byte_offset = pointer_value DIV 8
bit_index   = pointer_value MOD 8
operand     = M(byte_offset).(bit_index)

Worked examples:

Value in DBD Byte (DIV 8) Bit (MOD 8) Resolves to
0 0 0 M 0.0
7 0 7 M 0.7
8 1 0 M 1.0
15 1 7 M 1.7
87 10 7 M 10.7
1000 125 0 M 125.0
1007 125 7 M 125.7
1234 154 2 M 154.2
65535 8191 7 M 8191.7

The maximum legal value of a 32-bit bit address is 8 * (area_size_in_bytes) - 1. For the M area on an S7-300/400 the byte count is configured in the CPU hardware properties; if the active DB stores a pointer that is larger than the M area can hold, the CPU raises the standard area-length error and calls OB121 if it is loaded. The same arithmetic applies to DBX [DBD..], I [DBD..], and Q [DBD..] — the only thing that changes is which area the byte offset is applied to.

Register-Indirect Addressing: AR1 and AR2

When the source of the address is not in a data block (it may be calculated in an FB's static area, in a multi-instance, or in a local temp variable), STL provides two 32-bit address registers: AR1 and AR2. They are loaded with the LAR1 / LAR2 instructions and consumed with the [AR1, P#0.0] or [AR2, P#0.0] syntax. An optional constant P#x.y adds a fixed offset to the register content. A canonical sequence:

L     #Address              // INT or DINT byte offset, e.g. 10
SLD   3                      // Multiply by 8 -> bit pointer 80
LAR1                          // AR1 = P#10.0 (area-internal, M area by default)
OPN   DB    1
L     DBW   [AR1, P#0.0]    // Reads DB1.DBW10
T     #Result                // Move to local WORD

The shape of the value inside AR1 determines what the read will see:

AR1 contents (hex) Area Type Sample operand
00 00 00 50 (P#10.0) M (default area-internal) Bit / Byte / Word / DWord M[AR1, P#0.0] = M 10.0
00 00 01 18 DBX inside area-internal Bit DBX[AR1, P#0.0] = DBX 14.0
84 00 00 50 DB (area-crossing) Bit / Byte / Word / DWord DBW[AR1, P#0.0] with DB number in high word
81 00 00 50 Input (I) Bit / Byte / Word / DWord IB[AR1, P#0.0], IW[AR1, P#0.0]
82 00 00 50 Output (Q) Bit / Byte / Word / DWord QB[AR1, P#0.0]

The high byte in the table is the area code that the S7 uses to route the read. The default area-internal pointer 00 00 00 50 inherits the area from the instruction that uses it, so M[AR1, P#0.0] is M-area and DBW[AR1, P#0.0] is data-block (the DB opened by the most recent OPN DB or the multi-instance DB currently bound by the call stack).

Remember: AR1 is implicitly clobbered by the system when an FB is called. If you need a value to survive across an FB call boundary, use TAR1 / TAR2 to copy AR1/AR2 into the local stack, then re-load with LAR1 / LAR2 on return. This is a common source of corrupted-pointer crashes that only show up on certain scan orderings.

Building a Pointer with SLD 3

Most engineers keep their pointers in bytes (array index) or in element numbers (record number). STL, however, expects bits. SLD 3 ("shift left double-word by 3 bits") multiplies the source by 8 and slides the result into the pointer shape. The reason is straightforward: the low three bits are the bit selector, so to clear them and reserve them for the .0 you must shift the integer left by exactly three bit positions. Worked example:

L     #IndexByte            // INT, e.g. 10 = "the 10th byte"
SLD   3                      // 10 << 3 = 80 = P#10.0
LAR1                         // AR1 = 0x50
L     MB [AR1, P#0.0]      // Read MByte 10
T     #WorkByte

Alternative ways to land a byte index inside AR1:

Method STL snippet Effect
Shift + LAR L #n; SLD 3; LAR1 Cleanest; explicit "byte to bit pointer" intent
Direct pointer literal LAR1 P#10.0 Constant; ideal for fixed table walks
Pre-shifted DINT L L#80; LAR1 Use only when the source is already a bit pointer
From DBW (no shift) L DBW10; SLD 3; LAR1 Loads a stored byte index and prepares it
From DBD pointer L DBD140; LAR1 Use when DB already holds a bit pointer (no shift needed)

If you skip the SLD 3 on a byte-oriented index, the low three bits of the value are not zeroed. A stored index of 10 would land in M10.x where x is whatever was sitting in the low bits. This is the classic "off by a bit or three" bug that surfaces only at certain indices and is one of the harder things to spot during commissioning.

Area-Crossing vs Area-Internal Pointers

STEP 7 distinguishes two pointer formats that interact with the area code in different ways:

  1. Area-internal pointer (32 bits) — only the byte/bit field is meaningful; the high byte is zero. The instruction determines the area. Used with M[..], DBX[..], I[..], Q[..], L[..], etc.
  2. Area-crossing pointer (48 bits / 6 bytes) — used in the older POINTER data type and in FB parameter passing. Format: DB-number * 0x100000000 + byte.bit. The high 16 bits hold the DB number; the low 32 bits hold the byte.bit pointer.

The 48-bit pointer is what you see in the symbolik of an FB's IN/OUT/IN_OUT block parameters. To use it for area-crossing reads you need to pre-load the DB number into the high word and not call OPN DB — the register combination is the only thing the CPU trusts. Conversely, the 32-bit pointer you build with SLD 3 is area-internal and the open DB (or the implicit area in the instruction) wins.

Convert a 48-bit POINTER into a 32-bit area-internal pointer in three instructions:

L     P##SrcPointer          // Load the 48-bit POINTER (e.g. an FB IN)
LAR1                          // AR1 = low 32 bits = byte.bit (area-internal)
L     P##SrcPointer
T     #DBNumber              // High 16 bits; remember to call OPN DI / OPN DB

Step-by-Step: Implementing an Indirect Read

Prerequisites

  • STEP 7 V5.5 or compatible engineering tool installed
  • S7-300/S7-400 CPU with at least firmware V2.x
  • A source DB (e.g., DB 103) with DBD140, DBD384 already initialised to valid bit-address values
  • OB1, an FB, or an FC to host the STL network
  • OB121 and OB122 loaded so the CPU stays in RUN during transient bad-pointer conditions during commissioning

Procedure

  1. Open the source DB (here DB 103) with OPN DB 103.
  2. Load the indirect source bit with A M [DBD 140]. The CPU reads DB103.DBD140, decodes the byte/bit, and tests the corresponding M bit against the current RLO.
  3. Assign the RLO to the latching bit = M 206.0.
  4. Repeat steps 2-3 for any further slots (e.g., A M [DBD 384]; = M 206.2).
  5. If a different DB supplies the next pointer, change the open DB with OPN DB 104 before continuing.
  6. Save the block, download to the CPU, and place the CPU in RUN.

Verification

  • Open the DB online (Watch Table or Monitor/Modify) and confirm the pointer values are within range. Use a DB Monitor with the "All" display format to see both decimal and pointer (P#x.y) representations.
  • Force the M bit at the decoded address and confirm the result bit toggles.
  • Force an out-of-range value (e.g., 99999) and confirm that OB121 is installed and called — that proves the area-length check is active.
  • Capture the CPU diagnostic buffer; expect no "Area length error" entries during normal operation.

Step-by-Step: Converting a Byte Address to a Bit Pointer

Use this routine when an operator sets a recipe number (1, 2, 3, …) and your code must look up a parameter record in an FB's STAT area by record number.

  1. Read the recipe number into a local INT (#Index).
  2. Multiply by the record size (e.g., 20 bytes) to get the byte offset: L #Index; L 20; *I; T #ByteOffset.
  3. Convert the byte offset to a bit pointer: L #ByteOffset; SLD 3; LAR1.
  4. Read the record: L DBB [AR1, P#0.0]; T #WorkByte (or use DBW[..] / DBD[..] for word/double-word fields).
  5. If the record number is taken from an HMI tag, clamp it before step 2 to avoid OB121.
  6. Post-increment AR1 with +AR1 P#1.0 inside a loop body to walk the record byte-by-byte without reloading the shift constant each iteration.

Full example FB body (FB "RecipeLookup", STAT recipe array of 20 bytes, runtime #Index 1..50):

// Clamp index
L     #Index
L     1
<I    // If #Index < 1, jump to error
JC    ERR
L     #Index
L     50
>I    // If #Index > 50, jump to error
JC    ERR

// Build pointer
L     #Index
L     20
-I                      // Offset = (Index - 1) * 20
DEC                     // -1 because STL arrays are 0-based
SLD   3                 // Byte offset -> bit pointer
LAR1                    // AR1 = base address of record

// Read first field of the record (REAL temperature setpoint)
L     DBD [AR1, P#0.0]   // Record[0]: REAL Temperature_SP
T     #TemperatureSet

// Read second field (WORD machine code)
L     DBW [AR1, P#4.0]   // Record[4]: WORD MachineCode
T     #MachineCode

// Walk to next byte for a third field (BYTE options)
L     DBB [AR1, P#6.0]   // Record[6]: BYTE Options
T     #Options
Field tip: If you read elements rather than bytes (for example the i-th element of a 20-byte user-defined type), the shift is still SLD 3 provided the size of an element is 1 byte. For an element that is 4 bytes, use L 4; *D; SLD 3 to walk the array correctly. For a multi-field STRUCT of mixed types, advance AR1 with +AR1 P#fieldsize.0 instead of multiplying.

Working with the ANY Pointer Type

When an SFC such as SFC20 BLKMOV, SFC21 FILL, or SFC22 CREAT_DB accepts an ANY pointer, you must populate a 10-byte block that the CPU interprets as source or destination descriptor. The fields are:

Byte offset Field Content Example
0-1 Syntax ID 10h = BYTE, 11h = WORD, 12h = DWORD, 13h = REAL, 15h = BLOCK_DB, 16h = COUNTER, 19h = TIMER W#16#10 for BYTE
2-3 Data type length (bytes) 1, 2, 4, 8 W#16#1
4-5 Number of elements Replication count W#16#20 for 32 bytes
6-7 DB number (or 0) 0 for non-DB areas W#16#0 for M area
8-11 Area + byte.bit pointer 32-bit area-internal pointer P#M 100.0 BYTE 32

Build an ANY for a copy from MB100 (32 bytes) into DB200.DBB50 (32 bytes) using only STL:

// Source ANY (M area, byte 100, 32 bytes)
L     W#16#10                 // Syntax ID = BYTE
T     #SrcAny.syntaxID
L     1                       // Length of one element
T     #SrcAny.length
L     32                      // Replication count
T     #SrcAny.count
L     0                       // No DB for M area
T     #SrcAny.dbNumber
L     P#M 100.0 BYTE 32      // Area-internal pointer + repetition
T     #SrcAny.pointer

// Destination ANY (DB200, byte 50, 32 bytes)
L     W#16#10
T     #DstAny.syntaxID
L     1
T     #DstAny.length
L     32
T     #DstAny.count
L     200
T     #DstAny.dbNumber
L     P#DBX 50.0 BYTE 32
T     #DstAny.pointer

// Call SFC20
CALL  SFC   20
SRCBLK := #SrcAny
DSTBLK := #DstAny
RET_VAL := #RetVal

Indirect Addressing in FBs vs FCs

The block type affects which pointer sources are practical.

  • FCs have no STAT area, so any pointer must come from a global DB or a literal P#... The local stack (TEMP) is re-initialised every call, so saving AR1 across a call inside an FC is dangerous; use a global marker or DB slot instead.
  • FBs own a STAT area, which is the natural home for indirect-pointer bases, index accumulators, and look-up tables. Multi-instance FBs share a single instance DB per parent FB, so the pointer is the same data word regardless of which instance is called.
  • OB1 cannot keep state in TEMP across cycles. Any pointer that must survive from one OB1 cycle to the next lives in a global DB or the M area.

Pointer Survival Across Block Calls

AR1 is implicitly used by the system when an FB receives a parameter. If your STL code loaded AR1 with LAR1 P#M 100.0 before a CALL, you will see the value of AR1 changed on the return line. Save and restore in TEMP:

TAR1                          // Save current AR1 to accumulator
TAR1  AR2                     // Copy AR1 -> AR2
LAR1  P#M 100.0              // Set up pointer for this FB
CALL FB 200                    // System uses AR1 internally; AR1 may be clobbered
LAR2                          // Reload original pointer from AR2
// ... continue

For FBs the same idiom is implemented by saving the pointer into a STAT slot at the entry of the FB and reloading AR1 at the exit. Modern STEP 7 best practice is to keep all pointer work in STAT and avoid depending on AR1 surviving.

STL vs SCL: Choosing the Right Tool

Indirect addressing in STL is fast (single CPU instruction per read) and fully visible, but error-prone. The same logic in SCL on a S7-300/400 or S7-1500 reads like:

FOR i := 1 TO 50 BY 1 DO
    IF DB103.MarkerWord[i] THEN
        M206.0 := TRUE;
    END_IF;
END_FOR;

The trade-off matrix:

Criterion STL with M[DBD] SCL with array index
Readability Low — pointer math required High — symbolic array access
Performance (S7-300/400) One microsecond-class instruction Indexed call through compiler-generated loop
Compile-time type check None — symbol enters the brackets as a raw value Yes — array bounds enforced
Migration to S7-1500 STL is deprecated; manual port required SCL is the recommended path
Commissioning visibility Excellent — STL is inspectable online Compiles to STL; harder to step through

For new code on S7-1500 write SCL. For legacy S7-300/400 maintenance that already uses M[DBD] semantics, keep STL and rely on the patterns above.

Migration to S7-1500 / TIA Portal

S7-1500 CPUs in TIA Portal support SCL directly with full symbolic array access. STL is still accepted for legacy imports but is not the recommended language. The pointer concept disappears in the higher abstraction: an ARRAY[1..50] OF BOOL is indexed by an INT variable and the compiler enforces bounds. If the program must remain in STL on S7-1500, the same SLD 3 / LAR1 / [AR1, P#0.0] sequence still works for area-internal reads, but the older OPN-DB and DBD-indirect patterns are emulated as a compatibility layer. Validate by stepping through Watch Tables after the migration.

Pointer Format Reference Table

Field Width (bits) Range Notes
Byte number 24 (bits 3-31) 0 - 16 777 215 Address within the selected area
Bit number 3 (bits 0-2) 0 - 7 Bit offset inside the byte
Area code 8 (high byte of DWORD) 00, 81, 82, 83, 84, 85 0=M, 81=I, 82=Q, 83=L, 84=DB, 85=DI
DB number (48-bit pointer only) 16 0 - 65 535 High word of 48-bit POINTER / ANY

For an area-crossing pointer the high word is the 16-bit DB number and the low 32 bits follow the layout above. The area code for DB reads is 84 when the 48-bit pointer came from an FB parameter; for a multi-instance the code is 85. The high byte is only consulted by the CPU when the instruction is a area-crossing read; for an area-internal DBW[AR1,..] the high byte is ignored entirely.

Common STL Errors and Diagnostics

Symptom Likely cause Diagnostic Fix
CPU goes to STOP with SF lit, OB121 called Pointer outside the area Read STL stack info; last executed statement Clamp pointer or expand the DB
Always reads bit 0.0 regardless of pointer Forgot SLD 3 and the value was < 8 Watch table: DBD value vs. AR1 content Insert SLD 3
Reads from wrong area Area-crossing pointer loaded with area-internal layout Use VAT to display the DWORD as hex Adjust the high byte to the right area code
Corrupted pointer across FB call AR1 was reused by the called FB Step through with breakpoints Save AR1 with TAR1 and reload with LAR1
DB number ignored Used 32-bit area-internal pointer for an FB IN parameter Compare VAT view with parameter value Build full 48-bit pointer or strip the DB number and rely on OPN
SFC20 RET_VAL negative ANY descriptor corrupted or wrong length Inspect ANY in VAT in hex view Rebuild ANY with the correct syntax ID, length, count, DB number, pointer
Read returns stale data DBW/DWD read instead of DBX for a bit test Online monitor the ACCU2 result Use DBX[..] for bits, DBW[..] for words

Verification Checklist

  • Pointer value in the source DB equals a valid P#byte.bit when interpreted as bits.
  • AR1 / AR2 content shown online matches the intended byte/bit of the area.
  • All pointer values that arrive from the HMI / recipe are clamped to the legal range in OB100 or a startup FB.
  • OB121 (programming error), OB122 (I/O access error) are loaded in the CPU so the system stays in RUN when a fault is forced for testing.
  • The first execution cycle logs the decoded pointer to a VAT for an audit trail.
  • Multi-instance FBs share their pointer bases in the parent's instance DB; verify that no instance overwrites another instance's pointer.
  • For SFC20/SFC21 calls, the ANY descriptors are populated in the same cycle they are consumed; do not let interrupts write to a partially built ANY.

FAQ

What does M [DBD 140] actually mean in STEP 7 STL?

It is a memory-indirect bit operand. The CPU reads the 32-bit value stored in DB103.DBD140 (the active DB is DB 103), treats it as a bit address inside the M area, and uses the byte/bit split to locate the source bit. For example, the value 1234 resolves to M 154.2 because 1234 = 8 * 154 + 2.

Why do I need SLD 3 before LAR1?

STL address registers expect the full bit address (bits 0-2 are the bit selector). A byte index such as 10 must be shifted left by three bit positions so the value becomes 80 (hex 0x50), which is the bit-pointer form P#10.0. Skipping the shift leaves garbage in the bit-selector field and the read lands on a neighbouring bit.

How are AR1 and AR2 different?

AR1 and AR2 are functionally identical 32-bit address registers; the difference is purely conventional. AR1 is implicitly used by the system for parameter passing, so user code usually saves and restores it with TAR1 / LAR1 around calls. AR2 is generally free for application use, which makes it the preferred scratch register for pointer-heavy code.

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

An area-internal pointer is a single 32-bit value that holds the byte/bit only; the area (M, DB, I, Q) is taken from the instruction. An area-crossing pointer is 48 bits: a 16-bit DB number in the high word plus the 32-bit byte/bit in the low word. You need an area-crossing pointer when the address must carry the DB number along with it, such as an ANY or POINTER parameter to an FB.

How do I prevent OB121 from stopping the CPU on a bad pointer?

Load and configure an OB121 in the S7 project. The CPU will then call OB121 on a programming error (bad pointer, illegal operand combination) and continue with the next cycle. Combine the OB with a clamp routine at the source of every operator-supplied index so the bad value is corrected before it reaches the indirect read.

Back to blog