TWAI (CAN 2.0) APIs

The Twai class provides an idiomatic C++ interface to the ESP TWAI (Two-Wire Automotive Interface, i.e. CAN 2.0) peripheral. It wraps the modern node-based ESP-IDF driver (esp_driver_twai) and takes care of creating an on-chip TWAI node, registering the driver’s ISR event callbacks, and marshaling received frames (and optional error / state-change events) from ISR context into a task-context callback via an internal FreeRTOS queue and an espp::Task.

Because the driver’s on_rx_done / on_error / on_state_change callbacks run in ISR context, the Twai class copies each event into a FreeRTOS queue from the ISR (xQueueSendFromISR) and drains that queue from an internal task. Your on_receive, on_error, and on_state_change callbacks are therefore always invoked from task context - never from the ISR - so they may safely call blocking or non-IRAM-safe APIs. No lock is held while your callback runs.

The class supports the classic CAN 2.0 frame format (up to 8 data bytes). CAN-FD is a possible future extension; the underlying driver and frame types support it, but this wrapper intentionally keeps to classic CAN for a small, clean surface.

The example uses Mode::LOOPBACK (internal loopback + self-test) so it runs on a bare devkit with no CAN transceiver and no other node on the bus. To talk to a real bus, use Mode::NORMAL with a 3.3V CAN transceiver (e.g. SN65HVD230) wired to the TX/RX GPIOs and at least one other node present to acknowledge frames.

API Reference

Header File

Classes

class Twai : public espp::BaseComponent

A class to interface with the ESP TWAI (CAN 2.0) peripheral.

This class wraps the modern node-based ESP-IDF TWAI driver (esp_driver_twai) in an idiomatic C++ interface. It creates an on-chip TWAI node, registers ISR event callbacks, and marshals received frames (and optional error / state-change events) from ISR context into a task-context callback using an internal FreeRTOS queue and an espp::Task.

All user callbacks (on_receive, on_error, on_state_change) are invoked from the internal task context - never from the ISR - so they may safely call blocking / non-IRAM-safe APIs. No lock is held while a user callback is invoked.

The class supports the classic CAN 2.0 frame format (up to 8 data bytes). CAN-FD (up to 64 data bytes, bit-rate switching) is a possible future extension; the underlying driver and frame types support it, but this wrapper intentionally keeps to classic CAN for a small, clean surface.

TWAI (loopback) Example

  // Count the frames we receive back, so we can assert the round-trip.
  static std::atomic<int> num_received{0};

  // The receive callback runs in task context (not ISR). It is invoked for
  // every frame the node receives - in loopback mode that is every frame we
  // transmit.
  auto on_receive = [&](const espp::Twai::Message &msg) {
    num_received++;
    logger.info("RX: {}", msg);
  };

  auto on_state_change = [&](const espp::Twai::StateChange &sc) {
    logger.warn("State change: {} -> {}", static_cast<int>(sc.old_state),
                static_cast<int>(sc.new_state));
  };

  // NOTE: We use Mode::LOOPBACK (internal loopback + self-test) so this example
  // runs on a bare devkit with NO CAN transceiver and NO other node on the bus.
  // To talk to a real bus, use Mode::NORMAL with a transceiver (e.g.
  // SN65HVD230) wired to the tx/rx GPIOs, and make sure at least one other node
  // is present to acknowledge frames.
  espp::Twai twai({
      .tx_gpio = 5, // GPIO5 (change to match your board / transceiver)
      .rx_gpio = 4, // GPIO4 (change to match your board / transceiver)
      .baudrate = 500000,
      .mode = espp::Twai::Mode::LOOPBACK,
      .tx_queue_depth = 5,
      .on_receive = on_receive,
      .on_state_change = on_state_change,
      .log_level = espp::Logger::Verbosity::INFO,
  });

  std::error_code ec;
  if (!twai.initialize(ec)) {
    logger.error("Failed to initialize TWAI: {}", ec.message());
    return;
  }

  // Transmit a few frames; in loopback mode each is received back via on_receive.
  static constexpr int num_to_send = 5;
  for (int i = 0; i < num_to_send; i++) {
    espp::Twai::Message msg;
    msg.id = 0x100 + i;
    msg.extended = false;
    msg.rtr = false;
    msg.dlc = 4;
    msg.data = {static_cast<uint8_t>(i), 0xDE, 0xAD, 0xBE, 0, 0, 0, 0};
    if (twai.transmit(msg, ec)) {
      logger.info("TX: {}", msg);
    } else {
      logger.error("Failed to transmit frame {}: {}", i, ec.message());
    }
    std::this_thread::sleep_for(50ms);
  }

  // give the RX task a moment to drain the queue
  std::this_thread::sleep_for(200ms);

  logger.info("Sent {} frames, received {} frames", num_to_send, num_received.load());

  // print out the node status / statistics
  twai_node_status_t status;
  twai_node_record_t record;
  if (twai.get_info(status, record, ec)) {
    logger.info("Node state={}, tx_err={}, rx_err={}, bus_err_num={}",
                static_cast<int>(status.state), status.tx_error_count, status.rx_error_count,
                record.bus_err_num);
  }

  // assert the round trip
  if (num_received.load() == num_to_send) {
    logger.info("SUCCESS: all {} frames were received back in loopback mode", num_to_send);
  } else {
    logger.error("FAILURE: expected {} frames, got {}", num_to_send, num_received.load());
  }

Public Types

enum class Mode

The operating mode of the TWAI node.

Values:

enumerator NORMAL

Normal mode: transmit, receive, and acknowledge frames on the bus. Requires a transceiver and at least one other acknowledging node.

enumerator LISTEN_ONLY

Listen-only mode: the node only monitors the bus and never transmits or acknowledges. Useful for passive bus monitoring / sniffing.

enumerator LOOPBACK

Loopback self-test mode: the controller receives back the frames it transmits and does not require acknowledgement. This lets the node run with no transceiver and no other node on the bus (used by the example).

typedef std::function<void(const Message&)> receive_callback_fn

Receive callback type.

typedef std::function<void(const StateChange&)> state_change_callback_fn

State-change callback type.

typedef std::function<void(twai_error_flags_t)> error_callback_fn

Error callback type.

Public Functions

inline explicit Twai(const Config &config)

Construct a new Twai object.

Note

This does not touch the hardware; call initialize() to create the node and start the receive task.

Parameters:

config – The configuration for the TWAI node.

inline ~Twai()

Destructor. Stops the receive task and disables + deletes the node.

inline bool initialize(std::error_code &ec)

Initialize the TWAI node.

Creates the on-chip node, registers ISR event callbacks, applies the optional acceptance filter, creates the internal event queue and receive task, and (if Config::auto_start is true) enables the node.

Parameters:

ec – The error code, set if initialization fails.

Returns:

True on success, false on failure.

inline bool start(std::error_code &ec)

Enable (start) the TWAI node so it participates on the bus.

Parameters:

ec – The error code, set if the node could not be enabled.

Returns:

True on success, false otherwise.

inline bool stop(std::error_code &ec)

Disable (stop) the TWAI node.

Parameters:

ec – The error code, set if the node could not be disabled.

Returns:

True on success, false otherwise.

inline bool transmit(const Message &message, std::error_code &ec, int timeout_ms = DEFAULT_TX_TIMEOUT_MS)

Transmit a CAN message.

Parameters:
  • message – The message to transmit.

  • ec – The error code, set if the transmission could not be queued.

  • timeout_ms – Max time (ms) to wait if the TX queue is full (-1 = forever).

Returns:

True if the message was queued for transmission, false otherwise.

inline bool recover(std::error_code &ec)

Begin bus-off recovery.

Starts the recovery process for a node in the bus-off state. Use the on_state_change callback or get_status() to know when recovery has finished.

Parameters:

ec – The error code, set if recovery could not be started.

Returns:

True if recovery was started, false otherwise.

inline bool get_status(twai_node_status_t &status, std::error_code &ec)

Get the current node status (error state, error counters, TX queue space).

Parameters:
  • status – The status structure to fill in.

  • ec – The error code, set on failure.

Returns:

