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.

Why PLCopen Motion Control?

Every servo drive manufacturer has proprietary motion commands. Siemens has its technology objects, Rockwell has MAPC/MAAG/MAM, Beckhoff has NC PTP. The PLCopen Motion Control standard (Part 1–6) solves this fragmentation by defining a universal set of function blocks that work the same way across platforms.

Once you learn PLCopen, you can program Siemens S7-1500T, Beckhoff TwinCAT, CODESYS-based systems, B&R Automation Studio, and Schneider EcoStruxure — all with the same function block calls.

This article covers real servo programming from power-on to multi-axis synchronization, with production-grade Structured Text code.

The Axis State Machine

Every PLCopen axis follows a strict state model. Understanding it prevents 90% of "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 │────────────────────┘
              └───────────┘

Key rules:

  • You can only move from STANDSTILL
  • MC_Stop overrides everything — the axis goes to STOPPING, then STANDSTILL
  • ERRORSTOP (not shown) catches drive faults — must be reset with MC_Reset
  • Step 1: Power On and Homing

    Before any motion, you must 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 Explained

    | 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 |

    Production tip: Always implement a homing timeout. If homing takes longer than expected, something is physically wrong (sensor failure, mechanical jam). Don't let the axis keep searching forever.

    // 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

    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 |

    S-curve profiles reduce vibration and settling time on precision machines. Set the Jerk parameter to a reasonable value (typically 5–20× the acceleration):

    // 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);

    Multi-Position Sequencing

    Real machines move through sequences of positions. A position table pattern handles this cleanly:

    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 (slave) to follow another (master) at a fixed ratio. This is fundamental for:

  • Printing registration (print roller follows web speed)
  • Conveyor synchronization
  • 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;

    Dynamic Gear Ratio Changes

    In winding applications, the gear ratio must 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 is the most powerful PLCopen function — it links a slave axis to a master axis through a cam profile (arbitrary position-to-position mapping). Used for:

  • Rotary knife cut-to-length
  • Pick-and-place synchronization
  • Flying shear applications
  • // 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

    For cutting at a specific product length, the master is often a virtual axis driven by an encoder measuring 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

    Production machines need to verify that motion completed correctly:

    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

    For XY tables, gantries, and robots, multiple axes must move together. PLCopen Part 4 defines coordinated motion, but even basic synchronization can be done with 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;

    Summary

    | 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 |

    The PLCopen standard is your passport to cross-platform motion control. Learn these function blocks once, and you can program any servo system in the industry.