mirror of
https://github.com/espressif/esp-idf.git
synced 2025-10-03 18:40:59 +02:00
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:
@@ -530,6 +530,12 @@ examples/peripherals/twai/twai_self_test:
|
||||
temporary: true
|
||||
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:
|
||||
disable:
|
||||
- if: SOC_UHCI_SUPPORTED != 1
|
||||
|
8
examples/peripherals/twai/twai_utils/CMakeLists.txt
Normal file
8
examples/peripherals/twai/twai_utils/CMakeLists.txt
Normal 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)
|
572
examples/peripherals/twai/twai_utils/README.md
Normal file
572
examples/peripherals/twai/twai_utils/README.md
Normal 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
|
||||
```
|
4
examples/peripherals/twai/twai_utils/main/CMakeLists.txt
Normal file
4
examples/peripherals/twai/twai_utils/main/CMakeLists.txt
Normal 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 ".")
|
60
examples/peripherals/twai/twai_utils/main/Kconfig.projbuild
Normal file
60
examples/peripherals/twai/twai_utils/main/Kconfig.projbuild
Normal 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
|
51
examples/peripherals/twai/twai_utils/main/cmd_twai.c
Normal file
51
examples/peripherals/twai/twai_utils/main/cmd_twai.c
Normal 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");
|
||||
}
|
25
examples/peripherals/twai/twai_utils/main/cmd_twai.h
Normal file
25
examples/peripherals/twai/twai_utils/main/cmd_twai.h
Normal 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
|
749
examples/peripherals/twai/twai_utils/main/cmd_twai_core.c
Normal file
749
examples/peripherals/twai/twai_utils/main/cmd_twai_core.c
Normal 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");
|
||||
}
|
569
examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c
Normal file
569
examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c
Normal 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");
|
||||
}
|
144
examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h
Normal file
144
examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h
Normal 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
|
275
examples/peripherals/twai/twai_utils/main/cmd_twai_send.c
Normal file
275
examples/peripherals/twai/twai_utils/main/cmd_twai_send.c
Normal 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);
|
||||
}
|
||||
}
|
50
examples/peripherals/twai/twai_utils/main/twai_utils_main.c
Normal file
50
examples/peripherals/twai/twai_utils/main/twai_utils_main.c
Normal 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));
|
||||
}
|
335
examples/peripherals/twai/twai_utils/main/twai_utils_parser.c
Normal file
335
examples/peripherals/twai/twai_utils/main/twai_utils_parser.c
Normal 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");
|
||||
}
|
||||
}
|
267
examples/peripherals/twai/twai_utils/main/twai_utils_parser.h
Normal file
267
examples/peripherals/twai/twai_utils/main/twai_utils_parser.h
Normal 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
|
743
examples/peripherals/twai/twai_utils/pytest_twai_utils.py
Normal file
743
examples/peripherals/twai/twai_utils/pytest_twai_utils.py
Normal 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()
|
@@ -0,0 +1 @@
|
||||
CONFIG_EXAMPLE_ENABLE_TWAI_FD=y
|
Reference in New Issue
Block a user