diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 24872b7488..9f401658b1 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -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 diff --git a/examples/peripherals/twai/twai_utils/CMakeLists.txt b/examples/peripherals/twai/twai_utils/CMakeLists.txt new file mode 100644 index 0000000000..dbc9eca58a --- /dev/null +++ b/examples/peripherals/twai/twai_utils/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/twai/twai_utils/README.md b/examples/peripherals/twai/twai_utils/README.md new file mode 100644 index 0000000000..4dc7b8145f --- /dev/null +++ b/examples/peripherals/twai/twai_utils/README.md @@ -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 -t -r [opts]` | Initialize TWAI controller with GPIO and mode options | `ip link set can0 up type can bitrate 500000` | +| `twai_deinit ` | Deinitialize TWAI controller | `ip link set can0 down` | +| `twai_send ` | Send TWAI frame (standard/extended/RTR/FD) | `cansend can0 123#DEADBEEF` | +| `twai_dump [,filter] [-t mode]` / `twai_dump --stop` | Monitor TWAI traffic with hardware filtering and timestamps | `candump can0` | +| `twai_info ` | Display controller configuration, status | `ip -details link show can0` | +| `twai_recover [-t ]` | Recover controller from Bus-Off state | N/A | + +- Note: `twai_dump` runs continuously in the background. Use `twai_dump --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 -t -r [options]` + +**Arguments:** +- ``: 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 ` + +### `twai_send` +Sends a standard, extended, RTR, or TWAI-FD frame. + +**Usage:** +`twai_send ` + +**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 ] [,filter...]` +- `twai_dump --stop` + +- **Options:** +- `-t `: 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 ` + +**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 [-t ]` + +**Options:** +- `-t `: 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 ` 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 ` followed by `twai_init ` 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 +``` \ No newline at end of file diff --git a/examples/peripherals/twai/twai_utils/main/CMakeLists.txt b/examples/peripherals/twai/twai_utils/main/CMakeLists.txt new file mode 100644 index 0000000000..01368784f5 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/CMakeLists.txt @@ -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 ".") diff --git a/examples/peripherals/twai/twai_utils/main/Kconfig.projbuild b/examples/peripherals/twai/twai_utils/main/Kconfig.projbuild new file mode 100644 index 0000000000..06f67ff4b1 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/Kconfig.projbuild @@ -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 diff --git a/examples/peripherals/twai/twai_utils/main/cmd_twai.c b/examples/peripherals/twai/twai_utils/main/cmd_twai.c new file mode 100644 index 0000000000..9853fbd7bc --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/cmd_twai.c @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#include +#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"); +} diff --git a/examples/peripherals/twai/twai_utils/main/cmd_twai.h b/examples/peripherals/twai/twai_utils/main/cmd_twai.h new file mode 100644 index 0000000000..4b943f1b93 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/cmd_twai.h @@ -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 diff --git a/examples/peripherals/twai/twai_utils/main/cmd_twai_core.c b/examples/peripherals/twai/twai_utils/main/cmd_twai_core.c new file mode 100644 index 0000000000..dbfdddfea2 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/cmd_twai_core.c @@ -0,0 +1,749 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#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 )"); + return ESP_ERR_INVALID_ARG; + } + if (twai_init_args.rx_gpio->count == 0) { + ESP_LOGE(TAG, "RX GPIO argument is required (-r )"); + 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, "", "TWAI controller (twai0, twai1, etc.)"); + twai_init_args.tx_gpio = arg_int1("t", "tx", "", "TX GPIO pin number (required, e.g., 4)"); + twai_init_args.rx_gpio = arg_int1("r", "rx", "", "RX GPIO pin number (required, e.g., 5)"); + twai_init_args.rate = arg_int0("b", "bitrate", "", "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", "", "TWAI-FD data bitrate in bps (optional)"); + twai_init_args.clk_out_gpio = arg_int0("c", "clk-out", "", "Clock output GPIO pin (optional)"); + twai_init_args.bus_off_gpio = arg_int0("o", "bus-off", "", "Bus-off indicator GPIO pin (optional)"); + twai_init_args.end = arg_end(20); + + twai_deinit_args.controller = arg_str1(NULL, NULL, "", "TWAI controller (twai0, twai1)"); + twai_deinit_args.end = arg_end(20); + + twai_info_args.controller = arg_str1(NULL, NULL, "", "TWAI controller (twai0, twai1)"); + twai_info_args.end = arg_end(20); + + twai_recover_args.controller = arg_str1(NULL, NULL, "", "TWAI controller (twai0, twai1)"); + twai_recover_args.timeout = arg_int0("t", "timeout", "", "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 -t -r [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 # Block until complete (default)\n" + " twai_recover -t 0 # Async recovery\n" + " twai_recover -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"); +} diff --git a/examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c b/examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c new file mode 100644 index 0000000000..0b14f883cd --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/cmd_twai_dump.c @@ -0,0 +1,569 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#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: [,:[,:...]] */ + struct arg_lit *stop; /**< Stop option: --stop */ + struct arg_str *timestamp; /**< Timestamp mode: -t */ + 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, "[,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", "", + "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 ] [,filter...]\n" + " twai_dump --stop\n" + "\n" + "Options:\n" + " -t 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"); +} diff --git a/examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h b/examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h new file mode 100644 index 0000000000..839735867b --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/cmd_twai_internal.h @@ -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 +#include +#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 diff --git a/examples/peripherals/twai/twai_utils/main/cmd_twai_send.c b/examples/peripherals/twai/twai_utils/main/cmd_twai_send.c new file mode 100644 index 0000000000..e222e3f2b4 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/cmd_twai_send.c @@ -0,0 +1,275 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include +#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, "", "TWAI controller (e.g. twai0)"); + twai_send_args.frame = arg_str0(NULL, NULL, "", "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 \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 = " []", + .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); + } +} diff --git a/examples/peripherals/twai/twai_utils/main/twai_utils_main.c b/examples/peripherals/twai/twai_utils/main/twai_utils_main.c new file mode 100644 index 0000000000..4f53619622 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/twai_utils_main.c @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include +#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)); +} diff --git a/examples/peripherals/twai/twai_utils/main/twai_utils_parser.c b/examples/peripherals/twai/twai_utils/main/twai_utils_parser.c new file mode 100644 index 0000000000..6747255de6 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/twai_utils_parser.c @@ -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 +#include +#include + +/** + * @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"); + } +} diff --git a/examples/peripherals/twai/twai_utils/main/twai_utils_parser.h b/examples/peripherals/twai/twai_utils/main/twai_utils_parser.h new file mode 100644 index 0000000000..6dc06cf713 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/main/twai_utils_parser.h @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#include +#include +#include +#include +#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: {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: + * #{data} Data frame with up to 8 bytes + * #R{len} RTR frame with specified length + * #{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 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 diff --git a/examples/peripherals/twai/twai_utils/pytest_twai_utils.py b/examples/peripherals/twai/twai_utils/pytest_twai_utils.py new file mode 100644 index 0000000000..7c999ba040 --- /dev/null +++ b/examples/peripherals/twai/twai_utils/pytest_twai_utils.py @@ -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() diff --git a/examples/peripherals/twai/twai_utils/sdkconfig.defaults.esp32c5 b/examples/peripherals/twai/twai_utils/sdkconfig.defaults.esp32c5 new file mode 100644 index 0000000000..daff3b9a4d --- /dev/null +++ b/examples/peripherals/twai/twai_utils/sdkconfig.defaults.esp32c5 @@ -0,0 +1 @@ +CONFIG_EXAMPLE_ENABLE_TWAI_FD=y