Overview: The UDT Address Reversal Problem
When you pass a User Defined Type (UDT) from a shared Data Block (DB) into a Function (FC), the FC receives a copy of the structure in its local stack. The FC can read and write every element of the UDT, but it cannot natively ask: "Which instance of this UDT did you come from, and at which byte address does the source sit in the parent DB?" This information is lost unless you reverse-extract the pointer that the STEP 7 compiler deposits in the FC's local TEMP area when the input parameter is bound.
The compiler writes a 6-byte pointer into the FC's local memory for every typed input parameter that is bound to a structured variable (UDT, ARRAY, or STRUCT). Inside the FC you can load this pointer, strip out the DB number, the memory area identifier, and the byte/bit address, and reconstruct the original location of the data block element the caller passed. This unlocks two powerful capabilities: programmatic logging of which instance was passed, and calculation of the zero-based UDT instance index inside the parent DB when all instances are of equal length.
This article covers the pointer layout, the exact STL instruction sequence to extract it, the arithmetic to compute the instance index, and the pitfalls you will encounter on S7-300/400 versus the optimized block model of S7-1200/1500. The procedure is fully supported in Siemens Industry Online Support documentation for STL programming and remains valid in TIA Portal for non-optimized FCs.
Prerequisites
- STEP 7 V5.5+ or TIA Portal V13+ with an S7-300, S7-400, S7-1200, or S7-1500 project.
- The FC block must be authored in STL (Statement List). LAD/FBD cannot emit the pointer arithmetic.
- The FC input parameter must be typed as the matching UDT, declared with the In or InOut direction.
- The UDT length (in bytes) must be known. Open the UDT in the project tree, switch to the detail view, and read
Length =in the status bar. - For TIA Portal users: the FC must be created with Standard block access (not Optimized) so that the parameter pointer is exposed in local TEMP. See the S7-1200/1500 block access documentation for details.
- Online connection to the target CPU for verification via Monitor/Modify.
The 6-Byte Parameter Pointer Format
When a structured parameter is passed into an FC, STEP 7 deposits a 6-byte pointer in the FC's local TEMP area. This is the same pointer format used by the older STEP 7 classic and is documented in the SIMATIC S7 programming reference manuals. The layout is little-endian in word form and BCD-style for the address portion.
| Byte Offset | Size | Contents | Interpretation |
|---|---|---|---|
| 0 | 1 byte | DB number low byte | DB number is a WORD; low byte at offset 0, high byte at offset 1. |
| 1 | 1 byte | DB number high byte | For DBs 1-255 this is 0; for DBs > 255 it is non-zero. |
| 2 | 1 byte | Memory area identifier | 0x10=I, 0x11=Q, 0x12=M, 0x13=DB, 0x14=DI, 0x15=L, 0x16=V (old local stack of called block). |
| 3 | 1 byte | Byte address, low nibble | BCD nibbles; high nibble of byte 3 holds the byte address high nibble, low nibble holds the bit number (0-7). |
| 4 | 1 byte | Byte address, middle nibble | Middle nibble of byte address (BCD). |
| 5 | 1 byte | Byte address, high nibble | High nibble of byte address (BCD). |
Bits 0-2 of byte 3 encode the bit offset (0-7). Bits 3-7 of byte 3, plus bytes 4 and 5, encode the byte address in BCD. The byte address must be decoded from BCD before any arithmetic is performed. For example, the bytes 0x20 0x01 0x00 for the address triplet decode to byte 0x100 = 256, bit 0. Bytes 0x42 0x01 0x00 decode to byte 0x142 = 322, bit 2.
Memory Area Byte Codes Reference
| Hex Code | Area | Description |
|---|---|---|
| 0x10 | I | Process image input (read-only bits) |
| 0x11 | Q | Process image output (read/write bits) |
| 0x12 | M | Bit memory / Merker area |
| 0x13 | DB | Data Block (shared DB) |
| 0x14 | DI | Instance Data Block |
| 0x15 | L | Local data (TEMP) of the current block |
| 0x16 | V | Previous block's local data (preceding L stack) |
| 0x17 | PI | Peripheral inputs (direct I/O) |
| 0x18 | PQ | Peripheral outputs (direct I/O) |
For a UDT passed from a shared DB the area byte will be 0x13. For a UDT inside an instance DB of an FB the area byte will be 0x14. Always check the area byte before assuming the data sits in a shared DB.
Extracting DB Number and Address via STL
The TEMP variable holding the parameter itself is the start point. The compiler writes the 6-byte pointer before the TEMP variable's own local address. The instruction L P##UDT_IN loads the V-memory (local stack) offset of the parameter into ACCU1. LAR1 then moves that offset into Address Register 1. From there you can dereference AR1 to read the pointer contents.
// UDT_IN is the FC's IN parameter typed as the UDT
// _DB_number and _Memory_area_and_Address are FC-local TEMP variables
L P##UDT_IN // load pointer offset into ACCU1
LAR1 // ACCU1 -> AR1
L W [AR1,P#0.0] // WORD at offset 0 = DB number
T #_DB_number // store as INT
L D [AR1,P#2.0] // DWORD at offset 2 = area + BCD address
T #_Memory_area_and_Address // store as DWORD
The first L W loads two bytes (offsets 0 and 1) as a 16-bit WORD. This is the DB number; load it as INT or WORD depending on whether you want signed interpretation (DB numbers are always unsigned, so WORD is technically more correct, but INT works for DBs < 32768). The second L D loads four bytes (offsets 2, 3, 4, 5) as a DWORD; the upper byte is the memory area identifier and the lower three bytes are the BCD-encoded byte/bit address.
Step-by-Step Implementation
- Create or open the FC. Open its interface declaration.
- Add an Input parameter named
UDT_INwith data typeUDT1(replace with your UDT name). - Add two TEMP variables:
_DB_numberasINTand_Memory_area_and_AddressasDWORD. - Add a TEMP variable
_InstanceIndexasDINTfor the computed instance number. - Add a TEMP variable
_UDT_length_bytesasINT, set it to the UDT length in bytes (read from the UDT properties dialog). - Switch the FC editor to STL view.
- Insert Network 1 with the pointer extraction code shown above.
- Insert Network 2 with the instance calculation logic shown in the next section.
- Compile (Ctrl+B or the menu Project > Compile). Fix any syntax errors before downloading.
- Download to the CPU and open the FC online with Monitor/Modify.
Calculating the UDT Instance Index
Once the byte address is decoded from BCD, you can divide it by the UDT length to recover the zero-based instance index. The formula is:
InstanceIndex = floor(ByteAddress / UDT_Length_bytes)
For example, if the parent DB contains three UDT1 instances back-to-back at offsets 0, 10, and 20, and the caller passes the third one, you will read back byte address 20. Dividing 20 by the UDT length of 10 yields instance 2 (zero-based). If your application's instance count is one-based, add 1 to the result. Use integer division (DIV in STL) and ignore any remainder; the pointer is guaranteed to point to the start of a UDT instance because the compiler always passes the base address of the structured element, never an offset into it.
L #_Memory_area_and_Address // area + BCD address
SRD 3 // shift right 3 bits to expose byte 3-5 only
SRW 4 // wait - use CAW / CAD + mask instead
// proper approach: extract BCD byte address and convert
L #_Memory_area_and_Address
L W#16#FF // mask to keep only byte 5
AW // ACCU2 AND ACCU1
T #_temp5 // high nibble of byte address
L #_Memory_area_and_Address
SRD 8
L B#16#F // mask nibble
AW
T #_temp4 // middle nibble of byte address
L #_Memory_area_and_Address
SRD 16
L B#16#F0
AW
SRD 4
T #_temp3 // low byte nibble of byte address
// Reassemble byte address from BCD nibbles:
L #_temp3
L 16
*I
L #_temp4
+I
L 16
*I
L #_temp5
+I
T #_ByteAddress // final byte offset, decimal
L #_ByteAddress
L #_UDT_length_bytes
/I // integer divide
T #_InstanceIndex
A more compact alternative uses the standard library function FC31 (BCD_TO_INT) or rolls its equivalent into three shift-and-mask operations. The CAD/CBW instructions do not apply here because the lower byte contains both the byte address and the bit offset packed together.
Complete Working FC Example
FUNCTION FC 100 : VOID
TITLE = 'Resolve UDT origin address'
AUTHOR : 'FIELD_ENGINEER'
VERSION : '1.0'
VAR_INPUT
UDT_IN : UDT1; // input parameter of type UDT1
END_VAR
VAR_TEMP
_DB_number : INT;
_Memory_area_and_Address : DWORD;
_temp3 : BYTE;
_temp4 : BYTE;
_temp5 : BYTE;
_ByteAddress : INT;
_UDT_length_bytes : INT := 10; // set to actual UDT length
_InstanceIndex : DINT;
END_VAR
BEGIN
NETWORK 1 // Pointer extraction
L P##UDT_IN;
LAR1 ;
L W [AR1,P#0.0];
T #_DB_number;
L D [AR1,P#2.0];
T #_Memory_area_and_Address;
NETWORK 2 // Area check - only proceed if memory area is DB (0x13)
L #_Memory_area_and_Address;
L W#16#FF00;
AW ;
L W#16#1300;
==I ;
JC DBX;
JU END_FC;
DBX: NETWORK 3 // Decode BCD byte address
L #_Memory_area_and_Address;
L B#16#F0;
AW ;
SRD 4;
T #_temp3;
L #_Memory_area_and_Address;
SRD 8;
L B#16#F;
AW ;
T #_temp4;
L #_Memory_area_and_Address;
SRD 12;
L B#16#F;
AW ;
T #_temp5;
NETWORK 4 // Reassemble and divide
L #_temp3;
L 16;
*I ;
L #_temp4;
+I ;
L 16;
*I ;
L #_temp5;
+I ;
T #_ByteAddress;
L #_ByteAddress;
L #_UDT_length_bytes;
/I ;
T #_InstanceIndex;
END_FC: NOP 0;
END_FUNCTION
Add a RET_VAL or write the results back to a global DB if downstream logic needs the values. Note: JC DBX conditionally jumps over the arithmetic if the source area is not a DB. Replace DBX with the actual network jump label as configured in your STEP 7 editor.
Address Encoding: Why Bytes Are Not What They Seem
The biggest pitfall for newcomers is the BCD encoding of the byte address. If you see 0x142 in the lower three bytes of _Memory_area_and_Address, the byte address is not decimal 322; it is the result of reading BCD nibbles from a packed DWORD. The actual byte offset is the integer formed by the high nibble of byte 3, byte 4, and the high nibble of byte 5, interpreted as base-16 nibble digits concatenated.
A worked example for the address triplet 0x20 0x01 0x00:
- Byte 3 =
0x20: high nibble 0x2 is the bit offset (bit 2), low nibble 0x0 is the high nibble of byte address. - Byte 4 =
0x01: middle nibble of byte address. - Byte 5 =
0x00: low nibble of byte address. - Byte address (decimal) = (0x0 * 4096) + (0x1 * 256) + (0x0 * 16) + (0x0 * 1) = 256.
Wrongly treating the bytes as a binary DWORD would give 0x000120 = 288 decimal, off by 32 bytes. Always shift and mask the nibbles individually.
Alternative: TIA Portal SCL Approach
If you are programming on TIA Portal and prefer SCL, the same data is available via the symbolic parameter. You can read UDT_IN directly by symbolic name without pointer arithmetic. However, the moment you need the byte address or instance index, you still need to compute it. SCL offers PEEK/POKE via the standard library, but for a UDT bound to an IN parameter you cannot peek into the parent DB without using an AT overlay or a VARIANT pointer. The most portable approach across TIA Portal versions is to keep the FC in STL and expose the result via an OUT parameter or a marker DB. Refer to the TIA Portal SCL programming reference for VARIANT and AT-view details.
Edge Cases and Pitfalls
- Optimized blocks (S7-1200/1500): The compiler does not deposit a 6-byte pointer for optimized FCs. You cannot extract the DB number via STL in this case. Switch the FC's Block access attribute from Optimized to Standard in the FC properties.
- Multi-instance DBs: If the UDT came from an instance DB of an FB, the memory area byte will be 0x14 (DI) instead of 0x13. The instance index will reflect the FB's instance DB number, not the parent UDT instance index.
- UDTs with non-uniform lengths: The divide-by-length approach only works when every UDT instance in the parent DB has identical length. If you have a UDT containing an ARRAY of variable length, use the boundary check (remainder must be zero) to detect misaligned pointers.
-
Passing slices or elements: If the caller passes
DB1.Array1[5], the pointer still points to the slice base, not the original array. The arithmetic will return the wrong instance. -
Symbolic vs. absolute access: When the caller uses
P#DB1.DBX20.0 BYTE 10as a POINTER input, the memory area byte will still be 0x13 and the address will be 20.0, but no UDT structure is implicit. The instance calculation must be guarded. - Work-memory impact: Passing full UDTs by value copies the entire structure into the FC's local stack. For UDTs larger than a few hundred bytes, prefer an INOUT parameter of type VARIANT or POINTER to avoid stack overflow on S7-300 CPUs.
Verification Checklist
- Open the FC online. Watch
_DB_number; it should match the source DB number (e.g., 13 for DB13). - Watch
_Memory_area_and_Address. The lower three bytes should match the byte/bit address as shown in the DB editor online view (e.g., DBX224.0 appears as bytes 0x00, 0x0E, 0x02 after BCD decode, plus the area byte 0x13). - Watch
_InstanceIndex. Call the FC with the first UDT instance (offset 0) and confirm the index is 0. Call with the second (offset = UDT length) and confirm the index is 1. Continue through the last instance. - Trigger a forced value change to a UDT element from the online view; verify the FC sees the new value on the next cycle (confirms pointer is dynamic, not snapshotted).
- If you see SF (System Fault) on the CPU after downloading, check the diagnostic buffer for Area length error when reading or DB not loaded; both indicate the FC was given an invalid pointer that escaped your area check.
- For S7-1500 with optimized blocks: if the FC will not compile because
P##UDT_INis rejected, the block attribute Optimized is still set. Switch to Standard and recompile.
Troubleshooting Matrix
| Symptom | Likely Cause | Remedy |
|---|---|---|
_DB_number reads 0 |
Input parameter not bound to a DB symbol; it is a literal or temporary | Verify the caller's input pin shows the symbolic UDT element, not a constant |
_Memory_area_and_Address = 0x16... |
UDT was passed from a previous block's L-stack (V-area) | Add a check: 0x16 != 0x13; treat the call as a re-entrant call and log a warning |
| Instance index is off by one | Counting instances one-based in the caller | Add 1 to _InstanceIndex before downstream use, or accept zero-based and document it |
| STL compile error: "Unknown instruction P##" | FC editor is not in STL view | View > STL; ensure L P## is recognized only in STL networks |
| Runtime error: area length error reading | UDT length was declared larger than actual UDT; the read crosses the DB boundary | Re-check _UDT_length_bytes against the UDT properties dialog |
S7-1500: L P##UDT_IN generates "invalid operand" |
Block is optimized; the compiler stripped the pointer | FC Properties > Attributes > uncheck "Optimized block access" |
| Result is always the first instance | Caller passed UDT_IN := DB1.UDT_Array (whole array), so the address is the array base, not the slice base |
Pass the individual slice: UDT_IN := DB1.UDT_Array[2]
|
| Address reads as zero regardless of caller | Caller passed a non-typed (ANY) parameter that resolved to address 0.0 | Verify the data type on the caller's pin matches the FC's UDT_IN declaration exactly |
Performance and Memory Considerations
The 6-byte pointer extraction is two load instructions and two transfers. The BCD decode is six shifts and masks plus three multiply-add operations. The instance divide is one integer division. On an S7-315 the entire sequence executes in roughly 8 to 12 microseconds. For higher call rates, copy the pointer once at FC entry to a STAT variable in an FB wrapper and call the FB instead of the FC; FBs retain their local static data across calls and avoid reloading the pointer on every invocation.
Be aware of the S7-300 local-stack limit: 256 bytes per priority class by default. A UDT passed by value consumes its full length in the L-stack. If UDT1 is 200 bytes and you also allocate 30 bytes of TEMP, you are at 230 bytes and within budget. Two large UDT parameters plus their TEMP will exhaust the L-stack and trigger an OB121 time-of-day fault. Use the CPU properties dialog to raise the L-stack size per priority class if required; the maximum is 32 KB on S7-400.
Best Practices for Field Deployment
- Wrap the FC inside an FB and store
_DB_number,_ByteAddress, and_InstanceIndexas STAT. This eliminates repeated pointer extraction on every call. - Validate the memory area byte before trusting the address. Treat anything other than 0x13 (DB) and 0x14 (DI) as a programming error and route to an error handler.
- Bound-check the calculated byte address against the parent DB length using
L DBNO; L DB-LENGTH; ==Ior a static configuration in the FB instance. - Document the zero-based vs. one-based instance convention in the FC header comment. Engineers who inherit the code will assume the wrong convention otherwise.
- Reserve a dedicated diagnostic DB to log the resolved (DB number, instance index) pairs on the first call of each cycle. This is invaluable when chasing intermittent pointer mismatches in field commissioning.
- Prefer symbolic UDTs over absolute pointers in the FC interface declaration. The compiler still emits the 6-byte pointer for symbolic binding, so you get both readability and runtime introspection.
Migration Path to S7-1200/1500
The classic STL pointer trick does not work on optimized S7-1200/1500 blocks. If you need equivalent functionality on the newer platforms, use one of these patterns:
- Pass the parent DB number, the UDT length, and the desired instance index as separate IN parameters. This pushes the addressing decision to the caller and removes the need to reverse-extract.
- Use a
VARIANTinput instead of a typed UDT. Inside the FC, useVariantGetand theTypeOf(instruction family to introspect the variant; you can read its DB number and offset via theDB_GET/DWORD_TOfunctions in the standard library. - Use an
AToverlay on a larger data structure if the UDT lives inside a known DB. The compiler still permits absolute DB access from STL even on optimized CPUs when the AT view points to a known DB number.
Refer to the S7-1200/1500 programming and operating manual on the Siemens Industry Online Support site for the exact VariantGet instruction availability per firmware version.
FAQ
Does the 6-byte pointer extraction work on S7-1200/1500?
Only on FCs with the Standard block access attribute. For optimized blocks the compiler strips the pointer. Switch the FC to non-optimized access in the FC properties dialog, or migrate to a VARIANT-based approach with TypeOf introspection.
Why does _DB_number return zero even though I passed a UDT from DB13?
The input parameter is not bound to a DB symbol. Confirm the caller's pin uses the symbolic UDT element (for example, DB13.UDT_Slot3), not a literal or a temporary. Also check that the FC's interface declares the parameter as In (not InOut with a constant).
Can I use this technique to find the start address of an ARRAY element?
Yes. The 6-byte pointer is emitted for any structured parameter, including ARRAY slices. The decoded byte address will be the array element's base, and dividing by the element size yields the array index.
What happens if the UDT is not the same size in every instance?
The divide-by-length approach fails because non-uniform instances produce non-constant offsets. Use a lookup table or a boundary check instead, and validate the remainder of the division against an expected offset list.
Is there a TIA Portal SCL equivalent without using STL?
Use a VARIANT input parameter and the TypeOf / VariantGet instructions from the standard library. The variant carries the DB number and offset, and TypeOf returns the underlying data type for safe deserialization. This is the supported TIA Portal pattern and is firmware-version dependent; consult the S7-1200/1500 system manual on the Siemens support portal for the exact instruction set per CPU firmware.