Indirect Addressing: Pointing DB Values to M-Bits in STEP 7

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

1. Problem Overview and Engineering Context

In SIMATIC Manager (STEP 7 Classic, V5.5/V5.6) on a SIMATIC S7-300 or S7-400 controller, programmers often need to drive a Merker (flag) bit — for example M200.0 — whose address is stored numerically inside a data block. The typical use case is an HMI recipe that writes a single integer into DB1.DBW70 and the PLC logic is expected to translate that integer into the corresponding bit or byte address in the M-memory area and execute a Set/Reset on that location.

The naive expectation that writing 1600 as a decimal value into a DB INT will automatically point to M200.0 is incorrect, because the CPU never interprets a plain INT as a memory address. The address must be packed into a proper S7 pointer (POINTER, ANY, or area-crossing DWORD) before it can be used with an indirect statement such as S M [AR1, P#0.0] or U M [AR1, P#0.0].

This article demonstrates the correct procedure using Programming with STEP 7 as the primary reference, the system manual S7-300 CPU 31xC and CPU 31x: Technical Specifications, and the standard reference System Software for S7-300/400 – System and Standard Functions for the pointer format definitions.

2. Prerequisites

  1. SIMATIC STEP 7 V5.5 or V5.6 (also valid in older V5.4 projects) installed on Windows 7/10 with administrator rights.
  2. A configured S7-300 station (CPU 31x) or S7-400 station (CPU 41x) with at least 512 bytes of work memory free; the OB1 cycle time must remain below 100 ms with the new logic included.
  3. Knowledge of STL or LAD/FBD editing; the indirect statements used in this article are only available in STL. Convert blocks via View → STL if needed.
  4. An HMI or programmer connection to write test values into DB1.DBW70 using Monitor/Modify (Ctrl+F7) or Variable Table (VAT).
  5. Library blocks FC36 "BIT_BF" / FC37 "BIT_BL" and SFC20 "BLKMOV" are recommended if the project must remain portable to S7-1200/S7-1500 later (see Bool, Byte, Word, and DWord data types for cross-platform reference).

3. S7 Memory Areas and Their Identifiers

Before constructing a pointer, identify which memory area must be addressed. The S7-300/400 reserves distinct address areas, each with an 8-bit identifier used in the cross-area pointer (Section 4). The numeric ID is part of the 32-bit pointer value and is required whenever the destination area differs from the source area.

Area ID (hex) Area Symbol English / German name Typical access
0x81 I / E Process image of the inputs Bit, byte, word, dword
0x82 Q / A Process image of the outputs Bit, byte, word, dword
0x83 M / M Merker (flags) Bit, byte, word, dword
0x84 DB Global data blocks Bit, byte, word, dword
0x85 DI Instance data blocks Bit, byte, word, dword
0x86 L / L Local (temporary) data Bit, byte, word, dword
0x80 PIPE / PIB/PQB/... Periphery (direct I/O) Byte, word, dword only
Note: The value 1600 stored in the source DB cannot be used as a pointer directly. 1600 in decimal equals 0x0640, which contains the wrong area ID and bit-field structure. Section 4 explains the correct bit layout.

4. Pointer Formats in STEP 7

STEP 7 uses three pointer widths: the 32-bit area-internal pointer (P#Byte.Bit), the 48-bit POINTER data type (DB number + 32-bit pointer), and the 80-bit ANY data type (repetition factor, data type, length, DB number, area-internal pointer). For the M-bit application, the 32-bit area-crossing DWORD is the smallest structure that still carries the area identifier.

Byte offset Bits 31..24 Bits 23..16 Bits 15..8 Bits 7..3 Bits 2..0
Function Area ID (0x83 for M) Byte number high Byte number low Reserved (0) Bit number 0..7
M200.0 0x83 0x00 0xC8 0x00 0x00
M200.7 0x83 0x00 0xC8 0x00 0x07
M5.3 0x83 0x00 0x05 0x00 0x03

The 32-bit value is therefore 0x8300C800 for P#M200.0. The bit number is encoded in the lower three bits; the byte number is encoded in the next 13 bits. This layout is documented in the System and Standard Functions reference manual linked above and in the Beijer Electronics S7 ISO-over-TCP/IP driver manual which re-prints the same table for driver integrators.

Critical: Byte numbering in the cross-area pointer is always the absolute byte address, not the bit address 1600. To translate 1600 (which is a flat bit index in the M area) into the correct pointer: byte = 1600 / 8 = 200, bit = 1600 mod 8 = 0, resulting in P#M200.0 = 16#8300_C800.

5. Solution Architecture

Two practical solutions exist:

  1. Solution A (recommended): Store the complete 32-bit cross-area pointer in a DB DWORD, then use it directly with LAR1 and a memory-indirect statement.
  2. Solution B (legacy): Store a 16-bit "flat bit index" (e.g., 1600) in a DB INT, and let an FC compute the byte/bit split and the area ID at runtime.

Solution A is faster (no arithmetic in OB1) and easier to commission because the pointer can be observed directly with Monitor. Solution B is friendlier for HMI operators who only know decimal bit positions. Both are implemented below in STL and exported to LAD for documentation.

6. Solution A — Store the Full 32-bit Pointer in the DB

6.1 DB definition

DATA_BLOCK DB1
TITLE = Pointer Table for M-Bits
VERSION : 0.1
  STRUCT
    i_MBit_Pointer : DWORD;     // 16#8300_C800  = P#M200.0
    i_MBit_Command : BOOL;      // 1 = Set, 0 = Reset
    b_Result_OK    : BOOL;      // 1 = Access successful
    w_Diagnostic   : WORD;      // 16#0001 = OK, 16#8001 = Area out of range
  END_STRUCT;
END_DATA_BLOCK

6.2 STL code in OB1 (Solution A)

// OB1 — Network 1: Set/Reset M-bit using pointer stored in DB1
      L     DB1.DBD    0          // Load 32-bit pointer P#Mx.y (area ID 0x83)
      LAR1                          // Transfer to Address Register 1
      L     DB1.DBX    4.0         // Load command bit (1 = set)
      U     M     [AR1, P#0.0]     // Read current state (RLO unchanged)
      SPB   _SET                    // If 0 and command=1, jump to SET
      U     DB1.DBX    4.0         // Reload command bit
      SPB   _RES                    // If 1 and command=0, jump to RESET
      BEA                           // Otherwise leave
_SET: S     M     [AR1, P#0.0]     // Set M-bit pointed to by AR1
      SET
      S     DB1.DBX    5.0         // Set result bit
      BEA
_RES: R     M     [AR1, P#0.0]     // Reset M-bit pointed to by AR1
      SET
      S     DB1.DBX    5.0
      BEA
Note: The syntax M [AR1, P#0.0] is the memory-indirect, area-internal access. Because the area ID is already in the lower 32 bits of AR1 (bits 24-31 = 0x83), STEP 7 automatically selects the M area. The P#0.0 offset is added to AR1 for byte/bit displacement; it must remain zero for this application to keep the pointer exact.

6.3 Converting the HMI integer to the pointer value

Configure the HMI tag for DB1.DBD0 as a 32-bit unsigned value. The operator enters the value 16#8300_C800 in the HMI input field; STEP 7 monitors DB1.DBD0 in hex. For a quick lookup table that the HMI can use as a selection list, see Section 8.

7. Solution B — FC Computes the Pointer from a Bit Index

7.1 FC interface

FUNCTION FC100 : VOID
TITLE = Compute M-bit pointer from flat bit index
VERSION : 1.0
VAR_INPUT
    i_BitIndex   : INT;     // 0..8191 for CPU 31x (1 KiB M area)
END_VAR
VAR_OUTPUT
    o_Ready      : BOOL;
    o_Error      : BOOL;
END_VAR
VAR_TEMP
    dw_Pointer   : DWORD;   // Holds P#Mx.y with area ID 0x83
    w_Byte       : WORD;
    b_Bit        : BYTE;
END_VAR
BEGIN
NETWORK 1  // Range check
      L     #i_BitIndex
      L     0
      <I                              // Negative?
      SPB   _ERR
      L     #i_BitIndex
      L     8191
      >I                              // Larger than last M bit?
      SPB   _ERR
NETWORK 2  // Byte = index / 8, Bit = index mod 8
      L     #i_BitIndex
      SRD   3                          // Arithmetic shift right 3 bits = divide by 8
      T     #w_Byte
      L     #i_BitIndex
      SRD   3
      SLW   3                          // Multiply byte by 8
      TAK                              // Swap ACCU1 and ACCU2
      -I                                // Remainder = index - 8*byte
      T     #b_Bit
NETWORK 3  // Build DWORD: 0x83 0x00 byte bit
      L     #w_Byte
      SLD   8                           // Shift byte address into bits 8..15
      OW                                // (ACCU1 already zero in high half)
      L     #b_Bit
      OW                                // OR bit number into bits 0..2
      OD                                // OR with area ID placeholder
      L     16#83000000                // Area ID for Merker
      OD
      T     #dw_Pointer
      T     DB1.DBD    0                // Publish to DB
      SET
      =     #o_Ready
      R     #o_Error
      BEU
_ERR: CLR
      =     #o_Ready
      S     #o_Error
END_FUNCTION

7.2 OB1 call

// OB1 — call FC100 once per cycle
      CALL FC100
        i_BitIndex  := DB1.DBW    70     // From HMI, e.g., 1600
        o_Ready     := DB1.DBX    5.0
        o_Error     := DB1.DBX    5.1
// Then continue with Network 1 from Solution A (use DB1.DBD0 as pointer)

8. Reference Table: Bit Index to M-Bit

Engineers commissioning the HMI often need a quick lookup. The 1 KiB M-area of an S7-300 CPU 31x covers bit indices 0..8191. The values below are most frequently used.

HMI integer (DBW70) Byte / Bit Real M-bit Pointer DWORD (hex)
0 0.0 M0.0 16#8300_0000
1 0.1 M0.1 16#8300_0001
8 1.0 M1.0 16#8300_0100
64 8.0 M8.0 16#8300_0800
1600 200.0 M200.0 16#8300_C800
1601 200.1 M200.1 16#8300_C801
1607 200.7 M200.7 16#8300_C807
8000 1000.0 M1000.0 16#8303_E800
8191 1023.7 M1023.7 16#8303_FFFF

9. Ladder Logic (LAD) Equivalent

STEP 7 does not allow direct memory-indirect bit access in the LAD editor. To stay in LAD while keeping the indirection, wrap the STL inside a custom FC and call the FC from a LAD network. The body of the FC must be written in STL (View → STL). The call site in LAD looks like this:

      |  CALL FC100                         |
      |     i_BitIndex := DB1.DBW70         |
      |     o_Ready    := DB1.DBX5.0        |
      |     o_Error    := DB1.DBX5.1        |
      |                                   |
      |     U   DB1.DBX5.0                 |
      |     SPB  NEXT                      |
      |     S   M [AR1, P#0.0]             |  // inside FC101
      |NEXT:BE

10. Verification and Commissioning Procedure

  1. Download the project (Ctrl+D) to the CPU and switch to RUN. The CPU diagnostic buffer must show No errors; if SF lights up, the pointer value is outside the M area (most often 0x84xx was entered instead of 0x83xx).
  2. Open a VAT (PLC → Monitor/Modify Variable) and add DB1.DBD0, DB1.DBW70, M200.0, and DB1.DBX5.0.
  3. Force DB1.DBD0 = 16#8300_C800 and DB1.DBX4.0 = 1. Verify that M200.0 transitions to TRUE within one OB1 cycle (max 100 ms).
  4. Switch DB1.DBX4.0 to 0. M200.0 must return to FALSE.
  5. Test the boundary case: set DB1.DBD0 = 16#8303_FFFF and confirm M1023.7 toggles. If the CPU raises a range-check OB121, the area ID or byte offset is invalid — check that bits 19..23 are zero.
  6. Restore DB1.DBD0 to the HMI-driven value before going online with the operator panel.

11. Diagnostic and Troubleshooting Matrix

Symptom Likely cause Verification Corrective action
CPU goes to STOP, SF lit, diagnostic buffer: Area length error writing M Byte number in pointer exceeds the configured M-area size Check HW Config → CPU → Properties → Merker. Default is 1 KiB (0..1023) Reduce bit index, or expand the M area in HW Config and download a new hardware configuration
Bit toggles one cycle later than expected LAR1 called after the indirect statement Cross-reference in the STL editor Move LAR1 immediately before the S/R instruction
Bit never changes despite correct pointer AR1 is overwritten by a called FB Use Cross Reference → Address Register Use TAR1 / LAR1 save/restore around the FB call, or use AR2
Operator writes 1600 but nothing happens HMI tag is bound to DBW70 (Solution B), not DBD0 (Solution A) Inspect tag list in WinCC flexible / TIA Portal Bind the HMI tag to the correct address, or add the FC100 call
CPU raises OB121 (programming error) Pointer area ID is 0 (uninitialized DB DWORD) Monitor DB1.DBD0; initial value in DB must be 16#83000000 for M0.0 Set an initial value in the DB declaration or initialize with L 16#83000000; T DB1.DBD0 in OB100
Bit toggles in the wrong area (I or Q) Area ID byte (bits 24-31) is 0x81 or 0x82 instead of 0x83 Display DB1.DBD0 in hex; verify high byte Re-load pointer with area ID 0x83 (M) or use the lookup table in Section 8

12. Portability Notes: S7-300/400 vs. S7-1200/1500

The same 32-bit pointer layout is used in TIA Portal. However, the S7-1200/1500 prefers the POINTER data type with explicit DB number handling and the new VARIANT data type (see TIA Portal data types). The legacy AR1/AR2 registers are retained for compatibility; new code should consider PEEK/POKE instructions from the Extended Instructions palette as a more readable alternative for byte/word-level indirection. For bit-level indirection across areas, the classic L P#Mx.y; LAR1; S M[AR1, P#0.0] pattern remains the shortest expression and is fully supported through V17 of TIA Portal.

Safety advisory: Indirect memory writes targeting the M area can corrupt communication, status, or clock-memory bits reserved by the CPU firmware (for example M0.0 = "First cycle" retained on S7-300; the clock memory byte is also assigned to M-area). Add a range-check in the FC (Section 7.1) that rejects pointers targeting bits 0..7 of byte 0 or the configured clock-memory byte. This range-check is mandatory in projects that may be downloaded to a different CPU model in the family.

13. Performance and Cycle Time Impact

The pointer load and indirect access require roughly 12 μs on a CPU 315-2 PN/DP and 4 μs on a CPU 416-3 PN/DP. Adding the FC100 math (Solution B) increases the time by an additional 18 μs on a CPU 315 and 6 μs on a CPU 416. Neither impact is significant for OB1 cycle times of 20..100 ms, but in a high-priority OB35 (< 2 ms) the indirect statement should be removed from the cyclic class and executed from OB1 only.

14. Summary of Field-Proven Practices

  1. Always declare the pointer DWORD with the correct initial value; an uninitialized DWORD means area ID = 0, which causes OB121.
  2. Range-check the byte component in the FC; never trust an HMI integer blindly.
  3. Document the pointer layout in the DB header comment so that commissioning engineers do not have to decode 0x8300C800 manually.
  4. For S7-300, protect bits M0.0..M0.7 (startup / clock-mem overlap risk) and any retained bits the CPU uses internally.
  5. When migrating to S7-1200/1500, replace the cross-area DWORD with the formal POINTER or VARIANT type and rewrite the indirect statement as PEEK_BOOL(area := M, byteOffset := ..., bitOffset := ...).

What is the difference between area-internal pointer P#200.0 and cross-area pointer 16#8300C800?

Area-internal pointer P#200.0 is a 32-bit value without an area identifier and only works when the memory-indirect statement already specifies the area (for example S M [AR1, P#0.0]). The cross-area DWORD 16#8300C800 embeds the area ID 0x83 (M) in the upper byte, so it can be loaded into AR1 and used in any statement regardless of the operand prefix.

Why does writing the integer 1600 to a DB word not point to M200.0?

STEP 7 never interprets a plain INT as an address. The integer 1600 is just a numeric value 0x0640 with no area ID and no bit-field encoding. The DB DWORD must be loaded with the encoded 32-bit pointer 16#8300C800, or an FC must compute the byte/bit split from 1600 before the address is loaded into AR1.

Can I use memory-indirect bit access in LAD and FBD?

No. The syntax M [AR1, P#0.0] is only available in STL and SCL. In LAD/FBD, create a custom FC written in STL and call it from a LAD network. The LAD call site shows the input and output wiring, while the body of the FC performs the indirection.

Which AR register should I use for the pointer in a multi-instance FB?

Use AR1 for the local pointer inside a non-multi-instance FC. Inside a multi-instance FB, save and restore AR1 around the call with TAR1 / LAR1 because the calling FB chain may already use AR1. Alternatively, use AR2 which is reserved for FB-instance handling but can be repurposed inside a dedicated FC if no FB is in the call stack.

What happens if the M area is too small for the pointer byte offset?

The CPU raises an area-length error and calls OB121 (programming error). If OB121 is not loaded, the CPU goes to STOP. In the VAT, the diagnostic buffer shows entry Area length error when writing. The fix is either to reduce the bit index in the HMI or to expand the Merker area in HW Config to a larger value such as 4 KiB and download the new hardware configuration.

Back to blog