ODrive Pro - CAN Transmitting But Not Receiving

Hi,

We’re currently debugging an issue with the ODrive Pro CAN interface. The ODrive connections are identical to a previous project we’ve used with a Pro (albeit with slightly older firmware). CAN ground is connected to DC- and the bus appears to be terminated correctly. The bus speed is 500kbit/s. Compare to our previous project the bus is slightly longer at around 30m, as opposed to the previous 24m.

We’re receiving the ODrive heartbeat messages (and all other cyclic messages) on the CAN bus but the ODrive does not respond to any messages we send it (e.g. Set_Input_Pos or TxSdo and RxSdo). As we’re receiving the cyclic messages without any problems, we’re assuming that it isn’t a problem with the physical configuration of the CAN bus. n_rx remains at zero during the time period where we send the ODrive messages.

The ODrive firmware version is 0.6.11, which is slightly newer than the firmware version we used previously (0.6.10). Are there any new ODrive configuration settings that need to be set in order for the ODrive to respond to CAN commands? Are there any recommended debug steps?

Hello ! I am experiencing the same problem with a ODrive S1 Can Interface. The only messages I receive have ID’s 9 and 1, so the cyclic ones.
odrv0.request(vbus, 1000) get no response
odrv0.setState(ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL); neither
odrv0.getEndpoint() neither…

I’m searching but for now no idea what is the problem.

Hi! I would definitely double check that the node ID and baudrate are set, and not left at the default. Could you both run odrivetool backup-config config.json and paste the resulting config.json contents here? That’ll let me see the whole parameter tree.

Cc @jb803

Hi, thanks for your reply. In the Odrive GUI the node id is set (to 0 ) and baudrate too.
I have to add that the sinewave test motion works. So messages are passing through,one way (for the position control) and the other (for heartbeats and others cyclic messages). Even set state manage to power on and off the motor, it’s just it can’t get any response.
I will send you the JSON file you asked as soon as possible.
Thanks
Adrien

Interesting! So specifically everything’s working except for the request and getEndpoint messages? Can you post your overall Arduino code?

Well my understanding is more that ODrive doesn’t send messages except the cyclic ones. In fact for instance it seems that my function setVelocityLimit works (without acknowledgment response) but getVelocityLimit doesn’t.
Here is the code : it’s the sineWave example slightly modified for ESP32S3 with MCP2515 with additional functions home made in order to test those problems.



/*
Basé sur l'exemple SineWave de ODrive, avec des fonctions ajoutées  :
- une fonction pour Clear Errors
- une fonction pour Toggle MOTOR
- des fonction get et set pour quelques valeurs :
    - velocity limit
    - position gain
    - velocity gain
    - velocity intergrator gain

Pour tester on ajoute juste le toggle motors et le clear errors toutes les 15 sec,  avec le get vals
+ un set vals pour voir 

 * ────────────────────────────────────────────────────────────────────────
 * Fonctions exposées
 * ────────────────────────────────────────────────────────────────────────
 *  clearErrors()                          – efface les erreurs ODrive
 *  setMotorEnable(bool enable)            – active / coupe le moteur
 *  getVelocityLimit() / setVelocityLimit()
 *  getPositionGain()  / setPositionGain()
 *  getVelocityGain()  / setVelocityGain()
 *  getVelIntegratorGain() / setVelIntegratorGain()


CODE DU TEST CAN ODRIVE POUR ESP32S3
En utilisant la lib SPI classique : 
c'est à dire sans séparer les 2 SPI disponibles sur le S3 (VSPI et HSPI)
-> Quand on appelle SPI sans spécifier, c'est le VSPI qui est appelé

  Type de SPI :         VSPI          HSPI
  MOSI Pin                11            35
  MISO Pin                13            37
  CLK Pin                 12            36
  CS Pin                  10
  INT Pin (MCP2515)       14
*/



#include <Arduino.h>
#include "ODriveCAN.h"

// Documentation for this example can be found here:
// https://docs.odriverobotics.com/v/latest/guides/arduino-can-guide.html


/* Configuration of example sketch -------------------------------------------*/
#define SINEWAVE_MOVE

//#define TRIANGLE_MOVE
#define TRIANGLE_AMPLITUDE  1
#define TRIANGLE_SPEED      1

// CAN bus baudrate. Make sure this matches for every device on the bus
#define CAN_BAUDRATE 250000

// ODrive node_id for odrv0
#define ODRV0_NODE_ID 0


 #define IS_MCP2515 // Any board with external MCP2515 based extension module. See below to configure the module.


