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:
- PEEK / POKE for byte-, word-, dword-, and real-level I/O or DB access with separate area, DB number, byte offset, and bit inputs.
- UDT arrays where the index is the runtime variable and the data block is effectively the array itself.
- 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. |
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 |
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);
%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.
- Create a UDT
UDT_Recipewith a REAL field at offset 20 (declare it after 20 bytes of preceding data, e.g.WORDat 18 and aBYTEat 19). - 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.
- In an FB, declare
iDBNumber: INT,iOffset: DINT := 20,rValue: REAL. - Drop a
PEEK_REALmulti-instance into the FB static area. - 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);
- Bind
iDBNumberto the HMI tagDB_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.