Indirect Access to Array of UDTs in S7-400 SCL with Step 7 5.5

David Krause15 min read
S7-400SiemensTutorial / 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

An array of UDTs (User-Defined Data Types) is the Siemens S7-400 equivalent of a struct array in C, a record array in Pascal, or a Type array in VB6. It groups a fixed number of identically structured records into a single, symbolically addressable container inside a data block (DB) or function block (FB) static area. The classic use case is a fleet of identical field devices — variable-frequency drives, weigh scales, valve manifolds, servo axes — where each device exposes the same set of parameters (command word, status word, setpoint, actual value, fault code, runtime hours) and one set of application code is required to operate on any of them by index.

STEP 7 V5.5 with the SCL optional package supports this pattern natively. Unlike SCL on TIA Portal, STEP 7 V5.5 requires the UDT to be declared first as a separate object in the S7 program, then referenced by symbolic name inside a DB or FB. Once the UDT exists, the SCL compiler accepts a variable as the array index and resolves the address symbolically — no P#DBxxx.DBX... pointer arithmetic is required from the application engineer. The remainder of this reference walks through UDT definition, DB declaration, static and dynamic indexing, parameter passing, expansion, and verification on an S7-400 CPU (CPU 416-3, CPU 417-4, or similar) running a STEP 7 V5.5 project.

Prerequisites and Toolchain

Before declaring an array of UDTs in an S7-400 program, verify that the following are installed and licensed:

  • SIMATIC STEP 7 V5.5 + SPx (HFx recommended), including the SCL optional package S7-SCL V5.5. The SCL compiler and editor are not bundled with the base STEP 7 DVD; they ship as a separate installation with their own license key.
  • An S7-400 station (rack with PS, CPU 41x, and at least one IM/CP if networked). The patterns described also work on S7-300 CPUs that are programmed with STEP 7 V5.5, but the array size is bounded by the smaller DB limits of those CPUs.
  • A CPU firmware version that supports the SCL features intended. S7-SCL V5.5 generates STL code that runs on every S7-400 CPU; dynamic array indexing is compiled into indexed byte/word/dword read-write operations on the CPU work registers, so any S7-300/400 CPU supports it.
Note: In STEP 7 V5.5 the SCL editor is a separate tool launched from the SIMATIC Manager. Source files with extension .scl are stored in the S7 program's Sources container. The compiler generates STL blocks (.awl) that are then downloaded to the CPU. STL blocks can be edited only by re-importing the SCL source.

For STEP 7 V5.5 block concepts, see the STEP 7 V5.5 Programming and Operating Manual. For SCL syntax, see the SCL for S7-300/400 Programming and Operating Manual.

Defining the UDT (User-Defined Data Type)

The UDT defines the shape of one record. In SIMATIC Manager, open the S7 program, right-click Program Blocks, choose Insert New Object > User-Defined Data Type, and name it (for example, UDT_Drive). The declaration editor opens with two section markers: STRUCT and END_STRUCT. Declare every field that the application needs for one device. The example below is a typical drive-side dataset:

Name Type Initial Value Comment
CmdWord WORD W#16#0 Control word to drive (per PROFIdrive profile)
SetSpeed_RPM INT 0 Speed setpoint, signed RPM
ActSpeed_RPM INT 0 Speed actual value, signed RPM
ActCurrent_mA INT 0 Motor current, milliampere
StatusWord WORD W#16#0 Status word from drive
FaultCode WORD W#16#0 Latest fault code (0 = no fault)
RunHours DINT L#0 Total powered run hours
Name STRING[8] ' ' ASCII tag, 8 characters max
Reserved BYTE B#16#0 Byte alignment for word access

Save and close the UDT. The total length of this UDT is 18 bytes for the fixed fields plus the STRING header (2 bytes length field + 8 characters = 10 bytes for STRING[8]), totaling 28 bytes per element. Knowing the size per element is critical when computing DB capacity — see the S7-400 reference manual section on DB size limits.

Tip: Keep STRINGs at the end of the UDT and pad with a reserved BYTE if a UDT copy with SFC20 (BLKMOV) is required. SFC20 requires the source and destination lengths to be equal; aligned UDTs make the math predictable and avoid hidden bytes that shift the offset of subsequent fields.

Declaring an Array of UDTs in a Global DB

Open (or create) a global data block, e.g. DB_DriveData, and declare the array of UDTs in the declaration editor:

Address Name Type Initial Value Comment
0.0 NumDrives INT 0 Active count, written by HMI
2.0 MotorDrv ARRAY[1..5] OF "UDT_Drive" Drive 1..5 dataset

STEP 7 computes the array footprint automatically: 5 elements × 28 bytes = 140 bytes, starting at offset 4.0 of the DB. The array is symbolic, so applications can write Data.MotorDrv[3].SetSpeed_RPM := 1500; in SCL and the compiler emits the absolute address DBx.DBW 60 (offset 4 + (3-1)·28 + 2-byte offset of SetSpeed_RPM inside the UDT).

