Chapter 153: CAN Frame Transmission and Reception

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand and correctly utilize the twai_message_t structure for constructing and interpreting CAN frames.
  • Transmit standard CAN data frames using the TWAI driver.
  • Transmit extended CAN data frames by appropriately setting message flags.
  • Understand how to formulate and transmit CAN Remote Frames (RTR).
  • Receive both standard and extended CAN data frames.
  • Interpret received Remote Frames.
  • Implement both blocking and non-blocking (timeout-based) transmission and reception strategies.
  • Comprehend the role and behavior of the transmit (TX) and receive (RX) queues.
  • Identify and handle common return codes and potential errors during frame transmission and reception.

1. Introduction

In the preceding chapters, you’ve learned about the fundamentals of the CAN protocol (Chapter 151) and how to configure the ESP32‘s TWAI peripheral for operation (Chapter 152). With the driver installed and started, the next logical step is to actually exchange information over the CAN bus. This involves formatting data into CAN frames, initiating their transmission, and on the other side, receiving incoming frames and parsing their contents.

This chapter is dedicated to the core functions of sending and receiving CAN messages using the ESP-IDF TWAI driver. We will delve into the twai_message_t structure, which is the cornerstone for defining messages to be sent and interpreting those received. You will learn how to use the twai_transmit() and twai_receive() functions, understand their blocking and non-blocking behaviors, and manage the underlying software queues. Mastering these operations is essential for building any functional CAN-based application on the ESP32.

2. Theory

At the heart of sending and receiving CAN messages with the TWAI driver is the twai_message_t structure. This structure is used both to prepare a message for transmission and to store a message that has been received.

2.1. The twai_message_t Structure

Defined in driver/twai_types.h, this structure encapsulates all the necessary components of a Classical CAN frame:

C
typedef struct {
    // The following bits are for message formatting.
    uint32_t flags;                             /*!< Bit field of message flags. See Naming Convention section of API guide for details. */
                                                // Composed of:
                                                // - TX_MSG_FLAG_EXTD / RX_MSG_FLAG_EXTD (for extd field)
                                                // - TX_MSG_FLAG_RTR / RX_MSG_FLAG_RTR (for rtr field)
                                                // - TX_MSG_FLAG_SS (for single-shot transmission, ESP32 only)
                                                // - TX_MSG_FLAG_SELF (for self reception request, ESP32 only)
                                                // Note: For basic usage, we often set extd and rtr directly via dedicated members
                                                // if the simplified version of the struct is considered.
                                                // Let's look at the commonly documented/used version of the struct.

    // The ESP-IDF documentation and examples more commonly refer to a version of the struct
    // that has individual boolean flags rather than a single `flags` bitfield for basic CAN 2.0B usage.
    // For example, from `twai_transmit()` docs:
    // twai_message_t message = {
    //     .identifier = 0xAAAA, .extd = 1, .data_length_code = 4, .data = {0x01, 0x02, 0x03, 0x04}
    // };
    // Let's use the structure definition that aligns with this common usage pattern.
    // This usually means the `flags` field is abstracted or handled by macros/helper functions,
    // or there are distinct members for these common flags.

    // Re-checking `driver/twai_types.h` for ESP-IDF v5.1:
    // The `twai_message_t` structure indeed has a `uint32_t flags;` member.
    // And macros like `TWAI_MSG_FLAG_EXTD`, `TWAI_MSG_FLAG_RTR`.
    // However, it also defines:
    // uint32_t identifier;                     /*!< 11 or 29 bit identifier */
    // uint8_t data_length_code;                /*!< Data length code (0 to 8) */
    // uint8_t data[TWAI_FRAME_MAX_DLC];        /*!< Data bytes (not relevant in RTR frame) */

    // The `flags` field is used to set properties like:
    // - TWAI_MSG_FLAG_EXTD: Message is an extended frame
    // - TWAI_MSG_FLAG_RTR: Message is a remote frame
    // - TWAI_MSG_FLAG_SS: Single shot transmission (ESP32 only, message will not be retransmitted on error)
    // - TWAI_MSG_FLAG_SELF: Self reception request (ESP32 only, message will be received by the same node if loopback is enabled by SELF_TEST mode)
    // - TWAI_MSG_FLAG_DLC_NON_COMP: DLC non-compliance (transmit frame with DLC > 8, CAN FD only)

    // For clarity in a textbook, it's often easier to present the conceptual fields first.
    // Let's describe the conceptual parts and then map them to the `twai_message_t` structure members including `flags`.

    uint32_t identifier;            /*!< 11-bit (standard) or 29-bit (extended) identifier. */
    uint8_t data_length_code;       /*!< Data Length Code (DLC) indicating 0 to 8 bytes of data. */
    uint32_t flags;                 /*!< Bit-field of message flags.
                                         - Use `TWAI_MSG_FLAG_EXTD` to indicate an extended frame (29-bit ID).
                                         - Use `TWAI_MSG_FLAG_RTR` to indicate a Remote Transmission Request (RTR) frame.
                                         - Other flags include `TWAI_MSG_FLAG_SS` (Single Shot) and `TWAI_MSG_FLAG_SELF` (Self Reception). */
    uint8_t data[TWAI_FRAME_MAX_DLC]; /*!< Actual data bytes (0 to 8). Content is irrelevant if `TWAI_MSG_FLAG_RTR` is set.
                                         `TWAI_FRAME_MAX_DLC` is typically 8 for Classical CAN. */

    // Deprecated members (kept for source compatibility, but `flags` should be used):
    // uint8_t extd:1;               /*!< Extended Frame Format (29bit ID). True if extended, false if standard. Use TWAI_MSG_FLAG_EXTD in flags instead. */
    // uint8_t rtr:1;                /*!< Remote Transmission Request. True if RTR, false if data frame. Use TWAI_MSG_FLAG_RTR in flags instead. */
} twai_message_t;