/* Board-specific includes ---------------------------------------------------*/

#ifdef IS_MCP2515
// See https://github.com/sandeepmistry/arduino-CAN/
#include "MCP2515.h"
#include "ODriveMCPCAN.hpp"
#endif // IS_MCP2515






/* Board-specific settings ---------------------------------------------------*/



/* MCP2515-based extension modules -*/

#ifdef IS_MCP2515

MCP2515Class& can_intf = CAN;

// chip select pin used for the MCP2515
#define MCP2515_CS 10

// interrupt pin used for the MCP2515
// NOTE: not all Arduino pins are interruptable, check the documentation for your board!
#define MCP2515_INT 14

// freqeuncy of the crystal oscillator on the MCP2515 breakout board. 
// common values are: 16 MHz, 12 MHz, 8 MHz
#define MCP2515_CLK_HZ 8000000

/*
static inline void receiveCallback(int packet_size) {
  if (packet_size > 8) {
    Serial.println("Debug_TooBigMessage");
    return; // not supported
  }
  Serial.println("rcvdMessage");
  CanMsg msg = {.id = (unsigned int)CAN.packetId(), .len = (uint8_t)packet_size};
  CAN.readBytes(msg.buffer, packet_size);
  onCanMessage(msg);
}*/
void receiveMessage(void) {
  int packet_size = CAN.parsePacket();
  if (packet_size) {  
    if (packet_size > 8) {
      Serial.println("Debug_TooBigMessage");
      return; // not supported
    }
    //Serial.println("rcvdMessage");
    CanMsg msg = {.id = (unsigned int)CAN.packetId(), .len = (uint8_t)packet_size};
    CAN.readBytes(msg.buffer, packet_size);
    onCanMessage(msg);
  }
}

bool setupCan() {
  // configure and initialize the CAN bus interface
  CAN.setPins(MCP2515_CS, MCP2515_INT);
  //CAN.setClockFrequency(MCP2515_CLK_HZ);
  if (!CAN.begin(CAN_BAUDRATE)) {
    return false;
  }

  //CAN.onReceive(receiveCallback);
  return true;
}

#endif // IS_MCP2515





/* Example sketch ------------------------------------------------------------*/

// Instantiate ODrive objects
ODriveCAN odrv0(wrap_can_intf(can_intf), ODRV0_NODE_ID); // Standard CAN message ID
ODriveCAN* odrives[] = {&odrv0}; // Make sure all ODriveCAN instances are accounted for here

struct ODriveUserData {
  Heartbeat_msg_t last_heartbeat;
  bool received_heartbeat = false;
  Get_Encoder_Estimates_msg_t last_feedback;
  bool received_feedback = false;
};

// Keep some application-specific user data 
//for every ODrive.
ODriveUserData odrv0_user_data;


// Called every time a Heartbeat message arrives from the ODrive
void onHeartbeat(Heartbeat_msg_t& msg, void* user_data) {
  ODriveUserData* odrv_user_data = static_cast<ODriveUserData*>(user_data);
  odrv_user_data->last_heartbeat = msg;
  odrv_user_data->received_heartbeat = true;
  //Serial.println("Heartbeat_rcvd");
}

// Called every time a feedback message arrives from the ODrive
void onFeedback(Get_Encoder_Estimates_msg_t& msg, void* user_data) {
  ODriveUserData* odrv_user_data = static_cast<ODriveUserData*>(user_data);
  odrv_user_data->last_feedback = msg;
  odrv_user_data->received_feedback = true;
  //Serial.println("Feedback_rcvd");
}

// Called for every message that arrives on the CAN bus
void onCanMessage(const CanMsg& msg) {
  Serial.print("Received CAN message with ID: ");
  Serial.println(msg.id, HEX);  // Print CAN message ID for debugging
  for (auto odrive: odrives) {
    onReceive(msg, *odrive);
  }
}





////////////////////////////////////////// AJOUTS FONCTIONS test : ///////////////////////////////////////////////////

// ══════════════════════════════════════════════════════════════════════════
//  1. Clear Errors
// ══════════════════════════════════════════════════════════════════════════
/**
 * Efface toutes les erreurs actives sur l'ODrive.
 * Utilise odrv0.clearErrors() de ODriveCAN.h.
 */
void clearErrors()
{
    odrv0.clearErrors();
    Serial.println("[ODrive] clearErrors() envoyé.");
}
 
