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// #define IS_STM32_BUILTIN // STM32 boards with built-in CAN interface (e.g. STM32F4 Discovery).
 23// #define IS_ESP32_TWAI // ESP32 boards with built-in TWAI (CAN) interface. Directly uses the ESP-IDF TWAI driver.
 24
 25
 26/* Board-specific includes ---------------------------------------------------*/
 27
 28#if defined(IS_TEENSY_BUILTIN) + defined(IS_ARDUINO_BUILTIN) + defined(IS_MCP2515) + defined(IS_STM32_BUILTIN) + defined(IS_ESP32_TWAI) != 1
 29#warning "Select exactly one hardware option at the top of this file."
 30
 31#if CAN_HOWMANY > 0 || CANFD_HOWMANY > 0
 32#define IS_ARDUINO_BUILTIN
 33#warning "guessing that this uses HardwareCAN"
 34#else
 35#error "cannot guess hardware version"
 36#endif
 37
 38#endif
 39
 40#ifdef IS_ARDUINO_BUILTIN
 41// See https://github.com/arduino/ArduinoCore-API/blob/master/api/HardwareCAN.h
 42// and https://github.com/arduino/ArduinoCore-renesas/tree/main/libraries/Arduino_CAN
 43
 44#include <Arduino_CAN.h>
 45#include <ODriveHardwareCAN.hpp>
 46#endif // IS_ARDUINO_BUILTIN
 47
 48#ifdef IS_MCP2515
 49// See https://github.com/sandeepmistry/arduino-CAN/
 50#include "MCP2515.h"
 51#include "ODriveMCPCAN.hpp"
 52#endif // IS_MCP2515
 53
 54#ifdef IS_TEENSY_BUILTIN
 55// See https://github.com/tonton81/FlexCAN_T4
 56// clone https://github.com/tonton81/FlexCAN_T4.git into /src
 57#include <FlexCAN_T4.h>
 58#include "ODriveFlexCAN.hpp"
 59struct ODriveStatus; // hack to prevent teensy compile error
 60#endif // IS_TEENSY_BUILTIN
 61
 62#ifdef IS_STM32_BUILTIN
 63// See https://github.com/pazi88/STM32_CAN
 64#include <STM32_CAN.h>
 65#include "ODriveSTM32CAN.hpp"
 66#endif // IS_STM32_BUILTIN
 67
 68#ifdef IS_ESP32_TWAI
 69// See https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/twai.html
 70#include "driver/twai.h"
 71#include "ODriveESP32TWAI.hpp"
 72#endif // IS_ESP32_TWAI
 73
 74
 75
 76/* Board-specific settings ---------------------------------------------------*/
 77
 78
 79/* Teensy */
 80
 81#ifdef IS_TEENSY_BUILTIN
 82
 83FlexCAN_T4<CAN1, RX_SIZE_256, TX_SIZE_16> can_intf;
 84
 85bool setupCan() {
 86  can_intf.begin();
 87  can_intf.setBaudRate(CAN_BAUDRATE);
 88  can_intf.setMaxMB(16);
 89  can_intf.enableFIFO();
 90  can_intf.enableFIFOInterrupt();
 91  can_intf.onReceive(onCanMessage);
 92  return true;
 93}
 94
 95#endif // IS_TEENSY_BUILTIN
 96
 97
 98/* MCP2515-based extension modules -*/
 99
