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, including acceptance_code and acceptance_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:

C
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 the acceptance_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.

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 the Acceptance_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 the Acceptance_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:

C
#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).

  1. Acceptance Code: Shift the 11-bit ID to the MSB side.uint32_t std_id = 0x123;f_config.acceptance_code = std_id << 21;
  2. 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 creates 11111111111000000000000000000000 (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):

  1. Acceptance Code: Shift the 29-bit ID to the MSB side.uint32_t ext_id = 0x1ABCDE2;f_config.acceptance_code = ext_id << 3;
  2. 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) creates 111...111000 (29 ones followed by 3 zeros).
    • ~ inverts this to 000...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 is 00000001111 (7 zeros for must-match, 4 ones for don't-care). This 11-bit mask value is 0x00F. 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 is 0000000 (must match). The mask for the next 4 bits is 1111 (don't care). So the 11-bit section of the 32-bit mask (where 0=match, 1=don't care) should look like MMMMMMM DDDD where M=0, D=1. This is 00000001111 (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 bits f_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. The acceptance_code and acceptance_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.

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_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:

  1. Ensure KConfig options for GPIOs are set.
  2. Build, flash, and monitor.
  3. Observe: The receiver task should only log messages with ID 0x3AA. The message with ID 0x1FF 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.

C
// ... (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.

  1. Acceptance Code: Use the base of the range, 0x450.uint32_t base_id = 0x450;f_config.acceptance_code = base_id << 21;
  2. 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 in twai_filter_config_t should generally be set to true. 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 defining acceptance_code and acceptance_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.
  • Mask Logic: For ESP32 TWAI, mask bit '1' = "don't care", mask bit '0' = "must match" the code bit.
  • ID Shifting: Standard 11-bit IDs must be left-shifted by 21 positions into the 32-bit code/mask. Extended 29-bit IDs by 3 positions.
  • Binary Check: Write out the binary patterns for your ID, code, and mask to visually verify alignment and logic.
  • Test Incrementally: Start with TWAI_FILTER_CONFIG_ACCEPT_ALL(), then make small, verifiable changes to your filter.
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.
  • Correct Shifting: Ensure the ID in acceptance_code and the patterns in acceptance_mask use the correct shift (21 for standard, 3 for extended).
  • Hardware Distinction: The hardware inherently distinguishes between standard and extended frames. A filter for a standard ID usually won't match an extended ID unless the mask is very broad.
  • Explicit Configuration: If filtering for a specific ID type, make sure your code and mask are constructed for that type.
Using TWAI_FILTER_CONFIG_ACCEPT_ALL() Incorrectly Changed acceptance_code but kept default acceptance_mask (0xFFFFFFFF from macro), still accepts all messages.
  • Mask is Key: If acceptance_mask is 0xFFFFFFFF, all bits are "don't care," so the acceptance_code value has no effect; all messages pass.
  • Set Mask '0's: To filter, specific bits in the acceptance_mask (corresponding to the ID bits you want to match) MUST be set to '0'.
Filter Not Taking Effect / Dynamic Changes Not Working Changes to twai_filter_config_t after driver installation do not change filter behavior.
  • Apply at Install: Filter configuration (acceptance_code, acceptance_mask, single_filter_mode) is applied during twai_driver_install().
  • Re-install for Changes: To change the filter, the typical procedure is:
    1. Stop the driver: twai_stop().
    2. Uninstall the driver: twai_driver_uninstall().
    3. Update the twai_filter_config_t structure.
    4. Re-install the driver with the new config: twai_driver_install().
    5. Re-start the driver: twai_start().
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.
  • Hardware Limits: A single code/mask pair has limitations in expressiveness.
  • Software Filtering: For highly complex rules, configure the hardware filter to accept a broader superset of desired messages, then perform fine-grained filtering in your application software after receiving them.
  • ESP32 Classic Dual Mode: If using ESP32 classic, explore dual filter mode for potentially more hardware options, but note the added complexity and reduced portability.

6. Exercises

  1. Filter for Multiple Specific Standard IDs:
    • Can you configure a single filter (one acceptance_code and one acceptance_mask in single filter mode) to accept only standard ID 0x1A0 AND standard ID 0x1B0, but reject 0x1C0? Explain your reasoning and the mask/code. (Hint: Consider common bits and "don't care" bits). If not possible with one filter, explain why.
  2. 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).
  3. 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?)
  4. 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.

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 to twai_driver_install().
  • Key fields are acceptance_code, acceptance_mask, and single_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() sets acceptance_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:
  • 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.

Leave a Comment

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

Scroll to Top