Best practices for synchronized multi-axis control on large 3D printer (3x3m) using CAN?

Hi everyone,

I’m building a very large 3D printer (3x3m build volume) and using multiple ODrive Pros to control the motors via CAN from an ESP32 main controller. Due to the machine’s size, CAN is essentially my only option for reliable communication.

Current Setup:

  • ESP32 as main controller, calculating speed and position from gcode and transmitting via CAN
  • Each ODrive Pro running standard firmware
  • Controllers set to INPUT_MODE_TRAP_TRAJ
  • ESP32 sends a target position to each ODrive, then waits for all ODrives to report they’re within tolerance of that position before sending the next target

The Problem: I’m concerned about timing precision and synchronization between axes. My current approach of “move, wait for all axes to arrive, then move again” seems imprecise and leads to:

  • Stuttering motion as axes wait for the slowest one
  • Loss of smooth velocity profiles
  • Issues with axes starting and arriving at slightly different times

My Questions:

  1. Is this “wait for position” approach fundamentally wrong for coordinated motion? Should I be streaming positions at a fixed rate instead? Does the ODrive CAN implementation have a preferred way of achieving this?
  2. Is INPUT_MODE_TRAP_TRAJ the right mode for this application, or should I be using something else?
  3. If I switch to streaming positions, what update rate should I target for CAN commands? (50-100Hz?)
  4. Are there any ODrive Pro features I should be leveraging that are specifically designed for synchronized multi-axis systems?
  5. Has anyone successfully built a similar ODrive CAN system with good synchronization?

I’m using standard ODrive Pro firmware and would prefer not to go down the custom firmware route if possible.

Any advice, best practices, or examples of similar implementations would be greatly appreciated!

Thanks in advance!

Very cool project!

  • Is this “wait for position” approach fundamentally wrong for coordinated motion? Should I be streaming positions at a fixed rate instead? Does the ODrive CAN implementation have a preferred way of achieving this?

Yes, waiting for the actual position to settle is usually the wrong approach. Typically in a system like this, the right approach is to set up/tune the ODrive and trapezoidal motion parameters such that the trajectory acceleration/velocity/deceleration is all feasible, and then assume that the ODrive will reach the end position at the same timestamp that the trajectory is expected to take. This is especially the case with something like a 3D printer, where you’ll have a very consistent load profile on each axis (nearly entirely inertia driven).

For instance, if you can accelerate/decelerate at 2 m/s^2, and move at a max speed of 0.5 m/s^2, and you send a position setpoint to move 1.5m, you can calculate a total trajectory time of 3.25s. So you could just send the new setpoint and then assume that the ODrive will be at the final position in exactly 3.25s.

To make this easier, you can check trajectory_done as well – this will go to True once the trajectory move has finished. Note that this is set to true based on the trajectory setpoints – not the actual ODrive position/velocity – so you don’t have the issues you’re having with waiting for the actual ODrive position to settle. This is also a flag in the heartbeat CAN message.

For best performance, you should definitely also set the inertia term – this will allow the ODrive to add a torque feedforward to the acceleration/deceleration stages of the trapezoidal move, which makes the tracking performance much better, and brings transient error down to near zero.

  • Is INPUT_MODE_TRAP_TRAJ the right mode for this application, or should I be using something else?

You could switch to POS_FILTER and externally calculate+stream in the trajectory yourself. However, you’d want to be streaming at ≥100Hz (which can be a good bit of bandwidth and CPU time on the ESP32), and there’s not much reason to do this unless you’re doing something more advanced than trapezoidal movement in the first place (e.g. S-curves, blended motion, etc).

  • Are there any ODrive Pro features I should be leveraging that are specifically designed for synchronized multi-axis systems?

We have some upcoming functionality for trajectory preplanning and synchronization, but even just with the current CAN stack, you can get synchronization within +/- a few hundred microseconds, just by sending the position setpoint messages sequentially.

  • Has anyone successfully built a similar ODrive CAN system with good synchronization?

Yes! I’m not sure if there’s anything public I can point to, but we have some customers who synchronize multiple axis this way. Unfortunately I can’t share the exact details of their application, but the method of sending setpoints in TRAP_TRAJ mode is the most common one.