feat(driver_twai): add usb<->twai candlelight example

This commit is contained in:
wanckl
2026-08-19 21:00:12 +08:00
parent 21c1c30db5
commit d25d82923f
14 changed files with 1114 additions and 0 deletions
@@ -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
@@ -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)
@@ -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
```
@@ -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
)
@@ -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 <stdbool.h>
#include <stdint.h>
#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);
@@ -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 <string.h>
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.
}
@@ -0,0 +1,343 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <sys/param.h>
#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();
}
@@ -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;
}
@@ -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 <stddef.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
/* 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], &timestamp_us, sizeof(timestamp_us));
}
}
@@ -0,0 +1,3 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_tinyusb: "^2"
@@ -0,0 +1 @@
CONFIG_TINYUSB_VENDOR_COUNT=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB