The Problem
Many PLC projects initialize by accident.
A function block sets a first-cycle flag. Another one does some work in FB_init. A
service registers itself when it happens to run. A hardware wrapper assumes that a
configuration block has already copied its parameters. The machine starts because the
current call order happens to make it start.
That is not architecture. That is luck with a scan cycle.
The problem usually appears later:
- after an online change
- during simulation
- when a library is reused in another project
- when a service is called before it is registered
- when hardware needs more time to become available
- when commissioning exposes a startup race that never happened on the developer’s desk
Startup should be explicit. A PLC runs cyclically, but a project still has a lifecycle.
Cyclic Code Is Not Startup Code
Normal cyclic code should update the machine state. It should read inputs, update controllers, handle commands, write outputs, and expose diagnostics.
Startup code has a different job. It should prepare the system so cyclic code can run safely.
That usually means:
- loading or validating configuration
- initializing infrastructure services
- connecting references and interfaces
- checking hardware wrappers
- validating parameter ranges
- confirming required dependencies exist
- reporting startup faults
Mixing this into normal cyclic logic makes the program harder to reason about. A first-cycle flag looks harmless until every function block has one and the actual startup order becomes a treasure map drawn by committee.
The better model is simple:
Startup is a controlled state, not a side effect of cyclic execution.
FB_init Is Limited
FB_init is not bad. It exists for a reason.
It is useful for local setup that belongs to the function block itself. Simple initial values, local bookkeeping, and idempotent internal preparation can fit there.
It is less suitable for project-level wiring.
FB_init runs automatically. It is also called again during an online change. That
matters. If complex dependency registration is hidden inside FB_init, the behavior after
an online change can differ from the behavior after a cold start. Very exciting, if your
definition of exciting includes debugging a machine that only fails after lunch.
There is another practical issue: FB_init does not make the project startup order
obvious. When reading the application, it is harder to see which service is initialized
first, which module depends on it, and what happens if the dependency is missing.
A useful rule is:
Use
FB_initfor local, simple, repeatable setup. Use an explicit startup phase for project dependencies.
Use an Explicit Init Phase
For project-level initialization, expose an init() method on function blocks that need
setup. Call those methods from a dedicated startup sequence.
The important detail is timing. The explicit initialization phase runs after the function block instances already exist. That makes it possible to connect references, pass interfaces, validate configuration, and report failures in one visible place.
A small initialization method can be enough.
METHOD init : BOOL
VAR_INPUT
service : IService;
END_VAR
IF service <> 0 THEN
_service := service;
_initialized := TRUE;
_hasError := FALSE;
init := TRUE;
ELSE
_initialized := FALSE;
_hasError := TRUE;
init := FALSE;
END_IF
END_METHODThis example is intentionally small. The point is not the exact interface. The point is that dependency validation happens where the dependency is assigned.
The cyclic part should not silently run before initialization.
METHOD update
IF NOT _initialized THEN
_hasError := TRUE;
RETURN;
END_IF
// Normal cyclic logic runs here.
END_METHODReturning early is not enough by itself. The missing initialization must be observable through a diagnostic flag, error code, state, event, or HMI message. Silent failure is not graceful.
Put Startup in One Place
A project-level startup runner does not need to be complicated. It can be a small state machine owned by the application.
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE StartupState :
(
IDLE,
INIT_SERVICES,
INIT_HARDWARE,
INIT_MODULES,
RUNNING,
ERROR
) DINT;
END_TYPEThe application then controls the startup order explicitly.
CASE state OF
StartupState.IDLE:
state := StartupState.INIT_SERVICES;
StartupState.INIT_SERVICES:
IF logger.init() THEN
state := StartupState.INIT_HARDWARE;
ELSE
state := StartupState.ERROR;
END_IF
StartupState.INIT_HARDWARE:
IF pressureSensor.init() THEN
state := StartupState.INIT_MODULES;
ELSE
state := StartupState.ERROR;
END_IF
StartupState.INIT_MODULES:
IF controller.init(logger, pressureSensor) THEN
state := StartupState.RUNNING;
ELSE
state := StartupState.ERROR;
END_IF
StartupState.RUNNING:
controller.update();
StartupState.ERROR:
// Report the startup fault.
ELSE
state := StartupState.ERROR;
END_CASEThis is not a framework. It is a visible startup sequence. That is the point.
For a small project, this can live directly in the top-level application block. For a
larger project, it is usually cleaner to put it in a dedicated startup function block.
MAIN should still stay boring: instantiate top-level objects and call the runner.
Validate Dependencies
Initialization is not only about calling methods in order. It is also the best place to reject an invalid project state before the machine enters normal operation.
Useful checks include:
- interface variables are not
0 REFERENCE TOvariables pass__ISVALIDREF()- configuration values are inside valid ranges
- required infrastructure services are registered
- hardware wrappers have usable input data
- module limits match the configured machine variant
Do not spread these checks randomly through the codebase. A module should validate the dependencies it owns. The startup runner should decide whether the system may continue.
For references, check the reference before using it.
IF __ISVALIDREF(configuration) THEN
speedMax := configuration.speedMax;
ELSE
_hasError := TRUE;
END_IFFor interface variables, check against 0.
IF alarmService <> 0 THEN
alarmService.raise('Startup failed');
END_IFThese checks are small, but they prevent a common failure mode: a function block assumes the world is ready because the compiler accepted the code.
Report Initialization Faults
A startup sequence should fail loudly enough to be diagnosed.
At minimum, expose:
- the current startup state
- whether startup has completed
- whether startup has failed
- the failed step or module
- an error code or short error text
For small systems, a few properties may be enough.
PROPERTY HasError : BOOL
HasError := _hasError;
END_PROPERTYFor larger systems, use a consistent diagnostic structure. The important part is consistency. If every module reports errors differently, the HMI becomes a museum of local opinions.
Startup errors should also be visible during simulation and testing. A test setup should
be able to prove that a missing dependency prevents the project from entering
StartupState.RUNNING.
Hardware May Be Late
Not every dependency is ready in the first scan.
Some hardware needs several cycles before useful data is available. Fieldbus devices may still be changing state. External services may need time to connect. An OPC UA, MQTT, database, or ADS service may not be ready just because the PLC task has started.
That does not invalidate the startup sequence. It means the startup sequence may need waiting states with timeouts.
For example:
- wait for a hardware wrapper to report operational data
- wait for a service to become connected
- fail after a defined timeout
- allow a manual reset after the fault is corrected
Online Changes Need Testing
Online change is part of normal TwinCAT development. It is also one reason initialization logic needs discipline.
After an online change, some initialization behavior may run again. Some variables may keep their values. Others may not. References and registrations should not rely on folklore.
Test the project lifecycle explicitly:
- cold start
- warm restart
- online change
- simulation startup
- missing hardware or service
- invalid configuration
The startup runner should make these cases easier to test because the project has a visible state. Without that, the startup model is whatever happened last time. A bold testing strategy, in the same way jumping from a roof is a bold elevator strategy.
Conclusion
TwinCAT projects should not start by accident.
FB_init is useful for local setup, but it is not a project architecture. First-cycle
flags can solve small problems, but they do not create a clear lifecycle.
Use an explicit startup phase. Call init() methods in a known order. Validate
dependencies. Expose startup faults. Enter cyclic operation only when the system is ready.
The result is controlled startup behavior. It also forces the programmer to design the startup process explicitly.
