Controlling ODrive from an Arduino via CAN

Overview

This page describes how to control an ODrive via CAN using an Arduino and the ODriveArduino library.

The library is compatible with the following setups:

This library is primarily intended for runtime usage, such as changing states, sending setpoints and reading position/velocity feedback. While the CAN protocol allows for the modification of any configuration variable, this is often much easier to accomplish in the ODrive GUI or the odrivetool. More information about the underlying protocol can be found here.

Connecting the Arduino to the ODrive

First, connect your Arduino to the ODrive. Below you see an example setup. Your setup may look different depending on your hardware, please see the notes below.

Arduino connected to ODrive S1 via CAN

Arduino connected to ODrive S1 via CAN. Click to enlarge.

  • The Arduino UNO R4 Minima shown on this picture has a built-in CAN interface but no CAN transceiver. That means we need an external transceiver.

  • The CAN transceiver on this picture is a Waveshare SN65HVD230 CAN Board.

  • CAN bus ground must be connected to DC- at one single point in the system. On this picture, this is done with a single wire on the ODrive’s second CAN connector. See Hardware Setup for more details.

  • If you’re using multiple ODrives you can simply daisy-chain them together.

  • When using an ODrive Pro, it must be powered through DC+/- to be able to communicate via CAN.

Configuring the ODrive

Next, configure your ODrive according to your application. The easiest way to do this for the first time is via USB in the Web GUI.

Note

It is possible to set up a fresh ODrive purely via CAN, however this is currently not possible from the GUI and requires scripting instead. See Arbitrary Parameter Access for more info.

Connect your ODrive to your PC via a USB isolator, open the Web GUI and follow the Configuration wizard.

Most settings depend on your hardware so we won’t go into detail here. However there are a few settings specifically needed for this example:

  • Control Mode page: Select Filtered Position Control mode with a bandwidth of 100 rad/s.

GUI Control Mode configuration
  • Interfaces page: Enable CAN, enable feedback messages and set the Node ID to 0 (or something unique for every ODrive if you have multiple).

    GUI Interfaces configuration
  • Optionally you can also enable the watchdog (also on the Interfaces page). This will make the ODrive stop when the CAN connection gets disrupted.

    GUI Watchdog configuration

When you’re done configuring, go to the Dashboard to verify that you can send position setpoints from the GUI before proceeding with the next steps.

Warning

If you have already enabled CAN and come back to this step to change something, make sure to disable CAN first or disconnect/unpower the Arduino. Otherwise the Arduino sketch might interfer with the GUI.

Installing the ODriveArduino Library

  1. Open your Arduino IDE.

  2. Go to Sketch > Include Library > Manage Libraries.

  3. In the Library Manager, type “ODriveArduino” into the search box.

  4. Click on the entry for the ODriveArduino library, then click “Install”.

Example Sketch

