The principles of clean code—established in software engineering decades ago—apply equally to PLC programming.
The diagram above illustrates how
multiple clean code principles converge to produce readable, maintainable code
that delivers long-term value. As SCL adoption grows and PLC projects
become more complex, the importance of writing maintainable, readable code
cannot be overstated. A PLC program that runs correctly today but is
incomprehensible to the next engineer who maintains it creates technical debt
and increases the risk of costly errors. This article explores best practices
for writing clean, professional SCL code that stands the test of time.
The Cost of Messy Code
Before diving into best practices, it is worth
understanding why clean code matters in industrial automation. A poorly written
PLC program might function correctly initially, but as systems evolve and
requirements change, maintenance becomes increasingly difficult. Engineers
spend more time deciphering existing code than writing new code. Bugs are
harder to identify and fix. Testing becomes unreliable. The cost of these
inefficiencies multiplies over the lifetime of a system, often exceeding the
initial development cost many times over.
In safety-critical applications, messy code introduces
additional risks. If an engineer cannot quickly understand how a system works,
they cannot confidently make changes or troubleshoot issues. This can lead to
safety incidents and regulatory compliance problems.
Naming Conventions: The Foundation of Clarity
Clear, descriptive names are the foundation of readable
code. Variable, function, and block names should immediately convey their
purpose. Avoid cryptic abbreviations or single-letter variables (except in
mathematical contexts where conventions are well-established).
Poor Example:
VAR
x, y, z : REAL;
t1, t2 : INT;
f : BOOL;
END_VAR
Good Example:
VAR
motor_speed_rpm : REAL;
motor_temperature_celsius : REAL;
pressure_bar : REAL;
timer_delay_seconds : INT;
timer_elapsed_seconds : INT;
motor_fault_detected : BOOL;
END_VAR
Adopt a consistent naming convention across your
organization. Common approaches include:
•
Snake_case: motor_speed_rpm,
temperature_sensor_input
•
camelCase: motorSpeedRpm,
temperatureSensorInput
•
Hungarian notation: fMotorFault,
iMotorSpeed (where prefix indicates type)
The specific convention matters less than consistency.
Choose one and apply it uniformly across all projects.
Comments: Explain the Why, Not the What
Comments should explain the reasoning behind code, not
simply restate what the code does. Code that is well-written and properly named
largely explains itself. Comments should address the "why"—the
business logic, design decisions, and non-obvious implications.
Poor Comment:
// Increment counter
counter := counter + 1;
Good Comment:
// Increment counter to track
number of bottles processed
// Reset occurs when daily
production target is reached
counter := counter + 1;
Use comments to document assumptions, constraints, and
potential pitfalls. Explain complex algorithms and non-obvious optimizations.
Comment edge cases and error conditions.
Function Design: Single Responsibility Principle
Each function should have a single, well-defined
responsibility. A function that does multiple things is harder to test, reuse,
and maintain. Apply the Single Responsibility Principle (SRP) from software
engineering to PLC code.
Poor Design:
FUNCTION_BLOCK ProcessData
// Reads sensors, validates data,
calculates statistics, and logs results
// Does too many things
END_FUNCTION_BLOCK
Good Design:
FUNCTION_BLOCK ReadSensors
// Responsibility: Read sensor inputs and
perform basic validation
END_FUNCTION_BLOCK
FUNCTION_BLOCK
CalculateStatistics
// Responsibility: Calculate statistical
metrics from validated data
END_FUNCTION_BLOCK
FUNCTION_BLOCK LogResults
// Responsibility: Format and log results
to persistent storage
END_FUNCTION_BLOCK
Smaller, focused functions are easier to test, understand,
and reuse. They also facilitate code reuse across different projects.
DRY Principle: Don't Repeat Yourself
Duplicated code is a maintenance nightmare. When a bug is
found in duplicated logic, it must be fixed in every location. When
requirements change, all copies must be updated. Use functions and function
blocks to eliminate duplication.
Poor (Duplicated Code):
// In Module A
IF sensor_value > threshold
THEN
alarm_triggered := TRUE;
log_event("High sensor value
detected");
send_notification("Alert: Sensor
threshold exceeded");
END_IF;
// In Module B
IF another_sensor >
threshold THEN
alarm_triggered := TRUE;
log_event("High sensor value
detected");
send_notification("Alert: Sensor
threshold exceeded");
END_IF;
Good (Reusable Function):
FUNCTION TriggerAlarm
VAR_INPUT
sensor_name : STRING;
sensor_value : REAL;
END_VAR
alarm_triggered := TRUE;
log_event(CONCAT("High ",
sensor_name, " value detected"));
send_notification(CONCAT("Alert:
", sensor_name, " threshold exceeded"));
END_FUNCTION
// Usage in both modules
TriggerAlarm("sensor_value",
sensor_value);
TriggerAlarm("another_sensor",
another_sensor);
Modularity and Encapsulation
Organize code into logical modules, each with a clear
interface. Use User Defined Types (UDTs) and function blocks to encapsulate
related data and operations. This promotes code reuse and makes systems easier
to understand.
Example: Encapsulated Motor
Controller
TYPE MotorController
speed_setpoint : REAL;
current_speed : REAL;
temperature : REAL;
fault_detected : BOOL;
FUNCTION_BLOCK MotorController
// Initialize motor controller
END_FUNCTION_BLOCK
FUNCTION Start
// Start motor with safety checks
END_FUNCTION
FUNCTION Stop
// Stop motor gracefully
END_FUNCTION
FUNCTION UpdateSpeed
// Update motor speed based on setpoint
END_FUNCTION
END_TYPE
This encapsulation makes it clear what operations are
available on a motor controller and ensures consistent behavior.
Error Handling and Defensive Programming
Write code that anticipates and handles errors gracefully.
Use return codes or exceptions to signal error conditions. Validate inputs
before processing them.
Example: Defensive Function
FUNCTION CalculateAverage
VAR_INPUT
values : ARRAY[1..100] OF REAL;
count : INT;
END_VAR
VAR_OUTPUT
average : REAL;
error : BOOL;
END_VAR
// Validate input
IF count <= 0 OR count >
100 THEN
error := TRUE;
average := 0.0;
RETURN;
END_IF;
// Calculate average
VAR sum : REAL := 0.0;
FOR i := 1 TO count DO
sum := sum + values[i];
END_FOR;
average := sum / count;
error := FALSE;
Code Organization and Structure
Organize your SCL projects with a clear directory
structure and naming convention. Group related function blocks, functions, and
data types together. Use meaningful section comments to delineate different
parts of the code.
Example Project Structure:
Project/
├── GlobalData/
│ ├── DataTypes.scl (UDTs and custom types)
│ ├── Constants.scl (Project-wide constants)
│ └── GlobalVariables.scl (Global variables)
├── MotorControl/
│ ├── MotorController.scl (Motor control
function block)
│ └── MotorFunctions.scl (Helper functions)
├── Sensors/
│ ├── SensorReader.scl (Sensor input handling)
│ └── SensorCalibration.scl (Calibration
functions)
└── Main/
└──
Main.scl (Main program logic)
Testing and Validation
Write code with testability in mind. Use function blocks
that can be instantiated and tested independently. Create unit tests for
critical functions. Document test cases and expected results.
Example: Testable Function
FUNCTION_BLOCK PIDController
// Designed to be tested independently
// Inputs and outputs are clearly defined
// No dependencies on external systems
END_FUNCTION_BLOCK
// Test case
VAR
pid : PIDController;
setpoint, process_value : REAL;
output : REAL;
END_VAR
// Test 1: Zero error should
produce zero output
setpoint := 100.0;
process_value := 100.0;
pid(setpoint := setpoint,
process_value := process_value);
output := pid.output;
// Assert output ≈ 0.0
Documentation
Maintain comprehensive documentation that explains the
overall architecture, key algorithms, and design decisions. Document
assumptions about hardware, communication protocols, and external systems. Keep
documentation synchronized with code changes.
Documentation Checklist:
•
System architecture and data flow
•
Function and function block descriptions
•
Variable definitions and their units
•
Error codes and their meanings
•
Safety considerations and interlocks
•
Known limitations and future improvements
•
Change history and version control information
Conclusion
Clean code is not a luxury in PLC programming—it is a
necessity. As automation systems become more complex and the cost of downtime
increases, the ability to quickly understand and modify code becomes critical.
By following these best practices—clear naming, focused functions,
comprehensive comments, modularity, error handling, and thorough
documentation—engineers create code that is reliable, maintainable, and
professional.
The investment in writing clean code pays dividends
throughout the system's lifecycle. Bugs are easier to find and fix. New team
members can understand the code quickly. Changes can be made confidently. In
the long run, clean code reduces costs, improves reliability, and enables
innovation. For any engineer serious about their craft, mastering these
practices is essential.
References
[1] Clean Code: A Handbook of Agile Software Craftsmanship - Robert C. Martin
[2] Code Complete: A Practical Handbook of Software Construction - Steve McConnell
[3] Siemens TIA Portal Programming Guidelines - https://support.industry.siemens.com/cs/document/109742519