Grafito CANStepper

Production Python machines still use GCSP v1 (firmware 1.12). CANopen is a separate sketch (firmware 2.1) for PLC tools that import an EDS. Flash one protocol per board, and do not mix GCSP and CANopen nodes on the same CAN bus — the ID maps collide. Motion matches GCSP 1.12: S-curve closed loop, acceleration feedforward, and optional encoder LUT.

What you get

ItemVersion / value
Firmware2.1 — CiA 301 + CiA 402 subset (pp / pv / hm)
Physical layerCAN 2.0A, 11-bit, default 1 Mbps
Node ID1–127 (default 1). Not the GCSP 1–31 limit
Position unitsMT6701 encoder counts, 16384 / revolution
Identityvendor 0x000005A3 (placeholder), product 0x00004333, revision 0x00020100, device type 0x00040192
Device nameCANStepper

Vendor 0x5A3 is a development placeholder. Register a real CiA vendor ID before a production EDS.

Downloads (EDS, DCF, object dictionary, firmware)

FileUse
GrafitoCANStepper_C3_CANopen.inoCANopen sketch (fw 2.1)
canopen_stack.hRequired header — flash the folder, not the .ino alone
GrafitoCANStepper.edsCiA 306 EDS — fw 2.1 template (replace any 2.0 file)
GrafitoCANStepper_Node1.dcfDCF instance, node 1, 1 Mbps, fw 2.1
GrafitoCANStepper_Node2.dcfDCF instance, node 2, 1 Mbps (flash CO_FACTORY_NODE_ID=2)
object_dictionary.jsonObject dictionary (JSON)
object_dictionary.csvObject dictionary (CSV)
Grafito_CANStepper_EDS_Configuration_Guide.pdfPLC / EDS configuration guide (PDF)
File typeRole
EDSDevice template — add the product to TwinCAT / Codesys / a CANopen master
DCFOne instance (node ID + 1 Mbps + filled COB-IDs + ParameterValue)
JSON / CSVThe same object dictionary as a table (not for PLC import)
PDF guideFull EDS / PLC configuration walkthrough

Import the firmware 2.1 EDS as the device type (FileRevision 2, identity revision 0x00020100). If a PLC already has an older GrafitoCANStepper.eds (fw 2.0 / revision 0x00020000), delete it and re-import — that pack does not match this board.

If the tool wants a configured node, import GrafitoCANStepper_Node1.dcf or GrafitoCANStepper_Node2.dcf (node 2 needs -DCO_FACTORY_NODE_ID=2 at flash). Node2 DCF commissions 0x200C = 0 so the first jog is not killed by HOME. For another ID, regenerate from canstepper.canopen402.render_dcf(node_id=N) — do not reuse node-1 COB-IDs.

Flash

Full procedure: Flashing firmware (Arduino IDE + CLI, factory node 2, boot banner, NVS).

Same hardware and Arduino settings as GCSP: ESP32C3 Dev Module, USB CDC On Boot: Enabled, libraries FastAccelStepper + TMC2209. Apply Vin 5–24 V (typically 24 V); USB-C is data only.

Download both the .ino and canopen_stack.h into the same folder (GrafitoCANStepper_C3_CANopen/).

# Factory node 1 (default)
arduino-cli compile --upload -p /dev/ttyACM0 \
  --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc \
  path/to/GrafitoCANStepper_C3_CANopen

# Factory node 2 (second board on a daisy-chain)
arduino-cli compile --upload -p /dev/ttyACM0 \
  --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc \
  --build-property "compiler.cpp.extra_flags=-DCO_FACTORY_NODE_ID=2" \
  path/to/GrafitoCANStepper_C3_CANopen

Healthy boot (115200 baud). Opening USB-JTAG resets the C3 — wait for this banner and heartbeat 0x7F:

# GrafitoCANStepper CANopen fw 2.1 CiA402
702 0 00
# CAN started (1000000 bit/s) node 2
702 0 7F

Node ID is object 0x2000. Change it, write 0x2008 = 1 (save), then reset so COB-IDs update. NVS namespace is co402 (it does not inherit GCSP invert / node ID).

Do not mix with GCSP

GCSP uses CAN ID = (node_id << 6) | msg_id. That range overlaps CANopen SYNC (0x080), EMCY, PDOs, SDOs and heartbeats (0x700+n). One protocol per bus.

CiA 402 enable and motion

Modes of operation (0x6060): 1 profile position, 3 profile velocity, 6 homing. Set the mode before cyclic PDOs.

Enable sequence (controlword 0x6040):

6 → 7 → 15

Statusword 0x6041 low bits should walk 0x21 → 0x23 → 0x27 (with 0x0200 remote typically 0x0237 when enabled). Fault bit 0x0008: write 0x0080 to reset.

Homing methods (0x6098): 35 set zero here; 17 / 18 endstop; −1 / −2 StallGuard.

Manufacturer objects you will use on the bench:

