Grafito CANStepper

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 mm

Runnable scripts in the monorepo:

ScriptPurpose
examples/belt_move_mm.pyOne relative move (default RD 77.2)
examples/belt_oscillate.pyAbsolute ±50 mm soak about a median
examples/belt_speed_ramp.pyClimb 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 600

rotation_distance

rotation_distance is the linear travel per full motor turn (mm), same idea as Klipper:

degrees = (mm / rotation_distance) × 360

Example: 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_dir

belt_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)  # top

That 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):

RegimeTypical v_cmdNotes
Everyday50–150 mm/sComfortable, accurate
High200–550 mm/sFull ±50 osc often OK
Extreme~900–1200 mm/sShort cycles; cool between bursts
Beyond2000+ mm/sOccasional 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

CodeNameMeaning
4DRIVER_OTOver-temperature shutdown — cool, lower current/duty
5DRIVER_SHORTTMC 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]

Klippercanstepper
rotation_distanceAxis(..., rotation_distance=…)
microstepsnode.set_microsteps(16)
velocity / accelconvert 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: 0prefer try StealthChop first on this board
MCU step_pin / dir_pinfixed 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.

On this page