Skip to content

TechOverflow/esp32_can

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

170 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ESP32 CAN library with legacy and FD support

This is an Arduino library for the built-in CAN (TWAI) controller(s) on ESP32 family chips. It provides a high-level, Arduino-friendly API that works across all ESP-IDF versions from v4.x through v5.5+ and across all ESP32 variants — from the original single-controller ESP32 to the three-controller ESP32-P4 and the CAN-FD-capable ESP32-C5.

Check out the CANipulator as an example of a device with dual-CAN transceivers.

A real-world implementation of the device and this library can be found here. In this example, we take certain messages from an incompatible ECU's CAN Bus, modify (translate) them to known required ones for the body or powertrain CAN Bus on the other side.


Relationship to collin80/esp32_can

This library is based on collin80/esp32_can, which is itself part of a family of CAN libraries sharing the collin80/can_common abstract base class. That base class defines the CAN_COMMON interface and the CAN_FRAME / CAN_FRAME_FD frame types used throughout.

The key differences introduced in this fork:

IDF 5.5+ support (TWAI_TIER_NEW). The original library targets IDF ≤ 5.4 and uses twai_driver_install / twai_receive (blocking). IDF 5.5 replaced the entire driver with esp_driver_twai (twai_new_node_onchip, callback-based async RX). This fork adds a complete third implementation tier for the new driver, selected automatically at compile time.

CAN-FD support. On chips where SOC_TWAI_SUPPORT_FD is defined (currently ESP32-C5), setDataBaudrate() enables the FD data phase and sendFrameFD() / get_rx_buffFD() handle frames up to 64 bytes. The FD path is compiled away entirely on non-FD chips.

Three-tier build system. Rather than a single #if guard, the library now selects one of three complete implementations at compile time based on ESP_IDF_VERSION:

TWAI_TIER_V1  →  IDF < 5.2    legacy single-controller API
TWAI_TIER_V2  →  IDF 5.2–5.4  legacy _v2 handle-based API
TWAI_TIER_NEW →  IDF ≥ 5.5    new esp_driver_twai component

Existing sketches written for the original library compile and run unchanged.


Supported hardware

Chip TWAI controllers CAN-FD CAN0 CAN1 CAN2
ESP32 (original) 1 No
ESP32-S2 1 No
ESP32-S3 1 No
ESP32-C3 1 No
ESP32-C6 2 No
ESP32-C5 2 Yes
ESP32-P4 3 No

The library selects the correct number of CAN globals at compile time using SOC_TWAI_CONTROLLER_NUM, so no code changes are needed when switching targets.


Dependencies

  • collin80/can_common — provides CAN_COMMON, CAN_FRAME, and CAN_FRAME_FD. Install via the Arduino Library Manager or manually.

Installation

  1. Install can_common via Arduino Library Manager or clone into your libraries/ folder.
  2. Clone or download this repository into Arduino/libraries/esp32_can/.
  3. Select your ESP32 board in the Arduino IDE — the correct IDF tier is picked automatically.

Basic usage

Single CAN controller (ESP32, ESP32-S2, ESP32-S3, ESP32-C3)

#include <esp32_can.h>

void setup() {
    Serial.begin(115200);

    CAN0.setCANPins(GPIO_NUM_16, GPIO_NUM_17); // RX, TX — adjust to your board
    CAN0.begin(500000);   // 500 kbps
    CAN0.watchFor();      // accept all IDs
}

void loop() {
    CAN_FRAME frame;
    if (CAN0.available() && CAN0.read(frame)) {
        Serial.printf("ID: %08X  DLC: %d  Data: %02X %02X %02X %02X\n",
            frame.id, frame.length,
            frame.data.byte[0], frame.data.byte[1],
            frame.data.byte[2], frame.data.byte[3]);
    }

    // Transmit
    CAN_FRAME tx;
    tx.id       = 0x123;
    tx.extended = false;
    tx.length   = 4;
    tx.data.byte[0] = 0xDE;
    tx.data.byte[1] = 0xAD;
    tx.data.byte[2] = 0xBE;
    tx.data.byte[3] = 0xEF;
    CAN0.sendFrame(tx);
}

Filtering specific IDs

// Accept only standard IDs 0x100–0x1FF (mask = 0x700, id = 0x100)
CAN0.watchFor(0x100, 0x700);

// Accept a single extended ID
CAN0.watchForRange(0x18DA00F1, 0x18DA00F1);

// Per-filter callback — fires when a matching frame arrives
CAN0.setCallback(0, [](CAN_FRAME *frame) {
    Serial.printf("Filter 0 hit: ID %08X\n", frame->id);
});

Listen-only mode (bus monitoring / sniffer)

CAN0.setListenOnlyMode(true);
CAN0.begin(500000);
CAN0.watchFor();

Auto-detect bus speed

// Probes common speeds from 1 Mbps down to 20 kbps.
// Returns the detected speed, or 0 if none found.
uint32_t detected = CAN0.beginAutoSpeed();
Serial.printf("Detected: %lu bps\n", detected);

Dual classic CAN (ESP32-C6, ESP32-P4)

Both controllers use the same API. CAN1 is available automatically when SOC_TWAI_CONTROLLER_NUM >= 2.

#include <esp32_can.h>

void setup() {
    // CAN0 — controller 0
    CAN0.setCANPins(GPIO_NUM_0, GPIO_NUM_1);
    CAN0.begin(500000);
    CAN0.watchFor();

    // CAN1 — controller 1
    CAN1.setCANPins(GPIO_NUM_2, GPIO_NUM_3);
    CAN1.begin(500000);
    CAN1.watchFor();
}

void loop() {
    CAN_FRAME frame;

    // Forward CAN0 → CAN1
    if (CAN0.available() && CAN0.read(frame)) {
        CAN1.sendFrame(frame);
    }

    // Forward CAN1 → CAN0
    if (CAN1.available() && CAN1.read(frame)) {
        CAN0.sendFrame(frame);
    }
}

Three-controller classic CAN (ESP32-P4)

CAN2 is available when SOC_TWAI_CONTROLLER_NUM >= 3. Usage is identical to CAN0/CAN1.

#include <esp32_can.h>

void setup() {
    CAN0.setCANPins(GPIO_NUM_0,  GPIO_NUM_1);
    CAN0.begin(500000);
    CAN0.watchFor();

    CAN1.setCANPins(GPIO_NUM_4,  GPIO_NUM_5);
    CAN1.begin(500000);
    CAN1.watchFor();

    CAN2.setCANPins(GPIO_NUM_8,  GPIO_NUM_9);
    CAN2.begin(500000);
    CAN2.watchFor();
}

Note: The ESP32-P4 does not support CAN-FD. Its three TWAI controllers are classic CAN 2.0B only. FD frames on the bus will be interpreted as errors. Each controller requires its own external transceiver (e.g. TJA1051).


CAN-FD (ESP32-C5)

CAN-FD is only available on chips where SOC_TWAI_SUPPORT_FD is defined. The FD data-phase baud rate must be set before calling begin().

#include <esp32_can.h>

void setup() {
    // Set FD data phase BEFORE begin()
    CAN0.setCANPins(GPIO_NUM_4, GPIO_NUM_5);
    CAN0.setDataBaudrate(2000000); // 2 Mbps data phase
    CAN0.begin(500000);            // 500 kbps arbitration phase
    CAN0.watchFor();

    CAN1.setCANPins(GPIO_NUM_6, GPIO_NUM_7);
    CAN1.setDataBaudrate(2000000);
    CAN1.begin(500000);
    CAN1.watchFor();
}

void loop() {
    // Receive FD frame
    CAN_FRAME_FD rxFD;
    if (CAN0.readFD(rxFD) && rxFD.fdMode) {
        Serial.printf("FD frame ID: %08X  DLC: %d\n", rxFD.id, rxFD.length);
        for (int i = 0; i < rxFD.length; i++) {
            Serial.printf(" %02X", rxFD.data.uint8[i]);
        }
        Serial.println();
    }

    // Transmit FD frame
    CAN_FRAME_FD txFD;
    txFD.id       = 0x123;
    txFD.extended = false;
    txFD.fdMode   = 1;     // mark as FD frame
    txFD.length   = 16;    // up to 64 bytes
    for (int i = 0; i < 16; i++) txFD.data.uint8[i] = i;
    CAN0.sendFrameFD(txFD);

    // Classic frames still work normally on a CAN-FD bus
    CAN_FRAME classic;
    classic.id     = 0x456;
    classic.length = 4;
    classic.data.byte[0] = 0xAA;
    CAN0.sendFrame(classic);
}

Mixed-bus note: Classic CAN nodes and CAN-FD nodes can coexist on the same bus. Classic nodes will correctly acknowledge FD frames during the arbitration phase but will not participate in the higher-speed data phase.


API reference

Initialisation

Method Description
setCANPins(rx, tx) Set GPIO pins — call before begin()
setDataBaudrate(bps) Enable CAN-FD data phase — call before begin() (FD chips only)
begin(bps) Start the controller at the given bit rate
beginAutoSpeed() Auto-detect bus speed; returns detected bps or 0
set_baudrate(bps) Change speed on a running controller (triggers restart)
setListenOnlyMode(bool) Receive-only, no TX or ACK
setNoACKMode(bool) Transmit without requiring acknowledgement (self-test)
setRXBufferSize(n) Set RX queue depth before begin()

Filtering

Method Description
watchFor() Accept all frames (clears filter table)
watchFor(id, mask) Accept frames matching (id & mask) == (rxId & mask)
watchForRange(low, high) Accept IDs in the given inclusive range
setCallback(filter, fn) Register a callback for a specific filter slot
setGeneralCallback(fn) Register a callback for all accepted frames

Receive

Method Description
available() Number of frames waiting in the RX queue
read(frame) Read the oldest frame from the queue into frame
readFD(frame) Read an FD frame (FD chips only)

Transmit

Method Description
sendFrame(frame) Transmit a classic CAN frame; returns true on success
sendFrameFD(frame) Transmit a CAN-FD frame (FD chips only)

Reset

Method Description
resetIfStale(stallMs) Call from loop() if you want explicit sketch-side stall recovery

Diagnostics

Method Description
getControllerStats() Exposes the controller's hardware error counters straight from the IDF driver

These are the same values the controller's internal error-confinement state machine uses to drive bus-off recovery — TEC and REC follow CAN 2.0B / ISO 11898 semantics (increment on errors, decrement on success; cross 128 → error-passive, cross 256 → bus-off).

#include <esp32_can.h>
 
void loop() {
    CANControllerStats stats;
    if (CAN0.getControllerStats(stats)) {
        Serial.printf("TEC=%u  REC=%u  bus=%u  rx_overrun=%u\n",
                      stats.txErrorCount,
                      stats.rxErrorCount,
                      stats.busErrorCount,
                      stats.rxOverrunCount);
    }
    delay(1000);
}

Returns true on success; false if the controller hasn't been initialised yet or the underlying IDF call fails. On failure the contents of stats are undefined — always check the return value before reading.

Field Description
txErrorCount TEC. Increments on TX errors, decrements on successful TX. Reaches 128 → error-passive; 256 → bus-off.
rxErrorCount REC. Same scheme for RX errors.
busErrorCount Cumulative bus errors observed since boot or last bus-off recovery.
rxOverrunCount Frames discarded because the RX queue was full. Indicates either bus rate > what the driver can keep up with, or a starved RX task.

All four fields are saturated to uint16_t (max 65535). The V1/V2 drivers internally use uint32_t, but for the diagnostic use cases these counters are intended for, the upper bytes are noise — anything above a few hundred is already deep into pathological territory.

Note: The TIER_NEW (esp_driver_twai, IDF 5.5+) driver does not expose the RX overrun counter — there's no public accessor as of IDF 5.5 even though the hardware counter still increments, so getControllerStats() always returns 0 in that field rather than removing it from the struct for consistency across all three tiers. If a future IDF release adds the accessor, this field will be updated to return the true value.

Typical interpretations

  • TEC and REC both 0, no bus errors growing: healthy bus, no faults observed.
  • TEC climbing alone: TX side of the bus is unhealthy — bad termination on the local end, a node not acknowledging, or wiring fault.
  • REC climbing alone: RX side noise — likely EMI, ground bounce, or another node sending malformed frames.
  • Both climbing in lockstep: bus-wide problem — clock mismatch between nodes (wrong bit rate somewhere), severe noise, or shorted line.
  • rxOverrunCount non-zero (V1/V2 only): RX task can't keep up. Either lower the bus rate, raise the RX queue size with setRXBufferSize(), or check that your loop is not blocking.
  • Controller in TWAI_ERROR_BUS_OFF (visible via state callbacks or the library's automatic recovery): TEC reached 256. The library's watchdog will recover automatically; check counters afterward to find the root cause.

Troubleshooting

Linker errors referencing twai_* functions — you are likely mixing IDF versions or have a stale build cache. In Arduino IDE select Sketch → Clean Build Folder then rebuild.

Frames received but callbacks never fire — confirm you called watchFor() after begin(), and that the filter ID/mask matches your traffic. The filter table uses exact-match logic: (receivedId & mask) == (filterId & mask).

Bus-off recovery — the internal watchdog task polls every 200 ms and automatically restarts the controller after a bus-off event. No action needed in your sketch.

CAN-FD: setDataBaudrate() has no effect — it must be called before begin(). Calling it afterwards requires CAN0.disable(); CAN0.enable(); to take effect.

ESP32-P4 with three controllers — each controller needs its own transceiver chip and its own pair of GPIO pins. GPIO assignment is entirely flexible via setCANPins(); there are no fixed TWAI GPIO pins on the P4.


Additional examples

CANipulator_test_loopback
Verifies that both on-chip CAN controllers AND both external transceivers are working, and that they can communicate with each other over a shared physical bus by sending messages back and forth. Connect CAN-L to CAN-L, CAN-H to CAN-H. Ground is likely already common. A 120 Ω termination resistor between CAN-H and CAN-L at each end of the bus is recommended (two resistors total). Without termination, short test jumpers may still work, but a real bus needs them.

CANipulator_test_forwarder
This example simply forwards all traffic from CAN0 to CAN1 a vice-versa. Great for debugging which messages originate from a certain node.

CANipulator_C5_test_forwarder
Same as the previous, but with FD support.

Credits:
Collin80/esp32_can - Original esp32_can library
outlandnish/esp32_can - Majority of twai_v2 library updates
sans-ltd/esp32_can - Bugfixes

Implements a CAN driver for the built-in CAN hardware on an ESP32. The builtin CAN is called CAN0, and also CAN1 if there is a second CAN port on the ESP32 chip in use. This library requires the can_common library. That library is a common base that other libraries can be built off of to allow a more universal API for CAN.

The needed can_common library is found here: https://github.com/collin80/can_common Some examples use the SmartLeds library found here: https://github.com/RoboticsBrno/SmartLeds


Disclaimer

This library and any associated hardware are intended solely for development, testing, educational, and off-road applications. Users are responsible for ensuring compliance with all applicable laws and regulations. The software and associated hardware are provided as-is and are not certified for use in road-going vehicles and should not be used to modify, control, or interfere with vehicle systems while operating on public roads.

About

Arduino ESP32 library supporting multiple on-chip TWAI (CAN) controllers and CAN-FD

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • C++ 99.1%
  • C 0.9%