feat(twai): add TWAI utility commands and configuration

- Introduced TWAI utility commands for sending, dumping, and managing TWAI frames.
- Added configuration options for TWAI GPIO pins and support for TWAI-FD.
- Created necessary CMake and Kconfig files for building the TWAI utilities.

This enhancement provides a comprehensive interface for TWAI operations.
This commit is contained in:
Yuan Yu
2025-08-13 15:34:54 +08:00
parent 78f2b2ad10
commit f1da574ae5
16 changed files with 3859 additions and 0 deletions

View File

@@ -530,6 +530,12 @@ examples/peripherals/twai/twai_self_test:
temporary: true temporary: true
reason: lack of runners reason: lack of runners
examples/peripherals/twai/twai_utils:
disable:
- if: SOC_TWAI_SUPPORTED != 1
depends_components:
- esp_driver_twai
examples/peripherals/uart/uart_dma_ota: examples/peripherals/uart/uart_dma_ota:
disable: disable:
- if: SOC_UHCI_SUPPORTED != 1 - if: SOC_UHCI_SUPPORTED != 1

View File

@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(twai_utils)

View File

@@ -0,0 +1,572 @@
| Supported Targets | ESP32 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-H21 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- |
# TWAI Console Example
This example demonstrates using the TWAI (Two-Wire Automotive Interface) driver through an interactive console interface. It provides comprehensive TWAI functionality including frame transmission/reception, message filtering, and bus monitoring. The example can be used for both standalone testing via loopback mode and real TWAI network communication.
**Supported Commands:**
| Command | Description | Linux can-utils Equivalent |
|---------|-------------|----------------------------|
| `twai_init <controller> -t <tx> -r <rx> [opts]` | Initialize TWAI controller with GPIO and mode options | `ip link set can0 up type can bitrate 500000` |
| `twai_deinit <controller>` | Deinitialize TWAI controller | `ip link set can0 down` |
| `twai_send <controller> <frame>` | Send TWAI frame (standard/extended/RTR/FD) | `cansend can0 123#DEADBEEF` |
| `twai_dump <controller>[,filter] [-t mode]` / `twai_dump <controller> --stop` | Monitor TWAI traffic with hardware filtering and timestamps | `candump can0` |
| `twai_info <controller>` | Display controller configuration, status | `ip -details link show can0` |
| `twai_recover <controller> [-t <ms>]` | Recover controller from Bus-Off state | N/A |
- Note: `twai_dump` runs continuously in the background. Use `twai_dump <controller> --stop ` to stop monitoring.
## How to Use This Example
### Hardware Required
- Any ESP development board with TWAI support.
- A TWAI transceiver (e.g., SN65HVD230, TJA1050).
- Jumper wires.
### Hardware Setup
Connect the ESP board to a transceiver:
```
ESP32 Pin Transceiver TWAI Bus
--------- ----------- --------
GPIO4 (TX) --> CTX
GPIO5 (RX) <-- CRX
3.3V --> VCC
GND --> GND
TWAI_H <--> TWAI_H
TWAI_L <--> TWAI_L
```
*Note: The specific GPIO pins for TX and RX must be provided in the `twai_init` command.*
### Quick Start - No Transceiver Mode
For immediate testing without any external hardware, you can use the **No Transceiver Mode** by connecting a single GPIO pin to itself.
```bash
# Connect GPIO4 to itself (or leave it unconnected for self-test)
# Initialize with the same TX/RX GPIO, and enable loopback and self-test modes.
twai> twai_init twai0 -t 4 -r 4 --loopback --self-test
# Send a test frame
twai> twai_send twai0 123#DEADBEEF
# Check controller status
twai> twai_info twai0
```
This mode is ideal for learning the commands, testing application logic, and debugging frame formats without a physical bus.
### Configure the project
```bash
idf.py menuconfig
```
Navigate to **Example Configuration** -> **TWAI Configuration** and configure:
- **Default Arbitration Bitrate**: Default arbitration bitrate in bits per second (bps).
- **Default FD Data Bitrate**: Default data bitrate for TWAI-FD in bits per second (bps).
- **Enable TWAI-FD Support**: Enable TWAI-FD (Flexible Data-rate) support (default: disabled)
- **TX Queue Length**: Length of the transmission queue for TWAI messages (default: 10)
**Note:** For every controller, you must specify the TX and RX pins explicitly with the `-t` and `-r` options when issuing `twai_init`. Failing to do so will make initialization return an error.
### Build and Flash
```bash
idf.py -p PORT flash monitor
```
(To exit the serial monitor, type `Ctrl-]`.)
## Command Reference
### `twai_init`
Initializes and starts the TWAI driver. **TX and RX pins are required.**
**Usage:**
`twai_init <controller> -t <tx_gpio> -r <rx_gpio> [options]`
**Arguments:**
- `<controller>`: Controller ID (`twai0`, `twai1`).
- `-t, --tx`: TX GPIO pin number (required).
- `-r, --rx`: RX GPIO pin number (required).
- `-b, --bitrate`: Arbitration bitrate in bps (default: CONFIG_EXAMPLE_DEFAULT_BITRATE).
- `-B, --fd-bitrate`: Data bitrate for TWAI-FD (FD-capable chips only, default: CONFIG_EXAMPLE_DEFAULT_FD_BITRATE).
- `--loopback`: Enable loopback mode.
- `--self-test`: Enable self-test mode (internal loopback).
- `--listen`: Enable listen-only mode.
- `-c, --clk-out`: Clock output GPIO pin (optional).
- `-o, --bus-off`: Bus-off indicator GPIO pin (optional).
### `twai_deinit`
Stops and de-initializes the TWAI driver.
**Usage:**
`twai_deinit <controller>`
### `twai_send`
Sends a standard, extended, RTR, or TWAI-FD frame.
**Usage:**
`twai_send <controller> <frame_str>`
**Frame Formats:**
- **Standard:** `123#DEADBEEF` (11-bit ID)
- **Extended:** `12345678#CAFEBABE` (29-bit ID)
- **RTR:** `456#R` or `456#R8` (Remote Transmission Request)
- **TWAI-FD:** `123##{flags}{data}` (FD-capable chips only)
- **flags**: single hex nibble `0..F`
- bit0 (`0x1`) = BRS (Bit Rate Switch, accelerate data phase)
- bit1 (`0x2`) = ESI (Error State Indicator)
- other bits reserved (set to 0)
- **data**: up to 64 bytes (0..64) of hex pairs, optional `.` separators allowed (e.g. `11.22.33`)
- example: `123##1DEADBEEF` (BRS enabled, data = DE AD BE EF)
### `twai_dump`
Monitors TWAI bus messages with filtering and candump-style output. This command runs in the background.
**Usage:**
- `twai_dump [-t <mode>] <controller>[,filter...]`
- `twai_dump <controller> --stop`
- **Options:**
- `-t <mode>`: Timestamp mode. Output format is `(seconds.microseconds)` with 6-digit microsecond precision, e.g. `(1640995200.890123)`.
- `a`: Absolute time (esp_timer microseconds since boot)
- `d`: Delta time between frames (time since previous frame)
- `z`: Zero-relative time from start (time since dump started)
- `n`: No timestamp (default)
- `--stop`: Stop monitoring the specified controller.
**Filter Formats:**
- `id:mask`: Mask filter (e.g., `123:7FF`).
- `low-high`: Range filter (e.g., `100-200`, FD-capable chips only).
- Multiple filters can be combined with commas (e.g., `twai0,123:7FF,100-200`).
### `twai_info`
Displays the TWAI controller's configuration and real-time status.
**Usage:**
`twai_info <controller>`
**Output Includes:**
- Status (Stopped, Running, Bus-Off)
- Node State (Error Active, Error Passive, etc.)
- TX/RX Error Counters
- Bitrate (Arbitration and Data)
- Configured GPIOs and operational modes.
### `twai_recover`
Initiates recovery for a controller that is in the Bus-Off state.
**Usage:**
`twai_recover <controller> [-t <ms>]`
**Options:**
- `-t <ms>`: Recovery timeout.
- `-1`: Block indefinitely until recovery completes (default).
- `0`: Asynchronous recovery (returns immediately).
- `>0`: Timeout in milliseconds.
**Notes:**
- Recovery only works when the controller is in Bus-Off state
- Use `twai_info <controller>` to check current node state
- Recovery may fail if bus conditions are still problematic
- In async mode (timeout=0), use `twai_info` to monitor recovery progress
**Typical Command Sequence:**
1. `twai_init` - Initialize controller
2. `twai_info` - Check status
3. `twai_dump` - Start monitoring (optional)
4. `twai_send` - Send frames
5. `twai_recover` - Recover from errors (if needed)
6. `twai_deinit` - Cleanup
Basic usage example:
```bash
# Initialize controller 0 (bitrate 500 kbps, specify TX/RX pins)
twai> twai_init twai0 -b 500000 -t 4 -r 5
# Display controller information
twai> twai_info twai0
TWAI0 Status: Running
Node State: Error Active
Error Counters: TX=0, RX=0
Bitrate: 500000 bps
GPIOs: TX=GPIO4, RX=GPIO5
# Send standard frame on controller 0
twai> twai_send twai0 123#DEADBEEF
# Start monitoring controller 0 (accept all frames)
twai> twai_dump twai0
# Example received frame display (with default no timestamps)
twai0 123 [4] DE AD BE EF
```
### FD-Capable Chips Example
```bash
# Initialize controller 0 with TWAI-FD enabled
twai> twai_init twai0 -b 1000000 -t 4 -r 5 -B 2000000
twai> twai_info twai0
TWAI0 Status: Running
Node State: Error Active
Error Counters: TX=0, RX=0
Bitrate: 1000000 bps (FD: 2000000 bps)
GPIOs: TX=GPIO4, RX=GPIO5
# Send FD frame with BRS on controller 0
twai> twai_send twai0 456##1DEADBEEFCAFEBABE1122334455667788
```
### PC Environment Setup (For Full Testing)
To test bidirectional communication between ESP32 and PC, set up a SocketCAN environment on Ubuntu:
#### Prerequisites
- Ubuntu 18.04 or later
- USB-to-CAN adapter (PEAK PCAN-USB recommended)
- sudo access for network interface configuration
#### Quick Setup
1. **Install CAN utilities:**
```bash
sudo apt update
sudo apt install -y can-utils
# Verify installation
candump --help
cansend --help
```
2. **Configure CAN interface:**
```bash
# Classic CAN setup (500 kbps)
sudo ip link set can0 up type can bitrate 500000
# CAN-FD setup (1M arbitration, 4M data) - requires FD-capable adapter
sudo ip link set can0 up type can bitrate 1000000 dbitrate 4000000 fd on
# Verify interface
ip -details link show can0
```
3. **Test PC setup:**
```bash
# Terminal 1: Monitor
candump can0
# Terminal 2: Send test frame
cansend can0 123#DEADBEEF
# Send FD frame (if FD adapter available)
cansend can0 456##1DEADBEEFCAFEBABE
```
### Bidirectional Testing
Once both PC and ESP32 are set up:
**ESP32 to PC:**
```bash
# PC: Start monitoring
candump can0
# ESP32: Initialize controller 0 and send frame
twai> twai_init twai0 -t 4 -r 5
twai> twai_send twai0 123#DEADBEEF
# PC shows: can0 123 [4] DE AD BE EF
# ESP32 shows: twai0 123 [4] DE AD BE EF
```
**PC to ESP32:**
```bash
# ESP32: Start monitoring controller 0
twai> twai_dump twai0
# PC: Send frame
cansend can0 456#CAFEBABE
# ESP32 shows: twai0 456 [4] CA FE BA BE
# Stop monitoring
twai> twai_dump twai0 --stop
```
## Advanced Features
### Frame Formats
- **Standard frames:** `123#DEADBEEF` (11-bit ID)
- **Extended frames:** `12345678#CAFEBABE` (29-bit ID)
- **RTR frames:** `456#R8` (Remote Transmission Request)
- **TWAI-FD frames:** `123##1DEADBEEF` (FD with flags, FD-capable chips only)
- **Data separators:** `123#DE.AD.BE.EF` (dots ignored)
### TWAI-FD Frame Format and Examples (FD-capable chips only)
TWAI-FD frames use two `#` characters: `ID##{flags}{data}`.
- Flags are a single hex nibble (`0..F`). Bit meanings:
- `0x1` BRS: Enable Bit Rate Switch for the data phase
- `0x2` ESI: Error State Indicator
- Other bits are reserved (set to 0)
- Data payload supports up to 64 bytes. The driver maps the payload length to the proper DLC per CAN FD rules automatically.
- Payload hex pairs may include `.` separators for readability (ignored by the parser).
```bash
# FD frame without BRS (flags = 0) on controller 0
twai> twai_send twai0 123##0DEADBEEFCAFEBABE1122334455667788
# FD frame with BRS (flags = 1, higher data speed)
twai> twai_send twai0 456##1DEADBEEFCAFEBABE1122334455667788
# FD frame with ESI (flags = 2)
twai> twai_send twai0 789##2DEADBEEF
# FD frame with BRS + ESI (flags = 3)
twai> twai_send twai0 ABC##3DEADBEEF
# Large FD frame (up to 64 bytes)
twai> twai_send twai0 DEF##1000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F
```
### Filtering and Monitoring
```bash
# Monitor controller 0 (accept all frames)
twai> twai_dump twai0
twai0 123 [4] DE AD BE EF
twai0 456 [2] CA FE
twai0 789 [8] 11 22 33 44 55 66 77 88
# Monitor with absolute timestamps
twai> twai_dump -t a twai0
(1640995200.890123) twai0 123 [4] DE AD BE EF
(1640995200.895555) twai0 456 [2] CA FE
(1640995200.901000) twai0 789 [8] 11 22 33 44 55 66 77 88
# Monitor with delta timestamps (time between frames)
twai> twai_dump -t d twai0
(0.000000) twai0 123 [4] DE AD BE EF
(0.005432) twai0 456 [2] CA FE
(0.005445) twai0 789 [8] 11 22 33 44 55 66 77 88
# Monitor with zero-relative timestamps (from start of monitoring)
twai> twai_dump -t z twai0
(0.000000) twai0 123 [4] DE AD BE EF
(0.005432) twai0 456 [2] CA FE
(0.010877) twai0 789 [8] 11 22 33 44 55 66 77 88
# Monitor without timestamps (default)
twai> twai_dump -t n twai0
twai0 123 [4] DE AD BE EF
twai0 456 [2] CA FE
twai0 789 [8] 11 22 33 44 55 66 77 88
# Monitor controller 0 with exact ID filter (only receive ID=0x123)
twai> twai_dump twai0,123:7FF
Mask Filter 0: ID=0x00000123, mask=0x000007FF, STD
twai0 123 [4] DE AD BE EF
# Monitor controller 0 with ID range 0x100-0x10F (mask filter approach)
twai> twai_dump twai0,100:7F0
Mask Filter 0: ID=0x00000100, mask=0x000007F0, STD
# Monitor controller 0 with range filter (0xa to 0x15) - FD-capable chips only
twai> twai_dump twai0,a-15
Range Filter 0: 0x0000000a - 0x00000015, STD
# Monitor controller 0 with range filter (0x000 to 0x666)
twai> twai_dump twai0,000-666
Range Filter 0: 0x00000000 - 0x00000666, STD
# Monitor controller 0 with mixed filters (mask + range)
twai> twai_dump twai0,123:7FF,a-15
Mask Filter 0: ID=0x00000123, mask=0x000007FF, STD
Range Filter 0: 0x0000000a - 0x00000015, STD
# Monitor controller 0 with dual filters
twai> twai_dump twai0,020:7F0,013:7F8
Mask Filter 0: ID=0x00000020, mask=0x000007F0, STD
Mask Filter 1: ID=0x00000013, mask=0x000007F8, STD
# Monitor controller 0 with multiple range filters
twai> twai_dump twai0,10-20,100-200
Range Filter 0: 0x00000010 - 0x00000020, STD
Range Filter 1: 0x00000100 - 0x00000200, STD
# Monitor all frames on controller 0 (no filter)
twai> twai_dump twai0
# Stop monitoring controller 0
twai> twai_dump twai0 --stop
```
**Filter Types (FD-capable chips only):**
- **Mask filters:** `id:mask` format - Uses bitwise matching with configurable mask
- **Range filters:** `low-high` format - Hardware range filtering for ID ranges
- **Mixed filtering:** Combine both types in one command for maximum flexibility
### Testing Modes
**No Transceiver Mode (Testing without external hardware):**
```bash
# Use same GPIO for TX and RX with loopback and self-test
twai> twai_init twai0 -t 4 -r 4 --loopback --self-test
twai> twai_dump twai0
twai> twai_send twai0 123#DEADBEEF
# Frame appears immediately in dump output:
twai0 123 [4] DE AD BE EF
twai> twai_dump twai0 --stop
```
**Note:** This mode is perfect for testing TWAI functionality without external transceivers or wiring. The same GPIO is used for both TX and RX, and the combination of `--loopback` and `--self-test` flags ensures frames are properly transmitted and received internally.
**Loopback mode (with external transceiver):**
```bash
twai> twai_init twai0 -t 4 -r 5 --loopback
twai> twai_dump twai0
twai> twai_send twai0 123#54455354
# Frame appears immediately in dump output:
twai0 123 [4] 54 45 53 54
# Stop monitoring when done
twai> twai_dump twai0 --stop
# FD loopback test (FD-capable chips only)
twai> twai_init twai0 -t 4 -r 5 --loopback -B 2000000
twai> twai_dump twai0
twai> twai_send twai0 456##1DEADBEEFCAFEBABE1122334455667788
twai0 456 [16] DE AD BE EF CA FE BA BE 11 22 33 44 55 66 77 88 # FD frame (BRS)
# Stop monitoring
twai> twai_dump twai0 --stop
```
**Listen-only mode:**
```bash
twai> twai_init twai0 -t 4 -r 5 --listen
twai> twai_dump twai0
# Can receive but cannot send frames
# Stop with: twai_dump twai0 --stop
```
### Error Recovery and Diagnostics
**Bus-Off Recovery:**
The TWAI controller can enter a Bus-Off state due to excessive error conditions. Use the `twai_recover` command to initiate recovery:
```bash
# Basic recovery (default: block until complete)
twai> twai_recover twai0
I (1234) cmd_twai_core: Starting recovery from Bus-Off state...
I (1345) cmd_twai_core: Waiting for recovery to complete...
I (1456) cmd_twai_core: Recovery completed successfully in 100 ms
# Recovery with custom timeout
twai> twai_recover twai0 -t 5000
I (1234) cmd_twai_core: Starting recovery from Bus-Off state...
I (1345) cmd_twai_core: Waiting for recovery to complete...
I (1456) cmd_twai_core: Recovery completed successfully in 150 ms
# Asynchronous recovery (return immediately)
twai> twai_recover twai0 -t 0
I (1234) cmd_twai_core: Starting recovery from Bus-Off state...
I (1245) cmd_twai_core: Recovery initiated (async mode)
# If node is not in Bus-Off state
twai> twai_recover twai0
I (1234) cmd_twai_core: Recovery not needed - node is Error Active
```
**Enhanced Status Information:**
The `twai_info` command now displays real-time dynamic status information:
```bash
twai> twai_info twai0
TWAI0 Status: Running
Node State: Error Active
Error Counters: TX=0, RX=0
Bitrate: 500000 bps
GPIOs: TX=GPIO4, RX=GPIO5
```
**Status and Node State Interpretations:**
- **Status: Running**: Driver initialized and operational (not Bus-Off)
- **Status: Bus-Off**: Controller offline due to excessive errors, requires recovery
- **Status: Stopped**: Driver not initialized
- **Node State: Error Active**: Normal operation, can transmit and receive freely
- **Node State: Error Warning**: Warning level reached (error counters ≥ 96)
- **Node State: Error Passive**: Passive mode (error counters ≥ 128, limited transmission)
- **Node State: Bus Off**: Controller offline (TX error counter ≥ 256, requires recovery)
## Troubleshooting
**Restoring Default Configuration:**
Use `twai_deinit <controller>` followed by `twai_init <controller>` to reset the driver to default settings.
**Bus-Off Recovery Issues:**
- **"Node is not in Bus-Off state"**: Recovery only works when the controller is in Bus-Off state. Use `twai_info` to check current node state.
- **Recovery timeout**: If recovery takes longer than expected, try increasing the timeout with `-t` option (e.g., `-t 15000` for 15 seconds).
- **Recovery fails**: Check physical bus conditions, ensure proper termination and that other nodes are present to acknowledge recovery frames.
**TWAI-FD specific issues:**
- **"TWAI-FD frames not supported"**: Your chip doesn't support FD mode. Use chips like ESP32-C5
- **FD bitrate validation**: Ensure FD data bitrate is higher than arbitration bitrate
- **PC FD compatibility**: Ensure your PC CAN adapter supports FD mode
**PC CAN issues:**
```bash
# Reset PC interface
sudo ip link set can0 down
sudo ip link set can0 up type can bitrate 500000
# For FD mode
sudo ip link set can0 up type can bitrate 1000000 dbitrate 4000000 fd on
```
**No communication:**
- Verify bitrates match on both sides
- For FD: Ensure both sides support FD and have compatible transceivers
- Check physical connections and transceiver power
- GPIO pins must be specified in the twai_init command (common: TX=GPIO4, RX=GPIO5)
- For no-transceiver testing, use the same GPIO for both TX and RX with `--loopback --self-test` flags
- Ensure proper CAN bus termination (120Ω resistors)
- Use loopback mode to test ESP32 functionality independently
**Quick diagnostic with no-transceiver mode:**
```bash
# Test if basic TWAI functionality works
twai> twai_init twai0 -t 4 -r 4 --loopback --self-test
twai> twai_send twai0 123#DEADBEEF
# If this fails, check ESP-IDF installation and chip support
```
**Common Error Messages:**
```bash
# Controller ID missing
twai> twai_send 123#DEADBEEF
E (1234) cmd_twai_send: Controller ID is required
# Interface not initialized
twai> twai_send twai0 123#DEADBEEF
E (1234) cmd_twai_send: TWAI0 not initialized
# Invalid frame format
twai> twai_send twai0 123DEADBEEF
E (1456) cmd_twai_send: Frame string is required (format: 123#AABBCC or 12345678#AABBCC)
# Invalid controller ID
twai> twai_init twai5 -t 4 -r 5
E (1678) cmd_twai_core: Invalid controller ID
```

