mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
feat(driver_twai): add usb<->twai candlelight example
This commit is contained in:
@@ -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:
|
||||
|
||||

|
||||
|
||||
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], ×tamp_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 |
Reference in New Issue
Block a user