engineering

PLC Programming Best Practices for Maintainable Code

Write clean, maintainable PLC code in Siemens TIA Portal: proven naming conventions, FB/FC structure, NAMUR alarm handling, Git and faster troubleshooting.

David Prybisch
15 min read
PLC Programming Best Practices for Maintainable Code

1.Introduction

PLC programming is more than just stringing logic blocks together. In over 10 years as an automation engineer, I've seen countless PLC projects – many with typical problems:

  • Unreadable variable names like M0.0, DB1.DBX0.0, or %MW100
  • Inconsistent programming language mix – LAD, FBD, STL, and SCL wildly mixed in the same project
  • Missing documentation – the original programmer left the company long ago

In this article, I share the most important best practices for maintainable PLC programs with Siemens TIA Portal.

2.Why Best Practices in PLC Programming?

2.1.The Problem: Legacy Code

Typical scenario from my practice:

A machine builder calls: "Our plant is down. Can you help?"

On-site disillusionment:

  • Variable names like M0.0, DB1.DBX0.0, %MW100 – nobody knows what they mean
  • LAD, FBD, STL, and SCL wildly mixed in the same project
  • No comments, no structure
  • Original programmer left the company years ago

Troubleshooting takes unnecessarily long because nobody can navigate the code.

2.2.The Solution: Professional Programming

Structured, maintainable PLC programs bring clear advantages:

  • Faster troubleshooting through descriptive variable names
  • Shorter onboarding time for new programmers
  • Fewer machine downtimes through better error handling

3.Best Practice 1: Consistently Use IEC 61131-3 Standards

IEC 61131-3 is the international standard for PLC programming and mandatory for CE-certified machines.

3.1.The 5 Programming Languages Overview

Standard (IEC)SiemensUse caseLimits
LD – Ladder DiagramKOPInterlocks, simple logicUnwieldy beyond ~50 networks
FBD – Function Block DiagramFUPControl, data flow logicHard to maintain in large programs
IL – Instruction ListAWLLegacy code onlyDeprecated since 2013
ST – Structured TextSCLAlgorithms, calculations, librariesLess illustrative for pure interlocks
SFC – Sequential Function ChartGRAPHSequential control (GRAFCET)Overhead for simple logic

Common misconception: SCL and ST are the same language — SCL is Siemens' implementation of Structured Text. Listing both separately counts one language twice and usually drops IL in exchange.

In depth: Which language suits which task, how the standard's names map onto the Siemens names, and why SCL and ST are the same language — see the dedicated article The 5 PLC Programming Languages of IEC 61131-3.

3.2.Structured Programming: The Pyramid

PLC program structure: Hierarchical block architecture from OB1 through FC_SystemControl to function blocks and data blocks

OB1 (Main)
  └── FC_SystemControl (Operating modes)
       ├── FB_PackMLStateMachine (Sequential control)
       │    ├── FB_MotorControl (Component)
       │    ├── FB_ValveControl (Component)
       │    └── FB_ConveyorControl (Component)
       └── FC_AlarmHandler (Utility)

Rule: Each level has exactly ONE clear responsibility.

4.Best Practice 2: Establish Naming Conventions

4.1.The Problem: Variable Name Chaos

Negative example (from practice):

// ❌ BAD
M0.0    // What is this?
DB1.DBX0.0   // Which motor?
%MW100  // Temperature? Pressure?

Positive example:

// ✅ GOOD
bMotorPumpMainRun : BOOL;  // Main pump motor running
iTemperatureTank1 : INT;   // Tank 1 temperature in °C
rPressureSetpoint : REAL;  // Pressure setpoint in bar

4.2.Standardized Prefixes (Hungarian Notation)

PrefixData TypeExample
bBOOLbMotorRun, bAlarmActive
iINTiCounter, iTemperature
rREALrSpeed, rPressure
sSTRINGsAlarmText, sRecipeName
dtDATE_TIMEdtStartTime, dtLastMaintenance
tTIMEtDelayStart, tCycleTime
udiUDINTudiPartCounter, udiTotalProduction

4.3.Inputs/Outputs with Prefix

// Inputs: ix (Input X)
ixMotorFeedbackRun : BOOL AT %I0.0;  // "x" = Bool
iwTemperatureSensor : WORD AT %IW0;   // "w" = Word

// Outputs: qx (Output X)
qxMotorStart : BOOL AT %Q0.0;
qwValvePosition : WORD AT %QW0;

Consistency is king: Decide on ONE convention with your team and document it.

In depth: Why a prefix table alone is not yet a standard — and what interface contracts, a versioned library and a defined exception path have to do with it — see the dedicated article PLC Coding Standards: From Naming Conventions to a Reusable Library.

5.Best Practice 3: Use Function Blocks vs. Functions Correctly

5.1.Function Blocks (FB) - Stateful

When to use?

  • Components with state (motor, valve, conveyor)
  • Alarm handlers
  • State machines

Example: Motor Control

FUNCTION_BLOCK FB_MotorControl
VAR_INPUT
  bStart : BOOL;        // Start command
  bStop : BOOL;         // Stop command
  bFeedback : BOOL;     // Motor running feedback
END_VAR

VAR_OUTPUT
  qxStart : BOOL;       // Motor start output
  bRunning : BOOL;      // Status: Motor running
  bAlarm : BOOL;        // Fault active
END_VAR

VAR
  tDelayStart : TON;    // Start delay
  tTimeoutFeedback : TON; // Feedback timeout
  bAlarmLatched : BOOL; // Latched fault
END_VAR

// Program logic here...
END_FUNCTION_BLOCK

Call:

// In OB1 or FC
dbMotorPumpMain(
  bStart := bSystemRun AND NOT bEmergencyStop,
  bStop := bSystemStop OR bEmergencyStop,
  bFeedback := ixMotorPumpMainFeedback
);
qxMotorPumpMainStart := dbMotorPumpMain.qxStart;

5.2.Functions (FC) - Stateless

When to use?

  • Calculations without state
  • Scaling
  • Mathematical operations
  • Utility functions

Example: Temperature Scaling

FUNCTION FC_ScaleTemperature : REAL
VAR_INPUT
  iwRawValue : INT;  // Raw sensor value (0-27648)
  rMinTemp : REAL;   // Minimum temperature (e.g., -50°C)
  rMaxTemp : REAL;   // Maximum temperature (e.g., +150°C)
END_VAR

// Scaling 0-27648 → rMinTemp to rMaxTemp
FC_ScaleTemperature := rMinTemp +
  (REAL#iwRawValue / 27648.0) * (rMaxTemp - rMinTemp);
END_FUNCTION

Call:

rTemperatureTank1 := FC_ScaleTemperature(
  iwRawValue := iwTempSensor1,
  rMinTemp := -50.0,
  rMaxTemp := 150.0
);

6.Best Practice 4: Error Handling According to NAMUR NE 107

NAMUR NE 107 is the industry standard for self-monitoring and diagnostics in process automation. In mechanical engineering, it has also become established.

6.1.The 4 States

NAMUR NE 107 diagnostic states: Normal Operation, Maintenance Required, Function Check, Failure

6.2.Implementation with FB_AlarmHandler

FUNCTION_BLOCK FB_AlarmHandler
VAR_INPUT
  bTrigger : BOOL;          // Alarm trigger
  eAlarmLevel : INT;        // 1=Info, 2=Warning, 3=Error, 4=Critical
  sAlarmText : STRING[80];  // Alarm text
END_VAR

VAR_OUTPUT
  bAlarmActive : BOOL;      // Alarm is active
  bAckRequired : BOOL;      // Acknowledgment required
END_VAR

VAR
  bAlarmLatched : BOOL;     // Latched alarm
  dtTimestamp : DATE_TIME;  // Alarm timestamp
END_VAR

// Logic: Latch alarm, set timestamp, acknowledge if needed
IF bTrigger AND NOT bAlarmLatched THEN
  bAlarmLatched := TRUE;
  dtTimestamp := CURRENT_TIMESTAMP();
  bAlarmActive := TRUE;

  // Logging to HMI/SCADA
  // ...
END_IF;

bAckRequired := bAlarmLatched AND eAlarmLevel >= 3;
END_FUNCTION_BLOCK

Best Practice: A central alarm manager collects all alarms and communicates them bundled to the HMI.

7.Best Practice 5: Version Control with Git

Yes, Git works for PLC code too! Since TIA Portal v18, integration has become significantly easier.

7.1.Setup: TIA Portal with Git

1. Activate Multiuser Engineering

TIA Portal → Project → Properties → Multiuser Engineering → Activate

2. Save project in Git-friendly format

# .gitignore for TIA Portal
*.ap18
*.zap18
__OPNB*
*.bak

3. Initialize Git repository

git init
git add .
git commit -m "Initial commit: TIA Portal project setup"

7.2.Branching Strategy for PLC Projects

main (Production code)
  ├── develop (Development)
  │    ├── feature/motor-control-update
  │    ├── feature/add-safety-logic
  │    └── bugfix/alarm-text-typo
  └── hotfix/critical-emergency-stop-bug

Workflow:

  1. Create feature branch: git checkout -b feature/new-conveyor-logic
  2. Commit changes: git commit -m "Add conveyor start delay 2s"
  3. Create pull request (GitHub, GitLab)
  4. Code review by second programmer
  5. Merge into develop
  6. Testing on development PLC
  7. Release branch → main

8.Best Practice 6: Comments and Documentation

8.1.Code Comments: The 3-Line Rule

Bad:

// ❌ Obvious, adds nothing
bMotorRun := TRUE;  // Start motor

Good:

// ✅ Explains WHY, not WHAT
// 2s delay due to pressure build-up in hydraulic circuit
// (See requirement REQ-HYD-012)
tDelayStart(IN := bStartRequest, PT := T#2S);
qxMotorStart := tDelayStart.Q;

8.2.Documentation Standards

For each FB/FC:

(*
  Name: FB_MotorControl
  Version: 1.2.0
  Author: David Prybisch
  Date: 2025-11-22

  Description:
    Standardized motor control with start delay,
    feedback monitoring and fault latching.

  Change History:
    v1.2.0 - 2025-11-22 - Added timeout monitoring
    v1.1.0 - 2025-10-15 - NAMUR alarm integration
    v1.0.0 - 2025-09-01 - Initial version
*)
FUNCTION_BLOCK FB_MotorControl
// ...
END_FUNCTION_BLOCK

UML Diagrams for System Architecture:

Use tools like PlantUML or Microsoft Visio to visualize overall architecture.

In depth: What belongs in a complete plant documentation, the 12 points that matter at handover, and how to reconstruct lost as-built documentation — see the dedicated article PLC Documentation: What Belongs In It and What Matters at Handover.

9.Best Practice 7: Testing and Simulation

9.1.Simulation with PLCSIM

Siemens PLCSIM Advanced allows testing PLC programs without physical hardware:

Workflow:

  1. Develop PLC program in TIA Portal
  2. Start PLCSIM Advanced and load project
  3. Set and check variables via watch table
  4. Systematically test sequential controls

Advantages:

  • Detect errors early – in the office instead of at the customer's site
  • New programmers can test safely
  • Validate changes before commissioning

10.Best Practice 8: Performance Optimization

10.1.Keep CPU Utilization in Check

Common performance killers:

  1. Too many timers/counters → Use arrays instead of individual variables
  2. String operations in real-time cycles → Move to lower-priority tasks
  3. Complex calculations in OB1 → Use cyclic interrupts (OB35)

Example: Array-based Timer Management

// ❌ BAD: 100 individual timers
VAR
  tMotor1 : TON;
  tMotor2 : TON;
  // ... 98 more timers
END_VAR

// ✅ GOOD: Array of timers
VAR
  atMotorTimers : ARRAY[1..100] OF TON;
END_VAR

// Call in loop
FOR i := 1 TO 100 DO
  atMotorTimers[i](IN := abMotorStart[i], PT := T#2S);
  aqxMotorStart[i] := atMotorTimers[i].Q;
END_FOR;

10.2.Memory Optimization with Multi-Instances

Problem: Each FB instance consumes its own DB (instance DB).

Solution: Multi-instances for recurring blocks.

// FB_ConveyorLine contains 10x FB_MotorControl as multi-instance
FUNCTION_BLOCK FB_ConveyorLine
VAR
  Motor1 : FB_MotorControl;  // Multi-instance
  Motor2 : FB_MotorControl;
  // ...
END_VAR

// Create only ONE instance of FB_ConveyorLine
dbConveyorLine : FB_ConveyorLine;  // Saves 90% memory vs. 10 separate FBs

11.PLC Troubleshooting: Systematic Instead of Trial-and-Error

Every best practice so far pays off at the latest when a plant is down and the fault has to be found under time pressure. Whoever programs in a structured way doesn't hunt for faults – they narrow them down. Trial-and-error, by contrast, costs hours and risks consequential damage.

11.1.Narrow Down Systematically Instead of Guessing

Every efficient troubleshooting session follows the same chain: symptom → affected block → signal path → root cause. Start with the concrete symptom ("conveyor won't start"), locate the responsible block (FB_ConveyorControl), and trace the signal backwards from the output to its conditions. This is exactly where a clear block structure pays off: if every plant component is encapsulated in its own FB with a descriptive name, you know immediately where to look – instead of digging through thousands of networks in OB1.

11.2.The Right TIA Portal Tools

  • Watch tables show live values without disturbing the process – the standard tool for reading along with signals.
  • Cross-references reveal where a variable is written and read – the first weapon against double assignments.
  • The call structure shows whether a block is even being called cyclically.
  • The CPU diagnostic buffer logs hardware events, module failures, and STOP causes with timestamps – the first place to look on any fault.
  • An online/offline comparison exposes undocumented changes "from the last night shift".

11.3.Know the Typical Fault Classes

  • Double assignments: the same variable is written in two blocks – the one processed last "wins". Instantly visible via cross-references.
  • Edge-detection faults: an action fires continuously instead of once because a rising edge (R_TRIG) is missing or an edge memory bit is reused.
  • Timing and cycle-time issues: a timer whose condition is reset within the same cycle never elapses; excessive cycle times trigger the time-error OB (OB80).
  • Hardware vs. software: isolate this before searching the code. If the diagnostic buffer shows a module failure or a peripheral LED signals a fault, the cause isn't in the program.

11.4.Clean Code Speeds Up Every Diagnosis

The FC/FB separation, descriptive names, and clear hierarchy from the previous chapters aren't ends in themselves: they turn hours of guesswork into a targeted narrowing-down of minutes. A commented block with a single responsibility tells you at a glance what it's supposed to do – and therefore where it deviates from the target.

In depth: The full method — rule out hardware first, the five most common fault classes, and the sequence that holds up under pressure — see the dedicated article PLC Troubleshooting: Narrow Down Systematically.

12.Conclusion: Professional PLC Programming Pays Off

The investment in structured, maintainable PLC programs pays off multiple times:

  • Faster troubleshooting through descriptive variable names and clear structure
  • Shorter onboarding time for new programmers
  • Fewer machine downtimes through better error handling
  • Easier maintenance over the entire system lifetime

12.1.Checklist for Your Next PLC Project

  • Naming conventions agreed and documented with team
  • Hierarchical program structure planned (UML diagram)
  • IEC 61131-3 standards followed
  • Alarm management according to NAMUR NE 107 implemented
  • Git repository set up and .gitignore configured
  • Simulation with PLCSIM prepared
  • Code review process established
  • Documentation (FB header, system architecture) created

13.Infographic: PLC Best Practices at a Glance

PLC Programming Best Practices Infographic: Checklist for structured, maintainable PLC programs

14.Further Resources


About the Author: David Prybisch has been working as a PLC programmer for over 10 years. He supports machine builders with structured programming using Siemens TIA Portal, code reviews, and commissioning – in Luxembourg, Saarland and the Trier region, as well as for clients in Switzerland.

Tags

SPS-ProgrammierungTIA PortalIEC 61131-3SiemensBest PracticesCode QualityNAMUR NE 107SCLAutomatisierung

Questions about your automation project?

As an automation engineer based in Stadtbredimus, Luxembourg, I offer free initial consultations for companies in the Greater Region Saar-Lor-Lux.

David Prybisch · PLC · HMI · Commissioning

Related Articles

Frequently Asked Questions

Sollte ich Funktionsbausteine (FB) oder Funktionen (FC) verwenden?

Verwenden Sie Funktionsbausteine (FB) für zustandsbehaftete Logik (z.B. Motorsteuerung mit Störungsspeicher, Ventilsteuerung mit Feedback) und Funktionen (FC) für zustandslose Berechnungen (z.B. Skalierung, mathematische Operationen). FBs haben einen Instanz-Datenbaustein und behalten Werte zwischen Aufrufen – ideal für Anlagenteile. FCs sind Speicher-effizienter für wiederkehrende Berechnungen ohne Zustand.

Wie strukturiere ich große SPS-Programme am besten?

Nutzen Sie eine hierarchische Struktur: (1) Hauptprogramm (OB1) ruft nur Ablaufsteuerungen auf, (2) Ablaufsteuerungen (z.B. PackML-Zustände) koordinieren Anlagenteile, (3) Anlagenteile (Motoren, Ventile) sind als FB mit Standardinterface implementiert, (4) Utility-Funktionen (Skalierung, Alarme) in FC ausgelagert. Verwenden Sie Multiinstanzen, um Speicher zu sparen. Dokumentieren Sie die Struktur in einem UML-Diagramm.

Was sind die wichtigsten Namenskonventionen für SPS-Code?

Etablierte Best Practices: (1) Präfixe für Datentypen (bMotorRun für Bool, iTemperature für Int), (2) Pascal Case für Bausteine (FB_MotorControl), (3) Prefix für E/A (ixInputName, qxOutputName), (4) Sprechende Namen statt Abkürzungen (bMotorPumpMainRun statt bMPMR), (5) Konsistente Sprache (entweder DE oder EN, niemals gemischt). Tools wie Siemens Code Quality Check helfen bei der Einhaltung.

Wie implementiere ich professionelle Fehlerbehandlung in SPS-Code?

Best Practice ist ein zentraler Alarm-Baustein nach NAMUR NE 107 mit 4 Ebenen: (1) Fehler (Error) - Anlage stoppt, (2) Warnung (Warning) - Anlage läuft, Wartung erforderlich, (3) Info (Message) - Statusmeldungen, (4) Out of Service - Bauteil deaktiviert. Nutzen Sie die Siemens ProDiag-Bibliothek oder erstellen Sie einen FB_AlarmHandler, der Störungen mit Zeitstempel im HMI anzeigt und per OPC UA an übergeordnete Systeme meldet.

Kann ich Git für SPS-Projekte verwenden?

Ja, und Sie sollten! TIA Portal v18+ unterstützt Git-Integration über Multiuser Engineering. Speichern Sie TIA-Projekte im Openness-Format (XML-basiert), das Git-freundlich ist. Verwenden Sie .gitignore für Binary-Dateien. Commit-Messages sollten Änderungen klar beschreiben (z.B. 'Add motor start delay in FB_MotorControl'). Branching-Strategie: Feature-Branches für neue Funktionalität, Hotfix-Branches für kritische Fehler. Tools wie PlcGit erleichtern Diff-Visualisierung.

Was ist der Unterschied zwischen SCL und AWL in der SPS-Programmierung?

SCL (Structured Control Language) ist eine höhere Programmiersprache ähnlich Pascal/C, ideal für komplexe Berechnungen, Schleifen und Datenverarbeitung. AWL (Anweisungsliste) ist eine Low-Level-Sprache ähnlich Assembler, die direkten Registerzugriff ermöglicht. Empfehlung: Verwenden Sie SCL für 80% des Codes (bessere Lesbarkeit, einfachere Wartung) und AWL nur für zeitkritische Optimierungen oder Legacy-Systeme. TIA Portal bietet beide Sprachen, moderne Projekte setzen fast ausschließlich auf SCL.

Wie lange dauert es, SPS-Programmierung zu lernen?

Mit Grundkenntnissen in Programmierung: 3-6 Monate für solide Basics, 1-2 Jahre für professionelles Niveau. Lernpfad: (1) IEC 61131-3 Grundlagen (2 Wochen), (2) TIA Portal Bedienung (1 Monat), (3) Erstes Projekt mit Mentor (2-3 Monate), (4) Vertiefung Safety, HMI, Netzwerke (6+ Monate). Wichtig: Theorie allein reicht nicht – praktische Erfahrung an echten Anlagen ist unverzichtbar. Siemens SCE bietet kostenlose Schulungsunterlagen.

Welche SPS-Programmiersprache sollte ich als Anfänger lernen?

Starten Sie mit LAD (Kontaktplan) für einfache Logik – es ist visuell und intuitiv für Elektriker. Parallel dazu SCL (Structured Control Language) lernen für komplexere Aufgaben. SCL ist die Zukunft: kompakter, besser lesbar, und näher an modernen Programmiersprachen. FBD (Funktionsbausteindiagramm) ist gut für Regelungstechnik. SFC (Ablaufsprache) für Sequenzen. Tipp: Beherrschen Sie SCL + LAD, dann sind Sie für 95% aller Projekte gewappnet.