Mapping Physical I/O to Siemens UDT Tags in TIA Portal V14+
User-Defined Data Types (UDTs) in Siemens TIA Portal are the cleanest way to group repeated process signals (thermal-relay confirms, contactor confirms, coil outputs, timers) into a single data template. They shine on S7-1200 and S7-1500 controllers where you have identical "device records" - the example below uses five chemical-addition routes that all share the same field signature. The catch shows up the moment you try to bind a real %I0.0 or %Q0.0 to a tag inside a UDT instance, and the temptation is to reach for a MOVE block. That box is the wrong tool, and a deep dive into why - and what to use instead - follows.
Why a Direct MOVE from I/O to UDT Tag Fails
The TIA Portal compiler treats every BOOL tag the same way: it occupies one bit. A MOVE box on a BOOL source, however, is not legal in STEP 7 - the IEC 61131-3 MOVE is defined for word-level operands (BYTE, WORD, DWORD, LWORD, INT, DINT, REAL, etc.). You will see the editor grey it out or refuse to download with a "Type conflict in operand" message. Engineers therefore fall into one of two traps:
- They wrap the BOOL into a temporary
WORD,MOVEit, and unwrap - producing a flurry of useless tags. - They search for a "MOVE_BLK" that magically couples a UDT to the process image - but
MOVE_BLKandFILL_BLKare byte-oriented, not BOOL-typed, so they too cannot address a UDT element bit by bit.
The right mental model is the opposite of MOVE: instead of pushing the I/O into the UDT, you expose each UDT element as a parameter of a Function Block (FB) and let the FB caller wire the physical address to the parameter at the call site. The UDT never owns a fixed physical address; it owns the shape of the data, and the FB owns the behaviour.
Prerequisites
- TIA Portal V14 SP1 (V15, V15.1, V16, V17, V18, or V19 behaviour is identical for this pattern).
- S7-1200 (any CPU from V2.0 firmware up - S7-1200 family page) or S7-1500 (any CPU from V1.0 firmware up).
- Configured hardware with the digital input and output module already added to the device view so that the system tags
%I0.0and%Q0.0are reserved. - Symbolic I/O access enabled (default on S7-1200/1500, configurable in PLC properties > General > Symbolic addressing).
- Basic knowledge of LAD/FBD editor and the SCL snippet editor.
Step 1 - Define the UDT
In the project tree, right-click PLC_x > PLC data types > Add new data type. Name it OutputDevice (or Dispositivo_De_Salida as the original poster did) and build the structure that matches the repeated field signature. A typical chemical-route element looks like the table below.
| Element | Data type | Meaning |
|---|---|---|
| ThermalRelayConfirm | BOOL | Hard-wired NO contact of the thermal overload |
| ContactorConfirm | BOOL | Auxiliary contactor confirm |
| BitOutput | BOOL | Command sent to valve or pump coil |
| ActivationTime | TIME | On-time accumulator |
| CycleCounter | DINT | Number of completed dosing events |
| LastFault | WORD | Bit-packed fault word |
Step 2 - Build the Global Data Block from the UDT
Right-click Program blocks > Add new block > Data block. Choose Type: my UDT "OutputDevice". Open the DB and add five rows, each row's Data type being OutputDevice. Name them Route_1 through Route_5. The DB now has symbolic access "MyDB".Route_1.ThermalRelayConfirm through "MyDB".Route_5.LastFault. The UDT does not know which real I/O it will receive - the binding is still abstract.
Step 3 - Solution A: Function Block with IN/OUT Parameters (Recommended)
Create a Function Block called FB_ChemRoute with the interface shown below.
| Section | Name | Type | Direction | Comment |
|---|---|---|---|---|
| Input | i_bThermalConfirm | BOOL | IN | Hard-wired thermal relay NO |
| Input | i_bContactorConfirm | BOOL | IN | Auxiliary contactor confirm |
| InOut | io_Device | OutputDevice | IN_OUT | The whole UDT instance for this route |
| Output | o_bValveCommand | BOOL | OUT | Physical output to valve or pump |
| Temp | ton_Pulse | TON_TIME | STAT | Local pulse timer |
Inside the FB body (SCL view, drop down from LAD if you prefer), the assignment is the textbook one-liner the discussion thread calls out:
// SCL body of FB_ChemRoute
io_Device.ThermalRelayConfirm := i_bThermalConfirm; // input binds to UDT element
io_Device.ContactorConfirm := i_bContactorConfirm;
ton_Pulse(IN := i_bThermalConfirm,
PT := T#2s);
io_Device.ActivationTime := ton_Pulse.ET;
// Decide coil command from accumulated time and confirmation bits
IF io_Device.BitOutput AND ton_Pulse.Q THEN
o_bValveCommand := TRUE;
io_Device.CycleCounter := io_Device.CycleCounter + 1;
ELSE
o_bValveCommand := FALSE;
END_IF;
The crucial line is the first one: io_Device.ThermalRelayConfirm := i_bThermalConfirm;. It is an assignment, not a MOVE, and the editor is perfectly happy because both sides are BOOL. The assignment runs every scan, so the UDT inside the DB always mirrors the live input.
Step 4 - Call the FB Once per Route
Drop FB_ChemRoute into OB1 five times. Each call becomes a multi-instance (or you can mark them as single-instance with separate DBs - the multi-instance form is preferred because it shares one IDB and keeps the project tree clean). The parameter wiring is what actually fixes the I/O mapping:
// OB1 call wiring - LAD view (SCL equivalent shown for clarity)
// Call 1
FB_ChemRoute_1(
i_bThermalConfirm := %I0.0, // physical input I0.0
i_bContactorConfirm := %I0.1, // physical input I0.1
io_Device := "MyDB".Route_1, // UDT instance
o_bValveCommand := %Q0.0 // physical output Q0.0
);
// Call 2
FB_ChemRoute_2(
i_bThermalConfirm := %I0.2,
i_bContactorConfirm := %I0.3,
io_Device := "MyDB".Route_2,
o_bValveCommand := %Q0.1
);
// ...continue through Call 5 with %I0.6, %I0.7, %I1.0, %I1.1, %Q0.4
Note that %I0.0 is the absolute I/O address (decimal I0.0 in STEP 7 notation) - the leading percent sign is the TIA Portal symbol for "direct hardware access". You may also wire a symbolic tag that is mapped to %I0.0 in the PLC tags table. Either is valid; direct addresses are easier to read on first commissioning.
i_bThermalConfirm to a different input from the watch table without recompiling.Step 5 - Why This Pattern Beats Every Alternative
-
Reusability. Drop a sixth route anywhere, wire the new
%Iand%Qto the FB inputs, pointio_Deviceat"MyDB".Route_6- done. The same FB body runs for every route. -
Symbolic debugging. In a watch table you can type
"MyDB".Route_1.ThermalRelayConfirmand the value updates live. No need to remember which input pin a particular route uses. -
Testability. Force
i_bThermalConfirm := TRUEfrom the watch table to prove the interlock chain without the real relay being present - the FB does not know or care that the value is forced. - Portability. Move the program to a new CPU with a different slot layout, change the absolute I/O addresses at the call sites, the FB body does not change.
Solution B - Bulk-Transfer the UDT Image with MOVE_BLK
There is a legitimate case for moving the whole UDT in one go: a Profibus DP slave or a Profinet device hands you a 16-byte input slice, and you want that slice stored into a UDT instance. Use MOVE_BLK or, more idiomatically, the S7-1500 Serialize / Deserialize instructions. The caveat is the same: the destination must be byte-oriented, not BOOL-by-BOOL.
// Capture a 16-byte Profinet input slice into the UDT of Route_1
// Source: %I200..%I215 (16 bytes starting at PIB 200)
// Dest : "MyDB".Route_1 starting at byte 0
MOVE_BLK(IN := P#I 200.0 BYTE 16,
OUT := P#DB 100 DBW 0 BYTE 16);
Once the bytes are inside the DB you can scatter them back into symbolic names:
// Scatter a packed word into individual flags
"MyDB".Route_1.ThermalRelayConfirm := %IW200.%X0; // bit 0 of input word 200
"MyDB".Route_1.ContactorConfirm := %IW200.%X1;
"MyDB".Route_1.BitOutput := %QW200.%X0;
The %X slicing operator on a word is legal in LAD/FBD/SCL and is the most compact way to fan a byte-or-word I/O region into individual UDT bits without an intermediate BOOL tag.
Solution C - Serialize / Deserialize for S7-1500
On S7-1500 firmware V1.8 and later (TIA Portal V14+ is the minimum) the Serialize block converts a structured tag (a UDT, an FB instance, a PLC data type) into a BYTE array, while Deserialize is the inverse. This is the cleanest way to push a complete UDT to a Profinet slot or to a partner CPU over PUT/GET.
// Serialize a UDT into a 32-byte buffer for OPC UA publishing
"fbSerialize"(SRC := "MyDB".Route_1,
DST := "CommBuffer".Route1Raw);
Note that the source must be a structured tag, not a scalar - which is exactly what a UDT provides. The destination receives the byte image with no MOVE box and no manual %X slicing.
Scatter / Gather When a UDT Maps onto a Process Image
Sometimes the UDT element names mirror an existing slice of the process image, and you do not want to wire the FB at all - the UDT is the I/O. In that case the Scatter and Gather blocks available in the SCL instruction set (TIA Portal V15+) are useful. Scatter explodes a byte array into individual BOOL/DINT/REAL members of a UDT, and Gather recomposes them. Used correctly, the UDT looks as if it were a direct I/O image, but the underlying data lives in the DB and can be archived, forced, or traced.
Scatter/Gather blocks work on contiguous memory and will not tolerate packing gaps. If the UDT is not laid out with BOOLs first, then BYTE/WORD, then DWORD/REAL, the SCL compiler inserts hidden alignment bytes and your offsets drift. Laying the UDT out manually as in Step 1 is the only safe option.Why the Original "Assign by Move" Question Fails
The original poster was essentially asking how to take I0.0 and MOVE it into a BOOL UDT element. The answer, already in the thread, is simply: do not MOVE it, assign it. The smallest correct unit in IEC 61131-3 is the BOOL, and the only legal operation to put a BOOL somewhere is the assignment :=. Anything else - a MOVE, a block move, a Variant cast - is fighting the type system. The UDT is a value, the FB is the function that consumes the value, and the call site is the place where value meets the real world.
Verification and Commissioning Checklist
- Compile the program (Project tree > PLC_x > Compile > Software). The build must complete with zero errors and zero warnings - any "uninitialised UDT access" or "parameter missing" warning indicates an FB instance was called without wiring the UDT element.
- Download to the CPU. In the Online > Diagnostics view confirm the operating mode is
RUNand the status LED is green. - Open a watch table and force
%I0.0 := TRUE. Verify"MyDB".Route_1.ThermalRelayConfirmfollows within one scan. Reset force. - Toggle
%I0.0through the watch table. Verify%Q0.0follows the logic inFB_ChemRoute. Use a logic analyser on the physical output to confirm the 24 V coil pulse. - Monitor the
CycleCounterin"MyDB".Route_1- it should increment on every completed pulse. - Repeat for routes 2 through 5 to confirm the assignment pattern generalises.
- From the Online > Back-up tab, save the project archive so the tested mapping is locked in.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| UDT element never updates | FB is not being called, or io_Device wired to the wrong DB row |
Confirm five FB instances in OB1, check io_Device parameter shows "MyDB".Route_X
|
| Compiler "MOVE not allowed for BOOL" | Engineer tried to use MOVE on a single-bit tag |
Replace with assignment :=, or use %X slice on a word |
| Inputs are inverted vs. expected polarity | NO/NC wired at the wrong point in the FB | Move the inversion inside the FB (i_bThermalConfirm := NOT i_bThermalConfirm at the top of the body) so the UDT always holds the logical sense |
| CycleCounter wraps at 32767 | Counter declared as INT instead of DINT | Change UDT member to DINT - covers 2.1 billion cycles |
| Force on %I0.0 does not propagate to UDT | FB instance is being called from an OB with lower priority, or symbolic access is disabled | Check the OB1 call, enable symbolic I/O access in PLC properties |
| Download fails with "UDT length mismatch" | UDT was edited in the project after download, PLC holds the old image | Do a full download, not a delta, or perform a memory reset of the CPU first |
| Other routes flicker randomly when Route_1 is forced | Both routes share the same UDT instance by accident | Open the DB and check that each route row is a distinct UDT member, not the same instance |
Field-Proven Caveats
- Multi-instance FBs share their parent FB's IDB; if you make any UDT member
RETAIN(e.g.CycleCounter), the entire IDB becomes retain - which is usually what you want, but be aware of it. - On S7-1200 firmware V4.0 and earlier, the
Serialize/Deserializeblocks are not available - downgrade toMOVE_BLK+%Xslicing. - Avoid using time-of-day or date-and-time in a UDT that is fed to
Serialize; the layout is platform-specific and breaks cross-CPU compatibility. - Always declare a default value for every UDT element. TIA Portal will initialise the DB on first download, but a subsequent online delta download that changes the UDT structure may leave new members uninitialised until the next cold start.
- If the controller is on Profinet and the device is wired through a Profisafe slice, never force
%Iaddresses that belong to the F-slave - the safety stack will reject the force and may shut the device down.
FAQ
Why can't I use a MOVE box to copy a BOOL input into a UDT element?
The IEC 61131-3 MOVE instruction operates on word-level data types (BYTE, WORD, DWORD, INT, DINT, REAL). For BOOL, the only legal operation is the assignment operator :=. Wire the I/O to an FB input and write io_Device.ThermalRelayConfirm := i_bThermalConfirm; in SCL, or use a simple = coil in LAD to assign the value into a UDT-typed tag.
Do I have to write a separate Function Block for every UDT I create?
No. One FB body can drive five (or fifty) UDT instances through the IN_OUT parameter. The FB is a behaviour template; the UDT is a data template. Call the FB once per route, wire each call to a different io_Device element ("MyDB".Route_1 through "MyDB".Route_5), and the same body handles all five.
Can I bind an entire UDT to a process image partition in one go?
Not directly - a UDT is a structured tag and the process image is byte-addressed. The closest single-instruction pattern is Serialize on S7-1500 (V14+), or a MOVE_BLK plus %X slicing for S7-1200. Both are described in the Bulk-Transfer section above.
Does the same pattern work on S7-300/400 with STEP 7 V5.x?
Yes, the assignment := on BOOL works identically. The only difference is the editor: in STEP 7 V5.x the UDT is edited in the data-type editor and called "UDT1", "UDT2", etc. The FB pattern is unchanged. Serialize/Deserialize are not available there - use MOVE_BLK.
What happens to UDT tag values during a CPU warm restart?
Non-retain UDT members are zeroed. Members declared Retain keep their last value, but the FB is still called from OB1 on the first scan, so the input assignments io_Device.ThermalRelayConfirm := i_bThermalConfirm; are re-executed on that first scan and the live I/O overwrites the retained value. Counters and accumulators should be declared DINT and Retain; real-time inputs should not.