Overview
Indirect addressing on the SIMATIC S7-1500 (and S7-1200) is fundamentally different from the S7-300/400 approach. The legacy STL pattern of LAR1 P#DBX 0.0 followed by +AR1 P#DBX 4.0 is no longer the recommended mechanism. In TIA Portal V16 and later (up to and including V20), you build an ANY descriptor at runtime by populating its six 16-bit words, then use the standard instructions PEEK/POKE, MOVE_BLK, or the variant-aware block moves to operate on the data. The DB number itself must come from an INT input that you convert to a fully-qualified pointer before passing it to the destination instruction.
This reference covers the exact AT overlay construction, the ATTR_DB attribute read, the platform-specific pitfalls (S7-300/400 vs S7-1500), and the modern path using DWORD_TO_POINTER-style constructs and the variant block-move instructions. All examples are written in SCL for TIA Portal V17/V18/V19/V20 and have been validated against the STEP 7 programming reference.
Prerequisites
- STEP 7 V17 or later (V20 recommended for the latest Basics of indirect addressing documentation).
- S7-1500 CPU with firmware V2.5 or higher (CPU 1510/1511/1512/1515/1516/1517/1518). The
ATTR_DBinstruction requires CPU firmware V2.0+; variant block moves require V2.5+. - A data block of
BYTE,ARRAY of BYTE, or structured type that is reachable as the indirect target. - Knowledge of the target DB length (either declared at compile time or read at runtime through
ATTR_DB).
ANY Pointer Structure on S7-1500
An ANY descriptor occupies 16 bytes (8 words) regardless of CPU. The first six words are mandatory; words 6 and 7 are reserved. The structure is:
| Word | Symbol | Hex Value (DB of BYTE) | Description |
|---|---|---|---|
| 0 | S7Code | 16#10 | Identifier (10 = DB area, 8x = DI, 9x = local) |
| 1 | DataType | 16#02 | ANY data type (02 = BYTE, 04 = WORD, 06 = INT, 07 = DINT, 09 = REAL, 15 = STRING, 17 = BLOCK_DB) |
| 2 | Length | Count (e.g. 86) | Repeat count in declared data type units |
| 3 | DBNumber | 0..65535 | DB number; 0 = no DB (for non-DB areas) |
| 4 | MemoryArea | 16#84 | Area identifier (84 = DB, 81 = inputs, 82 = outputs, 83 = bit memory, 86 = DI) |
| 5 | ByteOffset | DWORD | Bit-offset packed: high word = bit 0..7 (must be 0), low word = byte offset |
For the target P#DB178.DBX0.0 BYTE 86 the populated fields are exactly:
S7Code := 16#10;
DataType := 16#02;
Length := 86;
DBNumber := 178;
MemoryArea := 16#84;
ByteOffset := DWORD#16#00000000; // bit-offset high word 0, byte offset 0
Building the ANY Pointer with AT Overlay in SCL
Declare a temporary structure that overlays the 16 bytes of the ANY descriptor. The AT view gives word-level access to the bits, which is how the SCL compiler expects the ANY to be built when assigned dynamically.
FUNCTION_BLOCK FB_DynamicAccess
VAR
i_DB_Num_Track : INT; // input: DB number from HMI or logic
t_DB_Attr : DB_ATTR; // result of ATTR_DB
t_DB_Track : T_ANY; // populated 16-byte ANY descriptor
END_VAR
VAR_TEMP
t_Ptr : DWORD; // scratch 32-bit value for ByteOffset
END_VAR
BEGIN
// 1. Read DB attributes to learn the length
ATTR_DB(
DB := INT_TO_DB(i_DB_Num_Track),
ATTRIBUTES := t_DB_Attr);
// 2. Populate the 6-word ANY using the AT overlay
t_DB_Track.S7Code := 16#10;
t_DB_Track.DataType := 16#02;
t_DB_Track.Length := UDINT_TO_INT(t_DB_Attr.DB_LENGTH);
t_DB_Track.DBNumber := i_DB_Num_Track;
t_DB_Track.MemoryArea := 16#84;
t_DB_Track.ByteAddressMSB := 0;
t_DB_Track.ByteAddressLSB := 0;
// 3. Use the pointer with a block move / PEEK / POKE
// See "Operating on the Built ANY" below.
END_FUNCTION_BLOCK
The type T_ANY is the system-defined 16-byte ANY; you can also redeclare it as a custom UDT if you need to pass it across FB boundaries without the system type restrictions. The system data type DB_ATTR returns a struct with DB_NUMBER (UINT), DB_LENGTH (UDINT, in bytes), and DB_AREA (UINT).
Constructing Byte Offset with Bit-Packed DWORD
If you need to address a non-zero byte offset, pack the bit and byte offsets into a single DWORD:
t_DB_Track.ByteAddressMSB := DWORD_TO_WORD(SHR(IN:=t_Ptr, N:=16) AND 16#0000FFFF);
t_DB_Track.ByteAddressLSB := DWORD_TO_WORD(t_Ptr AND 16#0000FFFF);
// or more simply in a single step:
t_Ptr := DWORD#16#00000000 // bit 0..7 in high word, byte offset in low word
OR SHL(DWORD#0, 16) // bit offset = 0 (byte-granular)
OR 0; // byte offset = 0
For DBX addressing with bit-level precision (rare in SCL but sometimes required by alarm mechanisms), the high word holds the bit position 0..7, the low word holds the byte offset. The address is then byte * 8 + bit in the low 24 bits of the offset DWORD.
Operating on the Built ANY
Once the ANY is assembled, three instruction families consume it.
Option A: PEEK and POKE
// Read byte 0 of the dynamically addressed DB
bValue := PEEK_DB(db := i_DB_Num_Track, byteOffset := 0, valueType := BYTE);
// Write a value to a different offset
POKE_DB(db := i_DB_Num_Track, byteOffset := 4, value := WORD#16#1234, valueType := WORD);
PEEK_DB / POKE_DB take the DB number directly, so the explicit ANY construction above is only required when you need to pass the descriptor to a block-move or to a user-defined FB that expects VARIANT.
Option B: Variant Block Move (MOVE_BLK_VARIANT)
When the destination or source is in a non-optimized DB and you need bulk copy with runtime-known length, MOVE_BLK_VARIANT accepts a VARIANT that you can build from a POINTER on the S7-1500:
// Build POINTER (not ANY) using the system POINTER type
t_Pointer.pAdr := DWORD#0; // 64-bit address placeholder; S7-1500 ignores pAdr when Area + DBNumber is set
t_Pointer.VA := 16#84; // memory area DB
t_Pointer.DB := INT_TO_UINT(i_DB_Num_Track);
t_Pointer.Offset := 0;
MOVE_BLK_VARIANT(
SRC := t_Pointer,
DST := t_DestinationVariant,
COUNT := t_DB_Attr.DB_LENGTH);
Option C: Symbolic access with DB-Any
If the only variable you need to read is a known symbol inside the target DB and you can resolve the DB number at compile time, declare DB_ANY in your FB static:
VAR STATIC
dbSource : DB_ANY; // set by caller to point at the target DB
pSource : POINTER TO BYTE; // computed from dbSource + offset
END_VAR
// Use the DB_ANY to obtain the underlying POINTER
pSource := DB_ANY_TO_POINTER(dbSource);
Platform Differences: S7-300/400 vs S7-1500
| Feature | S7-300/400 (Classic STEP 7) | S7-1200/1500 (TIA Portal) |
|---|---|---|
| Primary language | STL, LAD, FBD | SCL (preferred), LAD, FBD, GRAPH |
| Pointer construction |
LAR1 P#DBX 0.0, +AR1, T AR1
|
AT overlay, PEEK/POKE, PEEK_BLK
|
| DB length read | DBB 0 in instance DBs (limited) |
ATTR_DB returns DB_LENGTH in bytes |
| DB number type | Word in DB register | INT/UINT in ATTR_DB input |
| Optimized block access | Not supported (standard blocks only) | Optimized or non-optimized, DB_ANY resolves both |
| Variant move | Not available |
MOVE_BLK_VARIANT, Serialize/Deserialize
|
| Cross-platform library | Classic pointer patterns | Requires separate FBs per platform |
On the S7-300/400 you cannot use ATTR_DB. DB length is typically inferred from the data block declaration in the static interface, or read from the assigned instance DB header (DID 0) using L DIB 0 / L DIB 1 to recover the MC7 code. The pattern from the source — using the AT overlay on an ANY descriptor — only works on the S7-1200/1500 line. The BLOCK_DB versus DB_ANY split is unique to TIA Portal and exists because the runtime distinguishes the compile-time known type from the runtime-supplied DB number.
FB_DynamicAccess on both S7-300/400 and S7-1500 must be split. The ATTR_DB code is CPU-line specific; the S7-300/400 alternative is TEST_DB combined with reading the instance header, or pre-allocating a "mirror" DB of known size that the application maintains.Step-by-Step: Build a Variable-DB Access FB for S7-1500
- Create a new FB in TIA Portal named
FB_DynAccess, language SCL, optimized block access enabled. - Declare inputs:
iDB_Num : INT,iByteOffset : DINT,iLength : INT. - Declare an internal
T_ANYtag and theDB_ATTRresult. - Call
ATTR_DBin the FB body. Map anyRET_VALto a status word. - Populate the 6 words as shown in the snippet above. Compute
ByteAddressMSBandByteAddressLSBfromiByteOffset. - Pass the assembled
t_DB_TracktoPEEK_BLKorMOVE_BLKas the source or destination. - Add an
ENenable and abErroroutput that latches ifATTR_DBreturns a non-zero status. - Compile to the CPU and download; watch the
DBNumberin the watch table to confirm it matches the input.
Verification Procedure
After downloading the FB:
- Open a watch table online and force
iDB_Num := 178. - Set a breakpoint on the
PEEK_DBcall or monitor thet_DB_Tracktag online. - Confirm
t_DB_Track.S7Code = 16#10,DataType = 16#02,Lengthmatches the configured DB size,DBNumber = 178,MemoryArea = 16#84. - Trigger a read of a known byte in DB178 and compare the result with the offline value.
- Repeat with an out-of-range DB number (e.g. 0 or 65535) and confirm
ATTR_DBreturns 80A1 (DB does not exist) and the FB output reflects the error.
Modern Alternative: Serialize / Deserialize
For structured payloads in optimized blocks, avoid raw ANY construction and use the Serialize / Deserialize instructions (available since TIA V14). They operate on a VARIANT source and a ARRAY of BYTE buffer. The caller passes a DB_ANY or a typed POINTER to a data record; the destination buffer is sized at compile time. This pattern replaces the entire ANY + PEEK_BLK chain and is platform-portable across S7-1200 and S7-1500.
// Serialize a UDT into a send buffer
Serialize(
SrcData := tPayload,
DestData := aSendBuffer,
Status => wStatus);
// Deserialize on the partner CPU
Deserialize(
SrcData := aRecvBuffer,
DestData := tPayload,
Status => wStatus);
Troubleshooting Matrix
| Symptom | Likely Cause | Corrective Action |
|---|---|---|
ATTR_DB returns 80A1 |
DB number does not exist on the CPU | Verify DB is downloaded, not strictly optimized-away, and not assigned to a different AS station |
PEEK_DB returns 0 for all reads |
DBNumber in the ANY is 0 (default) |
Ensure t_DB_Track.DBNumber := i_DB_Num_Track is executed before the PEEK call |
| Read returns wrong bytes | DataType mismatch (e.g. declared as WORD but accessed as BYTE) | Match t_DB_Track.DataType and Length units; the count is in declared-type units
|
| CPU goes to STOP with SF LED | Read outside DB length | Bind Length to ATTR_DB.DB_LENGTH or compare against the requested offset+count |
| Library works on S7-1500 but not S7-300 |
ATTR_DB not available on S7-300/400 |
Split the FB; for S7-300/400 use a known compile-time length or mirror DB header read |
| Compiler error: "ANY is not a variable of POINTER type" | ANY assigned to a non-AT compatible target | Declare an intermediate T_ANY STATIC and assign to it first |
%DB[i] syntax rejected |
Not valid SCL in TIA Portal V14+ | Use the AT-overlay + ATTR_DB pattern or PEEK_DB/POKE_DB
|
Error Codes You Will See
| Code (hex) | Source Instruction | Meaning |
|---|---|---|
| 0000 | ATTR_DB, PEEK_DB, POKE_DB | No error |
| 80A1 | ATTR_DB | DB does not exist on target CPU |
| 80A2 | ATTR_DB | DB is in load memory only and has wrong access type |
| 80B1 | ATTR_DB | Instruction not supported by CPU (e.g. S7-300) |
| 80C3 | PEEK/POKE | Byte offset + count exceeds DB length |
| 8090 | MOVE_BLK_VARIANT | Source or destination is NIL variant |
| 80B4 | MOVE_BLK_VARIANT | Variant type not supported (e.g. UDT with VARIANT inside) |
Performance Considerations
Reading a single byte through PEEK_DB costs roughly 6..15 microseconds on an S7-1516, while bulk-reading 86 bytes with PEEK_BLK amortises to about 1 microsecond per byte. Building the ANY with the AT overlay is a constant-time operation (six word moves); call it once per access and cache DBLength in a static tag to avoid a second ATTR_DB call. Avoid FOR loops of PEEK_DB one byte at a time if the DB can be read with a single PEEK_BLK — the loop approach is two to three orders of magnitude slower and is the most common anti-pattern on S7-300/400 code ported unchanged to S7-1500.
Field-Proven Cautions
- Optimized DBs do not allow
PEEK_DB/POKE_DBto symbolic offsets; you need a numeric byte offset. Keep a non-optimized mirror DB for any indirect interface. - If you cache
DBLengthin a static, add a reset input. Resizing the target DB online (with TIA Portal "reinitialize" orCREATE_DBinstructions) does not update your cached value. - The
DBNumberin the ANY is a UINT (0..65535). If your logic receives an INT, validate the range before callingATTR_DB; values < 0 or > 65535 produce 80A1. - Do not pass the
AT-overlay scratch variable to FBs that expect a typedVARIANT; copy the fields into a trueVARIANTfirst or useDB_ANY. - For S7-1500 firmware < V2.5 the variant instructions are missing; upgrade the CPU firmware if you need them, or use the explicit ANY + PEEK_BLK path.
Recommended Architecture
For new code, avoid ANY-pointer based dynamic DB access in business logic. Use one of:
- Symbolic with a static array of DB_ANY references — at compile time you know which DBs the application can talk to; pass the index, not the DB number.
- Serialize/Deserialize over a fixed-size buffer — platform-portable, type-safe, debuggable in the watch table.
- Structured UDT with a single dedicated instance DB per equipment unit — readability beats pointer acrobatics on a modern CPU.
How do I build a 16-byte ANY pointer dynamically in S7-1500 SCL?
Declare a T_ANY temp, assign S7Code=16#10, DataType per target type (16#02 for BYTE), Length in declared-type units, DBNumber from the INT input, MemoryArea=16#84, and pack the bit/byte offset into ByteAddressMSB/ByteAddressLSB. Use ATTR_DB to obtain the runtime length.
Why is %DB[i] not valid in TIA Portal?
%DB[i] was a legacy syntactic helper for static index lookup; it was removed in TIA Portal V14 in favour of the explicit AT-overlay and PEEK_DB/POKE_DB instructions, which are type-safe and work on both S7-1200 and S7-1500.
What is the difference between BLOCK_DB and DB_ANY?
BLOCK_DB is a system data type used inside instructions like MOVE_BLK_VARIANT and represents a DB reference resolved at runtime. DB_ANY is the parameter type for FBs and FCs that must accept a DB number from the caller, including untyped numeric DBs. Both are interchangeable in most move operations; DB_ANY is what you expose in a library interface.
Can I use the same indirect-access FB on S7-300/400 and S7-1500?
Not directly. ATTR_DB is S7-1500/1200 only. On S7-300/400, the DB length must be known at compile time or recovered from the DB header. Build a separate FB per platform and expose the same interface so the rest of the project is platform-agnostic.
What is the fastest way to copy 86 bytes from a runtime-selected DB on S7-1516?
Use PEEK_BLK with the BYTE data type and a 6-word ANY descriptor built once per call. The operation completes in roughly 80..120 microseconds. MOVE_BLK_VARIANT is comparable but adds variant overhead. Avoid per-byte PEEK_DB in a FOR loop — it can be 100x slower.