Problem Overview: Copying a UDT Instance Between Two Data Blocks
User-Defined Data Types (UDTs) in TIA Portal are the standard mechanism for grouping related process variables into a single structured template. A typical use case is a recipe system: a recipe_udt contains a recipe number, a target temperature (Int), a duration (DInt), a recipe name (String), and a set of enable flags (Bool array). The same UDT is instantiated in many data blocks so each DB holds a complete recipe, or so that an "active recipe" DB is overlaid by a copy of the source recipe.
Engineers new to TIA Portal regularly hit a road block when they try to copy an entire UDT instance from DB100 into DB101 and find that the assignment behaves differently depending on whether the blocks are optimized, the block access type, the editor in use (LAD/FBD vs. SCL), and the version of the runtime firmware on the CPU. This article consolidates the four working methods, the constraints, the firmware-specific behavior, and the commissioning checks for copying a UDT instance between two DBs on S7-1200 and S7-1500 controllers.
Prerequisites
- TIA Portal V16, V17, V18, or V19 (any of these will run the techniques shown; V18 or V19 is recommended for the latest SCL compiler diagnostics).
- Target CPU: SIMATIC S7-1200 (CPU 1211C, 1212C, 1214C, 1215C, 1217C) or S7-1500 (CPU 1510, 1511, 1512, 1515, 1516, 1517, 1518). The MOVE-on-UDT technique works on every S7-1200/1500 firmware version; firmware-specific caveats are noted in the troubleshooting matrix.
- Identical UDT definition across all source and target DBs. If the source and target UDT definitions drift (e.g. one has an extra Bool at the end), the assignment still succeeds on optimized blocks, but the structural mismatch will cause uninitialized values in the trailing elements.
- Symbolic access (default for S7-1200/1500 since firmware V4.0). The Optimized block access attribute must be set on both DBs for the symbolic MOVE to compile cleanly.
- The "Recipes" / "User-defined data types" function in TIA Portal. See the SIMATIC S7-1200 Programmable Controller System Manual (entry ID 109751826) for the CPU section, and the SIMATIC S7-1500 Automation System System Manual (entry ID 109478121) for the equivalent S7-1500 reference.
UDT Copy Method Comparison
| Method | Editor | Optimized block access | Indexed / variable source | Code volume | Recommended use |
|---|---|---|---|---|---|
| 1. MOVE box | LAD / FBD | Required for clean compile | No (fixed IN/OUT) | 1 box per UDT pair | Single recipe handover, quick commissioning |
| 2. SCL direct assignment | SCL | Required | No (fixed source/dest) | 1 line | Single handover, clearly readable |
| 3. Array of UDT in one DB | SCL | Required | Yes (index variable) | ~5 lines | Multi-recipe (10-1000 recipes) systems |
| 4. PEEK / POKE or AT view | SCL | Not required (works on non-optimized) | Yes | ~20 lines + loop | Legacy S7-300/400 transfer, mixed block types |
Method 1: MOVE Box in Ladder / FBD
The simplest copy is a single MOVE instruction in a network. The source UDT instance and the target UDT instance must be of the same UDT type (TIA Portal enforces this; a mismatched assignment is rejected at compile time, not at runtime).
- Open the FC/OB where the copy should occur.
- From the Instructions task card, open Basic instructions → Move operations → MOVE.
- Drop the
MOVEbox into a network. - Wire the source UDT instance to
IN(e.g."DB100".recipe_udt). - Wire the target UDT instance to
OUT1(e.g."DB101".recipe_udt). - Drive the
ENinput with a Bool tag that controls the copy trigger (rising-edge recommended).
Once Optimized block access is enabled on both DBs (right-click the DB → Properties → Attributes → Optimized block access), the MOVE box compiles. On non-optimized blocks, the source and target must have the same absolute length; otherwise the compiler emits a Lengths of source and destination are different error and the box will not download.
Method 2: SCL Direct Assignment
For a single handover from one named DB to another, SCL is the most readable. The assignment operator := between two UDT instances performs a complete, byte-by-byte copy with the same type-checking the LAD MOVE box enforces.
// One-shot copy on a rising edge
IF "copy_trigger" THEN
"DB_Active".recipe_udt := "DB_Source".recipe_udt;
"copy_trigger" := FALSE;
END_IF;
The line "DB_Active".recipe_udt := "DB_Source".recipe_udt; copies every member of the source UDT into the target UDT, including nested structs, arrays of Bool/Int/Real, and STRING/WSTRING. The SCL compiler emits a single MOVE_BLK-style operation when the UDT is contiguous; when it is fragmented by alignment rules (typical when the UDT mixes 1-byte and 8-byte members), the compiler emits the appropriate MOVE sequence automatically.
Method 3: Array-Based Recipe DB (Multi-Recipe Systems)
For a 10-, 30-, or 100-recipe system, the cleanest architecture is one DB that contains an Array [lo..hi] of recipe_udt, plus one active-recipe DB that always contains a single recipe_udt. The copy reduces to a single indexed assignment.
- Create
RecipeDBas a new DB with attribute Optimized block access on. - Inside, declare a single static variable:
recipe : Array[0..99] of "recipe_udt"; - Create
ActiveRecipeDBas a new DB withrecipe : "recipe_udt";(single instance, no array). - In an FC, read the HMI recipe index into a static Int tag
selected_recipe(range 0-99). - Assign the indexed UDT element into the active-recipe UDT.
// SCL - copy recipe index n from the recipe array to the active recipe
IF "load_active" AND ("selected_recipe" >= 0) AND ("selected_recipe" <= 99) THEN
"DB_Active".recipe := "DB_Recipes".recipe["selected_recipe"];
"load_active" := FALSE;
END_IF;
This pattern replaces 30 or 60 individual MOVE instructions in ladder with a single line of SCL, and the same code scales from 10 to 1000 recipes by changing the array upper bound and the bounds-check on selected_recipe. HMI tags can drive the index directly; typical HMI widgets (Siemens WinCC Comfort/Advanced, TIA Unified Comfort Panels) bind an Int tag to a recipe-pick list, and the PLC just reads it.
"DB_Recipes".recipe["selected_recipe"] := "DB_Active".recipe;Always wrap the write with a bounds check and a write-protection flag so a corrupt HMI index cannot clobber recipe 0 (the default power-on recipe).
Method 4: Indexed Copy Using SCL Loops (UDT > 1 Element Across Multiple DBs)
If the application has ten separate recipe DBs (a structure inherited from an older S7-300/400 program), the UDT can still be copied without writing 10 MOVE boxes. The most common pattern is to use an AT view on a non-optimized block, or to step through elements with MOVE_BLK.
// Non-optimized source DB, copy to non-optimized target DB via AT view
// SourceDB and TargetDB are both standard (non-optimized) data blocks
VAR_TEMP
i : Int;
END_VAR
FOR i := 0 TO 9 DO
// Manual element copy using the symbolic DB name (optimized) or
// a generated instance DB of the UDT (both supported on V16+)
"DB_Target".recipe[i] := "DB_Source".recipe[i];
END_FOR;
If the source and target DBs are not declared as Array of UDT but as ten discrete UDT instances, generate a UDT-array view of both via the following approach:
- In each DB, add a temporary static variable:
udt_array_view AT "recipe" : Array[0..9] of "recipe_udt";This requires the DB to be non-optimized because the AT view is an overlay on the absolute memory. - Step through
ifrom 0 to 9, copying"DB_Source".udt_array_view[i] := "DB_Target".udt_array_view[i];in a single SCL loop. - For STRING members larger than the default 254 bytes, the UDT must declare a max length larger than the actual data; the runtime copies the full declared length. Trim by length attribute after the copy if needed.
Optimized vs Non-Optimized Block Access
The block attribute Optimized block access (TIA Portal) controls whether symbolic access is the only access mode (optimized) or whether the DB can additionally be addressed by absolute address such as DB100.DBX0.0 (non-optimized). All S7-1200 and S7-1500 firmware versions default to Optimized; only deliberate uncheck of the attribute in the DB properties changes it.
| Aspect | Optimized (symbolic) | Non-optimized (absolute) |
|---|---|---|
| Symbolic tag access | Yes (only mode) | Yes |
| Absolute access (DB100.DBX0.0) | No | Yes |
| UDT-to-UDT MOVE / assignment | Yes (clean compile) | Yes if lengths match |
| AT view overlay | Restricted (AT only on declared tags, not on UDT-internal slices) | Yes (overlays allowed at any byte) |
| PEEK / POKE | Limited (PEEK only on non-optimized targets) | Yes (full) |
| Performance | Faster on S7-1500 (symbolic pointers resolved at compile time) | Comparable on S7-1200, slightly slower on S7-1500 |
| Default since | S7-1200/1500 firmware V4.0 (Sep 2012) | Legacy S7-300/400 default |
The single most common cause of "the MOVE does nothing" or "the assignment does not compile" in fresh TIA Portal projects is that the source UDT is defined in one DB with the Optimized attribute on, while the target DB was migrated from an older project with Optimized off. Open both DBs, right-click → Properties → Attributes, set both to the same access mode, and recompile.
PEEK and POKE for Absolute DB Addressing
On S7-1200/1500, the PEEK and POKE instructions (formerly only on S7-300/400) are now part of the SCL instruction set as of TIA Portal V15.1. They work on non-optimized DBs and are useful when the source and target UDTs share the same byte layout but live in DBs whose UDT names differ slightly. They are not the recommended approach for a clean TIA Portal program, but they appear in legacy migration projects.
// Copy 200 bytes from DB100 starting at byte 0 to DB101 starting at byte 0
// using POKE_BLK (block write of byte array)
"POKE_BLK"(area_src := 16#84, // 16#84 = DB area
db_src := 100,
byte_src := 0,
area_dst := 16#84,
db_dst := 101,
byte_dst := 0,
count := 200);
For a S7-1500 CPU, the modern equivalent is MOVE_BLK_VARIANT or Serialize/Deserialize when the UDT layout must survive a communications transition (e.g. to a remote OPC UA server).
Verification and Commissioning Checks
After the code is downloaded, perform these checks before releasing the system to production:
-
Online watch both DBs. In TIA Portal, right-click the source DB → Monitor & Force; do the same on the target. Trigger the copy (set
copy_trigger= TRUE for one cycle). All UDT members in the target must show the same value as the source, including STRING contents, BOOL array values, and any nested structures. -
Change one element and re-copy. Modify a single tag inside the source UDT (e.g.
DB100.recipe.target_temp := 175), retrigger the copy, and confirm the target reflects the new value. If only some elements update, the UDT definitions are not identical (drift) — recompile both UDTs from the same source. - Cycle-time impact. A MOVE on a 256-byte UDT is below 5 µs on an S7-1516. A loop of 100 such moves stays well under 1 ms. Watch the OB1 cycle time in the online diagnostics to confirm there is no regression.
- Retain behaviour. If the UDT is in a non-retain DB and the CPU goes to STOP-RUN, the values reset to their initial values. For recipes, mark the DB as Retain = All, or set the individual UDT members as retain. (Right-click DB → Properties → Attributes → Retain.)
- Edge: write during commissioning. Force values to a test recipe, trigger the copy from a watched Bool, then unforce. The PLC should retain the test recipe across a power cycle only if the DB is retain; otherwise the next power-on returns to initial values.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| MOVE box shows red diagonal in LAD; will not compile | Source and target UDT names differ (e.g. recipe_udt_v1 vs. recipe_udt_v2) |
Re-link the IN/OUT tags to instances of the same UDT. TIA Portal will not auto-convert. |
| Compiler error: Different lengths for IN and OUT of the MOVE box | One DB optimized, the other non-optimized, so the symbolic size does not match the absolute size | Set both DBs to the same access mode in Properties → Attributes |
| Copy runs but STRING members stay at the initial value | STRING declared with max length 0, or with max length < the actual data written by the HMI | Set the STRING max length in the UDT to at least the largest string the HMI will write (typical 50 or 254 characters) |
| Index out of range in SCL: Area length error at the indexed assignment | HMI recipe index is outside the declared array bounds (e.g. 105 written to a [0..99] array) | Clamp the index in the PLC: IF idx > 99 THEN idx := 99; END_IF; IF idx < 0 THEN idx := 0; END_IF;
|
| Only the first element of a STRING array updates | The STRING array was declared as a non-retentive static, but the loop runs only once | Confirm the loop bounds and the SCL FOR semantics; SCL unwinds the loop at compile time, so a wrong upper bound will compile but copy the wrong number of elements |
| Target UDT clears to 0 after CPU restart | Target DB is not retain | Set Retain = All on the DB or set retain on each UDT member |
| AT view of the UDT will not compile | AT view is being used on an optimized block | Switch the DB to non-optimized, or remove the AT view and use a simple UDT-to-UDT assignment |
| SCL assignment: The data types of the operands are not compatible | Source and target are different UDT types even though the names look the same (different version) | Re-import the UDT from a single source, or open both UDT editors and confirm member-by-member equality |
| Copy executes but a BOOL inside the UDT does not change | HMI is writing the BOOL by absolute address to a non-optimized DB and the PLC copy uses symbolic — the absolute write happens after the copy in the OB1 scan | Use symbolic HMI tags only, or sequence the absolute write into the symbolic write explicitly |
| On S7-1200 firmware V4.0-V4.2, MOVE on a UDT containing WSTRING fails | WSTRING support in UDT copy was added in firmware V4.3 | Update CPU firmware to V4.3 or later, or convert WSTRING members to STRING |
Memory, Performance, and Retain Footprint
For a 30-recipe system with a 200-byte recipe_udt, the recipe array DB occupies 30 × 200 = 6000 bytes in the load memory and (if marked retain) the same 6000 bytes in the retain memory. S7-1200 CPUs from 1214C upward support retain up to the full work-memory limit; the 1211C/1212C are limited to 10 kB of retain. Always check the Load memory / Work memory / Retain memory figures in the CPU's device description (TIA Portal → Devices & networks → CPU → Properties → Memory). For a S7-1500, the bit-granular retain setting (per UDT member) allows the operator to retain only the recipe number and the target temperature while leaving the operator-editable STRING members as non-retain, saving several hundred bytes per recipe.
Field-Proven Patterns and Anti-Patterns
Use symbolic names everywhere. Every UDT instance, every MOVE, every SCL assignment should reference the symbolic tag. Absolute addressing on an S7-1200/1500 optimized block is illegal and the compiler will refuse the download. Even when the block is non-optimized, mixing symbolic and absolute makes the program brittle: a UDT edit will move the absolute address, and the next download will silently break the program.
Keep the UDT flat or shallow. Nested structs are supported, but a deeply nested UDT (struct of struct of struct of array) defeats online readability and slows the online diff tools. Aim for one or two levels of nesting.
Do not use a UDT for the HMI alarm log. Alarm logs grow continuously; an array of UDTs without bound management will exhaust work memory. Use the standard WinCC alarm logging for that use case.
Avoid copying UDTs in OB1 unconditionally. A MOVE on a 256-byte UDT in OB1 every scan will burn a few microseconds of CPU time and is wasteful if the source has not changed. Use a "dirty" flag set by the HMI or by the recipe edit logic to trigger the copy once.
Edge Cases and Diagnostic Procedures
UDT version drift. When a recipe_udt gains a new Bool (e.g. enable_cooling), all instances of the UDT pick up the new member, but the values of the existing members in the retain area are preserved. The new member is initialized to its declared initial value (FALSE for Bool, 0 for Int). If the program logic depends on the new member being non-zero, initialize it explicitly in the startup OB (OB100 for S7-1200/1500).
STRING length handling. A STRING occupies 2 bytes of header (max length, actual length) plus the data. The MOVE / assignment copies the full declared max length, not just the actual length. A STRING of max length 254 declared in the UDT therefore always costs 256 bytes in the load memory, even if the operator wrote "AB". This is a common surprise when estimating memory.
WSTRING (UTF-16) handling. WSTRING members in a UDT behave like STRING but with double-byte characters. The same copy semantics apply; firmware V4.3+ on S7-1200 is required.
Trigger from HMI versus trigger from PLC logic. If the HMI writes the recipe number to an Int tag, the PLC can read the new value in the next OB1 scan and trigger the copy itself. If the HMI instead sets a Bool "load" tag and clears it after the copy, the PLC must do the clear in the same FC to avoid a race. The standard pattern is: HMI sets load := TRUE, PLC copies and sets load_ack := TRUE, HMI clears load := FALSE when it sees load_ack.
FAQ
Why does my UDT MOVE box show a red diagonal in TIA Portal?
The source and target UDT types are not identical. TIA Portal enforces strict type matching on UDT-to-UDT moves. Re-link the IN and OUT1 tags to instances of the same UDT, then recompile. Also confirm both DBs have the same Optimized block access attribute, because a mix of optimized and non-optimized DBs will surface as a "lengths differ" error.
Can I copy only a single element of the UDT (e.g. just the temperature) between DBs?
Yes. Symbolic access on the UDT member is supported: "DB_Active".recipe.target_temp := "DB_Source".recipe.target_temp; in SCL, or a single MOVE box from the member to the member in LAD/FBD. There is no need to copy the whole UDT if only one value changes.
Which firmware version on the S7-1200 first supports MOVE on a UDT containing WSTRING?
WSTRING support in UDT copy semantics was introduced in S7-1200 firmware V4.3. On V4.0-V4.2, MOVE on a UDT that contains a WSTRING will fail to compile or run incorrectly. Update the CPU firmware to V4.3 or later, or replace the WSTRING with a regular STRING.
How do I let the HMI choose which recipe to copy without writing 30 MOVE instructions?
Store the recipes in a single DB as Array[0..29] of "recipe_udt"; and store a single instance in the active-recipe DB. Read the HMI recipe index (Int tag) and execute one indexed assignment: "DB_Active".recipe := "DB_Recipes".recipe["selected_recipe"]; in SCL. One line replaces 30 MOVE boxes.
What is the smallest block size I can copy a UDT into on an S7-1200?
There is no minimum size; an empty UDT (no members, length 0) is allowed and the copy degenerates to a no-op. In practice, a UDT under 4 bytes is uncommon because every Bool, Int, Real, and STRING header contributes at least 2 bytes. A 200-byte recipe_udt copies in roughly 4-6 microseconds on an S7-1214C and 1-2 microseconds on an S7-1516.