Advisory Schedule a Technical Discovery Call — Book your session today! »

· Eduardo Vieira · Industrial Programming  · 6 min read

PLC Clean Code: Practical IEC 61131-3 Patterns

A practical guide to readable PLC software: scan semantics, state machines, function blocks, timers, diagnostics, testing, and controlled releases.

A practical guide to readable PLC software: scan semantics, state machines, function blocks, timers, diagnostics, testing, and controlled releases.

Clean PLC code makes control behavior easier to inspect, change, and recover. It does not make an application compliant, certified, safe, or portable by itself. IEC 61131-3 specifies programming-language syntax and semantics; engineering teams still have to define architecture, review rules, tests, deployment controls, and the safety lifecycle for their equipment.

This guide uses Structured Text (ST) examples, but the decisions apply across IEC 61131-3 languages. Exact task scheduling, timer resolution, initialization, retention, and online-change behavior depend on the controller, runtime, project settings, and vendor version. Verify them in the documentation and on representative hardware.

Start with cyclic scan semantics

A common PLC task reads inputs, executes logic, and updates outputs repeatedly. Code observes a sampled value during one invocation, not every physical transition between scans. Task period, priority, jitter, I/O update timing, and communication tasks therefore belong in the design record.

Make each scan deterministic: derive commands from the current inputs and retained state, invoke each stateful block once in a predictable place, and assign important outputs on every relevant path. Hidden write order is dangerous. If two program organization units write the same output, the later execution may mask the earlier decision.

Document assumptions such as “called every 20 ms” beside configuration, not as an unexplained magic constant. Measure worst-case execution time and jitter under realistic load when timing matters; source layout alone cannot prove either.

Express sequences as explicit state machines

Use a named enumeration and CASE when equipment has mutually exclusive operating phases. Define entry conditions, exit conditions, timeout behavior, reset policy, and a fallback for an invalid state. Set safe command defaults before the CASE, then enable only what the active state requires.

TYPE E_ConveyorState : (Idle, Starting, Running, Fault); END_TYPE

xMotorCommand := FALSE;

CASE eState OF
    E_ConveyorState.Idle:
        IF rtStart.Q THEN eState := E_ConveyorState.Starting; END_IF
    E_ConveyorState.Starting:
        tonStart(IN := TRUE, PT := T#3s);
        IF xAtSpeed THEN
            eState := E_ConveyorState.Running;
        ELSIF tonStart.Q THEN
            eState := E_ConveyorState.Fault;
        END_IF
    E_ConveyorState.Running:
        xMotorCommand := TRUE;
        IF NOT xPermit THEN eState := E_ConveyorState.Fault; END_IF
    E_ConveyorState.Fault:
        IF xReset AND xPermit THEN eState := E_ConveyorState.Idle; END_IF
ELSE
    eState := E_ConveyorState.Fault;
END_CASE

This is an architectural example, not production safety logic. A real machine needs defined stopping behavior, actuator feedback, interlocks, fault latching, and hazard analysis.

Choose function and function-block boundaries deliberately

Use a function for a calculation whose result depends only on its arguments and that needs no retained execution state. Use a function block when behavior owns state across scans, such as debounce, sequencing, or equipment status. Keep hardware mapping and orchestration outside reusable equipment blocks so logic can be exercised with ordinary values.

An FB instance is state. Do not accidentally share one timer, trigger, or equipment instance between unrelated devices. Keep interfaces small and directional: commands and observations in; status, diagnostics, and requested outputs out. Avoid global variables where an explicit input or output communicates ownership.

Make edges and time visible

A held pushbutton is not a one-shot command. Use an edge detector such as R_TRIG when an action must occur once on a false-to-true transition, and call that instance every intended scan. The CODESYS Standard library documents R_TRIG as a stateful FB whose Q reports a rising edge.

Timers also retain state. The documented CODESYS TON starts on a rising IN, resets on falling IN, and raises Q after PT elapses while IN remains true. Do not infer identical resolution, overflow, persistence, or scheduling behavior on every runtime. Give timers purpose names such as tonStartTimeout, and keep preset units explicit.

Let types and names carry intent

Prefer enumerations for modes, structures for related values, and units in names when the type system cannot express them: rPressure_bar, udiPulseCount, tStartTimeout. Distinguish raw I/O, engineering values, commands, feedback, and status. A team convention matters more than decorative prefixes; enforce one documented vocabulary consistently.

Convert at clear boundaries. Check range and precision before narrowing numeric types. Define startup and retained-data behavior explicitly, especially after download, power loss, or a structure change. Never let an uninitialized or stale retained value silently become an actuator command.

Design diagnostics as part of behavior

A single xFault bit is rarely enough. Expose state, active fault identifier, first-fault context, relevant timer elapsed value, command-versus-feedback disagreement, and reset eligibility. Timestamping may belong in a supervisory layer if the PLC clock is unsuitable, but event identity should originate near the decision.

Keep operator messages actionable and stable. Separate present condition, latched history, and acknowledgement. Diagnostics should explain why a transition did not happen without requiring an online code trace.

Create test seams around scan logic

Separate I/O mapping from decisions, inject observations and elapsed time where the platform permits, and capture requested outputs instead of writing hardware directly. Then exercise sequences scan by scan: normal start, held start, timeout boundary, permit loss, reset denial, invalid state, restart, and retained-data migration.

The repository verifier contains a deterministic Node.js standard-library fixture that mirrors the example’s scan, rising-edge, on-delay, and state transitions. It is simulation only: it checks article logic and testability, not PLC timing, vendor equivalence, hardware behavior, IEC compliance, certification, or safety validation.

Treat online change as a controlled release

Before deployment, archive source, resolved library versions, compiler/runtime versions, device configuration, checksums where available, and a tested restore package. Review data-layout and initialization changes, retained values, instance changes, I/O mapping, and whether a full download or restart is required.

“Online change” does not mean zero downtime or zero risk. Vendor and change type determine what can be transferred and what execution effects occur. Define approval, production window, backup, acceptance checks, rollback trigger, responsible person, and a proven rollback procedure. Test the exact path on representative equipment before using it in production.

Keep standard control and safety assurance separate

Readable standard PLC code can support review, but it does not replace risk assessment, safety requirements, a suitable safety controller, validated safety functions, independence rules, proof testing, or change control. Do not treat a standard task’s software interlock as the sole risk-reduction measure for a hazardous motion. Safety-related work belongs to qualified people following the applicable standards, vendor safety manuals, and site lifecycle.

Primary sources

Accessed 2026-07-24:

Use the edition and vendor documentation that match the installed engineering tool and target runtime.

Back to Blog

Related Posts

View All Posts »