Refer to the SIMATIC S7-400 Automation System, System Manual for the maximum DB size supported by your CPU family (CPU 412: 64 KB; CPU 414: 256 KB; CPU 416/417: up to 1 MB per DB on later firmware or with S7-400H).

Declaring an Array of UDTs in FB STAT

If the array is used only by one FB and its multi-instances, declare it in the static area of the FB instead of a global DB. This keeps the data encapsulated and gives the FB its own scratchpad across calls.

FUNCTION_BLOCK FB_DriveHandler
VAR
    bInit : BOOL;
END_VAR
VAR_TEMP
    i : INT;
END_VAR
VAR_STAT
    Drives : ARRAY[1..5] OF "UDT_Drive";
    iLastIndex : INT;
END_VAR
BEGIN
    // Body
END_FUNCTION_BLOCK

The static area of an FB in S7-400 is backed by the instance DB (the DB whose number matches the FB's instance) and the limits of that DB apply. The advantage over a global DB is automatic scoping: the drives array is visible only to instances of FB_DriveHandler, which simplifies maintenance on large projects.

Static Indexed Access in SCL

With the array declared, SCL accepts a constant or compile-time-evaluable index directly:

// Read drive 3 speed
iSpeed := Data.MotorDrv[3].ActSpeed_RPM;

// Write drive 1 command word
Data.MotorDrv[1].CmdWord := W#16#047F; // PROFIdrive "Enable Operation"

// Latch drive 5 fault
Data.MotorDrv[5].FaultCode := wFaultFromIO;

No special syntax is required — SCL resolves the symbolic path to an absolute address at compile time. This works in every S7-300/400 CPU that accepts SCL blocks.

Dynamic (Variable) Indexed Access in SCL

The whole point of using an array of UDTs is that the index is known only at runtime: the operator selects Drive 3 on the HMI, the application must look at element 3. SCL accepts any INT or DINT expression as the array index, provided its value is within the declared array bounds. The compiler inserts range-check code only when the option Generate range check is on; otherwise the index is used as-is and the CPU will fault with SF (OB121 programming error — area length error) if the index is out of range.

FUNCTION FC_ReadDriveSpeed : INT
// Returns ActSpeed_RPM for the drive whose 1-based index is supplied.
VAR_INPUT
    iIndex : INT;
END_VAR
BEGIN
    FC_ReadDriveSpeed := Data.MotorDrv[iIndex].ActSpeed_RPM;
END_FUNCTION

Call it from LAD, FBD, STL, or SCL:

// In SCL
iAct := FC_ReadDriveSpeed(iIndex := 3);

// In STL
CALL FC_ReadDriveSpeed
     iIndex := MW10
     RET_VAL := MW12

This is the symbolic, array-of-UDT, variable-index pattern the original question asks about. It compiles and runs identically to the array-of-INT case, because SCL lowers arr[i].field to a base-pointer + index·element-size + field-offset operation internally. The fact that the elements are UDTs rather than INTs is invisible to the indexing logic.

Passing the Array of UDTs to an FB

To let a generic FB operate on any drive in the array, pass the array as a parameter. The cleanest mechanism is an INOUT parameter, which gives the FB read/write access without copying the data:

FUNCTION_BLOCK FB_DriveControl
VAR_INPUT
    iTarget : INT;        // 1-based drive index
    wCommand : WORD;      // command word to issue
END_VAR
VAR_INOUT
    Drives : ARRAY[1..5] OF "UDT_Drive";
END_VAR
VAR_TEMP
    i : INT;
END_VAR
BEGIN
    IF (iTarget >= 1) AND (iTarget <= 5) THEN
        Drives[iTarget].CmdWord := wCommand;
    ELSE
        // Index out of range - set local error flag, do not write
    END_IF;
END_FUNCTION_BLOCK

Call it from OB1 (or any cyclic OB):

// STL
CALL FB_DriveControl, DB10
     iTarget := MW10
     wCommand := MW12
     Drives  := "DB_DriveData".MotorDrv

The Drives INOUT parameter is bound to the symbolic slice "DB_DriveData".MotorDrv. Any write inside the FB is visible immediately to other blocks that read from the same global DB, because INOUT parameters in SCL are passed by reference (pointer) and not by value.

Important: SCL on STEP 7 V5.5 does not allow an entire array-of-UDT to be passed VAR_INPUT by value when the element type is a UDT. The supported signature for large aggregates is VAR_INOUT. Trying to declare VAR_INPUT Drives : ARRAY[1..5] OF "UDT_Drive" will produce a compile error of the form "Function value or parameter of a structured data type is not allowed".

Complete Working Code Example

The following SCL source is a drop-in illustration: it scans the array, sums the actual currents of all drives with status bits "Ready to switch on", "Ready to operate", and "Operation enabled" set, and writes the highest fault code into a tag for the HMI alarm log. It uses only SCL keywords and a runtime index variable.

FUNCTION FC_ScanDrives : REAL
// Returns sum of motor currents in mA for drives reporting "Enabled".
VAR_INPUT
    iStartIndex : INT;   // 1-based
    iEndIndex   : INT;   // 1-based, inclusive
END_VAR
VAR_TEMP
    i : INT;
    rSum_mA : REAL;
    wStat   : WORD;
END_VAR
BEGIN
    rSum_mA := 0.0;
    FOR i := iStartIndex TO iEndIndex BY 1 DO
        wStat := Data.MotorDrv[i].StatusWord;
        // Bit 0 = "Ready to switch on"
        // Bit 1 = "Ready to operate"
        // Bit 2 = "Operation enabled" -- only these drives count
        IF (wStat AND W#16#0007) = W#16#0007 THEN
            rSum_mA := rSum_mA + INT_TO_REAL(Data.MotorDrv[i].ActCurrent_mA);
        END_IF;
    END_FOR;
    FC_ScanDrives := rSum_mA;
END_FUNCTION

Compile with File > Compile in the SCL editor. The compiler will list any unresolved symbolic references; resolve them by ensuring the UDT, the DB, and the function all live in the same S7 program and that the UDT's symbolic name (e.g. UDT_Drive) matches exactly.

Why Pure STL / Direct Pointer Access Differs

The classic STEP 7 approach to variable indexing uses the address registers AR1/AR2, the L D[AR1,P#0.0] / T D[AR1,P#0.0] instructions, and a manually computed byte offset. The required pseudo-code is:

// STL - manually build pointer to MotorDrv[iIdx].ActSpeed_RPM
L     #iIdx                    // 1..5
L     L#28                     // bytes per UDT element
*D
L     P#DBX 4.0                // base offset of MotorDrv in DB
+D
LAR1
L     DBW [AR1,P#2.0]          // offset of ActSpeed_RPM inside UDT
T     #iActSpeed

Two problems appear in production. First, the constant 28 (UDT size) is a magic number; if a field is added to the UDT, the program must be re-walked and every constant updated. Second, the literal DB number and base offset break symbolic programming. The SCL approach removes both: Data.MotorDrv[iIdx].ActSpeed_RPM is recompiled automatically by SCL when the UDT changes, and the symbol table continues to show the field name in every cross-reference, watch table, and online view.

If a block move of one UDT element to a peripheral area is needed (e.g. for a PROFIdrive cyclic telegram), call SFC20 (BLKMOV) from SCL and pass the symbolic source — STEP 7 will emit the correct ANY pointer:

// SCL
iRet := BLKMOV(SRCBLK := Data.MotorDrv[iIdx],
               DSTBLK := P#DB100.DBX 0.0 BYTE 28);

Expanding the Array Without Code Breakage

Suppose the application grows from 5 to 8 drives. The changes are:

  1. Open DB_DriveData and edit the array line to ARRAY[1..8] OF "UDT_Drive". STEP 7 will re-layout the DB and the new offsets are computed automatically.
  2. Re-compile all SCL sources in the S7 program. SCL regenerates the absolute addresses in every arr[i].field reference.
  3. Adjust the bound checks in any FB that loops over the array (e.g. IF i <= 8 THEN ...) to read the bound from the DB itself, "DB_DriveData".NumDrives, which the HMI writes.
  4. Re-download the project to the CPU. The DB retains its initial values; the application logic still works without rewiring cross-references.

Compare that with the STL/offset approach: every constant 28 would still be valid (UDT size is unchanged), but every P#DBX 4.0 base offset must be checked, the array end condition must be re-coded, and any FC that hard-codes the loop bound must be hand-edited. The SCL/UDT pattern is the only one that scales.

Verification and Commissioning Checks

After download, verify the pattern in the standard commissioning sequence:

  1. Open the instance/global DB online in STEP 7 and confirm that Data.MotorDrv shows the declared element count and that the byte offset matches the design spreadsheet.
  2. Force-drive an element from a watch table: Data.MotorDrv[3].SetSpeed_RPM := 1500. The value should appear at the symbolic address in the DB, and the cross-reference view should show the new value update.
  3. Trace the SCL function with breakpoints. Set a breakpoint on the line FC_ReadDriveSpeed := Data.MotorDrv[iIndex].ActSpeed_RPM;, then call the function with iIndex in MW10. Stepping into the block should show the symbolic expression evaluated to the correct absolute address.
  4. Run the integrated test: from the HMI, change the drive selection to 3 and confirm that the correct drive responds. The application code never sees the literal index 3 — it only sees the HMI-supplied INT.
  5. Stress the bounds: set iIndex := 0 and iIndex := 99. With Generate range check off, the CPU raises OB121 and the SF LED. With the check on, the SCL block raises a programmable range error that can be handled in OB121. Either way, the system must fail safe, not silent.

For the SCL block properties dialog (where the range-check option lives) see the SCL manual S7-SCL for S7-300/400, Programming and Operating Manual, section on compiler options.

Troubleshooting Matrix

Symptom Likely Root Cause Fix
Compile error: "UDT <name> is unknown" The UDT was renamed or resides in a different S7 program than the DB or SCL source Open the S7 program's Symbols table and verify the UDT's symbolic name; recompile from the same program folder
Compile error: "Array index out of declared range" The runtime index variable type is too small (e.g. BYTE used where INT is required) or is signed and holds a negative value Change to INT, ensure the variable is initialised before the access
Compile error: "Function value of structured data type is not allowed" on a UDT return Tried to declare FUNCTION_BLOCK ... RETVAL : "UDT_Drive" — not all SCL versions allow structured RETVAL Use a VAR_OUTPUT of UDT type instead of RETVAL
Online: SF LED on, diagnostic buffer shows OB121 Out-of-range index from HMI/PLC at runtime (range check off) Enable SCL range check in the compiler options, or clamp the index inside the FB
Online: writes have no effect Array passed as VAR_INPUT by value (a copy) — not supported for structured aggregates Change to VAR_INOUT so the parameter is passed by reference
Online: read returns the wrong element Off-by-one error: SCL arrays are 1-based by default and the HMI is 0-based Add or subtract 1 at the boundary; declare the array with explicit ARRAY[0..4] if the application is 0-based
Online: STRING field is garbage STRING length field overwritten because of misaligned copy Move the STRING to the end of the UDT and add a pad BYTE before the next field
Cross-reference is broken after UDT change SCL sources were not recompiled after the UDT was edited Right-click the S7 program's Sources container and select Check and Compile All
Safety: If the SCL block is part of a safety program (F-systems, e.g. F-CPU 416F), the array-of-UDT pattern is not permitted inside F-runtime groups — the F-library uses a fixed, certifiable structure. Use standard (non-F) SCL blocks for the array-of-UDT pattern, and isolate them from F-blocks with standard/safety boundary functions. See the S7-400 system manual chapter on F-programming for boundary rules.

Field-Notes Summary

  • An array of UDTs in STEP 7 V5.5 is the right tool any time a project has homogeneous field devices and a runtime index that the HMI, recipe system, or batch manager controls.
  • Declare the UDT once. Declare the array once (DB or FB-STAT). Index symbolically. The compiler does the rest.
  • Use VAR_INOUT for array parameters in FBs. Do not try to pass large structured aggregates VAR_INPUT by value.
  • Enable the SCL range check during commissioning; leave it on in production if the additional cycle time is acceptable. The check is a watchdog against runaway HMI tags.
  • When the device count grows, the change is one line: the array bound. SCL does the rest of the recompile work.

FAQ

Can I pass an array of UDTs to an FC in STEP 7 V5.5 SCL?

Yes, but only as a VAR_INOUT parameter, not as VAR_INPUT. VAR_INOUT passes the array by reference, so the FC reads and writes the same memory the caller uses. Declaring a structured VAR_INPUT parameter of an array-of-UDT type is rejected by the SCL V5.5 compiler.

Do I need STL or AR1/AR2 pointer tricks to use a variable index with an array of UDTs in SCL?

No. SCL accepts any INT/DINT expression as the array index. The compiler lowers arr[i].field to a base-pointer + index-times-element-size + field-offset operation internally and emits the equivalent STL/AR1 code for you. This is the standard, supported pattern.

What is the maximum array size of a UDT in a global DB on an S7-400?

It is bounded by the DB size limit of the CPU. CPU 412 supports 64 KB per DB, CPU 414 supports 256 KB, and CPU 416/417 support larger DBs (up to 1 MB on later firmware). With a 28-byte UDT, a 64 KB DB holds roughly 2,335 elements. Always check the S7-400 system manual for the exact number for your CPU and firmware.

Why does the SCL compiler reject "RET_VAL : UDT_name" for a function?

STEP 7 V5.5 SCL does not allow a function's return value to be a structured type, including a UDT. Use a VAR_OUTPUT parameter of the UDT type instead and assign the result to that output before the END_FUNCTION line.

Can I mix UDTs in a single array, for example ARRAY OF UDT1 OR UDT2?

No. An ARRAY in STEP 7 V5.5 must contain elements of one declared type. To hold a mix, define a parent UDT that embeds both UDT1 and UDT2 as STRUCT sub-records, or use the POINTER/ANY mechanism and store a type tag alongside the pointer. See the SCL for S7-300/400 manual for examples of the ANY technique.

Back to blog