Finally, upload the following example sketch to the Arduino. You can also find this under File > Examples > ODriveArduino. This example will put the ODrive into closed loop control and then move the motor back and forth in a sine wave pattern.

  1#include <Arduino.h>
  2#include "ODriveCAN.h"
  3
  4// Documentation for this example can be found here:
  5// https://docs.odriverobotics.com/v/latest/guides/arduino-can-guide.html
  6
  7
  8/* Configuration of example sketch -------------------------------------------*/
  9
 10// CAN bus baudrate. Make sure this matches for every device on the bus
 11#define CAN_BAUDRATE 250000
 12
 13// ODrive node_id for odrv0
 14#define ODRV0_NODE_ID 0
 15
 16// Uncomment below the line that corresponds to your hardware.
 17// See also "Board-specific settings" to adapt the details for your hardware setup.
 18
 19// #define IS_TEENSY_BUILTIN // Teensy boards with built-in CAN interface (e.g. Teensy 4.1). See below to select which interface to use.
 20// #define IS_ARDUINO_BUILTIN // Arduino boards with built-in CAN interface (e.g. Arduino Uno R4 Minima)
 21// #define IS_MCP2515 // Any board with external MCP2515 based extension module. See below to configure the module.
 22
 23
 24/* Board-specific includes ---------------------------------------------------*/
 25
 26#if defined(IS_TEENSY_BUILTIN) + defined(IS_ARDUINO_BUILTIN) + defined(IS_MCP2515) != 1
 27#warning "Select exactly one hardware option at the top of this file."
 28
 29#if CAN_HOWMANY > 0 || CANFD_HOWMANY > 0
 30#define IS_ARDUINO_BUILTIN
 31#warning "guessing that this uses HardwareCAN"
 32#else
 33#error "cannot guess hardware version"
 34#endif
 35
 36#endif
 37
 38#ifdef IS_ARDUINO_BUILTIN
 39// See https://github.com/arduino/ArduinoCore-API/blob/master/api/HardwareCAN.h
 40// and https://github.com/arduino/ArduinoCore-renesas/tree/main/libraries/Arduino_CAN
 41
 42#include <Arduino_CAN.h>
 43#include <ODriveHardwareCAN.hpp>
 44#endif // IS_ARDUINO_BUILTIN
 45
 46#ifdef IS_MCP2515
 47// See https://github.com/sandeepmistry/arduino-CAN/
 48#include "MCP2515.h"
 49#include "ODriveMCPCAN.hpp"
 50#endif // IS_MCP2515
 51
 52#ifdef IS_TEENSY_BUILTIN
 53// See https://github.com/tonton81/FlexCAN_T4
 54// clone https://github.com/tonton81/FlexCAN_T4.git into /src
 55#include <FlexCAN_T4.h>
 56#include "ODriveFlexCAN.hpp"
 57struct ODriveStatus; // hack to prevent teensy compile error
 58#endif // IS_TEENSY_BUILTIN
 59
 60
 61
 62
 63/* Board-specific settings ---------------------------------------------------*/
 64
 65
 66/* Teensy */
 67
 68#ifdef IS_TEENSY_BUILTIN
 69
 70FlexCAN_T4<CAN1, RX_SIZE_256, TX_SIZE_16> can_intf;
 71
 72bool setupCan() {
 73  can_intf.begin();
 74  can_intf.setBaudRate(CAN_BAUDRATE);
 75  can_intf.setMaxMB(16);
 76  can_intf.enableFIFO();
 77  can_intf.enableFIFOInterrupt();
 78  can_intf.onReceive(onCanMessage);
 79  return true;
 80}
 81
 82#endif // IS_TEENSY_BUILTIN
 83
 84
 85/* MCP2515-based extension modules -*/
 86
 87#ifdef IS_MCP2515
 88
 89MCP2515Class& can_intf = CAN;
 90
 91// chip select pin used for the MCP2515
 92#define MCP2515_CS 10
 93
 94// interrupt pin used for the MCP2515
 95// NOTE: not all Arduino pins are interruptable, check the documentation for your board!
 96#define MCP2515_INT 2
 97
 98// freqeuncy of the crystal oscillator on the MCP2515 breakout board. 
 99// common values are: 16 MHz, 12 MHz, 8 MHz
