Grafito CANStepper

1. What you need

  • A CANStepper Adapter Board mounted on a NEMA17, diametric magnet centered over the encoder.
  • 24 V motor supply and a USB-C cable.
  • For more than one board: CAN daisy-chain cables and 120 Ω termination at both physical ends of the chain.

2. Flash the firmware (once per board)

  1. Arduino IDE or arduino-cli with the esp32 core. Board: ESP32C3 Dev Module, Tools → USB CDC On Boot: Enabled.
  2. Libraries: FastAccelStepper (gin66) and TMC2209 (janelia-arduino).
  3. Open firmware/GrafitoCANStepper_C3/GrafitoCANStepper_C3.ino and upload.
  4. Serial monitor at 115200 baud. Healthy boot looks like:
# GrafitoCANStepper fw 1.2 proto 1
# MT6701 raw=0x2A5F40 angle=238.71 deg
# CAN started (1 Mbps)

Use firmware ≥1.2 for high-speed closed-loop (trapezoid + velocity feedforward).

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.2'}

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.2 plans a trapezoid and tracks it with feedforward. Production tune measured on a PR42HS40-1204AF-02 NEMA 17 (1.2 A, 3.2 mH) at 24 V:

node.configure_closed_loop_speed(
    4800.0,                 # 800 RPM cruise
    run_current=70,
    microsteps=8,
    stealthchop=False,      # SpreadCycle
    persist=True,
)
node.enable()
node.set_zero()
node.move_to(720.0, blocking=True)
Open-loop run()Closed-loop move_to
Max measured~1200 RPM (7200 deg/s)~1000 RPM cruise (6000 deg/s)
Production800 RPM (4800 deg/s), 10 min soak 100% success

Details, matrix tables, and soak log: 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 in Homing and endstops.

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