From d25d82923f2002f5877b464678f9e6aecf2695a1 Mon Sep 17 00:00:00 2001 From: wanckl Date: Thu, 9 Jul 2026 15:43:29 +0800 Subject: [PATCH] feat(driver_twai): add usb<->twai candlelight example --- docs/en/api-reference/peripherals/twai.rst | 1 + docs/zh_CN/api-reference/peripherals/twai.rst | 1 + examples/peripherals/.build-test-rules.yml | 8 + .../twai/usb_twai_adapter/CMakeLists.txt | 8 + .../twai/usb_twai_adapter/README.md | 96 +++++ .../twai/usb_twai_adapter/main/CMakeLists.txt | 7 + .../main/candlelight_internal.h | 127 +++++++ .../usb_twai_adapter/main/candlelight_main.c | 32 ++ .../usb_twai_adapter/main/candlelight_twai.c | 343 ++++++++++++++++++ .../twai/usb_twai_adapter/main/gs_usb.c | 282 ++++++++++++++ .../twai/usb_twai_adapter/main/gs_usb.h | 205 +++++++++++ .../usb_twai_adapter/main/idf_component.yml | 3 + .../twai/usb_twai_adapter/sdkconfig.defaults | 1 + .../usb_twai_adapter/wireshark_can0_snap.png | Bin 0 -> 42765 bytes 14 files changed, 1114 insertions(+) create mode 100644 examples/peripherals/twai/usb_twai_adapter/CMakeLists.txt create mode 100644 examples/peripherals/twai/usb_twai_adapter/README.md create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/CMakeLists.txt create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/candlelight_internal.h create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/candlelight_main.c create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/candlelight_twai.c create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/gs_usb.c create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/gs_usb.h create mode 100644 examples/peripherals/twai/usb_twai_adapter/main/idf_component.yml create mode 100644 examples/peripherals/twai/usb_twai_adapter/sdkconfig.defaults create mode 100644 examples/peripherals/twai/usb_twai_adapter/wireshark_can0_snap.png diff --git a/docs/en/api-reference/peripherals/twai.rst b/docs/en/api-reference/peripherals/twai.rst index eb91aa087e1..c226ac8b044 100644 --- a/docs/en/api-reference/peripherals/twai.rst +++ b/docs/en/api-reference/peripherals/twai.rst @@ -459,6 +459,7 @@ Application Examples - :example:`peripherals/twai/twai_error_recovery` demonstrates how to recover nodes from the bus-off state and resume communication, as well as bus error reporting, node state changes, and other event information. - :example:`peripherals/twai/twai_network` using 2 nodes with different roles: transmitting and listening, demonstrates how to use the driver for single and bulk data transmission, as well as configure filters to receive these data. - :example:`peripherals/twai/cybergear` demonstrates how to control XiaoMi CyberGear motors via TWAI interface. + - :example:`peripherals/twai/usb_twai_adapter` demonstrates how to make an USB-CAN adapter and enumerate it to a socket can device. API Reference ============= diff --git a/docs/zh_CN/api-reference/peripherals/twai.rst b/docs/zh_CN/api-reference/peripherals/twai.rst index 4d8842ae26b..9af9893e3cd 100644 --- a/docs/zh_CN/api-reference/peripherals/twai.rst +++ b/docs/zh_CN/api-reference/peripherals/twai.rst @@ -459,6 +459,7 @@ TWAI控制器能够检测由于总线干扰产生的/损坏的不符合帧格式 - :example:`peripherals/twai/twai_error_recovery` 演示了总线错误上报,节点状态变化等事件信息,以及如何从离线状态恢复节点并重新进行通信。 - :example:`peripherals/twai/twai_network` 通过发送、监听, 2 个不同角色的节点,演示了如何使用驱动程序进行单次的和大量的数据发送,以及配置过滤器以接收这些数据。 - :example:`peripherals/twai/cybergear` 演示了如何通过 TWAI 接口控制 XiaoMi CyberGear 电机。 + - :example:`peripherals/twai/usb_twai_adapter` 演示了如何制作一个 USB-CAN 适配器并将其枚举为 SocketCAN 设备。 API 参考 ======== diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 90f9bb4e7a6..87a921ace77 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -810,6 +810,14 @@ examples/peripherals/twai/twai_utils: - console - soc +examples/peripherals/twai/usb_twai_adapter: + disable: + - if: SOC_TWAI_SUPPORTED != 1 or SOC_USB_OTG_SUPPORTED != 1 + depends_components: + - esp_driver_twai + - esp_hal_twai + - soc + examples/peripherals/uart/uart_dma_ota: disable: - if: SOC_UHCI_SUPPORTED != 1 diff --git a/examples/peripherals/twai/usb_twai_adapter/CMakeLists.txt b/examples/peripherals/twai/usb_twai_adapter/CMakeLists.txt new file mode 100644 index 00000000000..b57aedb6ca2 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following five 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.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +idf_build_set_property(MINIMAL_BUILD ON) +project(usb_twai_adapter) diff --git a/examples/peripherals/twai/usb_twai_adapter/README.md b/examples/peripherals/twai/usb_twai_adapter/README.md new file mode 100644 index 00000000000..555266af4d7 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/README.md @@ -0,0 +1,96 @@ +| Supported Targets | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | +| ----------------- | -------- | -------- | -------- | -------- | --------- | + +# USB TWAI Adapter Example + +This example turns an ESP chip into a USB-CAN adapter compatible with the Linux `gs_usb` driver. After flashing, the board appears on the host as a CAN network interface and forwards frames between USB and the TWAI bus. CAN FD is enabled on chips that support TWAI FD. + +## Hardware Required + +- An ESP development board with USB device support and TWAI support. +- A TWAI FD capable chip is required for CAN FD operation. +- A TWAI transceiver, such as SN65HVD230 or TJA1050. +- A USB cable and jumper wires. + +## Hardware Setup + +Connect the ESP board to a TWAI transceiver: + +``` +ESP Pin Transceiver TWAI Bus +------- ----------- -------- +GPIO4 (TX) -> CTX +GPIO5 (RX) <- CRX +3.3V/5V -> VCC +GND -> GND + TWAI_H -> TWAI_H + TWAI_L -> TWAI_L +``` + +## Configure the Project + +The example uses the following defaults: + +- TWAI TX GPIO: `GPIO4` +- TWAI RX GPIO: `GPIO5` + +To change pins or defaults, edit [candlelight_internal.h](main/candlelight_internal.h). + +## Build and Flash + +```bash +idf.py -p PORT flash monitor +``` + +## Use on Linux + +After plugging the board into a Linux host via the chip's native USB device port, confirm the device enumerates (OpenMoko candleLight VID/PID so the in-tree `gs_usb` driver binds): + +```bash +lsusb +# Bus 001 Device 011: ID 1d50:606f OpenMoko, Inc. Geschwister Schneider CAN adapter +``` + +Then check that a CAN interface appears: + +```bash +ip link show +``` + +Bring the interface up, then use standard SocketCAN tools: + +```bash +sudo ip link set can0 up type can bitrate 500000 dbitrate 2000000 fd on +candump can0 +cansend can0 123##1DEADBEEF +``` + +For classic CAN only, omit the FD options: + +```bash +sudo ip link set can0 up type can bitrate 500000 +``` + +Monitor CAN frames transaction: + +```bash +candump can0 -ex +``` + +Which should print the frames you have send or received like (where TX/RX shows directions): +``` +~$ candump can0 -ex + can0 TX B - 123 [04] DE AD BE EF + can0 RX - - 0B7 [04] 60 88 DE 53 + can0 RX - - 09D [16] 8B A9 E4 1E 2E 07 13 58 8B A9 E4 1E 2E 07 13 58 +``` + +Or monitor transactions from `wireshark`, it will show both send and echo frames: + +![Wireshark CAN0 capture](wireshark_can0_snap.png) + +Bring the interface down when finished: + +```bash +sudo ip link set can0 down +``` diff --git a/examples/peripherals/twai/usb_twai_adapter/main/CMakeLists.txt b/examples/peripherals/twai/usb_twai_adapter/main/CMakeLists.txt new file mode 100644 index 00000000000..9a6f98b7fb0 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS "candlelight_main.c" + "candlelight_twai.c" + "gs_usb.c" + INCLUDE_DIRS "." + REQUIRES esp_driver_twai esp_timer +) diff --git a/examples/peripherals/twai/usb_twai_adapter/main/candlelight_internal.h b/examples/peripherals/twai/usb_twai_adapter/main/candlelight_internal.h new file mode 100644 index 00000000000..a3a51495635 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/candlelight_internal.h @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * USB-CAN (gs_usb / candleLight) adapter internals. + * + * Control path: TinyUSB vendor control transfers (bit timing, start/stop, caps). + * Data path: vendor bulk endpoints carry a fixed-length byte stream of gs_host_frame. + * Host TX confirmation: after TWAI finishes a host-originated frame, echo the same + * gs_host_frame back on USB (see tx_echo_task). RX frames use echo_id = UINT32_MAX. + */ + +#pragma once + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "esp_err.h" +#include "esp_twai.h" +#include "esp_twai_onchip.h" +#include "hal/twai_types.h" +#include "gs_usb.h" + +#define CANDLELIGHT_TAG "candlelight_twai" + +/* sw_version: keep > 2 so Linux does not apply legacy device quirks; YYMMDD is fine. + * hw_version: board/hardware revision, start from 1. + */ +#define GS_DEVICE_SW_VERSION 260715 +#define GS_DEVICE_HW_VERSION 1 +#define GS_DEVICE_CHANNEL_COUNT 1 + +#define TWAI_TX_GPIO 4 +#define TWAI_RX_GPIO 5 + +/* Frame pool depth for each directional buffer (USB->TWAI and TWAI->USB), must be a power of 2. */ +#define FRAME_POOL_DEPTH 256 +_Static_assert((UINT32_MAX % FRAME_POOL_DEPTH) == (FRAME_POOL_DEPTH - 1), "invalid FRAME_POOL_DEPTH value"); + +enum { + ITF_NUM_VENDOR = 0, /* TinyUSB vendor interface index for gs_usb bulk endpoints */ + ITF_NUM_TOTAL, /* Number of USB interfaces in the configuration descriptor */ +}; + +/** + * One pool slot: TWAI header + gs_usb wire frame. + * twai_frame.buffer points at gs_frame.data so payload is zero-copied. + */ +typedef struct { + twai_frame_t twai_frame; + struct gs_host_frame gs_frame; +} adapter_frame_t; + +/** + * Ring of adapter frames for one direction. + * + * TX and RX use separate pools: USB->TWAI (tx_pool) and TWAI->USB (rx_pool) have + * different producers/consumers and overflow rules (RX keeps one slot for error frames). + * in_idx is the next free write slot; out_idx is the next slot to consume. + */ +typedef struct { + adapter_frame_t frame[FRAME_POOL_DEPTH]; + uint32_t in_idx; + uint32_t out_idx; +} adapter_frame_pool_t; + +/* Shared runtime context for the USB-to-TWAI adapter tasks and state. */ +typedef struct { + adapter_frame_pool_t tx_pool; + adapter_frame_pool_t rx_pool; + SemaphoreHandle_t usb_tx_mutex; + SemaphoreHandle_t tx_done_sem; + SemaphoreHandle_t rx_cnt_sem; + + TaskHandle_t twai_rx_task_handle; + TaskHandle_t tx_echo_task_handle; + + struct gs_host_config host_config; + struct gs_device_bt_const_extended gsdev_bt_const; + struct gs_device_bittiming requested_bittiming; + struct gs_device_bittiming requested_data_bittiming; + struct gs_device_mode requested_mode; + struct gs_device_state device_state; + uint32_t device_timestamp_us; + + twai_node_handle_t node_hdl; + uint32_t usb_rx_frame_size; /* Host -> device bulk frame size (no timestamp) */ + uint32_t usb_tx_frame_size; /* Device -> host bulk frame size (may include timestamp) */ + volatile uint32_t tud_rx_pending; +} adapter_ctx_t; + +extern adapter_ctx_t g_ctx; + +static inline adapter_frame_t *frame_pool_slot(adapter_frame_pool_t *pool, uint32_t idx) +{ + return &pool->frame[idx % FRAME_POOL_DEPTH]; +} + +static inline uint32_t frame_pool_count(const adapter_frame_pool_t *pool) +{ + return (uint32_t)(pool->in_idx - pool->out_idx); +} + +static inline bool frame_pool_full_with_reserved(const adapter_frame_pool_t *pool, uint32_t reserved_slots) +{ + return frame_pool_count(pool) >= (FRAME_POOL_DEPTH - reserved_slots); +} + +/* Populate GS-USB descriptors with the local TWAI hardware capabilities. */ +void candlelight_fetch_hw_caps(void); + +/* Initialize the USB device stack used by the Candlelight adapter. */ +esp_err_t candlelight_init_usb(void); + +/* Create and start the TWAI node used to exchange frames with the bus. */ +esp_err_t candlelight_twai_init_and_start(void); + +/* Send a frame to the TWAI driver. */ +void candlelight_twai_send_frame(adapter_frame_t *frame); + +/* Stop TWAI traffic and tasks, and delete the TWAI node. */ +void candlelight_twai_stop_and_delete(void); diff --git a/examples/peripherals/twai/usb_twai_adapter/main/candlelight_main.c b/examples/peripherals/twai/usb_twai_adapter/main/candlelight_main.c new file mode 100644 index 00000000000..9d7da1e32b6 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/candlelight_main.c @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "candlelight_internal.h" +#include "esp_log.h" +#include + +adapter_ctx_t g_ctx; // global context for the adapter + +void app_main(void) +{ + memset(&g_ctx, 0, sizeof(g_ctx)); + + /* + * Point each TWAI frame buffer at the same slot's gs_usb payload. + * Host MODE will chooses classic vs FD, classic uses first 8 bytes, FD uses up to 64. + */ + for (int i = 0; i < FRAME_POOL_DEPTH; i++) { + g_ctx.tx_pool.frame[i].twai_frame.buffer = g_ctx.tx_pool.frame[i].gs_frame.data; + g_ctx.rx_pool.frame[i].twai_frame.buffer = g_ctx.rx_pool.frame[i].gs_frame.data; + g_ctx.rx_pool.frame[i].twai_frame.buffer_len = 64; + } + ESP_LOGI(CANDLELIGHT_TAG, "Buffer initialized: %d slots for burst data", FRAME_POOL_DEPTH); + + // populate the hardware capabilities and initialize the USB stack + candlelight_fetch_hw_caps(); + candlelight_init_usb(); + // just return the main task, the tinyusb task already there handling. +} diff --git a/examples/peripherals/twai/usb_twai_adapter/main/candlelight_twai.c b/examples/peripherals/twai/usb_twai_adapter/main/candlelight_twai.c new file mode 100644 index 00000000000..82b480c68e8 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/candlelight_twai.c @@ -0,0 +1,343 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "candlelight_internal.h" +#include "esp_check.h" +#include "esp_log.h" +#include "tinyusb.h" + +/* Convert gs_usb header fields only; payload stays in the shared gs_frame.data buffer. */ +static void frame_gs_to_twai(twai_frame_t *twai_out, const struct gs_host_frame *gs_in) +{ + bool is_ext = !!(gs_in->can_id & CAN_EFF_FLAG); + + twai_out->header.id = gs_in->can_id & (is_ext ? TWAI_EXT_ID_MASK : TWAI_STD_ID_MASK); + twai_out->header.dlc = gs_in->can_dlc; + twai_out->header.ide = is_ext; + twai_out->header.rtr = !!(gs_in->can_id & CAN_RTR_FLAG); + twai_out->header.fdf = !!(gs_in->flags & GS_CAN_FLAG_FD); + twai_out->header.brs = !!(gs_in->flags & GS_CAN_FLAG_BRS); + twai_out->header.esi = !!(gs_in->flags & GS_CAN_FLAG_ESI); + twai_out->header.timestamp = 0; // tx don't use timestamp +} + +/* Same as frame_gs_to_twai: header only, payload already in place. */ +static void frame_twai_to_gs(struct gs_host_frame *gs_out, const twai_frame_t *twai_in, uint32_t echo_id) +{ + const twai_frame_header_t *twai_header = &twai_in->header; + + gs_out->echo_id = echo_id; + gs_out->can_id = twai_header->id & (twai_header->ide ? TWAI_EXT_ID_MASK : TWAI_STD_ID_MASK); + if (twai_header->ide) { + gs_out->can_id |= CAN_EFF_FLAG; + } + if (twai_header->rtr) { + gs_out->can_id |= CAN_RTR_FLAG; + } + gs_out->can_id = (twai_header->id & CAN_ERR_FLAG) ? twai_header->id : gs_out->can_id; + gs_out->can_dlc = twai_header->dlc; + gs_out->channel = 0; + gs_out->flags = (twai_header->fdf ? GS_CAN_FLAG_FD : 0) | + (twai_header->brs ? GS_CAN_FLAG_BRS : 0) | + (twai_header->esi ? GS_CAN_FLAG_ESI : 0); + if (g_ctx.requested_mode.flags & GS_CAN_MODE_HW_TIMESTAMP) { + /* TWAI node fills header.timestamp when timestamp_resolution_hz is enabled. */ + gs_host_frame_set_timestamp(gs_out, !!(g_ctx.requested_mode.flags & GS_CAN_MODE_FD), (uint32_t)twai_header->timestamp); + } +} + +static void timing_config_gs_to_twai(twai_timing_advanced_config_t *twai_bt, const struct gs_device_bittiming *gs_bt, bool is_fd) +{ + // gs_usb describes SEG1 as prop_seg + phase_seg1, but don't know them's hardware limits; split it for the TWAI HAL limits. + twai_timing_limits_t timing_limits = {}; + twai_node_onchip_get_timing_limits(is_fd, &timing_limits); + + uint32_t whole_seg1 = gs_bt->phase_seg1 + gs_bt->prop_seg; + twai_bt->tseg_1 = (whole_seg1 * 3) / 4; // tseg_1 is usually larger than prop_seg. + twai_bt->tseg_1 = MAX(timing_limits.tseg1_min, MIN(twai_bt->tseg_1, timing_limits.tseg1_max)); + twai_bt->prop_seg = whole_seg1 - twai_bt->tseg_1; + twai_bt->tseg_2 = gs_bt->phase_seg2; + twai_bt->sjw = gs_bt->sjw; + twai_bt->brp = gs_bt->brp; +} + +// The gs_usb driver receives state (active, warning ...) as special RX frame. +static void IRAM_ATTR make_state_change_frame(adapter_frame_t *frame, twai_error_state_t new_state) +{ + twai_frame_header_t *twai_header = &frame->twai_frame.header; + uint8_t *data = frame->gs_frame.data; + + memset(twai_header, 0, sizeof(twai_frame_header_t)); + memset(data, 0, CAN_ERR_DLC); + + twai_header->id = CAN_ERR_FLAG; + twai_header->dlc = CAN_ERR_DLC; + + switch (new_state) { + case TWAI_ERROR_ACTIVE: + twai_header->id |= CAN_ERR_CRTL; + data[1] = CAN_ERR_CRTL_ACTIVE; + break; + case TWAI_ERROR_WARNING: + twai_header->id |= CAN_ERR_CRTL; + data[1] = CAN_ERR_CRTL_TX_WARNING | CAN_ERR_CRTL_RX_WARNING; + break; + case TWAI_ERROR_PASSIVE: + twai_header->id |= CAN_ERR_CRTL; + data[1] = CAN_ERR_CRTL_TX_PASSIVE | CAN_ERR_CRTL_RX_PASSIVE; + break; + case TWAI_ERROR_BUS_OFF: + twai_header->id |= CAN_ERR_BUSOFF; + break; + default: + break; + } +} + +static bool IRAM_ATTR twai_tx_done_callback(twai_node_handle_t handle, const twai_tx_done_event_data_t *edata, void *user_ctx) +{ + (void)handle; + (void)edata; + (void)user_ctx; + + BaseType_t task_woken = pdFALSE; + xSemaphoreGiveFromISR(g_ctx.tx_done_sem, &task_woken); + return (task_woken == pdTRUE); +} + +static bool IRAM_ATTR twai_rx_done_callback(twai_node_handle_t handle, const twai_rx_done_event_data_t *edata, void *user_ctx) +{ + (void)edata; + (void)user_ctx; + + BaseType_t task_woken = pdFALSE; + adapter_frame_pool_t *rx_pool = &g_ctx.rx_pool; + + // Keep one slot free for state-change error frames. + if (frame_pool_full_with_reserved(rx_pool, 1)) { + ESP_EARLY_LOGW(CANDLELIGHT_TAG, "No mem, drop esp rx frame"); + return false; + } + + twai_frame_t *rx_frame = &frame_pool_slot(rx_pool, rx_pool->in_idx)->twai_frame; + if (twai_node_receive_from_isr(handle, rx_frame) == ESP_OK) { + rx_pool->in_idx++; + xSemaphoreGiveFromISR(g_ctx.rx_cnt_sem, &task_woken); + } + return (task_woken == pdTRUE); +} + +static bool IRAM_ATTR twai_state_change_callback(twai_node_handle_t handle, const twai_state_change_event_data_t *edata, void *user_ctx) +{ + (void)handle; + (void)user_ctx; + + BaseType_t task_woken = pdFALSE; + adapter_frame_pool_t *rx_pool = &g_ctx.rx_pool; + + if (frame_pool_full_with_reserved(rx_pool, 0)) { + ESP_EARLY_LOGW(CANDLELIGHT_TAG, "No mem, drop state frame"); + return false; + } + + // The state-change and RX callbacks run from the same ISR context, so in_idx does not need extra locking here. + make_state_change_frame(frame_pool_slot(rx_pool, rx_pool->in_idx), edata->new_sta); + rx_pool->in_idx++; + xSemaphoreGiveFromISR(g_ctx.rx_cnt_sem, &task_woken); + return (task_woken == pdTRUE); +} + +/* Echo host TX frames back on USB after TWAI TX-done; gs_usb uses this as TX confirmation. */ +static void tx_echo_task(void *param) +{ + (void)param; + + uint32_t pending_len = g_ctx.usb_tx_frame_size; + adapter_frame_pool_t *tx_pool = &g_ctx.tx_pool; + + while (1) { + xSemaphoreTake(g_ctx.usb_tx_mutex, portMAX_DELAY); + while (pending_len < g_ctx.usb_tx_frame_size) { + adapter_frame_t *frame = frame_pool_slot(tx_pool, tx_pool->out_idx); + uint8_t *usb_frame = (uint8_t *)&frame->gs_frame; + + pending_len += tud_vendor_n_write(ITF_NUM_VENDOR, usb_frame + pending_len, g_ctx.usb_tx_frame_size - pending_len); + tud_vendor_n_write_flush(ITF_NUM_VENDOR); + if (pending_len == g_ctx.usb_tx_frame_size) { + tx_pool->out_idx++; + break; + } + } + xSemaphoreGive(g_ctx.usb_tx_mutex); + + if (xSemaphoreTake(g_ctx.tx_done_sem, portMAX_DELAY) != pdTRUE) { + continue; + } + pending_len = 0; + } +} + +static void twai_rx_task(void *param) +{ + (void)param; + + uint32_t pending_len = g_ctx.usb_tx_frame_size; + adapter_frame_pool_t *rx_pool = &g_ctx.rx_pool; + + while (1) { + xSemaphoreTake(g_ctx.usb_tx_mutex, portMAX_DELAY); + while (pending_len < g_ctx.usb_tx_frame_size) { + adapter_frame_t *frame = frame_pool_slot(rx_pool, rx_pool->out_idx); + uint8_t *usb_frame = (uint8_t *)&frame->gs_frame; + + frame_twai_to_gs(&frame->gs_frame, &frame->twai_frame, GS_HOST_FRAME_ECHO_ID_RX); + if (frame->gs_frame.can_id & CAN_ERR_FLAG) { + twai_node_status_t twai_status; + twai_node_get_info(g_ctx.node_hdl, &twai_status, NULL); + frame->gs_frame.data[6] = twai_status.tx_error_count; + frame->gs_frame.data[7] = twai_status.rx_error_count; + } + + pending_len += tud_vendor_n_write(ITF_NUM_VENDOR, usb_frame + pending_len, g_ctx.usb_tx_frame_size - pending_len); + tud_vendor_n_write_flush(ITF_NUM_VENDOR); + if (pending_len == g_ctx.usb_tx_frame_size) { + rx_pool->out_idx++; + break; + } + } + xSemaphoreGive(g_ctx.usb_tx_mutex); + + if (xSemaphoreTake(g_ctx.rx_cnt_sem, portMAX_DELAY) != pdTRUE) { + continue; + } + pending_len = 0; + } +} + +/* Queue one USB-originated frame to TWAI; tx_echo_task reports completion to the host. */ +void candlelight_twai_send_frame(adapter_frame_t *frame) +{ + frame_gs_to_twai(&frame->twai_frame, &frame->gs_frame); + twai_node_transmit(g_ctx.node_hdl, &frame->twai_frame, portMAX_DELAY); +} + +// --------------- init and delete helpers --------------- +static void semaphore_delete_and_set_null(SemaphoreHandle_t *semaphore) +{ + if (*semaphore) { + vSemaphoreDelete(*semaphore); + *semaphore = NULL; + } +} + +static void runtime_resources_delete(void) +{ + semaphore_delete_and_set_null(&g_ctx.rx_cnt_sem); + semaphore_delete_and_set_null(&g_ctx.tx_done_sem); + semaphore_delete_and_set_null(&g_ctx.usb_tx_mutex); + g_ctx.tx_pool.in_idx = 0; + g_ctx.tx_pool.out_idx = 0; + g_ctx.rx_pool.in_idx = 0; + g_ctx.rx_pool.out_idx = 0; +} + +static esp_err_t runtime_resources_create(void) +{ + g_ctx.rx_cnt_sem = xSemaphoreCreateCounting(FRAME_POOL_DEPTH, 0); + g_ctx.tx_done_sem = xSemaphoreCreateCounting(FRAME_POOL_DEPTH, 0); + g_ctx.usb_tx_mutex = xSemaphoreCreateMutex(); + if (g_ctx.usb_tx_mutex && g_ctx.rx_cnt_sem && g_ctx.tx_done_sem) { + return ESP_OK; + } + runtime_resources_delete(); + return ESP_ERR_NO_MEM; +} + +esp_err_t candlelight_twai_init_and_start(void) +{ + esp_err_t ret = ESP_OK; + + candlelight_twai_stop_and_delete(); + + twai_onchip_node_config_t node_config = { + .io_cfg = { + .tx = TWAI_TX_GPIO, + .rx = TWAI_RX_GPIO, + .quanta_clk_out = GPIO_NUM_NC, + .bus_off_indicator = GPIO_NUM_NC, + }, + .bit_timing = { + .bitrate = 500000, // Just tmp bitrate for driver install, the usb will update the bitrate later. + }, + .timestamp_resolution_hz = (g_ctx.requested_mode.flags & GS_CAN_MODE_HW_TIMESTAMP) ? 1000000 : 0, + .tx_queue_depth = FRAME_POOL_DEPTH, + .fail_retry_cnt = (g_ctx.requested_mode.flags & GS_CAN_MODE_ONE_SHOT) ? 0 : -1, + .flags = { + .enable_loopback = !!(g_ctx.requested_mode.flags & GS_CAN_MODE_LOOP_BACK), + .enable_listen_only = !!(g_ctx.requested_mode.flags & GS_CAN_MODE_LISTEN_ONLY), + }, + }; + ESP_GOTO_ON_ERROR(runtime_resources_create(), err, CANDLELIGHT_TAG, "Failed to create runtime resources"); + ESP_GOTO_ON_ERROR(twai_new_node_onchip(&node_config, &g_ctx.node_hdl), err, CANDLELIGHT_TAG, "Failed to create TWAI node"); + + twai_event_callbacks_t user_cbs = { + .on_tx_done = twai_tx_done_callback, + .on_rx_done = twai_rx_done_callback, + .on_state_change = twai_state_change_callback, + }; + ESP_GOTO_ON_ERROR(twai_node_register_event_callbacks(g_ctx.node_hdl, &user_cbs, NULL), err, CANDLELIGHT_TAG, "Failed to register TWAI callbacks"); + + twai_timing_advanced_config_t btcfg = {}, dbtcfg = {}, *dbtcfg_ptr = NULL; + timing_config_gs_to_twai(&btcfg, &g_ctx.requested_bittiming, false); + // Classic TWAI maps non-zero ssp_offset to triple sampling; FD uses it as secondary sample point. + if (g_ctx.requested_mode.flags & GS_CAN_MODE_TRIPLE_SAMPLE) { + btcfg.ssp_offset = (uint8_t)(btcfg.prop_seg + btcfg.tseg_1); + } + ESP_LOGI(CANDLELIGHT_TAG, "btcfg brp %u prop %u seg1 %u seg2 %u sjw %u ssp %u", btcfg.brp, btcfg.prop_seg, btcfg.tseg_1, btcfg.tseg_2, btcfg.sjw, btcfg.ssp_offset); + if (g_ctx.requested_mode.flags & GS_CAN_MODE_FD) { + timing_config_gs_to_twai(&dbtcfg, &g_ctx.requested_data_bittiming, true); + dbtcfg_ptr = &dbtcfg; + ESP_LOGI(CANDLELIGHT_TAG, "dbtcfg brp %u prop %u seg1 %u seg2 %u sjw %u", dbtcfg.brp, dbtcfg.prop_seg, dbtcfg.tseg_1, dbtcfg.tseg_2, dbtcfg.sjw); + } + ESP_GOTO_ON_ERROR(twai_node_reconfig_timing(g_ctx.node_hdl, &btcfg, dbtcfg_ptr), err, CANDLELIGHT_TAG, "Failed to reconfigure TWAI timing"); + + ESP_GOTO_ON_ERROR(twai_node_enable(g_ctx.node_hdl), err, CANDLELIGHT_TAG, "Failed to enable TWAI node"); + + ESP_GOTO_ON_FALSE(pdPASS == xTaskCreate(tx_echo_task, "tx_echo_task", 4096, NULL, 5, &g_ctx.tx_echo_task_handle), + ESP_ERR_NO_MEM, err, CANDLELIGHT_TAG, "Failed to create TX echo task"); + ESP_GOTO_ON_FALSE(pdPASS == xTaskCreate(twai_rx_task, "twai_rx_task", 4096, NULL, 5, &g_ctx.twai_rx_task_handle), + ESP_ERR_NO_MEM, err, CANDLELIGHT_TAG, "Failed to create TWAI RX task"); + + return ESP_OK; + +err: + candlelight_twai_stop_and_delete(); + return ret; +} + +static void task_delete_and_set_null(TaskHandle_t *task_handle) +{ + if (*task_handle) { + vTaskDelete(*task_handle); + *task_handle = NULL; + } +} + +void candlelight_twai_stop_and_delete(void) +{ + if (g_ctx.node_hdl) { + twai_node_disable(g_ctx.node_hdl); + } + task_delete_and_set_null(&g_ctx.tx_echo_task_handle); + task_delete_and_set_null(&g_ctx.twai_rx_task_handle); + if (g_ctx.node_hdl) { + twai_node_delete(g_ctx.node_hdl); + g_ctx.node_hdl = NULL; + } + runtime_resources_delete(); +} diff --git a/examples/peripherals/twai/usb_twai_adapter/main/gs_usb.c b/examples/peripherals/twai/usb_twai_adapter/main/gs_usb.c new file mode 100644 index 00000000000..c0ceeac145e --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/gs_usb.c @@ -0,0 +1,282 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "candlelight_internal.h" +#include "esp_clk_tree.h" +#include "esp_check.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "tinyusb.h" +#include "tinyusb_default_config.h" + +#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_VENDOR_DESC_LEN) + +// gs_usb driver endpoints +enum { + EDPT_VENDOR_OUT = 0x02, + EDPT_VENDOR_IN = 0x81, +}; + +static const struct gs_device_config s_device_config = { + .icount = GS_DEVICE_CHANNEL_COUNT - 1, + .sw_version = GS_DEVICE_SW_VERSION, + .hw_version = GS_DEVICE_HW_VERSION, +}; + +// Fixed VID/PID (openmoko candleLight) so Linux loads the in-tree gs_usb driver. +static const tusb_desc_device_t s_device_desc = { + .bLength = sizeof(s_device_desc), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x1D50, + .idProduct = 0x606F, + .bcdDevice = 0x0100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01, +}; + +static const char *s_string_desc[] = { + (const char[]){ 0x09, 0x04 }, // 0: English (0x0409) + "Espressif System (SH).", // 1: Manufacturer + "TWAI based CandleLight CANFD", // 2: Product + "260715", // 3: Serial +}; + +static const uint8_t s_vendor_fs_config_desc[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, 0, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 0, EDPT_VENDOR_OUT, EDPT_VENDOR_IN, 64), +}; + +#if (TUD_OPT_HIGH_SPEED) +static const uint8_t s_vendor_hs_config_desc[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, 0, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 0, EDPT_VENDOR_OUT, EDPT_VENDOR_IN, 512), +}; +#endif // TUD_OPT_HIGH_SPEED + +static enum gs_can_state twai_state_to_gs_state(twai_error_state_t state) +{ + switch (state) { + case TWAI_ERROR_ACTIVE: + return GS_CAN_STATE_ERROR_ACTIVE; + case TWAI_ERROR_WARNING: + return GS_CAN_STATE_ERROR_WARNING; + case TWAI_ERROR_PASSIVE: + return GS_CAN_STATE_ERROR_PASSIVE; + case TWAI_ERROR_BUS_OFF: + return GS_CAN_STATE_BUS_OFF; + default: + return GS_CAN_STATE_STOPPED; + } +} + +static void timing_const_twai_to_gs(struct can_bt_const *bt_const, const twai_timing_limits_t *timing_limits) +{ + bt_const->tseg1_min = timing_limits->tseg1_min + timing_limits->prop_min; + bt_const->tseg1_max = timing_limits->tseg1_max + timing_limits->prop_max; + bt_const->tseg2_min = timing_limits->tseg2_min; + bt_const->tseg2_max = timing_limits->tseg2_max; + bt_const->sjw_max = timing_limits->sjw_max; + bt_const->brp_min = timing_limits->brp_min; + bt_const->brp_max = timing_limits->brp_max; + bt_const->brp_inc = timing_limits->brp_inc; +} + +/* + * gs_usb vendor control path. Each bRequest has SETUP then ACK stages. + * Typical host sequence: HOST_FORMAT -> GET_BT_CONST[_EXT] -> SET_BITTIMING + * [-> SET_DATA_BITTIMING] -> MODE(start) ... MODE(stop). + */ +bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +{ + if (request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR || + request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_INTERFACE) { + return false; + } + + ESP_LOGD(CANDLELIGHT_TAG, "tud_vendor_control_xfer_cb: request->bRequest = %d, stage = %d", request->bRequest, stage); + switch ((enum gs_usb_breq)request->bRequest) { + case GS_USB_BREQ_HOST_FORMAT: /* endianness probe */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, &g_ctx.host_config, sizeof(g_ctx.host_config)); + } + return true; + + case GS_USB_BREQ_DEVICE_CONFIG: /* channel count / versions */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, (void *)&s_device_config, sizeof(s_device_config)); + } + return true; + + case GS_USB_BREQ_GET_BT_CONST: /* classic timing limits */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, (void *)&g_ctx.gsdev_bt_const, sizeof(struct gs_device_bt_const)); + } + return true; + + /* only chips who report `GS_CAN_FEATURE_BT_CONST_EXT` will trigger this request */ + case GS_USB_BREQ_GET_BT_CONST_EXT: /* classic + FD data-phase limits */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, (void *)&g_ctx.gsdev_bt_const, sizeof(struct gs_device_bt_const_extended)); + } + return true; + + case GS_USB_BREQ_SET_BITTIMING: /* arbitration / classic bitrate */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, &g_ctx.requested_bittiming, sizeof(g_ctx.requested_bittiming)); + } + return true; + + case GS_USB_BREQ_SET_DATA_BITTIMING: /* FD data-phase bitrate */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, &g_ctx.requested_data_bittiming, sizeof(g_ctx.requested_data_bittiming)); + } + return true; + + case GS_USB_BREQ_MODE: /* start/stop channel; create/delete TWAI node */ + if (stage == CONTROL_STAGE_SETUP) { + return tud_control_xfer(rhport, request, &g_ctx.requested_mode, sizeof(g_ctx.requested_mode)); + } else if (stage == CONTROL_STAGE_ACK) { + if (g_ctx.requested_mode.mode == GS_CAN_MODE_START) { + // host request start, save configs and create twai node + g_ctx.tud_rx_pending = 0; + bool is_fd = g_ctx.requested_mode.flags & GS_CAN_MODE_FD; + bool hw_ts = g_ctx.requested_mode.flags & GS_CAN_MODE_HW_TIMESTAMP; + g_ctx.usb_rx_frame_size = is_fd ? GS_HOST_FRAME_FD_SIZE : GS_HOST_FRAME_CLASSIC_SIZE; + g_ctx.usb_tx_frame_size = g_ctx.usb_rx_frame_size + + (hw_ts ? GS_HOST_FRAME_TIMESTAMP_SIZE : 0); + if (candlelight_twai_init_and_start() != ESP_OK) { + g_ctx.requested_mode.mode = GS_CAN_MODE_RESET; + g_ctx.usb_rx_frame_size = 0; + g_ctx.usb_tx_frame_size = 0; + return false; + } + } else { + // host request stop, stop twai node and reset configs + candlelight_twai_stop_and_delete(); + g_ctx.usb_rx_frame_size = 0; + g_ctx.usb_tx_frame_size = 0; + } + } + return true; + + case GS_USB_BREQ_GET_STATE: /* error state + TEC/REC */ + if (stage == CONTROL_STAGE_SETUP) { + g_ctx.device_state.state = GS_CAN_STATE_STOPPED; + g_ctx.device_state.rxerr = 0; + g_ctx.device_state.txerr = 0; + twai_node_status_t status = {}; + if (g_ctx.node_hdl && g_ctx.requested_mode.mode == GS_CAN_MODE_START && + twai_node_get_info(g_ctx.node_hdl, &status, NULL) == ESP_OK) { + g_ctx.device_state.state = twai_state_to_gs_state(status.state); + g_ctx.device_state.rxerr = status.rx_error_count; + g_ctx.device_state.txerr = status.tx_error_count; + } + return tud_control_xfer(rhport, request, &g_ctx.device_state, sizeof(g_ctx.device_state)); + } + return true; + + case GS_USB_BREQ_TIMESTAMP: /* µs clock for host HW timestamp sync */ + if (stage == CONTROL_STAGE_SETUP) { + g_ctx.device_timestamp_us = (uint32_t)esp_timer_get_time(); + ESP_LOGI(CANDLELIGHT_TAG, "ts_sync: %u", g_ctx.device_timestamp_us); + return tud_control_xfer(rhport, request, &g_ctx.device_timestamp_us, sizeof(g_ctx.device_timestamp_us)); + } + return true; + + default: + return false; + } +} + +/* + * USB OUT path: host sends a fixed-length byte stream of gs_host_frame. + * Reassemble with usb_rx_frame_size (classic 20 or FD 76), then hand off to TWAI. + */ +void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint16_t bufsize) +{ + (void)buffer; + (void)bufsize; + + adapter_frame_pool_t *tx_pool = &g_ctx.tx_pool; + + if (g_ctx.usb_rx_frame_size == 0) { + return; + } + + // Host sends a fixed-length stream; slice into frames of usb_rx_frame_size. + while (tud_vendor_n_available(itf) > 0) { + uint8_t *tmp_frame = (uint8_t *) & (frame_pool_slot(tx_pool, tx_pool->in_idx)->gs_frame); + + g_ctx.tud_rx_pending += tud_vendor_n_read(itf, tmp_frame + g_ctx.tud_rx_pending, g_ctx.usb_rx_frame_size - g_ctx.tud_rx_pending); + if (g_ctx.tud_rx_pending < g_ctx.usb_rx_frame_size) { + break; + } + g_ctx.tud_rx_pending = 0; + + // The input stream writes directly into the next slot; keep that slot free until a full frame arrives. + if (frame_pool_full_with_reserved(tx_pool, 1)) { + ESP_LOGW(CANDLELIGHT_TAG, "No mem, drop usb frame"); + break; + } + + // as `tud_vendor_rx_cb` is task context, we can send frame here + candlelight_twai_send_frame(frame_pool_slot(tx_pool, tx_pool->in_idx)); + tx_pool->in_idx++; + } +} + +void candlelight_fetch_hw_caps(void) +{ + twai_timing_limits_t timing_limits = {}; + twai_node_onchip_get_timing_limits(false, &timing_limits); + timing_const_twai_to_gs(&g_ctx.gsdev_bt_const.bt_const, &timing_limits); + + uint32_t clk_src_freq_hz = 0; + esp_clk_tree_src_get_freq_hz(TWAI_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &clk_src_freq_hz); + g_ctx.gsdev_bt_const.fclk_can = clk_src_freq_hz; + g_ctx.gsdev_bt_const.feature = GS_CAN_FEATURE_LISTEN_ONLY | GS_CAN_FEATURE_LOOP_BACK | + GS_CAN_FEATURE_ONE_SHOT | GS_CAN_FEATURE_GET_STATE | + GS_CAN_FEATURE_TRIPLE_SAMPLE | GS_CAN_FEATURE_BERR_REPORTING | + GS_CAN_FEATURE_HW_TIMESTAMP; + +#if SOC_HAS(TWAI_FD) + twai_node_onchip_get_timing_limits(true, &timing_limits); + timing_const_twai_to_gs(&g_ctx.gsdev_bt_const.dbt_const, &timing_limits); + g_ctx.gsdev_bt_const.feature |= GS_CAN_FEATURE_FD | GS_CAN_FEATURE_BT_CONST_EXT; +#endif +} + +esp_err_t candlelight_init_usb(void) +{ + tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(); + tusb_cfg.phy.skip_setup = false; + tusb_cfg.phy.self_powered = false; + tusb_cfg.descriptor.device = &s_device_desc; + tusb_cfg.descriptor.string = s_string_desc; + tusb_cfg.descriptor.string_count = sizeof(s_string_desc) / sizeof(s_string_desc[0]); + tusb_cfg.descriptor.full_speed_config = s_vendor_fs_config_desc; +#if (TUD_OPT_HIGH_SPEED) + tusb_cfg.descriptor.high_speed_config = s_vendor_hs_config_desc; + tusb_cfg.descriptor.qualifier = NULL; +#endif // TUD_OPT_HIGH_SPEED + + ESP_RETURN_ON_ERROR(tinyusb_driver_install(&tusb_cfg), CANDLELIGHT_TAG, "tinyusb_driver_install failed"); + ESP_LOGI(CANDLELIGHT_TAG, "tinyusb_driver_install success"); + return ESP_OK; +} diff --git a/examples/peripherals/twai/usb_twai_adapter/main/gs_usb.h b/examples/peripherals/twai/usb_twai_adapter/main/gs_usb.h new file mode 100644 index 00000000000..be82cc62af2 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/gs_usb.h @@ -0,0 +1,205 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * gs_usb wire protocol definitions. + * + * Names and layout follow Linux drivers/net/can/usb/gs_usb.c + * (CAN names are kept on purpose). `struct can_bt_const` groups the + * timing-range fields that the kernel inlines in gs_device_bt_const. + */ + +#pragma once + +#include +#include +#include +#include + +/* Vendor control bRequest values used by the Linux gs_usb host driver. + * Comments mark requests this example does not handle (still kept for protocol parity). + */ +enum gs_usb_breq { + GS_USB_BREQ_HOST_FORMAT = 0, /* Host writes endianness probe value */ + GS_USB_BREQ_SET_BITTIMING, /* Classic / arbitration bit timing */ + GS_USB_BREQ_MODE, /* Start or stop the CAN channel */ + GS_USB_BREQ_BERR, /* Not implemented here (legacy bus-error counter) */ + GS_USB_BREQ_GET_BT_CONST, /* Classic bit-timing limits */ + GS_USB_BREQ_DEVICE_CONFIG, /* Channel count and versions */ + GS_USB_BREQ_TIMESTAMP, /* Device µs timestamp (host clock sync) */ + GS_USB_BREQ_IDENTIFY, /* Not implemented here (blink/identify LED) */ + GS_USB_BREQ_GET_USER_ID, /* Not implemented here */ + GS_USB_BREQ_SET_USER_ID, /* Not implemented here */ + GS_USB_BREQ_SET_DATA_BITTIMING, /* CAN FD data-phase bit timing */ + GS_USB_BREQ_GET_BT_CONST_EXT, /* Classic + FD data-phase limits */ + GS_USB_BREQ_SET_TERMINATION, /* Not implemented here (bus termination) */ + GS_USB_BREQ_GET_TERMINATION, /* Not implemented here */ + GS_USB_BREQ_GET_STATE, /* Error state and TEC/REC */ + /* Optional HW filter; Linux SocketCAN uses host-side software filters instead. */ + GS_USB_BREQ_SET_FILTER, /* Not implemented here */ + GS_USB_BREQ_GET_FILTER, /* Not implemented here */ +}; + +/* Channel start/stop, sent in gs_device_mode.mode */ +enum gs_can_mode { + GS_CAN_MODE_RESET = 0, + GS_CAN_MODE_START, +}; + +/* Controller error state, sent in gs_device_state.state */ +enum gs_can_state { + GS_CAN_STATE_ERROR_ACTIVE = 0, + GS_CAN_STATE_ERROR_WARNING, + GS_CAN_STATE_ERROR_PASSIVE, + GS_CAN_STATE_BUS_OFF, + GS_CAN_STATE_STOPPED, + GS_CAN_STATE_SLEEPING, +}; + +/* gs_device_mode.flags: host-requested operating modes */ +#define GS_CAN_MODE_NORMAL 0 +#define GS_CAN_MODE_LISTEN_ONLY (1U << 0) +#define GS_CAN_MODE_LOOP_BACK (1U << 1) +#define GS_CAN_MODE_TRIPLE_SAMPLE (1U << 2) +#define GS_CAN_MODE_ONE_SHOT (1U << 3) +#define GS_CAN_MODE_HW_TIMESTAMP (1U << 4) +#define GS_CAN_MODE_PAD_PKTS_TO_MAX_PKT_SIZE (1U << 7) +#define GS_CAN_MODE_FD (1U << 8) +#define GS_CAN_MODE_BERR_REPORTING (1U << 12) + +/* gs_device_bt_const.feature: capabilities advertised to the host */ +#define GS_CAN_FEATURE_LISTEN_ONLY (1U << 0) +#define GS_CAN_FEATURE_LOOP_BACK (1U << 1) +#define GS_CAN_FEATURE_TRIPLE_SAMPLE (1U << 2) +#define GS_CAN_FEATURE_ONE_SHOT (1U << 3) +#define GS_CAN_FEATURE_HW_TIMESTAMP (1U << 4) +#define GS_CAN_FEATURE_IDENTIFY (1U << 5) +#define GS_CAN_FEATURE_USER_ID (1U << 6) +#define GS_CAN_FEATURE_PAD_PKTS_TO_MAX_PKT_SIZE (1U << 7) +#define GS_CAN_FEATURE_FD (1U << 8) +#define GS_CAN_FEATURE_BT_CONST_EXT (1U << 10) +#define GS_CAN_FEATURE_TERMINATION (1U << 11) +#define GS_CAN_FEATURE_BERR_REPORTING (1U << 12) +#define GS_CAN_FEATURE_GET_STATE (1U << 13) + +/* gs_host_frame.flags */ +#define GS_CAN_FLAG_OVERFLOW (1U << 0) /* RX overflow since last frame */ +#define GS_CAN_FLAG_FD (1U << 1) /* CAN FD frame */ +#define GS_CAN_FLAG_BRS (1U << 2) /* Bit-rate switch */ +#define GS_CAN_FLAG_ESI (1U << 3) /* Error state indicator */ + +/* SocketCAN can_id flag bits, stored in gs_host_frame.can_id */ +#define CAN_EFF_FLAG 0x80000000U /* Extended 29-bit ID */ +#define CAN_RTR_FLAG 0x40000000U /* Remote transmission request */ +#define CAN_ERR_FLAG 0x20000000U /* Error frame (not a data frame) */ + +#define CAN_ERR_DLC 8 /* Error frames always use DLC 8 */ + +/* Error-class bits in can_id when CAN_ERR_FLAG is set */ +#define CAN_ERR_CRTL 0x00000004U +#define CAN_ERR_BUSOFF 0x00000040U +#define CAN_ERR_RESTARTED 0x00000100U + +/* Error-frame data[1] when CAN_ERR_CRTL is set */ +#define CAN_ERR_CRTL_RX_WARNING 0x04 +#define CAN_ERR_CRTL_TX_WARNING 0x08 +#define CAN_ERR_CRTL_RX_PASSIVE 0x10 +#define CAN_ERR_CRTL_TX_PASSIVE 0x20 +#define CAN_ERR_CRTL_ACTIVE 0x40 + +#define GS_HOST_FRAME_ECHO_ID_RX UINT32_MAX /* echo_id for frames received from the bus */ + +struct gs_host_config { + uint32_t byte_order; /* Host writes 0x0000beef so the device can detect endianness */ +} __attribute__((packed)); + +struct gs_device_config { + uint8_t reserved1; + uint8_t reserved2; + uint8_t reserved3; + uint8_t icount; /* Number of CAN channels minus 1 */ + uint32_t sw_version; + uint32_t hw_version; +} __attribute__((packed)); + +struct gs_device_mode { + uint32_t mode; /* GS_CAN_MODE_RESET or GS_CAN_MODE_START */ + uint32_t flags; /* GS_CAN_MODE_* bit mask */ +} __attribute__((packed)); + +struct gs_device_bittiming { + uint32_t prop_seg; /* Propagation segment, in time quanta */ + uint32_t phase_seg1; /* Phase segment 1, in time quanta */ + uint32_t phase_seg2; /* Phase segment 2, in time quanta */ + uint32_t sjw; /* Synchronization jump width, in time quanta */ + uint32_t brp; /* Bit-rate prescaler */ +} __attribute__((packed)); + +/* Hardware bit-timing ranges. Linux stores these fields inline in gs_device_bt_const. */ +struct can_bt_const { + uint32_t tseg1_min; /* Minimum of (prop_seg + phase_seg1) */ + uint32_t tseg1_max; + uint32_t tseg2_min; + uint32_t tseg2_max; + uint32_t sjw_max; + uint32_t brp_min; + uint32_t brp_max; + uint32_t brp_inc; /* Prescaler step (1 or 2 depending on hardware) */ +} __attribute__((packed)); + +struct gs_device_bt_const { + uint32_t feature; /* GS_CAN_FEATURE_* bit mask */ + uint32_t fclk_can; /* CAN clock in Hz, used with brp to form bit time */ + struct can_bt_const bt_const; /* Classic / arbitration timing limits */ +} __attribute__((packed)); + +/* Layout must begin with the same three members as gs_device_bt_const (feature, + * fclk_can, bt_const), in the same order/size, so GET_BT_CONST can reuse the + * leading bytes of this extended struct. + */ +struct gs_device_bt_const_extended { + uint32_t feature; + uint32_t fclk_can; + struct can_bt_const bt_const; /* Classic / arbitration timing limits */ + struct can_bt_const dbt_const; /* CAN FD data-phase timing limits */ +} __attribute__((packed)); + +struct gs_device_state { + uint32_t state; /* GS_CAN_STATE_* */ + uint32_t rxerr; /* Receive error counter (REC) */ + uint32_t txerr; /* Transmit error counter (TEC) */ +} __attribute__((packed)); + +struct gs_host_frame { + uint32_t echo_id; /* Host TX cookie; echo the same value when TX finishes. UINT32_MAX = RX from bus */ + uint32_t can_id; /* 11/29-bit ID plus CAN_EFF_FLAG / CAN_RTR_FLAG / CAN_ERR_FLAG */ + uint8_t can_dlc; /* DLC field (0-8 classic, 0-15 FD), not the byte length */ + uint8_t channel; /* CAN channel index on this USB device (gs_usb supports multi-port; this example has one channel, so always 0) */ + uint8_t flags; /* GS_CAN_FLAG_* */ + uint8_t reserved; + uint8_t data[64]; /* Payload; classic uses first 8 bytes, FD uses up to 64 */ + /* Appended on device->host frames when HW_TIMESTAMP is enabled (classic overlays data[8..11] instead). */ + uint32_t timestamp_us; +} __attribute__((packed)); + +#define GS_HOST_FRAME_HEADER_SIZE offsetof(struct gs_host_frame, data) +#define GS_HOST_FRAME_CLASSIC_SIZE (GS_HOST_FRAME_HEADER_SIZE + 8) +#define GS_HOST_FRAME_FD_SIZE (GS_HOST_FRAME_HEADER_SIZE + 64) +#define GS_HOST_FRAME_TIMESTAMP_SIZE sizeof(uint32_t) +#define GS_HOST_FRAME_CLASSIC_TS_SIZE (GS_HOST_FRAME_CLASSIC_SIZE + GS_HOST_FRAME_TIMESTAMP_SIZE) +#define GS_HOST_FRAME_FD_TS_SIZE (GS_HOST_FRAME_FD_SIZE + GS_HOST_FRAME_TIMESTAMP_SIZE) +_Static_assert(GS_HOST_FRAME_FD_TS_SIZE == sizeof(struct gs_host_frame), "FD+TS wire size must match struct"); +_Static_assert(GS_HOST_FRAME_CLASSIC_TS_SIZE == GS_HOST_FRAME_HEADER_SIZE + 12, "classic+TS wire size"); + +/* Classic+TS stores timestamp at data[8]; FD+TS uses timestamp_us after data[64]. */ +static inline void gs_host_frame_set_timestamp(struct gs_host_frame *frame, bool is_fd, uint32_t timestamp_us) +{ + if (is_fd) { + frame->timestamp_us = timestamp_us; + } else { + memcpy(&frame->data[8], ×tamp_us, sizeof(timestamp_us)); + } +} diff --git a/examples/peripherals/twai/usb_twai_adapter/main/idf_component.yml b/examples/peripherals/twai/usb_twai_adapter/main/idf_component.yml new file mode 100644 index 00000000000..a93ac9d21e2 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/main/idf_component.yml @@ -0,0 +1,3 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_tinyusb: "^2" diff --git a/examples/peripherals/twai/usb_twai_adapter/sdkconfig.defaults b/examples/peripherals/twai/usb_twai_adapter/sdkconfig.defaults new file mode 100644 index 00000000000..6462eb12fa0 --- /dev/null +++ b/examples/peripherals/twai/usb_twai_adapter/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_TINYUSB_VENDOR_COUNT=1 diff --git a/examples/peripherals/twai/usb_twai_adapter/wireshark_can0_snap.png b/examples/peripherals/twai/usb_twai_adapter/wireshark_can0_snap.png new file mode 100644 index 0000000000000000000000000000000000000000..7f816ac1c8c3b757a59c69cb3347586b1a977354 GIT binary patch literal 42765 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