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
OPandPRE_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_TYPEINTERFACE ISensor
PROPERTY Value : LREAL
PROPERTY OpState : OperationState
END_INTERFACEThis 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_INTERFACEThe 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_IFThis 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
TRUEmeans 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_VARThe 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_IFThis 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_VARIts 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_IFThe properties expose the contract.
PROPERTY Value : LREALGetter:
Value := valueActual;PROPERTY OpState : OperationStateGetter:
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()orclose()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_INTERFACEThe 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.
