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.
.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.
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.
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:
- Open
DB_DriveDataand edit the array line toARRAY[1..8] OF "UDT_Drive". STEP 7 will re-layout the DB and the new offsets are computed automatically. - Re-compile all SCL sources in the S7 program. SCL regenerates the absolute addresses in every
arr[i].fieldreference. - 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. - 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:
-
Open the instance/global DB online in STEP 7 and confirm that
Data.MotorDrvshows the declared element count and that the byte offset matches the design spreadsheet. -
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. -
Trace the SCL function with breakpoints. Set a breakpoint on the line
FC_ReadDriveSpeed := Data.MotorDrv[iIndex].ActSpeed_RPM;, then call the function withiIndexin MW10. Stepping into the block should show the symbolic expression evaluated to the correct absolute address. - 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.
-
Stress the bounds: set
iIndex := 0andiIndex := 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 |
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_INOUTfor array parameters in FBs. Do not try to pass large structured aggregatesVAR_INPUTby 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.