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