View File

@@ -0,0 +1,4 @@
idf_component_register(SRCS "cmd_twai_dump.c" "cmd_twai_send.c" "cmd_twai_core.c" "cmd_twai.c" "twai_utils_main.c"
"twai_utils_parser.c"
REQUIRES esp_driver_twai esp_timer esp_driver_gpio console
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,60 @@
menu "TWAI Configuration"
depends on SOC_TWAI_SUPPORTED
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
config EXAMPLE_ENABLE_TWAI_FD
bool "Enable TWAI-FD Support"
depends on SOC_TWAI_SUPPORT_FD
default false
help
Enable TWAI-FD (Flexible Data-rate) support.
Allows up to 64 bytes of data per frame and dual bit rates.
Only available on chips that support TWAI-FD.
config EXAMPLE_DEFAULT_BITRATE
int "Default Arbitration Bitrate"
default 500000
help
Default arbitration bitrate in bits per second (bps).
config EXAMPLE_DEFAULT_FD_BITRATE
int "Default FD Data Bitrate"
depends on EXAMPLE_ENABLE_TWAI_FD
default 1000000
help
Default data bitrate for TWAI-FD in bits per second (bps).
config EXAMPLE_TX_QUEUE_LEN
int "TX Queue Length"
range 1 100
default 10
help
Length of the transmission queue for TWAI messages.
config EXAMPLE_DUMP_QUEUE_SIZE
int "TWAI Dump Queue Size"
default 32
help
Size of the queue used to store received TWAI frames for dump task.
config EXAMPLE_DUMP_TASK_STACK_SIZE
int "TWAI Dump Task Stack Size"
default 4096
help
Stack size of the TWAI dump task.
config EXAMPLE_DUMP_TASK_PRIORITY
int "TWAI Dump Task Priority"
default 10
range 0 24
help
Priority of the TWAI dump task.
config EXAMPLE_DUMP_TASK_TIMEOUT_MS
int "TWAI Dump Task Timeout"
default 300
help
Timeout for the TWAI dump task.
endmenu

View File

@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "cmd_twai.h"
#include "esp_log.h"
#include "esp_console.h"
#include "cmd_twai_internal.h"
static const char *TAG = "cmd_twai";
twai_controller_ctx_t g_twai_controller_ctx[SOC_TWAI_CONTROLLER_NUM];
/* =============================================================================
* COMMAND REGISTRATION
* =============================================================================*/
twai_controller_ctx_t* get_controller_by_id(int controller_id)
{
if (controller_id < 0 || controller_id >= SOC_TWAI_CONTROLLER_NUM) {
ESP_LOGE(TAG, "Invalid controller ID: %d (valid range: 0-%d)",
controller_id, SOC_TWAI_CONTROLLER_NUM - 1);
return NULL;
}
return &g_twai_controller_ctx[controller_id];
}
void register_twai_commands(void)
{
register_twai_core_commands();
register_twai_send_commands();
register_twai_dump_commands();
ESP_LOGI(TAG, "TWAI commands registered successfully");
}
void unregister_twai_commands(void)
{
unregister_twai_dump_commands();
unregister_twai_send_commands();
unregister_twai_core_commands();
ESP_LOGI(TAG, "TWAI commands unregistered successfully");
}

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* =============================================================================
* MACRO DEFINITIONS
* =============================================================================*/
/**
* @brief Register TWAI commands with the console
*/
void register_twai_commands(void);
void unregister_twai_commands(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,749 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "argtable3/argtable3.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_err.h"
#include "esp_twai.h"
#include "esp_twai_onchip.h"
#include "cmd_twai_internal.h"
#include "esp_check.h"
#include "twai_utils_parser.h"
static const char *TAG = "cmd_twai_core";
/** @brief Command line arguments for twai_init command */
static struct {
struct arg_str *controller;
struct arg_int *rate;
struct arg_lit *loopback;
struct arg_lit *self_test;
struct arg_lit *listen;
struct arg_int *fd_rate;
struct arg_int *tx_gpio;
struct arg_int *rx_gpio;
struct arg_int *clk_out_gpio;
struct arg_int *bus_off_gpio;
struct arg_end *end;
} twai_init_args;
/** @brief Command line arguments for twai_deinit command */
static struct {
struct arg_str *controller;
struct arg_end *end;
} twai_deinit_args;
/** @brief Command line arguments for twai_info command */
static struct {
struct arg_str *controller;
struct arg_end *end;
} twai_info_args;
/** @brief Command line arguments for twai_recover command */
static struct {
struct arg_str *controller;
struct arg_int *timeout;
struct arg_end *end;
} twai_recover_args;
/**
* @brief State change callback for TWAI controller
*
* @param[in] handle TWAI node handle
* @param[in] edata Event data with state information
* @param[in] user_ctx Controller context pointer
*
* @return @c true if higher priority task woken, @c false otherwise
*/
static bool twai_state_change_callback(twai_node_handle_t handle, const twai_state_change_event_data_t *edata, void *user_ctx)
{
ESP_UNUSED(handle);
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)user_ctx;
bool higher_task_awoken = false;
if (edata->new_sta == TWAI_ERROR_BUS_OFF) {
int id = (int)(controller - g_twai_controller_ctx);
ESP_EARLY_LOGW(TAG, "TWAI%d entered Bus-Off state, use 'twai_recover twai%d' to recover", id, id);
} else if (edata->old_sta == TWAI_ERROR_BUS_OFF && edata->new_sta == TWAI_ERROR_ACTIVE) {
int id = (int)(controller - g_twai_controller_ctx);
ESP_EARLY_LOGI(TAG, "TWAI%d recovered from Bus-Off state", id);
}
return higher_task_awoken;
}
/**
* @brief Create and configure a TWAI controller
*
* @param[in] controller Controller context to start
*
* @return TWAI node handle on success, @c NULL on failure
*/
static twai_node_handle_t twai_start(twai_controller_ctx_t *controller)
{
twai_node_handle_t res = NULL;
esp_err_t ret = ESP_OK;
twai_core_ctx_t *ctx = &controller->core_ctx;
/* Check if the TWAI driver is already running */
if (atomic_load(&ctx->is_initialized)) {
ESP_LOGD(TAG, "TWAI driver is already running. Please stop it first.");
return controller->node_handle;
}
if (controller->node_handle) {
ESP_LOGW(TAG, "Cleaning up old TWAI node handle");
ret = twai_node_delete(controller->node_handle);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to delete old TWAI node: %s", esp_err_to_name(ret));
}
controller->node_handle = NULL;
}
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
if (ctx->driver_config.data_timing.bitrate > 0) {
if (ctx->driver_config.data_timing.bitrate < ctx->driver_config.bit_timing.bitrate) {
ESP_LOGW(TAG, "TWAI-FD disabled: data bitrate (%" PRIu32 ") must be higher than arbitration bitrate (%" PRIu32 ")",
ctx->driver_config.data_timing.bitrate, ctx->driver_config.bit_timing.bitrate);
ctx->driver_config.data_timing.bitrate = 0; /* Disable FD */
} else {
ESP_LOGD(TAG, "TWAI-FD enabled: Arbitration=%" PRIu32 " bps, Data=%" PRIu32 " bps",
ctx->driver_config.bit_timing.bitrate, ctx->driver_config.data_timing.bitrate);
}
}
#endif
ESP_GOTO_ON_ERROR(twai_new_node_onchip(&(ctx->driver_config), &(controller->node_handle)),
err, TAG, "Failed to create TWAI node");
res = controller->node_handle;
/* Register event callbacks including our state change callback */
ctx->driver_cbs.on_state_change = twai_state_change_callback;
ESP_GOTO_ON_ERROR(twai_node_register_event_callbacks(controller->node_handle, &(ctx->driver_cbs), controller),
err_node, TAG, "Failed to register callbacks");
ESP_GOTO_ON_ERROR(twai_node_enable(controller->node_handle),
err_node, TAG, "Failed to enable node");
atomic_store(&ctx->is_initialized, true);
return res;
err_node:
if (controller->node_handle) {
ret = twai_node_delete(controller->node_handle);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to delete TWAI node during error cleanup: %s", esp_err_to_name(ret));
}
controller->node_handle = NULL;
}
err:
return NULL;
}
/**
* @brief Stop a TWAI controller, disable the node and delete it
*
* @param[in] controller Controller context to stop
*
* @return @c ESP_OK on success, error code on failure
*/
static esp_err_t twai_stop(twai_controller_ctx_t *controller)
{
twai_core_ctx_t *ctx = &controller->core_ctx;
esp_err_t ret = ESP_OK;
if (!atomic_load(&ctx->is_initialized)) {
ESP_LOGI(TAG, "TWAI not running");
return ret;
}
if (controller->node_handle) {
ret = twai_node_disable(controller->node_handle);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to disable TWAI node: %s", esp_err_to_name(ret));
ret = twai_node_delete(controller->node_handle);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to delete TWAI node: %s", esp_err_to_name(ret));
controller->node_handle = NULL;
}
atomic_store(&ctx->is_initialized, false);
return ret;
}
/**
* @brief Initialize and start TWAI controller `twai_init twai0 -t 4 -r 5 -b 500000` command handler
*
* @param[in] argc Argument count
* @param[in] argv Argument vector
*
* @return @c ESP_OK on success, error code on failure
*
* @note Parses GPIO, timing, and mode configuration. -t=TX GPIO, -r=RX GPIO, -b=bitrate
*/
static int twai_init_handler(int argc, char **argv)
{
esp_err_t ret = ESP_OK;
int controller_id;
twai_controller_ctx_t *controller;
twai_core_ctx_t *ctx;
int tx_gpio, rx_gpio, clk_gpio, bus_off_gpio;
int nerrors = arg_parse(argc, argv, (void **)&twai_init_args);
if (nerrors != 0) {
arg_print_errors(stderr, twai_init_args.end, argv[0]);
return ESP_ERR_INVALID_ARG;
}
/* Check required arguments first */
if (twai_init_args.controller->count == 0) {
ESP_LOGE(TAG, "Controller argument is required (e.g., twai0, twai1)");
return ESP_ERR_INVALID_ARG;
}
if (twai_init_args.tx_gpio->count == 0) {
ESP_LOGE(TAG, "TX GPIO argument is required (-t <gpio>)");
return ESP_ERR_INVALID_ARG;
}
if (twai_init_args.rx_gpio->count == 0) {
ESP_LOGE(TAG, "RX GPIO argument is required (-r <gpio>)");
return ESP_ERR_INVALID_ARG;
}
controller_id = parse_controller_string(twai_init_args.controller->sval[0]);
ret = (controller_id >= 0) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid controller ID");
controller = get_controller_by_id(controller_id);
ret = (controller != NULL) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Controller %d not found", controller_id);
ctx = &controller->core_ctx;
ret = (!atomic_load(&ctx->is_initialized)) ? ESP_OK : ESP_ERR_INVALID_STATE;
ESP_GOTO_ON_ERROR(ret, err, TAG, "TWAI%d already running", controller_id);
/* Configure TX GPIO */
tx_gpio = twai_init_args.tx_gpio->ival[0];
ret = (tx_gpio >= 0 && GPIO_IS_VALID_OUTPUT_GPIO(tx_gpio)) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid TX GPIO: %d", tx_gpio);
ctx->driver_config.io_cfg.tx = tx_gpio;
ESP_LOGI(TAG, "TX GPIO set to %d", tx_gpio);
/* Configure RX GPIO */
rx_gpio = twai_init_args.rx_gpio->ival[0];
ret = GPIO_IS_VALID_GPIO(rx_gpio) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid RX GPIO: %d", rx_gpio);
ctx->driver_config.io_cfg.rx = rx_gpio;
ESP_LOGI(TAG, "RX GPIO set to %d", rx_gpio);
/* Configure optional clock output GPIO */
if (twai_init_args.clk_out_gpio->count > 0) {
clk_gpio = twai_init_args.clk_out_gpio->ival[0];
if (clk_gpio >= 0) {
ret = GPIO_IS_VALID_OUTPUT_GPIO(clk_gpio) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid CLK out GPIO: %d", clk_gpio);
ctx->driver_config.io_cfg.quanta_clk_out = clk_gpio;
ESP_LOGI(TAG, "Clock output GPIO set to %d", clk_gpio);
}
} else {
ctx->driver_config.io_cfg.quanta_clk_out = -1;
ESP_LOGI(TAG, "Clock output disabled");
}
/* Configure optional bus-off indicator GPIO */
if (twai_init_args.bus_off_gpio->count > 0) {
bus_off_gpio = twai_init_args.bus_off_gpio->ival[0];
if (bus_off_gpio >= 0) {
ret = GPIO_IS_VALID_OUTPUT_GPIO(bus_off_gpio) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid bus-off GPIO: %d", bus_off_gpio);
ctx->driver_config.io_cfg.bus_off_indicator = bus_off_gpio;
ESP_LOGI(TAG, "Bus-off indicator GPIO set to %d", bus_off_gpio);
}
} else {
ctx->driver_config.io_cfg.bus_off_indicator = -1;
ESP_LOGI(TAG, "Bus-off indicator disabled");
}
/* Verify required IO configuration */
ret = (ctx->driver_config.io_cfg.tx >= 0 && ctx->driver_config.io_cfg.rx >= 0) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Both TX and RX GPIO must be configured");
/* Update timing configuration */
if (twai_init_args.rate->count > 0) {
ctx->driver_config.bit_timing.bitrate = twai_init_args.rate->ival[0];
}
ESP_LOGI(TAG, "Bitrate set to %" PRIu32 " bps", ctx->driver_config.bit_timing.bitrate);
ctx->driver_config.flags.enable_loopback = (twai_init_args.loopback->count > 0);
ctx->driver_config.flags.enable_self_test = (twai_init_args.self_test->count > 0);
ctx->driver_config.flags.enable_listen_only = (twai_init_args.listen->count > 0);
if (ctx->driver_config.flags.enable_loopback) {
ESP_LOGI(TAG, "Loopback mode enabled");
}
if (ctx->driver_config.flags.enable_self_test) {
ESP_LOGI(TAG, "Self-test mode enabled");
}
if (ctx->driver_config.flags.enable_listen_only) {
ESP_LOGI(TAG, "Listen-only mode enabled");
}
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
if (twai_init_args.fd_rate->count > 0) {
ctx->driver_config.data_timing.bitrate = twai_init_args.fd_rate->ival[0];
} else {
ctx->driver_config.data_timing.bitrate = CONFIG_EXAMPLE_DEFAULT_FD_BITRATE;
}
#else
ctx->driver_config.data_timing.bitrate = 0; /* FD disabled */
#endif
ESP_LOGI(TAG, "FD bitrate set to %" PRIu32, ctx->driver_config.data_timing.bitrate);
/* Start TWAI controller */
controller->node_handle = twai_start(controller);
ret = (controller->node_handle != NULL) ? ESP_OK : ESP_FAIL;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to start TWAI controller");
return ESP_OK;
err:
return ret;
}
/**
* @brief Stop and deinitialize TWAI controller command handler
*
* @param[in] argc Argument count
* @param[in] argv Argument vector
*
* @return @c ESP_OK on success, error code on failure
*
* @note Stops dump monitoring and controller
*/
static int twai_deinit_handler(int argc, char **argv)
{
int controller_id;
esp_err_t ret = ESP_OK;
int nerrors = arg_parse(argc, argv, (void **)&twai_deinit_args);
if (nerrors != 0) {
arg_print_errors(stderr, twai_deinit_args.end, argv[0]);
return ESP_ERR_INVALID_ARG;
}
if (twai_deinit_args.controller->count == 0) {
ESP_LOGE(TAG, "Controller ID is required");
return ESP_ERR_INVALID_ARG;
}
controller_id = parse_controller_string(twai_deinit_args.controller->sval[0]);
ret = (controller_id >= 0) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Invalid controller ID");
twai_controller_ctx_t* controller = get_controller_by_id(controller_id);
ret = (controller != NULL) ? ESP_OK : ESP_ERR_INVALID_ARG;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Controller %d not found", controller_id);
twai_core_ctx_t *ctx = &controller->core_ctx;
if (!atomic_load(&ctx->is_initialized)) {
ESP_LOGI(TAG, "TWAI%d not running", controller_id);
return ESP_OK;
}
/* Auto-stop dump monitoring if it's running */
esp_err_t dump_ret = twai_dump_stop_internal(controller_id);
if (dump_ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to stop dump for controller %d: %s", controller_id, esp_err_to_name(dump_ret));
}
ret = twai_stop(controller);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to stop TWAI%d: %s", controller_id, esp_err_to_name(ret));
return ESP_OK;
err:
return ret;
}
/**
* @brief Recover from Bus-Off state `twai_recover twai0` command handler
*
* @param[in] argc Argument count
* @param[in] argv Argument vector
*
* @return @c ESP_OK on success, error code on failure
*
* @note Supports async, blocking, and timeout modes
*/
static int twai_recover_handler(int argc, char **argv)
{
int controller_id;
esp_err_t ret = ESP_OK;
int nerrors = arg_parse(argc, argv, (void **)&twai_recover_args);
if (nerrors != 0) {
arg_print_errors(stderr, twai_recover_args.end, argv[0]);
return ESP_ERR_INVALID_ARG;
}
if (twai_recover_args.controller->count == 0) {
ESP_LOGE(TAG, "Controller ID is required");
return ESP_ERR_INVALID_ARG;
}
controller_id = parse_controller_string(twai_recover_args.controller->sval[0]);
ESP_GOTO_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, err, TAG, "Invalid controller ID");
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
ESP_GOTO_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, err, TAG, "Controller %d not found", controller_id);
if (!controller->node_handle) {
ESP_LOGE(TAG, "TWAI%d not initialized", controller_id);
return ESP_ERR_INVALID_STATE;
}
/* timeout: -1 = block, 0 = async, >0 = timeout (ms) */
int32_t timeout_ms = -1;
if (twai_recover_args.timeout->count > 0) {
timeout_ms = twai_recover_args.timeout->ival[0];
}
if (timeout_ms < -1) {
ESP_LOGE(TAG, "Invalid timeout value: %d (must be -1, 0 or positive)", timeout_ms);
return ESP_ERR_INVALID_ARG;
}
twai_node_status_t node_status;
ret = twai_node_get_info(controller->node_handle, &node_status, NULL);
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to get node%d status: %s", controller_id, esp_err_to_name(ret));
ret = (node_status.state == TWAI_ERROR_BUS_OFF) ? ESP_OK : ESP_ERR_INVALID_STATE;
ESP_GOTO_ON_ERROR(ret, err, TAG, "Recovery not needed - node is %s", twai_state_to_string(node_status.state));
ESP_LOGI(TAG, "Starting recovery from Bus-Off state...");
ret = twai_node_recover(controller->node_handle);
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to start recovery: %s", esp_err_to_name(ret));
if (timeout_ms == 0) {
ESP_LOGI(TAG, "Recovery initiated (async mode)");
return ESP_OK;
}
ESP_LOGI(TAG, "Waiting for recovery to complete...");
uint32_t elapsed_ms = 0;
const uint32_t check_interval_ms = 100;
uint32_t limit_ms = (timeout_ms < 0) ? UINT32_MAX : (uint32_t)timeout_ms;
while (elapsed_ms < limit_ms) {
vTaskDelay(pdMS_TO_TICKS(check_interval_ms));
elapsed_ms += check_interval_ms;
ret = twai_node_get_info(controller->node_handle, &node_status, NULL);
ESP_GOTO_ON_ERROR(ret, err, TAG, "Failed to check recovery status: %s", esp_err_to_name(ret));
if (node_status.state == TWAI_ERROR_ACTIVE) {
ESP_LOGI(TAG, "Recovery completed successfully in %" PRIu32 " ms", elapsed_ms);
return ESP_OK;
}
if (elapsed_ms % 1000 == 0) {
ESP_LOGI(TAG, "Recovery in progress... (state: %s, elapsed: %" PRIu32 " ms)",
twai_state_to_string(node_status.state), elapsed_ms);
}
}
ESP_LOGI(TAG, "Recovery timeout after %" PRIu32 " ms (current state: %s)",
limit_ms, twai_state_to_string(node_status.state));
return ESP_ERR_TIMEOUT;
err:
return ret;
}
/**
* @brief Display controller information `twai_info twai0` command handler
*
* @param[in] argc Argument count
* @param[in] argv Argument vector
*
* @return @c ESP_OK on success, error code on failure
*
* @note Shows status, configuration, and error counters
*/
static int twai_info_handler(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&twai_info_args);
if (nerrors != 0) {
arg_print_errors(stderr, twai_info_args.end, argv[0]);
return ESP_ERR_INVALID_ARG;
}
if (twai_info_args.controller->count == 0) {
ESP_LOGE(TAG, "Controller ID is required");
return ESP_ERR_INVALID_ARG;
}
int controller_id = parse_controller_string(twai_info_args.controller->sval[0]);
if (controller_id < 0) {
return ESP_ERR_INVALID_ARG;
}
twai_controller_ctx_t* controller = get_controller_by_id(controller_id);
if (!controller) {
return ESP_ERR_INVALID_ARG;
}
twai_core_ctx_t *ctx = &controller->core_ctx;
char tx_gpio_buf[16], rx_gpio_buf[16];
if (!atomic_load(&ctx->is_initialized)) {
printf("TWAI%d Status: Stopped\n", controller_id);
} else if (controller->node_handle) {
twai_node_status_t node_status;
if (twai_node_get_info(controller->node_handle, &node_status, NULL) == ESP_OK &&
node_status.state == TWAI_ERROR_BUS_OFF) {
printf("TWAI%d Status: Bus-Off\n", controller_id);
} else {
printf("TWAI%d Status: Running\n", controller_id);
}
} else {
printf("TWAI%d Status: Initializing\n", controller_id);
}
/* Node status and error counters */
if (controller->node_handle && atomic_load(&ctx->is_initialized)) {
twai_node_status_t node_status;
esp_err_t ret = twai_node_get_info(controller->node_handle, &node_status, NULL);
if (ret == ESP_OK) {
printf("Node State: %s\n", twai_state_to_string(node_status.state));
printf("Error Counters: TX=%u, RX=%u\n", node_status.tx_error_count, node_status.rx_error_count);
if (node_status.state == TWAI_ERROR_BUS_OFF) {
printf(" ! Use 'twai_recover twai%d' to recover from Bus-Off\n", controller_id);
}
} else {
printf("Node State: Unable to read status\n");
}
} else {
printf("Node State: Not initialized\n");
}
/* Configuration */
printf("Bitrate: %" PRIu32 " bps", ctx->driver_config.bit_timing.bitrate);
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
if (ctx->driver_config.data_timing.bitrate > 0) {
printf(" (FD: %" PRIu32 " bps)", ctx->driver_config.data_timing.bitrate);
}
#endif
printf("\n");
format_gpio_pin(ctx->driver_config.io_cfg.tx, tx_gpio_buf, sizeof(tx_gpio_buf));
format_gpio_pin(ctx->driver_config.io_cfg.rx, rx_gpio_buf, sizeof(rx_gpio_buf));
printf("GPIOs: TX=%s, RX=%s\n", tx_gpio_buf, rx_gpio_buf);
/* Special modes (only if not normal) */
if (ctx->driver_config.flags.enable_loopback ||
ctx->driver_config.flags.enable_self_test ||
ctx->driver_config.flags.enable_listen_only) {
printf("Modes: ");
bool first = true;
if (ctx->driver_config.flags.enable_self_test) {
printf("Self-Test");
first = false;
}
if (ctx->driver_config.flags.enable_loopback) {
if (!first) {
printf(", ");
}
printf("Loopback");
first = false;
}
if (ctx->driver_config.flags.enable_listen_only) {
if (!first) {
printf(", ");
}
printf("Listen-Only");
}
printf("\n");
}
return ESP_OK;
}
/**
* @brief Register TWAI core commands with console
*
* @note Initializes controllers and registers all command handlers
*/
void register_twai_core_commands(void)
{
// Initialize all controllers
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
twai_controller_ctx_t* controller = &g_twai_controller_ctx[i];
twai_core_ctx_t *ctx = &controller->core_ctx;
ctx->driver_config = (twai_onchip_node_config_t) {
.io_cfg = {
.tx = GPIO_NUM_NC,
.rx = GPIO_NUM_NC,
.quanta_clk_out = GPIO_NUM_NC,
.bus_off_indicator = GPIO_NUM_NC,
},
.clk_src = 0,
.bit_timing = {
.bitrate = CONFIG_EXAMPLE_DEFAULT_BITRATE,
.sp_permill = 0,
.ssp_permill = 0,
},
.data_timing = {
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
.bitrate = CONFIG_EXAMPLE_DEFAULT_FD_BITRATE,
.sp_permill = 0,
.ssp_permill = 700,
#else
.bitrate = 0,
.sp_permill = 0,
.ssp_permill = 0,
#endif
},
.fail_retry_cnt = -1,
.tx_queue_depth = CONFIG_EXAMPLE_TX_QUEUE_LEN,
.intr_priority = 0,
.flags = {
.enable_self_test = false, /* Default: Self-test disabled */
.enable_loopback = false, /* Default: Loopback disabled */
.enable_listen_only = false, /* Default: Listen-only disabled */
.no_receive_rtr = 0,
},
};
atomic_init(&ctx->is_initialized, false);
ESP_LOGD(TAG, "Default config set for TWAI%d (TX=%d, RX=%d).",
i, ctx->driver_config.io_cfg.tx, ctx->driver_config.io_cfg.rx);
}
/* Register command arguments */
twai_init_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1, etc.)");
twai_init_args.tx_gpio = arg_int1("t", "tx", "<gpio>", "TX GPIO pin number (required, e.g., 4)");
twai_init_args.rx_gpio = arg_int1("r", "rx", "<gpio>", "RX GPIO pin number (required, e.g., 5)");
twai_init_args.rate = arg_int0("b", "bitrate", "<bps>", "Arbitration bitrate in bps (default: 500000)");
twai_init_args.loopback = arg_lit0(NULL, "loopback", "Enable loopback mode for testing");
twai_init_args.self_test = arg_lit0(NULL, "self-test", "Enable self-test mode for testing");
twai_init_args.listen = arg_lit0(NULL, "listen", "Enable listen-only mode (no transmission)");
twai_init_args.fd_rate = arg_int0("B", "fd-bitrate", "<bps>", "TWAI-FD data bitrate in bps (optional)");
twai_init_args.clk_out_gpio = arg_int0("c", "clk-out", "<gpio>", "Clock output GPIO pin (optional)");
twai_init_args.bus_off_gpio = arg_int0("o", "bus-off", "<gpio>", "Bus-off indicator GPIO pin (optional)");
twai_init_args.end = arg_end(20);
twai_deinit_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1)");
twai_deinit_args.end = arg_end(20);
twai_info_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1)");
twai_info_args.end = arg_end(20);
twai_recover_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (twai0, twai1)");
twai_recover_args.timeout = arg_int0("t", "timeout", "<ms>", "Recovery timeout in milliseconds (default: -1=block)\n -1 = block until complete\n 0 = async (return immediately)\n >0 = timeout in ms");
twai_recover_args.end = arg_end(20);
/* Register commands */
const esp_console_cmd_t twai_init_cmd = {
.command = "twai_init",
.help = "Initialize and start the TWAI driver\n"
"Usage: twai_init <controller> -t <tx_gpio> -r <rx_gpio> [options]\n"
"Example: twai_init twai0 -t 4 -r 5 -b 500000\n"
"Example: twai_init twai0 -t 4 -r 5 --loopback --self-test",
.hint = NULL,
.func = &twai_init_handler,
.argtable = &twai_init_args
};
const esp_console_cmd_t twai_deinit_cmd = {
.command = "twai_deinit",
.help = "Stop and deinitialize the TWAI driver",
.hint = NULL,
.func = &twai_deinit_handler,
.argtable = &twai_deinit_args
};
const esp_console_cmd_t twai_info_cmd = {
.command = "twai_info",
.help = "Display TWAI controller information and status",
.hint = NULL,
.func = &twai_info_handler,
.argtable = &twai_info_args
};
const esp_console_cmd_t twai_recover_cmd = {
.command = "twai_recover",
.help = "Recover TWAI controller from Bus-Off error state\n"
"Usage:\n"
" twai_recover <controller> # Block until complete (default)\n"
" twai_recover <controller> -t 0 # Async recovery\n"
" twai_recover <controller> -t 5000 # 5 second timeout\n"
"\n"
"Examples:\n"
" twai_recover twai0 # Block until complete\n"
" twai_recover twai0 -t 0 # Async recovery\n"
" twai_recover twai1 -t 15000 # 15 second timeout",
.hint = NULL,
.func = &twai_recover_handler,
.argtable = &twai_recover_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_init_cmd));
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_deinit_cmd));
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_info_cmd));
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_recover_cmd));
}
/**
* @brief Unregister TWAI core commands and cleanup resources
*/
void unregister_twai_core_commands(void)
{
esp_err_t ret = ESP_OK;
/* Cleanup all controllers */
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
twai_core_ctx_t *ctx = &controller->core_ctx;
/* Stop dump and other modules first to avoid callback issues */
ret = twai_dump_stop_internal(i);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to stop dump for controller %d: %s", i, esp_err_to_name(ret));
}
/* Disable and delete TWAI node if it exists */
if (controller->node_handle) {
if (atomic_load(&ctx->is_initialized)) {
ret = twai_node_disable(controller->node_handle);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to disable TWAI node for controller %d: %s", i, esp_err_to_name(ret));
}
}
ret = twai_node_delete(controller->node_handle);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to delete TWAI node for controller %d: %s", i, esp_err_to_name(ret));
} else {
ESP_LOGD(TAG, "Deleted TWAI node for controller %d", i);
}
controller->node_handle = NULL;
}
/* Clear initialization flag */
atomic_store(&ctx->is_initialized, false);
/* Clear callbacks */
memset(&ctx->driver_cbs, 0, sizeof(ctx->driver_cbs));
}
ESP_LOGI(TAG, "TWAI core commands unregistered and resources cleaned up");
}