// ══════════════════════════════════════════════════════════════════════════
//  2. Allumer / éteindre le moteur
// ══════════════════════════════════════════════════════════════════════════
/**
 * Active ou coupe le moteur.
 *   enable = true  → CLOSED_LOOP_CONTROL  (moteur sous tension, asservi)
 *   enable = false → IDLE                 (moteur libre)
 *
 * Utilise odrv0.setState() de ODriveCAN.h.
 * Attend la confirmation via le Heartbeat (avec timeout 2 s).
 */
void setMotorEnable(bool enable)
{
    ODriveAxisState target = enable
        ? ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL
        : ODriveAxisState::AXIS_STATE_IDLE;
 
    uint32_t deadline = millis() + 2000;
 
    while (odrv0_user_data.last_heartbeat.Axis_State != target) {
        if (millis() > deadline) {
            Serial.printf("[ODrive] setMotorEnable(%d) : timeout\n", enable);
            return;
        }
        odrv0.clearErrors();
        delay(1);
        odrv0.setState(target);
 
        // Pomper les événements CAN pendant ~150 ms pour recevoir le Heartbeat
        for (int i = 0; i < 15; ++i) {
            delay(10);
            pumpEvents(can_intf);
        }
    }
 
    Serial.printf("[ODrive] Moteur %s\n",
                  enable ? "ALLUMÉ (CLOSED_LOOP)" : "ÉTEINT (IDLE)");
}
 
// ══════════════════════════════════════════════════════════════════════════
//  3. Velocity Limit  (vitesse max en tr/s)
// ══════════════════════════════════════════════════════════════════════════
/**
 * Lit la Velocity Limit ET le Current Soft Max actuels.
 * Utilise odrv0.setLimits() pour les deux à la fois (protocole CAN Simple).
 *
 * Note : la librairie ne fournit pas de getter dédié pour ces valeurs ;
 * on utilise getEndpoint<float>() avec les IDs du flat_endpoints.json.
 *
 * EP 213 = axis0.controller.config.vel_limit
 * EP 215 = axis0.motor.config.current_soft_max  (courant max)
 */
static constexpr uint16_t EP_VEL_LIMIT        = 213;
static constexpr uint16_t EP_CURRENT_SOFT_MAX = 215;



float getVelocityLimit()
{
    float val = odrv0.getEndpoint<float>(EP_VEL_LIMIT);
    Serial.printf("[ODrive] Velocity Limit = %.4f tr/s\n", val);
    return val;
}
 
/**
 * Modifie la Velocity Limit.
 * current_soft_max est lu au préalable pour ne pas l'écraser.
 */
void setVelocityLimit(float vel_limit)
{
    float current_max = odrv0.getEndpoint<float>(EP_CURRENT_SOFT_MAX);
    odrv0.setLimits(vel_limit, current_max);
    Serial.printf("[ODrive] Velocity Limit → %.4f tr/s\n", vel_limit);
}
 
// ══════════════════════════════════════════════════════════════════════════
//  4. Position Gain  (boucle de position)
// ══════════════════════════════════════════════════════════════════════════
/**
 * Lit le Position Gain.
 * EP 160 = axis0.controller.config.pos_gain
 */
static constexpr uint16_t EP_POS_GAIN = 160;
 
float getPositionGain()
{
    float val = odrv0.getEndpoint<float>(EP_POS_GAIN);
    Serial.printf("[ODrive] Position Gain = %.4f\n", val);
    return val;
}
 
/**
 * Modifie le Position Gain.
 * Utilise odrv0.setPosGain() de ODriveCAN.h.
 */
void setPositionGain(float pos_gain)
{
    odrv0.setPosGain(pos_gain);
    Serial.printf("[ODrive] Position Gain → %.4f\n", pos_gain);
}
 
// ══════════════════════════════════════════════════════════════════════════
//  5. Velocity Gain  (boucle de vitesse)
// ══════════════════════════════════════════════════════════════════════════
/**
 * Lit le Velocity Gain.
 * EP 161 = axis0.controller.config.vel_gain
 */
static constexpr uint16_t EP_VEL_GAIN = 161;
 
float getVelocityGain()
{
    float val = odrv0.getEndpoint<float>(EP_VEL_GAIN);
    Serial.printf("[ODrive] Velocity Gain = %.4f\n", val);
    return val;
}
 
/**
 * Modifie le Velocity Gain (et conserve le Vel Integrator Gain existant).
 * Utilise odrv0.setVelGains() de ODriveCAN.h.
 */
void setVelocityGain(float vel_gain)
{
    // On lit l'intégrateur pour ne pas l'écraser
    float vel_int = odrv0.getEndpoint<float>(EP_VEL_GAIN + 1); // EP 162
    odrv0.setVelGains(vel_gain, vel_int);
    Serial.printf("[ODrive] Velocity Gain → %.4f\n", vel_gain);
}
 