100#define MCP2515_CLK_HZ 8000000
101
102
103static inline void receiveCallback(int packet_size) {
104  if (packet_size > 8) {
105    return; // not supported
106  }
107  CanMsg msg = {.id = (unsigned int)CAN.packetId(), .len = (uint8_t)packet_size};
108  CAN.readBytes(msg.buffer, packet_size);
109  onCanMessage(msg);
110}
111
112bool setupCan() {
113  // configure and initialize the CAN bus interface
114  CAN.setPins(MCP2515_CS, MCP2515_INT);
115  CAN.setClockFrequency(MCP2515_CLK_HZ);
116  if (!CAN.begin(CAN_BAUDRATE)) {
117    return false;
118  }
119
120  CAN.onReceive(receiveCallback);
121  return true;
122}
123
124#endif // IS_MCP2515
125
126
127/* Arduinos with built-in CAN */
128
129#ifdef IS_ARDUINO_BUILTIN
130
131HardwareCAN& can_intf = CAN;
132
133bool setupCan() {
134  return can_intf.begin((CanBitRate)CAN_BAUDRATE);
135}
136
137#endif
138
139
140/* Example sketch ------------------------------------------------------------*/
141
142// Instantiate ODrive objects
143ODriveCAN odrv0(wrap_can_intf(can_intf), ODRV0_NODE_ID); // Standard CAN message ID
144ODriveCAN* odrives[] = {&odrv0}; // Make sure all ODriveCAN instances are accounted for here
145
146struct ODriveUserData {
147  Heartbeat_msg_t last_heartbeat;
148  bool received_heartbeat = false;
149  Get_Encoder_Estimates_msg_t last_feedback;
150  bool received_feedback = false;
151};
152
153// Keep some application-specific user data for every ODrive.
154ODriveUserData odrv0_user_data;
155
156// Called every time a Heartbeat message arrives from the ODrive
157void onHeartbeat(Heartbeat_msg_t& msg, void* user_data) {
158  ODriveUserData* odrv_user_data = static_cast<ODriveUserData*>(user_data);
159  odrv_user_data->last_heartbeat = msg;
160  odrv_user_data->received_heartbeat = true;
161}
162
163// Called every time a feedback message arrives from the ODrive
164void onFeedback(Get_Encoder_Estimates_msg_t& msg, void* user_data) {
165  ODriveUserData* odrv_user_data = static_cast<ODriveUserData*>(user_data);
166  odrv_user_data->last_feedback = msg;
167  odrv_user_data->received_feedback = true;
168}
169
170// Called for every message that arrives on the CAN bus
171void onCanMessage(const CanMsg& msg) {
172  for (auto odrive: odrives) {
173    onReceive(msg, *odrive);
174  }
175}
176
177void setup() {
178  Serial.begin(115200);
179
180  // Wait for up to 3 seconds for the serial port to be opened on the PC side.
181  // If no PC connects, continue anyway.
182  for (int i = 0; i < 30 && !Serial; ++i) {
183    delay(100);
184  }
185  delay(200);
186
187
188  Serial.println("Starting ODriveCAN demo");
189
190  // Register callbacks for the heartbeat and encoder feedback messages
191  odrv0.onFeedback(onFeedback, &odrv0_user_data);
192  odrv0.onStatus(onHeartbeat, &odrv0_user_data);
193
194  // Configure and initialize the CAN bus interface. This function depends on
195  // your hardware and the CAN stack that you're using.
196  if (!setupCan()) {
197    Serial.println("CAN failed to initialize: reset required");
198    while (true); // spin indefinitely
199  }
200
201  Serial.println("Waiting for ODrive...");
202  while (!odrv0_user_data.received_heartbeat) {
203    pumpEvents(can_intf);
204    delay(100);
205  }
206
207  Serial.println("found ODrive");
208
209  // request bus voltage and current (1sec timeout)
210  Serial.println("attempting to read bus voltage and current");
211  Get_Bus_Voltage_Current_msg_t vbus;
212  if (!odrv0.request(vbus, 1)) {
213    Serial.println("vbus request failed!");
214    while (true); // spin indefinitely
215  }
216
217  Serial.print("DC voltage [V]: ");
218  Serial.println(vbus.Bus_Voltage);
219  Serial.print("DC current [A]: ");
220  Serial.println(vbus.Bus_Current);
221
222  Serial.println("Enabling closed loop control...");
223  while (odrv0_user_data.last_heartbeat.Axis_State != ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL) {
224    odrv0.clearErrors();
225    delay(1);
226    odrv0.setState(ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL);
227
228    // Pump events for 150ms. This delay is needed for two reasons;
229    // 1. If there is an error condition, such as missing DC power, the ODrive might
230    //    briefly attempt to enter CLOSED_LOOP_CONTROL state, so we can't rely
231    //    on the first heartbeat response, so we want to receive at least two
232    //    heartbeats (100ms default interval).
233    // 2. If the bus is congested, the setState command won't get through
234    //    immediately but can be delayed.
235    for (int i = 0; i < 15; ++i) {
236      delay(10);
237      pumpEvents(can_intf);
238    }
239  }
240
241  Serial.println("ODrive running!");
242}
243
244void loop() {
245  pumpEvents(can_intf); // This is required on some platforms to handle incoming feedback CAN messages
246
247  float SINE_PERIOD = 2.0f; // Period of the position command sine wave in seconds
248
249  float t = 0.001 * millis();
250  
251  float phase = t * (TWO_PI / SINE_PERIOD);
252
253  odrv0.setPosition(
254    sin(phase), // position
255    cos(phase) * (TWO_PI / SINE_PERIOD) // velocity feedforward (optional)
256  );
257
258  // print position and velocity for Serial Plotter
259  if (odrv0_user_data.received_feedback) {
260    Get_Encoder_Estimates_msg_t feedback = odrv0_user_data.last_feedback;
261    odrv0_user_data.received_feedback = false;
262    Serial.print("odrv0-pos:");
263    Serial.print(feedback.Pos_Estimate);
264    Serial.print(",");
265    Serial.print("odrv0-vel:");
266    Serial.println(feedback.Vel_Estimate);
267  }
268}