View File

@@ -0,0 +1,569 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <string.h>
#include <stdatomic.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "argtable3/argtable3.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_err.h"
#include "esp_twai.h"
#include "esp_twai_onchip.h"
#include "cmd_twai_internal.h"
#include "esp_timer.h"
#include "esp_check.h"
#include "twai_utils_parser.h"
#define DUMP_OUTPUT_LINE_SIZE 128
/**
* @brief Structure for queuing received frames with embedded buffer
*/
typedef struct {
twai_frame_t frame; /**< TWAI frame with embedded buffer */
int64_t timestamp_us; /**< Frame timestamp in microseconds */
uint8_t buffer[TWAI_FRAME_BUFFER_SIZE]; /**< Frame data buffer (supports both TWAI and TWAI-FD) */
} rx_queue_item_t;
/** @brief Command line arguments structure */
static struct {
struct arg_str *controller_filter; /**< Format: <controller>[,<id>:<mask>[,<id>:<mask>...]] */
struct arg_lit *stop; /**< Stop option: --stop */
struct arg_str *timestamp; /**< Timestamp mode: -t <mode> */
struct arg_end *end;
} twai_dump_args;
static const char *TAG = "cmd_twai_dump";
/**
* @brief Parse TWAI filters from a string and configure the controller
*
* @param[in] filter_str Filter string to parse
* @param[in] controller Controller context to configure
* @param[out] mask_count_out Number of mask filters configured
*
* @return ESP_OK on success, error code on failure
*/
static esp_err_t parse_twai_filters(const char *filter_str, twai_controller_ctx_t *controller, int *mask_count, int *range_count)
{
int mask_idx = 0;
#ifdef SOC_TWAI_RANGE_FILTER_NUM
int range_idx = 0;
#endif
size_t slen = strlen(filter_str);
if (filter_str && slen > 0) {
const char *start = filter_str;
const char *comma;
while (start && *start) {
comma = strchr(start, ',');
size_t tok_len = comma ? (size_t)(comma - start) : strlen(start);
if (tok_len == 0) {
start = comma ? comma + 1 : NULL;
continue;
}
uint32_t lhs, rhs;
size_t lhs_chars, rhs_chars;
bool is_mask_filter = false;
#if SOC_TWAI_RANGE_FILTER_NUM
bool is_range_filter = false;
#endif
/* Try mask filter first: "id:mask" */
if (parse_pair_token(start, tok_len, ':', &lhs, &lhs_chars, &rhs, &rhs_chars) == PARSE_OK) {
ESP_RETURN_ON_FALSE(mask_idx < SOC_TWAI_MASK_FILTER_NUM, ESP_ERR_INVALID_ARG, TAG,
"Too many mask filters (max %d)", SOC_TWAI_MASK_FILTER_NUM);
is_mask_filter = true;
}
#if SOC_TWAI_RANGE_FILTER_NUM
/* Try range filter: "low-high" */
else if (parse_pair_token(start, tok_len, '-', &lhs, &lhs_chars, &rhs, &rhs_chars) == PARSE_OK) {
ESP_RETURN_ON_FALSE(range_idx < SOC_TWAI_RANGE_FILTER_NUM, ESP_ERR_INVALID_ARG, TAG,
"Too many range filters (max %d)", SOC_TWAI_RANGE_FILTER_NUM);
is_range_filter = true;
}
#endif
else {
ESP_LOGE(TAG, "Invalid filter token: %.*s", (int)tok_len, start);
return ESP_ERR_INVALID_ARG;
}
/* Common processing: determine if extended frame and validate */
bool is_ext = (lhs_chars > TWAI_STD_ID_CHAR_LEN) || (rhs_chars > TWAI_STD_ID_CHAR_LEN) ||
(lhs > TWAI_STD_ID_MASK) || (rhs > TWAI_STD_ID_MASK);
uint32_t id_domain = is_ext ? TWAI_EXT_ID_MASK : TWAI_STD_ID_MASK;
/* Validate values are within domain */
ESP_RETURN_ON_FALSE(lhs <= id_domain && rhs <= id_domain, ESP_ERR_INVALID_ARG, TAG,
"Filter values exceed %s domain", is_ext ? "extended" : "standard");
if (is_mask_filter) {
/* Configure mask filter */
twai_mask_filter_config_t *cfg = &controller->dump_ctx.mask_filter_configs[mask_idx];
cfg->id = lhs;
cfg->mask = rhs;
cfg->is_ext = is_ext;
ESP_LOGD(TAG, "Parsed mask filter %d: ID=0x%08" PRIX32 ", mask=0x%08" PRIX32 " (%s)",
mask_idx, cfg->id, cfg->mask, is_ext ? "extended" : "standard");
mask_idx++;
}
#if SOC_TWAI_RANGE_FILTER_NUM
else if (is_range_filter) {
/* Additional validation for range filter */
ESP_RETURN_ON_FALSE(lhs <= rhs, ESP_ERR_INVALID_ARG, TAG,
"Range filter: low (0x%08" PRIX32 ") > high (0x%08" PRIX32 ")", lhs, rhs);
/* Configure range filter */
twai_range_filter_config_t *cfg = &controller->dump_ctx.range_filter_configs[range_idx];
cfg->range_low = lhs;
cfg->range_high = rhs;
cfg->is_ext = is_ext;
ESP_LOGD(TAG, "Parsed range filter %d: low=0x%08" PRIX32 ", high=0x%08" PRIX32 " (%s)",
range_idx, cfg->range_low, cfg->range_high, is_ext ? "extended" : "standard");
range_idx++;
}
#endif
start = comma ? comma + 1 : NULL;
}
}
*mask_count = mask_idx;
#if SOC_TWAI_RANGE_FILTER_NUM
*range_count = range_idx;
#endif
return ESP_OK;
}
/**
* @brief TWAI receive done callback for dump functionality
*
* @param[in] handle TWAI node handle
* @param[in] event_data Receive event data
* @param[in] user_ctx Controller context pointer
*
* @return @c true if higher priority task woken, @c false otherwise
*/
static IRAM_ATTR bool twai_dump_rx_done_cb(twai_node_handle_t handle, const twai_rx_done_event_data_t *event_data, void *user_ctx)
{
ESP_UNUSED(handle);
ESP_UNUSED(event_data);
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)user_ctx;
BaseType_t higher_priority_task_woken = pdFALSE;
/* Validate user_ctx pointer */
if (controller == NULL || !atomic_load(&controller->dump_ctx.is_running)) {
return false;
}
/* Check if queue exists before using */
if (controller->dump_ctx.rx_queue == NULL) {
return false;
}
rx_queue_item_t item = {0};
item.frame.buffer = item.buffer;
item.frame.buffer_len = sizeof(item.buffer);
if (ESP_OK == twai_node_receive_from_isr(handle, &item.frame)) {
item.timestamp_us = esp_timer_get_time();
/* Non-blocking queue send with explicit error handling */
if (xQueueSendFromISR(controller->dump_ctx.rx_queue, &item, &higher_priority_task_woken) != pdTRUE) {
/* Queue full - frame dropped silently to maintain ISR performance */
}
}
return (higher_priority_task_woken == pdTRUE);
}
/**
* @brief Frame reception task for dump functionality
*
* @param[in] parameter Controller context pointer
*/
static void dump_task(void *parameter)
{
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)parameter;
twai_dump_ctx_t *dump_ctx = &(controller->dump_ctx);
int controller_id = controller - g_twai_controller_ctx;
char output_line[DUMP_OUTPUT_LINE_SIZE];
ESP_LOGD(TAG, "Dump task started for controller %d", controller_id);
while (atomic_load(&dump_ctx->is_running)) {
rx_queue_item_t item;
if (xQueueReceive(dump_ctx->rx_queue, &item, pdMS_TO_TICKS(CONFIG_EXAMPLE_DUMP_TASK_TIMEOUT_MS)) == pdPASS) {
format_twaidump_frame(dump_ctx->timestamp_mode, &item.frame, item.timestamp_us,
dump_ctx->start_time_us, &dump_ctx->last_frame_time_us,
controller_id, output_line, sizeof(output_line));
printf("%s", output_line);
}
}
/* Clean up our own resources */
vTaskSuspendAll();
dump_ctx->dump_task_handle = NULL;
xTaskResumeAll();
vTaskDelete(NULL);
}
/**
* @brief Initialize TWAI dump module for a controller
*
* @param[in] controller Controller context to initialize
*
* @return @c ESP_OK on success, error code on failure
*/
static esp_err_t twai_dump_init_controller(twai_controller_ctx_t *controller)
{
/* Just register the callback, resources will be created when dump starts */
controller->core_ctx.driver_cbs.on_rx_done = twai_dump_rx_done_cb;
/* Initialize atomic flags and handles */
atomic_init(&controller->dump_ctx.is_running, false);
controller->dump_ctx.rx_queue = NULL;
controller->dump_ctx.dump_task_handle = NULL;
return ESP_OK;
}
/**
* @brief Start dump for a controller - create resources and task
*
* @param[in] controller Controller context to start dump for
*
* @return @c ESP_OK on success, error code on failure
*/
static esp_err_t twai_dump_start_controller(twai_controller_ctx_t *controller)
{
int controller_id = controller - g_twai_controller_ctx;
twai_dump_ctx_t *dump_ctx = &controller->dump_ctx;
/* Create frame queue */
dump_ctx->rx_queue = xQueueCreate(CONFIG_EXAMPLE_DUMP_QUEUE_SIZE, sizeof(rx_queue_item_t));
if (!dump_ctx->rx_queue) {
ESP_LOGE(TAG, "Failed to create frame queue for controller %d", controller_id);
return ESP_ERR_NO_MEM;
}
/* Set running flag before creating task */
atomic_store(&dump_ctx->is_running, true);
/* Create dump task */
BaseType_t task_ret = xTaskCreate(
dump_task,
"twai_dump_task",
CONFIG_EXAMPLE_DUMP_TASK_STACK_SIZE,
controller, /* Pass controller as user data */
CONFIG_EXAMPLE_DUMP_TASK_PRIORITY,
&dump_ctx->dump_task_handle);
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_FALSE(task_ret == pdPASS, ESP_ERR_NO_MEM, err, TAG, "Failed to create dump task for controller %d", controller_id);
return ESP_OK;
err:
atomic_store(&dump_ctx->is_running, false);
if (dump_ctx->rx_queue != NULL) {
vQueueDelete(dump_ctx->rx_queue);
dump_ctx->rx_queue = NULL;
}
return ret;
}
/**
* @brief Deinitialize TWAI dump module for a controller
*
* @param[in] controller Controller context to deinitialize
*/
static void twai_dump_deinit_controller(twai_controller_ctx_t *controller)
{
int controller_id = controller - g_twai_controller_ctx;
twai_dump_stop_internal(controller_id);
/* Clear callback */
controller->core_ctx.driver_cbs.on_rx_done = NULL;
ESP_LOGD(TAG, "Dump module deinitialized for controller %d", controller_id);
}
/**
* @brief Command handler for twai_dump command
*
* @param[in] argc Argument count
* @param[in] argv Argument vector
*
* @return @c ESP_OK on success, error code on failure
*/
static int twai_dump_handler(int argc, char **argv)
{
esp_err_t ret = ESP_OK;
int nerrors = arg_parse(argc, argv, (void **)&twai_dump_args);
if (nerrors != 0) {
arg_print_errors(stderr, twai_dump_args.end, argv[0]);
return ESP_ERR_INVALID_ARG;
}
/* Stop dump */
if (twai_dump_args.stop->count > 0) {
/* For --stop option, controller ID is in the controller_filter argument */
const char *controller_str = twai_dump_args.controller_filter->sval[0];
int controller_id = parse_controller_string(controller_str);
ESP_RETURN_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid controller ID: %s", controller_str);
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Failed to get controller for ID: %d", controller_id);
ret = twai_dump_stop_internal(controller_id);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to stop dump on controller %d", controller_id);
return ESP_OK;
}
/* Start dump */
const char *controller_str = twai_dump_args.controller_filter->sval[0];
/* Parse controller ID, e.g. "twai0" -> 0 */
int controller_id = -1;
const char *filter_str = NULL;
filter_str = parse_controller_id(controller_str, &controller_id);
ESP_RETURN_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to parse controller ID");
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Failed to get controller for ID: %d", controller_id);
/* Check if already running */
if (atomic_load(&controller->dump_ctx.is_running)) {
ESP_LOGW(TAG, "Dump already running for controller %d", controller_id); // Already running, no need to start again
return ESP_OK;
}
/* Parse filter string directly using simplified logic */
int mask_count = 0;
#ifdef SOC_TWAI_RANGE_FILTER_NUM
int range_count = 0;
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
/* Clear filter configs first */
memset(controller->dump_ctx.mask_filter_configs, 0, sizeof(controller->dump_ctx.mask_filter_configs));
#if SOC_TWAI_RANGE_FILTER_NUM
memset(controller->dump_ctx.range_filter_configs, 0, sizeof(controller->dump_ctx.range_filter_configs));
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
/* Parse filters using the helper function */
#ifdef SOC_TWAI_RANGE_FILTER_NUM
ret = parse_twai_filters(filter_str, controller, &mask_count, &range_count);
#else
ret = parse_twai_filters(filter_str, controller, &mask_count, NULL);
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to parse filters: %s", esp_err_to_name(ret));
/* Check if controller is initialized */
if (!atomic_load(&controller->core_ctx.is_initialized)) {
ESP_LOGE(TAG, "TWAI%d not initialized", (controller - g_twai_controller_ctx));
return ESP_ERR_INVALID_STATE;
}
/* Configure filters */
#if SOC_TWAI_RANGE_FILTER_NUM
if (mask_count > 0 || range_count > 0) {
#else
if (mask_count > 0) {
#endif
/* Always disable and reconfigure to apply new filter settings */
ret = twai_node_disable(controller->node_handle);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to disable TWAI node%d for filter configuration: %s", controller_id, esp_err_to_name(ret));
ret = (SOC_TWAI_MASK_FILTER_NUM > 0) ? ESP_OK : ESP_ERR_INVALID_STATE;
ESP_RETURN_ON_ERROR(ret, TAG, "TWAI%d does not support %d mask filters", controller_id, SOC_TWAI_MASK_FILTER_NUM);
if (mask_count > 0) {
for (int i = 0; i < mask_count; i++) {
ret = twai_node_config_mask_filter(controller->node_handle, i,
&controller->dump_ctx.mask_filter_configs[i]);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to configure mask filter %d", i);
ESP_LOGD(TAG, "Configured mask filter %d: %08X : %08X", i,
controller->dump_ctx.mask_filter_configs[i].id,
controller->dump_ctx.mask_filter_configs[i].mask);
}
}
#if SOC_TWAI_RANGE_FILTER_NUM
ret = (SOC_TWAI_RANGE_FILTER_NUM > 0) ? ESP_OK : ESP_ERR_INVALID_STATE;
ESP_RETURN_ON_ERROR(ret, TAG, "TWAI%d does not support %d range filters", controller_id, SOC_TWAI_RANGE_FILTER_NUM);
if (range_count > 0) {
for (int i = 0; i < range_count; i++) {
ret = twai_node_config_range_filter(controller->node_handle, i,
&controller->dump_ctx.range_filter_configs[i]);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to configure range filter %d", i);
/* If no mask filter is configured, disable mask filter 0 which enabled by default */
if (mask_count == 0) {
twai_mask_filter_config_t mfilter_cfg = {
.id = 0xFFFFFFFF,
.mask = 0xFFFFFFFF,
};
esp_err_t mask_ret = twai_node_config_mask_filter(controller->node_handle, 0, &mfilter_cfg);
ESP_RETURN_ON_ERROR(mask_ret, TAG, "Failed to configure node%d default mask filter: %s", controller_id, esp_err_to_name(mask_ret));
}
ESP_LOGD(TAG, "Configured range filter %d: %08X - %08X", i,
controller->dump_ctx.range_filter_configs[i].range_low,
controller->dump_ctx.range_filter_configs[i].range_high);
}
}
#endif /* SOC_TWAI_RANGE_FILTER_NUM */
esp_err_t enable_ret = twai_node_enable(controller->node_handle);
ESP_RETURN_ON_ERROR(enable_ret, TAG, "Failed to enable TWAI node%d after filter configuration: %s", controller_id, esp_err_to_name(enable_ret));
}
/* Parse timestamp mode */
controller->dump_ctx.timestamp_mode = TIMESTAMP_MODE_NONE;
if (twai_dump_args.timestamp->count > 0) {
char mode = twai_dump_args.timestamp->sval[0][0];
switch (mode) {
case 'a': case 'd': case 'z': case 'n':
controller->dump_ctx.timestamp_mode = (timestamp_mode_t)mode;
break;
default:
ESP_LOGE(TAG, "Invalid timestamp mode: %c (use a/d/z/n)", mode);
return ESP_ERR_INVALID_ARG;
}
}
/* Initialize timestamp base time */
int64_t current_time = esp_timer_get_time();
controller->dump_ctx.start_time_us = current_time;
controller->dump_ctx.last_frame_time_us = current_time;
/* Start dump task and create resources */
ret = twai_dump_start_controller(controller);
ESP_RETURN_ON_FALSE(ret == ESP_OK, ret, TAG, "Failed to start dump task");
return ESP_OK;
}
/**
* @brief Stop dump and wait for task to exit naturally
*
* @param[in] controller_id Controller ID to stop dump for
*
* @return @c ESP_OK on success, error code on failure
*/
esp_err_t twai_dump_stop_internal(int controller_id)
{
if (controller_id < 0 || controller_id >= SOC_TWAI_CONTROLLER_NUM) {
ESP_LOGE(TAG, "Invalid controller ID: %d", controller_id);
return ESP_ERR_INVALID_ARG;
}
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Invalid controller ID: %d", controller_id);
twai_dump_ctx_t *dump_ctx = &controller->dump_ctx;
if (!atomic_load(&dump_ctx->is_running)) {
ESP_LOGD(TAG, "Dump not running for controller %d", controller_id);
return ESP_OK;
}
/* Signal task to stop */
if (dump_ctx->dump_task_handle) {
atomic_store(&dump_ctx->is_running, false);
ESP_LOGD(TAG, "Signaled dump task to stop for controller %d", controller_id);
/* Wait for dump task to finish */
int timeout_ms = CONFIG_EXAMPLE_DUMP_TASK_TIMEOUT_MS * 2;
vTaskDelay(pdMS_TO_TICKS(timeout_ms));
ESP_RETURN_ON_FALSE(dump_ctx->dump_task_handle == NULL, ESP_ERR_TIMEOUT, TAG,
"Dump task did not exit naturally, timeout after %d ms", timeout_ms);
}
/* Clean up queue */
if (dump_ctx->rx_queue != NULL) {
vQueueDelete(dump_ctx->rx_queue);
dump_ctx->rx_queue = NULL;
}
return ESP_OK;
}
/**
* @brief Register TWAI dump commands with console
*/
void register_twai_dump_commands(void)
{
/* Initialize all controller dump modules */
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
esp_err_t ret = twai_dump_init_controller(controller);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to initialize dump module for TWAI%d: %s", i, esp_err_to_name(ret));
}
}
/* Register command */
twai_dump_args.controller_filter = arg_str1(NULL, NULL, "<controller>[,filter]",
"Controller ID and optional filters");
twai_dump_args.stop = arg_lit0(NULL, "stop",
"Stop monitoring the specified controller");
twai_dump_args.timestamp = arg_str0("t", "timestamp", "<mode>",
"Timestamp mode: a=absolute, d=delta, z=zero, n=none (default: n)");
twai_dump_args.end = arg_end(3);
const esp_console_cmd_t cmd = {
.command = "twai_dump",
.help = "Monitor TWAI bus messages with timestamps\n"
"Usage:\n"
" twai_dump [-t <mode>] <controller>[,filter...]\n"
" twai_dump <controller> --stop\n"
"\n"
"Options:\n"
" -t <mode> Timestamp mode: a=absolute, d=delta, z=zero, n=none (default: n)\n"
" --stop Stop monitoring the specified controller\n"
"\n"
"Filter formats:\n"
" id:mask Mask filter (e.g., 123:7FF)\n"
" low-high Range filter (e.g., a-15)\n"
"\n"
"Examples:\n"
" twai_dump twai0 # Monitor without timestamps (default)\n"
" twai_dump -t a twai0 # Monitor with absolute timestamps\n"
" twai_dump -t d twai0 # Monitor with delta timestamps\n"
" twai_dump -t n twai0,123:7FF # Monitor ID 0x123 without timestamps\n"
" twai_dump twai0,a-15 # Monitor range: [0xa, 0x15]\n"
" twai_dump twai0,123:7FF,a-15 # Mix mask and range filters\n"
" twai_dump twai0,000-666 # Monitor range: [0x000, 0x666]\n"
" twai_dump twai0 --stop # Stop monitoring TWAI0\n"
,
.hint = NULL,
.func = &twai_dump_handler,
.argtable = &twai_dump_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
/**
* @brief Unregister dump commands and cleanup resources
*/
void unregister_twai_dump_commands(void)
{
/* Cleanup all controller dump modules */
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
twai_dump_deinit_controller(controller);
}
ESP_LOGI(TAG, "TWAI dump commands unregistered and resources cleaned up");
}