Let’s break down the key components:

  • identifier (uint32_t): This holds the CAN message identifier.
    • For Standard Frames, this is an 11-bit value (0x000 to 0x7FF).
    • For Extended Frames, this is a 29-bit value (0x00000000 to 0x1FFFFFFF).The nature of the identifier (standard or extended) is determined by the TWAI_MSG_FLAG_EXTD bit in the flags field.
  • data_length_code (uint8_t): The DLC specifies the number of data bytes in the data field.
    • Its value can range from 0 to 8 for Classical CAN.
    • Even if TWAI_MSG_FLAG_RTR is set (for a Remote Frame), the DLC should still be set to the expected number of bytes in the data frame that is being requested.
  • flags (uint32_t): This is a bit-field used to specify various properties of the message. The most important flags for basic transmission and reception are:
    • TWAI_MSG_FLAG_EXTD: If this flag is set, the message is treated as an Extended Frame (using a 29-bit identifier). If clear, it’s a Standard Frame (11-bit identifier).
    • TWAI_MSG_FLAG_RTR: If this flag is set, the message is a Remote Transmission Request (RTR) frame. An RTR frame is used by a node to request another node to send a data frame with a specific identifier. RTR frames have an identifier and a DLC, but no data payload. If clear, the message is a Data Frame.
    • TWAI_MSG_FLAG_SS (Single Shot, ESP32 Classic only): If set, the TWAI controller will attempt to transmit the message only once. If arbitration is lost or an error occurs, the message will not be automatically retransmitted. By default (flag not set), the controller will attempt retransmission until successful or until it enters an error state (like Bus-Off).
    • TWAI_MSG_FLAG_SELF (Self Reception, ESP32 Classic only): If set along with TWAI_MODE_SELF_TEST in general configuration, this specific message will be looped back for reception by the same node. If not set, even in TWAI_MODE_SELF_TEST, this particular message might not be self-received. This provides finer control over which messages are looped back during self-test. For other chips (S2, S3, C3 etc.), TWAI_MODE_SELF_TEST usually implies all transmitted messages are candidates for self-reception if filters allow.
  • data[TWAI_FRAME_MAX_DLC] (uint8_t array): This array holds the actual data payload of the CAN message. TWAI_FRAME_MAX_DLC is typically 8.
    • For Data Frames (TWAI_MSG_FLAG_RTR is clear), this array contains 0 to 8 bytes of data, as specified by data_length_code.
    • For Remote Frames (TWAI_MSG_FLAG_RTR is set), the content of this array is irrelevant for transmission, though the receiver will still see the DLC value sent by the RTR frame’s originator.

Example of Populating twai_message_t for a Standard Data Frame:

C
twai_message_t tx_message_std;
tx_message_std.identifier = 0x123;          // 11-bit Standard ID
tx_message_std.flags = 0;                   // Clear all flags initially (Data frame, Standard ID)
// tx_message_std.flags &= ~TWAI_MSG_FLAG_EXTD; // Explicitly ensure standard (optional if flags = 0)
// tx_message_std.flags &= ~TWAI_MSG_FLAG_RTR;  // Explicitly ensure data frame (optional if flags = 0)
tx_message_std.data_length_code = 4;
tx_message_std.data[0] = 0xDE;
tx_message_std.data[1] = 0xAD;
tx_message_std.data[2] = 0xBE;
tx_message_std.data[3] = 0xEF;

Example of Populating twai_message_t for an Extended Data Frame:

C
twai_message_t tx_message_ext;
tx_message_ext.identifier = 0x1ABCD123;     // 29-bit Extended ID
tx_message_ext.flags = TWAI_MSG_FLAG_EXTD;  // Set EXTD flag
// tx_message_ext.flags &= ~TWAI_MSG_FLAG_RTR; // Explicitly ensure data frame
tx_message_ext.data_length_code = 2;
tx_message_ext.data[0] = 0xAA;
tx_message_ext.data[1] = 0xBB;

Example of Populating twai_message_t for a Standard Remote Frame:

C
twai_message_t tx_message_rtr;
tx_message_rtr.identifier = 0x456;          // 11-bit Standard ID of the data frame being requested
tx_message_rtr.flags = TWAI_MSG_FLAG_RTR;   // Set RTR flag
// tx_message_rtr.flags &= ~TWAI_MSG_FLAG_EXTD; // Explicitly ensure standard
tx_message_rtr.data_length_code = 8;        // Requesting a data frame that should have 8 bytes
// tx_message_rtr.data field is not used for sending an RTR frame

2.2. Transmission Process (twai_transmit())

The twai_transmit() function is used to queue a message for transmission on the CAN bus.

