Motion Control
PLC Motion Control & Servo Programming: PLCopen Function Blocks in Structured Text
From enabling a servo axis to synchronized multi-axis camming — the complete guide to PLCopen motion control function blocks implemented in IEC 61131-3 Structured Text.
What PLCopen motion control standardizes
Every servo drive manufacturer has its own motion commands. Siemens has technology objects, Rockwell has MAPC/MAG/MAM, Beckhoff has NC PTP. The PLCopen Motion Control standard (Part 1–6) defines one set of function blocks that behave the same way across platforms, so you are not relearning motion for every vendor.
Once you know PLCopen, you can program Siemens S7-1500T, Beckhoff TwinCAT, CODESYS-based systems, B&R Automation Studio and Schneider EcoStruxure with largely the same function block calls.
This article goes from power-on to multi-axis synchronization, with Structured Text you can adapt for a real machine.
The axis state machine
Every PLCopen axis follows a strict state model. Know it, and you will avoid most "why won't my axis move?" debugging sessions:
┌───────────┐
┌───►│ DISABLED │◄──────────────────┐
│ └─────┬─────┘ │
│ MC_Power│(TRUE) │MC_Power(FALSE)
│ ┌─────▼─────┐ │
│ │ STANDSTILL ├──────────────┐ │
│ └──┬──┬──┬──┘ │ │
│ │ │ │ MC_Home │ │
│ │ │ │ ┌──────▼──┐ │
│ │ │ └──────────┤ HOMING │ │
│ │ │ └────┬────┘ │
│ │ │ │Done │
│ │ │ ┌──────▼──┐ │
│ │ └──────────►│STANDSTILL│ │
│ │ └─────────┘ │
│ MC_MoveAbsolute/Relative/Velocity │
│ ┌──▼────────┐ │
│ │ MOVING ├──────Done──►STANDSTILL
│ └──┬────────┘ │
│ MC_Stop│ │
│ ┌──▼────────┐ │
└────┤ STOPPING │────────────────────┘
└───────────┘
The rules to remember:
Step 1: Power on and homing
Before any motion you have to enable the drive and establish a reference position:
PROGRAM ServoStartup
VAR
Axis1 : AXIS_REF; // Axis reference (platform-specific)
// Function block instances
fbPower : MC_Power;
fbReset : MC_Reset;
fbHome : MC_Home;
fbMoveAbs : MC_MoveAbsolute;
// Sequence control
Step : INT := 0;
EnableDrive : BOOL := FALSE;
HomeComplete : BOOL := FALSE;
AxisReady : BOOL := FALSE;
ErrorPresent : BOOL := FALSE;
END_VAR
// Always running: power management
fbPower(
Axis := Axis1,
Enable := EnableDrive,
Enable_Positive := TRUE, // Allow positive direction
Enable_Negative := TRUE, // Allow negative direction
Status => AxisReady
);
// Startup sequence
CASE Step OF
0: // WAIT FOR ENABLE COMMAND
IF EnableDrive THEN
Step := 1;
END_IF;
1: // WAIT FOR DRIVE READY
IF fbPower.Status THEN
// Drive is enabled and ready
Step := 2;
ELSIF fbPower.Error THEN
// Drive fault — need reset
Step := 10;
END_IF;
2: // START HOMING
fbHome(
Axis := Axis1,
Execute := TRUE,
Position := 0.0, // Define home as position 0
HomingMode := 2, // Platform-specific: sensor-based
Velocity := 10.0 // Slow homing speed (mm/s or deg/s)
);
IF fbHome.Done THEN
fbHome(Execute := FALSE);
HomeComplete := TRUE;
Step := 3;
ELSIF fbHome.Error THEN
fbHome(Execute := FALSE);
Step := 10;
END_IF;
3: // READY FOR OPERATION
AxisReady := TRUE;
// Axis is homed and ready for motion commands
10: // ERROR HANDLING
ErrorPresent := TRUE;
fbReset(
Axis := Axis1,
Execute := TRUE
);
IF fbReset.Done THEN
fbReset(Execute := FALSE);
ErrorPresent := FALSE;
Step := 1; // Try again
END_IF;
END_CASE;
Homing modes
| Mode | Method | Use Case |
|---|---|---|
| 1 | Direct set position | Absolute encoder, no physical motion needed |
| 2 | Sensor-based | Move until home sensor, then index pulse |
| 3 | Mechanical stop | Move until torque limit (hard stop homing) |
| 4 | Incremental encoder reference | Move to Z-pulse after sensor |
// Homing timeout protection
VAR
tmrHomingTimeout : TON;
END_VAR
tmrHomingTimeout(IN := (Step = 2), PT := T#30S);
IF tmrHomingTimeout.Q THEN
fbHome(Execute := FALSE);
// Generate alarm: homing timeout — check home sensor
Step := 10;
END_IF;
Step 2: Point-to-point motion
These are the three core motion commands:
// MC_MoveAbsolute — move to a specific position
fbMoveAbs(
Axis := Axis1,
Execute := StartMove,
Position := 150.0, // Target: 150.0 mm
Velocity := 500.0, // Speed: 500 mm/s
Acceleration := 2000.0, // Accel: 2000 mm/s²
Deceleration := 2000.0, // Decel: 2000 mm/s²
Jerk := 10000.0 // Jerk limit: 10000 mm/s³
);
// MC_MoveRelative — move a distance from current position
VAR
fbMoveRel : MC_MoveRelative;
END_VAR
fbMoveRel(
Axis := Axis1,
Execute := StartRelMove,
Distance := 25.0, // Move 25mm from current position
Velocity := 200.0,
Acceleration := 1000.0,
Deceleration := 1000.0
);
// MC_MoveVelocity — continuous motion at a set speed
VAR
fbMoveVel : MC_MoveVelocity;
END_VAR
fbMoveVel(
Axis := Axis1,
Execute := StartConveyor,
Velocity := 100.0, // 100 mm/s continuous
Acceleration := 500.0,
Direction := 1 // 1=Positive, 2=Negative
);
// Runs until MC_Stop or another motion command
Motion profiles: trapezoidal vs. S-curve
| Profile | Jerk Parameter | Use Case |
|---|---|---|
| Trapezoidal | 0 or very high | Fast positioning, less smooth |
| S-Curve | Finite value | Smooth motion, less mechanical stress |
// Trapezoidal: instant acceleration change
fbMoveAbs(Axis:=Axis1, Execute:=TRUE, Position:=100.0,
Velocity:=500.0, Acceleration:=5000.0,
Deceleration:=5000.0, Jerk:=0);
// S-Curve: smooth acceleration ramp
fbMoveAbs(Axis:=Axis1, Execute:=TRUE, Position:=100.0,
Velocity:=500.0, Acceleration:=5000.0,
Deceleration:=5000.0, Jerk:=50000.0);
Running a sequence of positions from a table
Real machines move through a series of positions. A position table keeps each target, its speeds, its dwell and the action to run there in one struct per step:
TYPE MotionPosition :
STRUCT
Position : REAL;
Velocity : REAL;
Accel : REAL;
Decel : REAL;
DwellTime : TIME; // Wait time at position
OutputAction : INT; // 0=None, 1=Clamp, 2=Drill, 3=Dispense
END_STRUCT;
END_TYPE
PROGRAM PositionSequencer
VAR
Positions : ARRAY[1..20] OF MotionPosition;
NumPositions : INT := 4;
CurrentPos : INT := 1;
SeqState : INT := 0;
fbMoveAbs : MC_MoveAbsolute;
tmrDwell : TON;
CycleRunning : BOOL;
CycleComplete: BOOL;
END_VAR
IF NOT CycleRunning THEN
SeqState := 0;
CurrentPos := 1;
CycleComplete := FALSE;
RETURN;
END_IF;
CASE SeqState OF
0: // START MOVE
fbMoveAbs(
Axis := Axis1,
Execute := TRUE,
Position := Positions[CurrentPos].Position,
Velocity := Positions[CurrentPos].Velocity,
Acceleration := Positions[CurrentPos].Accel,
Deceleration := Positions[CurrentPos].Decel
);
SeqState := 1;
1: // WAIT FOR POSITION REACHED
IF fbMoveAbs.Done THEN
fbMoveAbs(Execute := FALSE);
// Execute action at this position
CASE Positions[CurrentPos].OutputAction OF
1: (* Activate clamp *) ;
2: (* Start drill cycle *) ;
3: (* Dispense material *) ;
END_CASE;
SeqState := 2;
ELSIF fbMoveAbs.Error THEN
fbMoveAbs(Execute := FALSE);
CycleRunning := FALSE; // Abort on error
END_IF;
2: // DWELL TIME
tmrDwell(IN := TRUE, PT := Positions[CurrentPos].DwellTime);
IF tmrDwell.Q THEN
tmrDwell(IN := FALSE);
// Advance to next position
CurrentPos := CurrentPos + 1;
IF CurrentPos > NumPositions THEN
CycleComplete := TRUE;
CycleRunning := FALSE;
ELSE
SeqState := 0; // Move to next position
END_IF;
END_IF;
END_CASE;
Electronic gearing
MC_GearIn locks one axis (the slave) to follow another (the master) at a fixed ratio. You use it for printing registration, where the print roller follows web speed, for conveyor synchronization, and for winding with tension control.
PROGRAM ElectronicGearing
VAR
MasterAxis : AXIS_REF;
SlaveAxis : AXIS_REF;
fbGearIn : MC_GearIn;
fbGearOut : MC_GearOut;
GearRatio_Num : DINT := 3; // Numerator
GearRatio_Den : DINT := 1; // Denominator — slave moves 3:1
EngageGear : BOOL;
DisengageGear : BOOL;
END_VAR
IF EngageGear THEN
fbGearIn(
Master := MasterAxis,
Slave := SlaveAxis,
Execute := TRUE,
RatioNumerator := GearRatio_Num,
RatioDenominator := GearRatio_Den,
Acceleration := 5000.0,
Deceleration := 5000.0
);
IF fbGearIn.InGear THEN
EngageGear := FALSE;
fbGearIn(Execute := FALSE);
// Slave is now locked to master
END_IF;
END_IF;
IF DisengageGear THEN
fbGearOut(
Slave := SlaveAxis,
Execute := TRUE
);
IF fbGearOut.Done THEN
DisengageGear := FALSE;
fbGearOut(Execute := FALSE);
END_IF;
END_IF;
Changing the gear ratio while running
In a winding application the ratio has to change as the roll diameter grows:
// Winding: adjust ratio based on roll diameter
VAR
CoreDiameter : REAL := 76.0; // mm — empty core
CurrentDiameter : REAL;
MaterialThickness: REAL := 0.05; // mm per layer
LayerCount : DINT := 0;
LineSpeed : REAL; // Master speed
WinderRPM : REAL; // Required winder speed
END_VAR
// Calculate current roll diameter
CurrentDiameter := CoreDiameter + (2.0 * INT_TO_REAL(LayerCount) * MaterialThickness);
// Required winder surface speed = line speed
// RPM = (LineSpeed * 60) / (PI * Diameter)
WinderRPM := (LineSpeed * 60.0) / (3.14159 * CurrentDiameter);
// Update gear ratio dynamically
// This requires MC_GearInDyn or re-engaging MC_GearIn with new ratio
Electronic camming
MC_CamIn links a slave axis to a master axis through a cam profile, which is an arbitrary position-to-position mapping. It is the PLCopen block with the widest reach: rotary knife cut-to-length, pick-and-place synchronization and flying shears all run on it.
// Define cam table: master position → slave position
// This example: rotary knife that accelerates to match web speed, cuts, then retracts
TYPE CamPoint :
STRUCT
MasterPos : REAL; // Master degrees (0–360)
SlavePos : REAL; // Slave degrees
END_STRUCT;
END_TYPE
PROGRAM CamProfile
VAR
CamTable : ARRAY[1..8] OF CamPoint;
fbCamIn : MC_CamIn;
MasterAxis : AXIS_REF;
SlaveAxis : AXIS_REF;
END_VAR
// Define cam profile — rotary knife
CamTable[1].MasterPos := 0.0; CamTable[1].SlavePos := 0.0;
CamTable[2].MasterPos := 45.0; CamTable[2].SlavePos := 5.0; // Slow start
CamTable[3].MasterPos := 90.0; CamTable[3].SlavePos := 30.0; // Accelerating
CamTable[4].MasterPos := 135.0; CamTable[4].SlavePos := 90.0; // Matching speed
CamTable[5].MasterPos := 180.0; CamTable[5].SlavePos := 180.0; // CUT POINT — 1:1 ratio
CamTable[6].MasterPos := 225.0; CamTable[6].SlavePos := 270.0; // Matching speed
CamTable[7].MasterPos := 315.0; CamTable[7].SlavePos := 350.0; // Decelerating
CamTable[8].MasterPos := 360.0; CamTable[8].SlavePos := 360.0; // Full cycle
// Engage cam
fbCamIn(
Master := MasterAxis,
Slave := SlaveAxis,
Execute := TRUE,
MasterOffset := 0.0,
SlaveOffset := 0.0,
StartMode := 1 // 1=Ramp in, 2=Immediate
);
Cut-to-length with registration
When you cut at a set product length, the master is often a virtual axis driven by an encoder that measures web travel:
PROGRAM CutToLength
VAR
WebEncoder : AXIS_REF; // Virtual axis from web encoder
KnifeAxis : AXIS_REF; // Physical rotary knife
ProductLength : REAL := 300.0; // mm — desired cut length
KnifeCircumf : REAL := 500.0; // mm — knife circumference
// Registration correction
RegSensor : BOOL; // Registration mark sensor
RegPosition : REAL; // Where mark was detected
ExpectedPos : REAL; // Where mark should be
CorrectionOffset : REAL;
CorrectionGain : REAL := 0.5; // Apply 50% of error per cycle
END_VAR
// Registration correction — adjust slave offset
IF RegSensor THEN
CorrectionOffset := (ExpectedPos - RegPosition) * CorrectionGain;
// Apply offset to cam — shift cut position
// This smoothly corrects for print stretch, slip, etc.
END_IF;
Position and torque monitoring
A production machine has to confirm that a move actually finished where it should, and settled there:
FUNCTION_BLOCK FB_MotionVerify
VAR_INPUT
Axis : AXIS_REF;
TargetPos : REAL;
PositionWindow : REAL := 0.05; // ±0.05mm acceptable
SettlingTime : TIME := T#200MS;
TorqueLimit : REAL := 80.0; // % of rated torque
END_VAR
VAR_OUTPUT
InPosition : BOOL;
Settled : BOOL;
TorqueWarning : BOOL;
PositionError : REAL;
END_VAR
VAR
tmrSettle : TON;
ActualPos : REAL;
ActualTorque : REAL;
END_VAR
// Read actual values from axis (platform-specific)
// ActualPos := MC_ReadActualPosition(Axis);
// ActualTorque := MC_ReadActualTorque(Axis);
PositionError := ABS(ActualPos - TargetPos);
InPosition := PositionError <= PositionWindow;
// Settling: must stay in position window for SettlingTime
tmrSettle(IN := InPosition, PT := SettlingTime);
Settled := tmrSettle.Q;
// Torque monitoring
TorqueWarning := ABS(ActualTorque) > TorqueLimit;
Coordinated multi-axis motion
On XY tables, gantries and robots, several axes have to move together. PLCopen Part 4 defines coordinated motion, but you can do basic synchronization with the single-axis blocks:
PROGRAM XY_PickAndPlace
VAR
AxisX : AXIS_REF;
AxisY : AXIS_REF;
AxisZ : AXIS_REF;
fbMoveX : MC_MoveAbsolute;
fbMoveY : MC_MoveAbsolute;
fbMoveZ : MC_MoveAbsolute;
Step : INT := 0;
// Pick position
PickX : REAL := 100.0;
PickY : REAL := 200.0;
PickZ : REAL := -50.0; // Down into part
// Place position
PlaceX : REAL := 400.0;
PlaceY : REAL := 200.0;
PlaceZ : REAL := -45.0;
SafeZ : REAL := 0.0; // Clearance height
Gripper : BOOL := FALSE;
END_VAR
CASE Step OF
0: // MOVE Z TO SAFE HEIGHT
fbMoveZ(Axis:=AxisZ, Execute:=TRUE, Position:=SafeZ,
Velocity:=200.0, Acceleration:=2000.0, Deceleration:=2000.0);
IF fbMoveZ.Done THEN fbMoveZ(Execute:=FALSE); Step := 1; END_IF;
1: // MOVE XY TO PICK (simultaneous)
fbMoveX(Axis:=AxisX, Execute:=TRUE, Position:=PickX,
Velocity:=500.0, Acceleration:=3000.0, Deceleration:=3000.0);
fbMoveY(Axis:=AxisY, Execute:=TRUE, Position:=PickY,
Velocity:=500.0, Acceleration:=3000.0, Deceleration:=3000.0);
IF fbMoveX.Done AND fbMoveY.Done THEN
fbMoveX(Execute:=FALSE); fbMoveY(Execute:=FALSE);
Step := 2;
END_IF;
2: // MOVE Z DOWN TO PICK
fbMoveZ(Axis:=AxisZ, Execute:=TRUE, Position:=PickZ,
Velocity:=100.0, Acceleration:=1000.0, Deceleration:=1000.0);
IF fbMoveZ.Done THEN fbMoveZ(Execute:=FALSE); Gripper := TRUE; Step := 3; END_IF;
3: // RETRACT Z
fbMoveZ(Axis:=AxisZ, Execute:=TRUE, Position:=SafeZ,
Velocity:=200.0, Acceleration:=2000.0, Deceleration:=2000.0);
IF fbMoveZ.Done THEN fbMoveZ(Execute:=FALSE); Step := 4; END_IF;
4: // MOVE XY TO PLACE
fbMoveX(Axis:=AxisX, Execute:=TRUE, Position:=PlaceX,
Velocity:=500.0, Acceleration:=3000.0, Deceleration:=3000.0);
fbMoveY(Axis:=AxisY, Execute:=TRUE, Position:=PlaceY,
Velocity:=500.0, Acceleration:=3000.0, Deceleration:=3000.0);
IF fbMoveX.Done AND fbMoveY.Done THEN
fbMoveX(Execute:=FALSE); fbMoveY(Execute:=FALSE);
Step := 5;
END_IF;
5: // PLACE
fbMoveZ(Axis:=AxisZ, Execute:=TRUE, Position:=PlaceZ,
Velocity:=100.0, Acceleration:=1000.0, Deceleration:=1000.0);
IF fbMoveZ.Done THEN fbMoveZ(Execute:=FALSE); Gripper := FALSE; Step := 6; END_IF;
6: // RETRACT AND DONE
fbMoveZ(Axis:=AxisZ, Execute:=TRUE, Position:=SafeZ,
Velocity:=200.0, Acceleration:=2000.0, Deceleration:=2000.0);
IF fbMoveZ.Done THEN fbMoveZ(Execute:=FALSE); Step := 0; END_IF;
END_CASE;
Function block quick reference
| Topic | Key Function Block | Purpose |
|---|---|---|
| Power On | MC_Power | Enable drive, allow direction |
| Homing | MC_Home | Establish position reference |
| Point-to-Point | MC_MoveAbsolute/Relative | Position moves |
| Continuous | MC_MoveVelocity | Constant speed (conveyors) |
| Stop | MC_Stop | Controlled stop from any state |
| Error Reset | MC_Reset | Clear drive faults |
| Gearing | MC_GearIn/GearOut | Lock axes at fixed ratio |
| Camming | MC_CamIn | Arbitrary position-position profiles |
| Multi-Axis | Coordinated sequences | XY/XYZ pick-and-place |