Below is the expected output in the Arduino IDE’s Serial Monitor (Tools > Serial Monitor):

Starting ODriveCAN demo
Waiting for ODrive...
found ODrive
attempting to read bus voltage and current
DC voltage [V]: 17.31
DC current [A]: 0.00
Enabling closed loop control...
ODrive running!
odrv0-pos:0.72,odrv0-vel:0.01
odrv0-pos:0.72,odrv0-vel:-0.01
odrv0-pos:0.65,odrv0-vel:-11.80
odrv0-pos:0.55,odrv0-vel:-9.42
odrv0-pos:0.45,odrv0-vel:-10.08
odrv0-pos:0.35,odrv0-vel:-9.88
odrv0-pos:0.25,odrv0-vel:-10.03
odrv0-pos:0.15,odrv0-vel:-9.91
odrv0-pos:0.05,odrv0-vel:-10.02
odrv0-pos:-0.05,odrv0-vel:-9.95
odrv0-pos:-0.15,odrv0-vel:-10.03
odrv0-pos:-0.25,odrv0-vel:-9.94
odrv0-pos:-0.35,odrv0-vel:-9.98

And here’s the Serial Plotter (Tools > Serial Plotter):

Serial plotter output

Troubleshooting

Arduino hangs at Waiting for ODrive...

This means the Arduino is not receiving heartbeat messages from the ODrive.

  • Make sure heartbeat messages are enabled according to Configuring the ODrive.

  • Make sure the configured CAN baudrate of the ODrive and the Arduino sketch match. The default is 250000 for both.

  • Likewise, make sure the configured Node ID matches between ODrive and Arduino sketch. The default is 0 for both.

  • Double check your wiring.

  • In the example sketch, in the onMessage() function, insert a Serial.println() statement. This can help you debug if your Arduino is receiving any CAN messages at all.

  • If you have an oscilloscope, CAN logger or CAN-to-USB dongle, use them to check if there are any CAN messages on the bus.

Arduino hangs at Enabling closed loop control...

This means that the Arduino can talk to the ODrive and vice versa, but the ODrive refuses to enter closed loop control mode.

This can have lots of reasons (ODrive unpowered, ODrive not calibrated, etc.). The easiest way to find out why, is to open the GUI and check the status bar.

Arduino Library Reference

class ODriveCAN

Public Functions

bool clearErrors()

Clear all errors on the ODrive.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setState(ODriveAxisState requested_state)

Tells the ODrive to change its axis state.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setControllerMode(uint8_t control_mode, uint8_t input_mode)

