Chapter 31 / 32 · Part 8 · Industrial patterns

Motion control with PLCopen

Power, home and move a servo axis with PLCopen MC blocks, handle Busy, Done and Error, and sequence moves from a state machine.

What PLCopen standardised, and what it left to the vendor

Twenty-five years ago, moving a servo axis from a PLC meant learning one vendor's private dialect. A Siemens positioning module had its own function blocks and data blocks, an Allen-Bradley 1756-M02AE servo card had its own instructions, a Lenze or SEW drive had its own parameter-channel handshake, and nothing you learned on one transferred to the next. The PLCopen organisation, set up in 1992 to promote IEC 61131-3, formed Technical Committee 2 to fix that for motion. The result is Function Blocks for Motion Control, usually called PLCopen Motion Control Part 1. It was first published in 2001; version 2.0 in 2011 folded the old Part 2 extensions into it. Later parts build on it: Part 3 is user guidelines, Part 4 covers coordinated motion (axis groups and interpolation), Part 5 adds homing procedures and Part 6 fluid power.

Part 1 standardises four things, and they are the four things this chapter teaches:

  1. Block names and pins. MC_Power, MC_Home, MC_MoveAbsolute, MC_MoveRelative, MC_MoveVelocity, MC_Stop, MC_Halt, MC_Reset and about forty more, each with a defined set of inputs and outputs.
  2. The axis state diagram. Every axis is always in exactly one state, and each state says which commands are legal.
  3. The handshake. How Execute, Enable, Busy, Done, Active, CommandAborted, Error and ErrorID behave, scan by scan, on every block.
  4. What happens when two commands meet. The buffer modes.

What Part 1 does not standardise is just as important, because that is where you will spend your commissioning days. It says nothing about how an axis is configured: the units (mm, degrees, product counts), the gear ratio between motor and load, the encoder resolution, the modulo range of a rotary table, the tuning of the position and velocity loops, the fieldbus that carries the setpoints, or the actual numbers in ErrorID. Those belong to the vendor's axis object, and you set them up in TIA Portal, Studio 5000, the CODESYS device tree or the TwinCAT System Manager, long before a line of ST runs.

So the deal is this. The program that sequences a pick-and-place reads almost the same on an Omron NJ, a Beckhoff CX, a CODESYS-based Schneider or Bosch controller, and in large part on an S7-1500. The axis underneath is configured completely differently on each, and the error numbers never match. Learn the PLCopen handshake once, properly, and you can read anyone's motion code.

This engine implements fifteen of the Part 1 blocks with a real trapezoidal motion model: position and velocity advance with the scan time, so a longer move really takes longer and Done means the target was reached. Camming, gearing, axis groups and touch probes are not modelled, and the build rejects them by name rather than letting them silently return zeros.

The axis is an object: AXIS_REF

In PLCopen the axis is not a set of I/O points. It is an object that lives in the controller's motion runtime, and your program holds a reference to it of type AXIS_REF. Every MC block has an Axis pin, and passing the same AXIS_REF to several blocks is how you say "these blocks all command the same motor".

What sits behind the reference depends on the platform. On an S7-1500 it is a technology object such as TO_PositioningAxis, with its own data block. In Studio 5000 it is an AXIS_CIP_DRIVE tag tied to a Kinetix drive on EtherNet/IP. In CODESYS SoftMotion it is an AXIS_REF_SM3 object under the drive in the device tree. In TwinCAT 3 it is an AXIS_REF variable linked to an NC axis. The motion runtime runs the interpolator (often in its own faster task, 1 to 4 ms is typical) and sends setpoints to the drive; your PLC program only issues commands and reads status.

Here, you declare Ax : AXIS_REF; in VAR, and the watch table shows the axis as its state and position, for example Standstill @ 100.0. The program cannot read a field of the reference directly: like in most PLCopen libraries, you read the axis through MC_ReadActualPosition, MC_ReadActualVelocity and MC_ReadStatus, which are Enable blocks — they report live values every scan while Enable is TRUE.

Nothing moves until the drive is powered. MC_Power is the first block every motion program calls:

PROGRAM Main
VAR
    Enable  : BOOL;              (* input: drive enable selector *)
    TryMove : BOOL;              (* input: request a move *)
    Ax      : AXIS_REF;          (* one servo axis, units mm *)
    Power   : MC_Power;
    Status  : MC_ReadStatus;
    Move    : MC_MoveAbsolute;
END_VAR

Power(Axis := Ax, Enable := Enable);
Status(Axis := Ax, Enable := TRUE);
Move(Axis := Ax, Execute := TryMove, Position := 50.0, Velocity := 100.0);

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Enable, TryMove.

Leave Enable off and switch TryMove on. The move is refused: Move.Error goes TRUE with ErrorID 1, "axis not powered", and the axis stays Disabled. Switch TryMove off (the error clears with the falling edge), turn Enable on, and Power.Status and Status.StandStill come on. Now TryMove starts a real move to 50 mm.

On a real drive Power.Status does not come on in the same scan. The drive has to close its main contactor or confirm its DC bus, release the holding brake (typically 50 to 200 ms for a spring-applied brake), switch the current loop on and report "operation enabled" over the fieldbus. Half a second from Enable to Status is normal. Never start a move on the scan you raise Enable; wait for Status.

The axis state diagram

Every PLCopen axis is in exactly one of these states, and MC_ReadStatus has one BOOL output per state. The diagram is the single most useful thing in the standard, because it tells you why a command was refused.

StateWhat the axis is doingHow it gets thereCommands it accepts
DisabledDrive off, no torque, brake appliedPower-up, MC_Power Enable FALSEMC_Power
StandstillPowered, holding positionMC_Power Enable TRUE; any move finishingAll motion commands, MC_Home
HomingRunning a reference sequenceMC_HomeMC_Stop only — PLCopen rejects a new move here with Error, and homing carries on
Discrete MotionMoving to a targetMC_MoveAbsolute, MC_MoveRelative, MC_HaltAny motion command (Aborting)
Continuous MotionMoving with no end pointMC_MoveVelocity, joggingAny motion command (Aborting)
Synchronized MotionFollowing a master axisMC_GearIn, MC_CamInAny motion command
StoppingBraking under MC_Stop, or held by itMC_StopNothing: every move is refused
ErrorStopFaulted: drive error, following error, limitAny axis fault, from any stateMC_Reset only

Three rules hold the diagram together. ErrorStop beats everything: any axis fault drops the axis there from any state, and only MC_Reset leaves it. Stopping is a lock: while an MC_Stop holds the axis, every motion command is rejected. Moving states accept new moves: a new command on a moving axis does not wait, it takes over, which is the Aborting behaviour covered below.

Two details on this engine differ from the letter of Part 1. PLCopen puts an axis that is braking under MC_Halt in Discrete Motion; this engine reports it as Stopping, though it does not lock it. And the engine has no drive or limit model, so nothing ever drives an axis into ErrorStop here; MC_Reset and MC_ReadAxisError exist and behave, but the faults that need them come later in this chapter as vendor listings.

Execute is an edge, Enable is a level

PLCopen blocks come in two families, and mixing them up is the commonest motion bug there is.

Execute blocks (MC_Home, MC_MoveAbsolute, MC_MoveRelative, MC_MoveVelocity, MC_Stop, MC_Halt, MC_Reset, MC_SetPosition) start one command on the rising edge of Execute. The block reads its inputs on that edge — Position, Velocity, Acceleration — and works on that snapshot. Holding Execute TRUE does not repeat the command, and changing Position while Execute stays TRUE does nothing at all. PLCopen 2.0 added an optional ContinuousUpdate input for the cases where you really want a moving target; most code never uses it.

Enable blocks (MC_Power, MC_ReadActualPosition, MC_ReadStatus, MC_SetOverride) work while Enable is TRUE and refresh their outputs every call. Their outputs are Valid or Status, plus Busy, Error and ErrorID.

An Execute block reports through a fixed set of outputs:

OutputMeaning
BusyThe command has been accepted and is not finished. TRUE from the edge until Done, Error or CommandAborted.
ActiveThis block is the one currently in control of the axis.
DoneThe command completed: for a move, the axis stands at the target.
CommandAbortedAnother command took the axis before this one finished, or power was lost.
Error / ErrorIDThe command was refused or failed, and why.

Busy, Done, CommandAborted and Error are mutually exclusive: when Busy falls, exactly one of the other three is TRUE. That gives you the pattern for every motion step: raise Execute, wait while Busy, then look at which of the three you got. The outputs stay set while Execute stays TRUE, and clear when Execute falls. If Execute falls before the command finishes, Part 1 says the result is still reported, TRUE for one scan only. This engine keeps Done TRUE until the next edge in that case; real controllers give you one scan. Do not write code that depends on either: hold Execute until you have seen the result.

PROGRAM Main
VAR
    Go     : BOOL;               (* input: move command *)
    Ax     : AXIS_REF;
    Power  : MC_Power;
    Move   : MC_MoveAbsolute;
    ReadX  : MC_ReadActualPosition;
    ReadV  : MC_ReadActualVelocity;
    PosX   : REAL;               (* mm *)
    Speed  : REAL;               (* mm/s *)
END_VAR

Power(Axis := Ax, Enable := TRUE);

Move(Axis := Ax, Execute := Go, Position := 100.0, Velocity := 50.0,
     Acceleration := 100.0, Deceleration := 100.0);

ReadX(Axis := Ax, Enable := TRUE);
ReadV(Axis := Ax, Enable := TRUE);
PosX  := ReadX.Position;
Speed := ReadV.Velocity;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Go.

Switch Go on and step through. Busy and Active come on at once, Speed ramps up by 10 mm/s per 100 ms scan to 50 mm/s, holds, ramps down, and Done comes on with PosX at exactly 100. Leave Go on and nothing more happens: one edge, one move. Switch it off and Done clears. Switch it on again and Busy comes on and goes straight back off with Done, because the axis is already at 100 mm.

Home, then move absolute, relative or at a velocity

MC_Home gives the axis a known zero. An incremental encoder only counts pulses from wherever it was at power-up, so its position means nothing until the axis has been referenced. The classic homing run drives slowly towards a reference cam (a proximity switch), reverses off it, and latches the position at the encoder's next zero mark, giving repeatability of a few micrometres. Other methods home against a hard stop by watching for a torque rise, or on the current position. CiA 402 drives (most EtherCAT and CANopen servos) list over thirty numbered homing methods in the drive itself, and Part 5 of PLCopen defines step blocks for building your own. With a multi-turn absolute encoder you set the reference once at commissioning and never home again, which is why most new machines use them: no homing run after every power cut, no crash risk from a missing reference.

This engine models a homing run as a travel to the reference at 100 units/s, after which the position is set to the block's Position input. The sequencing rules are what matter: Execute edge, Busy while it runs, the axis in Homing, Done when referenced.

The three moves differ in what they are given:

BlockGivenFinishes whenTypical use
MC_MoveAbsolutePosition (absolute target)The axis stands at Position: DonePick and place stations, positions from a recipe
MC_MoveRelativeDistance (signed, from where it is now)The axis has moved Distance: DoneIndexing tables, feed-to-length, stepping a magazine
MC_MoveVelocityVelocity and a directionNever. It reports InVelocity at speedConveyors, spindles, unwinders

The difference between absolute and relative is not cosmetic. An absolute move sent twice lands in the same place; a relative move sent twice moves twice as far. If a relative index is interrupted halfway and you send it again, the table ends 1.5 indexes on. Prefer absolute targets wherever the position has meaning, and use relative moves for genuinely repeating steps — and even then, many engineers compute the next absolute target (Target := Target + 60.0) so an interrupted step can be repeated safely.

PROGRAM Main
VAR
    GoAbs  : BOOL;               (* input: go to 100 mm *)
    GoRel  : BOOL;               (* input: move 25 mm further *)
    Ax     : AXIS_REF;
    Power  : MC_Power;
    ToPos  : MC_MoveAbsolute;
    Step25 : MC_MoveRelative;
    ReadX  : MC_ReadActualPosition;
    PosX   : REAL;
END_VAR

Power(Axis := Ax, Enable := TRUE);

ToPos(Axis := Ax, Execute := GoAbs, Position := 100.0, Velocity := 200.0);
Step25(Axis := Ax, Execute := GoRel, Distance := 25.0, Velocity := 200.0);

ReadX(Axis := Ax, Enable := TRUE);
PosX := ReadX.Position;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: GoAbs, GoRel.

Toggle GoRel on and off three times: the axis walks to 25, 50, 75 mm. Toggle GoAbs: it goes to 100 and stays there however often you repeat it. Now toggle GoRel again, then GoAbs: the relative move takes the axis to 125, the absolute one brings it back to 100. Both blocks command the same axis, and the last edge wins.

MC_MoveVelocity is the odd one out. It has no target, so it has no Done; it reports InVelocity once the speed is reached and then runs until another command replaces it or a stop ends it. On this engine Direction is an INT, 1 for positive and 2 for negative; PLCopen defines it as an enumeration, and vendors spell the values MC_Positive_Direction (TwinCAT) or positive and negative in an MC_DIRECTION type (CODESYS). This is the one pin to check in the vendor manual before you copy code across.

Stopping: MC_Stop, MC_Halt and the power switch

There are three ways to stop an axis from the program, and they are not interchangeable.

MC_Halt is a normal, controlled stop. The axis brakes at the given deceleration and ends in Standstill. Any new move may interrupt the braking, in which case the halt reports CommandAborted. Use it for "pause between operations", or for a cycle stop at the end of a step.

MC_Stop brakes the same way, but it also locks the axis. While its Execute stays TRUE, the axis is held in Stopping and every motion command is refused (on this engine with ErrorID 2). Drop Execute and the axis returns to Standstill. Use it when the stop must win: a guard door opened, a fault, an operator stop.

MC_Power with Enable FALSE switches the drive off. The running command is aborted. What the motor does next depends on the drive: it may brake on a ramp, coast, or drop the holding brake on a moving shaft. This is not a stop you choose for normal operation.

PROGRAM Main
VAR
    RunCmd   : BOOL;             (* input: run the belt *)
    StopBtn  : BOOL;             (* input: stop, held = locked *)
    PauseBtn : BOOL;             (* input: controlled halt *)
    Belt     : AXIS_REF;         (* conveyor axis, units mm *)
    Power    : MC_Power;
    Drive    : MC_MoveVelocity;
    StopIt   : MC_Stop;
    PauseIt  : MC_Halt;
    ReadV    : MC_ReadActualVelocity;
    Speed    : REAL;
END_VAR

Power(Axis := Belt, Enable := TRUE);
Drive(Axis := Belt, Execute := RunCmd, Velocity := 200.0,
      Acceleration := 500.0, Deceleration := 500.0);
StopIt(Axis := Belt, Execute := StopBtn, Deceleration := 1000.0);
PauseIt(Axis := Belt, Execute := PauseBtn, Deceleration := 400.0);

ReadV(Axis := Belt, Enable := TRUE);
Speed := ReadV.Velocity;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: RunCmd, StopBtn, PauseBtn.

Turn RunCmd on; the belt ramps to 200 mm/s and InVelocity comes on. Press PauseBtn: it brakes, Drive.CommandAborted shows the halt took over. Release PauseBtn and toggle RunCmd off and on: it runs again. Now hold StopBtn: it stops harder, and while you hold it, toggling RunCmd or PauseBtn gets nowhere — try PauseBtn and watch ErrorID 2. Release StopBtn and the axis is free.

The last row deserves a sentence of its own. MC_Stop is a controlled stop issued by standard software; nobody has validated it as a safety function. An emergency stop on a servo axis is done by the drive's own safety functions from IEC 61800-5-2, typically SS1 (brake on a monitored ramp, then remove torque) followed by STO (Safe Torque Off), triggered by a safety relay or a safety PLC with the right performance level. Your ST program still calls MC_Stop when the e-stop chain opens, so the axis states and the sequence stay consistent, but it is not what keeps people safe.

Who owns the axis: aborting and buffer modes

Several blocks can be wired to one axis, but only one of them commands it at a time. When a new command starts on an axis that is already moving, the PLCopen default is Aborting: the new command takes over immediately, and the old block reports CommandAborted. The axis does not stop in between; it changes its profile from wherever it is, braking or reversing as needed.

PROGRAM Main
VAR
    GoFar  : BOOL;               (* input: go to 300 mm *)
    GoNear : BOOL;               (* input: go to 50 mm *)
    Ax     : AXIS_REF;
    Power  : MC_Power;
    ToFar  : MC_MoveAbsolute;
    ToNear : MC_MoveAbsolute;
    ReadX  : MC_ReadActualPosition;
    PosX   : REAL;
END_VAR

Power(Axis := Ax, Enable := TRUE);
ToFar(Axis := Ax, Execute := GoFar, Position := 300.0, Velocity := 100.0);
ToNear(Axis := Ax, Execute := GoNear, Position := 50.0, Velocity := 100.0);

ReadX(Axis := Ax, Enable := TRUE);
PosX := ReadX.Position;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: GoFar, GoNear.

Switch GoFar on and, while the axis is on its way, switch GoNear on. ToFar.Busy drops, ToFar.CommandAborted comes on, and the axis brakes, reverses and settles at 50 mm with ToNear.Done. The aborted block never reports Done. A sequence that waits only for ToFar.Done would now wait forever — which is why every motion step must also check CommandAborted and Error.

Part 1 defines other buffer modes, chosen with a BufferMode input on each move:

BufferModeThe new command…
Abortingtakes over at once (the default)
Bufferedwaits until the running one is Done, then starts from standstill
BlendingLowstarts when the running one reaches its target, without stopping, at the lower of the two velocities
BlendingPrevious / BlendingNextblends at the first command's / second command's velocity
BlendingHighblends at the higher of the two velocities

Blending is how a palletiser moves through a series of points without stopping at each one, saving 100 to 300 ms per point. This engine models Aborting only, so the other modes are vendor code:

(* TwinCAT 3, Tc2_MC2: queue the second move behind the first, blend at the lower speed *)
MoveA(Axis := AxisX, Execute := bGo, Position := 400.0, Velocity := 800.0);
MoveB(Axis := AxisX, Execute := MoveA.Busy, Position := 650.0, Velocity := 400.0,
      BufferMode := MC_BlendingLow);

Buffered and blended moves come with their own trap: a queued block reports Busy while it waits, but it is not Active yet. Logic that reads Busy as "the axis is doing this move" gets confused; read Active for that.

Sequencing moves from a state machine

Real machines string moves together: home, go to pick, close the gripper, go to place, open, come back. That is a sequence, and chapter 12 showed how to write sequences as a CASE on a step number. Motion adds one rule that matters more than any other: call every MC block on every scan, outside the CASE, and let the step decide its Execute. It is the same rule as for timers in chapter 9, and for the same reason: a block only sees its Execute fall if it is called while it is FALSE.

Ignore the rule and you get one of the nastiest bugs in motion programming, because it looks like it works.

PROGRAM Main
VAR
    Ax     : AXIS_REF;
    Power  : MC_Power;
    ToA    : MC_MoveAbsolute;
    ToB    : MC_MoveAbsolute;
    Step   : INT;
    Cycles : INT;
END_VAR

Power(Axis := Ax, Enable := TRUE);

CASE Step OF
    0:  IF Power.Status THEN Step := 10; END_IF;
    10: ToA(Axis := Ax, Execute := TRUE, Position := 100.0, Velocity := 500.0);
        IF ToA.Done THEN Step := 20; END_IF;
    20: ToB(Axis := Ax, Execute := TRUE, Position := 0.0, Velocity := 500.0);   (* BUG *)
        IF ToB.Done THEN Step := 10; Cycles := Cycles + 1; END_IF;
END_CASE;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it.

The first round trip is real. After that, Cycles races upwards while the axis sits at 0 mm. When the sequence leaves step 10, ToA is no longer called, so it never sees Execute go FALSE. Its Done from the first trip is still TRUE, and its memory of Execute is still TRUE. When step 10 comes round again, the call with Execute := TRUE is not an edge, no move starts, and the stale Done sends the sequence straight on. On a real line this shows up as a counter that says 4,000 cycles for a shift that made 1,200 parts, or a station that "completes" a move it never made and closes a gripper on air.

Here is the same idea written properly, as a pick-and-place with one horizontal servo axis and a vacuum cup: home, wait for start, travel to the pick position at 50 mm, build vacuum for 300 ms, travel to the place position at 400 mm, release for 200 ms, return to 0.

PROGRAM Main
VAR
    Enable       : BOOL;        (* input: drive enable selector *)
    Start        : BOOL;        (* input: cycle start push button *)
    Reset        : BOOL;        (* input: fault reset *)
    AxisX        : AXIS_REF;    (* horizontal servo axis, units mm *)
    Power        : MC_Power;
    Home         : MC_Home;
    MoveX        : MC_MoveAbsolute;
    StartEdge    : R_TRIG;
    GripTimer    : TON;
    ReleaseTimer : TON;
    Step         : INT;
    Target       : REAL;
    Vacuum       : BOOL;        (* output: vacuum generator valve *)
    Cycles       : INT;
END_VAR

Power(Axis := AxisX, Enable := Enable);
StartEdge(CLK := Start);

CASE Step OF
    0:  (* wait for the drive *)
        IF Power.Status THEN Step := 10; END_IF;
    10: (* reference the axis *)
        IF Home.Done THEN Step := 20; END_IF;
    20: (* at home, waiting for a start *)
        IF StartEdge.Q THEN Target := 50.0; Step := 30; END_IF;
    30: (* travel to the pick position *)
        IF MoveX.Done THEN Step := 40; END_IF;
    40: (* vacuum on, let it build *)
        IF GripTimer.Q THEN Target := 400.0; Step := 50; END_IF;
    50: (* travel to the place position *)
        IF MoveX.Done THEN Step := 60; END_IF;
    60: (* vacuum off, let the part drop *)
        IF ReleaseTimer.Q THEN Target := 0.0; Step := 70; END_IF;
    70: (* back to home *)
        IF MoveX.Done THEN Cycles := Cycles + 1; Step := 20; END_IF;
    900: (* fault: wait for power and a reset *)
        IF Reset AND Power.Status THEN Step := 10; END_IF;
END_CASE;

(* Every motion block called every scan; the step decides Execute *)
Home(Axis := AxisX, Execute := Step = 10, Position := 0.0);
MoveX(Axis := AxisX, Execute := Step = 30 OR Step = 50 OR Step = 70,
      Position := Target, Velocity := 500.0,
      Acceleration := 2000.0, Deceleration := 2000.0);
GripTimer(IN := Step = 40, PT := T#300ms);
ReleaseTimer(IN := Step = 60, PT := T#200ms);

(* Any refusal, abort or loss of power ends the cycle *)
IF Home.Error OR MoveX.Error OR MoveX.CommandAborted
   OR (Step > 0 AND Step < 900 AND NOT Power.Status) THEN
    Step := 900;
END_IF;

Vacuum := Step = 40 OR Step = 50;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Enable, Start, Reset.

Turn Enable on: the axis powers up, homes (instantly here, because it starts at 0) and waits in step 20. Pulse Start and follow the cycle. Then start a second cycle and switch Enable off while the axis is travelling: CommandAborted sends the sequence to step 900 and the vacuum drops. Turn Enable back on and press Reset: the axis homes back to 0 from wherever it stopped, then waits for a start. Re-homing after a fault is a deliberate choice here; with an absolute encoder you could skip it, with an incremental encoder after a power loss you could not.

A few details carry the design:

  • One move block, several steps. MoveX serves steps 30, 50 and 70 with a Target set on the transition into each. That works only because steps 40 and 60 sit between them: Execute falls for at least one call, so each move gets a fresh edge. Go straight from one move step to another with the same instance and the second move never starts — the prediction below shows it.
  • Set the target on the transition, not in the step. Target is written once, as the sequence enters the move step, and the block reads it on the edge in that same scan.
  • One fault check for all steps. Error, CommandAborted and loss of Power.Status are checked after the calls, on every scan, whatever the step. A sequence with a fault check in only some steps will one day hang in the step that has none.
  • Outputs from the step. Vacuum is computed from the step number, so step 900 drops it automatically. Whether a part held by vacuum should be dropped on a fault is a process decision; on many machines the vacuum is held and the cup has a non-return valve.

Manual mode: jogging and software limits

Every axis needs a manual mode. Maintenance must be able to drive it off a crash, align it to a fixture, or move it to change a belt. The standard tool is jogging: the axis moves while a button is held and stops when it is released. Most PLCopen libraries provide MC_Jog with JogForward and JogBackward inputs (CODESYS SoftMotion, TwinCAT, Omron); Siemens calls it MC_MoveJog. Where there is no jog block, people build one from MC_MoveVelocity on the button's rising edge and MC_Halt on its falling edge.

Manual mode has rules that come from risk assessments, not from the software:

  • Jog only in manual mode, selected on a key switch or a mode selector, never as a side effect of automatic logic.
  • Reduced speed. 250 mm/s, the reduced-speed figure from the robot standard ISO 10218, is widely borrowed for other machines; your machine's risk assessment sets the real number.
  • Hold-to-run. The axis moves only while the button is held. A dropped button, a lost HMI connection or a second button pressed at the same time must stop it. HMI jog buttons are particularly treacherous: if the touch panel loses its connection while a button is pressed, the PLC never sees the release. Use a hardwired enabling device, or a heartbeat from the HMI that drops the jog when it stops.
  • Limits apply in manual mode too, with one exception: an axis that is not referenced has no meaningful position, so software limits cannot apply and the jog speed must be lower still.

Software limits are best configured in the axis itself (TIA Portal and Studio 5000 both have software limit switches or soft overtravel, CODESYS and TwinCAT have them in the axis parameters). When the drive owns them, it stops the axis on its own and refuses moves outside the range. When it does not, or when you want the program to know the limit is coming, you add a check in ST. The check has to allow for braking: an axis jogging at 50 mm/s with a 500 mm/s² ramp travels v² / 2a = 2.5 mm after you take the command away, plus whatever it covers during one scan. On a 100 ms scan at 50 mm/s that is another 5 mm. That is why the program limit sits a little inside the hardware limit switches, and the hardware switches sit inside the mechanical stops.

PROGRAM Main
VAR
    Manual     : BOOL;           (* input: mode selector in manual *)
    JogFwd     : BOOL;           (* input: hold to jog forward *)
    JogRev     : BOOL;           (* input: hold to jog backward *)
    Slide      : AXIS_REF;       (* linear slide, units mm *)
    Power      : MC_Power;
    Jog        : MC_Jog;
    ReadPos    : MC_ReadActualPosition;
    Pos        : REAL;
    FwdAllowed : BOOL;
    RevAllowed : BOOL;
END_VAR

Power(Axis := Slide, Enable := TRUE);
ReadPos(Axis := Slide, Enable := TRUE);
Pos := ReadPos.Position;

(* Program limit at 100 mm, a little inside the hardware switch *)
FwdAllowed := Manual AND JogFwd AND NOT JogRev AND Pos < 100.0;
RevAllowed := Manual AND JogRev AND NOT JogFwd AND Pos > 0.0;

Jog(Axis := Slide, JogForward := FwdAllowed, JogBackward := RevAllowed,
    Velocity := 50.0, Acceleration := 500.0, Deceleration := 500.0);

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Manual, JogFwd, JogRev.

Turn Manual on and hold JogFwd. The slide moves at 50 mm/s and stops at the 100 mm limit. Here it lands on 100.0 exactly, because the engine's 5 mm steps per scan happen to meet the limit; a real axis with a real ramp stops a few millimetres beyond it, by the braking distance worked out above, plus whatever one scan adds. Holding the button longer does nothing. JogRev takes it back. Press both and nothing moves; switch Manual off mid-jog and it stops.

Following error, limits and the faults that end in ErrorStop

Everything so far has gone to plan. On a real axis, the faults that matter most come from the drive and the mechanics, and they all end the same way: the axis drops into ErrorStop, the running block reports Error (or CommandAborted), and nothing moves until the cause is fixed and MC_Reset has run.

Following error (also called lag or position deviation) is the difference between where the interpolator says the axis should be and where the encoder says it is. Some lag is normal: a position loop with a gain Kv of 30 s⁻¹ running at 500 mm/s lags by about 500 / 30 ≈ 17 mm without velocity feedforward, and by a fraction of a millimetre with it. The drive or motion runtime monitors the lag against a limit, typically a few millimetres on a belt axis and well under a millimetre on a precision ball screw. When the lag exceeds the limit, the axis faults. The causes, roughly in order of how often you will meet them on site: a mechanical jam or a crash; acceleration set higher than the motor and gearbox can deliver; a load heavier than the one the axis was tuned for; a brake that did not release; a loop that was never tuned properly. A following-error fault is the axis telling you it could not do what you asked. Raising the limit until it stops complaining is how gearboxes get broken.

In-position monitoring is the other side of the same coin. A move is not Done the moment the setpoint reaches the target. TIA Portal, for example, waits until the actual position has been within a positioning window (say ±0.05 mm) for a minimum dwell time (say 50 ms), and faults the axis if it does not settle within a tolerance time. That is why a badly tuned axis shows Done late, and why a sequence with tight cycle-time budgets often has more to gain from tuning than from faster moves.

Limits come in three layers, from inside out: software limits in the axis configuration, which the motion runtime enforces on every setpoint; hardware limit switches, normally-closed and wired to the drive or a fast input, which fault the axis when the software has failed; and the mechanical end stops, which are the last resort and are usually damaged when reached. After a hardware limit trip, the axis must be reset and jogged out in the free direction only — most platforms allow exactly that, and refuse motion into the limit until the switch is clear.

Recovering from ErrorStop is always the same dance: find out why (MC_ReadAxisError, plus the drive's own diagnostic buffer), fix the cause, MC_Reset on a rising edge, check that the axis is back in Standstill (or Disabled, if power was lost), and re-home if the fault could have cost the position. Because this engine never puts an axis into ErrorStop, the recovery step is shown as a listing in the style of the CODESYS and TwinCAT libraries:

(* Recovery step in a sequence: generic PLCopen pattern *)
900: (* ErrorStop handling *)
    ReadErr(Axis := AxisX, Enable := TRUE);
    LastAxisError := ReadErr.AxisErrorID;          (* for the HMI and the log *)
    IF ResetEdge.Q AND NOT LimitSwitchActive THEN
        ResetCmd := TRUE;                           (* MC_Reset.Execute, called every scan below *)
    END_IF;
    IF ResetAxis.Done THEN
        ResetCmd := FALSE;
        Step := 0;                                   (* power check, then re-home *)
    ELSIF ResetAxis.Error THEN
        ResetCmd := FALSE;                           (* cause still present: stay here *)
    END_IF;

Vendor reality

The PLCopen handshake travels well. The details around it do not, and these are the ones that cost time when you move between platforms.

Siemens S7-1500 and S7-1200 (TIA Portal). Axes are technology objects: TO_SpeedAxis, TO_PositioningAxis and, on the S7-1500, TO_SynchronousAxis, usually driving SINAMICS S120, S210 or V90 drives over PROFINET with a standard PROFIdrive telegram. The instructions follow PLCopen names (MC_Power, MC_Home, MC_MoveAbsolute, MC_MoveRelative, MC_MoveVelocity, MC_Halt, MC_Reset), with Siemens extensions: the jog is MC_MoveJog, MC_Power has StartMode and StopMode inputs, and MC_Home takes a Mode that selects between direct, passive and active homing and absolute-encoder adjustment. Unlike strict PLCopen, you can read the technology object directly — "Axis_1".ActualPosition, "Axis_1".StatusWord — and most Siemens programs do. Errors come back as a WORD ErrorID, with the technology alarm shown in TIA's diagnostics.

// TIA Portal SCL on an S7-1500: multi-instances in the FB's Static section
#PowerX(Axis := "AxisX", Enable := #DriveEnable, StartMode := 1, StopMode := 0);
#MoveX(Axis := "AxisX", Execute := #Step = 30, Position := #Target,
       Velocity := 500.0, Acceleration := 2000.0, Deceleration := 2000.0, Jerk := 0.0);
#ActualX := "AxisX".ActualPosition;

Rockwell Studio 5000 (ControlLogix, CompactLogix with Kinetix drives). The native instructions are not PLCopen: MSO (servo on), MSF (servo off), MAH (home), MAM (move, with a move type for absolute or incremental), MAJ (jog), MAS (stop), MAFR (fault reset). Each takes the axis tag (AXIS_CIP_DRIVE) and a MOTION_INSTRUCTION tag whose bits do the handshake: .EN, .DN, .ER, .IP (in process) and .PC (process complete). The trap for PLCopen programmers: on MAM, .DN means the instruction was accepted, not that the axis has arrived. Arrival is .PC. In ST there is no Execute pin; the instruction acts when it is scanned, so you call it on a one-shot inside an IF. Position is read straight from the axis tag, AxisX.ActualPosition. Rockwell also publishes a PLCopen-style add-on instruction library for users who want the standard names.

// Studio 5000 ST: move on a one-shot, wait for .PC, not .DN
// (MAM takes some twenty operands; only the first few are shown here)
IF StartMove AND NOT StartMove_Last THEN
    MAM(AxisX, MAM_X, 0, Target, 500.0, Unitspersec, ... );   // move type 0 = absolute
END_IF;
StartMove_Last := StartMove;
AtTarget := MAM_X.PC;

CODESYS SoftMotion (Schneider, Bosch Rexroth ctrlX, Festo, Wago and many others build on it). The SM3_Basic library is close to the standard text: AXIS_REF_SM3 axes in the device tree, MC_Power with extra bRegulatorOn and bDriveStart inputs, MC_Jog, the MC_BUFFER_MODE enumeration, and a long list of SMC_ blocks for what PLCopen does not cover. Error IDs are values of the SMC_ERROR enumeration, which makes them readable in the online view.

Beckhoff TwinCAT 3 (NC PTP). The Tc2_MC2 library implements PLCopen with Beckhoff extensions. MC_Power has Enable_Positive, Enable_Negative and an Override in percent. The AXIS_REF is linked to an NC axis in the motion configuration, and its status is only refreshed when you call AxisX.ReadStatus() at the start of each cycle — forget it and AxisX.Status freezes. Errors come back as NC error numbers, which the documentation lists by range.

Omron Sysmac (NJ/NX). The closest to the standard text of the four: PLCopen blocks with _mcAborting, _mcBuffered and the blending modes as enumeration values, axes as _sAXIS_REF variables, and EtherCAT servos configured in Sysmac Studio.

Next: what is on the belt

A servo axis knows exactly where it is. A conveyor belt carrying forty products usually does not know what any of them are: which one failed the vision check, which one belongs to order 4711, which one a maintenance technician just lifted off by hand. Keeping that knowledge matched to the product as it moves is the job of shift registers, ring buffers and FIFO queues, built from the arrays and loops you already know. Chapter 32 builds them.

Practise this chapter