Bench-validated notes for a linear belt driven by one CANStepper node,
using the Python Axis helper (Klipper-style millimetres).
Quick recipe
from canstepper import Axis, CANStepperBus, Param
RD = 77.2 # mm per motor revolution (measure your belt/pulley)
with CANStepperBus.serial("/dev/ttyACM0") as bus:
node = bus.node(1)
node.set_run_current(70).set_hold_current(35)
node.set_microsteps(16)
node.set_closed_loop(False) # open-loop steps (like Klipper manual_stepper)
node.set_stealthchop(True) # SpreadCycle at high I often trips short flags
node.set_direction(True) # flip if +mm goes the wrong way
# speed / accel in deg/s (mm/s → deg/s via RD)
v = 150 / RD * 360
a = 800 / RD * 360
node.set_max_speed(v).set_acceleration(a)
node.enable()
axis = Axis(node, rotation_distance=RD, name="belt")
axis.set_zero() # soft zero at current pose
axis.move_by(50.0, speed=150.0, blocking=True) # +50 mmRunnable scripts in the monorepo:
| Script | Purpose |
|---|---|
examples/belt_move_mm.py | One relative move (default RD 77.2) |
examples/belt_oscillate.py | Absolute ±50 mm soak about a median |
examples/belt_speed_ramp.py | Climb v_cmd for visual max-speed checks |
cd can_stepper
PYTHONPATH=. python3 examples/belt_move_mm.py /dev/ttyACM0 1 50 77.2 150
PYTHONPATH=. python3 examples/belt_oscillate.py /dev/ttyACM0 1 200 10
PYTHONPATH=. python3 examples/belt_speed_ramp.py /dev/ttyACM0 1 150 30 600rotation_distance
rotation_distance is the linear travel per full motor turn (mm), same idea
as Klipper:
degrees = (mm / rotation_distance) × 360Example: 77.2 mm/rev → 10 mm ≈ 46.6° motor shaft.
Direction (invert_dir)
The encoder is fixed to the motor. If a positive millimetre command moves into a hard stop or limit, flip:
node.set_direction(True) # invert_dirbelt_move_mm.py defaults to invert so +mm is away from the limit on
the validation rig; pass noinvert if your machine is the other way.
Absolute oscillation (important)
Do not build long soaks only with chained relative moves like
+50, −100, +100, … if a leg can hit a hard stop. A clipped relative
move shifts the soft coordinate frame and the next “down” can dig into the
lower obstruction (looks like “it only moves down”).
Correct pattern — soft-zero once at the median, then absolute targets:
axis.set_zero() # median = 0
axis.move_to(+50, speed=v) # top
for _ in range(cycles):
axis.move_to(-50, speed=v) # bottom
axis.move_to(+50, speed=v) # topThat is what belt_oscillate.py does. Envelope: ±50 mm about the median
(100 mm full stroke). Park near mid-travel with clearance both ways before start.
Bench speed notes (open-loop, RD 77.2, µ16, StealthChop)
Validation on a belt load (fw 1.7, after fixing a wiring mismatch):
| Regime | Typical v_cmd | Notes |
|---|---|---|
| Everyday | 50–150 mm/s | Comfortable, accurate |
| High | 200–550 mm/s | Full ±50 osc often OK |
| Extreme | ~900–1200 mm/s | Short cycles; cool between bursts |
| Beyond | 2000+ mm/s | Occasional fault 5 or missed steps |
Measured average speed on 100 mm legs is lower than v_cmd because of
accel/decel. Raising v_cmd without enough travel or accel plateaus avg speed.
Prefer ~55–70% run current for long high-speed soaks. Klipper-style 1 A + SpreadCycle + very high step rates on this board often raised fault 5.
TMC faults under high speed
| Code | Name | Meaning |
|---|---|---|
| 4 | DRIVER_OT | Over-temperature shutdown — cool, lower current/duty |
| 5 | DRIVER_SHORT | TMC short / low-side short flag |
Fault 5 can be a real wiring/motor short, or a false trip at high step rate / load (inductive spike). Inspect:
d = node.get_driver_status()
print(d.low_side_short_a, d.low_side_short_b,
d.short_to_gnd_a, d.short_to_gnd_b, d.drv_err, d.cs_actual)Mitigations: cool between stages, slightly lower current, StealthChop on, shorter cycles, fix motor wiring if flags persist at low speed.
Mapping from a Klipper [manual_stepper]
| Klipper | canstepper |
|---|---|
rotation_distance | Axis(..., rotation_distance=…) |
microsteps | node.set_microsteps(16) |
velocity / accel | convert mm/s and mm/s² → deg/s via RD; set_max_speed / set_acceleration |
run_current (A) | percent of driver scale (set_run_current) |
stealthchop_threshold: 0 | prefer try StealthChop first on this board |
MCU step_pin / dir_pin | fixed on the CANStepper board (not host-configured) |
Firmware
Use production GrafitoCANStepper_C3 (GCSP), e.g. fw 1.7+ (HOME pull-up, status GPIO8 raw diagnostics). Not the optional Wi‑Fi portal sketch when running these Python tests.