S7-300/400 Memory-Indirect Addressing via DB Pointer in STL

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

Memory-indirect addressing lets a STEP 7 program build the absolute address of a bit, byte, word, or doubleword at runtime instead of hard-coding it. The address is stored in a tag (a DB word, an M word, or a temporary local) and the CPU dereferences it when the instruction executes. This is the correct tool when the target address is data-driven: a recipe number selects a parameter slot, a station index selects a status word, or—as in the source scenario—a value in DB1.DBW70 points at a status bit in the M area (M20.0, M20.1).

This article covers the S7-300/400 STL implementation used in SIMATIC Manager (STEP 7 V5.x) and finishes with the equivalent syntax in TIA Portal for the S7-1500. All S7-300 and S7-400 CPUs that support STL (all standard CPUs, no F/FH variants required) can run the code shown.

Pointer-vs.-value distinction. The DB word does not contain the bit. It addresses the bit. The number 160 stored in DB1.DBW70 is decoded by the CPU as byte 20, bit 0, which is M20.0 when used as an M-area pointer.

Prerequisites

  1. SIMATIC Manager with STEP 7 V5.5 or V5.6 (or compatible V5.4 SPx) installed.
  2. An S7-300 or S7-400 project with at least one DB1 and a free FC/FB block to host the pointer logic.
  3. The S7 program must be compiled with STL as the active editor language (Options > Customize > STL: enable "Address field width 24" is not required but is recommended for clarity).
  4. Basic familiarity with bit memory layout: M0.0 is bit 0 of byte 0; M0.7 is bit 7 of byte 0; M1.0 is bit 0 of byte 1.

How the Pointer Word Is Structured

STEP 7 (S7-300/400) uses a 16-bit pointer word for memory-indirect bit/byte/word/dword access. The layout is fixed by Siemens and applies to every area except the DB area, which uses a 32-bit DBW pointer format.

Bit position Field Meaning Example for M20.0 Example for M20.1
15 … 8 Reserved (0) Always 0 for intra-area pointers 0x00 0x00
7 … 3 Byte address Byte number in the area (0…65535) 0001 0100 = 20 0001 0100 = 20
2 … 0 Bit address Bit number within the byte (0…7) 000 001

Decimal form follows the simple formula:

pointer_value = (byte_number × 8) + bit_number

Worked examples from the source scenario:

  • M20.0 → byte 20, bit 0 → pointer = (20 × 8) + 0 = 160 (hex 0x00A0)
  • M20.1 → byte 20, bit 1 → pointer = (20 × 8) + 1 = 161 (hex 0x00A1)
  • M0.0 → pointer = 0
  • M255.7 → pointer = (255 × 8) + 7 = 2047 (hex 0x07FF — the largest intra-area M pointer)
Boundary. The S7-300 has at least 2048 M bits (M0.0 … M255.7) on every standard CPU. The pointer math above is valid for the entire default M area. For CPUs that ship with larger M areas (e.g., CPU 319 with 8 KB), only the lowest 2 KB of bit memory can be reached with a 16-bit byte×8+bit pointer; the remainder must be addressed with a 32-bit area-crossing pointer.

Step 1 — Create a Reusable Pointer-Build FC

Create a new function (FC) in your S7 program: right-click the Blocks container → Insert New Object → Function. Name it FC100 "Build_M_Pointer" (or your own convention). Open the FC and declare the following interface in the Interface pane of SIMATIC Manager (top section of the editor):

Name Type Direction Initial / Comment
byte_nr INT Input Byte number of the target M bit (0 … 65535)
bit_nr INT Input Bit number within the byte (0 … 7)
out_ptr DWORD Output Pointer word usable as M-area indirect address
temp_1 WORD Temp Intermediate shifted byte value

Switch the FC body to STL (View → STL), then enter the following network:

//  Network 1: Build M-area pointer from byte and bit numbers
      L     #byte_nr           // Load byte number (e.g., 20)
      SLW   3                  // Shift left word by 3 bits (= × 8)
      T     #temp_1            // Store the shifted byte in temp
      L     #bit_nr            // Load bit number (e.g., 1)
      L     W#16#7             // Mask: keep only bits 0..2 (0x0007)
      AW                       // AND word -> isolate the bit field
      L     #temp_1            // Bring the shifted byte back into ACCU1
      OW                       // OR bit field into low 3 bits
      T     #out_ptr           // Result is a 16-bit pointer, zero-extended to DWORD

The output is left-justified in the low word of out_ptr; the high word remains zero, which is the correct representation for use as an M-area intra-area pointer.

Step 2 — Read a Bit Indirectly

Call FC100 from any point in your program (OB1, FB, or another FC) and use the resulting out_ptr as the address operand of a bit-test instruction. The standard pattern is to load the pointer into AR1 with LAR1 and then dereference with the [AR1, P#0.0] syntax.

//  Network 2: Read status of M20.0 / M20.1 driven by DB1.DBW70
      L     DB1.DBW70          // Load pointer value from DB1 (e.g., 160 or 161)
      T     #ptr_word          // Local WORD temp, or feed straight into FC call

      CALL  FC    100
        byte_nr := 20          // optional fixed; can also be derived
        bit_nr  := #bit_idx    // optional
        out_ptr := #m_ptr      // DWORD temp

      L     #m_ptr             // Load pointer value
      LAR1                     //  -> address register AR1

      A     M [AR1, P#0.0]     // Read the addressed M bit into RLO
      =     M     100.0        // Mirror it to a fixed bit for monitoring

If you want the byte and bit numbers to be data-driven as well, store them in the DB:

//  DB1 layout (relevant slice):
//  DBW70  WORD  Pointer value   (byte_nr * 8 + bit_nr)
//  DBW72  INT   Byte number     (e.g., 20)
//  DBW74  INT   Bit number      (0..7)

      L     DB1.DBW72          // Byte number
      T     #byte_nr
      L     DB1.DBW74          // Bit number
      T     #bit_nr

      CALL  FC    100
        byte_nr := #byte_nr
        bit_nr  := #bit_nr
        out_ptr := #m_ptr

      L     #m_ptr
      LAR1
      A     M [AR1, P#0.0]
      =     M     100.0

Step 3 — Write a Bit Indirectly

Writing uses the assignment instruction S (set), R (reset), or = with the same indirect form. The same out_ptr from FC100 drives the address.

//  Network 3: Force the addressed M bit ON if DB1.DBX76.0 is true
      L     DB1.DBW72
      T     #byte_nr
      L     DB1.DBW74
      T     #bit_nr

      CALL  FC    100
        byte_nr := #byte_nr
        bit_nr  := #bit_nr
        out_ptr := #m_ptr

      L     #m_ptr
      LAR1

      A     DB1.DBX76.0        // Force enable from DB
      S     M [AR1, P#0.0]     // Set the addressed bit

      A     DB1.DBX76.1        // Reset enable from DB
      R     M [AR1, P#0.0]     // Reset the addressed bit
Mutually exclusive controls. Always separate the S and R enables with the same out_ptr. If you call FC100 twice in one network with different inputs, both resulting addresses are valid; STEP 7 will simply operate on two different bits in the same scan.

Step 4 — Address the Same Bit Repeatedly

When several networks need the same M bit, build the pointer once in OB1, store it in a global M-word, and dereference the same value everywhere. Avoid calling FC100 in every network — it costs CPU time and creates a maintenance hazard (each call must use identical inputs).

//  OB1, Network 1: refresh global M pointer at start of cycle
      L     DB1.DBW72
      T     MB   200           // MB200..MB203 hold the DWORD pointer
      L     DB1.DBW74
      T     MB   202           // not strictly needed for low-word pointer,
                               // but reserved for future area-crossing use

//  Now M[MW200] is a valid pointer to the addressed M bit.

Then in any downstream block, use:

      L     MW   200
      LAR1
      A     M [AR1, P#0.0]
      =     M     110.0

Step 5 — S7-1500 Equivalent in TIA Portal STL

The S7-1500 keeps the memory-indirect concept but changes the syntax: tag-based indirect addressing replaces the address-register form used on S7-300/400. The official Siemens documentation describes the two flavours:

Worked example for the S7-1500 targeting the same M20.0:

//  In a function block with tags:
//     "ptrWord"   : Word    (16-bit pointer value)
//     "mBitState" : Bool
//     "i"         : Int     (loop index, optional)

      // build pointer once
      #ptrWord := INT_TO_WORD(#byteNr * 8) OR WORD#16#0000; // bit number added separately
      #ptrWord := #ptrWord OR (INT_TO_WORD(#bitNr) AND W#16#0007);

      // indirect read - tag-based, not register-based
      #mBitState := "%DB"."M"."M20_0"; // not the indirect form
      // The truly indirect form in S7-1500 STL is the PEEK/POKE runtime,
      // or symbolic Variant dispatch; see Siemens 109011420.

For bit-level indirect access, the S7-1500 typically relies on the PEEK / POKE instructions (slice of a Word) rather than the legacy A M [AR1, P#0.0] form. Slice syntax: "M"."M20".%X0 (bit 0 of MW20) and "M"."M20".%X1 (bit 1) are reachable symbolically; for true runtime indirection, convert the pointer to a VARIANT and use the VariantGet / VariantPut instructions, or use the PEEK_BOOL block from the standard library. See the TIA Portal help index for "Indirect addressing in STL" for the exact block available in your firmware.

Common Errors and Field Diagnostics

Symptom Likely root cause Fix
Bit never reads TRUE, even though the physical M is set out_ptr is loaded into ACCU1 but LAR1 is missing or the wrong AR is used. Insert LAR1 after the L and verify with a watch table on AR1.
Wrong bit toggles Bit field not masked with W#16#7; bit number larger than 7 is corrupting the byte field. Re-check FC100: L W#16#7; AW must run before the OW step.
CPU goes to STOP with SF / "Area length error" Pointer value exceeds the M area, or the area-crossing pointer format was used with an intra-area instruction. Range-check byte_nr against the CPU's M size; clamp to 0…255 on S7-300.
Indirect write writes to the wrong byte Forgot the SLW 3; the byte field is at bits 3..15, not bits 0..7. Re-insert the SLW 3 in FC100 and recompile.
Online monitor shows pointer value but the bit doesn't toggle Watch table is monitoring an absolute address while the program writes to an absolute M bit that is overlapped by an FB static area. Verify the M area is not in use as FB multi-instance or process-image partition; check the symbol table.
Program compiles in STL but the FC returns 0 for every call The FC is called with the interface in optimized form (newer STEP 7 V5.x projects). Optimized block access breaks LAR1-based indirection. Right-click the FC → Object Properties ⇒ Attributes → uncheck Optimized block access for this FC. Re-compile.

Verification Procedure

  1. Open the project in SIMATIC Manager, download HW Config and the S7 program to the CPU in RUN/PROGRAM mode, and clear the CPU to a defined state.
  2. Open a watch table (VAT) and force DB1.DBW70 = 160 (decimal) and observe M20.0 in the same VAT — set M20.0 to TRUE; the result of the indirect read should follow.
  3. Change DB1.DBW70 = 161 and verify that the read is now tracking M20.1 instead.
  4. Force out-of-range values (DB1.DBW70 = 2048 for a CPU whose M stops at byte 255) and confirm the CPU does not go to STOP — the FC should be guarded with range checks if the application can produce out-of-range inputs.
  5. Use a VAT single-step to cycle through 0 … 2047 in DB1.DBW70 and record the addressed M bit at each step; spot-check the first and last entries (0 ↔ M0.0, 2047 ↔ M255.7).
  6. Capture a trace with the S7-Trace tool: trigger on the OB1 cycle start, sample AR1, DB1.DBW70, and the targeted M bit to confirm the pointer is loaded correctly every cycle.

Area-Crossing Pointer Reference (Optional)

When the address you need lives outside the standard M area — for example, in a different DB, in the process image of inputs (PEB), or in the timer/counter area — STEP 7 requires the 32-bit area-crossing pointer. Its bit layout is:

Byte Bit 7…0 meaning
Byte 0 Bits 0…2: bit address. Bits 3…7: byte address low 5 bits (0…31). Byte 0 = (byte_addr × 8) + bit_addr & 0x07.
Byte 1 Byte address high 8 bits (0…255 for a 32-bit byte address, total 13-bit byte field).
Byte 2 Area identifier: 0x81 = PE, 0x82 = PA, 0x83 = M, 0x84 = DB / DI, 0x85 = DB (old form), 0x86 = L (local), 0x87 = V (preceding-data).
Byte 3 0x00 for "any" length; 0xFF = indirect.

For the M area the area-crossing pointer is 0x83000000 + (byte×8 + bit). For M20.0 that gives 0x830000A0; for M20.1, 0x830000A1. With area-crossing pointers, dereference using LAR1 + the L / T byte/word/dword pointer form, e.g. L B [AR1, P#0.0].

Key Formula and Constants

Quantity Formula / Value Notes
Byte-to-pointer shift SLW 3 (multiply by 8) Required because the byte address occupies bits 3…15 of the pointer word.
Bit mask W#16#0007 Keep only bits 0…2; reject any out-of-range bit numbers.
Max intra-area M pointer 2047 (decimal), 0x07FF Byte 255, bit 7.
Min intra-area M pointer 0 (decimal), 0x0000 Byte 0, bit 0 = M0.0.
M area identifier (area-crossing) 0x83 High byte of the 32-bit pointer.

Field-Proven Caveats

  • Never place the pointer word in a retain M area if the addressed bit is also in the non-retain area — the M area is not coherent across retain/non-retain boundaries in some CPUs, and the indirect form can read from the wrong physical region.
  • The block in which the LAR1 / [AR1, P#0.0] sequence executes must be an STL block. LAD and FBD do not expose the address-register syntax; if you have to remain in LAD/FBD, use the symbolic Variant dispatch from S7-1500 (TIA Portal) or rewrite the logic as a case statement over the small set of expected addresses.
  • Do not nest more than two LAR1 / LAR2 levels per block. STEP 7 V5.x reserves AR1 and AR2 for the system, and any STL function with deep address-register usage can corrupt the caller's pointers.
  • If your project is on an S7-400 with multiple OBs of different priority classes, be aware that an OB with a higher priority can change the addressed M bit while the lower-priority OB is mid-indirect. Use the process image partition assignment to serialize access to the affected range.
  • Always define the out_ptr as DWORD, not WORD. Some instructions in STEP 7 V5.x sign-extend a 16-bit pointer; using DWORD avoids that pitfall and keeps the door open for an area-crossing pointer without re-declaring the interface.

FAQ

What decimal value do I put in DB1.DBW70 to read M20.0?

160 (decimal). Compute it as (byte_number × 8) + bit_number, so 20 × 8 + 0 = 160. The bit number 1 is at decimal 161.

Why does the FC use SLW 3 and then AND with W#16#7?

SLW 3 shifts the byte number into bits 3…15, which is where the byte field of the pointer lives. W#16#7 (binary 0000 0111) masks the bit number to bits 0…2. OR-ing the two halves yields a valid intra-area pointer.

My pointer is 16 bits — do I need to declare out_ptr as WORD or DWORD?

Declare it as DWORD. STEP 7 zero-extends the 16-bit pointer to 32 bits for AR1 loads, and DWORD gives you headroom to switch to an area-crossing pointer (which is always 32 bits) without changing the interface.

Can the same DB word point to inputs (PEB) or outputs (PAB) instead of M?

Yes, but you must use the 32-bit area-crossing pointer with the area identifier 0x81 (PE) or 0x82 (PA). The 16-bit form shown above is intra-area and only works for M, L, and a few other intra-area operands.

Does this technique port directly to the S7-1500 in TIA Portal?

Only in concept. The S7-1500 replaces the address-register form with tag-based indirect addressing and the PEEK / POKE / Variant instructions. See the Siemens TIA Portal V20 manual on Indirect addressing in STL (S7-1500) and Support entry 109011420 for the exact syntax in your firmware version.

Back to blog