View File

@@ -0,0 +1,144 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "stdbool.h"
#include <stdatomic.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_twai.h"
#include "esp_twai_onchip.h"
/** @brief Frame buffer size based on TWAI-FD configuration */
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
#define TWAI_FRAME_BUFFER_SIZE TWAIFD_FRAME_MAX_LEN
#else
#define TWAI_FRAME_BUFFER_SIZE TWAI_FRAME_MAX_LEN
#endif
/**
* @brief Time stamp mode for candump-style output
*/
typedef enum {
TIMESTAMP_MODE_ABSOLUTE = 'a', /**< Absolute time (default) */
TIMESTAMP_MODE_DELTA = 'd', /**< Delta time between frames */
TIMESTAMP_MODE_ZERO = 'z', /**< Relative time from start */
TIMESTAMP_MODE_NONE = 'n' /**< No timestamp */
} timestamp_mode_t;
/**
* @brief Core TWAI driver context
*/
typedef struct {
twai_onchip_node_config_t driver_config; /**< Cached driver configuration */
twai_event_callbacks_t driver_cbs; /**< Driver event callbacks */
atomic_bool is_initialized; /**< Initialization flag */
} twai_core_ctx_t;
/**
* @brief Context structure for the TWAI send command
*/
typedef struct {
SemaphoreHandle_t tx_done_sem; /**< Semaphore for TX completion signaling */
atomic_bool is_tx_pending; /**< Flag to indicate if TX is in progress */
twai_frame_t tx_frame; /**< TX frame structure */
uint8_t tx_frame_buffer[TWAI_FRAME_BUFFER_SIZE]; /**< TX frame buffer */
} twai_send_ctx_t;
/**
* @brief TWAI dump module context
*/
typedef struct {
atomic_bool is_running; /**< Dump running flag */
twai_mask_filter_config_t mask_filter_configs[SOC_TWAI_MASK_FILTER_NUM]; /**< Mask filter configurations */
#if SOC_TWAI_RANGE_FILTER_NUM
twai_range_filter_config_t range_filter_configs[SOC_TWAI_RANGE_FILTER_NUM]; /**< Range filter configurations */
#endif
QueueHandle_t rx_queue; /**< RX frame queue */
TaskHandle_t dump_task_handle; /**< Handle for dump task */
timestamp_mode_t timestamp_mode; /**< Time stamp mode */
int64_t start_time_us; /**< Start time in microseconds */
int64_t last_frame_time_us; /**< Last frame timestamp for delta */
} twai_dump_ctx_t;
/**
* @brief Core state machine for the TWAI console
*
* This structure manages core driver resources, synchronization primitives,
* and resources for different functional modules (send, dump, player).
* It embeds twai_utils_status_t to handle bus status and statistics.
*/
typedef struct {
/** @brief Core Driver Resources */
twai_core_ctx_t core_ctx; /**< Core driver context */
twai_node_handle_t node_handle; /**< TWAI node handle */
/** @brief Module Contexts */
twai_send_ctx_t send_ctx; /**< Send context for this controller */
twai_dump_ctx_t dump_ctx; /**< Dump module context */
} twai_controller_ctx_t;
/** @brief Global controller context array */
extern twai_controller_ctx_t g_twai_controller_ctx[SOC_TWAI_CONTROLLER_NUM];
/**
* @brief Get controller by ID
*
* @param[in] controller_id Controller ID
*
* @return Pointer to controller context, or NULL if invalid
*/
twai_controller_ctx_t* get_controller_by_id(int controller_id);
/**
* @brief Register TWAI core commands with console
*/
void register_twai_core_commands(void);
/**
* @brief Register TWAI send commands with console
*/
void register_twai_send_commands(void);
/**
* @brief Register TWAI dump commands with console
*/
void register_twai_dump_commands(void);
/**
* @brief Unregister TWAI core commands and cleanup resources
*/
void unregister_twai_core_commands(void);
/**
* @brief Unregister TWAI send commands and cleanup resources
*/
void unregister_twai_send_commands(void);
/**
* @brief Unregister TWAI dump commands and cleanup resources
*/
void unregister_twai_dump_commands(void);
/**
* @brief Stop dump and wait for task to exit naturally
*
* @param[in] controller_id Controller ID to stop dump for
*
* @return @c ESP_OK on success, error code on failure
*/
esp_err_t twai_dump_stop_internal(int controller_id);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,275 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "argtable3/argtable3.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_err.h"
#include "esp_check.h"
#include "esp_twai.h"
#include "esp_twai_onchip.h"
#include "cmd_twai_internal.h"
#include "twai_utils_parser.h"
/** @brief Log tag for this module */
static const char *TAG = "cmd_twai_send";
/** @brief Command line arguments for sending frames - supports positional and option formats */
static struct {
struct arg_str *controller; /**< Controller ID (required) */
struct arg_str *frame; /**< Frame string (required) */
struct arg_end *end;
} twai_send_args;
/**
* @brief TX Callback for TWAI event handling
*
* @param[in] handle TWAI node handle
* @param[in] event_data TX done event data
* @param[in] user_ctx Controller context pointer
*
* @return @c true if higher priority task woken, @c false otherwise
*/
static bool twai_send_tx_done_cb(twai_node_handle_t handle, const twai_tx_done_event_data_t *event_data, void *user_ctx)
{
ESP_UNUSED(handle);
ESP_UNUSED(event_data);
twai_controller_ctx_t *controller = (twai_controller_ctx_t *)user_ctx;
/* Signal TX completion */
if (atomic_load(&controller->send_ctx.is_tx_pending)) {
atomic_store(&controller->send_ctx.is_tx_pending, false);
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(controller->send_ctx.tx_done_sem, &xHigherPriorityTaskWoken);
return xHigherPriorityTaskWoken == pdTRUE;
}
return false;
}
/**
* @brief Initialize the send module for a controller
*
* @param[in] controller Pointer to the controller context
*
* @return @c ESP_OK on success, error code on failure
*/
static esp_err_t twai_send_init_controller(twai_controller_ctx_t *controller)
{
int controller_id = controller - &g_twai_controller_ctx[0];
/* Create TX completion semaphore */
controller->send_ctx.tx_done_sem = xSemaphoreCreateBinary();
if (controller->send_ctx.tx_done_sem == NULL) {
ESP_LOGE(TAG, "Failed to create TX semaphore for controller %d", controller_id);
return ESP_ERR_NO_MEM;
}
/* Initialize TX pending flag */
atomic_init(&controller->send_ctx.is_tx_pending, false);
/* Register TX done callback */
twai_core_ctx_t *core_ctx = &controller->core_ctx;
core_ctx->driver_cbs.on_tx_done = twai_send_tx_done_cb;
return ESP_OK;
}
/**
* @brief Deinitialize the send module for a controller
*
* @param[in] controller Pointer to the controller context
*/
static void twai_send_deinit_controller(twai_controller_ctx_t *controller)
{
/* Clear pending flag */
atomic_store(&controller->send_ctx.is_tx_pending, false);
/* Delete TX completion semaphore */
if (controller->send_ctx.tx_done_sem) {
vSemaphoreDelete(controller->send_ctx.tx_done_sem);
controller->send_ctx.tx_done_sem = NULL;
}
/* Clear callback */
controller->core_ctx.driver_cbs.on_tx_done = NULL;
}
/**
* @brief Send a TWAI frame with the provided parameters
*
* @param[in] controller Pointer to the TWAI controller context
* @param[in] frame Pointer to the TWAI frame to send
* @param[in] timeout_ms Timeout in milliseconds to wait for TX completion
*
* @return @c ESP_OK on success, error code on failure
*/
static esp_err_t send_frame_sync(twai_controller_ctx_t *controller, const twai_frame_t *frame, uint32_t timeout_ms)
{
if (!controller) {
ESP_LOGE(TAG, "Invalid controller pointer");
return ESP_ERR_INVALID_ARG;
}
int controller_id = controller - &g_twai_controller_ctx[0];
esp_err_t ret = ESP_OK;
twai_core_ctx_t *ctx = &controller->core_ctx;
/* Check if TWAI driver is running */
ESP_RETURN_ON_FALSE(atomic_load(&ctx->is_initialized), ESP_ERR_INVALID_STATE, TAG, "TWAI%d not initialized", controller_id);
/* Mark TX as pending */
atomic_store(&controller->send_ctx.is_tx_pending, true);
/* Transmit the frame */
ret = twai_node_transmit(controller->node_handle, frame, timeout_ms);
ESP_GOTO_ON_ERROR(ret, err, TAG, "Node %d: Failed to queue TX frame: %s", controller_id, esp_err_to_name(ret));
/* Wait for TX completion or timeout */
ESP_GOTO_ON_FALSE(xSemaphoreTake(controller->send_ctx.tx_done_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE, ESP_ERR_TIMEOUT, err, TAG,
"Node %d: TX timed out after %"PRIu32" ms", controller_id, timeout_ms);
return ESP_OK;
err:
atomic_store(&controller->send_ctx.is_tx_pending, false);
return ret;
}
/**
* @brief Command handler for `twai_send twai0 123#AABBCC` command
*
* @param[in] argc Argument count
* @param[in] argv Argument vector
*
* @return @c ESP_OK on success, error code on failure
*/
static int twai_send_handler(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&twai_send_args);
if (nerrors != 0) {
arg_print_errors(stderr, twai_send_args.end, argv[0]);
return ESP_ERR_INVALID_ARG;
}
/* Check for mandatory arguments */
if (twai_send_args.controller->count == 0) {
ESP_LOGE(TAG, "Controller ID is required");
return ESP_ERR_INVALID_ARG;
}
/* Parse controller id */
int controller_id = parse_controller_string(twai_send_args.controller->sval[0]);
ESP_RETURN_ON_FALSE(controller_id >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid controller ID: %s", twai_send_args.controller->sval[0]);
twai_controller_ctx_t *controller = get_controller_by_id(controller_id);
ESP_RETURN_ON_FALSE(controller != NULL, ESP_ERR_INVALID_ARG, TAG, "Controller not found: %d", controller_id);
/* Prepare frame buffer on stack for synchronous transmission */
twai_frame_t frame = {0};
uint8_t data_buffer[TWAI_FRAME_BUFFER_SIZE] = {0};
frame.buffer = data_buffer;
/* Check if frame string is provided */
const char *frame_str = twai_send_args.frame->sval[0];
const char *sep = NULL;
int hash_count = 0;
bool is_fd = false;
int res = locate_hash(frame_str, &sep, &hash_count);
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Failed to locate '#' in frame string: %s", frame_str);
if (hash_count == 1) {
is_fd = false;
} else if (hash_count == 2) {
is_fd = true;
} else {
ESP_LOGE(TAG, "Invalid '#' count in frame string: %s", frame_str);
return ESP_ERR_INVALID_ARG;
}
/* Parse ID */
size_t id_len = (size_t)(sep - frame_str);
res = parse_twai_id(frame_str, id_len, &frame);
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Invalid ID: %.*s, error code: %d", (int)id_len, frame_str, res);
/* Parse frame body */
const char *body = sep + hash_count;
if (is_fd) {
#if CONFIG_EXAMPLE_ENABLE_TWAI_FD
frame.header.fdf = 1;
res = parse_twaifd_frame(body, &frame);
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Invalid TWAI-FD frame: %.*s, error code: %d", (int)id_len, frame_str, res);
#else
ESP_LOGE(TAG, "TWAI-FD not enabled in this build");
return ESP_ERR_INVALID_ARG;
#endif
} else {
res = parse_classic_frame(body, &frame);
ESP_RETURN_ON_FALSE(res == PARSE_OK, ESP_ERR_INVALID_ARG, TAG, "Invalid TWAI classic frame: %.*s, error code: %d", (int)id_len, frame_str, res);
}
/* Send frame with 1 second timeout */
esp_err_t ret = send_frame_sync(controller, &frame, 1000);
ESP_RETURN_ON_ERROR(ret, TAG, "Failed to send frame: %s", esp_err_to_name(ret));
return ESP_OK;
}
void register_twai_send_commands(void)
{
/* Initialize send context for all controllers */
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
esp_err_t ret = twai_send_init_controller(controller);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to initialize send module for TWAI%d: %s", i, esp_err_to_name(ret));
}
}
/* Register command arguments */
twai_send_args.controller = arg_str1(NULL, NULL, "<controller>", "TWAI controller (e.g. twai0)");
twai_send_args.frame = arg_str0(NULL, NULL, "<frame_str>", "Frame string in format 123#AABBCC (standard) or 12345678#AABBCC (extended)");
twai_send_args.end = arg_end(20);
/* Register command */
const esp_console_cmd_t twai_send_cmd = {
.command = "twai_send",
.help = "Send a TWAI frame using string format\n"
"Usage: twai_send <controller> <frame_str>\n"
"\n"
"Frame Formats:\n"
" Standard: 123#DEADBEEF (11-bit ID)\n"
" Extended: 12345678#CAFEBABE (29-bit ID)\n"
" RTR: 456#R or 456#R8 (Remote Transmission Request)\n"
" TWAI-FD: 123##1AABBCC (FD frame with flags)\n"
"\n"
"Examples:\n"
" twai_send twai0 123#DEADBEEF # Standard frame\n"
" twai_send twai0 12345678#CAFEBABE # Extended frame\n"
" twai_send twai0 456#R8 # RTR frame\n"
" twai_send twai0 123##1DEADBEEFCAFEBABE # TWAI-FD frame\n"
,
.hint = "<controller> [<frame_str>]",
.func = &twai_send_handler,
.argtable = &twai_send_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&twai_send_cmd));
}
void unregister_twai_send_commands(void)
{
/* Cleanup all controller send modules */
for (int i = 0; i < SOC_TWAI_CONTROLLER_NUM; i++) {
twai_controller_ctx_t *controller = &g_twai_controller_ctx[i];
twai_send_deinit_controller(controller);
}
}