C
esp_err_t twai_transmit(const twai_message_t *message, TickType_t ticks_to_wait);
  • message (const twai_message_t *): A pointer to the twai_message_t structure containing the message to be transmitted.
  • ticks_to_wait (TickType_t): The maximum number of FreeRTOS ticks to block if the transmit queue is full.
    • If ticks_to_wait is 0, the function will return immediately if the TX queue is full (ESP_ERR_TIMEOUT). This is non-blocking.
    • If ticks_to_wait is portMAX_DELAY, the function will block indefinitely until there is space in the TX queue.
    • For any other positive value, it will block for that duration.

Transmission Flow:

  1. The application prepares a twai_message_t structure.
  2. twai_transmit() is called.
  3. The TWAI driver attempts to place the message into its software transmit queue (configured by tx_queue_len in twai_general_config_t).
  4. If the TX queue has space, the message is added, and the function returns ESP_OK (unless ticks_to_wait caused an earlier timeout if the queue was initially full but cleared within the timeout).
  5. The TWAI hardware then takes messages from this queue (or its internal hardware buffer) and attempts to send them over the CAN bus according to CAN arbitration rules.
  6. If the TX queue is full and ticks_to_wait expires, the function returns ESP_ERR_TIMEOUT.
graph TB
    %% Mermaid Diagram for twai_transmit() Message Flow
    %% Styles
    classDef app fill:#FEF3C7,stroke:#D97706,color:#92400E; 
    classDef data fill:#E0E7FF,stroke:#4338CA,color:#3730A3; 
    classDef func fill:#DBEAFE,stroke:#2563EB,color:#1E40AF; 
    classDef queue fill:#FCE7F3,stroke:#DB2777,color:#9D174D; 
    classDef hardware fill:#D1FAE5,stroke:#059669,color:#065F46; 
    classDef bus fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; 
    classDef decision fill:#FEE2E2,stroke:#DC2626,color:#991B1B; 

    A["Application Logic"]:::app
    A -- "Prepares" --> B["<span style='font-family:monospace;font-size:0.9em;'>twai_message_t</span><br>(ID, DLC, Flags, Data)"]:::data
    B -- "Passes to" --> C["<span style='font-family:monospace;font-size:0.9em;'>twai_transmit(&message, ticks_to_wait)</span>"]:::func
    
    C --> D{"TX Queue Full?"}:::decision
    D -- "No (Space Available)" --> E["TWAI Driver TX Queue<br>(Software Buffer)"]:::queue
    D -- "Yes" --> D_Block{"Block for <span style='font-family:monospace;font-size:0.8em;'>ticks_to_wait</span>?"}:::decision
    
    D_Block -- "Yes (Timeout > 0)" --> D_Wait["Wait for Space"]:::func
    D_Wait --> D_Space{"Space Available<br>within Timeout?"}:::decision
    D_Space -- "Yes" --> E
    D_Space -- "No (Timeout Expired)" --> RetTimeout["Return <span style='font-family:monospace;font-size:0.8em;'>ESP_ERR_TIMEOUT</span>"]:::error
    
    D_Block -- "No (Timeout = 0)" --> RetTimeout

    E -- "Dequeues Message" --> F["TWAI Hardware TX Buffer"]:::hardware
    F -- "Handles Arbitration & Transmission" --> G["CAN Transceiver"]:::hardware
    G -- "Drives Signals" --> H["CAN Bus (CAN_H, CAN_L)"]:::bus

    C -.-> RetOK["Return <span style='font-family:monospace;font-size:0.8em;'>ESP_OK</span> (on successful enqueuing)"]:::hardware
    
    subgraph "TWAI Driver & Hardware"
      direction LR
      E
      F
    end

    style RetOK fill:#D1FAE5, stroke:#059669
    style RetTimeout fill:#FEE2E2, stroke:#DC2626

Return Values for twai_transmit():

Return Value Meaning Common Cause / Action
ESP_OK Success Message was successfully enqueued into the transmit queue.
ESP_ERR_INVALID_ARG Invalid Argument Input pointer message is NULL, or DLC > 8 for Classical CAN, or other invalid message parameters. Check message structure.
ESP_ERR_TIMEOUT Timeout Transmit queue was full, and ticks_to_wait expired before space became available. Retry later, increase queue size, or reduce transmission rate.
ESP_ERR_INVALID_STATE Invalid State TWAI driver is not in a state that can transmit (e.g., not started via twai_start(), or in Bus-Off state).
ESP_FAIL Generic Failure Could indicate an uninitialized transmit queue or other underlying driver issue. Check driver installation.
ESP_ERR_NOT_SUPPORTED Operation Not Supported The combination of flags in the twai_message_t is not supported (e.g., trying to use CAN FD specific flags when controller is not in CAN FD mode).

Tip: Always check the return value of twai_transmit() to ensure the message was successfully queued. If it returns ESP_ERR_TIMEOUT, your application might need to retry later or implement a strategy to handle a full TX buffer (e.g., increase queue size, slow down transmission rate).

2.3. Reception Process (twai_receive())

The twai_receive() function is used to retrieve a received message from the TWAI driver’s receive queue.