Sets the control mode and input mode of the ODrive.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setPosition(float position, float velocity_feedforward = 0.0f, float torque_feedforward = 0.0f)

Sends a position setpoint with optional velocity and torque feedforward.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setVelocity(float velocity, float torque_feedforward = 0.0f)

Sends a velocity setpoint with optional torque feedforward.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setTorque(float torque)

Sends a torque setpoint to the ODrive.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setLimits(float velocity_limit, float current_soft_max)

Sets the velocity and current limits.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setPosGain(float pos_gain)

Sets the position gain.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setVelGains(float vel_gain, float vel_integrator_gain)

Sets the velocity and velocity integrator gains.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setAbsolutePosition(float abs_pos)

Sets the encoder’s absolute position and enables absolute positioning.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setTrapezoidalVelLimit(float vel_limit)

Sets the coast velocity for subsequent trapezoidal moves.

This function returns immediately and does not check if the ODrive received the CAN message.

bool setTrapezoidalAccelLimits(float accel_limit, float decel_limit)

Sets the acceleration and deceleration values for subsequent trapezoidal moves.

This function returns immediately and does not check if the ODrive received the CAN message.

bool getCurrents(Get_Iq_msg_t &msg, uint16_t timeout_ms = 10)

Requests motor current. Iq_measured represents torque-generating current.

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool getTemperature(Get_Temperature_msg_t &msg, uint16_t timeout_ms = 10)

Requests motor temperature.

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool getError(Get_Error_msg_t &msg, uint16_t timeout_ms = 10)

Requests error information.

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool getVersion(Get_Version_msg_t &msg, uint16_t timeout_ms = 10)

Requests hardware and firmware version information.

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool getFeedback(Get_Encoder_Estimates_msg_t &msg, uint16_t timeout_ms = 10)

Requests encoder feedback data. May trigger onFeedback callback if it’s registered.

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool getBusVI(Get_Bus_Voltage_Current_msg_t &msg, uint16_t timeout_ms = 10)

Requests ODrive DC bus voltage and current.

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool getPower(Get_Powers_msg_t &msg, uint16_t timeout_ms = 10)

Requests mechanical and electrical power data (used for spinout detection)

This function will block and wait for up to timeout_ms (default 10msec) for ODrive to reply

bool reset(ResetAction action = ResetAction::Reboot)

Resets the ODrive with the given action.

Valid actions:

  • Reboot (0)

  • Save (1)

  • Erase (2)

inline void onFeedback(void (*callback)(Get_Encoder_Estimates_msg_t &feedback, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive feedback processing.

inline void onStatus(void (*callback)(Heartbeat_msg_t &feedback, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive axis state feedback.

void onReceive(uint32_t id, uint8_t length, const uint8_t *data)

Processes received CAN messages for the ODrive.

template<typename T>
inline bool request(T &msg, uint16_t timeout_ms = 10)

Sends a request message and awaits a response.

Blocks until the response is received or the timeout is reached. Returns false if the ODrive does not respond within the specified timeout.

template<typename T>
inline bool send(const T &msg)

Sends a specified message over CAN.

template<typename T>
inline T getEndpoint(uint16_t endpoint_id, uint16_t timeout_ms = 10)

Get value at the endpoint.

Blocks until the response is received or the timeout is reached.

Template Parameters:

T – The data type expected from the endpoint

Parameters:
  • endpoint_id – Unique ID from flat_endpoints.json

  • timeout_ms – Time to wait for a response from ODrive

Returns:

T Data from the endpoint, or 0 on timeout

template<typename T>
inline bool setEndpoint(uint16_t endpoint_id, T value)

Set endpoint to value.

This function returns immediately and does not check if the ODrive received the CAN message.

Template Parameters:

T – Type of the value from flat_endpoints.json

Parameters:
  • endpoint_id – Unique ID of endpoint from flat_endpoints.json

  • value – value to write to the endpoint