View File

@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_console.h"
#include "cmd_twai.h"
static const char *TAG = "twai_example";
/**
* @brief Main application entry point
*
*/
void app_main(void)
{
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.prompt = "twai>";
ESP_LOGI(TAG, "Initializing TWAI console example");
/* Initialize console REPL environment based on configuration */
#if CONFIG_ESP_CONSOLE_UART
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_CDC
esp_console_dev_usb_cdc_config_t cdc_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&cdc_config, &repl_config, &repl));
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
esp_console_dev_usb_serial_jtag_config_t usbjtag_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usbjtag_config, &repl_config, &repl));
#else
ESP_LOGE(TAG, "No console device configured");
return;
#endif
/* Register TWAI commands with console */
register_twai_commands();
/* Start console REPL */
ESP_ERROR_CHECK(esp_console_start_repl(repl));
}

View File

@@ -0,0 +1,335 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "twai_utils_parser.h"
#include <string.h>
#include <ctype.h>
#include <stdio.h>
/**
* @brief Format timestamp string based on the specified mode
*
* @param[in] timestamp_mode Timestamp mode configuration
* @param[in] frame_timestamp Frame timestamp in microseconds
* @param[in] start_time_us Start time for zero-based timestamps
* @param[in,out] last_frame_time_us Pointer to last frame time for delta mode (updated if delta mode)
* @param[out] timestamp_str Buffer to store formatted timestamp string
* @param[in] max_len Maximum length of timestamp string buffer
*/
void format_timestamp(timestamp_mode_t timestamp_mode, int64_t frame_timestamp,
int64_t start_time_us, int64_t *last_frame_time_us,
char *timestamp_str, size_t max_len)
{
if (timestamp_mode == TIMESTAMP_MODE_NONE) {
timestamp_str[0] = '\0';
return;
}
int64_t timestamp_us;
switch (timestamp_mode) {
case TIMESTAMP_MODE_ABSOLUTE:
timestamp_us = frame_timestamp;
break;
case TIMESTAMP_MODE_DELTA:
timestamp_us = frame_timestamp - *last_frame_time_us;
*last_frame_time_us = frame_timestamp;
break;
case TIMESTAMP_MODE_ZERO:
timestamp_us = frame_timestamp - start_time_us;
break;
default:
timestamp_str[0] = '\0';
return;
}
/* Format output: (seconds.microseconds) */
snprintf(timestamp_str, max_len, "(%lld.%06lld) ", timestamp_us / 1000000, timestamp_us % 1000000);
}
int parse_hex_segment(const char *str, size_t len, uint32_t *out)
{
if (!str || len == 0 || len > TWAI_EXT_ID_CHAR_LEN || !out) {
return PARSE_INVALID_ARG;
}
uint32_t result = 0;
for (size_t i = 0; i < len; i++) {
uint8_t nibble;
if (parse_nibble(str[i], &nibble) != PARSE_OK) {
return PARSE_ERROR;
}
result = (result << 4) | nibble;
}
*out = result;
return PARSE_OK;
}
/**
* @brief Parse payload bytes (hex pairs) up to max length, skipping '.' separators
*
* This function reads up to max bytes from the ASCII hex string s,
* ignoring any '.' separators. Each pair of hex digits is converted
* into one byte and stored into buf.
*
* @param[in] s Null-terminated input string containing hex digits and optional '.' separators
* @param[out] buf Buffer to store parsed byte values
* @param[in] max Maximum number of bytes to parse (buffer capacity)
*
* @return On success, returns the number of bytes parsed (0..max).
* Returns PARSE_INVALID_ARG if input pointers are NULL or max <= 0.
* Returns PARSE_ERROR if a non-hex digit is encountered before parsing max bytes
*/
static inline int parse_payload(const char *s, uint8_t *buf, int max)
{
if (!s || !buf || max <= 0) {
return PARSE_INVALID_ARG;
}
int cnt = 0;
while (*s && cnt < max) {
if (*s == '.') {
s++;
continue;
}
/* Check if we have valid hex pair */
if (!isxdigit((unsigned char)s[0])) {
if (cnt == 0 && *s != '\0') {
return PARSE_ERROR;
}
break;
}
if (!isxdigit((unsigned char)s[1])) {
return PARSE_ERROR;
}
uint8_t high, low;
if (parse_nibble(s[0], &high) != PARSE_OK || parse_nibble(s[1], &low) != PARSE_OK) {
return PARSE_ERROR;
}
buf[cnt++] = (high << 4) | low;
s += 2;
}
return cnt;
}
/**
* @brief Parse hex ID substring of given length
*
* @param[in] str Pointer to the start of the hex substring
* @param[in] len Number of characters in the hex substring (3 or 8)
* @param[out] out Pointer to the variable to receive the parsed ID value
* @param[out] is_ext Pointer to store whether the ID is extended format
*
* @return PARSE_OK on success;
* PARSE_INVALID_ARG if pointers are NULL or len is out of range;
* PARSE_ERROR if any character is not a valid hex digit or length mismatch
*/
static inline int parse_hex_id(const char *str, size_t len, uint32_t *out, bool *is_ext)
{
if (!str || !out || !is_ext || len == 0 || len > TWAI_EXT_ID_CHAR_LEN) {
return PARSE_INVALID_ARG;
}
int ret = parse_hex_segment(str, len, out);
if (ret != PARSE_OK) {
return ret;
}
*is_ext = (len > TWAI_STD_ID_CHAR_LEN) || (*out > TWAI_STD_ID_MASK);
if ((*is_ext && *out > TWAI_EXT_ID_MASK) || (!*is_ext && *out > TWAI_STD_ID_MASK)) {
return PARSE_OUT_OF_RANGE;
}
return PARSE_OK;
}
int parse_twai_id(const char *str, size_t len, twai_frame_t *f)
{
if (!str || !f) {
return PARSE_INVALID_ARG;
}
bool is_ext = false;
uint32_t id = 0;
int res = parse_hex_id(str, len, &id, &is_ext);
if (res != PARSE_OK) {
return res;
}
f->header.id = id;
f->header.ide = is_ext ? 1 : 0;
return PARSE_OK;
}
int parse_classic_frame(const char *body, twai_frame_t *f)
{
if (!body || !f) {
return PARSE_INVALID_ARG;
}
/* Handle RTR frame */
if (*body == 'R' || *body == 'r') {
f->header.rtr = true;
f->buffer_len = 0; // RTR frames have no data payload.
const char *dlc_str = body + 1;
uint8_t dlc = TWAI_RTR_DEFAULT_DLC; // Default DLC for RTR frame if not specified.
if (*dlc_str != '\0') {
// An explicit DLC is provided, e.g., "R8".
char *endptr;
dlc = (uint8_t)strtoul(dlc_str, &endptr, 16);
if (*endptr != '\0' || dlc > TWAI_FRAME_MAX_LEN) {
return PARSE_ERROR;
}
}
f->header.dlc = dlc;
return PARSE_OK;
}
/* Handle data frame */
f->header.rtr = false; // Ensure RTR flag is cleared.
int dl = parse_payload(body, f->buffer, TWAI_FRAME_MAX_LEN);
if (dl < 0) {
return dl;
}
/* Check for optional _dlc suffix */
const char *underscore = strchr(body, '_');
if (underscore && underscore[1] != '\0') {
uint8_t dlc = (uint8_t)strtoul(underscore + 1, NULL, 16);
if (dlc <= TWAI_FRAME_MAX_LEN) {
f->header.dlc = dlc;
} else {
f->header.dlc = TWAI_FRAME_MAX_LEN;
}
} else {
f->header.dlc = (uint8_t)dl;
}
f->buffer_len = dl;
return PARSE_OK;
}
int parse_twaifd_frame(const char *body, twai_frame_t *f)
{
if (!body || !f) {
return PARSE_INVALID_ARG;
}
uint8_t flags;
if (parse_nibble(*body++, &flags) != PARSE_OK || flags > TWAI_FD_FLAGS_MAX_VALUE) {
return PARSE_OUT_OF_RANGE;
}
f->header.fdf = true;
f->header.brs = !!(flags & TWAI_FD_BRS_FLAG_MASK);
f->header.esi = !!(flags & TWAI_FD_ESI_FLAG_MASK);
int dl = parse_payload(body, f->buffer, TWAIFD_FRAME_MAX_LEN);
if (dl < 0) {
return dl;
}
f->buffer_len = dl;
f->header.dlc = (uint8_t)twaifd_len2dlc((uint16_t)dl);
return PARSE_OK;
}
int parse_pair_token(const char *tok, size_t tok_len, char sep,
uint32_t *lhs, size_t *lhs_chars,
uint32_t *rhs, size_t *rhs_chars)
{
if (!tok || tok_len == 0 || !lhs || !rhs || !lhs_chars || !rhs_chars) {
return PARSE_INVALID_ARG;
}
const char *mid = (const char *)memchr(tok, sep, tok_len);
if (!mid) {
return PARSE_NOT_FOUND; /* not this token kind */
}
size_t l_len = (size_t)(mid - tok);
size_t r_len = tok_len - l_len - 1;
if (l_len == 0 || r_len == 0) {
return PARSE_ERROR;
}
int rl = parse_hex_segment(tok, l_len, lhs);
int rr = parse_hex_segment(mid + 1, r_len, rhs);
if (rl != PARSE_OK || rr != PARSE_OK) {
return PARSE_ERROR;
}
*lhs_chars = l_len;
*rhs_chars = r_len;
return PARSE_OK;
}
const char *twai_state_to_string(twai_error_state_t state)
{
switch (state) {
case TWAI_ERROR_ACTIVE: return "Error Active";
case TWAI_ERROR_WARNING: return "Error Warning";
case TWAI_ERROR_PASSIVE: return "Error Passive";
case TWAI_ERROR_BUS_OFF: return "Bus Off";
default: return "Unknown";
}
}
int format_gpio_pin(int gpio_pin, char *buffer, size_t buffer_size)
{
if (gpio_pin == GPIO_NUM_NC || gpio_pin < 0) {
return snprintf(buffer, buffer_size, "Disabled");
} else {
return snprintf(buffer, buffer_size, "GPIO%d", gpio_pin);
}
}
int parse_controller_string(const char *controller_str)
{
int controller_id;
const char *end = parse_controller_id(controller_str, &controller_id);
return end ? controller_id : PARSE_ERROR;
}
void format_twaidump_frame(timestamp_mode_t timestamp_mode, const twai_frame_t *frame,
int64_t frame_timestamp, int64_t start_time_us, int64_t *last_frame_time_us,
int controller_id, char *output_line, size_t max_len)
{
char timestamp_str[64] = {0};
int pos = 0;
/* Format timestamp */
format_timestamp(timestamp_mode, frame_timestamp, start_time_us, last_frame_time_us,
timestamp_str, sizeof(timestamp_str));
/* Add timestamp if enabled */
if (strlen(timestamp_str) > 0) {
pos += snprintf(output_line + pos, max_len - pos, "%s", timestamp_str);
}
/* Add interface name (e.g. use twai0, twai1) */
pos += snprintf(output_line + pos, max_len - pos, "twai%d ", controller_id);
/* Format TWAI ID (formatted as: 3 digits for SFF, 8 digits for EFF) */
if (frame->header.ide) {
/* Extended frame: 8 hex digits */
pos += snprintf(output_line + pos, max_len - pos, "%08" PRIX32 " ", frame->header.id);
} else {
/* Standard frame: 3 hex digits (or less if ID is smaller) */
pos += snprintf(output_line + pos, max_len - pos, "%03" PRIX32 " ", frame->header.id);
}
if (frame->header.rtr) {
/* RTR frame: add [R] and DLC */
pos += snprintf(output_line + pos, max_len - pos, "[R%d]", frame->header.dlc);
} else {
/* Data frame: add DLC and data bytes with spaces */
printf("frame->header.dlc: %d\n", frame->header.dlc);
int actual_len = twaifd_dlc2len(frame->header.dlc);
pos += snprintf(output_line + pos, max_len - pos, "[%d]", actual_len);
for (int i = 0; i < actual_len && i < frame->buffer_len && pos < max_len - 4; i++) {
pos += snprintf(output_line + pos, max_len - pos, " %02X", frame->buffer[i]);
}
}
/* Add newline */
if (pos < max_len - 1) {
pos += snprintf(output_line + pos, max_len - pos, "\n");
}
}

