Skip to main content

posts

PLC Architecture: Design Modules Around Contracts, Not Hardware

Design PLC modules around stable behavioral contracts so hardware details stay at the boundary and application logic becomes easier to test, simulate, and reuse.

The Problem

PLC programs often start close to the hardware. That is natural. Inputs are mapped. Outputs are mapped. Drives expose status words. Analog terminals return raw values. Fieldbus devices have vendor-specific diagnostics.

The problem starts when those details spread into the application logic.

A pressure controller should not need to know that the pressure signal comes from a 12-bit analog terminal. A winch controller should not need to know the exact bit layout of a drive status word. A machine sequence should not care whether a valve is connected through local I/O, EtherCAT, CANopen, or a simulation block.

When hardware details become part of the application architecture, every hardware change becomes a software change in too many places. Very efficient, if the goal is to make a terminal replacement feel like archaeology.

The better rule is simple:

Hardware details belong at the boundary. Application modules should depend on contracts.

Hardware Is a Boundary

Hardware-facing code has an important job. It translates physical, electrical, and vendor-specific details into something the rest of the program can use safely.

That boundary code may deal with:

  • raw analog ranges
  • scaling and engineering units
  • fieldbus status words
  • terminal diagnostics
  • vendor-specific fault codes
  • simulation values
  • stale data
  • hardware states such as OP and PRE_OP

Those details are real. They should not be ignored. But they should be contained.

Inside the application, the code should work with stable meanings:

  • pressure in bar
  • speed as a relative command
  • valve open or closed
  • drive operational or pre-operational
  • sensor value available only when the sensor is OP

The boundary converts from hardware representation to application representation. That is where raw types, exact terminal ranges, and protocol layouts belong.

Contracts Describe Behavior

A contract defines what a module provides, not how it is implemented.

In TwinCAT, a small interface is often enough. If the project uses operational states instead of diagnostic booleans, define that state explicitly.

{attribute 'qualified_only'}
{attribute 'strict'}
TYPE OperationState :
(
    PRE_OP,
    OP
) DINT;
END_TYPE
INTERFACE ISensor

PROPERTY Value : LREAL
PROPERTY OpState : OperationState

END_INTERFACE

This contract says that the object can provide a value and an operational state. A sensor in OperationState.OP has a valid value. A sensor in OperationState.PRE_OP does not. The contract does not say whether the value comes from an analog input, an EtherCAT terminal, a calculation, or a test object.

A valve contract can be just as small.

INTERFACE IValve

METHOD open
METHOD close
PROPERTY OpState : OperationState

END_INTERFACE

The controller using the valve does not need to know whether open() writes a digital output, sends a fieldbus command, or changes a simulated state. It only needs the agreed behavior.

That agreement is the architecture.

A Hardware-Coupled Design

Consider a pressure relief function. In a weak design, the controller directly knows the raw input range and the output mapping.

FUNCTION_BLOCK PressureRelief
VAR_INPUT
    pressureRaw : DINT;
END_VAR
VAR
    valveOutput AT %Q* : BOOL;
END_VAR
VAR CONSTANT
    PRESSURE_RAW_LIMIT : DINT := 27648;
END_VAR

IF pressureRaw > PRESSURE_RAW_LIMIT THEN
    valveOutput := TRUE;
ELSE
    valveOutput := FALSE;
END_IF

This is small, but the coupling is already visible.

The block depends on:

  • the raw input scale
  • the physical output mapping
  • the chosen terminal representation
  • the absence of an operational state
  • the assumption that TRUE means valve open

If the pressure signal changes to another terminal type, the controller changes. If the valve becomes a fieldbus valve, the controller changes. If simulation is needed, the controller changes. If diagnostics are added, the controller changes again, because apparently the controller volunteered to become the entire system.

A Contract-Based Design

The same behavior can be expressed through contracts.

FUNCTION_BLOCK PressureRelief
VAR
    sensor : ISensor;
    valve : IValve;
    initialized : BOOL;
END_VAR
VAR CONSTANT
    PRESSURE_LIMIT : LREAL := 120.0;
END_VAR

The dependencies are injected explicitly during initialization.

METHOD init : BOOL
VAR_INPUT
    sensorInput : ISensor;
    valveInput : IValve;
END_VAR

IF sensorInput = 0 OR valveInput = 0 THEN
    initialized := FALSE;
    init := FALSE;
    RETURN;
END_IF

sensor := sensorInput;
valve := valveInput;
initialized := TRUE;
init := TRUE;

The cyclic logic now depends on meaning instead of hardware.

METHOD update

IF NOT initialized THEN
    RETURN;
END_IF

IF sensor <> 0 AND_THEN valve <> 0 THEN
    IF valve.OpState = OperationState.OP THEN
        IF sensor.OpState = OperationState.OP
            AND_THEN sensor.Value > PRESSURE_LIMIT
        THEN
            valve.open();
        ELSE
            valve.close();
        END_IF
    END_IF
END_IF

This code says what the machine should do:

If the valve can be controlled, and the sensor is operational, open the relief valve when pressure is too high.