C
esp_err_t twai_receive(twai_message_t *message, TickType_t ticks_to_wait);
  • message (twai_message_t *): A pointer to a twai_message_t structure where the received message will be stored.
  • ticks_to_wait (TickType_t): The maximum number of FreeRTOS ticks to block if the receive queue is empty.
    • If ticks_to_wait is 0, the function will return immediately if the RX queue is empty (ESP_ERR_TIMEOUT). This is non-blocking.
    • If ticks_to_wait is portMAX_DELAY, the function will block indefinitely until a message is received.
    • For any other positive value, it will block for that duration.

Reception Flow:

  1. The TWAI hardware controller receives a valid message from the CAN bus (after passing acceptance filtering).
  2. The message is placed into the TWAI driver’s software receive queue (configured by rx_queue_len in twai_general_config_t).
  3. The application calls twai_receive().
  4. If the RX queue contains a message, the oldest message is copied into the user-provided twai_message_t structure, removed from the queue, and the function returns ESP_OK.
  5. If the RX queue is empty and ticks_to_wait expires, the function returns ESP_ERR_TIMEOUT.
graph TB
    %% Mermaid Diagram for twai_receive() Message Flow
    %% Styles
    classDef bus fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; 
    classDef hardware fill:#D1FAE5,stroke:#059669,color:#065F46; 
    classDef filter fill:#A7F3D0,stroke:#047857,color:#064E3B; 
    classDef queue fill:#FCE7F3,stroke:#DB2777,color:#9D174D; 
    classDef func fill:#DBEAFE,stroke:#2563EB,color:#1E40AF; 
    classDef data fill:#E0E7FF,stroke:#4338CA,color:#3730A3; 
    classDef app fill:#FEF3C7,stroke:#D97706,color:#92400E; 
    classDef decision fill:#FEE2E2,stroke:#DC2626,color:#991B1B; 

    A["CAN Bus (CAN_H, CAN_L)"]:::bus
    A -- "Signals to" --> B["CAN Transceiver"]:::hardware
    B -- "Digital Data to" --> C["TWAI Hardware Controller"]:::hardware
    C --> F1["Hardware Acceptance Filter"]:::filter
    F1 -- "Message Matches Filter?" --> F2{Matches}:::decision
    
    F2 -- "Yes" --> D["TWAI Driver RX Queue<br>(Software Buffer)"]:::queue
    F2 -- "No" --> F3["Message Discarded"]:::error
    
    D -- "Application Calls" --> E["<span style='font-family:monospace;font-size:0.9em;'>twai_receive(&message, ticks_to_wait)</span>"]:::func
    
    E --> F{"RX Queue Empty?"}:::decision
    F -- "No (Message Available)" --> G["Copy Message to User's <span style='font-family:monospace;font-size:0.8em;'>twai_message_t</span>"]:::data
    G -- "Returns <span style='font-family:monospace;font-size:0.8em;'>ESP_OK</span>" --> H["Application Logic<br>(Processes Received Message)"]:::app
    
    F -- "Yes" --> F_Block{"Block for <span style='font-family:monospace;font-size:0.8em;'>ticks_to_wait</span>?"}:::decision
    F_Block -- "Yes (Timeout > 0)" --> F_Wait["Wait for Message"]:::func
    F_Wait --> F_MsgAvail{"Message Arrives<br>within Timeout?"}:::decision
    F_MsgAvail -- "Yes" --> G
    F_MsgAvail -- "No (Timeout Expired)" --> RetTimeout["Return <span style='font-family:monospace;font-size:0.8em;'>ESP_ERR_TIMEOUT</span>"]:::error
    
    F_Block -- "No (Timeout = 0)" --> RetTimeout

    subgraph "TWAI Driver & Hardware"
      direction LR
      C
      F1
      D
    end
    

    style RetTimeout fill:#FEE2E2, stroke:#DC2626

Interpreting Received Messages:

After twai_receive() returns ESP_OK, the message structure will be populated:

  • message->identifier will contain the 11-bit or 29-bit ID.
  • message->flags will indicate if it was an Extended Frame (TWAI_MSG_FLAG_EXTD) or an RTR Frame (TWAI_MSG_FLAG_RTR).
  • message->data_length_code will contain the DLC.
  • message->data[] will contain the data bytes if it was a Data Frame.

Return Values for twai_receive():

Return Value Meaning Common Cause / Action
ESP_OK Success A message was successfully received from the RX queue and copied to the user’s buffer.
ESP_ERR_INVALID_ARG Invalid Argument Input pointer message is NULL. Check pointer validity.
ESP_ERR_TIMEOUT Timeout Receive queue was empty, and ticks_to_wait expired before a message arrived. This is normal if no messages are pending.
ESP_ERR_INVALID_STATE Invalid State TWAI driver is not in a state that can receive (e.g., not started via twai_start()).

2.4. Data Frames vs. Remote Frames

  • Data Frames: Carry actual data. TWAI_MSG_FLAG_RTR in flags is 0. The data array and data_length_code are significant.
  • Remote Frames (RTR): Used to request a Data Frame with a specific ID. TWAI_MSG_FLAG_RTR in flags is 1.
    • When transmitting an RTR frame, the data array is ignored by the TWAI hardware, but data_length_code should be set to the DLC of the expected data frame response.
    • When receiving an RTR frame, message->flags will have TWAI_MSG_FLAG_RTR set. message->identifier will be the ID of the requested data, and message->data_length_code will be the DLC specified in the RTR frame. The message->data array will not contain meaningful data from the bus for an RTR frame.
    • Responding to RTR: The ESP32’s TWAI peripheral itself does not automatically respond to RTR frames. If your node needs to respond to an RTR, your application software must:
      1. Receive the RTR frame.
      2. Check its identifier and DLC.
      3. Prepare a corresponding Data Frame with the same identifier and the requested data.
      4. Transmit this Data Frame.
