diff --git a/components/esp_driver_twai/esp_twai.c b/components/esp_driver_twai/esp_twai.c index 26d86a5174b..69515cde732 100644 --- a/components/esp_driver_twai/esp_twai.c +++ b/components/esp_driver_twai/esp_twai.c @@ -23,12 +23,12 @@ * | tseg2/2 ^ ^ * sjw sample_point */ -uint32_t twai_node_timing_calc_param(const uint32_t source_freq, const twai_timing_basic_config_t *in_param, const twai_timing_constraint_t *hw_limit, twai_timing_advanced_config_t *out_param) +uint32_t twai_node_timing_calc_param(const uint32_t source_freq, const twai_timing_basic_config_t *in_param, const twai_timing_limits_t *hw_limit, twai_timing_advanced_config_t *out_param) { uint32_t total_div = (source_freq + in_param->bitrate / 2) / in_param->bitrate; uint32_t pre_div = hw_limit->brp_min; uint16_t tseg = 0; - for (; pre_div <= hw_limit->brp_max; pre_div ++) { + for (; pre_div <= hw_limit->brp_max; pre_div += hw_limit->brp_inc) { tseg = total_div / pre_div; if (total_div != tseg * pre_div) { continue; // no integer tseg diff --git a/components/esp_driver_twai/esp_twai_onchip.c b/components/esp_driver_twai/esp_twai_onchip.c index ae528af92a4..08bbaf4b491 100644 --- a/components/esp_driver_twai/esp_twai_onchip.c +++ b/components/esp_driver_twai/esp_twai_onchip.c @@ -436,19 +436,11 @@ static esp_err_t _node_calc_set_bit_timing(twai_node_handle_t node, const twai_t ESP_RETURN_ON_FALSE((!timing_fd->bitrate) || (timing_fd->bitrate == timing->bitrate), ESP_ERR_INVALID_ARG, TAG, "FD stage bitrate is not supported"); #endif - twai_timing_constraint_t hw_const = { - .brp_min = TWAI_LL_BRP_MIN, - .brp_max = TWAI_LL_BRP_MAX, - .prop_max = TWAI_LL_PROP_MAX, - .tseg1_min = TWAI_LL_TSEG1_MIN, - .tseg1_max = TWAI_LL_TSEG1_MAX, - .tseg2_min = TWAI_LL_TSEG2_MIN, - .tseg2_max = TWAI_LL_TSEG2_MAX, - .sjw_max = TWAI_LL_SJW_MAX, - }; + twai_timing_limits_t hw_limits = {}; + twai_hal_get_timing_limits(&hw_limits); twai_timing_advanced_config_t timing_adv = {}, *timing_fd_ptr = NULL; - uint32_t real_baud = twai_node_timing_calc_param(twai_ctx->src_freq_hz, timing, &hw_const, &timing_adv); + uint32_t real_baud = twai_node_timing_calc_param(twai_ctx->src_freq_hz, timing, &hw_limits, &timing_adv); ESP_LOGD(TAG, "timing: src %ld brp %ld prop %d seg1 %d seg2 %d sjw %d ssp %d", twai_ctx->src_freq_hz, timing_adv.brp, timing_adv.prop_seg, timing_adv.tseg_1, timing_adv.tseg_2, timing_adv.sjw, timing_adv.ssp_offset); ESP_RETURN_ON_FALSE(real_baud, ESP_ERR_INVALID_ARG, TAG, "bitrate can't achieve!"); if (timing->bitrate != real_baud) { @@ -457,12 +449,8 @@ static esp_err_t _node_calc_set_bit_timing(twai_node_handle_t node, const twai_t #if SOC_HAS(TWAI_FD) twai_timing_advanced_config_t timing_adv_fd = {}; if (timing_fd->bitrate) { - hw_const.brp_max = TWAI_LL_BRP_MAX_FD; - hw_const.prop_max = TWAI_LL_PROP_MAX_FD; - hw_const.tseg1_max = TWAI_LL_TSEG1_MAX_FD; - hw_const.tseg2_max = TWAI_LL_TSEG2_MAX_FD; - hw_const.sjw_max = TWAI_LL_SJW_MAX_FD; - real_baud = twai_node_timing_calc_param(twai_ctx->src_freq_hz, timing_fd, &hw_const, &timing_adv_fd); + twai_hal_get_timing_limits_fd(&hw_limits); + real_baud = twai_node_timing_calc_param(twai_ctx->src_freq_hz, timing_fd, &hw_limits, &timing_adv_fd); ESP_LOGD(TAG, "timing_fd: src %ld brp %ld prop %d seg1 %d seg2 %d sjw %d ssp %d", twai_ctx->src_freq_hz, timing_adv_fd.brp, timing_adv_fd.prop_seg, timing_adv_fd.tseg_1, timing_adv_fd.tseg_2, timing_adv_fd.sjw, timing_adv_fd.ssp_offset); ESP_RETURN_ON_FALSE(real_baud, ESP_ERR_INVALID_ARG, TAG, "bitrate can't achieve!"); if (timing_fd->bitrate != real_baud) { @@ -843,3 +831,19 @@ err: _node_destroy(node); return ret; } + +esp_err_t twai_node_onchip_get_timing_limits(bool is_fd, twai_timing_limits_t *timing_limits) +{ + ESP_RETURN_ON_FALSE(timing_limits, ESP_ERR_INVALID_ARG, TAG, "Invalid argument: null"); + + if (is_fd) { +#if SOC_HAS(TWAI_FD) + twai_hal_get_timing_limits_fd(timing_limits); +#else + ESP_RETURN_ON_ERROR(ESP_ERR_NOT_SUPPORTED, TAG, "FD is not supported"); +#endif + } else { + twai_hal_get_timing_limits(timing_limits); + } + return ESP_OK; +} diff --git a/components/esp_driver_twai/include/esp_private/twai_utils.h b/components/esp_driver_twai/include/esp_private/twai_utils.h index 587fc8a45f6..81239f8bd24 100644 --- a/components/esp_driver_twai/include/esp_private/twai_utils.h +++ b/components/esp_driver_twai/include/esp_private/twai_utils.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -11,22 +11,6 @@ extern "C" { #endif -/** - * @brief TWAI hardware-dependent bit-timing constant - * - * Used for calculating and checking bit-timing parameters - */ -typedef struct { - uint32_t brp_min; /* Bit-rate prescaler */ - uint32_t brp_max; - uint8_t prop_max; /* Propagation segment */ - uint8_t tseg1_min; /* Time segment 1 = prop_seg + phase_seg1 */ - uint8_t tseg1_max; - uint8_t tseg2_min; /* Time segment 2 = phase_seg2 */ - uint8_t tseg2_max; - uint8_t sjw_max; /* Synchronisation jump width */ -} twai_timing_constraint_t; - /** * @brief Calculate TWAI timing parameters for a given source frequency and baud rate. * @@ -40,7 +24,7 @@ typedef struct { * @param out_param Pointer to the output structure where the calculated timing parameters will be stored. * @return the actual hardware adopted baudrate. */ -uint32_t twai_node_timing_calc_param(const uint32_t source_freq, const twai_timing_basic_config_t *in_param, const twai_timing_constraint_t *hw_limit, twai_timing_advanced_config_t *out_param); +uint32_t twai_node_timing_calc_param(const uint32_t source_freq, const twai_timing_basic_config_t *in_param, const twai_timing_limits_t *hw_limit, twai_timing_advanced_config_t *out_param); #ifdef __cplusplus } diff --git a/components/esp_driver_twai/include/esp_twai_onchip.h b/components/esp_driver_twai/include/esp_twai_onchip.h index a5773868e55..ce95a8997c5 100644 --- a/components/esp_driver_twai/include/esp_twai_onchip.h +++ b/components/esp_driver_twai/include/esp_twai_onchip.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -101,6 +101,15 @@ static inline twai_mask_filter_config_t twai_make_dual_filter(uint32_t id1, uint return dual_cfg; } +/** + * @brief Get the hardware-dependent timing limits + * + * @param[in] is_fd True for FD data timing, false for classic timing + * @param[out] timing_limits Pointer to timing limits structure + * @return ESP_OK if successful, ESP_ERR_INVALID_ARG if invalid argument + */ +esp_err_t twai_node_onchip_get_timing_limits(bool is_fd, twai_timing_limits_t *timing_limits); + #ifdef __cplusplus } #endif diff --git a/components/esp_hal_twai/include/hal/twai_hal.h b/components/esp_hal_twai/include/hal/twai_hal.h index c3cec1b68e0..f6d204fc1c0 100644 --- a/components/esp_hal_twai/include/hal/twai_hal.h +++ b/components/esp_hal_twai/include/hal/twai_hal.h @@ -132,6 +132,20 @@ bool twai_hal_init(twai_hal_context_t *hal_ctx, const twai_hal_config_t *config) */ void twai_hal_deinit(twai_hal_context_t *hal_ctx); +/** + * @brief Get the hardware-dependent timing limits const + * + * @param t_const Pointer to timing limits const structure + */ +void twai_hal_get_timing_limits(twai_timing_limits_t *t_const); + +/** + * @brief Get the hardware-dependent timing limits const for FD data timing + * + * @param t_const_fd Pointer to timing limits const structure for FD data + */ +void twai_hal_get_timing_limits_fd(twai_timing_limits_t *t_const_fd); + /** * @brief Configure the TWAI peripheral for legacy driver (deprecated) * diff --git a/components/esp_hal_twai/include/hal/twai_types.h b/components/esp_hal_twai/include/hal/twai_types.h index 5577c147ee7..224a3e81b66 100644 --- a/components/esp_hal_twai/include/hal/twai_types.h +++ b/components/esp_hal_twai/include/hal/twai_types.h @@ -74,6 +74,24 @@ typedef struct { uint8_t ssp_offset; /**< Secondary sample point offset refet to Sync seg, in quanta time, set 0 to disable ssp */ } twai_timing_advanced_config_t; +/** + * @brief TWAI hardware-dependent timing limits const + * + * Used for calculating and checking bit-timing parameters + */ +typedef struct { + uint32_t brp_min; /**< Bit-rate prescaler minimum value */ + uint32_t brp_max; /**< Bit-rate prescaler maximum value */ + uint32_t brp_inc; /**< Bit-rate prescaler increment step */ + uint32_t prop_min; /**< Propagation segment minimum value */ + uint32_t prop_max; /**< Propagation segment maximum value */ + uint32_t tseg1_min; /**< Time segment 1 (phase_seg1) minimum value */ + uint32_t tseg1_max; /**< Time segment 1 (phase_seg1) maximum value */ + uint32_t tseg2_min; /**< Time segment 2 (phase_seg2) minimum value */ + uint32_t tseg2_max; /**< Time segment 2 (phase_seg2) maximum value */ + uint32_t sjw_max; /**< Synchronisation jump width maximum value */ +} twai_timing_limits_t; + /** * @brief Configuration for TWAI mask filter */ diff --git a/components/esp_hal_twai/twai_hal_v1.c b/components/esp_hal_twai/twai_hal_v1.c index 8db40957686..4ea5990daa1 100644 --- a/components/esp_hal_twai/twai_hal_v1.c +++ b/components/esp_hal_twai/twai_hal_v1.c @@ -92,14 +92,29 @@ void twai_hal_configure(twai_hal_context_t *hal_ctx, const twai_timing_config_t twai_ll_set_clkout(hal_ctx->dev, clkout_divider); } +void twai_hal_get_timing_limits(twai_timing_limits_t *t_const) +{ + t_const->brp_min = TWAI_LL_BRP_MIN; + t_const->brp_max = TWAI_LL_BRP_MAX; + t_const->brp_inc = 2; // see `twai_ll_check_brp_validation()` + t_const->prop_min = 0; // hardware don't support prop_seg + t_const->prop_max = TWAI_LL_PROP_MAX; + t_const->tseg1_min = TWAI_LL_TSEG1_MIN; + t_const->tseg1_max = TWAI_LL_TSEG1_MAX; + t_const->tseg2_min = TWAI_LL_TSEG2_MIN; + t_const->tseg2_max = TWAI_LL_TSEG2_MAX; + t_const->sjw_max = TWAI_LL_SJW_MAX; +} + bool twai_hal_check_timing_valid(twai_hal_context_t *hal_ctx, const twai_timing_advanced_config_t *t_config, bool is_fd) { (void) is_fd; bool valid = true; if (t_config) { + int hw_seg1 = t_config->tseg_1 + t_config->prop_seg; valid &= twai_ll_check_brp_validation(t_config->brp); valid &= (t_config->sjw >= 1) && (t_config->sjw <= TWAI_LL_SJW_MAX); - valid &= (t_config->tseg_1 >= TWAI_LL_TSEG1_MIN) && (t_config->tseg_1 <= TWAI_LL_TSEG1_MAX); + valid &= (hw_seg1 >= TWAI_LL_TSEG1_MIN) && (hw_seg1 <= TWAI_LL_TSEG1_MAX); valid &= (t_config->tseg_2 >= TWAI_LL_TSEG2_MIN) && (t_config->tseg_2 <= TWAI_LL_TSEG2_MAX); } return valid; diff --git a/components/esp_hal_twai/twai_hal_v2.c b/components/esp_hal_twai/twai_hal_v2.c index 2b5c46966d3..cfceb8715c2 100644 --- a/components/esp_hal_twai/twai_hal_v2.c +++ b/components/esp_hal_twai/twai_hal_v2.c @@ -48,6 +48,34 @@ void twai_hal_deinit(twai_hal_context_t *hal_ctx) memset(hal_ctx, 0, sizeof(twai_hal_context_t)); } +void twai_hal_get_timing_limits(twai_timing_limits_t *t_const) +{ + t_const->brp_min = TWAI_LL_BRP_MIN; + t_const->brp_max = TWAI_LL_BRP_MAX; + t_const->brp_inc = 1; + t_const->prop_min = 1; + t_const->prop_max = TWAI_LL_PROP_MAX; + t_const->tseg1_min = TWAI_LL_TSEG1_MIN; + t_const->tseg1_max = TWAI_LL_TSEG1_MAX; + t_const->tseg2_min = TWAI_LL_TSEG2_MIN; + t_const->tseg2_max = TWAI_LL_TSEG2_MAX; + t_const->sjw_max = TWAI_LL_SJW_MAX; +} + +void twai_hal_get_timing_limits_fd(twai_timing_limits_t *t_const_fd) +{ + t_const_fd->brp_min = TWAI_LL_BRP_MIN; + t_const_fd->brp_max = TWAI_LL_BRP_MAX_FD; + t_const_fd->brp_inc = 1; + t_const_fd->prop_min = 1; + t_const_fd->prop_max = TWAI_LL_PROP_MAX_FD; + t_const_fd->tseg1_min = TWAI_LL_TSEG1_MIN; + t_const_fd->tseg1_max = TWAI_LL_TSEG1_MAX_FD; + t_const_fd->tseg2_min = TWAI_LL_TSEG2_MIN; + t_const_fd->tseg2_max = TWAI_LL_TSEG2_MAX_FD; + t_const_fd->sjw_max = TWAI_LL_SJW_MAX_FD; +} + bool twai_hal_check_timing_valid(twai_hal_context_t *hal_ctx, const twai_timing_advanced_config_t *t_config, bool is_fd) { bool valid = true; 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 c614127e9fd..565c3171c17 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -820,6 +820,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 00000000000..7f816ac1c8c Binary files /dev/null and b/examples/peripherals/twai/usb_twai_adapter/wireshark_can0_snap.png differ