Skip to main content

FB vs MAIN Action — TwinCAT PLC architecture decision

PLC Architecture Decision Record

Function Block
vs.
MAIN Action

Code organization choice for a Tray Inverter machine on TwinCAT 3. This article breaks down how the two styles differ in maintenance cost, debug observability, and community practice, then lands on a verdict for this specific machine.

MachineTray InverterControllerBeckhoff TwinCAT 3Date2026-05-12
§ 1   ESSENCE

Essential difference

Under IEC 61131-3 these are different levels of abstraction. A Function Block is a stateful, instantiable unit; an ACTION is just a named sub-snippet inside a program.

Function Block

FUNCTION_BLOCK FB_TaskMoveAbs
State
Each instance owns its own VAR (FSM, MC_* instance)
Parameters
Explicit signature via VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT
Reuse
The same FB instantiates N times, each running independently
OOP
METHOD / PROPERTY / INTERFACE / EXTENDS
Online change
Impact scoped to the FB
Cross-reference
Strong type / input naming makes Find References reliable

MAIN Action

ACTION MoveAbs : BOOL
State
Shares MAIN's VAR — a single global copy
Parameters
None; values flow through MAIN's global variables
Reuse
Not possible — only copy-paste or a giant CASE
OOP
None
Online change
Treated as a change to the entire MAIN program
Cross-reference
Can only search variable names — high noise
§ 2   INSTANCE FAN-OUT

11 servos, 11 separate states

Each of the 11 servos on this machine needs its own MC_MoveAbsolute instance, its own eState, and its own xBusy / xDone. The diagram shows how an FB fans out into 11 independent instances, while ACTION forces every axis to share one state space.

FB instances

FB_TaskMoveAbs × 11
FB_TaskMoveAbsFUNCTION_BLOCK[0] RotMasterDONE[1] RotLBUSY[2] RotRIDLE[3] LiftZDONE[4] GripLBUSY[5] GripRIDLE[6] ConvLdDONE[7] ConvUldIDLE[8] BarLD_ZBUSY[9] BarULD_ZIDLE[10] PusherXDONE

MAIN Action

11 axes sharing MAIN.VAR
MAIN.eStateMAIN.nAxisIdsingle state, 11 callersAX0AX1AX2AX3AX4AX5AX6AX7AX8AX9AX10

In practice, the ACTION route has only two variants: (a) hand-copy 11 versions — change one place, change 11 places; (b) declare 11 axis-specific global variables plus a giant CASE — hand-rolling OOP in a procedural style and killing Cross-reference.

§ 3   SCOPE OF THIS MACHINE

11 local axes + Master/Slave coupling

Below is the servo list for this machine. AX_Flip_RotMaster fans out to RotL / RotR through the NC MC_GearIn coupling — exactly where instance-level state management proves its worth.

RotMaster
virtual
RotL
slave
RotR
slave
LiftZ
servo
GripL
servo
GripR
servo
ConvLd
servo
ConvUld
servo
BarLD_Z
servo
BarULD_Z
servo
PusherX
servo
§ 4   DEBUG OBSERVABILITY

Online view: visible vs invisible

Whether a running PLC is actually debuggable comes down to whether you can see every axis's state at the same moment. The mockup below shows TwinCAT online view under each architecture.

FB — per-instance watch

fbMoveAbs[0].eStateDONE
fbMoveAbs[1].eStateBUSY
fbMoveAbs[2].eStateDONE
fbMoveAbs[3].eStateERROR
fbMoveAbs[4].eStateBUSY
fbMoveAbs[5].eStateIDLE
fbMoveAbs[6].eStateIDLE
fbMoveAbs[7].eStateBUSY
fbMoveAbs[8].eStateDONE
fbMoveAbs[9].eStateIDLE
fbMoveAbs[10].eStateIDLE

All 11 parallel tasks' real states are visible and comparable at the same instant. A breakpoint can pin to "the instance for axis 3" and hit precisely.

ACTION — shared state

MAIN.eState = ???
Who wrote it? Which axis is moving?
// axis index lives in MAIN.nAxisId
// but nAxisId got overwritten the previous cycle
Cannot distinguish state across 11 axes

All variables share one copy in MAIN. A breakpoint can only attach to a code line; when 11 axes run through the same line, you can't tell them apart. Synchronization / fan-out scenarios become nearly undebuggable.

§ 5   MAINTENANCE COST

Change one place vs change 11 places

Maintenance cost isn't about how pretty today's code is — it's about how many places need to change when requirements shift three months out.

Function Block

  • PROChange one FB and all 11 axes pick it up; regression scope is clear
  • PROVAR_INPUT / VAR_OUTPUT acts as an interface contract — team handoffs and C# bridges read directly from the signature
  • PROOnline change impact stays narrow, keeping hot-patch risk controllable
  • PROUnit tests can isolate each FB individually
  • PROFind All References has a high hit rate
  • NEUFirst-time instance declaration costs an extra line

MAIN Action

  • PROUseful for splitting a short linear sequence visually; up to ~5 lines it can improve readability
  • PRONo instance to declare — saves a line on first write
  • CONMAIN's variable space balloons into a "god program" as the machine grows
  • CONCannot fan out — multi-axis scenarios force copy-paste
  • CONNo input / output signature; the C# integration layer struggles to align
  • CONOnline change scope equals the entire MAIN — risk amplified
§ 6   INDUSTRY CONSENSUS

Where the community lands

This one is lopsided. The chart below shows where major Beckhoff / IEC 61131-3 communities sit on "which to use at the architecture level". ACTION retains residual value only for "splitting a short sub-snippet inside an FB".

Beckhoff InfoSys (official)
FB 96%
PLCopen / IEC 61131-3
FB 94%
Beckhoff Forum (large projects)
FB 92%
plctalk.net
FB 85%
reddit r/PLC
FB 88%
STEP7 / Codesys V2 (legacy)
ACTION 55%

※ Percentages are qualitative estimates reflecting the mainstream recommendation ratio across these communities for "FB vs ACTION at the architecture level" — not rigorous survey data.

§ 7   VERDICT

Verdict for this machine

Keep the current FB decomposition.

This machine's profile — 11 axes running in parallel plus Master/Slave coupling — makes per-instance observability a hard requirement for debugging. Building the architecture on ACTION would mean giving up instance-level state inspection and bounded Online change scope.

ACTION's reasonable role downgrades to a layout-splitting tool inside an FB — for example a 5-line "reset latch" sub-snippet inside MAIN_Alarm, or a named partition for each FSM state inside an FB.

Architecture-level concerns (task dispatch, interlock, motion task) stay in FB. The current FB_PlcTaskDispatcher / FB_TaskMoveAbs / FB_TaskHome / FB_Interlock decomposition is correctly oriented and does not need refactoring.

Tray Inverter / Beckhoff TwinCAT 3   ·   ASE_TrayInverterSystem   ·   2026-05-12