May 20, 2026

Clean Code for PLCs: Best Practices for Writing Readable and Maintainable SCL

The principles of clean code—established in software engineering decades ago—apply equally to PLC programming.

 

Title: Clean Code Principles Framework - Description: Clean Code Principles Framework

 

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

No comments: