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:
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 thedata
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 withTWAI_MODE_SELF_TEST
in general configuration, this specific message will be looped back for reception by the same node. If not set, even inTWAI_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 bydata_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.
- For Data Frames (
Example of Populating twai_message_t
for a Standard Data Frame:
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:
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:
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.
esp_err_t twai_transmit(const twai_message_t *message, TickType_t ticks_to_wait);
message
(const twai_message_t *
): A pointer to thetwai_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
is0
, the function will return immediately if the TX queue is full (ESP_ERR_TIMEOUT
). This is non-blocking. - If
ticks_to_wait
isportMAX_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.
- If
Transmission Flow:
- The application prepares a
twai_message_t
structure. twai_transmit()
is called.- The TWAI driver attempts to place the message into its software transmit queue (configured by
tx_queue_len
intwai_general_config_t
). - If the TX queue has space, the message is added, and the function returns
ESP_OK
(unlessticks_to_wait
caused an earlier timeout if the queue was initially full but cleared within the timeout). - 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.
- If the TX queue is full and
ticks_to_wait
expires, the function returnsESP_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 returnsESP_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.
esp_err_t twai_receive(twai_message_t *message, TickType_t ticks_to_wait);
message
(twai_message_t *
): A pointer to atwai_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
is0
, the function will return immediately if the RX queue is empty (ESP_ERR_TIMEOUT
). This is non-blocking. - If
ticks_to_wait
isportMAX_DELAY
, the function will block indefinitely until a message is received. - For any other positive value, it will block for that duration.
- If
Reception Flow:
- The TWAI hardware controller receives a valid message from the CAN bus (after passing acceptance filtering).
- The message is placed into the TWAI driver’s software receive queue (configured by
rx_queue_len
intwai_general_config_t
). - The application calls
twai_receive()
. - 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 returnsESP_OK
. - If the RX queue is empty and
ticks_to_wait
expires, the function returnsESP_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
inflags
is 0. Thedata
array anddata_length_code
are significant. - Remote Frames (RTR): Used to request a Data Frame with a specific ID.
TWAI_MSG_FLAG_RTR
inflags
is 1.- When transmitting an RTR frame, the
data
array is ignored by the TWAI hardware, butdata_length_code
should be set to the DLC of the expected data frame response. - When receiving an RTR frame,
message->flags
will haveTWAI_MSG_FLAG_RTR
set.message->identifier
will be the ID of the requested data, andmessage->data_length_code
will be the DLC specified in the RTR frame. Themessage->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:
- Receive the RTR frame.
- Check its identifier and DLC.
- Prepare a corresponding Data Frame with the same identifier and the requested data.
- Transmit this Data Frame.
- When transmitting an RTR frame, the
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:
#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:
- Ensure KConfig options
CONFIG_EXAMPLE_TWAI_TX_GPIO
andCONFIG_EXAMPLE_TWAI_RX_GPIO
are defined (e.g., insdkconfig.defaults
or viamenuconfig
). - Build using
idf.py build
.
Run/Flash/Observe Steps:
- Flash:
idf.py -p (PORT) flash
. - Monitor:
idf.py -p (PORT) monitor
. - 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:
// 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.
// 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.
// 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 functionstwai_transmit()
andtwai_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
andTWAI_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 needingTWAI_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
andrx_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. |
|
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. |
|
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. |
|
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). |
|
Using Blocking Calls in Time-Critical Tasks | A task using twai_transmit() or twai_receive() with portMAX_DELAY becomes unresponsive and misses other deadlines. |
|
6. Exercises
- 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.
- 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).
- Modify the receiver part of Example 1 to only process and print messages that have a specific identifier (e.g.,
- 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?
- If you have two ESP32s with CAN transceivers:
- 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()
withticks_to_wait = 0
. - Log the return value of each
twai_transmit()
call. Observe how many succeed and how many returnESP_ERR_TIMEOUT
. - What happens if you add a small delay (e.g.,
vTaskDelay(pdMS_TO_TICKS(10))
) inside the loop?
- Configure the TWAI driver with a small
- RTR Request and Response Simulation (Self-Test):
- Transmit an RTR frame requesting data for ID
0x350
with DLC3
. - In the receive logic, if an RTR frame for ID
0x350
is detected:- Prepare a data frame with ID
0x350
, DLC3
, and some sample data (e.g.,{0x01, 0x02, 0x03}
). - Transmit this data frame.
- Prepare a data frame with ID
- Verify that both the RTR frame and the subsequent data frame are “received” in self-test mode.
- Transmit an RTR frame requesting data for ID
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 theidentifier
,data_length_code
,flags
(for EXTD, RTR, etc.), anddata
payload. twai_transmit(&message, ticks_to_wait)
queues a message for transmission. Check its return value for success (ESP_OK
) or errors likeESP_ERR_TIMEOUT
.twai_receive(&message, ticks_to_wait)
retrieves a message from the RX queue. Check its return value.- The
flags
member oftwai_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 forticks_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
- ESP-IDF TWAI API Reference:
- TWAI Driver Functions (Covers
twai_transmit
,twai_receive
, etc.) - TWAI Data Structures (Details on
twai_message_t
and flags likeTWAI_MSG_FLAG_EXTD
,TWAI_MSG_FLAG_RTR
).
- TWAI Driver Functions (Covers
