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:
Arduinos with built-in CAN interfaces that use the native Arduino framework (such as Arduino UNO R4 Minima, Arduino UNO R4 WiFi and others)
Teensys with built-in CAN interfaces that are compatibly with FlexCAN_T4 (Teensy 4.0, Teensy 4.1)
Arduino-compatible boards with MCP2515-based CAN shields
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.
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.
odrv0.axis0.controller.config.control_mode = ControlMode.POSITION_CONTROL odrv0.axis0.controller.config.input_mode = InputMode.POS_FILTER odrv0.axis0.controller.config.input_filter_bandwidth = 20
Interfaces page: Enable CAN, enable feedback messages and set the Node ID to 0 (or something unique for every ODrive if you have multiple).
Optionally you can also enable the watchdog (also on the Interfaces page). This will make the ODrive stop when the CAN connection gets disrupted.
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
Open your Arduino IDE.
Go to Sketch > Include Library > Manage Libraries.
In the Library Manager, type “ODriveArduino” into the search box.
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):
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 aSerial.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 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
-
bool clearErrors()