It does not say how the pressure is measured. It does not say how the valve is wired. Those decisions are outside this module.

That separation is the point.

Implementation Blocks Adapt Hardware

Contract-based design does not remove hardware code. It gives hardware code a proper place to live.

A real analog pressure sensor can implement ISensor.

FUNCTION_BLOCK AnalogPressureSensor IMPLEMENTS ISensor
VAR
    valueActual : LREAL;
    opStateActual : OperationState := OperationState.PRE_OP;
END_VAR
VAR CONSTANT
    RAW_MIN : LREAL := 0.0;
    RAW_MAX : LREAL := 32767.0;
    PRESSURE_MIN : LREAL := 0.0;
    PRESSURE_MAX : LREAL := 250.0;
END_VAR

Its update method handles the boundary conversion. The linear scaling is kept in a helper function so the sensor block shows intent instead of algebra.

METHOD update
VAR_INPUT
    valueRaw : DINT;
    opStateTerminal : OperationState;
END_VAR

opStateActual := opStateTerminal;

IF opStateActual = OperationState.OP THEN
    valueActual := transformLinear(
        valueInput := TO_LREAL(valueRaw),
        valueInputMin := RAW_MIN,
        valueInputMax := RAW_MAX,
        valueOutputMin := PRESSURE_MIN,
        valueOutputMax := PRESSURE_MAX,
        limitOutput := TRUE);
ELSE
    valueActual := 0.0;
END_IF

The properties expose the contract.

PROPERTY Value : LREAL

Getter:

Value := valueActual;
PROPERTY OpState : OperationState

Getter:

OpState := opStateActual;

A simulated sensor can implement the same contract without any I/O.

FUNCTION_BLOCK SimulatedPressureSensor IMPLEMENTS ISensor
VAR
    valueActual : LREAL;
    opStateActual : OperationState := OperationState.OP;
END_VAR

METHOD setValue
VAR_INPUT
    valueInput : LREAL;
END_VAR

valueActual := valueInput;

The pressure relief controller can use either implementation. It does not change.

Testing Becomes a Consequence

When application logic depends on contracts, testing becomes much easier.

The controller can be tested with fake objects:

  • a fake sensor that returns selected pressure values
  • a fake valve that stores whether open() or close() was called

No analog terminal is required. No real valve is required. No fieldbus device is required.

The test checks the machine rule:

Above the pressure limit, the valve shall open.

That is better than testing a raw number from one specific terminal. The raw number is a hardware detail. The pressure limit is the application behavior.

This does not make hardware tests unnecessary. It only means the business logic can be tested before the cabinet exists. A strange idea, apparently, but useful.

Contracts Need Semantics

An interface only defines syntax. It does not define engineering meaning.

This interface is not enough by itself:

INTERFACE ISensor

PROPERTY Value : LREAL
PROPERTY OpState : OperationState

END_INTERFACE

The team must also define the semantics:

Value:
    Engineering value in bar.
    Updated before the controller update method is called.
    Meaningful only when OpState is OP.

OpState:
    OP when the value is fresh, scaled, and based on healthy input data.
    PRE_OP when the signal is missing, stale, invalid, or not ready for use.

Without that agreement, two blocks can implement the same interface with different meanings. The compiler will accept it. The machine may not.

Good contracts need both parts:

  • a technical shape
  • an engineering convention

The interface gives the shape. Documentation, naming, tests, and reviews enforce the convention.

Where This Pattern Helps

Contract-based modules are useful when implementations may change while behavior stays stable.

Typical examples are:

  • analog sensors
  • digital actuators
  • proportional valves
  • drives
  • axes
  • winches
  • HMI commands
  • alarm services
  • logging services
  • communication adapters
  • simulation models

The pattern is also useful when a framework must support machine variants. One vessel may use one drive vendor. Another may use a different inverter setup. One project may use real I/O. Another may run in simulation. If the application depends on contracts, those variants can be adapted at the boundary.

Limitations

Contracts are useful, but they are not free.

Bad abstractions are worse than no abstractions. If an interface hides important device behavior, the application may lose information it actually needs. Diagnostics, limits, operational state, and timing behavior must remain visible.

Contracts also do not remove real-time concerns. Task cycle times, update order, stale data, timeouts, and fieldbus behavior still matter. A clean interface cannot fix an impossible timing requirement.

Functional safety is separate. A normal PLC interface can make control logic cleaner, but it does not replace a safety PLC, safety-rated components, risk assessment, or certified safety design.

Conclusion

Hardware details are unavoidable in PLC programming. They are not the problem. The problem is letting those details define the application architecture.

Keep raw values, terminal ranges, fieldbus words, and vendor-specific behavior at the system boundary. Translate them into stable contracts with clear engineering meaning.

Application modules should communicate through behavior:

  • this sensor provides an operational pressure value
  • this valve can open and close
  • this drive can accept a relative speed command
  • this service can publish a diagnostic message

That makes modules easier to test, easier to simulate, easier to replace, and easier to reuse.

The practical rule is simple:

Design application modules around contracts, not hardware.