Motion Axes: Creating and Managing
Getting a motor to move is, for most people, the hardest part of bringing up a new machine. This chapter is the authoritative guide to axes in AutoCore: what they are, how to create them, how to configure their behavior, and how to avoid the handful of mistakes that cost people a day each. Chapter 8 walks through the EtherCAT-specific scan/PDO steps with a concrete Teknic example; this chapter is the reference that example points back to.
What an axis is
An axis is a high-level motion abstraction. It pairs:
- a motion core — the CiA-402 state machine plus scaling, homing, and limit
configuration (
autocore_std::motion::Axis+AxisConfig), and - a backend — where the drive actually lives.
The motion core is fieldbus-agnostic. The generic Axis controller talks to
its drive through a narrow AxisView trait (control word, status word, target,
feedback, modes), so the same control-program code drives:
- an EtherCAT servo (the
AxisViewis generated PDO wiring), or - a virtual / simulated drive (the
AxisViewis an in-processSimDrive), with no hardware at all.
This is why an axis is defined separately from the slave. The slave entry
describes the physical EtherCAT device and its PDOs; the axis entry describes the
motion abstraction layered on top. Swapping the motor to a different drive is a
one-line change to the axis (link), and the control program never changes.
Key idea. You write your process logic against the axis, not the drive. The axis is the stable contract; the backend is an implementation detail.
Where axes live
Axes are listed in an axes array. There are two homes, both read by codegen:
| Location | For | Notes |
|---|---|---|
modules.ethercat.config.axes | EtherCAT-backed axes | The legacy/default home. Every existing project uses it. An untagged axis here is an EtherCAT axis. |
modules.motion.config.axes | Backend-neutral axes (e.g. virtual) | The home for axes that aren’t tied to the EtherCAT bus. |
You normally don’t choose by hand — acctl add-axis puts each axis in the right
place. EtherCAT axes can stay untagged in the ethercat config (fully
back-compatible); virtual axes carry an explicit backend tag.
Creating an axis
The fast way: acctl add-axis
# An EtherCAT axis bound to a scanned slave:
acctl add-axis --name Press --link AKD_3
# A virtual / simulated axis (no fieldbus):
acctl add-axis --name SimShuttle --backend virtual
| Flag | Default | Meaning |
|---|---|---|
--name | (required) | Axis name → the generated handle struct (e.g. Press). Describe the function, not the hardware. |
--link | — | Slave name to bind to. Required for ethercat, omitted for virtual. |
--type | pp | CiA-402 profile type (Profile Position). |
--backend | ethercat | ethercat or virtual. |
The command is idempotent on --name and writes the axis into the correct home.
It does not seed drive-behavior defaults — see
Drive-behavior defaults
below — because the CLI can’t read the EtherCAT device library; the IDE or a hand
edit fills those in.
After adding an axis, run acctl codegen to generate its drive handle.
By hand
Add an object to the appropriate axes array. A minimal EtherCAT axis:
"modules": { "ethercat": { "config": {
"axes": [
{ "name": "Press", "link": "AKD_3", "type": "pp" }
],
"slaves": [ /* ... */ ]
} } }
A virtual axis goes in the neutral home and carries the backend tag:
"modules": { "motion": { "enabled": false, "config": {
"axes": [
{ "name": "SimShuttle", "type": "pp", "backend": { "kind": "virtual" } }
]
} } }
modules.motionis a config-only namespace, not a runtime module — set"enabled": false. Codegen reads itsconfig.axes, but there is no motion module binary; leaving it enabled (the default) makes the server’s supervisor try to spawn a nonexistent “motion” executable.acctl add-axissets this for you.
The axis schema
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Axis name → generated handle struct. |
link | string | ethercat only | Slave name this axis binds to (matches a slaves entry). |
type | string | yes | CiA-402 profile, currently "pp". |
backend | object | no | { "kind": "ethercat" } (default if omitted) or { "kind": "virtual" }. An optional link inside the ethercat backend overrides the top-level link. |
options | object | no | Sensor/limit wiring and drive-behavior traits — see below. |
outputs | object | no | Axis status values published to GlobalMemory each tick. |
options
| Field | Type | Default | Description |
|---|---|---|---|
positive_limit | string | — | GM bool for the positive limit switch |
negative_limit | string | — | GM bool for the negative limit switch |
home_sensor | string | — | GM bool for the home reference sensor |
error_code | string | — | GM u16 for the drive error code |
maximum_pos_limit | string | — | GM numeric: dynamic max software position limit (user units) |
minimum_pos_limit | string | — | GM numeric: dynamic min software position limit (user units) |
invert_direction | bool | false | Negate position targets and feedback (reverse direction in software) |
halt_blocks_setpoint_ack | bool | false | Drive trait. See Drive-behavior defaults. |
soft_home_method | int | 37 | CiA-402 homing method (0x6098) for “current position = home”. E.g. 35 for the Inovance SV660N. |
The string-valued options name GM variables; the bool/int options are baked
directly into the generated handle’s AxisConfig. Software-position-limit
behavior is covered in Chapter 8.
outputs
Each field names a GM variable that the generated tick() writes every cycle
(all optional):
| Field | GM type | Field | GM type | |
|---|---|---|---|---|
position | f64 | in_motion | bool | |
raw_position | i64 | moving_positive | bool | |
speed | f64 | moving_negative | bool | |
is_busy | bool | at_max_limit | bool | |
is_error | bool | at_min_limit | bool | |
error_code | u32/i32 | at_positive_limit_switch | bool | |
error_message | string | at_negative_limit_switch | bool | |
motor_on | bool | home_sensor | bool |
You do not hand-create these variables. Name them in
outputs/options, andacctl codegenscaffolds the missing ones into the shared-memory layout with the right types (see The workflow).
Drive-behavior defaults (the thing that bites people)
Different CiA-402 drives disagree on subtle protocol behavior. The one that costs
people a day is the halt cancel handshake, surfaced as
halt_blocks_setpoint_ack.
Symptom
You command a stop (e.g. move-to-load reaches its target and calls halt()).
The motor physically stops, but the axis reports an error:
Halt timeout: cancel not acknowledged
Cause
To cleanly close out a halt, the axis issues a new set-point (current position, zero velocity) and waits for the drive to acknowledge it (status-word bit 12). Drives disagree on whether they’ll acknowledge a set-point while Halt (control word bit 8) is still asserted:
- Kollmorgen AKD, Inovance SV660N — will not acknowledge while halted, so
the handshake must clear halt first. This is the CiA-402-standard behavior; set
halt_blocks_setpoint_ack: true. - Teknic ClearPath — acknowledges while halted, and resumes the prior move
if you clear halt early, so it needs halt held through the handshake. It is the
oddball; leave
halt_blocks_setpoint_ack: false.
The default is false — chosen as the safe fallback, not the common case.
On an unknown drive, a wrong false merely times out with the motor already
stopped (loud and safe); a wrong true could provoke unexpected motion. So if
you hit the halt timeout on an AKD/Inovance, the fix is to set the option true.
soft_home_method
The CiA-402 method (0x6098) used when declaring “current position = home”. The
default 37 works on modern drives (Teknic ClearPath); the Inovance SV660N’s
range stops at 35. Set it per drive when homing won’t latch.
Seeding defaults instead of memorizing them
These values are properties of the drive model, recorded in the EtherCAT
device library (device_definitions.json, via ext_definitions/<vendor>.json).
You don’t have to remember which drive needs what:
- In the IDE — run
autocore: Seed axis drive defaults. It asks the live target (ethercat.list_devices) for each bound drive’s profile and writes the matchingoptionsintoproject.json. It is sparse (only writes what the drive declares — Teknic gets nothing, so it stays at the safe default) and non-destructive (never overwrites a value you set, so re-running is safe). - By hand — set
halt_blocks_setpoint_ack/soft_home_methodin the axisoptionsper the table above.
Either way the value ends up in project.json options and is baked into the
generated handle by acctl codegen.
Virtual / simulated axes
A virtual axis has no fieldbus. Its AxisView is an in-process SimDrive
(autocore_std::motion::sim::SimDrive) that emulates a CiA-402 Profile Position
drive: the enable state machine, the set-point-acknowledge handshake, and motion
integration. Because Axis is generic over AxisView, your process code is
identical whether the axis is real or simulated.
acctl add-axis --name SimPress --backend virtual
acctl codegen
Use them to:
- develop and test process logic with no hardware (CI-friendly), and
- A/B a process against sim vs. a real drive by swapping the axis backend.
Notes and limits:
- The generated virtual handle’s
sync()advances the sim by a fixedSIM_DT(default 10 ms). Match it to your control program’s scan period if timing matters. - SDO and persistent-position methods are omitted on the virtual handle (they’re meaningless without a fieldbus).
- Virtual axes require autocore-std ≥ 3.3.55 (the version that introduced
SimDrive).acctl codegenchecks this for you — see The workflow.
The workflow
acctl add-axis --name Press --link AKD_3 # 1. create the axis
# (IDE: "Seed axis drive defaults") # 2. seed drive-behavior options
acctl codegen # 3. generate gm.rs + scaffold vars
acctl push control --start # 4. build + deploy
What acctl codegen does for axes, server-side:
-
Generates the drive handle in
control/src/gm.rs(an EtherCATDriveHandleor aSimDrive-backed handle), baking in theoptions(invert_direction,halt_blocks_setpoint_ack,soft_home_method). -
Scaffolds the axis variables. The
outputs/optionsvariable names are injected into the shared-memory layout automatically (via the server’sProject::normalize), for axes in both homes. You do not runethercat.generate_variablesfor axis variables — that command now handles only PDO variables. -
Checks the autocore-std floor. Codegen emits constructs that need a minimum autocore-std (
GmCompat→ 3.3.52,halt_blocks_setpoint_ack→ 3.3.54,SimDrive→ 3.3.55). Ifcontrol/Cargo.lockpins an older version,acctl codegenstops before writinggm.rswith an actionable message rather than letting you hit a cryptic Rust compile error:✗ autocore-std too old for this server's codegen output. control/Cargo.lock pins 3.3.51, but the generated gm.rs needs ≥ 3.3.55. Bump it, then re-run codegen: (cd control && cargo update -p autocore-std --precise 3.3.55)
Using the generated handle
The handle bundles the Axis state machine with its backend (a PDO snapshot or a
SimDrive). Each tick:
sync(gm)— pull feedback (EtherCAT: TxPDO from shared memory; virtual: advance the sim).- Issue commands —
enable(),move_absolute(pos, vel, accel, decel),move_relative(...),home(method),halt(),reset_faults(),set_position(...),disable(). tick(gm, client)— advance the state machine and publish outputs.
Status reads mirror the outputs: position(), speed(), is_busy(),
is_error(), error_code(), error_message(), motor_on(), in_motion().
See Chapter 8 for a full control-program example.
Managing axes
- Swap the drive — change the axis
linkto the new slave name and re-runacctl codegen. The control program is untouched. - Change behavior — edit
optionsand re-runacctl codegen. Re-running the IDE seed command won’t clobber a value you set by hand. - Rename — changing
namerenames the generated handle struct; update the control program’s references. - Remove — delete the axis object and re-run
acctl codegen. (Orphaned output variables can be removed fromvariablesif you no longer want them.)
Adding, removing, or re-binding an axis does not change the shared-memory layout hash unless it adds or removes output variables — so an axis edit is generally a recompile, not a server-restart.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Halt timeout: cancel not acknowledged (motor stops, axis errors) | Drive won’t ack a set-point while halted (AKD/Inovance) | Set halt_blocks_setpoint_ack: true (or run the IDE seed command). |
| Homing never latches | Wrong CiA-402 homing method for the drive | Set soft_home_method (e.g. 35 for SV660N). |
gm.<axis var> not found when building | Axis output variable not scaffolded | Run acctl codegen (it injects them). Confirm the outputs name is spelled the same everywhere. |
cannot find SimDrive / no field halt_blocks_setpoint_ack / GmCompat not found | Control program’s autocore-std is older than the codegen output needs | Bump autocore-std (the acctl codegen gate prints the exact --precise version). |
| Move rejected / quick-stop near a limit | Dynamic software position limit tripped | Check maximum_pos_limit / minimum_pos_limit GM values; a move away from the limit is always allowed. |