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:

  • Motion starts from STANDSTILL. A new move command can also take over from one that is already running, but nothing moves from DISABLED, STOPPING or ERRORSTOP.
  • MC_Stop overrides any motion. The axis goes to STOPPING, then STANDSTILL.
  • ERRORSTOP (not shown in the diagram) is where drive faults land, and you leave it only with MC_Reset.
  • 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

    ModeMethodUse Case
    1Direct set positionAbsolute encoder, no physical motion needed
    2Sensor-basedMove until home sensor, then index pulse
    3Mechanical stopMove until torque limit (hard stop homing)
    4Incremental encoder referenceMove to Z-pulse after sensor
    Put a timeout on homing. If homing takes longer than it should, something is physically wrong, such as a failed sensor or a mechanical jam, and the axis should not 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

    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

    ProfileJerk ParameterUse Case
    Trapezoidal0 or very highFast positioning, less smooth
    S-CurveFinite valueSmooth motion, less mechanical stress
    An S-curve profile reduces vibration and settling time on precision machines. A sensible jerk value is 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);
    

    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

    TopicKey Function BlockPurpose
    Power OnMC_PowerEnable drive, allow direction
    HomingMC_HomeEstablish position reference
    Point-to-PointMC_MoveAbsolute/RelativePosition moves
    ContinuousMC_MoveVelocityConstant speed (conveyors)
    StopMC_StopControlled stop from any state
    Error ResetMC_ResetClear drive faults
    GearingMC_GearIn/GearOutLock axes at fixed ratio
    CammingMC_CamInArbitrary position-position profiles
    Multi-AxisCoordinated sequencesXY/XYZ pick-and-place
    These blocks carry over between vendors. Learn them on one platform and most of what you know works on the next servo system you meet.