The Grafito CAN Stepper Protocol v1 is the contract between hosts and
boards. The Python reference codec is canstepper/protocol.py; the firmware
implements the same tables.
Physical layer
- CAN 2.0A, standard 11-bit identifiers, 1 Mbps.
- Daisy-chain, 120 Ω termination at both physical ends, up to 31 nodes.
Addressing
CAN ID (11 bit) = (node_id << 6) | msg_id| Field | Bits | Range | Meaning |
|---|---|---|---|
| node_id | 10..6 | 0–31 | 0 = broadcast (all nodes execute), 1–31 = one board |
| msg_id | 5..0 | 0–63 | 0–31 command (host to node), 32–63 telemetry (node to host) |
- All multi-byte values are little-endian.
- A remote frame (RTR) with a telemetry msg_id polls that telemetry.
- Nodes broadcast telemetry periodically: the fast group (POSITION,
MOTION, PID_STATUS) at
fast_rate_hz(default 10 Hz), the slow group (STATUS, DRIVER, ENV, FOLLOW_STATUS, CAN_HEALTH) atslow_rate_hz(default 1 Hz). Rate 0 disables a group.
Serial bridge
Any node bridges its USB CDC port (115200 baud) to the bus; one text line per frame in each direction:
<CAN_ID hex> <RTR 0|1> <payload hex>\nExample — move node 1 to 180°: 44 0 0000000000C06640
(0x44 = (1 shl 6) or 4 = MOVE_ABS; payload is f64 180.0).
- Payload length = hex digits / 2 (0–8 bytes). RTR lines carry no payload.
- Lines starting with
#are debug output — skip them. - The bridging node executes frames addressed to it (or broadcast) and re-transmits them on CAN; everything it sees on the bus, plus its own telemetry, is printed to the port.
Commands (msg_id 0–31)
| # | Name | Payload | Behavior |
|---|---|---|---|
| 0 | PING | — | Node answers with STATUS immediately |
| 1 | ESTOP | — | Instant stop + driver disable. Latched until ENABLE 1 |
| 2 | STOP | — | Decelerating stop, driver stays energized |
| 3 | ENABLE | u8 0/1 | Driver off/on; enabling clears the e-stop latch |
| 4 | MOVE_ABS | f64 deg | Absolute position. OL: FAS trap. CL fw ≥1.2: trap plan + v_ff + PID |
| 5 | MOVE_REL | f64 deg | Relative to active target (or current). Same profiles as MOVE_ABS |
| 6 | MOVE_VEL | f32 deg/s | Signed continuous velocity; 0 = ramped stop |
| 7 | SET_ZERO | — | Stop; current angle becomes 0° |
| 8 | HOME | u8 method, i8 dir, f32 speed | Homing routine (methods: 0 set-zero, 1 endstop, 2 StallGuard) |
| 9 | SET_PARAM | u8 id + 4-byte value | Node replies with PARAM (status + readback) |
| 10 | GET_PARAM | u8 id | Node replies with PARAM |
| 11 | SAVE_CONFIG | — | Persist parameter table to flash |
| 12 | LOAD_DEFAULTS | — | Factory defaults + persist; node keeps its ID |
| 13 | FOLLOW | u8 en, u8 leader, u8 flags, u8 rsvd, f32 ratio | flags: bit0 invert, bit1 encoder-corrected |
| 14 | FOLLOW_SYNC | — | Recapture leader/local zero on next leader frame |
| 15 | SET_POSITION | f64 deg | Relabel current shaft position without physical movement |
| 16–31 | reserved | ignored |
Closed-loop motion profile (firmware ≥1.2)
When closed_loop = 1 and the node receives MOVE_ABS / MOVE_REL:
- Plan a rest-to-rest trapezoid (or triangle if short) from the encoder
angle to the target using
vmax = min(max_speed, cl_max_speed)andamax = cl_max_accel. - Each 5 ms: reference position
r(t)and feedforward velocityv_ff(t). - Track:
v_cmd = v_ff + Kp·(r − encoder) + Ki·∫ − Kd·v_meas. - Settle when the plan is finished,
|target − encoder| ≤ pid_tolerance, and speed is low →MOVE_DONE(event 7).
Open-loop position uses FastAccelStepper’s trapezoid on step counts.
MOVE_VEL is open-loop continuous velocity. FOLLOW with encoder correction
uses a continuous braking-law chase (not a full re-planned trap each frame).
Tuning, measured RPM, and soak data: Closed-loop speed tuning.
Parameters
4-byte values, u32 or f32. Ranges are hardware-sanity checks only — motion limits are configurable defaults, never firmware clamps. Persisted by SAVE_CONFIG, loaded at power-on.
| ID | Name | Type | Default | Range | Description |
|---|---|---|---|---|---|
| 1 | node_id | u32 | 1 | 1–31 | CAN address |
| 2 | steps_per_rev | u32 | 200 | 1–100000 | full steps per motor rev |
| 3 | microsteps | u32 | 16 | 1–256, power of 2 | TMC microstepping |
| 4 | run_current | u32 | 30 | 1–100 | % of driver max |
| 5 | hold_current | u32 | 15 | 0–100 | standstill current, % |
| 6 | stall_threshold | u32 | 10 | 0–255 | SGTHRS, higher = more sensitive |
| 7 | invert_dir | u32 | 0 | 0/1 | flip physical rotation |
| 8 | closed_loop | u32 | 1 | 0/1 | encoder position loop |
| 9 | max_speed | f32 | 720 | positive | open-loop position cruise, deg/s |
| 10 | acceleration | f32 | 2880 | positive | open-loop position ramp, deg/s² |
| 11 | cl_max_speed | f32 | 720 | positive | CL trapezoid cruise vmax, deg/s (fw ≥1.2) |
| 12 | cl_max_accel | f32 | 2880 | positive | CL trapezoid accel/decel, deg/s² |
| 13 | pid_kp | f32 | 12.0 | ≥ 0 | tracking Kp on (r − encoder); v_ff carries the move |
| 14 | pid_ki | f32 | 0.3 | ≥ 0 | tracking Ki |
| 15 | pid_kd | f32 | 0.10 | ≥ 0 | D-on-measured-velocity |
| 16 | pid_tolerance | f32 | 0.35 | 0.001–360 | settle window, deg |
| 17 | fast_rate_hz | u32 | 10 | 0–500 | POSITION/MOTION/PID_STATUS |
| 18 | slow_rate_hz | u32 | 1 | 0–500 | STATUS/DRIVER/ENV/… |
| 19 | enable_on_boot | u32 | 1 | 0/1 | |
| 20 | zero_on_boot | u32 | 0 | 0/1 | |
| 21 | standstill_mode | u32 | 0 | 0–3 | normal / freewheel / brake / strong |
| 22 | endstop_enable | u32 | 0 | 0/1 | |
| 23 | endstop_active_high | u32 | 0 | 0/1 | 0 = active low, pull-up |
| 24 | endstop_action | u32 | 1 | 0–2 | report / stop / stop+zero |
| 25 | homing_current | u32 | 0 | 0–100 | 0 = keep run current |
| 26 | homing_backoff | f32 | 2.0 | ≥ 0 | deg |
| 27 | homing_timeout_ms | u32 | 30000 | 100–600000 | |
| 28 | stealthchop | u32 | 1 | 0/1 | off = SpreadCycle |
Telemetry (msg_id 32–63)
| # | Name | Payload | When |
|---|---|---|---|
| 32 | STATUS | u8 flags, u8 mode, u8 fault, u8 fw_major, u8 fw_minor, u8 proto, u8 node_id | slow, PING, RTR |
| 33 | POSITION | f64 encoder deg (multi-turn) | fast, RTR — drives followers |
| 34 | MOTION | f32 velocity deg/s, f32 position error deg | fast, RTR |
| 35 | TARGET | f64 active target deg | RTR |
| 36 | EVENT | u8 event, u8 detail, f32 data | on occurrence |
| 37 | DRIVER | u16 StallGuard, u8 TMC-UART-ok | slow, RTR |
| 38 | ENV | f32 MCU temp °C, f32 bus volts | slow, RTR |
| 39 | PARAM | u8 id, u8 status (0 ok / 1 unknown / 2 rejected), 4-byte value | reply to SET/GET_PARAM |
| 40 | FOLLOW_STATUS | u8 en, u8 leader, u8 flags, u8 synced, f32 ratio | slow (following), RTR |
| 41 | CAN_HEALTH | u8 state, u8 tx_err, u8 rx_err, u8 recoveries, u16 tx_failed, u16 bus_errors | slow, RTR |
| 42 | ENC_COUNTS | i64 counts (16384 per rev) | RTR |
| 43 | PID_STATUS | u8 state, u8 fault, f32 output deg/s | fast (closed loop), RTR |
| 44–63 | reserved |
STATUS flag bits
| Bit | Meaning |
|---|---|
| 0x01 | driver enabled |
| 0x02 | moving |
| 0x04 | homed |
| 0x08 | e-stop latched |
| 0x10 | endstop active |
| 0x20 | stall latched |
| 0x40 | encoder frame OK |
Enumerations
- mode: 0 idle, 1 position, 2 velocity, 3 homing, 4 following
- fault: 0 none, 1 encoder invalid, 2 no progress, 3 homing timeout
- event: 1 boot, 2 endstop hit, 3 endstop released, 4 stall,
5 homing done, 6 homing failed, 7 move done, 8 e-stop, 9 fault
(
datais usually the position in degrees at the event)
Leader / follower
FOLLOW makes a node track another node's POSITION broadcasts entirely on-bus. The first leader frame after enable (or FOLLOW_SYNC) captures both zero references, so engagement never jumps:
follower = local_ref + (leader − leader_ref) × ratio × (invert ? −1 : 1)With flags bit1 the follower closes its own encoder loop on the mapped target (recommended); otherwise it steps open-loop. A 0.15° hysteresis deadband rejects stationary leader-encoder noise without adding lag.
Bus load
A 1 Mbps standard frame with 8-byte payload is roughly 111–135 bits with
stuffing. At default rates (about 35 frames/s per node), 31 nodes produce
about 1085 frames/s — roughly 15 % bus load. Budget accordingly when
raising fast_rate_hz.