View File

@@ -0,0 +1,267 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include "esp_twai.h"
#include "cmd_twai_internal.h"
#ifdef __cplusplus
extern "C" {
#endif
/* TWAI frame constants */
#define TWAI_STD_ID_CHAR_LEN 3
#define TWAI_EXT_ID_CHAR_LEN 8
/** @brief Parser return codes */
#define PARSE_OK 0
#define PARSE_ERROR -1
#define PARSE_INVALID_ARG -2
#define PARSE_OUT_OF_RANGE -3
#define PARSE_TOO_LONG -4
#define PARSE_NOT_FOUND -5
/* Additional constants */
#define TWAI_RTR_DEFAULT_DLC 0
#define TWAI_FD_FLAGS_MAX_VALUE 15
#define TWAI_FD_BRS_FLAG_MASK 0x01
#define TWAI_FD_ESI_FLAG_MASK 0x02
/**
* @brief Parse TWAI ID from string
*
* @param[in] str Pointer to the start of the ID string
* @param[in] len Length of the ID string
* @param[out] f Pointer to frame structure to fill
*
* @return PARSE_OK on success;
* PARSE_INVALID_ARG if pointers are NULL;
* PARSE_ERROR or PARSE_OUT_OF_RANGE on format or range error
*/
int parse_twai_id(const char *str, size_t len, twai_frame_t *f);
/**
* @brief Parse TWAI-FD frames with flags and extended payload
*
* Body format: <flags>{data}
* flags: single hex nibble (0..F)
* data: up to 64 bytes hex pairs
*
* @param[in] body Pointing to the substring after '#'
* @param[out] f Pointer to frame structure to fill
*
* @return PARSE_OK on success;
* PARSE_INVALID_ARG if arguments are NULL;
* PARSE_ERROR or PARSE_OUT_OF_RANGE on format or range error
*/
int parse_twaifd_frame(const char *str, twai_frame_t *f);
/**
* @brief Parse Classical TWAI data and RTR frames
*
* Supports:
* <twai_id>#{data} Data frame with up to 8 bytes
* <twai_id>#R{len} RTR frame with specified length
* <twai_id>#{data}_{dlc} Data frame with extended DLC (9..F)
*
* @param[in] body Pointing to the substring after '#'
* @param[out] f Pointer to frame structure to fill
*
* @return PARSE_OK on success;
* PARSE_INVALID_ARG if arguments are NULL;
* PARSE_ERROR or PARSE_OUT_OF_RANGE on format or range error
*/
int parse_classic_frame(const char *str, twai_frame_t *f);
/**
* @brief Parse controller string and return controller ID
*
* @param[in] controller_str Controller string (e.g., "twai0")
*
* @return Controller ID (0-9) on success, PARSE_ERROR on failure
*/
int parse_controller_string(const char *controller_str);
/**
* @brief Convert TWAI state to string
*
* @param[in] state TWAI error state
*
* @return Pointer to the string representation of the state
*/
const char *twai_state_to_string(twai_error_state_t state);
/**
* @brief Format GPIO pin display
*
* @param[in] gpio_pin GPIO pin number
* @param[out] buffer Buffer to store the formatted string
* @param[in] buffer_size Size of the buffer
*
* @return Number of characters written to buffer
*/
int format_gpio_pin(int gpio_pin, char *buffer, size_t buffer_size);
/**
* @brief Parse hex string with specified length (no null terminator required)
*
* @param[in] str Input string pointer
* @param[in] len Length of hex string to parse
* @param[out] out Output value pointer
*
* @return PARSE_OK on success, PARSE_ERROR on format error
*/
int parse_hex_segment(const char *str, size_t len, uint32_t *out);
/**
* @brief Parse a "lhs <sep> rhs" token where both sides are hex strings.
*
* The function splits by @p sep, parses both halves as hex (no null terminators required),
* and returns their values and lengths.
*
* @param[in] tok Pointer to token start
* @param[in] tok_len Token length in bytes
* @param[in] sep Separator character (':' for mask, '-' for range)
* @param[out] lhs Parsed left-hand value
* @param[out] lhs_chars Characters consumed by left-hand substring
* @param[out] rhs Parsed right-hand value
* @param[out] rhs_chars Characters consumed by right-hand substring
*
* @return PARSE_OK on success;
* PARSE_INVALID_ARG for bad args;
* PARSE_ERROR if separator missing or hex parse fails.
*/
int parse_pair_token(const char *tok, size_t tok_len, char sep,
uint32_t *lhs, size_t *lhs_chars,
uint32_t *rhs, size_t *rhs_chars);
/**
* @brief Parse a single hex nibble character
*
* @param[in] c Input character (0-9, A-F, a-f)
* @param[out] out Output pointer to store the parsed nibble value (0-15)
*
* @return PARSE_OK on success;
* PARSE_INVALID_ARG if out pointer is NULL;
* PARSE_ERROR if character is not a valid hex digit
*/
static inline int parse_nibble(char c, uint8_t *out)
{
if (!out) {
return PARSE_INVALID_ARG;
}
if (c >= '0' && c <= '9') {
*out = (uint8_t)(c - '0');
return PARSE_OK;
}
if (c >= 'A' && c <= 'F') {
*out = (uint8_t)(c - 'A' + 10);
return PARSE_OK;
}
if (c >= 'a' && c <= 'f') {
*out = (uint8_t)(c - 'a' + 10);
return PARSE_OK;
}
return PARSE_ERROR;
}
/**
* @brief Locate first '#' and count consecutives
*
* @param[in] input Input string
* @param[out] sep Pointer to the separator
* @param[out] hash_count Pointer to the hash count
*
* @return PARSE_OK if successful, PARSE_INVALID_ARG if input is NULL, PARSE_ERROR if no '#' is found
*/
static inline int locate_hash(const char *input, const char **sep, int *hash_count)
{
if (!input || !sep || !hash_count) {
return PARSE_INVALID_ARG;
}
const char *s = strchr(input, '#');
if (!s) {
return PARSE_ERROR;
}
*sep = s;
*hash_count = 1;
while (s[*hash_count] == '#') {
(*hash_count)++;
}
return PARSE_OK;
}
/**
* @brief Format timestamp string based on the specified mode
*
* @param[in] timestamp_mode Timestamp mode configuration
* @param[in] frame_timestamp Frame timestamp in microseconds
* @param[in] start_time_us Start time for zero-based timestamps
* @param[in,out] last_frame_time_us Pointer to last frame time for delta mode (updated if delta mode)
* @param[out] timestamp_str Buffer to store formatted timestamp string
* @param[in] max_len Maximum length of timestamp string buffer
*/
void format_timestamp(timestamp_mode_t timestamp_mode, int64_t frame_timestamp,
int64_t start_time_us, int64_t *last_frame_time_us,
char *timestamp_str, size_t max_len);
/**
* @brief Format TWAI frame in twai_dump format
*
* @param[in] timestamp_mode Timestamp mode configuration
* @param[in] frame TWAI frame structure
* @param[in] frame_timestamp Frame timestamp in microseconds
* @param[in] start_time_us Start time for zero-based timestamps
* @param[in,out] last_frame_time_us Pointer to last frame time for delta mode (updated if delta mode)
* @param[in] controller_id Controller ID for interface name
* @param[out] output_line Buffer to store formatted output line
* @param[in] max_len Maximum length of output line buffer
*/
void format_twaidump_frame(timestamp_mode_t timestamp_mode, const twai_frame_t *frame,
int64_t frame_timestamp, int64_t start_time_us, int64_t *last_frame_time_us,
int controller_id, char *output_line, size_t max_len);
/**
* @brief Parse the controller ID string and return the end of the controller substring
*
* This function parses a controller string in the format "twai0", "twai1", ..., "twaix"
* and extracts the controller ID (0-x). It also supports controller strings with filters,
* such as "twai0,123:7FF", and returns a pointer to the end of the substring(e.g. the ',' or '\0').
*
* @param[in] controller_str Input controller string (e.g., "twai0" or "twai0,123:7FF")
* @param[out] controller_id Output pointer to store the parsed controller ID
*
* @return Pointer to the end of the controller substring (e.g., the ',' or '\0'), or NULL on error
*/
static inline const char *parse_controller_id(const char *controller_str, int *controller_id)
{
if (!controller_str || !controller_id) {
return NULL;
}
/* Support "twai0" ~ "twaix" format (which is dependent on SOC_TWAI_CONTROLLER_NUM) */
if (strncmp(controller_str, "twai", 4) == 0 && strlen(controller_str) >= 5) {
char id_char = controller_str[4];
if (id_char >= '0' && id_char <= '9' && id_char < '0' + SOC_TWAI_CONTROLLER_NUM) {
*controller_id = id_char - '0';
/* Return pointer to character after the ID digit */
return controller_str + 5;
}
}
return NULL;
}
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,743 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import logging
import re
import subprocess
import time
from collections.abc import Generator
from contextlib import contextmanager
from typing import Any
import can
import pexpect
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
# ---------------------------------------------------------------------------
# Constants / Helpers
# ---------------------------------------------------------------------------
PROMPTS = ['esp>', 'twai>', '>']
def _ctrl(controller_id: int) -> str:
return f'twai{controller_id}'
def _id_pattern(controller_str: str, can_id: int) -> str:
"""Return regex pattern for a dump line that contains this ctrl & CAN ID."""
hex_part = f'{can_id:08X}' if can_id > 0x7FF else f'{can_id:03X}'
return rf'{controller_str}\s+{hex_part}\s+\['
class TestConfig:
"""Test configuration"""
# Hardware configuration
DEFAULT_BITRATE = 500000
BITRATES = [125000, 250000, 500000, 1000000]
DEFAULT_TX_GPIO = 4
DEFAULT_RX_GPIO = 5
NO_TRANSCEIVER_GPIO = 4
# Test frame data
BASIC_FRAMES = [
('123#', 'Empty data'),
('124#AA', '1 byte'),
('125#DEADBEEF', '4 bytes'),
('126#DEADBEEFCAFEBABE', '8 bytes'),
]
EXTENDED_FRAMES = [
('12345678#ABCD', 'Extended frame'),
('1FFFFFFF#AA55BB66', 'Max extended ID'),
]
RTR_FRAMES = [
('123#R', 'RTR default'),
('124#R8', 'RTR 8 bytes'),
]
# FD frames (if FD is supported)
FD_FRAMES = [
('123##0AABBCC', 'FD frame without BRS'),
('456##1DEADBEEF', 'FD frame with BRS'),
('789##2CAFEBABE', 'FD frame with ESI'),
('ABC##3112233', 'FD frame with BRS+ESI'),
]
# Boundary ID tests
BOUNDARY_ID_FRAMES = [
('7FF#AA', 'Max standard ID'),
('800#BB', 'Min extended ID (in extended format: 00000800)'),
('000#CC', 'Min ID'),
]
INVALID_FRAMES = [
('G123#DEAD', 'Invalid ID character'),
('123#GG', 'Invalid data character'),
('123', 'Missing separator'),
('123#DEADBEEFCAFEBABEAA', 'Too much data'),
('123###DEAD', 'Too many separators'),
('123##', 'FD frame without data or flags'),
]
# Filter tests (includes both basic and extended frame filtering)
FILTER_TESTS = [
# No filter - basic functionality
(
'',
[
('123#DEAD', 0x123, True), # Standard frame passes
('12345678#CAFE', 0x12345678, True), # Extended frame passes
],
),
# Standard frame mask filter (is_ext=false by length=3, value<=0x7FF)
(
'123:7FF',
[
('123#DEAD', 0x123, True), # Standard frame matches
('456#BEEF', 0x456, False), # Standard frame doesn't match
('12345678#CAFE', 0x12345678, False), # Extended frame filtered out
],
),
# Extended frame mask filter (is_ext=true by length>3)
(
'12345678:1FFFFFFF',
[
('123#DEAD', 0x123, False), # Standard frame filtered out
('12345678#CAFE', 0x12345678, True), # Extended frame matches
],
),
# Extended frame mask filter (is_ext=true by value>0x7FF)
(
'800:1FFFFFFF',
[
('7FF#BEEF', 0x7FF, False), # Max standard ID filtered out
('800#CAFE', 0x800, True), # Extended ID matches exactly
],
),
]
# Range filter tests
RANGE_FILTER_TESTS = [
# Standard frame range filter
(
'a-15', # Test hex range parsing
[
('00a#DEAD', 0x00A, True), # Within range
('00f#BEEF', 0x00F, True), # Within range
('015#CAFE', 0x015, True), # At upper bound
('009#BABE', 0x009, False), # Below range
('016#FEED', 0x016, False), # Above range
],
),
# Extended frame range filter
(
'10000000-1FFFFFFF',
[
('123#DEAD', 0x123, False), # Standard frame filtered out
('0FFFFFFF#BEEF', 0x0FFFFFFF, False), # Below range
('10000000#CAFE', 0x10000000, True), # At lower bound
('1FFFFFFF#FEED', 0x1FFFFFFF, True), # At upper bound
],
),
]
# Rapid succession test frames
RAPID_FRAMES = ['123#AA', '124#BB', '125#CC', '126#DD', '127#EE']
# Basic send test frames
BASIC_SEND_FRAMES = ['123#DEADBEEF', '7FF#AA55', '12345678#CAFEBABE']
# ---------------------------------------------------------------------------
# TWAI helper (refactored)
# ---------------------------------------------------------------------------
class TwaiTestHelper:
"""TWAI test helper built on small, reusable atomic operations."""
def __init__(self, dut: Dut) -> None:
self.dut = dut
self.timeout = 5
self._wait_ready()
# ------------------------- atomic I/O ops -------------------------
def _wait_ready(self) -> None:
try:
self.dut.expect(PROMPTS, timeout=10)
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):
self.sendline('help')
self.expect(['Commands:'], timeout=5)
def sendline(self, cmd: str) -> None:
self.dut.write(f'\n{cmd}\n')
def expect(self, patterns: list[str] | str, timeout: float | None = None) -> bool:
timeout = timeout or self.timeout
try:
self.dut.expect(patterns, timeout=timeout)
return True
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):
return False
def run(self, cmd: str, expect: list[str] | str | None = None, timeout: float | None = None) -> bool:
self.sendline(cmd)
return self.expect(expect or PROMPTS, timeout)
# ------------------------- command builders -------------------------
def build_init(
self,
*,
controller_id: int = 0,
tx_gpio: int | None = None,
rx_gpio: int | None = None,
bitrate: int | None = None,
clk_out_gpio: int | None = None,
bus_off_gpio: int | None = None,
fd_bitrate: int | None = None,
loopback: bool = False,
self_test: bool = False,
listen: bool = False,
) -> str:
ctrl = _ctrl(controller_id)
parts = [f'twai_init {ctrl}']
if tx_gpio is not None:
parts += [f'-t {tx_gpio}']
if rx_gpio is not None:
parts += [f'-r {rx_gpio}']
if bitrate is not None:
parts += [f'-b {bitrate}']
if fd_bitrate is not None:
parts += [f'-B {fd_bitrate}']
if clk_out_gpio is not None:
parts += [f'-c {clk_out_gpio}']
if bus_off_gpio is not None:
parts += [f'-o {bus_off_gpio}']
if loopback:
parts += ['--loopback']
if self_test:
parts += ['--self-test']
if listen:
parts += ['--listen']
return ' '.join(parts)
def build_dump_start(self, *, controller_id: int = 0, dump_filter: str | None = None) -> str:
cmd = f'twai_dump {_ctrl(controller_id)}'
if dump_filter:
cmd += f',{dump_filter}'
return cmd
def build_dump_stop(self, *, controller_id: int = 0) -> str:
return f'twai_dump {_ctrl(controller_id)} --stop'
# ------------------------- high-level ops -------------------------
def init(self, controller_id: int = 0, **kwargs: Any) -> bool:
return self.run(self.build_init(controller_id=controller_id, **kwargs))
def deinit(self, controller_id: int = 0) -> bool:
return self.run(f'twai_deinit {_ctrl(controller_id)}')
def dump_start(self, controller_id: int = 0, dump_filter: str | None = None) -> bool:
return self.run(self.build_dump_start(controller_id=controller_id, dump_filter=dump_filter))
def dump_stop(self, controller_id: int = 0) -> tuple[bool, bool]:
"""Stop dump and return (stopped_ok, timeout_warning_seen)."""
self.sendline(self.build_dump_stop(controller_id=controller_id))
# If the dump task does not exit naturally, the implementation prints this warning.
warning_seen = self.expect(r'Dump task did not exit naturally, timeout', timeout=5)
# Whether or not warning appears, we should be back to a prompt.
prompt_ok = self.expect(PROMPTS, timeout=2) or True # relax
return prompt_ok, warning_seen
def send(self, frame_str: str, controller_id: int = 0) -> bool:
return self.run(f'twai_send {_ctrl(controller_id)} {frame_str}')
def info(self, controller_id: int = 0) -> bool:
return self.run(
f'twai_info {_ctrl(controller_id)}',
[rf'TWAI{controller_id} Status:', r'Node State:', r'Bitrate:'],
)
def recover(self, controller_id: int = 0, timeout_ms: int | None = None) -> bool:
cmd = f'twai_recover {_ctrl(controller_id)}'
if timeout_ms is not None:
cmd += f' -t {timeout_ms}'
return self.run(cmd, ['Recovery not needed', 'node is Error Active', 'ESP_ERR_INVALID_STATE']) # any
def expect_info_format(self, controller_id: int = 0) -> bool:
self.sendline(f'twai_info {_ctrl(controller_id)}')
checks = [
rf'TWAI{controller_id} Status: \w+',
r'Node State: \w+',
r'Error Counters: TX=\d+, RX=\d+',
r'Bitrate: \d+ bps',
]
return all(self.expect(p, timeout=2) for p in checks)
def invalid_should_fail(self, cmd: str, timeout: float = 2.0) -> bool:
self.sendline(cmd)
return self.expect([r'Command returned non-zero error code:', r'ERROR', r'Failed', r'Invalid'], timeout=timeout)
def test_with_patterns(self, cmd: str, patterns: list[str], timeout: float = 3.0) -> bool:
self.sendline(cmd)
return all(self.expect(p, timeout=timeout) for p in patterns)
def send_and_expect_in_dump(
self,
frame_str: str,
expected_id: int,
controller_id: int = 0,
timeout: float = 3.0,
) -> bool:
ctrl = _ctrl(controller_id)
self.sendline(f'twai_send {ctrl} {frame_str}')
return self.expect(_id_pattern(ctrl, expected_id), timeout=timeout)
# ------------------------- context manager -------------------------
@contextmanager
def session(
self,
*,
controller_id: int = 0,
mode: str = 'no_transceiver',
start_dump: bool = True,
dump_filter: str | None = None,
**kwargs: Any,
) -> Generator['TwaiTestHelper', None, None]:
"""Manage init/dump lifecycle consistently.
- mode="no_transceiver": loopback + self_test on a single GPIO.
- mode="standard": caller must provide tx_gpio/rx_gpio (or we use defaults).
"""
# Build effective init args
init_args = dict(kwargs)
init_args['controller_id'] = controller_id
if mode == 'no_transceiver':
init_args |= dict(
tx_gpio=TestConfig.NO_TRANSCEIVER_GPIO,
rx_gpio=TestConfig.NO_TRANSCEIVER_GPIO,
bitrate=kwargs.get('bitrate', TestConfig.DEFAULT_BITRATE),
loopback=True,
self_test=True,
)
elif mode == 'standard':
init_args.setdefault('tx_gpio', TestConfig.DEFAULT_TX_GPIO)
init_args.setdefault('rx_gpio', TestConfig.DEFAULT_RX_GPIO)
init_args.setdefault('bitrate', kwargs.get('bitrate', TestConfig.DEFAULT_BITRATE))
else:
raise ValueError(f'Unknown mode: {mode}')
if not self.init(**init_args):
raise RuntimeError(f'Failed to initialize TWAI in {mode} mode')
dump_started = False
dump_timeout_flag = False
try:
if start_dump:
dump_started = self.dump_start(controller_id=controller_id, dump_filter=dump_filter)
yield self
finally:
if dump_started:
_, warning = self.dump_stop(controller_id=controller_id)
dump_timeout_flag = warning
self.deinit(controller_id=controller_id)
if dump_timeout_flag:
pytest.fail(f'Dump stop timed out for {_ctrl(controller_id)}')
# ---------------------------------------------------------------------------
# CAN bus manager (external hardware)
# ---------------------------------------------------------------------------
class CanBusManager:
"""CAN bus manager for external hardware tests"""
def __init__(self, interface: str = 'can0'):
self.interface = interface
self.bus: can.Bus | None = None
@contextmanager
def managed_bus(self, bitrate: int = 500000) -> Generator[can.Bus, None, None]:
try:
result = subprocess.run(['ip', '-details', 'link', 'show', self.interface], capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f'CAN interface {self.interface} not found')
interface_up = 'UP' in result.stdout
current_bitrate = None
m = re.search(r'bitrate (\d+)', result.stdout)
if m:
current_bitrate = int(m.group(1))
if current_bitrate != bitrate:
logging.info(
f'Configuring CAN interface: current_bitrate={current_bitrate}, required_bitrate={bitrate}'
)
try:
if interface_up:
subprocess.run(
['sudo', '-n', 'ip', 'link', 'set', self.interface, 'down'], check=True, capture_output=True
)
subprocess.run(
[
'sudo',
'-n',
'ip',
'link',
'set',
self.interface,
'up',
'type',
'can',
'bitrate',
str(bitrate),
],
check=True,
capture_output=True,
)
time.sleep(0.5)
except subprocess.CalledProcessError:
raise Exception(
f'Failed to configure CAN interface {self.interface}. '
f'Try: sudo ip link set {self.interface} down && '
f'sudo ip link set {self.interface} up type can bitrate {bitrate}'
)
self.bus = can.Bus(interface='socketcan', channel=self.interface)
yield self.bus
except Exception as e:
pytest.skip(f'CAN interface not available: {str(e)}')
finally:
if self.bus:
try:
self.bus.shutdown()
except Exception:
pass
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def twai(dut: Dut) -> TwaiTestHelper:
return TwaiTestHelper(dut)
@pytest.fixture
def can_manager() -> CanBusManager:
return CanBusManager()
# ---------------------------------------------------------------------------
# CORE TESTS
# ---------------------------------------------------------------------------
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_basic_operations(twai: TwaiTestHelper) -> None:
with twai.session(
mode='standard', tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO, start_dump=False
):
# Test basic send operation
assert twai.send('123#DEADBEEF'), 'Basic send operation failed'
# Test dump filter operations - first start should succeed
assert twai.dump_start(dump_filter='123:7FF'), 'First dump start failed'
# Second start should be handled gracefully (already running)
twai.dump_start(dump_filter='456:7FF') # Should handle "already running" case
# Stop should work normally
stopped_ok, warning = twai.dump_stop()
assert stopped_ok, 'Dump stop failed'
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_bitrate_configuration(twai: TwaiTestHelper) -> None:
for bitrate in TestConfig.BITRATES:
with twai.session(
mode='standard', bitrate=bitrate, tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO
):
assert twai.info(), f'Info failed for bitrate {bitrate}'
# TWAI-FD bitrate validation (intentionally invalid: data bitrate < arbitration)
if twai.init(
tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO, bitrate=1_000_000, fd_bitrate=500_000
):
try:
ok = twai.test_with_patterns(
f'twai_info {_ctrl(0)}',
[r'TWAI0 Status:', r'Bitrate: 1000000'],
)
assert ok, 'FD bitrate validation info failed'
finally:
twai.deinit()
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_frame_formats(twai: TwaiTestHelper) -> None:
with twai.session():
for frame_str, desc in TestConfig.BASIC_FRAMES:
can_id = int(frame_str.split('#')[0], 16)
assert twai.send_and_expect_in_dump(frame_str, can_id), f'Basic frame failed: {frame_str} ({desc})'
for frame_str, desc in TestConfig.EXTENDED_FRAMES:
can_id = int(frame_str.split('#')[0], 16)
assert twai.send_and_expect_in_dump(frame_str, can_id), f'Extended frame failed: {frame_str} ({desc})'
for frame_str, desc in TestConfig.RTR_FRAMES:
assert twai.send(frame_str), f'RTR frame failed: {frame_str} ({desc})'
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_info_and_recovery(twai: TwaiTestHelper) -> None:
with twai.session():
assert twai.info(), 'Info command failed'
assert twai.expect_info_format(), 'Info format check failed'
assert twai.test_with_patterns(
f'twai_info {_ctrl(0)}',
[
r'TWAI0 Status: Running',
r'Node State: Error Active',
r'Error Counters: TX=0, RX=0',
],
), 'Expected status patterns not found'
assert twai.recover(), 'Recover status check failed'
assert twai.recover(timeout_ms=1000), 'Recover command with timeout failed'
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_input_validation(twai: TwaiTestHelper) -> None:
with twai.session(start_dump=False):
for frame_str, desc in TestConfig.INVALID_FRAMES:
assert twai.invalid_should_fail(f'twai_send {_ctrl(0)} {frame_str}'), (
f'Invalid frame should be rejected: {frame_str} ({desc})'
)
invalid_commands = [
'twai_init', # Missing controller ID
f'twai_init {_ctrl(0)}', # Missing required GPIO
'twai_init twai99 -t 4 -r 5', # Invalid controller ID
f'twai_recover {_ctrl(0)} -t -5', # Invalid timeout value
f'twai_init {_ctrl(0)} -t -1 -r 5', # Negative TX GPIO
f'twai_init {_ctrl(0)} -t 99 -r 5', # High GPIO number
f'twai_init {_ctrl(0)} -t 4 -r 5 -c -1', # Negative clk_out GPIO
f'twai_init {_ctrl(0)} -t 4 -r 5 -b 0', # Zero bitrate
]
for cmd in invalid_commands:
assert twai.invalid_should_fail(cmd), f'Invalid command should fail: {cmd}'
uninitialized_ops = [f'twai_send {_ctrl(0)} 123#DEAD', f'twai_recover {_ctrl(0)}', f'twai_dump {_ctrl(0)}']
for cmd in uninitialized_ops:
assert twai.invalid_should_fail(cmd), f'Non-initialized operation should fail: {cmd}'
with twai.session(start_dump=False):
assert twai.invalid_should_fail(f'twai_init {_ctrl(0)} -t 4 -r 5'), (
'Duplicate initialization should be prevented'
)
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_gpio_and_basic_send(twai: TwaiTestHelper) -> None:
with twai.session():
assert twai.send('123#DEADBEEF'), 'No-transceiver send failed'
with twai.session(mode='standard', tx_gpio=TestConfig.DEFAULT_TX_GPIO, rx_gpio=TestConfig.DEFAULT_RX_GPIO):
assert twai.info(), 'GPIO info failed'
assert twai.test_with_patterns(
f'twai_info {_ctrl(0)}',
[rf'GPIOs: TX=GPIO{TestConfig.DEFAULT_TX_GPIO}, RX=GPIO{TestConfig.DEFAULT_RX_GPIO}'],
)
for frame_str in TestConfig.BASIC_SEND_FRAMES:
assert twai.send(frame_str), f'Standard mode send failed: {frame_str}'
if twai.init(tx_gpio=4, rx_gpio=5, clk_out_gpio=6, bus_off_gpio=7):
try:
assert twai.info(), 'Optional GPIO info failed'
assert twai.test_with_patterns(f'twai_info {_ctrl(0)}', [r'TWAI0 Status:', r'GPIOs: TX=GPIO4']), (
'GPIO info format failed'
)
finally:
twai.deinit()
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_send_various_frames(twai: TwaiTestHelper) -> None:
with twai.session():
for frame_str, desc in TestConfig.BOUNDARY_ID_FRAMES:
assert twai.send(frame_str), f'Boundary ID failed: {frame_str} ({desc})'
for frame_str in TestConfig.RAPID_FRAMES:
assert twai.send(frame_str), f'Rapid send failed: {frame_str}'
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORT_FD == 1'), indirect=['target'])
def test_twai_fd_frames(twai: TwaiTestHelper) -> None:
with twai.session():
for frame_str, desc in TestConfig.FD_FRAMES:
assert twai.send(frame_str), f'FD frame failed: {frame_str} ({desc})'
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_filtering(twai: TwaiTestHelper) -> None:
"""Test TWAI filtering including automatic extended frame detection."""
for filter_str, test_frames in TestConfig.FILTER_TESTS:
with twai.session(dump_filter=filter_str):
failed_cases: list[str] = []
for frame_str, expected_id, should_receive in test_frames:
received = twai.send_and_expect_in_dump(frame_str, expected_id, timeout=1.0)
if received != should_receive:
expected_action = 'receive' if should_receive else 'filter out'
actual_action = 'received' if received else 'filtered out'
failed_cases.append(f'{frame_str}: expected {expected_action}, got {actual_action}')
if failed_cases:
pytest.fail(
f'Filter test failed for filter "{filter_str or "no filter"}":\n'
+ '\n'.join(failed_cases)
+ '\n\nNote: Filters auto-detect extended frames by:'
'\n- String length > 3 chars or ID value > 0x7FF'
)
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_RANGE_FILTER_NUM > 0'), indirect=['target'])
def test_twai_range_filters(twai: TwaiTestHelper) -> None:
"""Test TWAI range filters (available on chips with range filter support)."""
for filter_str, test_frames in TestConfig.RANGE_FILTER_TESTS:
with twai.session(dump_filter=filter_str):
failed_cases: list[str] = []
for frame_str, expected_id, should_receive in test_frames:
received = twai.send_and_expect_in_dump(frame_str, expected_id, timeout=1.0)
if received != should_receive:
expected_action = 'receive' if should_receive else 'filter out'
actual_action = 'received' if received else 'filtered out'
failed_cases.append(f'{frame_str}: expected {expected_action}, got {actual_action}')
if failed_cases:
pytest.fail(f'Range filter failed for filter "{filter_str}":\n' + '\n'.join(failed_cases))
# ---------------------------------------------------------------------------
# EXTERNAL HARDWARE TESTS
# ---------------------------------------------------------------------------
@pytest.mark.twai_std
@pytest.mark.temp_skip_ci(targets=['esp32c5'], reason='no runner')
@idf_parametrize('target', soc_filtered_targets('SOC_TWAI_SUPPORTED == 1'), indirect=['target'])
def test_twai_external_communication(twai: TwaiTestHelper, can_manager: CanBusManager) -> None:
"""
Test bidirectional communication with external CAN interface (hardware level).
Requirements:
- ESP node connected to physical CAN transceiver, properly wired to PC's socketcan
interface (default can0) via CANH/CANL.
- PC has `python-can` and can0 is available.
- Bitrate matches TestConfig.DEFAULT_BITRATE (default 500 kbps).
"""
test_frames = [
('123#DEADBEEF', 0x123, bytes.fromhex('DEADBEEF'), False),
('7FF#AA55', 0x7FF, bytes.fromhex('AA55'), False),
('12345678#CAFEBABE', 0x12345678, bytes.fromhex('CAFEBABE'), True),
]
with can_manager.managed_bus(bitrate=TestConfig.DEFAULT_BITRATE) as can_bus:
with twai.session(
mode='standard',
tx_gpio=TestConfig.DEFAULT_TX_GPIO,
rx_gpio=TestConfig.DEFAULT_RX_GPIO,
bitrate=TestConfig.DEFAULT_BITRATE,
start_dump=False,
):
# --- ESP -> PC Connectivity Test ---
first_frame, test_id, test_data, test_extended = test_frames[0]
if not twai.send(first_frame):
pytest.skip(
f'ESP CAN send failed - check ESP GPIO '
f'{TestConfig.DEFAULT_TX_GPIO}/{TestConfig.DEFAULT_RX_GPIO} -> '
f'CAN transceiver connection'
)
deadline = time.time() + 3.0
got: can.Message | None = None
while time.time() < deadline:
try:
msg = can_bus.recv(timeout=0.2)
if msg and msg.arbitration_id == test_id:
got = msg
break
except Exception as e:
logging.debug(f'PC CAN receive exception: {e}')
if got is None:
pytest.skip(
'ESP->PC communication failed - check CAN transceiver -> PC can0 connection. '
"Verify wiring and 'sudo ip link set can0 up type can bitrate 500000'"
)
if got is not None and bytes(got.data) != test_data:
pytest.fail(
f'ESP->PC data corruption detected: expected {test_data.hex()}, got {bytes(got.data).hex()}'
)
# --- Full ESP -> PC Test ---
for frame_str, expected_id, expected_data, is_extended in test_frames:
assert twai.send(frame_str), f'ESP->PC send failed: {frame_str}'
deadline = time.time() + 1.0
got = None
while time.time() < deadline:
try:
msg = can_bus.recv(timeout=0.1)
if msg and msg.arbitration_id == expected_id:
got = msg
break
except Exception:
continue
assert got is not None, f'ESP->PC receive timeout for ID=0x{expected_id:X}'
assert bool(got.is_extended_id) == is_extended, (
f'ESP->PC extended flag mismatch for 0x{expected_id:X}: '
f'expected {is_extended}, got {got.is_extended_id}'
)
assert bytes(got.data) == expected_data, (
f'ESP->PC data mismatch for 0x{expected_id:X}: '
f'expected {expected_data.hex()}, got {bytes(got.data).hex()}'
)
# --- PC -> ESP ---
assert twai.dump_start(), 'Failed to start twai_dump'
assert twai.info(), 'Failed to get twai_info'
test_msg = can.Message(arbitration_id=test_id, data=test_data, is_extended_id=test_extended)
try:
can_bus.send(test_msg)
time.sleep(0.2)
assert twai.expect(_id_pattern('twai0', test_id), timeout=2.0), (
f'PC->ESP frame not received: ID=0x{test_id:X}, data={test_data.hex()}'
)
for frame_str, expected_id, expected_data, is_extended in test_frames[1:]:
msg = can.Message(arbitration_id=expected_id, data=expected_data, is_extended_id=is_extended)
can_bus.send(msg)
time.sleep(0.1)
assert twai.expect(_id_pattern('twai0', expected_id), timeout=1.0), (
f'PC->ESP frame not received: ID=0x{expected_id:X}, data={expected_data.hex()}'
)
finally:
twai.dump_stop()

View File

@@ -0,0 +1 @@
CONFIG_EXAMPLE_ENABLE_TWAI_FD=y