// ══════════════════════════════════════════════════════════════════════════
//  6. Velocity Integrator Gain
// ══════════════════════════════════════════════════════════════════════════
/**
 * Lit le Velocity Integrator Gain.
 * EP 162 = axis0.controller.config.vel_integrator_gain
 */
static constexpr uint16_t EP_VEL_INTEGRATOR_GAIN = 162;
 
float getVelIntegratorGain()
{
    float val = odrv0.getEndpoint<float>(EP_VEL_INTEGRATOR_GAIN);
    Serial.printf("[ODrive] Vel Integrator Gain = %.4f\n", val);
    return val;
}
 
/**
 * Modifie le Velocity Integrator Gain (et conserve le Vel Gain existant).
 * Utilise odrv0.setVelGains() de ODriveCAN.h.
 */
void setVelIntegratorGain(float vel_integrator_gain)
{
    float vel_gain = odrv0.getEndpoint<float>(EP_VEL_GAIN);
    odrv0.setVelGains(vel_gain, vel_integrator_gain);
    Serial.printf("[ODrive] Vel Integrator Gain → %.4f\n", vel_integrator_gain);
}
///////////////////////////////////////////fin AJOUTS test /////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  Serial.println("test1");
  delay(100);
  Serial.println("test2");

delay(1000);
  Serial.println("test3");

  // Wait for up to 3 seconds for the serial port to be opened on the PC side.
  // If no PC connects, continue anyway.
//  for (int i = 0; i < 30 && !Serial; ++i) {
//    delay(100);
//  }
  delay(200);


  Serial.println("Starting ODriveCAN demo");

  // Register callbacks for the heartbeat and encoder feedback messages
  odrv0.onFeedback(onFeedback, &odrv0_user_data);
  odrv0.onStatus(onHeartbeat, &odrv0_user_data);

  // Configure and initialize the CAN bus interface. This function depends on
  // your hardware and the CAN stack that you're using.
  if (!setupCan()) {
    Serial.println("CAN failed to initialize: reset required");
    while (true); // spin indefinitely
  }

  Serial.println("Waiting for ODrive...");
  while (!odrv0_user_data.received_heartbeat) {
    pumpEvents(can_intf);
    receiveMessage();
    delay(100);
  }

  Serial.println("found ODrive");

  // request bus voltage and current (1sec timeout)
  Serial.println("attempting to read bus voltage and current");
  Get_Bus_Voltage_Current_msg_t vbus;
  if (!odrv0.request(vbus, 1000)) {
    Serial.println("vbus request failed!");
    //while (true); // spin indefinitely
  }

  Serial.print("DC voltage [V]: ");
  Serial.println(vbus.Bus_Voltage);
  Serial.print("DC current [A]: ");
  Serial.println(vbus.Bus_Current);

  Serial.println("Enabling closed loop control...");
  while (odrv0_user_data.last_heartbeat.Axis_State != ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL) {
    odrv0.clearErrors();
    delay(1);
    odrv0.setState(ODriveAxisState::AXIS_STATE_CLOSED_LOOP_CONTROL);

    // Pump events for 150ms. This delay is needed for two reasons;
    // 1. If there is an error condition, such as missing DC power, the ODrive might
    //    briefly attempt to enter CLOSED_LOOP_CONTROL state, so we can't rely
    //    on the first heartbeat response, so we want to receive at least two
    //    heartbeats (100ms default interval).
    // 2. If the bus is congested, the setState command won't get through
    //    immediately but can be delayed.
    for (int i = 0; i < 15; ++i) {
      delay(10);
      pumpEvents(can_intf);
      receiveMessage();
    }
  }

  Serial.println("ODrive running!");
}

bool motorState=1;
#define DELAY_BOUCLE_TOGGLE 10000
uint32_t cur_time_ms, toggle_time_ms;
void loop() {
  cur_time_ms=millis();
  pumpEvents(can_intf); // This is required on some platforms to handle incoming feedback CAN messages
                        // Note that on MCP2515-based platforms, this will delay for a fixed 10ms.
                        //
                        // This has been found to reduce the number of dropped messages, however it can be removed
                        // for applications requiring loop times over 100Hz.
  receiveMessage();

  if (((cur_time_ms - toggle_time_ms) > DELAY_BOUCLE_TOGGLE)){
    toggle_time_ms=cur_time_ms;
    motorState=!motorState;
    if (!motorState){
      setMotorEnable(0);
    }
    else{
      clearErrors();
      setMotorEnable(1);  
      float test=getVelocityLimit();
      getPositionGain();
      getVelocityGain();
      getVelIntegratorGain();
      //setVelocityLimit(test-0.1);

        // request bus voltage and current (1sec timeout)
      Serial.println("attempting to read bus voltage and current");
      Get_Bus_Voltage_Current_msg_t vbus;
      if (!odrv0.request(vbus, 5000)) {
        Serial.println("vbus request failed!");
        //while (true); // spin indefinitely
      }

      Serial.print("DC voltage [V]: ");
      Serial.println(vbus.Bus_Voltage);
      Serial.print("DC current [A]: ");
      Serial.println(vbus.Bus_Current);
    }
  }

  if (motorState){
    float SINE_PERIOD = 2.0f;//1.0f; // Period of the position command sine wave in seconds

    float t = 0.001 * millis();
    
    float phase = t * (TWO_PI / SINE_PERIOD);


    #ifdef SINEWAVE_MOVE
    odrv0.setPosition(
      sin(phase),//*3, // position
      cos(phase) * (TWO_PI / SINE_PERIOD) // velocity feedforward (optional)
    );
    #endif
    #ifdef TRIANGLE_MOVE
    static uint32_t po;
    #endif

    // print position and velocity for Serial Plotter
    if (odrv0_user_data.received_feedback) {
      Get_Encoder_Estimates_msg_t feedback = odrv0_user_data.last_feedback;
      odrv0_user_data.received_feedback = false;
      //Serial.print("odrv0-pos:");
      //Serial.print(feedback.Pos_Estimate);
      //Serial.print(",");
      //Serial.print("odrv0-vel:");
      //Serial.println(feedback.Vel_Estimate);
    }
  }
}


Two issues:

  1. I think your endpoint IDs are wrong. I would definitely double check your endpoints against the flat_endpoints.json matching your ODrive and firmware version.

From your code:

static constexpr uint16_t EP_VEL_LIMIT        = 213;
static constexpr uint16_t EP_CURRENT_SOFT_MAX = 215;

But on an S1 running 0.6.11 firmware, axis0.controller.config.vel_limit is endpoint ID 374 and axis0.config.motor.current_soft_max is endpoint ID 305. Endpoint IDs 213 and 215 actually correspond to axis0.current_state and axis0.pos_estimate. I’d make sure you’re using the correct file from here: ODrive Releases

  1. Here’s the main issue – this line is commented out :slight_smile:

As a result, the received messages are never getting routed to your code, which explains why you can send but not receive. Try uncommenting that line and see if things work!

Wow I don’t remember why I commented this line. Thank you a lot for the quick answer, obviously it should be this, I will try it tonight.
Sorry to bother you with such a silly mistake.

I will verify and confirm this quickly.

No worries, glad I could help, and I hope that’s the issue! Let me know how it goes!

Hello ! Okay I corrected my code, it didn’t help and it made me remember why I had commented this line in the first place.
At the beginning I had troubles with the fact that I am on a ESP32S3 board with a MCP2515. So I needed to modify a little the Sandeep Mistry Can lib to make it work (there has been some similar discussions in the forum here in the past) but I think some interrupt functions doesn’t work well. So the receiveCallback() function never activates, that’s why I had created the similar receiveMessage() function that I had paste manually in the loop in each while loop waiting for pumpEvents(). It did the trick to make it work but misses probably my other commands.

I am waiting for a TWAI-to-CAN transceiver delivery to test the direct CAN control from the ESP32, it should be a radical solution but I still wonder about the MCP2515 problems.
What I don’t get is the initial cause of my troubles. In the code the pumpEvents() funtion with MCP2515 looks like a simple delay, which means that a ISR should be running somewhere waiting for a response, but I don’t succeed in activiting it.

Another question, unrelated : the Endpoints change for each board and each firmware version ? It means that we need to adapt the code at each firmware update ? Do we have a way code it more permanently ?

Have a good day
Adrien

Oh interesting, yes I remember someone having an issue with MCP2515+ESP32 before – same thing with the interrupts, something doesn’t play nice. Switching to an external transceiver + TWAI is likely your best bet (a generic SN65HVD230 CAN breakout works great).

Regarding the endpoints change, yes that needs to be synchronized with the board type and firmware. I actually have it on my TODO to make it a bit easier to pull in endpoint definitions from a given flat_endpoints.json in the Arduino library, good reminder to poke around a bit with that :slight_smile: