Skip to main content

ControlForge Motion Control Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.535


1. Overview

ControlForge implements the PLCopen Motion Control function block model — the same standard used by CODESYS, Beckhoff TwinCAT, and Bosch Rexroth IndraWorks. Create axes, enable power, home, and execute absolute/relative/velocity moves with trapezoidal motion profiles, all from Structured Text.

Axes are software-simulated by default with callback hooks for connecting to real hardware (stepper drivers, servo amplifiers, VFDs, or any actuator reachable via GPIO, serial, or protocol).

PLCopen State Machine

Every axis follows the standard PLCopen state diagram:

StateCodeDescription
Disabled0Power off — no motion possible
Standstill1Powered, idle, ready for commands
Homing2Homing sequence active
DiscreteMotion3Point-to-point move in progress
ContinuousMotion4Velocity/jog mode active
SynchronizedMotion5Reserved (not yet implemented)
Stopping6Controlled deceleration to stop
ErrorStop7Fault — requires MC_RESET

2. Axis Lifecycle

Create and Configure

PROGRAM POU_Motion
VAR
axis : INT;
ok : BOOL;
state : INT := 0;
END_VAR

CASE state OF
0: (* Create axis *)
axis := MC_CREATE_AXIS('X');

(* Configure motion parameters *)
MC_CONFIG(axis, 'max_velocity', 1000.0); (* units/sec *)
MC_CONFIG(axis, 'max_accel', 5000.0); (* units/sec² *)
MC_CONFIG(axis, 'max_decel', 5000.0); (* units/sec² *)
MC_CONFIG(axis, 'max_jerk', 50000.0); (* units/sec³ *)
MC_CONFIG(axis, 'units_per_rev', 1000.0); (* encoder scaling *)
state := 1;

1: (* Enable power *)
ok := MC_POWER(axis, TRUE);
IF MC_IS_ENABLED(axis) THEN
state := 2;
END_IF;

2: (* Home the axis *)
ok := MC_HOME(axis);
state := 3;

3: (* Wait for homing complete *)
MC_UPDATE(axis);
IF MC_IS_HOMED(axis) THEN
state := 10;
END_IF;

10: (* Ready for motion commands *)
MC_UPDATE(axis);
END_CASE;
END_PROGRAM

Configuration Parameters

ParameterDefaultUnitsDescription
max_velocity1000.0units/secMaximum travel speed
max_accel5000.0units/sec²Maximum acceleration
max_decel5000.0units/sec²Maximum deceleration
max_jerk50000.0units/sec³Jerk limit (for S-curve profiles)
units_per_rev1000.0units/revEncoder/resolver scaling

3. Motion Commands

All motion commands are non-blocking — they initiate the move and return immediately. Call MC_UPDATE every scan cycle to advance the trajectory. Check MC_IS_MOVING and MC_MOVE_DONE for status.

MC_MOVE_ABSOLUTE — Move to Position

(* Move to position 5000 at 500 units/sec *)
ok := MC_MOVE_ABSOLUTE(axis, 5000.0, 500.0);

(* With custom accel/decel *)
ok := MC_MOVE_ABSOLUTE(axis, 5000.0, 500.0, 2000.0, 2000.0);
ParamTypeRequiredDescription
axis_idINTYesAxis identifier
positionREALYesTarget position (absolute)
velocityREALNoTravel speed (default: max_velocity)
accelREALNoAcceleration (default: max_accel)
decelREALNoDeceleration (default: max_decel)

MC_MOVE_RELATIVE — Move by Distance

(* Move 1000 units forward *)
ok := MC_MOVE_RELATIVE(axis, 1000.0, 500.0);

(* Move 500 units backward *)
ok := MC_MOVE_RELATIVE(axis, -500.0, 200.0);

Same parameters as MC_MOVE_ABSOLUTE, but distance instead of position.

MC_MOVE_VELOCITY — Continuous Motion (Jog)

Alias: MC_JOG

(* Jog forward at 100 units/sec *)
ok := MC_MOVE_VELOCITY(axis, 100.0);

(* Jog reverse *)
ok := MC_MOVE_VELOCITY(axis, -50.0);

(* Jog with custom acceleration *)
ok := MC_MOVE_VELOCITY(axis, 200.0, 1000.0);

Runs continuously until MC_STOP or MC_HALT. Enters ContinuousMotion state.

MC_STOP — Controlled Stop

(* Stop with default deceleration *)
ok := MC_STOP(axis);

(* Stop with custom deceleration *)
ok := MC_STOP(axis, 10000.0);

MC_HALT — Emergency Stop

ok := MC_HALT(axis);

Stops with 2x the configured max_decel. Enters ErrorStop state — requires MC_RESET before new commands.


4. Cyclic Update

MC_UPDATE must be called every scan cycle for each active axis. It advances the trajectory simulation, updates position and velocity, and handles state transitions.

(* In your scan loop — REQUIRED *)
MC_UPDATE(axis);

(* With explicit time step (default: 1ms) *)
MC_UPDATE(axis, 0.001); (* dt in seconds *)

If you forget to call MC_UPDATE, the axis position will never change.


5. Status and Monitoring

Position and Velocity

pos := MC_READ_POSITION(axis); (* Current position *)
vel := MC_READ_VELOCITY(axis); (* Current velocity *)

Motion Status

IF MC_IS_MOVING(axis) THEN
(* Motion in progress *)
END_IF;

IF MC_MOVE_DONE(axis) THEN
(* Move completed — start next move *)
END_IF;

IF MC_IS_ENABLED(axis) THEN ... END_IF;
IF MC_IS_HOMED(axis) THEN ... END_IF;

Full Status

status := MC_READ_STATUS(axis);
(* Returns map:
state — PLCopen state code (0-7)
enabled — Power enabled
homed — Homing complete
error — Error active
actual_position — Current position
actual_velocity — Current velocity
target_position — Commanded position
move_active — Motion in progress
move_complete — Move finished
*)

State Code

state_code := MC_GET_STATE(axis);
(* 0=Disabled, 1=Standstill, 2=Homing, 3=DiscreteMotion,
4=ContinuousMotion, 5=Synchronized, 6=Stopping, 7=ErrorStop *)

Error Handling

err := MC_READ_ERROR(axis);
(* Returns: {error: bool, error_id: int, error_msg: string} *)

IF MC_GET_STATE(axis) = 7 THEN (* ErrorStop *)
MC_RESET(axis); (* Clear error → Standstill *)
END_IF;

Set Position (Homing Override)

MC_SET_POSITION(axis, 0.0); (* Zero the axis at current physical location *)

List All Axes

axes := MC_LIST_AXES();
(* Returns array of axis IDs *)

6. Motion Profile

Currently supports trapezoidal profiles:

Velocity
^
| ┌────────────────┐
| /│ │\
| / │ Constant Vel │ \
| / │ │ \
| / │ │ \
| / │ │ \
+--/-----+----------------+-----\----> Time
Accel Decel

The profile ensures:

  • Acceleration phase ramps up to commanded velocity
  • Constant velocity phase (if distance allows)
  • Deceleration phase ramps down to zero at target
  • Short moves may be triangular (no constant velocity phase)

7. Complete Example: Pick and Place

A 3-axis pick-and-place machine:

PROGRAM POU_PickAndPlace
VAR
x_axis : INT;
y_axis : INT;
z_axis : INT;
state : INT := 0;
ok : BOOL;

(* Positions *)
pick_x : REAL := 1000.0;
pick_y : REAL := 500.0;
pick_z : REAL := 100.0;
place_x : REAL := 3000.0;
place_y : REAL := 1500.0;
place_z : REAL := 100.0;
safe_z : REAL := 500.0;

gripper : BOOL := FALSE;
END_VAR

CASE state OF
0: (* Initialize axes *)
x_axis := MC_CREATE_AXIS('X');
y_axis := MC_CREATE_AXIS('Y');
z_axis := MC_CREATE_AXIS('Z');

MC_CONFIG(x_axis, 'max_velocity', 2000.0);
MC_CONFIG(y_axis, 'max_velocity', 2000.0);
MC_CONFIG(z_axis, 'max_velocity', 500.0);

MC_POWER(x_axis, TRUE);
MC_POWER(y_axis, TRUE);
MC_POWER(z_axis, TRUE);
state := 1;

1: (* Home all axes *)
MC_HOME(x_axis);
MC_HOME(y_axis);
MC_HOME(z_axis);
state := 2;

2: (* Wait for all homed *)
IF MC_IS_HOMED(x_axis) AND MC_IS_HOMED(y_axis) AND MC_IS_HOMED(z_axis) THEN
state := 10;
END_IF;

10: (* Move Z to safe height *)
MC_MOVE_ABSOLUTE(z_axis, safe_z, 500.0);
state := 11;

11: IF MC_MOVE_DONE(z_axis) THEN
(* Move XY to pick position *)
MC_MOVE_ABSOLUTE(x_axis, pick_x, 2000.0);
MC_MOVE_ABSOLUTE(y_axis, pick_y, 2000.0);
state := 12;
END_IF;

12: IF MC_MOVE_DONE(x_axis) AND MC_MOVE_DONE(y_axis) THEN
(* Lower Z to pick *)
MC_MOVE_ABSOLUTE(z_axis, pick_z, 200.0);
state := 13;
END_IF;

13: IF MC_MOVE_DONE(z_axis) THEN
gripper := TRUE; (* Close gripper *)
state := 14;
END_IF;

14: (* Raise Z *)
MC_MOVE_ABSOLUTE(z_axis, safe_z, 500.0);
state := 15;

15: IF MC_MOVE_DONE(z_axis) THEN
(* Move XY to place position *)
MC_MOVE_ABSOLUTE(x_axis, place_x, 2000.0);
MC_MOVE_ABSOLUTE(y_axis, place_y, 2000.0);
state := 16;
END_IF;

16: IF MC_MOVE_DONE(x_axis) AND MC_MOVE_DONE(y_axis) THEN
(* Lower Z to place *)
MC_MOVE_ABSOLUTE(z_axis, place_z, 200.0);
state := 17;
END_IF;

17: IF MC_MOVE_DONE(z_axis) THEN
gripper := FALSE; (* Open gripper *)
state := 18;
END_IF;

18: (* Raise Z and cycle back *)
MC_MOVE_ABSOLUTE(z_axis, safe_z, 500.0);
state := 19;

19: IF MC_MOVE_DONE(z_axis) THEN
state := 10; (* Repeat cycle *)
END_IF;
END_CASE;

(* REQUIRED: Update all axes every scan *)
MC_UPDATE(x_axis);
MC_UPDATE(y_axis);
MC_UPDATE(z_axis);

END_PROGRAM

8. Complete Example: Jog Panel

Manual jogging from HMI buttons:

PROGRAM POU_JogPanel
VAR
axis : INT;
initialized : BOOL := FALSE;

(* HMI inputs *)
jog_fwd : BOOL;
jog_rev : BOOL;
jog_speed : REAL := 100.0;
move_to_pos : REAL;
go_cmd : BOOL;
home_cmd : BOOL;
stop_cmd : BOOL;

(* HMI outputs *)
current_pos : REAL;
current_vel : REAL;
is_moving : BOOL;
is_homed : BOOL;
axis_state : INT;
END_VAR

IF NOT initialized THEN
axis := MC_CREATE_AXIS('manual');
MC_CONFIG(axis, 'max_velocity', 500.0);
MC_POWER(axis, TRUE);
initialized := TRUE;
END_IF;

(* Jog control *)
IF jog_fwd AND NOT jog_rev THEN
MC_MOVE_VELOCITY(axis, jog_speed);
ELSIF jog_rev AND NOT jog_fwd THEN
MC_MOVE_VELOCITY(axis, -jog_speed);
ELSIF MC_GET_STATE(axis) = 4 THEN (* ContinuousMotion *)
MC_STOP(axis);
END_IF;

(* Point-to-point move *)
IF go_cmd THEN
MC_MOVE_ABSOLUTE(axis, move_to_pos, jog_speed);
go_cmd := FALSE;
END_IF;

(* Home *)
IF home_cmd THEN
MC_HOME(axis);
home_cmd := FALSE;
END_IF;

(* Emergency stop *)
IF stop_cmd THEN
MC_HALT(axis);
stop_cmd := FALSE;
END_IF;

(* Error recovery *)
IF MC_GET_STATE(axis) = 7 THEN
MC_RESET(axis);
END_IF;

(* Update trajectory *)
MC_UPDATE(axis);

(* Feedback to HMI *)
current_pos := MC_READ_POSITION(axis);
current_vel := MC_READ_VELOCITY(axis);
is_moving := MC_IS_MOVING(axis);
is_homed := MC_IS_HOMED(axis);
axis_state := MC_GET_STATE(axis);

END_PROGRAM

9. Hardware Integration

By default, axes are software-simulated — position and velocity are calculated mathematically. To connect to real hardware, register callback hooks:

CallbackTriggered ByUse
OnPowerChangeMC_POWEREnable/disable servo drive
OnMoveMC_MOVE_*Send position/velocity commands to drive
OnHomeMC_HOMETrigger drive homing sequence
OnStopMC_STOP/HALTSend stop command to drive

Hardware integration examples:

  • Stepper/Dir via P2: MC_MOVE triggers P2_CMD with step pulses
  • Modbus VFD: MC_MOVE_VELOCITY writes speed register via MB_WRITE_REGISTER
  • EtherNet/IP servo: MC_MOVE sends tag writes via ENIP_SCANNER_WRITE_REAL
  • G-code machine: MC_MOVE generates G1 commands via GCODE_SEND_CMD

The software axis handles trajectory planning (acceleration profiles, position tracking) — the callback just sends the output to hardware.


Appendix A: Quick Reference

FunctionParametersReturnsDescription
MC_CREATE_AXIS(name)1INTCreate axis, returns ID
MC_CONFIG(id, param, value)3BOOLSet axis parameter
MC_POWER(id, enable)2BOOLEnable/disable power
MC_HOME(id)1BOOLStart homing
MC_RESET(id)1BOOLClear error state
MC_MOVE_ABSOLUTE(id, pos [,vel,acc,dec])2-5BOOLMove to position
MC_MOVE_RELATIVE(id, dist [,vel,acc,dec])2-5BOOLMove by distance
MC_MOVE_VELOCITY(id, vel [,acc,dec])2-4BOOLContinuous velocity/jog
MC_JOG(id, vel [,acc,dec])2-4BOOLAlias for MOVE_VELOCITY
MC_STOP(id [,decel])1-2BOOLControlled stop
MC_HALT(id)1BOOLEmergency stop (2x decel)
MC_UPDATE(id [,dt])1-2BOOLCyclic — advance trajectory
MC_READ_POSITION(id)1REALCurrent position
MC_READ_VELOCITY(id)1REALCurrent velocity
MC_READ_STATUS(id)1MAPFull axis status
MC_READ_ERROR(id)1MAPError info
MC_GET_STATE(id)1INTPLCopen state code (0-7)
MC_IS_ENABLED(id)1BOOLPower on?
MC_IS_HOMED(id)1BOOLHoming done?
MC_IS_MOVING(id)1BOOLMotion active?
MC_MOVE_DONE(id)1BOOLMove complete?
MC_SET_POSITION(id, pos)2BOOLOverride position
MC_LIST_AXES()0ARRAYAll axis IDs

ControlForge v1.0.535 | PLCopen Motion Control | 23 Functions | Trapezoidal Profiles

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides