Building Reusable UDT and FB Templates in TIA Portal

David Krause18 min read
SiemensTIA PortalTutorial / 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

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 STRUCT that 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.

Rule of thumb: If the logic could be drawn as a single combinational truth table, it is an FC. If it has a state, a latch, a timer that must remember its accumulated time, or an edge bit that must persist, it is an FB.

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
Instruction memory: Some instructions — most timers, counters, and edge flags — are only fully supported inside FBs. The F1 help in TIA Portal lists which instructions require an instance. Calling them from a FC will compile but produce a global edge bit that is shared across all FC callers, which is a notorious source of "phantom" triggers.

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.

  1. 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.
  2. 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.
  3. Add a UDT for the higher-level tank state. Create UDT_Tank containing everything that is not raw I/O: enable, mode, setpoints, fault word, runtime counters, HMI-visible values.
  4. Map the physical I/O to tag names. In PLC tags > Default tag table, add symbols such as Tank1_Start at %I0.0, Tank1_Stop at %I0.1, Tank1_LevelLow at %I0.2, Tank1_LevelHigh at %I0.3, Tank1_Pump at %Q0.0, and Tank1_Lamp at %Q0.1. Repeat for Tank2..Tank4 with their own offset ranges (or use slot-based addressing on the ET 200SP).
  5. Create the global instance DB for I/O. Add a new DB named DB_TankIo. Inside it, declare four variables of type UDT_TankIo: Tank1, Tank2, Tank3, Tank4.
  6. 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 of DB_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.
  7. Create the FB that owns the tank logic. Add an FB named FB_Tank. Its InOut interface takes the UDT_TankIo for that tank. Its Input interface carries enable, mode, and setpoints from a higher-level UDT_Tank. Section 7 lists the full SCL.
  8. Create the FC that calls four FB instances. Add an FC named FC_Tanks. Inside it, declare four statTank1..statTank4 static variables of type FB_Tank. These are multi-instance FBs — they share a single Instance DB rather than generating four separate DBs.
  9. Call FC_CopyInputs and FC_Tanks from a cyclic OB. In OB1 (or in a dedicated OB with a higher priority for fast I/O), call FC_CopyInputs first, then FC_Tanks. The order matters: the FB must see the I/O snapshot, not the live process image, if you intend to add filtering later.
  10. Add an HMI tag DB. The HMI connects to the same DB_TankIo and to the higher-level DB_Tank. Use the standard HMI faceplate pattern of one UDT per icon, so dragging a tank symbol onto a screen binds to DB_Tank in a single click.
  11. 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
Why explicit edge memory instead of 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
UDT size limit: A UDT can contain up to 65535 members, but a single DB instance is bounded by the CPU's work memory and by the maximum block length. A practical ceiling is around 1000 members per UDT; beyond that, the editor's responsiveness drops sharply.

11. Commissioning and Verification

  1. 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.
  2. 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.
  3. Open the Instance DB in watch mode. With TIA Portal online, expand Program blocks > System blocks > Instance DB (FC_Tanks) and watch the four statTankN structures. The statStartEdge, statStopEdge, statRunState, instBlink.ET, and instBlink.Q tags should be visible.
  4. Force a test input. Right-click DB_TankIo.Tank1.iStart in the watch table and select Modify > Modify to 1. Verify that statTank1.statRunState goes TRUE on the next scan, that ioIO.qPumpRun goes TRUE, and that instBlink.Q toggles every 500 ms.
  5. Test the stop path. Force iStop to 1 for one scan (use a Modify with the Once trigger). Verify that statRunState returns to FALSE and the lamp stops blinking within 1 s.
  6. Test the interlock. Force iLevelHigh to 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.
  7. Verify the multi-instance isolation. Force Tank1.iStart while watching Tank2.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.
  8. 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 Static tag 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_ConveyorIo with iStart, iStop, iVfdFault, iAtSpeed, qRun, qLamp. The FB adds a stage machine: Idle → Starting → Running → Stopping → Fault.
  • Valve: Add a UDT_ValveIo with iOpenLS, 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;.

Back to blog