True on success, false otherwise.

inline bool get_statistics(twai_node_record_t &record, std::error_code &ec)

Get the node statistics (cumulative bus error count).

Parameters:
  • record – The statistics structure to fill in.

  • ec – The error code, set on failure.

Returns:

True on success, false otherwise.

inline bool get_info(twai_node_status_t &status, twai_node_record_t &record, std::error_code &ec)

Get both the node status and statistics.

Parameters:
  • status – The status structure to fill in.

  • record – The statistics structure to fill in.

  • ec – The error code, set on failure.

Returns:

True on success, false otherwise.

inline bool is_enabled() const

Whether the node is currently enabled (started).

Returns:

True if the node is enabled, false otherwise.

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns:

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters:

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters:

rate_limit – The rate limit to use for the logger

Public Static Attributes

static constexpr size_t MAX_DATA_LEN = 8

Maximum number of data bytes in a classic CAN 2.0 frame.

static constexpr int DEFAULT_TX_TIMEOUT_MS = 100

Default timeout (ms) used when transmitting a frame.

struct Config

Configuration for the TWAI node.

Public Members

int tx_gpio = {-1}

GPIO number for TWAI TX. Must be set (validated).

int rx_gpio = {-1}

GPIO number for TWAI RX. Must be set (validated).

uint32_t baudrate = {500000}

Bus baud rate / bit rate in bits/second (e.g. 500000).

Mode mode = {Mode::NORMAL}

Operating mode of the node.

size_t tx_queue_depth = {5}

Depth of the hardware transmit queue.

std::optional<Filter> filter = {}

Optional acceptance filter (default: accept all).

receive_callback_fn on_receive = {nullptr}

Called (in task context) for each received frame.

error_callback_fn on_error = {nullptr}

Optional: called (in task context) on a bus error.

state_change_callback_fn on_state_change{nullptr}

Optional: called (in task context) on an error-state change.

bool auto_start = {true}

If true, the node is enabled at the end of initialize().

size_t rx_queue_size = {16}

Size (number of events) of the internal ISR->task event queue.

Task::BaseConfig task_config  {.name = "Twai Task",.stack_size_bytes = 4096,.priority = 10,}

Configuration for the internal receive task.

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Log verbosity.

struct Filter

An acceptance (hardware) filter for received frames.

A frame is accepted if (received_id & mask) == (id & mask). A mask bit of 1 means the corresponding ID bit must match; a mask bit of 0 means “don’t care”. Therefore id = 0, mask = 0 accepts all frames.

Public Members

uint32_t id = {0}

The base ID to match.

uint32_t mask = {0}

The mask (1 = bit must match, 0 = don’t care).

bool extended = {false}

True to filter on extended (29-bit) IDs, false for standard (11-bit).

bool dual = {false}

Configure the filter as a dual 16-bit filter (advanced).

struct Message

A classic CAN 2.0 message / frame.

Public Functions

inline twai_frame_t to_twai_frame() const

Convert this Message into a driver twai_frame_t for transmission.

Note

The returned frame’s buffer points into this Message’s data array, so the Message must outlive the returned frame (which it does for the duration of a synchronous transmit call).

Returns:

A twai_frame_t describing this message.

Public Members

uint32_t id = {0}

Arbitration ID (11-bit standard, or 29-bit if extended is true).

bool extended = {false}

True for an extended (29-bit) ID, false for standard (11-bit) ID.

bool rtr = {false}

True if this is a Remote Transmission Request frame (no data).

uint8_t dlc = {0}

Data Length Code / number of valid data bytes (0-8).

std::array<uint8_t, MAX_DATA_LEN> data = {}

Frame payload (only the first dlc bytes valid).

Public Static Functions

static inline Message from_twai_frame(const twai_frame_t &frame)

Build a Message from a driver twai_frame_t.

Parameters:

frame – The driver frame (as returned by twai_node_receive_from_isr).

Returns:

A Message copy of the frame’s header + data.

struct StateChange

Data passed to the state-change callback.

Public Members

twai_error_state_t old_state

The previous error state.

twai_error_state_t new_state

The new error state.