From da8960bb50aace81825afa2d29d7d6b4c01929be Mon Sep 17 00:00:00 2001 From: Solomon Greenberg Date: Tue, 6 Jan 2026 10:05:47 -0800 Subject: [PATCH 1/2] Add support for ESP32 boards using the native TWAI (CAN) driver. Changes: - Add ODriveESP32TWAI.hpp interface that calls twai_transmit/twai_receive directly - Add IS_ESP32_TWAI option to SineWaveCAN example - Add esp32 PlatformIO target --- examples/SineWaveCAN/SineWaveCAN.ino | 57 +++++++++++++++++++++++- platformio.ini | 8 +++- src/ODriveESP32TWAI.hpp | 66 ++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/ODriveESP32TWAI.hpp diff --git a/examples/SineWaveCAN/SineWaveCAN.ino b/examples/SineWaveCAN/SineWaveCAN.ino index fb65417..b0f7fdc 100644 --- a/examples/SineWaveCAN/SineWaveCAN.ino +++ b/examples/SineWaveCAN/SineWaveCAN.ino @@ -21,11 +21,12 @@ // #define IS_ARDUINO_BUILTIN // Arduino boards with built-in CAN interface (e.g. Arduino Uno R4 Minima) // #define IS_MCP2515 // Any board with external MCP2515 based extension module. See below to configure the module. // #define IS_STM32_BUILTIN // STM32 boards with built-in CAN interface (e.g. STM32F4 Discovery). +// #define IS_ESP32_TWAI // ESP32 boards with built-in TWAI (CAN) interface. Directly uses the ESP-IDF TWAI driver. /* Board-specific includes ---------------------------------------------------*/ -#if defined(IS_TEENSY_BUILTIN) + defined(IS_ARDUINO_BUILTIN) + defined(IS_MCP2515) + defined(IS_STM32_BUILTIN) != 1 +#if defined(IS_TEENSY_BUILTIN) + defined(IS_ARDUINO_BUILTIN) + defined(IS_MCP2515) + defined(IS_STM32_BUILTIN) + defined(IS_ESP32_TWAI) != 1 #warning "Select exactly one hardware option at the top of this file." #if CAN_HOWMANY > 0 || CANFD_HOWMANY > 0 @@ -65,6 +66,12 @@ struct ODriveStatus; // hack to prevent teensy compile error #include "ODriveSTM32CAN.hpp" #endif // IS_STM32_BUILTIN +#ifdef IS_ESP32_TWAI +// See https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/twai.html +#include "driver/twai.h" +#include "ODriveESP32TWAI.hpp" +#endif // IS_ESP32_TWAI + /* Board-specific settings ---------------------------------------------------*/ @@ -160,6 +167,54 @@ bool setupCan() { #endif // IS_STM32_BUILTIN +/* ESP32 boards with built-in TWAI (CAN) */ + +#ifdef IS_ESP32_TWAI + +// Pins used to connect to CAN bus transceiver +#define ESP32_TWAI_TX_PIN 5 +#define ESP32_TWAI_RX_PIN 4 + +ESP32TWAIIntf can_intf; + +bool setupCan() { + twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT( + (gpio_num_t)ESP32_TWAI_TX_PIN, + (gpio_num_t)ESP32_TWAI_RX_PIN, + TWAI_MODE_NORMAL + ); + + twai_timing_config_t t_config; + switch (CAN_BAUDRATE) { + case 1000000: t_config = TWAI_TIMING_CONFIG_1MBITS(); break; + case 800000: t_config = TWAI_TIMING_CONFIG_800KBITS(); break; + case 500000: t_config = TWAI_TIMING_CONFIG_500KBITS(); break; + case 250000: t_config = TWAI_TIMING_CONFIG_250KBITS(); break; + case 125000: t_config = TWAI_TIMING_CONFIG_125KBITS(); break; + case 100000: t_config = TWAI_TIMING_CONFIG_100KBITS(); break; + case 50000: t_config = TWAI_TIMING_CONFIG_50KBITS(); break; + case 25000: t_config = TWAI_TIMING_CONFIG_25KBITS(); break; + default: t_config = TWAI_TIMING_CONFIG_250KBITS(); break; + } + + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + + if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + return false; + } + + if (twai_start() != ESP_OK) { + twai_driver_uninstall(); + return false; + } + + can_intf.initialized = true; + return true; +} + +#endif // IS_ESP32_TWAI + + /* Example sketch ------------------------------------------------------------*/ // Instantiate ODrive objects diff --git a/platformio.ini b/platformio.ini index 993d4af..c768ff0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,4 +46,10 @@ build_flags = ; See https://github.com/pazi88/STM32_CAN -DHAL_CAN_MODULE_ENABLED lib_deps = - https://github.com/pazi88/STM32_CAN.git#1.2.0 \ No newline at end of file + https://github.com/pazi88/STM32_CAN.git#1.2.0 + +[env:esp32] +platform = espressif32 +board = esp32dev +framework = arduino +build_flags = -DIS_ESP32_TWAI \ No newline at end of file diff --git a/src/ODriveESP32TWAI.hpp b/src/ODriveESP32TWAI.hpp new file mode 100644 index 0000000..c826c61 --- /dev/null +++ b/src/ODriveESP32TWAI.hpp @@ -0,0 +1,66 @@ +// CAN glue layer for ESP32 platforms using the native TWAI driver. +// See ODriveHardwareCAN.hpp for documentation. + +#pragma once + +#include "ODriveCAN.h" +#include "driver/twai.h" + +// Simple struct to hold TWAI interface state. Unlike other platforms, ESP32's +// TWAI driver uses global functions rather than a class instance. +struct ESP32TWAIIntf { + bool initialized = false; +}; + +struct CanMsg { + uint32_t id; + uint8_t len; + uint8_t buffer[8]; +}; + +// Must be defined by the application +void onCanMessage(const CanMsg& msg); + +static bool sendMsg(ESP32TWAIIntf& intf, uint32_t id, uint8_t length, const uint8_t* data) { + if (!intf.initialized) { + return false; + } + + twai_message_t tx_msg = {}; + tx_msg.identifier = id; + tx_msg.data_length_code = length; + tx_msg.extd = (id > 0x7FF) ? 1 : 0; + tx_msg.rtr = (data == nullptr) ? 1 : 0; + + if (data) { + for (int i = 0; i < length; ++i) { + tx_msg.data[i] = data[i]; + } + } + + return twai_transmit(&tx_msg, pdMS_TO_TICKS(100)) == ESP_OK; +} + +static void onReceive(const CanMsg& msg, ODriveCAN& odrive) { + odrive.onReceive(msg.id, msg.len, msg.buffer); +} + +static void pumpEvents(ESP32TWAIIntf& intf, int max_events = 100) { + if (!intf.initialized) { + return; + } + + // max_events prevents an infinite loop if messages come at a high rate + twai_message_t rx_msg; + while (twai_receive(&rx_msg, 0) == ESP_OK && max_events--) { + CanMsg msg; + msg.id = rx_msg.identifier; + msg.len = rx_msg.data_length_code; + for (int i = 0; i < rx_msg.data_length_code && i < 8; ++i) { + msg.buffer[i] = rx_msg.data[i]; + } + onCanMessage(msg); + } +} + +CREATE_CAN_INTF_WRAPPER(ESP32TWAIIntf) From 2bafe65c3951999d4f2655a5f91f163c386115e7 Mon Sep 17 00:00:00 2001 From: Solomon Greenberg Date: Wed, 7 Jan 2026 10:42:24 -0800 Subject: [PATCH 2/2] Remove CAN setup check for ESP32 TWAI, change extended ID flag format, make twai_transmit non-blocking --- examples/SineWaveCAN/SineWaveCAN.ino | 1 - src/ODriveESP32TWAI.hpp | 18 +++++------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/examples/SineWaveCAN/SineWaveCAN.ino b/examples/SineWaveCAN/SineWaveCAN.ino index b0f7fdc..0af6543 100644 --- a/examples/SineWaveCAN/SineWaveCAN.ino +++ b/examples/SineWaveCAN/SineWaveCAN.ino @@ -208,7 +208,6 @@ bool setupCan() { return false; } - can_intf.initialized = true; return true; } diff --git a/src/ODriveESP32TWAI.hpp b/src/ODriveESP32TWAI.hpp index c826c61..a6a4f2d 100644 --- a/src/ODriveESP32TWAI.hpp +++ b/src/ODriveESP32TWAI.hpp @@ -8,9 +8,7 @@ // Simple struct to hold TWAI interface state. Unlike other platforms, ESP32's // TWAI driver uses global functions rather than a class instance. -struct ESP32TWAIIntf { - bool initialized = false; -}; +struct ESP32TWAIIntf {}; struct CanMsg { uint32_t id; @@ -22,14 +20,11 @@ struct CanMsg { void onCanMessage(const CanMsg& msg); static bool sendMsg(ESP32TWAIIntf& intf, uint32_t id, uint8_t length, const uint8_t* data) { - if (!intf.initialized) { - return false; - } - + (void)intf; twai_message_t tx_msg = {}; tx_msg.identifier = id; tx_msg.data_length_code = length; - tx_msg.extd = (id > 0x7FF) ? 1 : 0; + tx_msg.extd = (id & 0x80000000) ? 1 : 0; tx_msg.rtr = (data == nullptr) ? 1 : 0; if (data) { @@ -38,7 +33,7 @@ static bool sendMsg(ESP32TWAIIntf& intf, uint32_t id, uint8_t length, const uint } } - return twai_transmit(&tx_msg, pdMS_TO_TICKS(100)) == ESP_OK; + return twai_transmit(&tx_msg, 0) == ESP_OK; } static void onReceive(const CanMsg& msg, ODriveCAN& odrive) { @@ -46,10 +41,7 @@ static void onReceive(const CanMsg& msg, ODriveCAN& odrive) { } static void pumpEvents(ESP32TWAIIntf& intf, int max_events = 100) { - if (!intf.initialized) { - return; - } - + (void)intf; // max_events prevents an infinite loop if messages come at a high rate twai_message_t rx_msg; while (twai_receive(&rx_msg, 0) == ESP_OK && max_events--) {