S7-1200 Indirect Addressing: PEEK/POKE, Arrays, and DB_ANY

David Krause12 min read
S7-1200SiemensTutorial / 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 on the S7-1200 Differs from S7-300/400

Indirect addressing on the SIMATIC S7-1200 is fundamentally different from the legacy S7-300/400 pointer mechanics. The S7-1200 CPU family (firmware V4.0 and later, and the entire S7-1500 range) ships with optimized block access enabled by default, and absolute memory pointers (P#, DBW[AR1,P#0.0]) are not accepted in the editor the way they were in STEP 7 V5.x. To get a runtime-calculated operand address you have to choose between three production-ready techniques:

  1. PEEK / POKE for byte-, word-, dword-, and real-level I/O or DB access with separate area, DB number, byte offset, and bit inputs.
  2. UDT arrays where the index is the runtime variable and the data block is effectively the array itself.
  3. DB_ANY + absolute offset to pass the DB number into an FB/FC and dereference the resulting operand symbolically.

Each method has a specific sweet spot. Use the Siemens TIA Portal V20 documentation entry "Basics of indirect addressing (S7-1200, S7-1500) - STEP 7" as the canonical reference, and the companion article "Indirect addressing of a data block via DB_ANY data type (S7-1200, S7-1500)" for the DB_ANY pattern. A related SIMATIC WinCC entry on indirect addressing is available at Siemens Industry Online Support ID 109747174.

Prerequisites and Compatibility

Item Requirement Notes
CPU S7-1200 firmware V4.0 or later PEEK/POKE is available from V4.0 onwards. S7-1500 supports it natively from V1.0.
Engineering tool TIA Portal V13 SP1 or later (V18/V19/V20 recommended) DB_ANY input parameter requires V14 SP1 minimum.
Block attribute "Optimized block access" toggle Must be OFF for PEEK/POKE. Keep it ON for DB_ANY / array access on S7-1200 firmware V4.2+.
Knowledge STEP 7 ladder, FBD, or SCL basics SCL syntax is required for variant pointers.
DB DB_ANY-compatible global DB or instance DB Use global DBs for recipe data; instance DBs for FB-internal parameter sets.
Important: Disabling "Optimized block access" is not a free choice. It changes the byte layout of the DB and forces all tags to be addressed by absolute offset, which breaks symbolic HMI tags, integrated PLC data types, and download-without-reinitialization. Plan a single block as "non-optimized for PEEK/POKE" and keep the rest optimized.

Step 1: Confirm Why the Original Syntax Fails

The most common first attempt is a copy of a STEP 7 V5.x pattern such as:

// This will NOT compile in TIA Portal for S7-1200
L     P#DBX 0.0
LAR1
A     DBX[AR1,P#0.0]

The S7-1200 rejects the P# pointer constant and the area-cross accumulator/address-register indirection that the S7-300 used. Even the simpler variant that worked in SCL for S7-300:

// Non-portable S7-300 SCL pattern
MyTag := "MyDB".MyField[i];   // works
"MyDB".MyField[i] := Value;    // works ONLY if MyField is an array element

will compile on the S7-1200 only when the indexed tag is part of an ARRAY declaration. The PLC does not accept indexing into a UDT instance field by integer variable, nor into a non-arrayed tag list. That is the source of the "syntax won't work" message you will see in the program editor.

Step 2: Choose the Right Indirect Method

Method Data type coverage Block access DB number dynamic Typical use case
PEEK / POKE BYTE, WORD, DWORD, REAL, BOOL (POKE_BOOL with bit) Must be NON-optimized Yes (input) Reading recipe data from a numbered DB selected by HMI
UDT array Any UDT field Optimized or non-optimized Implicit (index = instance) Per-station / per-motor / per-axis parameters
DB_ANY + offset Any tag (symbolic) Optimized (recommended) Yes (DB_ANY carries it) Generic FB that operates on a recipe DB chosen at runtime
VARIANT pointer Any tag with runtime variable Optimized or non-optimized Yes (via Variant) Block library that accepts any parameter set

Step 3: Method 1 - PEEK / POKE (Non-Optimized DB)

PEEK and POKE are the direct replacement for the S7-300 pointer pair on the S7-1200. They are standard library instructions in TIA Portal: open the Instructions task card, expand Basic Instructions > Extended Instructions > Addressing, or call them from the libraries: PEEK_BOOL, PEEK_BYTE, PEEK_WORD, PEEK_DWORD, PEEK_REAL and the matching POKE_* set.

3.1 FB / FC interface

FUNCTION_BLOCK FB_RecipeAccess
VAR_INPUT
    iDBNumber   : INT;     // 1..65535 - target DB number
    iByteOffset : DINT;    // byte offset, positive only on S7-1200
    iBit        : INT;     // 0..7 for PEEK/POKE_BOOL only
    bEnable     : BOOL;
END_VAR
VAR_OUTPUT
    wValue      : WORD;
    bError      : BOOL;
    wStatus     : WORD;    // 0x0000 = OK, see Status table below
END_VAR
VAR
    _PEEK       : PEEK_WORD;  // multi-instance
END_VAR

3.2 PEEK_WORD body

IF bEnable THEN
    _PEEK(area     := 16#84,           // 0x84 = DB area
          dbNumber := iDBNumber,
          byteOffset := iByteOffset,
          value    => wValue,
          error    => bError,
          status   => wStatus);
END_IF;

3.3 PEEK / POKE area codes

Area constant (hex) Memory area Comment
16#80 Process image input (I) Inputs in PIB/PIW/PID/PI_BOOL
16#81 Process image output (Q) Outputs in PQB/PQW/PQD
16#82 Bit memory (M) MB/MW/MD/M_BOOL
16#83 Absolute I/O direct (P) Peripheral accesses, not image
16#84 Data block (DB) The one you need for indirect DB number

3.4 Status / error code mapping

Status (hex) Meaning Recommended action
0x0000 No error Continue
0x80C0 DB does not exist Check DB number from HMI, validate range 1..65535
0x80C1 Area identifier invalid Use 0x80..0x84 only
0x80C3 Offset is negative or out of range Clamp iByteOffset to 0..DB-size - sizeof(target)
0x80C4 Alignment error (word/dword on odd byte) Round offsets down to 2 (WORD) or 4 (DWORD/REAL)
0x80C5 Bit offset > 7 for PEEK_BOOL Mask iBit := iBit AND 7
0x80C6 Optimized block access conflict Disable "Optimized block access" on the target DB
Important: PEEK/POKE reads/writes absolute bytes. A DWORD at offset 6 in a non-optimized DB will not be at byte 6 if the DB also contains BOOLs - they are byte-packed in the order of declaration. Always verify offsets in the DB's "Offline > Data view" address column before commissioning.

Step 4: Method 2 - UDT Array (Recommended for Per-Instance Parameter Sets)

If you control the data layout, the cleanest solution is to declare a UDT and instantiate it as an array inside a global DB. The array index replaces the "DB number" entirely.

4.1 Define the UDT

TYPE UDT_MotorData :
STRUCT
    iRatedCurrent_mA : INT;       // in mA to avoid REAL scaling issues
    rSetSpeed_pct    : REAL;
    sDescription     : STRING[32];
    bEnabled         : BOOL;
    bAlarmActive     : BOOL;
END_STRUCT
END_TYPE

4.2 Declare the array DB

DATA_BLOCK DB_MotorPool
STRUCT
    aMotors : ARRAY[1..32] OF UDT_MotorData;
END_STRUCT
END_DATA_BLOCK

Inside any FB, SCL, or STL block you can now do:

// SCL - clean, fully symbolic
IF bStartMotor THEN
    "DB_MotorPool".aMotors[iMotorIndex].bEnabled := TRUE;
    "DB_MotorPool".aMotors[iMotorIndex].rSetSpeed_pct := 50.0;
END_IF;

This works in optimized and non-optimized blocks, survives HMI symbol re-export, and supports array-of-array nesting. The limitation is that iMotorIndex is an integer index, not a "DB number" - you cannot load an arbitrary, user-defined data block at runtime through this mechanism.

Step 5: Method 3 - DB_ANY + Absolute Offset

When the application truly needs "the DB number is given by the HMI", and you want to keep optimized block access, use a DB_ANY input. DB_ANY is a special data type introduced in TIA V14 SP1 that wraps a DB reference into a regular in-out variable.

5.1 FB interface

FUNCTION_BLOCK FB_DynamicRecipe
VAR_INPUT
    tRecipeDB  : DB_ANY;     // accepts any DB reference at call site
    iOffset    : DINT;       // byte offset within the recipe DB
END_VAR
VAR_OUTPUT
    rValue     : REAL;
    bOK        : BOOL;
END_VAR
VAR_TEMP
    sInfo      : DB_ANY_INFO;   // diagnostic structure
END_VAR

5.2 Body in SCL

// Read symbolic field "RecipeValue" at iOffset of the runtime DB
rValue := "DB_ANY_TO_REAL"(recipe := tRecipeDB, // pre-built helper FB
                           offset := iOffset);
bOK := NOT _isInvalid(tRecipeDB);

The actual recipe lookup is done via a small helper FB that combines DB_ANY_TO_VARIANT and a symbol resolution against the UDT/STRUCT inside the recipe DB. See the official Siemens entry "Indirect addressing of a data block via DB_ANY data type (S7-1200, S7-1500)" for the complete pattern. Quoting the documentation:

"In order to access the internal tags of the data block, use the name of the block parameter of data type DB_ANY and the absolute address of the tag, separated by a dot."

In practice the in-FB notation looks like:

// Inside FB_DynamicRecipe, accessing a WORD named "SetTemperature" at offset 12
wSet := WORD_TO_INT(%DB(tRecipeDB).DBW12);
Important: The %DB slice used inside the FB still resolves to a fixed offset; you do not get symbolic indirection. The DB is dynamic, but the byte offset of the field is still declared statically. The DB_ANY pattern is for "I want the FB to operate on whichever DB the caller chose", not "I want to compute the byte offset at runtime" - that latter case still needs PEEK/POKE.

Step 6: Method 4 - VARIANT and AT View

For the most flexible pattern, use a VARIANT input and a structured AT view to map a generic byte stream to your UDT. This is the closest modern equivalent of the S7-300 ANY-pointer.

// SCL with AT view over a VARIANT
FUNCTION_BLOCK FB_Parser
VAR_INPUT
    vAnyDB  : VARIANT;   // caller passes a slice of the recipe DB
END_VAR
VAR_TEMP
    tBytes  : ARRAY[0..255] OF BYTE;
    tRecipe : UDT_Recipe AT tBytes;   // overlay
END_VAR

The caller then provides a VARIANT pointer to the relevant slice of the data block, and the AT view reinterprets those bytes as a UDT. The advantage is zero-offset math: the engineer writes tRecipe.rSetpoint symbolically. The disadvantage is byte-alignment: STRUCT padding follows S7-1200 UDT packing rules, so two adjacent UDTs of different sizes can have hidden gaps.

Step 7: PEEK/POKE Worked Example - Read a Real from DB200 at Offset 20

Goal: an HMI index DB_No selects one of 30 recipe DBs (DB 200..229) and the program reads a REAL at byte offset 20 inside that DB. All DBs share the same UDT layout.

  1. Create a UDT UDT_Recipe with a REAL field at offset 20 (declare it after 20 bytes of preceding data, e.g. WORD at 18 and a BYTE at 19).
  2. Create the 30 DBs (DB 200..DB 229) and set "Optimized block access" to off. Bulk-fill via "Create and initialize all DBs" in TIA Portal.
  3. In an FB, declare iDBNumber: INT, iOffset: DINT := 20, rValue: REAL.
  4. Drop a PEEK_REAL multi-instance into the FB static area.
  5. Wire the inputs in SCL:
// Called once per scan; wrap with EN/ENO if you want to cascade to other FBs
_PEEK_REAL(area       := 16#84,
           dbNumber   := iDBNumber,
           byteOffset := iOffset,
           value      => rValue,
           error      => bPeekErr,
           status     => wPeekStatus);
  1. Bind iDBNumber to the HMI tag DB_No.

Step 8: Array Worked Example - 32 Stations in a Single DB

// Global DB: DB_Station
DATA_BLOCK DB_Station
STRUCT
    aStation : ARRAY[1..32] OF UDT_Station;
END_STRUCT
END_DATA_BLOCK

// UDT_Station
TYPE UDT_Station :
STRUCT
    bRun         : BOOL;
    bFault       : BOOL;
    iCycleCount  : INT;
    rPressure    : REAL;
    sOperator    : STRING[16];
END_STRUCT
END_TYPE

Access from any FB:

IF bStartCycle[iStation] THEN
    "DB_Station".aStation[iStation].bRun := TRUE;
    "DB_Station".aStation[iStation].iCycleCount +=
        "DB_Station".aStation[iStation].iCycleCount + 1;
END_IF;

Step 9: Verification Checklist

Check Method Pass criterion
Compile clean Project > Compile > All blocks No errors, no warnings about pointer/ANY/DB_ANY
DB attribute Right-click DB > Properties > Attributes "Optimized block access" = OFF only for PEEK/POKE DBs
Download and start Online > Download to device CPU goes to RUN, no SF/BF diagnostic
Watch table Online > Watch and force Read iDBNumber=205, iByteOffset=20; rValue updates to expected REAL
Boundary test Force iDBNumber=0 and iDBNumber=9999 Status returns 0x80C0, bError=TRUE, no PLC stop
Boundary test 2 Force iByteOffset=-1 and large positive Status returns 0x80C3, no PLC stop
Bit test POKE_BOOL with iBit=7 then iBit=8 Bit 7 succeeds, bit 8 returns 0x80C5
Symbolic consistency Edit a tag name inside the recipe DB If optimized, PEEK/POKE call still compiles (offset unchanged); if not optimized, address column renumbers and PEEK offsets must be revalidated

Step 10: Troubleshooting Matrix

Symptom Likely cause Fix
"Invalid pointer" at compile time S7-300 syntax with P# or AR1 Switch to PEEK/POKE or array index
"The tag cannot be indexed" at compile time Indexed tag is not declared as an ARRAY element Reorganize data into an ARRAY of UDT or use DB_ANY
0x80C6 status at runtime DB is optimized Open DB > Properties > Attributes > untick "Optimized block access"
Wrong value read at runtime Offset misaligned (WORD on odd byte) Round offsets to 2 (WORD) / 4 (DWORD, REAL) and verify in DB address column
Watch shows initial value only Caller passes DB number 0 (system DB) Clamp iDBNumber to 1..65535 in the FB
"Type mismatch" on DB_ANY input Caller passed a non-DB variable Caller must use a fully qualified DB literal (e.g. "DB_Recipe_01") or a DB_ANY output from a higher-level block
VARIANT read returns garbage Caller passed the wrong slice size Re-check slice byte length; AT view must cover entire UDT

Step 11: Performance and Safety Notes

PEEK/POKE has measurable runtime cost. On a S7-1215C the execution time of one PEEK_REAL is roughly 1.5 µs; on a S7-1516 it is below 0.5 µs. In a 1 ms OB1 scan budget, hundreds of indirect accesses are still safe, but indirect calls inside fast I/O OBs (e.g. OB 91 of an S7-1500 motion task) should be replaced by direct symbolic access whenever the index is constant at compile time.

Security: the iDBNumber input is user-controlled if it comes from the HMI. Always clamp it. Unbounded DB numbers can also be used to scan the address space of the CPU and, on firmwares that allow DB download to a running CPU, to write to unexpected DBs. Treat any indirect write (POKE) as safety-relevant and gate it behind a privilege flag, the standard "user role" concept from SIMATIC S7-1200 Programmable Controller - System Manual, or a hardwired enable.

FAQ

Why does my S7-1200 reject P# and AR1 in STEP 7?

S7-1200 and S7-1500 CPUs run optimized bytecode and do not expose the area-cross pointer registers that S7-300 used. Use PEEK/POKE, ARRAY indices, or DB_ANY instead. The official guide is at Siemens TIA Portal V20 indirect addressing.

Do I have to disable "Optimized block access" for indirect addressing?

Only for PEEK/POKE. The UDT array pattern and the DB_ANY pattern both work with optimized block access enabled, which is the recommended setting on firmware V4.2 and later. Disabling optimization should be done per-DB and only for the DBs that truly need absolute PEEK/POKE offsets.

What status code means "DB does not exist" on PEEK?

0x80C0 is returned in STATUS when the supplied DB number is not present in the CPU. Validate the HMI input in the range 1..65535 and respond with a controlled error path instead of letting the OB report a programming error.

Can I pass a DB number as a function input on the S7-1200?

Use the DB_ANY data type introduced in TIA Portal V14 SP1. The caller passes a fully qualified DB literal and the FB reads it symbolically with %DB(tRecipeDB).DBW<offset>. Reference: Indirect addressing of a data block via DB_ANY.

How do I read a single bit inside an arbitrary DB with PEEK_BOOL?

Use PEEK_BOOL with area=16#84, dbNumber for the DB, byteOffset for the byte containing the bit, and bitOffset in 0..7. A value >7 returns status 0x80C5. For structured bit access in optimized blocks, use a UDT array and a boolean field by symbolic name.

Back to blog