100#ifdef IS_MCP2515
101
102MCP2515Class& can_intf = CAN;
103
104// chip select pin used for the MCP2515
105#define MCP2515_CS 10
106
107// interrupt pin used for the MCP2515
108// NOTE: not all Arduino pins are interruptable, check the documentation for your board!
109#define MCP2515_INT 2
110
111// freqeuncy of the crystal oscillator on the MCP2515 breakout board. 
112// common values are: 16 MHz, 12 MHz, 8 MHz
113#define MCP2515_CLK_HZ 8000000
114
115
116static inline void receiveCallback(int packet_size) {
117  if (packet_size > 8) {
118    return; // not supported
119  }
120  CanMsg msg = {.id = (unsigned int)CAN.packetId(), .len = (uint8_t)packet_size};
121  CAN.readBytes(msg.buffer, packet_size);
122  onCanMessage(msg);
123}
124
125bool setupCan() {
126  // configure and initialize the CAN bus interface
127  CAN.setPins(MCP2515_CS, MCP2515_INT);
128  CAN.setClockFrequency(MCP2515_CLK_HZ);
129  if (!CAN.begin(CAN_BAUDRATE)) {
130    return false;
131  }
132
133  CAN.onReceive(receiveCallback);
134  return true;
135}
136
137#endif // IS_MCP2515
138
139
140/* Arduinos with built-in CAN */
141
142#ifdef IS_ARDUINO_BUILTIN
143
144HardwareCAN& can_intf = CAN;
145
146bool setupCan() {
147  return can_intf.begin((CanBitRate)CAN_BAUDRATE);
148}
149
150#endif
151
152
153/* STM32 boards with built-in CAN */
154
155#ifdef IS_STM32_BUILTIN
156
157STM32_CAN Can1( CAN1 );
158STM32_CAN& can_intf = Can1;
159
160bool setupCan() {
161  can_intf.begin();
162  can_intf.setBaudRate(CAN_BAUDRATE);
163  return true;
164}
165
166#endif // IS_STM32_BUILTIN
167
168
169/* ESP32 boards with built-in TWAI (CAN) */
170
171#ifdef IS_ESP32_TWAI
172
173// Pins used to connect to CAN bus transceiver
174#define ESP32_TWAI_TX_PIN 26
175#define ESP32_TWAI_RX_PIN 25
176
177ESP32TWAIIntf can_intf;
178
179bool setupCan() {
180    twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
181        (gpio_num_t)ESP32_TWAI_TX_PIN,
182        (gpio_num_t)ESP32_TWAI_RX_PIN,
183        TWAI_MODE_NORMAL
184    );
185
186    twai_timing_config_t t_config;
187    switch (CAN_BAUDRATE) {
188        case 1000000: t_config = TWAI_TIMING_CONFIG_1MBITS(); break;
189        case 800000:  t_config = TWAI_TIMING_CONFIG_800KBITS(); break;
190        case 500000:  t_config = TWAI_TIMING_CONFIG_500KBITS(); break;
191        case 250000:  t_config = TWAI_TIMING_CONFIG_250KBITS(); break;
192        case 125000:  t_config = TWAI_TIMING_CONFIG_125KBITS(); break;
193        case 100000:  t_config = TWAI_TIMING_CONFIG_100KBITS(); break;
194        case 50000:   t_config = TWAI_TIMING_CONFIG_50KBITS(); break;
195        case 25000:   t_config = TWAI_TIMING_CONFIG_25KBITS(); break;
196        default:      t_config = TWAI_TIMING_CONFIG_250KBITS(); break;
197    }
198
199    twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
200
201    if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
202        return false;
203    }
204
205    if (twai_start() != ESP_OK) {
206        twai_driver_uninstall();
207        return false;
208    }
209
210    return true;
211}
212
213#endif // IS_ESP32_TWAI
214
215
216/* Example sketch ------------------------------------------------------------*/
217
218// Instantiate ODrive objects
219ODriveCAN odrv0(wrap_can_intf(can_intf), ODRV0_NODE_ID); // Standard CAN message ID
220ODriveCAN* odrives[] = {&odrv0}; // Make sure all ODriveCAN instances are accounted for here
221
222struct ODriveUserData {
223  Heartbeat_msg_t last_heartbeat;
224  bool received_heartbeat = false;
225  Get_Encoder_Estimates_msg_t last_feedback;
226  bool received_feedback = false;
227};
228
229// Keep some application-specific user data for every ODrive.
230ODriveUserData odrv0_user_data;
231
232// Called every time a Heartbeat message arrives from the ODrive
233void onHeartbeat(Heartbeat_msg_t& msg, void* user_data) {
234  ODriveUserData* odrv_user_data = static_cast<ODriveUserData*>(user_data);
235  odrv_user_data->last_heartbeat = msg;
236  odrv_user_data->received_heartbeat = true;
237}
238
239// Called every time a feedback message arrives from the ODrive
240void onFeedback(Get_Encoder_Estimates_msg_t& msg, void* user_data) {
241  ODriveUserData* odrv_user_data = static_cast<ODriveUserData*>(user_data);
242  odrv_user_data->last_feedback = msg;
243  odrv_user_data->received_feedback = true;
244}
245
246// Called for every message that arrives on the CAN bus
247void onCanMessage(const CanMsg& msg) {
248  for (auto odrive: odrives) {
249    onReceive(msg, *odrive);
250  }
251}
252
253void setup() {
254  Serial.begin(115200);
255
256  // Wait for up to 3 seconds for the serial port to be opened on the PC side.
257  // If no PC connects, continue anyway.
258  for (int i = 0; i < 30 && !Serial; ++i) {
259    delay(100);
260  }
261  delay(200);
262
263
264  Serial.println("Starting ODriveCAN demo");
265
266  // Register callbacks for the heartbeat and encoder feedback messages
267  odrv0.onFeedback(onFeedback, &odrv0_user_data);
268  odrv0.onStatus(onHeartbeat, &odrv0_user_data);
269
270  // Configure and initialize the CAN bus interface. This function depends on
271  // your hardware and the CAN stack that you're using.
272  if (!setupCan()) {
273    Serial.println("CAN failed to initialize: reset required");
274    while (true); // spin indefinitely
275  }
276
277  Serial.println("Waiting for ODrive...");
278  while (!odrv0_user_data.received_heartbeat) {
279    pumpEvents(can_intf);
280  }
281
282  Serial.println("found ODrive");
283
284  // request bus voltage and current (1sec timeout)
285  Serial.println("attempting to read bus voltage and current");
286  Get_Bus_Voltage_Current_msg_t vbus;
287  if (!odrv0.request(vbus, 1000)) {
288    Serial.println("vbus request failed!");
289    while (true); // spin indefinitely
290  }
291
292  Serial.print("DC voltage [V]: ");
293  Serial.println(vbus.Bus_Voltage);
294  Serial.print("DC current [A]: ");
295  Serial.println(vbus.Bus_Current);
296
297  Serial.println("Enabling closed loop control...");
298  while (odrv0_user_data.last_heartbeat.Axis_State != ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL) {
299    odrv0.clearErrors();
300    delay(1);
301    odrv0.setState(ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL);
302
303    // Pump events for 150ms. This delay is needed for two reasons;
304    // 1. If there is an error condition, such as missing DC power, the ODrive might
305    //    briefly attempt to enter CLOSED_LOOP_CONTROL state, so we can't rely
306    //    on the first heartbeat response, so we want to receive at least two
307    //    heartbeats (100ms default interval).
308    // 2. If the bus is congested, the setState command won't get through
309    //    immediately but can be delayed.
310    for (int i = 0; i < 15; ++i) {
311      delay(10);
312      pumpEvents(can_intf);
313    }
314  }
315
316  Serial.println("ODrive running!");
317}
318
319void loop() {
320  pumpEvents(can_intf); // This is required on some platforms to handle incoming feedback CAN messages
321                        // Note that on MCP2515-based platforms, this will delay for a fixed 10ms.
322                        //
323                        // This has been found to reduce the number of dropped messages, however it can be removed
324                        // for applications requiring loop times over 100Hz.
325
326  float SINE_PERIOD = 2.0f; // Period of the position command sine wave in seconds
327
328  float t = 0.001 * millis();
329  
330  float phase = t * (TWO_PI / SINE_PERIOD);
331
332  odrv0.setPosition(
333    sin(phase), // position
334    cos(phase) * (TWO_PI / SINE_PERIOD) // velocity feedforward (optional)
335  );
336
337  // print position and velocity for Serial Plotter
338  if (odrv0_user_data.received_feedback) {
339    Get_Encoder_Estimates_msg_t feedback = odrv0_user_data.last_feedback;
340    odrv0_user_data.received_feedback = false;
341    Serial.print("odrv0-pos:");
342    Serial.print(feedback.Pos_Estimate);
343    Serial.print(",");
344    Serial.print("odrv0-vel:");
345    Serial.println(feedback.Vel_Estimate);
346  }
347}

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 for the Arduino sketch is 250000 while the ODrive uses automatic baudrate detection by default since firmware version 0.6.11. On earlier firmware versions, the ODrive default is also 250000.

  • 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.

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 May trigger onBusVI callback if it’s registered.

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.

inline void onTorques(void (*callback)(Get_Torques_msg_t &feedback, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive torques feedback processing.

inline void onTemperature(void (*callback)(Get_Temperature_msg_t &feedback, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive temperature feedback.

inline void onBusVI(void (*callback)(Get_Bus_Voltage_Current_msg_t &feedback, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive bus voltage/current feedback.

inline void onCurrents(void (*callback)(Get_Iq_msg_t &feedback, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive currents feedback.

inline void onError(void (*callback)(Get_Error_msg_t &msg, void *user_data), void *user_data = nullptr)

Registers a callback for ODrive error messages.

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