Grafito CANStepper

1. What you need

  • A CANStepper Adapter Board mounted on a NEMA17, diametric magnet centered over the encoder.
  • 5–24 V DC supply (typically 24 V; max 24 V) and a USB-C cable.
  • For more than one board: CAN daisy-chain cables. Termination is enabled by default on each board (0 Ω jumper → 120 Ω path). Keep it on the two physical ends of the bus; remove the 0 Ω resistor on mid-chain boards (or short the termination pads on ends if needed). See Hardware → CAN bus rules.

2. Flash the firmware (once per board)

Full steps (Arduino IDE, arduino-cli, CANopen node 2, failure table): Flashing firmware.

  1. Apply main Vin (typically 24 V) — USB does not power the ESP32 for programming. Keep power connected during upload.
  2. Arduino IDE or arduino-cli with the esp32 core. Board: ESP32C3 Dev Module, Tools → USB CDC On Boot: Enabled.
  3. Libraries: FastAccelStepper (gin66) and TMC2209 (janelia-arduino).
  4. Download firmware 1.12 from the Firmware page (or GrafitoCANStepper_C3.ino), open it, and upload over USB-C with Vin applied.
  5. Serial monitor at 115200 baud. Healthy boot looks like:
# GrafitoCANStepper fw 1.12 proto 1
# HOME pin GPIO8 INPUT_PULLUP raw=HIGH (strapping: keep HIGH at power-on)
# HOME: INPUT_PULLUP + endstop enable=1 active_high=1 action=stop (fw 1.12)
# POWER: enable_on_boot=0 hold<=5 run<=40 standstill=freewheel (fw 1.9)
# MT6701 SSI mode=3 encoder-loss-filter=25ms
# CAN started (1 Mbps)

1.9 does not energize the motor at boot. Call node.enable() before the first move. Use firmware ≥1.2 for high-speed closed-loop (trapezoid + velocity feedforward). For a PLC / EDS workflow, flash CANopen 2.1 instead of this sketch.

3. Install the Python library

The PyPI name is grafito-canstepper; you import it as canstepper.

pip install grafito-canstepper

Package page: pypi.org/project/grafito-canstepper

Editable install from the monorepo (development):

git clone https://github.com/Grafito-Innovations/Grafito-Edge-Services.git
cd Grafito-Edge-Services/can_stepper
pip install -U pip setuptools
pip install -e ".[dev]"

4. First motion

from canstepper import CANStepperBus

bus = CANStepperBus.serial("/dev/ttyACM0")   # "COM5" on Windows
print(bus.discover())                        # {1: '1.12'}

node = bus.node(1)
node.set_run_current(40).set_hold_current(15).set_microsteps(16)
node.enable()
node.move_to(360.0, blocking=True)           # one full revolution
print(node.get_position())

Wrong direction? node.set_direction(inverted=True). Make it permanent: node.save_config().

Important for closed loop: positive commands must make the encoder angle increase. If the shaft drives itself off-target (NO_PROGRESS), invert and save.

Firmware ≥1.11 plans a 7-segment S-curve and tracks it with velocity + acceleration feedforward. Precision tune on a PR42HS40-1204AF-02 NEMA 17 at 24 V (fw 1.11 confirmation, LUT on):

node.calibrate_encoder_lut()
node.configure_closed_loop_speed(
    720.0,                  # 120 RPM cruise
    accel_deg_s2=1440.0,
    run_current=40,
    microsteps=8,
    stealthchop=False,      # SpreadCycle
    persist=True,
)
node.enable()
node.set_zero()
node.move_to(180.0, blocking=True)
print(node.get_position())  # median |err| 0.125°
Open-loop run()Closed-loop move_to
Max measured~3000 RPM (2 µstep, 80%)precision 120 RPM (median |err| 0.125°)
Not reachable5000 RPM—

Error table and RPM ladder: Closed-loop speed tuning.

6. Give each board an address

Every board ships as node 1. With one board at a time on the bus:

bus.node(1).set_node_id(2)   # applies, saves, and re-addresses the handle

7. Real-world units

from canstepper import Axis

axis = Axis(node, rotation_distance=8.0)   # 8 mm leadscrew -> everything in mm
axis.move_to(25.0, speed=10.0, blocking=True)

8. Homing

# Physical endstop on IO8 (mind the strapping-pin rule in the hardware guide):
node.configure_endstop(enabled=True, active_high=False, action=1)
axis.home(method="endstop", direction=-1, speed=8.0, backoff=1.0)

# No switch? Sensorless homing against the hard stop:
node.set_stall_threshold(60)
axis.home(method="stallguard", direction=-1, speed=5.0, current_percent=25)

More detail and example scripts in Homing and endstops:

PYTHONPATH=. python3 examples/endstop_monitor.py /dev/ttyACM0 1 30
PYTHONPATH=. python3 examples/home_endstop.py /dev/ttyACM0 1 -1
PYTHONPATH=. python3 examples/home_sensorless.py /dev/ttyACM0 1 -1 60

9. Emergency stop, three scopes

node.estop()                # one node — latched until node.enable()
bus.group([2, 3]).estop()   # a set of nodes
bus.estop_all()             # every node, one broadcast frame

On this page