Feature Data Frame Remote Frame (RTR)
Purpose To transmit actual data. To request another node to transmit a Data Frame with a specific ID.
TWAI_MSG_FLAG_RTR in flags Must be 0 (clear). Must be 1 (set).
identifier field Identifier of this data message. Identifier of the Data Frame being requested.
data_length_code (DLC) field Specifies the number of bytes in its own data field (0-8). Specifies the expected DLC of the Data Frame being requested (0-8).
data[] field (Payload) Contains the actual data bytes being transmitted. Irrelevant for transmission. The physical frame on the bus has no data field.
Automatic Response by TWAI Peripheral N/A (It is the data) No automatic response by the ESP32 TWAI peripheral. Application logic must detect a received RTR and transmit the corresponding Data Frame.

3. Practical Examples

These examples assume you have already configured and started the TWAI driver as shown in Chapter 152. For simplicity, GPIO pins and basic configuration will be hardcoded or briefly mentioned; refer to Chapter 152 for full setup details.

Example 1: Transmitting and Receiving a Standard Data Frame (Loopback)

This example uses TWAI_MODE_SELF_TEST to transmit a message and receive it on the same ESP32.

Prerequisites:

  • ESP-IDF v5.x project set up.
  • TWAI driver configured for TWAI_MODE_SELF_TEST and started. (e.g., 125kbps, accept-all filter).

Code Snippet:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/twai.h"
#include "esp_log.h"

static const char *TAG = "TWAI_TX_RX_EXAMPLE";

// Configuration (ensure these are set via Kconfig or defines)
#define TWAI_TX_GPIO_NUM CONFIG_EXAMPLE_TWAI_TX_GPIO // e.g., 21
#define TWAI_RX_GPIO_NUM CONFIG_EXAMPLE_TWAI_RX_GPIO // e.g., 22

// KConfig (ensure these are in your project's Kconfig.projbuild or sdkconfig.defaults)
// CONFIG_EXAMPLE_TWAI_TX_GPIO=21
// CONFIG_EXAMPLE_TWAI_RX_GPIO=22

static void twai_transmit_receive_task(void *pvParameters)
{
    // 1. Prepare message for transmission
    twai_message_t tx_msg;
    tx_msg.identifier = 0x1A1;              // Standard 11-bit ID
    tx_msg.flags = 0;                       // Standard Data Frame
    tx_msg.data_length_code = 4;
    tx_msg.data[0] = 'H';
    tx_msg.data[1] = 'E';
    tx_msg.data[2] = 'L';
    tx_msg.data[3] = 'O';

    ESP_LOGI(TAG, "Prepared TX message: ID=0x%03lX, DLC=%d, Data='%c%c%c%c'",
             tx_msg.identifier, tx_msg.data_length_code,
             tx_msg.data[0], tx_msg.data[1], tx_msg.data[2], tx_msg.data[3]);

    // 2. Transmit the message
    esp_err_t tx_result = twai_transmit(&tx_msg, pdMS_TO_TICKS(1000));
    if (tx_result == ESP_OK) {
        ESP_LOGI(TAG, "Message successfully queued for transmission.");
    } else {
        ESP_LOGE(TAG, "Failed to queue message for transmission: %s", esp_err_to_name(tx_result));
        vTaskDelete(NULL); // Exit task on failure
        return;
    }

    // 3. Attempt to receive the message (due to SELF_TEST mode)
    twai_message_t rx_msg;
    ESP_LOGI(TAG, "Attempting to receive message...");
    esp_err_t rx_result = twai_receive(&rx_msg, pdMS_TO_TICKS(2000)); // Wait up to 2 seconds

    if (rx_result == ESP_OK) {
        ESP_LOGI(TAG, "Message received successfully!");
        ESP_LOGI(TAG, "RX MSG ID: 0x%03lX, Flags: 0x%02lX, DLC: %d",
                 rx_msg.identifier, rx_msg.flags, rx_msg.data_length_code);
        
        printf("RX Data: ");
        for (int i = 0; i < rx_msg.data_length_code; i++) {
            printf("0x%02X (%c) ", rx_msg.data[i], rx_msg.data[i]);
        }
        printf("\n");

        // Verification
        if (rx_msg.identifier == tx_msg.identifier &&
            rx_msg.data_length_code == tx_msg.data_length_code &&
            rx_msg.data[0] == tx_msg.data[0] && rx_msg.data[1] == tx_msg.data[1] &&
            rx_msg.data[2] == tx_msg.data[2] && rx_msg.data[3] == tx_msg.data[3]) {
            ESP_LOGI(TAG, "Loopback successful: Transmitted and received messages match.");
        } else {
            ESP_LOGW(TAG, "Loopback mismatch or unexpected message.");
        }
    } else if (rx_result == ESP_ERR_TIMEOUT) {
        ESP_LOGW(TAG, "Timeout waiting for message reception. Loopback might have failed.");
    } else {
        ESP_LOGE(TAG, "Failed to receive message: %s", esp_err_to_name(rx_result));
    }

    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_LOGI(TAG, "TWAI Transmit/Receive Example (Loopback)");

    // Configure TWAI (Self Test Mode, 125kbps, Accept All)
    twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO_NUM, TWAI_RX_GPIO_NUM, TWAI_MODE_SELF_TEST);
    // For ESP32 classic, you might want to add TWAI_MSG_FLAG_SELF to the tx_msg.flags for reliable self-reception in SELF_TEST mode.
    // For other chips, SELF_TEST mode usually ensures loopback without this message flag.
    // Let's assume general self-test behavior for now.
    g_config.alerts_enabled = TWAI_ALERT_NONE;
    g_config.tx_queue_len = 5;
    g_config.rx_queue_len = 5;

    twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
    twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

    ESP_LOGI(TAG, "Installing TWAI driver...");
    if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
        ESP_LOGE(TAG, "Failed to install TWAI driver.");
        return;
    }
    ESP_LOGI(TAG, "Starting TWAI driver...");
    if (twai_start() != ESP_OK) {
        ESP_LOGE(TAG, "Failed to start TWAI driver.");
        twai_driver_uninstall();
        return;
    }
    ESP_LOGI(TAG, "TWAI driver started in SELF_TEST mode.");

    xTaskCreate(twai_transmit_receive_task, "twai_tx_rx_task", 4096, NULL, 5, NULL);

    // Let the task run. In a real app, you might wait for it or have other logic.
    // For this example, app_main will exit, but the task continues.
}

