10 PLC Programming Best Practices for Industrial Automation

Write better PLC code with these 10 industry-proven best practices for Structured Text programming in industrial automation.

Why Best Practices Matter

In industrial automation, PLC code controls real machinery. Poor code quality doesn't just cause bugs — it can cause safety hazards, production downtime, and maintenance nightmares. These ten practices will make your Structured Text code safer, more readable, and easier to maintain.

1. Use Consistent Naming Conventions

Adopt a clear naming scheme and stick to it across every project:

// ✅ Good: Clear, descriptive, consistent
pump1_StartCmd     : BOOL;
pump1_RunFeedback  : BOOL;
pump1_FaultAlarm   : BOOL;
tank1_LevelPercent : REAL;

// ❌ Bad: Inconsistent, unclear p1Start : BOOL; PUMP_running : BOOL; x42 : BOOL; temp : REAL; // Temperature? Temporary?

Tip: Hungarian notation (prefixing with type) is common in PLC: bRunning (BOOL), iCount (INT), rTemperature (REAL).

2. Structure Code with Function Blocks

Break logic into reusable Function Blocks rather than writing monolithic programs:

// One FB per equipment type
pump1Controller : FB_PumpControl;
pump2Controller : FB_PumpControl;
mixerController : FB_MixerControl;

// Clean, readable main program pump1Controller(Cmd := hmi_pump1Start, FB := di_pump1Running); pump2Controller(Cmd := hmi_pump2Start, FB := di_pump2Running); mixerController(Cmd := autoMode, Speed := recipe_mixSpeed);

3. Always Handle Error States

Never assume inputs will be valid or equipment will behave correctly:

// ✅ Good: Explicit error handling
IF sensorValue < SENSOR_MIN OR sensorValue > SENSOR_MAX THEN
    sensorFault := TRUE;
    processValue := lastGoodValue;  // Use last known good value
ELSE
    sensorFault := FALSE;
    lastGoodValue := sensorValue;
    processValue := sensorValue;
END_IF;

4. Use Constants, Not Magic Numbers

// ❌ Bad: What do these numbers mean?
IF temperature > 85.0 THEN
    output := 4095;
END_IF;

// ✅ Good: Self-documenting VAR CONSTANT TEMP_HIGH_ALARM : REAL := 85.0; DAC_MAX_OUTPUT : INT := 4095; END_VAR

IF temperature > TEMP_HIGH_ALARM THEN output := DAC_MAX_OUTPUT; END_IF;

5. Implement State Machines Properly

Use CASE statements with defined states for sequential processes:

Declarations (VAR panel):

PROGRAM Main
VAR
    machineState : INT;
    startCommand : BOOL;
    motorRunning : BOOL;
    startMotor : BOOL;
    stopCommand : BOOL;
    faultReset : BOOL;
END_VAR

Logic (PROGRAM panel):

CASE machineState OF
    0: // IDLE
        IF startCommand THEN machineState := 10; END_IF;

10: // STARTING startMotor := TRUE; IF motorRunning THEN machineState := 20; END_IF;

20: // RUNNING // Production logic IF stopCommand THEN machineState := 30; END_IF;

30: // STOPPING startMotor := FALSE; IF NOT motorRunning THEN machineState := 0; END_IF;

99: // FAULT startMotor := FALSE; IF faultReset THEN machineState := 0; END_IF; END_CASE;

Tip: Use multiples of 10 for states so you can insert intermediate steps later.

6. Comment Your Intent, Not Your Code

// ❌ Bad: Restates the obvious
// Set motorRun to TRUE
motorRun := TRUE;

// ✅ Good: Explains WHY // Bypass start delay during manual jog mode for maintenance motorRun := TRUE;

💡 Syntax fragment. Declare motorRun : BOOL; in the VAR panel to run this.

7. Limit Scan-Time Impact

Avoid operations that spike scan time unpredictably:

Declarations (VAR panel):

PROGRAM Main
VAR
    i, chunkStart : INT;
    processData, rawData : ARRAY[1..10000] OF REAL;
    scaleFactor : REAL;
END_VAR

Logic (PROGRAM panel):

// ❌ Bad: Processing entire array every scan
FOR i := 1 TO 10000 DO
    processData[i] := rawData[i] * scaleFactor;
END_FOR;

// ✅ Good: Process a chunk per scan FOR i := chunkStart TO chunkStart + 99 DO IF i <= 10000 THEN processData[i] := rawData[i] * scaleFactor; END_IF; END_FOR; chunkStart := chunkStart + 100; IF chunkStart > 10000 THEN chunkStart := 1; END_IF;

8. Separate HMI Interface from Logic

Keep HMI-facing tags in a thin "interface" layer; let your logic FB own the real state.

Declarations (VAR panel):

PROGRAM Main
VAR
    // HMI-facing tags (Modbus/OPC UA mapped)
    hmi_tankLevel    : REAL;
    hmi_pumpStatus   : BOOL;
    hmi_alarmActive  : BOOL;
    hmi_startButton  : BOOL;
    hmi_stopButton   : BOOL;

// Logic owns the real state — ProcessController is your own FB processController : ProcessController; END_VAR

Logic (PROGRAM panel):

// ✅ Good: Clear boundary between HMI and logic
// HMI interface variables (read by HMI)
hmi_tankLevel    := processController.CurrentLevel;
hmi_pumpStatus   := processController.PumpRunning;
hmi_alarmActive  := processController.HasFault;

// HMI commands (written by HMI) processController.StartCmd := hmi_startButton; processController.StopCmd := hmi_stopButton;

9. Use Version Control and Backups

  • Export your project after every significant change
  • Use meaningful names: Project_v2.3_AddedPumpFaultLogic
  • Keep a change log documenting what changed and why
  • Test on a simulator before downloading to live PLCs
  • 10. Test Edge Cases

    Before commissioning, verify your code handles:

  • Power-up state: Does everything initialize safely?
  • Sensor failures: What happens if an input goes to zero or max?
  • Simultaneous commands: Start AND stop pressed together?
  • Communication loss: HMI disconnects mid-operation?
  • Sequence interruptions: Stop pressed mid-cycle?
  • Practice These Principles

    Open our PLC simulator and practice writing clean, well-structured Structured Text code. Our project templates demonstrate many of these patterns in real-world scenarios.