Chapter 154: CAN Message Filtering and Acceptance
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and importance of acceptance filtering in CAN networks.
- Explain how the TWAI hardware acceptance filter works on ESP32 devices.
- Correctly configure the
twai_filter_config_t
structure, includingacceptance_code
andacceptance_mask
. - Implement filters to accept specific standard (11-bit) CAN message identifiers.
- Implement filters to accept specific extended (29-bit) CAN message identifiers.
- Understand how to create masks to accept a range of identifiers or IDs with common bit patterns.
- Differentiate between single and dual filter modes (and their applicability).
- Apply filtering configurations when installing the TWAI driver.
1. Introduction
In the previous chapter, you learned how to transmit and receive CAN frames. In many real-world CAN networks, especially in automotive systems or industrial environments, the bus can be very busy with numerous messages from various nodes. A typical Electronic Control Unit (ECU) or microcontroller node is usually interested in only a subset of these messages. Receiving and processing every single message on the bus would consume significant CPU resources and potentially overwhelm the application.
This is where acceptance filtering comes into play. The TWAI peripheral on ESP32 devices includes a hardware-based acceptance filter. This filter allows you to define criteria (based on the message identifier) so that the TWAI controller automatically screens incoming messages. Only messages that match the filter criteria are passed to the software receive queue and subsequently to your application. Messages that do not match are silently discarded by the hardware, saving valuable processing time.
This chapter will guide you through the theory and practical implementation of configuring the TWAI acceptance filter using the ESP-IDF. Mastering message filtering is key to building efficient and responsive CAN applications.
2. Theory
The TWAI controller’s acceptance filter works by comparing the identifier (and potentially other parts like RTR bit or data bytes, depending on mode) of an incoming CAN message against a configured Acceptance Code (ACR) and an Acceptance Mask (AMR).
2.1. The twai_filter_config_t
Structure
As briefly introduced in Chapter 152, the filter is configured using the twai_filter_config_t
structure when installing the TWAI driver:
typedef struct {
uint32_t acceptance_code; /**< 32-bit acceptance code */
uint32_t acceptance_mask; /**< 32-bit acceptance mask */
bool single_filter_mode; /**< Use single filter mode (see TRM for details on hardware behavior) */
} twai_filter_config_t;
acceptance_code
(uint32_t
): This 32-bit value specifies the pattern that incoming message identifiers (or parts of the message) should match.acceptance_mask
(uint32_t
): This 32-bit value determines which bits in theacceptance_code
are relevant for comparison and which bits are “don’t care.”single_filter_mode
(bool
):- If
true
: The filter operates in single filter mode. This is the most common mode and is available on all ESP32 variants with TWAI. It generally uses one set of 32-bit code and mask registers to filter messages. - If
false
: (Primarily relevant for ESP32 classic) The filter operates in dual filter mode, offering more complex filtering options, often by splitting the filter hardware into smaller, independent filters. This mode is more specialized. For most applications and for cross-variant compatibility,single_filter_mode = true
is recommended and will be the focus of this chapter.
- If
2.2. Acceptance Code and Acceptance Mask Logic
The ESP32 TWAI hardware (and thus the ESP-IDF driver’s interpretation for it) uses the following logic for each bit position i
in the filter:
A message is accepted if, for every bit position i being compared:
(Incoming_Message_Bit_i == Acceptance_Code_Bit_i) OR (Acceptance_Mask_Bit_i == 1)
This can be rephrased as:
- If
Acceptance_Mask_Bit_i
is 1, that bit position is a “don’t care.” The corresponding bit in the incoming message does not need to match theAcceptance_Code_Bit_i
. - If
Acceptance_Mask_Bit_i
is 0, that bit position is a “must match.” The corresponding bit in the incoming message must be equal to theAcceptance_Code_Bit_i
.
Important: This “mask bit 1 = don’t care” logic is specific to some CAN controllers, including the ESP32’s TWAI peripheral. Other CAN controllers might use the opposite convention (mask bit 1 = must match). Always refer to the specific hardware documentation.
graph TD %% Mermaid Flowchart for TWAI Acceptance Filter Logic %% Styles classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef accept fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef reject fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B; classDef config fill:#E0E7FF,stroke:#4338CA,color:#3730A3; A["Incoming CAN Message Arrives<br>(Contains Message_ID)"]:::start A --> B["Retrieve Filter Config:<br><span style='font-family:monospace;font-size:0.9em;'>twai_filter_config_t</span><br>- Acceptance Code (ACR)<br>- Acceptance Mask (AMR)<br>- single_filter_mode"]:::config B --> C["Align Incoming Message_ID with ACR/AMR<br>(Left-shift Std ID by 21, Ext ID by 3)"]:::process C --> D{"Iterate through relevant bits (e.g., 11 for Std, 29 for Ext)"}:::process LoopStart(Loop for each bit <i>i</i> in aligned ID) --> BitDecision{"AMR_Bit_<i>i</i> == 1 (Don't Care)?"}:::decision D --> LoopStart BitDecision -- "Yes" --> BitOK["Bit <i>i</i> Passes (Don't Care)"]:::accept BitDecision -- "No (AMR_Bit_<i>i</i> == 0, Must Match)" --> CompareBits{Aligned_Message_ID_Bit_<i>i</i> == ACR_Bit_<i>i</i>?}:::decision CompareBits -- "Yes (Match)" --> BitOK CompareBits -- "No (Mismatch)" --> AllBitsMatch{"Set 'All_Bits_Match_So_Far' = false"}:::reject AllBitsMatch --> LoopEnd(End Loop for bit <i>i</i>) BitOK --> LoopNext{More Bits to Check?}:::decision LoopNext -- "Yes" --> LoopStart LoopNext -- "No (All relevant bits checked)" --> FinalDecision{"'All_Bits_Match_So_Far' is still true?"}:::decision LoopEnd --> FinalDecision FinalDecision -- "Yes" --> AcceptMsg["Message Accepted<br>Pass to RX Queue"]:::accept FinalDecision -- "No" --> RejectMsg["Message Rejected<br>Discarded by Hardware"]:::reject subgraph "Per-Bit Logic within Filter Hardware" LoopStart BitDecision CompareBits BitOK AllBitsMatch LoopNext LoopEnd end
To Accept All Messages:
If you want the filter to accept all messages (effectively disabling filtering), every bit in the acceptance_mask should be set to ‘1’ (don’t care).
acceptance_code = 0x00000000
(value doesn’t matter if mask is all 1s)acceptance_mask = 0xFFFFFFFF
single_filter_mode = true
This is precisely what the TWAI_FILTER_CONFIG_ACCEPT_ALL()
macro provides:
#define TWAI_FILTER_CONFIG_ACCEPT_ALL() {.acceptance_code = 0, .acceptance_mask = 0xFFFFFFFF, .single_filter_mode = true}
Condition for a Single Bit (i ) |
Acceptance_Mask_Bit_i Value |
Meaning for Filtering | Result for Bit i |
---|---|---|---|
Comparison of Incoming_Message_Bit_i with Acceptance_Code_Bit_i |
0 (“Must Match”) |
The Incoming_Message_Bit_i must be equal to Acceptance_Code_Bit_i . |
Passes if equal, Fails if not equal. |
1 (“Don’t Care”) |
The value of Incoming_Message_Bit_i does not matter for this bit position. The Acceptance_Code_Bit_i is ignored for this bit. |
Always Passes (for this bit). | |
Overall Message Acceptance Rule: | A message is accepted only if all “Must Match” bits pass (i.e., are equal to their corresponding acceptance code bits). “Don’t Care” bits always pass. | ||
To Accept All Messages (e.g., using TWAI_FILTER_CONFIG_ACCEPT_ALL() ): |
Set acceptance_mask = 0xFFFFFFFF (all bits are ‘1’ – Don’t Care). acceptance_code can be anything (typically 0). |
2.3. Mapping Message IDs to the 32-bit Filter Registers
The acceptance_code
and acceptance_mask
are 32-bit values. The CAN message identifiers (11-bit standard or 29-bit extended) need to be correctly positioned within these 32-bit fields for the filter to work as expected.
The ESP-IDF TWAI driver expects the relevant bits of the identifier to be left-aligned (shifted to the Most Significant Bit side) within the 32-bit acceptance_code
and acceptance_mask
fields that you provide in twai_filter_config_t
.
- For Standard IDs (11-bit):The 11 bits of the standard identifier (ID[10:0]) are mapped to the most significant 11 bits of the 32-bit filter comparison logic. This means you need to left-shift your 11-bit ID by (32 – 11) = 21 positions.
acceptance_code_field = (uint32_t)standard_id << 21;
- The corresponding 11 bits in the
acceptance_mask
field will determine how these ID bits are filtered.
- For Extended IDs (29-bit):The 29 bits of the extended identifier (ID[28:0]) are mapped to the most significant 29 bits of the 32-bit filter comparison logic. This means you need to left-shift your 29-bit ID by (32 – 29) = 3 positions.
acceptance_code_field = (uint32_t)extended_id << 3;
- The corresponding 29 bits in the
acceptance_mask
field will determine filtering.
2.4. Configuring Filters for Specific IDs
To accept ONLY a specific Standard ID (e.g., 0x123):
You want the 11 ID bits to match exactly, and you don’t care about the remaining bits of the 32-bit filter field (or rather, the hardware only considers the relevant ID part).
- Acceptance Code: Shift the 11-bit ID to the MSB side.uint32_t std_id = 0x123;f_config.acceptance_code = std_id << 21;
- Acceptance Mask: The 11 bits corresponding to the ID must be ‘0’ (must match). The other 32 – 11 = 21 bits of the 32-bit mask field should be ‘1’ (don’t care).An 11-bit mask of all zeros is 0x000.The 32-bit mask where the top 11 bits are 0 and the bottom 21 bits are 1 is:f_config.acceptance_mask = ~(((uint32_t)0x7FF) << 21);
0x7FF
is an 11-bit mask with all bits set to 1 (111 1111 1111
).((uint32_t)0x7FF) << 21
creates11111111111000000000000000000000
(binary).- ~ inverts this to 00000000000111111111111111111111 (binary).This means the top 11 bits are “must match,” and the lower 21 bits are “don’t care.”
To accept ONLY a specific Extended ID (e.g., 0x1ABCDE2
):
- Acceptance Code: Shift the 29-bit ID to the MSB side.uint32_t ext_id = 0x1ABCDE2;f_config.acceptance_code = ext_id << 3;
- Acceptance Mask: The 29 bits corresponding to the ID must be ‘0’ (must match). The other 32 – 29 = 3 bits of the 32-bit mask field should be ‘1’ (don’t care).A 29-bit mask of all zeros is 0x00000000.The 32-bit mask where the top 29 bits are 0 and the bottom 3 bits are 1 is:f_config.acceptance_mask = ~(((uint32_t)0x1FFFFFFF) << 3);
0x1FFFFFFF
is a 29-bit mask with all bits set to 1.(((uint32_t)0x1FFFFFFF) << 3)
creates111...111000
(29 ones followed by 3 zeros).~
inverts this to000...000111
(29 zeros followed by 3 ones).
2.5. Filtering for a Range or Pattern
You can use the mask to accept a range of IDs or IDs that share a common prefix.
Example: Accept Standard IDs from 0x120 to 0x12F (i.e., 0x12X).
The common prefix is 0x12 for the top 7 bits of the 11-bit ID. The lower 4 bits can vary.
Filtering Goal | ID Type & Value(s) | acceptance_code (32-bit, after shift) |
acceptance_mask (32-bit, effective bits shown) |
Notes (single_filter_mode = true ) |
---|---|---|---|---|
Accept ALL Messages | Any | 0x00000000 |
0xFFFFFFFF (All ‘1’s = all don’t care) |
Standard TWAI_FILTER_CONFIG_ACCEPT_ALL() . |
Accept ONLY Standard ID 0x123 |
Standard, 0x123 |
(0x123 << 21) = 0x91800000 |
~((uint32_t)0x7FF << 21) = 0x001FFFFF (Top 11 bits '0' = must match, rest '1') |
Exact match for the 11-bit ID. |
Accept ONLY Extended ID 0x1ABCDE2 |
Extended, 0x1ABCDE2 |
(0x1ABCDE2 << 3) = 0x0D5E6F10 |
~((uint32_t)0x1FFFFFFF << 3) = 0x00000007 (Top 29 bits '0' = must match, rest '1') |
Exact match for the 29-bit ID. |
Accept Standard IDs 0x120 to 0x12F (0x12X) |
Standard Range | (0x120 << 21) = 0x90000000 (Base of the range) |
(~((uint32_t)0x7FF << 21)) | (0x00F << 21) = 0x001F8000 | 0x00780000 = 0x007F8000 (Top 7 bits of ID '0' (match 0x12- ), next 4 '1' (don't care -X ), rest '1')
Simplified: Mask's 11-bit ID part is 00000001111 |
Matches IDs where top 7 bits are 0010010 (0x12-), lower 4 bits can be anything. |
Accept Standard IDs with upper 8 bits matching 0xAB- (i.e., 0xAB0 to 0xABF ) |
Standard Range | (0xAB0 << 21) = 0x55800000 |
(~((uint32_t)0x7FF << 21)) | (0x007 << 21) = 0x001F8000 | 0x00380000 = 0x003F8000 (Top 8 bits of ID '0' (match 0xAB- ), next 3 '1' (don't care --X ), rest '1')
Simplified: Mask's 11-bit ID part is 00000000111 |
Matches IDs where top 8 bits are 10101011 (0xAB-). |
Binary representation:
0x120 = 001 0010 0000
0x12F = 001 0010 1111
Acceptance Code: Use the base of the range, 0x120.uint32_t base_id = 0x120;f_config.acceptance_code = base_id << 21;
Acceptance Mask:
- The top 7 bits (
001 0010
) must match. So, the mask bits for these positions are '0'. - The lower 4 bits (
XXXX
) can be anything. So, the mask bits for these positions are '1' (don't care). The 11-bit mask pattern is00000001111
(7 zeros for must-match, 4 ones for don't-care). This 11-bit mask value is0x00F
. Shifted to the MSB portion of the 32-bit mask:uint32_t eleven_bit_mask_pattern = 0x00F;
The 32-bit mask is constructed such that the 11-bit effective mask is(0x7FF & ~eleven_bit_mask_pattern)
for the "must match" part, and then inverted and shifted. More directly: The mask for the top 7 bits is0000000
(must match). The mask for the next 4 bits is1111
(don't care). So the 11-bit section of the 32-bit mask (where 0=match, 1=don't care) should look likeMMMMMMM DDDD
where M=0, D=1. This is00000001111
(binary) for the 11-bit ID part. The 32-bit mask (shifted and with other bits as 'don't care'):uint32_t relevant_mask_part = ((uint32_t)0x00F) << 21; // The '1's are the don't care bits for the ID part
uint32_t must_match_part_mask = ~(((uint32_t)0x7FF) << 21); // 0s for ID bits, 1s for non-ID bitsf_config.acceptance_mask = must_match_part_mask | relevant_mask_part;
This is equivalent to:
f_config.acceptance_mask = (~(((uint32_t)0x7F0) << 21)) & (((uint32_t)0x00F << 21) | ~(((uint32_t)0x7FF) << 21)));
This simplifies to:
The mask for the fixed part (0x12-
) should be 0
. The mask for the variable part (-X
) should be 1
.
Fixed part: ID[10:4]
(7 bits). Variable part: ID[3:0]
(4 bits).
Mask bits (for the 11-bit ID section): 00000001111
(7 zeros, 4 ones).
This 11-bit value is 0x00F
. T
he 32-bit mask is:
f_config.acceptance_mask = (~(((uint32_t)0x7FF) << 21)) | (((uint32_t)0x00F) << 21);
Here, ~((0x7FF) << 21)
sets the 11 MSBs to 0 and the rest to 1.
Then we OR it with (0x00F << 21)
, which sets the lower 4 of those 11 MSBs to 1.
So, the top 7 bits of the mask are 0, next 4 bits are 1, and lower 21 bits are 1.
Example: std_id = 0x125
. code = (0x120 << 21)
. mask = (0b00000001111 << 21) | lower_21_bits_all_1s
.
Let ID be I
. Code be C
. Mask be M
. Condition: (I_i == C_i) || M_i == 1
.
I = 0x125 = 00100100101
-
C_shifted = (0x120 << 21) = 00100100000...
M_shifted = (mask for 0x12X) = 00000001111...
(where ... are 1s for non-ID part)
Comparison for ID bits:
- ID bit 10 (0) vs Code bit 10 (0). Mask bit 10 (0). Match (0==0). OK.
- ...
- ID bit 4 (0) vs Code bit 4 (0). Mask bit 4 (0). Match (0==0). OK.
- ID bit 3 (1) vs Code bit 3 (0). Mask bit 3 (1). Don't care. OK.
- ID bit 2 (0) vs Code bit 2 (0). Mask bit 2 (1). Don't care. OK.
- ID bit 1 (1) vs Code bit 1 (0). Mask bit 1 (1). Don't care. OK.
- ID bit 0 (0) vs Code bit 0 (0). Mask bit 0 (1). Don't care. OK.
This works.
2.6. single_filter_mode
single_filter_mode = true
: This is the default and most straightforward mode. Theacceptance_code
andacceptance_mask
apply as a single 32-bit filter. The driver and hardware determine how standard/extended IDs (and potentially first data bytes on some older interpretations/chips) are mapped into this single filter. For ESP-IDF v5.x, the primary interpretation is filtering based on the MSB-aligned ID.- single_filter_mode = false (Dual Filter Mode - ESP32 Classic):On the original ESP32, setting this to false enables a dual filter mode. The hardware effectively provides two smaller, independent filters. The ESP-IDF API documentation suggests these filters might be used to separately filter based on ID and the first data byte, and ID and the second data byte. This mode is more complex to configure and less portable to other ESP32 variants, which typically only robustly support the single filter mode for ID-based filtering.
single_filter_mode Value |
Filter Mode | Description | Applicability / Recommendation |
---|---|---|---|
true |
Single Filter Mode | The filter hardware uses one set of 32-bit acceptance_code and acceptance_mask registers to filter all incoming messages (both standard and extended IDs, based on MSB alignment). |
Recommended for most applications. Available on all ESP32 variants with TWAI. Simpler to configure for ID-based filtering. TWAI_FILTER_CONFIG_ACCEPT_ALL() uses this mode. |
false |
Dual Filter Mode | (Primarily ESP32 classic specific) The filter hardware might be configurable as two smaller, independent filters or offer more complex filtering (e.g., based on ID and data bytes). Behavior and support vary significantly across ESP32 variants. | Specialized use cases, mainly on ESP32 classic. Less portable. For other variants, setting to false might not be supported or behave as expected for dual filtering. Stick to true for general ID filtering. |
Recommendation: For general-purpose ID filtering and cross-variant compatibility, stick to
single_filter_mode = true
.
3. Practical Examples
These examples demonstrate configuring the filter. You would typically integrate this into a full application that also initializes the driver, starts it, and has transmit/receive logic.
Example 1: Filter for a Single Standard ID
Goal: Accept only standard CAN messages with ID 0x3AA
.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/twai.h"
#include "esp_log.h"
static const char *TAG = "TWAI_FILTER_STD_EXAMPLE";
#define TWAI_TX_GPIO_NUM CONFIG_EXAMPLE_TWAI_TX_GPIO
#define TWAI_RX_GPIO_NUM CONFIG_EXAMPLE_TWAI_RX_GPIO
#define TARGET_STD_ID 0x3AA
// 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_receive_filtered_task(void *pvParameters)
{
twai_message_t rx_msg;
ESP_LOGI(TAG, "Receiver task started. Waiting for messages with ID 0x%03X...", TARGET_STD_ID);
while (1) {
esp_err_t rx_result = twai_receive(&rx_msg, pdMS_TO_TICKS(portMAX_DELAY));
if (rx_result == ESP_OK) {
ESP_LOGI(TAG, "Message received! ID: 0x%03lX, DLC: %d, Flags: 0x%lX",
rx_msg.identifier, rx_msg.data_length_code, rx_msg.flags);
// Further processing...
// We expect only TARGET_STD_ID due to the filter.
if (rx_msg.identifier == TARGET_STD_ID && !(rx_msg.flags & TWAI_MSG_FLAG_EXTD)) {
ESP_LOGI(TAG, "Correct ID (0x%03X) received as expected.", TARGET_STD_ID);
} else {
ESP_LOGW(TAG, "Unexpected message received despite filter! ID: 0x%lX, Flags: 0x%lX",
rx_msg.identifier, rx_msg.flags);
}
} else if (rx_result == ESP_ERR_TIMEOUT) {
// Should not happen with portMAX_DELAY unless driver stopped
ESP_LOGW(TAG, "Timeout (should not occur with portMAX_DELAY)");
} else {
ESP_LOGE(TAG, "Failed to receive message: %s", esp_err_to_name(rx_result));
break; // Exit on error
}
}
vTaskDelete(NULL);
}
// Dummy task to send messages (for testing with self_test mode)
static void twai_send_test_messages_task(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(2000)); // Wait for receiver to be ready
twai_message_t tx_msg;
tx_msg.flags = 0; // Standard data frame
// Message that should be accepted
tx_msg.identifier = TARGET_STD_ID;
tx_msg.data_length_code = 1;
tx_msg.data[0] = 0xAA;
ESP_LOGI(TAG, "TX Task: Sending message with ID 0x%03lX (should be accepted)", tx_msg.identifier);
twai_transmit(&tx_msg, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(500));
// Message that should be filtered out
tx_msg.identifier = 0x1FF; // Different ID
tx_msg.data_length_code = 1;
tx_msg.data[0] = 0xBB;
ESP_LOGI(TAG, "TX Task: Sending message with ID 0x%03lX (should be filtered)", tx_msg.identifier);
twai_transmit(&tx_msg, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(500));
// Another message that should be accepted
tx_msg.identifier = TARGET_STD_ID;
tx_msg.data_length_code = 1;
tx_msg.data[0] = 0xCC;
ESP_LOGI(TAG, "TX Task: Sending message with ID 0x%03lX (should be accepted again)", tx_msg.identifier);
twai_transmit(&tx_msg, portMAX_DELAY);
ESP_LOGI(TAG, "TX Task: Finished sending test messages.");
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_LOGI(TAG, "TWAI Filter Example: Accept Standard ID 0x%03X", TARGET_STD_ID);
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO_NUM, TWAI_RX_GPIO_NUM, TWAI_MODE_SELF_TEST);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
// Configure filter for TARGET_STD_ID
twai_filter_config_t f_config;
f_config.acceptance_code = ((uint32_t)TARGET_STD_ID) << 21;
f_config.acceptance_mask = ~(((uint32_t)0x7FF) << 21); // Match all 11 bits of std ID, rest don't care
f_config.single_filter_mode = true;
ESP_LOGI(TAG, "Filter Config: AC=0x%08lX, AM=0x%08lX, SingleMode=%d",
f_config.acceptance_code, f_config.acceptance_mask, f_config.single_filter_mode);
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.");
xTaskCreate(twai_receive_filtered_task, "twai_rx_filter_task", 4096, NULL, 10, NULL);
xTaskCreate(twai_send_test_messages_task, "twai_tx_test_task", 4096, NULL, 5, NULL);
}
Build and Run:
- Ensure KConfig options for GPIOs are set.
- Build, flash, and monitor.
- Observe: The receiver task should only log messages with ID
0x3AA
. The message with ID0x1FF
should be filtered out by the hardware and not appear in the receiver task's logs (or trigger an "Unexpected message" warning if it somehow gets through, indicating a filter misconfiguration).
Example 2: Filter for a Single Extended ID
Goal: Accept only extended CAN messages with ID 0x1234567
.
// ... (includes, TAG, GPIO defines similar to Example 1) ...
#define TARGET_EXT_ID 0x1234567
// Modify twai_receive_filtered_task to log extended ID format
static void twai_receive_filtered_ext_task(void *pvParameters)
{
twai_message_t rx_msg;
ESP_LOGI(TAG, "Receiver task started. Waiting for messages with EXT ID 0x%08lX...", TARGET_EXT_ID);
while (1) {
esp_err_t rx_result = twai_receive(&rx_msg, pdMS_TO_TICKS(portMAX_DELAY));
if (rx_result == ESP_OK) {
ESP_LOGI(TAG, "Message received! ID: 0x%08lX, DLC: %d, Flags: 0x%lX",
rx_msg.identifier, rx_msg.data_length_code, rx_msg.flags);
if (rx_msg.identifier == TARGET_EXT_ID && (rx_msg.flags & TWAI_MSG_FLAG_EXTD)) {
ESP_LOGI(TAG, "Correct EXT ID (0x%08lX) received as expected.", TARGET_EXT_ID);
} else {
ESP_LOGW(TAG, "Unexpected message received despite filter! ID: 0x%lX, Flags: 0x%lX",
rx_msg.identifier, rx_msg.flags);
}
} // ... error handling ...
}
vTaskDelete(NULL);
}
// Modify twai_send_test_messages_task for extended IDs
static void twai_send_test_ext_messages_task(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(2000));
twai_message_t tx_msg;
tx_msg.flags = TWAI_MSG_FLAG_EXTD; // Extended data frame
// Message that should be accepted
tx_msg.identifier = TARGET_EXT_ID;
tx_msg.data_length_code = 1;
tx_msg.data[0] = 0xCC;
ESP_LOGI(TAG, "TX Task: Sending message with EXT ID 0x%08lX (should be accepted)", tx_msg.identifier);
twai_transmit(&tx_msg, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(500));
// Message that should be filtered out
tx_msg.identifier = 0x7654321; // Different EXT ID
tx_msg.data_length_code = 1;
tx_msg.data[0] = 0xDD;
ESP_LOGI(TAG, "TX Task: Sending message with EXT ID 0x%08lX (should be filtered)", tx_msg.identifier);
twai_transmit(&tx_msg, portMAX_DELAY);
ESP_LOGI(TAG, "TX Task: Finished sending test messages.");
vTaskDelete(NULL);
}
void app_main(void) {
// ... (g_config, t_config setup as before) ...
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO_NUM, TWAI_RX_GPIO_NUM, TWAI_MODE_SELF_TEST);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
twai_filter_config_t f_config;
f_config.acceptance_code = ((uint32_t)TARGET_EXT_ID) << 3;
f_config.acceptance_mask = ~(((uint32_t)0x1FFFFFFF) << 3); // Match all 29 bits of ext ID
f_config.single_filter_mode = true;
ESP_LOGI(TAG, "Filter Config: AC=0x%08lX, AM=0x%08lX, SingleMode=%d",
f_config.acceptance_code, f_config.acceptance_mask, f_config.single_filter_mode);
// ... (driver install, start, task creation for ext tasks) ...
ESP_LOGI(TAG, "Installing TWAI driver...");
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { /* ... */ return; }
ESP_LOGI(TAG, "Starting TWAI driver...");
if (twai_start() != ESP_OK) { /* ... */ return; }
ESP_LOGI(TAG, "TWAI driver started.");
xTaskCreate(twai_receive_filtered_ext_task, "twai_rx_ext_task", 4096, NULL, 10, NULL);
xTaskCreate(twai_send_test_ext_messages_task, "twai_tx_ext_test_task", 4096, NULL, 5, NULL);
}
Observe: Only the extended ID 0x1234567
should pass the filter.
Example 3: Filter for a Range of Standard IDs (e.g., 0x450
to 0x45F
)
Common prefix is 0x45
(top 7 bits: 100 0101
). Lower 4 bits are variable.
- Acceptance Code: Use the base of the range, 0x450.uint32_t base_id = 0x450;f_config.acceptance_code = base_id << 21;
- Acceptance Mask:The top 7 bits must match (mask bits = 0). The lower 4 ID bits can vary (mask bits = 1).The 11-bit pattern for the ID part of the mask is 00000001111 (binary).This 11-bit value is 0x00F.The 32-bit mask is formed by setting the 11 MSB-aligned bits according to this pattern and the rest to '1' (don't care).f_config.acceptance_mask = (~(((uint32_t)0x7FF) << 21)) | (((uint32_t)0x00F) << 21);f_config.single_filter_mode = true;
You would then modify the sender task to send IDs like 0x450
, 0x455
, 0x45F
(should pass) and 0x460
, 0x123
(should be filtered). The receiver task would verify.
4. Variant Notes
- Filter Hardware:
- ESP32 (Classic): Supports both single and dual filter modes. Dual filter mode offers more complex filtering capabilities (e.g., filtering based on ID and specific data bytes), but its configuration via the ESP-IDF API can be less straightforward than single filter mode.
- ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: These variants primarily support single filter mode for ID-based filtering as described. The
single_filter_mode
flag intwai_filter_config_t
should generally be set totrue
. Attempting to use dual filter mode on these chips might lead to undefined behavior or may not be supported by the driver/hardware in the same way as ESP32 classic.
- API Consistency: The
twai_filter_config_t
structure and the method of definingacceptance_code
andacceptance_mask
for MSB-aligned ID filtering in single filter mode are generally consistent across all supported variants. - Number of Filters: The basic TWAI controller provides one set of acceptance code/mask registers. More advanced CAN controllers (not typically the base TWAI peripheral) might offer multiple independent filter banks. The ESP32 TWAI focuses on this primary filter mechanism.
- CAN FD: While this chapter focuses on Classical CAN, the filter mechanism can also apply to CAN FD identifiers. However, CAN FD does not typically involve filtering on data bytes in the same way some Classical CAN dual-filter modes might have attempted. The primary filtering is ID-based.
5. Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect Mask/Code Calculation or Bit Shifting | Filter accepts wrong messages or rejects correct messages. Logic seems off. |
|
Standard vs. Extended ID Mismatch in Filter Logic | Filter set for standard ID doesn't catch expected extended ID (or vice-versa), even if numerical ID values overlap partially. |
|
Using TWAI_FILTER_CONFIG_ACCEPT_ALL() Incorrectly |
Changed acceptance_code but kept default acceptance_mask (0xFFFFFFFF from macro), still accepts all messages. |
|
Filter Not Taking Effect / Dynamic Changes Not Working | Changes to twai_filter_config_t after driver installation do not change filter behavior. |
|
Overly Complex Filter Logic for Single Filter | Trying to accept multiple, disjoint specific IDs or very complex patterns with one code/mask pair becomes unmanageable or impossible. |
|
6. Exercises
- Filter for Multiple Specific Standard IDs:
- Can you configure a single filter (one
acceptance_code
and oneacceptance_mask
in single filter mode) to accept only standard ID0x1A0
AND standard ID0x1B0
, but reject0x1C0
? Explain your reasoning and the mask/code. (Hint: Consider common bits and "don't care" bits). If not possible with one filter, explain why.
- Can you configure a single filter (one
- Filter for RTR Frames Only:
- The TWAI hardware filter primarily acts on the ID. The RTR bit is part of the arbitration field. Research if the standard ESP32 TWAI filter mechanism (using
acceptance_code
/acceptance_mask
for IDs) can also explicitly filter based on whether a frame is a Data Frame or an RTR frame. If so, how? If not, how would you achieve this in software? - (Hint: Some CAN controllers map the RTR bit into the filterable bits. Check ESP32 TRM or experiment if the driver exposes this. Typically, you filter on ID, then check
msg->flags & TWAI_MSG_FLAG_RTR
in software).
- The TWAI hardware filter primarily acts on the ID. The RTR bit is part of the arbitration field. Research if the standard ESP32 TWAI filter mechanism (using
- Reject All Extended Frames:
- Configure the filter to accept all standard frames but reject all extended frames. (Hint: How are standard and extended IDs positioned in the filter registers? Can you make the mask such that the bit distinguishing them must be a certain value for standard frames?)
- Experiment with
single_filter_mode = false
(ESP32 Classic Only):- If you have an ESP32 classic, try setting
single_filter_mode = false
. The API documentation mentions it filters ID and first data byte, and ID and second data byte. Try to devise a test where this behavior could be observed. This is an advanced exercise. For other variants, this mode may not be applicable or behave as documented for ESP32 classic.
- If you have an ESP32 classic, try setting
7. Summary
- Acceptance Filtering is crucial for reducing CPU load by allowing the TWAI hardware to discard irrelevant CAN messages.
- Configuration is done via
twai_filter_config_t
passed totwai_driver_install()
. - Key fields are
acceptance_code
,acceptance_mask
, andsingle_filter_mode
. - ESP32 Mask Logic: A mask bit of '1' means "don't care"; a mask bit of '0' means the corresponding ID bit "must match" the
acceptance_code
bit. TWAI_FILTER_CONFIG_ACCEPT_ALL()
setsacceptance_mask = 0xFFFFFFFF
to accept all messages.- ID Alignment: Standard (11-bit) IDs are typically shifted left by 21 positions, and extended (29-bit) IDs by 3 positions, to align them with the MSB side of the 32-bit filter registers. The
acceptance_mask
must also be constructed with this alignment in mind. single_filter_mode = true
is the common, portable way to filter based on message IDs.- Careful calculation of code and mask is essential for correct filter behavior.
8. Further Reading
- ESP-IDF TWAI API Reference:
- TWAI Driver Configuration Structures (Details on
twai_filter_config_t
). - (Ensure to select your specific ESP-IDF version and target chip on the documentation website).
- TWAI Driver Configuration Structures (Details on
- ESP32 Technical Reference Manual (TRM):
- The chapter on TWAI (Controller Area Network in some TRMs) will have a detailed section on the Acceptance Filter hardware registers and their operation (ACR, AMR, filter modes). This provides the ground truth for hardware behavior.