Build Instructions:

  1. Ensure KConfig options CONFIG_EXAMPLE_TWAI_TX_GPIO and CONFIG_EXAMPLE_TWAI_RX_GPIO are defined (e.g., in sdkconfig.defaults or via menuconfig).
  2. Build using idf.py build.

Run/Flash/Observe Steps:

  1. Flash: idf.py -p (PORT) flash.
  2. Monitor: idf.py -p (PORT) monitor.
  3. Observe: You should see logs indicating the message was prepared, transmitted, and then received, with matching content.

Example 2: Transmitting an Extended Data Frame

Modify the twai_transmit_receive_task from Example 1:

C
// Inside twai_transmit_receive_task:
    twai_message_t tx_msg;
    tx_msg.identifier = 0x1FFFFFFA;         // Extended 29-bit ID
    tx_msg.flags = TWAI_MSG_FLAG_EXTD;      // Set EXTD flag for Extended Frame
    tx_msg.data_length_code = 2;
    tx_msg.data[0] = 0xBE;
    tx_msg.data[1] = 0xEF;

    ESP_LOGI(TAG, "Prepared TX message: ID=0x%08lX (Extended), DLC=%d, Data=0x%02X 0x%02X",
             tx_msg.identifier, tx_msg.data_length_code,
             tx_msg.data[0], tx_msg.data[1]);
    
    // ... rest of transmit and receive logic ...

    // When logging received message:
    if (rx_result == ESP_OK) {
        ESP_LOGI(TAG, "Message received successfully!");
        const char* frame_type = (rx_msg.flags & TWAI_MSG_FLAG_EXTD) ? "Extended" : "Standard";
        const char* rtr_type = (rx_msg.flags & TWAI_MSG_FLAG_RTR) ? "RTR" : "Data";
        ESP_LOGI(TAG, "RX MSG ID: 0x%0*lX (%s %s), Flags: 0x%02lX, DLC: %d",
                 (rx_msg.flags & TWAI_MSG_FLAG_EXTD) ? 8 : 3, // Print 8 hex chars for extd, 3 for std
                 rx_msg.identifier, frame_type, rtr_type, rx_msg.flags, rx_msg.data_length_code);
        // ... print data and verify ...
    }
// ...

Rebuild and run. The logs should now reflect the extended ID.

Example 3: Transmitting a Remote Frame (RTR)

This example shows how to send an RTR frame. Receiving and automatically responding to it requires application logic.

C
// Inside a task, after TWAI driver is started:
    twai_message_t rtr_msg;
    rtr_msg.identifier = 0x700;             // ID of the data frame being requested
    rtr_msg.flags = TWAI_MSG_FLAG_RTR;      // Set RTR flag
    // rtr_msg.flags &= ~TWAI_MSG_FLAG_EXTD; // Ensure standard ID if 0x700 is standard
    rtr_msg.data_length_code = 8;           // Requesting 8 bytes of data

    ESP_LOGI(TAG, "Preparing to transmit RTR frame for ID 0x%03lX, DLC=%d",
             rtr_msg.identifier, rtr_msg.data_length_code);

    if (twai_transmit(&rtr_msg, pdMS_TO_TICKS(1000)) == ESP_OK) {
        ESP_LOGI(TAG, "RTR frame successfully queued for transmission.");
    } else {
        ESP_LOGE(TAG, "Failed to queue RTR frame.");
    }

    // On the receiving side (another node, or loopback if configured):
    // twai_message_t received_rtr;
    // if (twai_receive(&received_rtr, pdMS_TO_TICKS(1000)) == ESP_OK) {
    //     if (received_rtr.flags & TWAI_MSG_FLAG_RTR) {
    //         ESP_LOGI(TAG, "Received an RTR frame for ID 0x%0*lX, requesting DLC %d",
    //                  (received_rtr.flags & TWAI_MSG_FLAG_EXTD) ? 8 : 3,
    //                  received_rtr.identifier, received_rtr.data_length_code);
    //         // Application logic here to find and send the corresponding data frame
    //     }
    // }

If you run this in TWAI_MODE_SELF_TEST, you should be able to receive this RTR frame. Your application would then need to check the TWAI_MSG_FLAG_RTR in the received message’s flags field.

Example 4: Non-Blocking Receive with Polling

This demonstrates using twai_receive() with a zero timeout for polling.

C
// Inside a task that runs periodically or in a loop:
static void polling_receive_task(void *pvParameters) {
    twai_message_t rx_message;
    esp_err_t result;
    int poll_count = 0;

    while(1) {
        result = twai_receive(&rx_message, 0); // 0 ticks_to_wait for non-blocking
        if (result == ESP_OK) {
            ESP_LOGI(TAG, "Polled and received message: ID 0x%lX", rx_message.identifier);
            // Process message...
            poll_count = 0; // Reset counter
        } else if (result == ESP_ERR_TIMEOUT) {
            // Queue is empty, no message received
            if ((poll_count % 5000) == 0) { // Log every ~5 seconds if polling at 1ms
                 ESP_LOGI(TAG, "Polling... RX queue empty.");
            }
            poll_count++;
        } else {
            ESP_LOGE(TAG, "Error receiving message: %s", esp_err_to_name(result));
            // Handle error, maybe break loop or re-init driver
        }
        vTaskDelay(pdMS_TO_TICKS(1)); // Poll at a reasonable rate, e.g., every 1ms
                                      // Adjust delay based on application needs.
                                      // Too frequent polling without messages can waste CPU.
    }
}
// Remember to create this task:
// xTaskCreate(polling_receive_task, "polling_rx_task", 4096, NULL, 5, NULL);

Warning: Continuous polling with a very short or zero delay can consume significant CPU resources if messages are infrequent. Consider using blocking calls with a timeout, or alerts (TWAI_ALERT_RX_DATA), for more efficient message reception in many applications.

4. Variant Notes

  • API Consistency: The core twai_message_t structure and the functions twai_transmit() and twai_receive() are consistent for Classical CAN operations across all ESP32 variants that support TWAI (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2).
  • CAN FD: For variants supporting CAN FD (ESP32-S3, ESP32-C6, ESP32-H2, and potentially newer ESP32-C3 revisions), sending and receiving CAN FD frames (with larger DLC and Bit Rate Switching) involves different message structures (e.g., twai_can_fd_frame_t or specific flags/APIs) and timing configurations. This chapter focuses on Classical CAN. CAN FD specifics will be detailed in Chapter 157.
  • TWAI_MSG_FLAG_SS and TWAI_MSG_FLAG_SELF: These flags are documented as being specific to the original ESP32’s TWAI controller implementation. While they might work on other variants, their behavior or necessity could differ. For instance, TWAI_MODE_SELF_TEST on newer chips generally ensures loopback without needing TWAI_MSG_FLAG_SELF on each message. Always test thoroughly if relying on these flags on variants other than the original ESP32.
  • Hardware TX/RX Buffers: The underlying hardware might have small FIFO buffers for transmission and reception. The tx_queue_len and rx_queue_len in the general configuration define software queues that sit on top of these, providing greater buffering capacity. The size of these hardware FIFOs is generally not a direct concern for the application developer using the ESP-IDF driver, as the software queues abstract this.

5. Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect flags Field for Frame Type Message transmitted as standard when extended was intended (or vice-versa); message sent as data frame when RTR was intended (or vice-versa); unexpected behavior on the bus.
  • Verify Flags: For standard data frames, flags can be 0.
  • Set TWAI_MSG_FLAG_EXTD in flags for 29-bit extended IDs.
  • Set TWAI_MSG_FLAG_RTR in flags for Remote Transmission Request frames.
  • Clear flags appropriately if they were set for a previous message. Initialize flags = 0; before setting specific bits.
Mismatched data_length_code (DLC) Transmitted data is truncated or padded with garbage; receiver gets unexpected data length; errors if DLC > 8 for Classical CAN.
  • Accurate DLC: Ensure data_length_code matches the actual number of valid bytes (0-8) in the data[] array for data frames.
  • RTR DLC: For RTR frames, set DLC to the expected length of the data frame being requested.
  • Network Agreement: All nodes processing a specific message ID should generally agree on its expected DLC.
Ignoring Return Values of twai_transmit() / twai_receive() Application assumes success when an error occurred (e.g., TX queue full, RX queue empty, driver not started). Leads to lost messages or unresponsive behavior.
  • Always Check: Check the esp_err_t return code of these functions.
  • Handle ESP_OK: Indicates success.
  • Handle ESP_ERR_TIMEOUT: Common; TX queue was full or RX queue was empty within the specified ticks_to_wait. Retry, delay, or adjust queue sizes.
  • Handle Other Errors: Investigate causes for ESP_ERR_INVALID_STATE, ESP_ERR_INVALID_ARG, etc.
