Building Reusable UDT and FB Templates in TIA Portal for S7-1200/1500
Replicating nearly identical logic across four tanks, eight conveyors, or sixteen lamps is the single most common source of maintenance pain in a Siemens project. The temptation is to start with a UDT and build everything top-down. The pragmatic, field-proven path is the opposite: get one tank working with global tags, replicate it once, and only then start carving out the UDT, FB, and FC templates. This article walks through that workflow in SIMATIC S7-1500 and SIMATIC S7-1200 using TIA Portal V18/V19/V20, with the IEC 61131-3 Structured Text (SCL) code for a 4-tank template and a reusable signal-conditioning FC.
1. Why DRY Matters on a PLC
The DRY principle (Don't Repeat Yourself) applies to PLC code exactly as it does to any other software. When you copy-paste a Start/Stop latch into eight FB instances, any future change to the interlock logic, the blink timing, or the fault behavior has to be made in eight places. The risk of forgetting one is not theoretical; it is the dominant cause of asymmetric faults in skid commissioning.
Siemens' official programming and operating manuals for the S7-1500 describe three primary Program Organization Units (POUs) you can use to fight repetition:
- FC (Function) — stateless, no Instance DB, ideal for pure calculations and signal routing.
- FB (Function Block) — stateful, owns an Instance DB, the workhorse for repeated control objects.
-
UDT (User-Defined Data Type) — a named
STRUCTthat groups tags so they can be passed as a single parameter.
The official S7-1500 system manual frames FCs as the correct choice when you have no memory requirement, and FBs as the right choice whenever the controller must keep track of state between scans — for example, a VFD enable sequence with stages such as enabling line feed in progress.
2. Prerequisites
Before building a template, confirm the development environment and target hardware support multi-instance FBs and optimized block access.
| Item | Minimum version | Why it matters |
|---|---|---|
| TIA Portal | V18 (or V19/V20) | Stable UDT editor, multi-instance DB, "know-how protection" improvements |
| S7-1200 CPU firmware | V4.4 or later | Multi-instance FBs, optimized access, full SCL support |
| S7-1500 CPU firmware | V2.9 or later (V3.0+ for new OPC UA features) | All multi-instance and GRAPH features |
| S7-1500 software controller (S7-1500S / ET 200SP CPU) | V21.9 firmware | Same FB/UDT rules as the rack PLC |
| STEP 7 Safety (optional) | V18 | Required only if the FB will become F-FB |
You also need the Siemens online help installed locally; F1 on any instruction in the editor opens the official help page and is the single best source for per-instruction restrictions.
3. FC vs FB Decision Matrix
Use this matrix when you are unsure which POU to create.
| Symptom in the logic | Use | Why |
|---|---|---|
Scales an INT input to engineering units |
FC | Combinational, no state |
| Computes a flow compensation formula | FC | Pure math |
| Routes 32 raw I/O bits into a UDT structure | FC | Copy-only, no state |
| Runs a Start/Stop latch with seal-in | FB | Latched state must survive scan |
| Steps a VFD through Enable → Run request → At-speed → Bypass | FB | Multi-stage state machine |
| Generates a 1 Hz blink for a lamp | FB (owns TP or IEC_TIMER instance) |
Timer must hold its accumulated time |
| Detects a rising or falling edge on a button | FB (or FC with global edge memory bit) | Edge memory bit must persist |
| Holds the last fault code for an HMI faceplate | FB (static tag) | State persists across resets |
4. UDT Design Patterns for Physical I/O
There are three common patterns for a UDT that wraps I/O. Pick one and stay consistent across the project.
4.1 Raw I/O only
The UDT holds the symbolic I/O addresses and nothing else. All interpretation (NOT, P_Trig, N_Trig, debounce) lives in the FB that consumes the UDT. This is the cleanest pattern for new projects and is the one used in the example below.
4.2 I/O plus derived bits
The UDT holds both the raw bits and the derived ones (iStart_P, iStop_N, iRun_Not). Useful only if the derived bits are read from many places, because every additional UDT member is a memory slot that must be kept coherent.
4.3 HMI-shaped UDT
The UDT contains the same fields the HMI faceplate displays, plus a few hidden control bits. Common in skid projects where the HMI is locked to a fixed faceplate. The risk is that PLC programmers and HMI programmers both want to extend the UDT, which leads to versioning pain.
5. Step-by-Step: 4-Tank Template in TIA Portal
The following sequence implements a 4-tank control using one UDT, one FB, one FC, and one global DB. The TIA Portal menu path shown assumes the English UI; in German the paths are the same word-for-word in most dialogs.
- Create the project and add the CPU. In the Project view, click Add new device and select the S7-1500 CPU (e.g. 6ES7515-2AM02-0AB0). Confirm the firmware version matches your target.
-
Add a UDT for the raw I/O group. In the project tree, expand PLC > PLC_1 > Program blocks and double-click Add new data type. Name it
UDT_TankIo. Replace the default content with the SCL shown in section 6. -
Add a UDT for the higher-level tank state. Create
UDT_Tankcontaining everything that is not raw I/O: enable, mode, setpoints, fault word, runtime counters, HMI-visible values. -
Map the physical I/O to tag names. In PLC tags > Default tag table, add symbols such as
Tank1_Startat%I0.0,Tank1_Stopat%I0.1,Tank1_LevelLowat%I0.2,Tank1_LevelHighat%I0.3,Tank1_Pumpat%Q0.0, andTank1_Lampat%Q0.1. Repeat for Tank2..Tank4 with their own offset ranges (or use slot-based addressing on the ET 200SP). -
Create the global instance DB for I/O. Add a new DB named
DB_TankIo. Inside it, declare four variables of typeUDT_TankIo:Tank1,Tank2,Tank3,Tank4. -
Copy raw tags into the UDT instance in a single FC. Add an FC named
FC_CopyInputs. The body simply copies the PLC tag bits into the corresponding members ofDB_TankIo. Keeping this in one place means that if you ever change the wiring from a PNP to an NPN sensor, you change one FC rather than four FBs. -
Create the FB that owns the tank logic. Add an FB named
FB_Tank. ItsInOutinterface takes theUDT_TankIofor that tank. ItsInputinterface carries enable, mode, and setpoints from a higher-levelUDT_Tank. Section 7 lists the full SCL. -
Create the FC that calls four FB instances. Add an FC named
FC_Tanks. Inside it, declare fourstatTank1..statTank4static variables of typeFB_Tank. These are multi-instance FBs — they share a single Instance DB rather than generating four separate DBs. -
Call
FC_CopyInputsandFC_Tanksfrom a cyclic OB. In OB1 (or in a dedicated OB with a higher priority for fast I/O), callFC_CopyInputsfirst, thenFC_Tanks. The order matters: the FB must see the I/O snapshot, not the live process image, if you intend to add filtering later. -
Add an HMI tag DB. The HMI connects to the same
DB_TankIoand to the higher-levelDB_Tank. Use the standard HMI faceplate pattern of one UDT per icon, so dragging a tank symbol onto a screen binds toDB_Tankin a single click. - Compile and download. Right-click PLC_1 > Compile > Software (rebuild all blocks). Resolve any access errors before downloading.
6. UDT and FC Source for the 4-Tank Template
The following SCL can be pasted directly into the TIA Portal SCL editor. SCL is the recommended language for templates because the syntax is portable across FBs, FCs, and methods.
6.1 UDT_TankIo — Raw I/O Group
TYPE "UDT_TankIo"
VERSION : 1.0
STRUCT
iStart : BOOL; // Start pushbutton, active high
iStop : BOOL; // Stop pushbutton, active high
iLevelLow : BOOL; // Low-level float switch
iLevelHigh : BOOL; // High-level float switch
qPumpRun : BOOL; // Pump contactor command
qLampRun : BOOL; // "Pump running" indication lamp
qLampFault : BOOL; // "Fault" indication lamp
END_STRUCT;
END_TYPE
6.2 UDT_Tank — Higher-Level Tank State
TYPE "UDT_Tank"
VERSION : 1.0
STRUCT
bEnable : BOOL; // Master enable from operator
bAuto : BOOL; // TRUE = auto, FALSE = manual
rLevelSetpoint : REAL; // Target level in % (0.0..100.0)
rLevelActual : REAL; // Scaled level feedback
wFault : WORD; // Bit-packed fault word
dwRuntimeSec : DINT; // Pump runtime accumulator
bRunning : BOOL; // Current run state (mirror of FB output)
END_STRUCT;
END_TYPE
6.3 FC_CopyInputs — Snapshot Raw I/O Into the UDT Instance
FUNCTION "FC_CopyInputs" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_TEMP
tInfo : INT;
END_VAR
BEGIN
// Tank 1
"DB_TankIo".Tank1.iStart := "Tank1_Start";
"DB_TankIo".Tank1.iStop := "Tank1_Stop";
"DB_TankIo".Tank1.iLevelLow := "Tank1_LevelLow";
"DB_TankIo".Tank1.iLevelHigh := "Tank1_LevelHigh";
// Tank 2
"DB_TankIo".Tank2.iStart := "Tank2_Start";
"DB_TankIo".Tank2.iStop := "Tank2_Stop";
"DB_TankIo".Tank2.iLevelLow := "Tank2_LevelLow";
"DB_TankIo".Tank2.iLevelHigh := "Tank2_LevelHigh";
// Tank 3 / 4 omitted for brevity; pattern is identical
// Outputs: zero them first if the FB does not drive them every scan
"DB_TankIo".Tank1.qPumpRun := FALSE;
"DB_TankIo".Tank1.qLampRun := FALSE;
"DB_TankIo".Tank1.qLampFault := FALSE;
END_FUNCTION
The FC deliberately sets every output to FALSE on entry. This prevents a stale TRUE from a previous scan surviving a download or a CPU stop/start transition. The FB reasserts the outputs it wants during the same OB cycle.
7. FB_Tank — The Reusable Logic Block
The FB owns all per-tank state, all edge flags, and a 1 Hz blink timer. It uses an InOut of type UDT_TankIo so the caller can keep the raw I/O in a single DB and pass it by reference.
FUNCTION_BLOCK "FB_Tank"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
iEnable : BOOL; // Master enable for this tank
iAuto : BOOL; // TRUE = auto, FALSE = manual
END_VAR
VAR_INOUT
ioIO : "UDT_TankIo";
END_VAR
VAR_OUTPUT
qRunning : BOOL;
qFault : BOOL;
END_VAR
VAR
statStartEdge : BOOL; // Edge memory for start button
statStopEdge : BOOL; // Edge memory for stop button
statRunState : BOOL; // Latched run state
instBlink : TP; // 1 Hz blink for the run lamp
instFaultBlink : TP; // 2 Hz blink for the fault lamp
END_VAR
VAR_TEMP
tStart : BOOL;
tStop : BOOL;
END_VAR
BEGIN
// 1. Edge detection (manual: explicit memory)
tStart := ioIO.iStart AND NOT statStartEdge;
tStop := ioIO.iStop AND NOT statStopEdge;
statStartEdge := ioIO.iStart;
statStopEdge := ioIO.iStop;
// 2. Latching logic with interlocks
IF iEnable THEN
IF tStart AND ioIO.iLevelLow AND NOT ioIO.iLevelHigh THEN
statRunState := TRUE;
END_IF;
IF tStop OR ioIO.iLevelHigh THEN
statRunState := FALSE;
END_IF;
ELSE
statRunState := FALSE;
END_IF;
// 3. Drive the pump output (final stage has the seal-in)
ioIO.qPumpRun := statRunState AND iEnable;
qRunning := ioIO.qPumpRun;
// 4. Fault: feedback mismatch with a 2 s debounce in the FB would
// be the next step. For clarity, the example uses a direct check.
qFault := statRunState AND (NOT ioIO.iLevelLow) AND (NOT ioIO.iLevelHigh);
// (running but neither level switch made = dry run)
// 5. 1 Hz blink on the run lamp while running
instBlink(IN := statRunState, PT := T#500MS);
ioIO.qLampRun := instBlink.Q;
// 6. 2 Hz blink on the fault lamp while faulted
instFaultBlink(IN := qFault, PT := T#250MS);
ioIO.qLampFault := instFaultBlink.Q;
END_FUNCTION_BLOCK
P_TRIG/N_TRIG? Both system instructions need an instance. Inside an FB, that instance is automatic and unique per FB call. If you try to use them inside a stateless FC, the compiler falls back to a shared global edge memory bit, which collides between blocks. Explicit statStartEdge/statStopEdge is verbose but deterministic and easy to read in the watch table.
8. FC_Tanks — The Multi-Instance Caller
FUNCTION "FC_Tanks" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR
statTank1 : "FB_Tank";
statTank2 : "FB_Tank";
statTank3 : "FB_Tank";
statTank4 : "FB_Tank";
END_VAR
BEGIN
statTank1(iEnable := "DB_TankHmi".Tank1.bEnable,
iAuto := "DB_TankHmi".Tank1.bAuto,
ioIO := "DB_TankIo".Tank1,
qRunning=> "DB_TankHmi".Tank1.bRunning,
qFault => "DB_TankHmi".Tank1.wFault.0);
statTank2(iEnable := "DB_TankHmi".Tank2.bEnable,
iAuto := "DB_TankHmi".Tank2.bAuto,
ioIO := "DB_TankIo".Tank2,
qRunning=> "DB_TankHmi".Tank2.bRunning,
qFault => "DB_TankHmi".Tank2.wFault.0);
statTank3(iEnable := "DB_TankHmi".Tank3.bEnable,
iAuto := "DB_TankHmi".Tank3.bAuto,
ioIO := "DB_TankIo".Tank3,
qRunning=> "DB_TankHmi".Tank3.bRunning,
qFault => "DB_TankHmi".Tank3.wFault.0);
statTank4(iEnable := "DB_TankHmi".Tank4.bEnable,
iAuto := "DB_TankHmi".Tank4.bAuto,
ioIO := "DB_TankIo".Tank4,
qRunning=> "DB_TankHmi".Tank4.bRunning,
qFault => "DB_TankHmi".Tank4.wFault.0);
END_FUNCTION
Because each statTankN is declared FB_Tank rather than DB_Tank1, TIA Portal stores all four instances inside a single Instance DB owned by FC_Tanks. This is the multi-instance pattern and it dramatically reduces the block count in the project tree.
9. OB Wiring
The OB that runs the application looks like this:
ORGANIZATION_BLOCK "OB_Cyclic_100ms"
VERSION : 1.0
VAR_TEMP
tInfo : INT;
END_VAR
BEGIN
"FC_CopyInputs"(); // Snapshot raw I/O into UDT
"FC_Tanks"(); // Run all four FB_Tank instances
"FC_HmiPublish"(); // Copy derived state to HMI-visible DB
END_ORGANIZATION_BLOCK
Using a dedicated cyclic OB (e.g. 100 ms) instead of OB1 isolates the tank logic from the default 10 ms OB1 background. This is the recommended approach for the S7-1500, where you can have up to 100 cyclic OBs.
10. Block Interface Best Practices
| Section | Use for | Typical examples |
|---|---|---|
Input |
Read-only values supplied each call | Enable, mode, setpoint, remote command |
Output |
Read-only values produced by the block | Running, fault, current level, ready |
InOut |
Variables the block both reads and writes | The UDT I/O group, an alarm-acknowledge struct |
Static |
Retained state owned by the FB | Edge memory, latches, timer instances, counters |
Temp |
Scratch space, never retained | Loop counters, intermediate results |
11. Commissioning and Verification
- Compile all blocks. In the project tree, right-click PLC_1 and choose Compile > Software (rebuild all blocks). Any access error appears in the inspector.
- Download to the target. Use the online menu to download all blocks. Watch for the warning Block exists with different interface on subsequent downloads — this means a static tag's type changed and the Instance DB will be re-initialized.
-
Open the Instance DB in watch mode. With TIA Portal online, expand Program blocks > System blocks > Instance DB (FC_Tanks) and watch the four
statTankNstructures. The statStartEdge,statStopEdge,statRunState,instBlink.ET, andinstBlink.Qtags should be visible. -
Force a test input. Right-click
DB_TankIo.Tank1.iStartin the watch table and select Modify > Modify to 1. Verify thatstatTank1.statRunStategoes TRUE on the next scan, thatioIO.qPumpRungoes TRUE, and thatinstBlink.Qtoggles every 500 ms. -
Test the stop path. Force
iStopto 1 for one scan (use a Modify with the Once trigger). Verify thatstatRunStatereturns to FALSE and the lamp stops blinking within 1 s. -
Test the interlock. Force
iLevelHighto 1. The pump must stop regardless of the start button. Release the force and confirm normal operation resumes only after the stop-then-start sequence. -
Verify the multi-instance isolation. Force
Tank1.iStartwhile watchingTank2.statStartEdge. They must move independently; if Tank2's edge bit changes when Tank1's input changes, the FB has been called with a shared global instance instead of multi-instance — check the Multi-instance checkbox in the FB properties. - Cycle-time check. In Online & diagnostics > Cycle time, confirm the OB runs in well under its scheduled time. A 100 ms OB should consume less than 30 ms even with four tanks and the FC overhead.
12. Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Compile error: Instance required for instruction TP/TOF | Timer used inside a FC | Move the timer to an FB Static section |
| Output toggles correctly on FB1 but is stuck on FB2 | FC using N_TRIG/P_TRIG with shared global edge bit |
Replace with explicit statEdge variables in the FB |
| All four tanks run the same state | Multi-instance checkbox not set on the FB | FB properties > Attributes > Multi-instance = checked |
| HMI shows the wrong tank | Tag prefix wrong on the HMI connection | HMI connection > Partner point = DB_TankIo, not DB_Tank1
|
| Output stays TRUE after a stop | FC does not clear qPumpRun on entry; FB not reasserting FALSE |
Add an explicit reset of ioIO.qPumpRun := FALSE at the top of the FB |
| Watch table shows the UDT members but the FB cannot see them | Optimized block access mismatch; the caller passes a non-optimized DB to an optimized FB InOut | Match the Optimized block access attribute on both DBs |
| Download fails with Block exists with different interface | UDT structure changed; Instance DB needs re-init | Accept the prompt to reinitialize; document the change in the project log |
| 1 Hz blink runs at 0.5 Hz on a slow OB | OB cycle time is greater than 500 ms, so TP cannot complete |
Move the blink to a faster OB or use a hardware clock bit (e.g. Clock_1Hz from the CPU properties) |
13. Common Pitfalls
- Designing the UDT first, then fighting the I/O. A UDT shaped like a wishlist produces a wall of empty members and no working logic. Build the OB, get one tank working with global tags, replicate it, and only then refactor into a UDT.
- Creating a separate Instance DB per FB. This defeats the purpose of multi-instance FBs and bloats the block count from 1 to 1 + N. With four tanks, the FC-based multi-instance approach uses one DB; the naive approach uses five.
- Mixing optimized and non-optimized access. Optimized access is the default for S7-1500 and is required for some features (e.g. download without reinitialization, partial DB read from HMI). Mixing it with the legacy non-optimized access on the same data structure causes the compiler to issue warnings that turn into runtime errors under certain HMI scenarios.
- Hiding the wiring inside the UDT. The UDT should be a data shape, not a behavior. If you start writing methods or derived signals in the UDT, you are building a class, which is not the IEC 61131-3 model and not what the Siemens editor supports.
- Skipping the Multi-instance checkbox. Without it, the FB expects a dedicated Instance DB and the static call in the FC fails to compile or silently creates one DB per call.
-
Forgetting the Retain setting. A run-state latched inside the FB will be lost on a power cycle unless its
Statictag has Retain = true. The default in TIA Portal is Non-retain; set it explicitly for anything that must survive a warm restart.
14. Extending the Template
Once the 4-tank pattern works, the same scaffolding scales cleanly to other equipment classes:
-
Conveyor: Add a
UDT_ConveyorIowithiStart,iStop,iVfdFault,iAtSpeed,qRun,qLamp. The FB adds a stage machine: Idle → Starting → Running → Stopping → Fault. -
Valve: Add a
UDT_ValveIowithiOpenLS,iClosedLS,qOpen,qClose. The FB implements the Open / Hold / Close state machine with torque-trip and interlock handling. - VFD: Use a dedicated FB per VFD even if they are identical drives; the state machine, fault history, and parameter buffer are large enough to justify one block per unit.
Keep the UDTs, FBs, and FCs in a project library so that any new project can drag the master copies and re-instance them in seconds. Siemens' S7-1500 system manual documents the library workflow under the Master copies and type instances section.
Should I build the UDT first or the working logic first?
Build the working logic first. Get one tank running with global PLC tags and a single FB, replicate it once to confirm the pattern is right, and only then refactor the I/O into a UDT_TankIo and the FB inputs into a UDT_Tank. A UDT designed in advance tends to accumulate empty members and never gets used.
When do I use an FC versus an FB in TIA Portal?
Use an FC for stateless logic — scaling, routing, copy operations, formulas with no memory. Use an FB whenever the block must remember anything between scans: latches, edge bits, timers, counters, state machines, fault history. The F1 help in the editor lists which instructions require an instance; timers, counters, and edge flags all do, which forces them into an FB's Static section.
How do I generate a 1 Hz blink for a lamp without a clock memory bit?
Declare an IEC_TIMER or TP instance inside the FB's Static section, drive it with IN := bRun and PT := T#500MS, and use Q as the lamp output. The pulse toggles every 500 ms while bRun is TRUE, producing a clean 1 Hz blink that follows the run state per tank.
What is a multi-instance FB and why should I use one?
A multi-instance FB is an FB whose Static tag inside another block (typically an FC) is of the FB's own type. TIA Portal stores all instances inside a single Instance DB owned by the caller, reducing the block count from N+1 to 2. Enable it via FB properties > Attributes > Multi-instance; without that flag, the FB expects a dedicated Instance DB and the multi-instance call fails to compile.
How do I avoid the shared global edge bit warning when using N_TRIG or P_TRIG?
Do not call N_TRIG or P_TRIG from a stateless FC — the compiler falls back to a shared global edge memory bit, which causes cross-talk between blocks. Either move the edge detection into the FB's Static section (where the instruction gets its own instance), or use explicit edge memory: tEdge := iInput AND NOT statPrev; statPrev := iInput;.