IndexNameNotes
0x2000Node IDSave + reset to apply COB-IDs
0x2003Run currentModest 30–40 for first jog
0x2006Invert directionSet 1 if a positive move runs the wrong way
0x2007Closed loop0 = open-loop step pulses; 1 = encoder PID
0x2008Save configWrite 1
0x200CEndstop enableSet 0 for the first jog if HOME is already active
0x6081Profile velocityCounts/s. Default ~720 °/s; 4096 ≈ 90 °/s
0x6083Profile accelerationCounts/s². Default 65536 ≈ 1440 °/s²
0x200F / 0x2010 / 0x2011 / 0x201APID Kp / Ki / Kd / KaDefaults 10 / 0.3 / 0.35 / 0.04 — tune from PLC
0x2012PID toleranceSettle window, default 0.35°
0x201BProfile jerkDeg/s³. 0 = auto (amax / 0.05 s)
0x201CLUT enableRequires a valid table (0x201E)
0x201DLUT commandWrite 0 calibrate, 1 enable, 2 disable, 3 clear

Default RPDO1: controlword + target position. TPDO1: statusword + actual position (firmware emits TPDOs every 20 ms in NMT operational).

Tune gains from the PLC

Closed-loop command is:

v = v_ff + Ka · a_ff + PID(r − encoder)

PID only trims lag. Cruise speed is not a PID gain — set that with 0x6081. Write the gain objects as REAL32 (IEEE-754). In TwinCAT / Codesys map them as REAL. Do not write integer 10 into 0x200F; it must be float 10.0.

Objects to change

IndexNameTypeFactoryWhat it does
0x200FKpREAL3210.0Stiffness. Raise if it lags the plan; lower if it buzzes.
0x2011KdREAL320.35Damping. Raise if it rings at the end of a move.
0x2010KiREAL320.3Steady-state. Keep small; too much causes overshoot / following error 0x7122.
0x201AKaREAL320.04Accel feedforward. Raise if it sags during accel/decel.
0x2012PID toleranceREAL320.35°Target-reached band. Too tight at high RPM → 0x7122.

Set these before touching PID

IndexNameNotes
0x2006Invert1 if a positive target runs the wrong way (PID will fight the encoder).
0x2003Run current %Start 30–40. Gains cannot fix a starved motor.
0x2007Closed loopMust be 1 for encoder PID (0 = open-loop step pulses).
0x6081Profile velocitycounts/s. Default 32768 ≈ 720 °/s.
0x6083Profile accelcounts/s². Default 65536 ≈ 1440 °/s².
0x201BJerkdeg/s³. 0 = auto (amax / 0.05 s).

Save so it survives power-cycle: write 0x2008 = 1.

Order on the PLC

  1. Enable 6 → 7 → 15, confirm 0x2007 = 1, invert with 0x2006 if needed.
  2. Set a modest 0x6081 / 0x6083 and current 30–40.
  3. Leave Ki = 0.3 and Ka = 0.04.
  4. Change Kp in steps of ~2 (try 8 → 10 → 12).
  5. If it oscillates at settle, raise Kd (0.35 → 0.5) or drop Kp.
  6. Only then nudge Ka or Ki.
  7. Write 0x2008 = 1 when it is good.

Measured precision defaults (fw 2.1 / GCSP 1.12): Kp 10, Ki 0.3, Kd 0.35, Ka 0.04, tolerance 0.35°. See Closed-loop speed tuning for the hardware tables (those pages use GCSP names; the objects above are the PLC map).

1 Mbps is the product maximum bitrate, not a slow cap. A PLC should command motion with PDOs (1–20 ms). USB hex-bridge SDO polling is for bring-up only and is much slower than the bus.

First-commission checklist

  1. Flash CANopen 2.1. Confirm banner and heartbeat 0x7n (n = node ID).
  2. Read 0x1000 (0x00040192) and 0x1018 (vendor / product / revision).
  3. If 0x2018 (endstop) is 1 and you are not at a real home, write 0x200C = 0.
  4. Set 0x2003 (current) and a low 0x6081 for the first move.
  5. Enable 6 → 7 → 15. Coils hold; shaft should not run.
  6. Command a small move (for example +455 counts ≈ +10°). If the shaft runs the other way, abort, set 0x2006 = 1, save, reset, retry.
  7. Only then raise speed or switch to RPDO cyclic control.

Daisy-chain (two nodes)

  1. Flash board A as node 1, board B as node 2 (CO_FACTORY_NODE_ID=2).
  2. Confirm IDs before joining CAN (two node-1s collide).
  3. CAN_H to CAN_H, CAN_L to CAN_L. 120 Ω only on the two physical ends.
  4. Vin on both boards. USB on either board is a hex bridge / bus spy.

Heartbeats: node 1 = 701 0 7F, node 2 = 702 0 7F (pre-op). SDO to 0x601 / 0x602.

Host tools (Python)

From can_stepper/ with PYTHONPATH=.:

# Identity / 402 enable on one USB node (no motion by default)
python3 tools/canopen_bringup.py /dev/ttyACM0 --node 1

# Two-node listen via the USB board (default USB = node 2)
python3 tools/canopen_two_node.py /dev/ttyACM0 --usb-node 2 --peer 1

# Regenerate EDS + DCF + JSON/CSV from the same object table
python3 -c "from canstepper.canopen402 import write_canopen_descriptions; print(write_canopen_descriptions())"

canstepper.canopen402 is the host-side source of truth for the object dictionary, EDS/DCF renderers, and the in-process slave used by pytest.

On this page