TX/RX Queue Overflows/Underflows twai_transmit() returns ESP_ERR_TIMEOUT frequently (TX queue full). twai_receive() always times out or messages are lost (RX queue full and new messages dropped by hardware/driver).
  • TX Flow Control: If transmitting rapidly, handle ESP_ERR_TIMEOUT by retrying after a delay, or buffer messages at application level. Consider increasing tx_queue_len.
  • RX Processing Speed: Ensure your application processes messages from the RX queue faster than they arrive on average.
  • Increase RX Queue: If bursts of messages are expected, increase rx_queue_len (within RAM limits).
  • Alerts: Use TWAI_ALERT_RX_QUEUE_FULL or TWAI_ALERT_RX_DATA to manage RX flow more efficiently.
Using Blocking Calls in Time-Critical Tasks A task using twai_transmit() or twai_receive() with portMAX_DELAY becomes unresponsive and misses other deadlines.
  • Use Timeouts: Provide a specific ticks_to_wait value instead of portMAX_DELAY.
  • Non-Blocking: Set ticks_to_wait = 0 for non-blocking calls and handle ESP_ERR_TIMEOUT.
  • Separate Tasks: Consider handling TWAI communication in a dedicated task with appropriate priority, separate from time-critical operations.
  • Event-Driven: Use FreeRTOS queues or event groups triggered by TWAI alerts for more responsive designs.

6. Exercises

  1. Sequential Message Transmission:
    • Write an ESP32 program that transmits a sequence of 5 standard data frames. Each frame should have a unique identifier (e.g., 0x101, 0x102, …, 0x105) and a different 1-byte data payload (e.g., 0x01, 0x02, …, 0x05).
    • Use TWAI_MODE_SELF_TEST and verify that all 5 messages are received correctly.
  2. ID-Specific Receiver:
    • Modify the receiver part of Example 1 to only process and print messages that have a specific identifier (e.g., 0x1A1). Messages with other IDs should be received but ignored (or logged differently).
  3. Ping-Pong Application (Conceptual or Two ESP32s):
    • If you have two ESP32s with CAN transceivers:
      • Node A: Sends a “PING” message (e.g., ID 0x200, data “PING”).
      • Node B: Receives ID 0x200. If data is “PING”, it sends a “PONG” message (e.g., ID 0x201, data “PONG”).
      • Node A: Receives ID 0x201. If data is “PONG”, logs success.
    • Conceptual for one ESP32 (Self-Test): Describe the logic flow for Node A and Node B as if they were separate. How would you manage state to know whether to send a PING or expect a PONG?
  4. TX Queue Behavior Experiment:
    • Configure the TWAI driver with a small tx_queue_len (e.g., 1 or 2).
    • In a loop, attempt to transmit 10 messages rapidly using twai_transmit() with ticks_to_wait = 0.
    • Log the return value of each twai_transmit() call. Observe how many succeed and how many return ESP_ERR_TIMEOUT.
    • What happens if you add a small delay (e.g., vTaskDelay(pdMS_TO_TICKS(10))) inside the loop?
  5. RTR Request and Response Simulation (Self-Test):
    • Transmit an RTR frame requesting data for ID 0x350 with DLC 3.
    • In the receive logic, if an RTR frame for ID 0x350 is detected:
      • Prepare a data frame with ID 0x350, DLC 3, and some sample data (e.g., {0x01, 0x02, 0x03}).
      • Transmit this data frame.
    • Verify that both the RTR frame and the subsequent data frame are “received” in self-test mode.
sequenceDiagram
    actor NodeA
    actor NodeB

    participant CANBus
    
    NodeA->>+CANBus: Transmit "PING" (ID: 0x200, Data: "PING")
    Note over NodeA, NodeB: Both nodes monitor CAN Bus

    CANBus-->>-NodeB: Receives Message (ID: 0x200)
    NodeB->>NodeB: Check ID == 0x200?
    NodeB->>NodeB: Check Data == "PING"?
    alt If ID & Data match
        NodeB->>+CANBus: Transmit "PONG" (ID: 0x201, Data: "PONG")
    else Else (ID/Data mismatch)
        NodeB->>NodeB: Ignore message or Log error
    end

    CANBus-->>-NodeA: Receives Message (ID: 0x201)
    NodeA->>NodeA: Check ID == 0x201?
    NodeA->>NodeA: Check Data == "PONG"?
    alt If ID & Data match
        NodeA->>NodeA: Log "Ping-Pong Success!"
    else Else (ID/Data mismatch)
        NodeA->>NodeA: Ignore message or Log error
    end

7. Summary

  • The twai_message_t structure is central to CAN communication, defining the identifier, data_length_code, flags (for EXTD, RTR, etc.), and data payload.
  • twai_transmit(&message, ticks_to_wait) queues a message for transmission. Check its return value for success (ESP_OK) or errors like ESP_ERR_TIMEOUT.
  • twai_receive(&message, ticks_to_wait) retrieves a message from the RX queue. Check its return value.
  • The flags member of twai_message_t is used to specify frame type:
    • TWAI_MSG_FLAG_EXTD: For 29-bit extended identifiers.
    • TWAI_MSG_FLAG_RTR: For Remote Transmission Request frames.
  • Application logic is required to detect received RTR frames and transmit the corresponding data frames.
  • Blocking (portMAX_DELAY), non-blocking (0), or timed waits can be used for ticks_to_wait.
  • Proper management of TX/RX queues and error checking are crucial for robust CAN applications.
  • The fundamental APIs for Classical CAN message handling are consistent across ESP32 variants supporting TWAI.

8. Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top