engineering

PLC Programming: 7 Best Practices for Clean, Maintainable Code

Field-tested guide from an automation engineer: naming conventions, modular structuring, alarm handling and version control for PLC projects with Siemens TIA Portal. Includes SCL code examples.

David Prybisch
15 min read
PLC Programming: 7 Best Practices for Clean, 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

LanguageUse CaseAdvantagesDisadvantages
SCL (Structured Control Language)Complex algorithms, calculationsClear, readable, type-safeNot for hard real-time
LAD (Ladder Diagram)Simple logic, electrician-friendlyIntuitive for electriciansUnclear with > 50 rungs
FBD (Function Block Diagram)Control, data flow logicGraphically understandableHard to maintain with large programs
ST (Structured Text)Like SCL, IEC 61131-3 standardCompact, fastSteep learning curve
SFC (Sequential Function Chart)Sequential control (GRAFCET)State machines visualizedOverhead for simple logic

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.

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.

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

11.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

12.Infographic: PLC Best Practices at a Glance

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

13.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.

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