diff --git a/components/bt/controller/esp32c6/Kconfig.in b/components/bt/controller/esp32c6/Kconfig.in index e8d1747e233..73e60603644 100644 --- a/components/bt/controller/esp32c6/Kconfig.in +++ b/components/bt/controller/esp32c6/Kconfig.in @@ -119,11 +119,16 @@ menu "HCI Config" The amount of rx memory received at the same time config BT_LE_HCI_LLDESCS_POOL_NUM - int "The amount of lldecs memory for driver dma mode" + int "The amount of lldecs memory for driver dma mode (Deprecated)" depends on BT_LE_UART_HCI_DMA_MODE default 20 + warning "This option is deprecated and is no longer used" help - The amount of lldecs memory for driver dma mode + The amount of lldecs memory for driver dma mode. + + Deprecated: this option is no longer used after HCI UART DMA + switched to UHCI driver APIs, and will be removed in a future release. + endmenu config BT_LE_CONTROLLER_NPL_OS_PORTING_SUPPORT diff --git a/components/bt/controller/esp32h2/Kconfig.in b/components/bt/controller/esp32h2/Kconfig.in index 165bacc526b..9af94360ed1 100644 --- a/components/bt/controller/esp32h2/Kconfig.in +++ b/components/bt/controller/esp32h2/Kconfig.in @@ -119,11 +119,15 @@ menu "HCI Config" The amount of rx memory received at the same time config BT_LE_HCI_LLDESCS_POOL_NUM - int "The amount of lldecs memory for driver dma mode" + int "The amount of lldecs memory for driver dma mode (Deprecated)" depends on BT_LE_UART_HCI_DMA_MODE default 20 + warning "This option is deprecated and is no longer used" help - The amount of lldecs memory for driver dma mode + The amount of lldecs memory for driver dma mode. + + Deprecated: this option is no longer used after HCI UART DMA + switched to UHCI driver APIs, and will be removed in a future release. endmenu config BT_LE_CONTROLLER_NPL_OS_PORTING_SUPPORT diff --git a/components/bt/porting/transport/driver/uart/hci_driver_uart_dma.c b/components/bt/porting/transport/driver/uart/hci_driver_uart_dma.c index 275961e9584..0c2c30441ef 100644 --- a/components/bt/porting/transport/driver/uart/hci_driver_uart_dma.c +++ b/components/bt/porting/transport/driver/uart/hci_driver_uart_dma.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -7,11 +7,13 @@ #include #include #include "freertos/FreeRTOS.h" -#include "freertos/queue.h" #include "freertos/task.h" #include "freertos/semphr.h" +#include "freertos/ringbuf.h" #include "esp_log.h" +#include "esp_heap_caps.h" #include "driver/uart.h" +#include "driver/uhci.h" #include "esp_hci_transport.h" #include "esp_hci_internal.h" #include "common/hci_driver_h4.h" @@ -19,458 +21,340 @@ #include "common/hci_driver_mem.h" #include "hci_driver_uart.h" -#include "esp_private/periph_ctrl.h" -#include "esp_private/gdma.h" -#include "hal/uhci_ll.h" - -/* - * UART DMA Desc struct +/** + * HCI UART DMA transport on top of the UHCI driver (driver/uhci.h). * - * -------------------------------------------------------------- - * | own | EoF | sub_sof | 5'b0 | length [11:0] | size [11:0] | - * -------------------------------------------------------------- - * | buf_ptr [31:0] | - * -------------------------------------------------------------- - * | next_desc_ptr [31:0] | - * -------------------------------------------------------------- + * RX (uhci_start_receive_continuous) + * TX (uhci_multi_buffer_transmit) */ -/* this bitfield is start from the LSB!!! */ -typedef struct uhci_lldesc_s { - volatile uint32_t size : 12, - length: 12, - offset: 5, /* h/w reserved 5bit, s/w use it as offset in buffer */ - sosf : 1, /* start of sub-frame */ - eof : 1, /* end of frame */ - owner : 1; /* hw or sw */ - volatile const uint8_t *buf; /* point to buffer data */ - union { - volatile uint32_t empty; - STAILQ_ENTRY(uhci_lldesc_s) qe; /* pointing to the next desc */ - }; -} uhci_lldesc_t; - /** * @brief Enumeration of HCI transport transmission states. */ typedef enum { - HCI_TRANS_TX_IDLE, ///< HCI Transport TX is in idle state. - HCI_TRANS_TX_START, ///< HCI Transport TX is starting transmission. - HCI_TRANS_TX_END, ///< HCI Transport TX has completed transmission. + HCI_TRANS_TX_IDLE, /*!< No UHCI TX transaction in flight; task may dequeue the next packet. */ + HCI_TRANS_TX_BUSY, /*!< uhci_multi_buffer_transmit() has been submitted; wait for on_tx_trans_done. */ } hci_trans_tx_state_t; +/** + * @brief Runtime context for the UHCI-based HCI UART DMA transport. + */ typedef struct { - TaskHandle_t task_handler; - hci_driver_uart_params_config_t *hci_uart_params; - SemaphoreHandle_t process_sem; - struct hci_h4_sm *h4_sm; - hci_driver_forward_fn *forward_cb; - struct os_mempool *hci_rx_data_pool; /*!< Init a memory pool for rx_data cache */ - uint8_t *hci_rx_data_buffer; - struct os_mempool *hci_rxinfo_pool; /*!< Init a memory pool for rxinfo cache */ - os_membuf_t *hci_rxinfo_buffer; - volatile bool rxinfo_mem_exhausted; /*!< Indicate rxinfo memory does not exist */ - volatile bool is_continue_rx; /*!< Continue to rx */ - volatile hci_trans_tx_state_t hci_tx_state; /*!< HCI Tx State */ - struct os_mempool lldesc_mem_pool;/*!< Init a memory pool for uhci_lldesc_t */ - uhci_lldesc_t *lldesc_mem; + TaskHandle_t task_handler; /*!< Process task: starts TX when idle and feeds RX bytes to H4. */ + hci_driver_uart_params_config_t *hci_uart_params; /*!< UART port / pins / baud used by uhci_controller_config_t. */ + SemaphoreHandle_t process_sem; /*!< Wakes the process task (TX enqueue, TX done, RX event). */ + struct hci_h4_sm *h4_sm; /*!< H4 state machine that reassembles HCI packets from the byte stream. */ + hci_driver_forward_fn *forward_cb; /*!< Host-bound callback invoked when H4 completes one packet. */ + uhci_controller_handle_t uhci_ctrl; /*!< Handle returned by uhci_new_controller(). */ + uint8_t *rx_dma_ring; /*!< Storage for uhci_start_receive_continuous(); valid until uhci_stop_receive(). */ + size_t rx_dma_ring_size; /*!< Size of rx_dma_ring, passed as buffer_size to continuous RX. */ + RingbufHandle_t rx_copy_ringbuf; /*!< ISR copies DMA-ring slices here; the process task drains it. */ + uhci_transmit_buffer_info_t *tx_segments; /*!< Scratch array of UHCI TX segments for one multi-buffer transaction. */ + volatile hci_trans_tx_state_t hci_tx_state; /*!< Only one HCI packet is in flight (tx-list entries cannot be mixed). */ + volatile bool rx_copy_overflow; /*!< Set in ISR when rx_copy_ringbuf cannot accept a DMA slice. */ } hci_driver_uart_dma_env_t; -#define ESP_BT_HCI_TL_STATUS_OK (0) /*!< HCI_TL Tx/Rx operation status OK */ -/* The number of lldescs pool */ -#define HCI_LLDESCS_POOL_NUM (CONFIG_BT_LE_HCI_LLDESCS_POOL_NUM) -/* Default block size for HCI RX data */ -#define HCI_RX_DATA_BLOCK_SIZE (DEFAULT_BT_LE_ACL_BUF_SIZE + HCI_TRANSPORT_CMD_SZ) -#define HCI_RX_DATA_POOL_NUM (CONFIG_BT_LE_HCI_TRANS_RX_MEM_NUM) -#define HCI_RX_INFO_POOL_NUM (CONFIG_BT_LE_HCI_TRANS_RX_MEM_NUM + 1) +/* Max UHCI TX segments in one uhci_multi_buffer_transmit(); maps to max_transmit_buffer_count. */ +#define HCI_TX_MAX_SEGMENT_COUNT (20) +/* Combined size of all TX segments in one transaction (H4 type byte + one ACL payload). */ +#define HCI_TX_MAX_SIZE (DEFAULT_BT_LE_ACL_BUF_SIZE + 1) +/* One HCI packet budget; used only to size the RX rings, not a mempool block anymore. */ +#define HCI_RX_PKT_BUDGET (DEFAULT_BT_LE_ACL_BUF_SIZE + HCI_TRANSPORT_CMD_SZ) -/** - * @brief callback function for HCI Transport Layer send/receive operations +#define HCI_RX_DMA_RING_SIZE ((HCI_RX_PKT_BUDGET) > 4096 ? (HCI_RX_PKT_BUDGET) : 4096) +/* + * uhci_controller_config_t.max_receive_internal_mem decides how many RX DMA descriptors + * UHCI allocates (node_count = size / DMA_DESCRIPTOR_BUFFER_MAX_SIZE). Keep this large + * enough for at least two nodes so continuous RX can ping-pong instead of overwriting + * a single node while the callback still copies. This is not the allocated DMA ring size. */ -typedef void (* esp_bt_hci_tl_callback_t) (void *arg, uint8_t status); - -struct uart_txrxchannel { - esp_bt_hci_tl_callback_t callback; - void *arg; - uhci_lldesc_t *link_head; -}; - -struct uart_env_tag { - struct uart_txrxchannel tx; - struct uart_txrxchannel rx; -}; - -typedef struct hci_message { - void *ptr; ///< Pointer to the message data. - uint32_t length; ///< Length of the message data. - STAILQ_ENTRY(hci_message) next; ///< Next element in the linked list. -} hci_message_t; - -static void hci_driver_uart_dma_recv_async(uint8_t *buf, uint32_t size, esp_bt_hci_tl_callback_t callback, void *arg); -int hci_driver_uart_dma_rx_start(uint8_t *rx_data, uint32_t length); -int hci_driver_uart_dma_tx_start(esp_bt_hci_tl_callback_t callback, void *arg); +#define HCI_UHCI_RX_DESC_MEM (CONFIG_BT_LE_HCI_TRANS_RX_MEM_NUM * HCI_RX_DMA_RING_SIZE) +#define HCI_RX_COPY_RINGBUF_SIZE HCI_RX_DMA_RING_SIZE static const char *TAG = "uart_dma"; static hci_driver_uart_dma_env_t s_hci_driver_uart_dma_env; static struct hci_h4_sm s_hci_driver_uart_h4_sm; +static portMUX_TYPE s_hci_tx_state_mux = portMUX_INITIALIZER_UNLOCKED; -/* The list for hci_rx_data */ -STAILQ_HEAD(g_hci_rxinfo_list, hci_message); +static int hci_driver_uart_dma_tx_submit(void); -DRAM_ATTR struct g_hci_rxinfo_list g_hci_rxinfo_head; -static DRAM_ATTR struct uart_env_tag uart_env; -static uhci_dev_t *s_uhci_hw = &UHCI0; -static DRAM_ATTR gdma_channel_handle_t s_rx_channel; -static DRAM_ATTR gdma_channel_handle_t s_tx_channel; - -static int hci_driver_uart_dma_memory_deinit(void) +/** + * @brief Free the DMA ring, software RX ringbuf and TX segment scratch array. + * + * Must be called only after uhci_stop_receive() has returned, so the DMA ring + * is no longer referenced by UHCI. + */ +static void +hci_driver_uart_dma_memory_deinit(void) { - - if (s_hci_driver_uart_dma_env.hci_rxinfo_buffer) { - free(s_hci_driver_uart_dma_env.hci_rxinfo_buffer); - s_hci_driver_uart_dma_env.hci_rxinfo_buffer = NULL; + if (s_hci_driver_uart_dma_env.rx_copy_ringbuf) { + vRingbufferDelete(s_hci_driver_uart_dma_env.rx_copy_ringbuf); + s_hci_driver_uart_dma_env.rx_copy_ringbuf = NULL; } - if (s_hci_driver_uart_dma_env.hci_rxinfo_pool) { - free(s_hci_driver_uart_dma_env.hci_rxinfo_pool); - s_hci_driver_uart_dma_env.hci_rxinfo_pool = NULL; + if (s_hci_driver_uart_dma_env.rx_dma_ring) { + heap_caps_free(s_hci_driver_uart_dma_env.rx_dma_ring); + s_hci_driver_uart_dma_env.rx_dma_ring = NULL; } - if (s_hci_driver_uart_dma_env.hci_rx_data_buffer) { - free(s_hci_driver_uart_dma_env.hci_rx_data_buffer); - s_hci_driver_uart_dma_env.hci_rx_data_buffer = NULL; + if (s_hci_driver_uart_dma_env.tx_segments) { + free(s_hci_driver_uart_dma_env.tx_segments); + s_hci_driver_uart_dma_env.tx_segments = NULL; + } +} + +/** + * @brief Allocate UHCI RX/TX working buffers. + * + * rx_dma_ring is DMA-capable internal memory required by uhci_start_receive_continuous(). + * tx_segments holds the segment descriptors for uhci_multi_buffer_transmit(). + */ +static int +hci_driver_uart_dma_memory_init(void) +{ + s_hci_driver_uart_dma_env.rx_dma_ring_size = HCI_UHCI_RX_DESC_MEM; + /* DMA + internal: UHCI/GDMA writes here; cache-safe path also expects internal RAM. */ + s_hci_driver_uart_dma_env.rx_dma_ring = heap_caps_calloc(1, s_hci_driver_uart_dma_env.rx_dma_ring_size, + MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + if (!s_hci_driver_uart_dma_env.rx_dma_ring) { + goto init_err; } - if (s_hci_driver_uart_dma_env.hci_rx_data_pool) { - free(s_hci_driver_uart_dma_env.hci_rx_data_pool); - s_hci_driver_uart_dma_env.hci_rx_data_pool = NULL; + /* BYTEBUF so ISR can push arbitrary UHCI slice lengths without pre-sized items. */ + s_hci_driver_uart_dma_env.rx_copy_ringbuf = xRingbufferCreate(HCI_RX_COPY_RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF); + if (!s_hci_driver_uart_dma_env.rx_copy_ringbuf) { + goto init_err; } - if (s_hci_driver_uart_dma_env.lldesc_mem) { - free(s_hci_driver_uart_dma_env.lldesc_mem); - s_hci_driver_uart_dma_env.lldesc_mem = NULL; + s_hci_driver_uart_dma_env.tx_segments = calloc(HCI_TX_MAX_SEGMENT_COUNT, sizeof(uhci_transmit_buffer_info_t)); + if (!s_hci_driver_uart_dma_env.tx_segments) { + goto init_err; } return 0; -} - -static int hci_driver_uart_dma_memory_init(void) -{ - int rc = 0; - - s_hci_driver_uart_dma_env.lldesc_mem = malloc(OS_MEMPOOL_SIZE(HCI_LLDESCS_POOL_NUM, - sizeof (uhci_lldesc_t)) * sizeof(os_membuf_t)); - if (!s_hci_driver_uart_dma_env.lldesc_mem) { - return -1; - } - - rc = os_mempool_init(&s_hci_driver_uart_dma_env.lldesc_mem_pool, HCI_LLDESCS_POOL_NUM, - sizeof (uhci_lldesc_t), s_hci_driver_uart_dma_env.lldesc_mem, "hci_lldesc_pool"); - if (rc) { - goto init_err; - } - - s_hci_driver_uart_dma_env.hci_rx_data_pool = (struct os_mempool *)malloc(sizeof(struct os_mempool)); - if (!s_hci_driver_uart_dma_env.hci_rx_data_pool) { - goto init_err; - } - - memset(s_hci_driver_uart_dma_env.hci_rx_data_pool, 0, sizeof(struct os_mempool)); - s_hci_driver_uart_dma_env.hci_rx_data_buffer = malloc(OS_MEMPOOL_SIZE(HCI_RX_DATA_POOL_NUM, - HCI_RX_DATA_BLOCK_SIZE) * sizeof(os_membuf_t)); - if (!s_hci_driver_uart_dma_env.hci_rx_data_buffer) { - goto init_err; - } - - memset(s_hci_driver_uart_dma_env.hci_rx_data_buffer, 0, OS_MEMPOOL_SIZE(HCI_RX_DATA_POOL_NUM, - HCI_RX_DATA_BLOCK_SIZE) * sizeof(os_membuf_t)); - rc = os_mempool_init(s_hci_driver_uart_dma_env.hci_rx_data_pool, HCI_RX_DATA_POOL_NUM, - HCI_RX_DATA_BLOCK_SIZE, s_hci_driver_uart_dma_env.hci_rx_data_buffer, - "hci_rx_data_pool"); - if (rc) { - goto init_err; - } - - - /* Malloc hci rxinfo pool */ - s_hci_driver_uart_dma_env.hci_rxinfo_pool = (struct os_mempool *)malloc(sizeof(struct os_mempool)); - if (!s_hci_driver_uart_dma_env.hci_rxinfo_pool) { - goto init_err; - } - - memset(s_hci_driver_uart_dma_env.hci_rxinfo_pool, 0, sizeof(struct os_mempool)); - s_hci_driver_uart_dma_env.hci_rxinfo_buffer = malloc(OS_MEMPOOL_SIZE(HCI_RX_INFO_POOL_NUM, - sizeof(hci_message_t)) * sizeof(os_membuf_t)); - if (!s_hci_driver_uart_dma_env.hci_rxinfo_buffer) { - goto init_err; - } - - memset(s_hci_driver_uart_dma_env.hci_rxinfo_buffer, 0, OS_MEMPOOL_SIZE(HCI_RX_INFO_POOL_NUM, - sizeof(hci_message_t)) * sizeof(os_membuf_t)); - rc = os_mempool_init(s_hci_driver_uart_dma_env.hci_rxinfo_pool, HCI_RX_INFO_POOL_NUM, - sizeof(hci_message_t), s_hci_driver_uart_dma_env.hci_rxinfo_buffer, - "hci_rxinfo_pool"); - if (rc) { - goto init_err; - } - - return rc; init_err: hci_driver_uart_dma_memory_deinit(); - return rc; + return -1; } -static IRAM_ATTR bool hci_uart_tl_rx_eof_callback(gdma_channel_handle_t dma_chan, gdma_event_data_t *event_data, void *user_data) +static void IRAM_ATTR +hci_driver_uart_dma_txstate_set(hci_trans_tx_state_t tx_state) { - esp_bt_hci_tl_callback_t callback = uart_env.rx.callback; - void *arg = uart_env.rx.arg; - assert(dma_chan == s_rx_channel); - assert(uart_env.rx.callback != NULL); - // clear callback pointer - uart_env.rx.callback = NULL; - uart_env.rx.arg = NULL; - // call handler - callback(arg, ESP_BT_HCI_TL_STATUS_OK); - return true; -} - -static IRAM_ATTR bool hci_uart_tl_tx_eof_callback(gdma_channel_handle_t dma_chan, gdma_event_data_t *event_data, void *user_data) -{ - esp_bt_hci_tl_callback_t callback = uart_env.tx.callback; - assert(dma_chan == s_tx_channel); - assert(uart_env.tx.callback != NULL); - // clear callback pointer - uart_env.tx.callback = NULL; - // call handler - callback(uart_env.tx.arg, ESP_BT_HCI_TL_STATUS_OK); - uart_env.tx.arg = NULL; - return true; -} - -uint8_t * IRAM_ATTR hci_driver_uart_dma_rxdata_memory_get(void) -{ - uint8_t *rx_data; - rx_data = os_memblock_get(s_hci_driver_uart_dma_env.hci_rx_data_pool); - return rx_data; -} - -hci_message_t * IRAM_ATTR hci_driver_uart_dma_rxinfo_memory_get(void) -{ - hci_message_t *rx_info; - rx_info = os_memblock_get(s_hci_driver_uart_dma_env.hci_rxinfo_pool); - return rx_info; -} - -void IRAM_ATTR hci_driver_uart_dma_cache_rxinfo(hci_message_t *hci_rxinfo) -{ - os_sr_t sr; - - OS_ENTER_CRITICAL(sr); - STAILQ_INSERT_TAIL(&g_hci_rxinfo_head, hci_rxinfo, next); - OS_EXIT_CRITICAL(sr); -} - -void IRAM_ATTR hci_driver_uart_dma_continue_rx_enable(bool enable) -{ - os_sr_t sr; - OS_ENTER_CRITICAL(sr); - s_hci_driver_uart_dma_env.is_continue_rx = enable; - OS_EXIT_CRITICAL(sr); -} - -void IRAM_ATTR hci_driver_uart_dma_rxinfo_mem_exhausted_set(bool is_exhausted) -{ - os_sr_t sr; - OS_ENTER_CRITICAL(sr); - s_hci_driver_uart_dma_env.rxinfo_mem_exhausted = is_exhausted; - OS_EXIT_CRITICAL(sr); -} - -void IRAM_ATTR hci_driver_uart_dma_recv_callback(void *arg, uint8_t status) -{ - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - hci_message_t *hci_rxinfo; - uint8_t *rx_data; - - if (s_hci_driver_uart_dma_env.rxinfo_mem_exhausted) { - ESP_LOGE(TAG, "Will lost rx data, need adjust rxinfo memory count\n"); - assert(0); - } - - hci_rxinfo = hci_driver_uart_dma_rxinfo_memory_get(); - if (!hci_rxinfo) { - ESP_LOGW(TAG, "set rxinfo mem exhausted flag\n"); - hci_driver_uart_dma_rxinfo_mem_exhausted_set(true); - xSemaphoreGiveFromISR(s_hci_driver_uart_dma_env.process_sem, &xHigherPriorityTaskWoken); - return; - } - - hci_rxinfo->ptr = (void *)uart_env.rx.link_head->buf; - hci_rxinfo->length = uart_env.rx.link_head->length; - hci_driver_uart_dma_cache_rxinfo(hci_rxinfo); - xSemaphoreGiveFromISR(s_hci_driver_uart_dma_env.process_sem, &xHigherPriorityTaskWoken); - rx_data = hci_driver_uart_dma_rxdata_memory_get(); - if (!rx_data) { - hci_driver_uart_dma_continue_rx_enable(true); - }else { - hci_driver_uart_dma_rx_start(rx_data, HCI_RX_DATA_BLOCK_SIZE); - } -} - -void IRAM_ATTR hci_driver_uart_dma_txstate_set(hci_trans_tx_state_t tx_state) -{ - os_sr_t sr; - OS_ENTER_CRITICAL(sr); + portENTER_CRITICAL_SAFE(&s_hci_tx_state_mux); s_hci_driver_uart_dma_env.hci_tx_state = tx_state; - OS_EXIT_CRITICAL(sr); + portEXIT_CRITICAL_SAFE(&s_hci_tx_state_mux); } -void IRAM_ATTR hci_driver_uart_dma_send_callback(void *arg, uint8_t status) +/** + * @brief UHCI on_rx_trans_event callback (ISR context, must be non-blocking). + * + * edata->data points into rx_dma_ring and is only guaranteed readable during this + * callback. Copy the slice out immediately: continuous RX does not stop DMA at EOF, + * so the same node will be overwritten on wrap-around. Both partial-node and EOF + * events are forwarded; H4 (not UHCI) decides packet boundaries. + * + * @return Whether a higher-priority task was woken (UHCI ISR yield contract). + */ +IRAM_ATTR static bool +hci_driver_uart_dma_rx_event_cb(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx) { - uhci_lldesc_t *lldesc_head; - uhci_lldesc_t *lldesc_nxt; BaseType_t xHigherPriorityTaskWoken = pdFALSE; - lldesc_head = uart_env.tx.link_head; - while (lldesc_head) { - lldesc_nxt = lldesc_head->qe.stqe_next; - os_memblock_put(&s_hci_driver_uart_dma_env.lldesc_mem_pool, lldesc_head); - lldesc_head = lldesc_nxt; + (void)uhci_ctrl; + (void)user_ctx; + + /* Abnormal EOF: UHCI reports data == NULL and recv_size == 0. Keep the session running. */ + if (!edata->data || edata->recv_size == 0) { + return false; } - uart_env.tx.link_head = NULL; + /* + * xRingbufferSendFromISR copies the DMA-ring slice. If this fails the DMA session + * still runs (no overrun callback from UHCI); the process task reports HCI sync loss. + */ + if (xRingbufferSendFromISR(s_hci_driver_uart_dma_env.rx_copy_ringbuf, edata->data, edata->recv_size, + &xHigherPriorityTaskWoken) != pdTRUE) { + s_hci_driver_uart_dma_env.rx_copy_overflow = true; + } + + xSemaphoreGiveFromISR(s_hci_driver_uart_dma_env.process_sem, &xHigherPriorityTaskWoken); + return xHigherPriorityTaskWoken == pdTRUE; +} + +/** + * @brief UHCI on_tx_trans_done callback (ISR context). + * + * For a multi-buffer transaction, edata->buffer only points at the first segment + * and is treated as a transaction id, not as sent_size bytes of contiguous memory. + * The tx-list entry is recycled on the next dequeue after last_frame, so the + * process task must run again now that the buffers may be freed. + */ +IRAM_ATTR static bool +hci_driver_uart_dma_tx_done_cb(uhci_controller_handle_t uhci_ctrl, const uhci_tx_done_event_data_t *edata, void *user_ctx) +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + (void)uhci_ctrl; + (void)edata; + (void)user_ctx; + hci_driver_uart_dma_txstate_set(HCI_TRANS_TX_IDLE); xSemaphoreGiveFromISR(s_hci_driver_uart_dma_env.process_sem, &xHigherPriorityTaskWoken); + return xHigherPriorityTaskWoken == pdTRUE; } -static IRAM_ATTR void hci_driver_uart_dma_recv_async(uint8_t *buf, uint32_t size, esp_bt_hci_tl_callback_t callback, void *arg) -{ - uhci_lldesc_t *lldesc_head; - assert(buf != NULL); - assert(size != 0); - assert(callback != NULL); - uart_env.rx.callback = callback; - uart_env.rx.arg = arg; - lldesc_head = uart_env.rx.link_head; - - while (lldesc_head) { - os_memblock_put(&s_hci_driver_uart_dma_env.lldesc_mem_pool, lldesc_head), - lldesc_head = lldesc_head->qe.stqe_next; - } - - uart_env.rx.link_head = NULL; - lldesc_head = os_memblock_get(&s_hci_driver_uart_dma_env.lldesc_mem_pool); - assert(lldesc_head); - memset(lldesc_head, 0, sizeof(uhci_lldesc_t)); - lldesc_head->buf = buf; - lldesc_head->size = size; - lldesc_head->eof = 0; - s_uhci_hw->pkt_thres.pkt_thrs = size; - uart_env.rx.link_head = lldesc_head; - gdma_start(s_rx_channel, (intptr_t)(uart_env.rx.link_head)); -} - -int IRAM_ATTR hci_driver_uart_dma_rx_start(uint8_t *rx_data, uint32_t length) -{ - hci_driver_uart_dma_recv_async(rx_data, length, hci_driver_uart_dma_recv_callback, NULL); - return 0; -} - -int hci_driver_uart_dma_tx_start(esp_bt_hci_tl_callback_t callback, void *arg) +/** + * @brief Dequeue one HCI packet and submit it with uhci_multi_buffer_transmit(). + * + * tx_list_dequeue() is one-entry-at-a-time: the first call yields the 1-byte H4 + * type, later calls yield payload fragments, last_frame marks the end of that + * packet. All fragments stay valid until on_tx_trans_done (the next dequeue + * after last_frame frees the event buffer / mbuf). + * + * @return 0 if a transaction was queued, -1 if the TX list is empty. + */ +static int +hci_driver_uart_dma_tx_submit(void) { void *data; - bool last_frame; - bool head_is_setted; + bool last_frame = false; uint32_t tx_len; - uhci_lldesc_t *lldesc_data; - uhci_lldesc_t *lldesc_head; - uhci_lldesc_t *lldesc_tail; + size_t seg_count = 0; + esp_err_t err; - lldesc_head = NULL; - lldesc_tail = NULL; - head_is_setted = false; - last_frame = false; - while (true) { + while (seg_count < HCI_TX_MAX_SEGMENT_COUNT) { tx_len = hci_driver_util_tx_list_dequeue(0xffffff, &data, &last_frame); if (!tx_len) { break; } - lldesc_data = os_memblock_get(&s_hci_driver_uart_dma_env.lldesc_mem_pool); - /* According to the current processing logic, It should not be empty */ - assert(lldesc_data); - memset(lldesc_data, 0, sizeof(uhci_lldesc_t)); - lldesc_data->length = tx_len; - lldesc_data->buf = data; - lldesc_data->eof = 0; - if (!head_is_setted) { - lldesc_head = lldesc_data; - head_is_setted = true; - } else { - lldesc_tail->qe.stqe_next = lldesc_data; - } - - lldesc_tail = lldesc_data; + /* Each fragment is a UHCI TX segment; UHCI concatenates them on the UART wire. */ + s_hci_driver_uart_dma_env.tx_segments[seg_count].write_buffer = data; + s_hci_driver_uart_dma_env.tx_segments[seg_count].buffer_size = tx_len; + seg_count++; if (last_frame) { break; } } - if (lldesc_head) { - lldesc_tail->eof = 1; - uart_env.tx.link_head = lldesc_head; - uart_env.tx.callback = callback; - uart_env.tx.arg = arg; - /* The DMA interrupt may have been triggered before setting the tx_state, - * So we set it first. - */ - hci_driver_uart_dma_txstate_set(HCI_TRANS_TX_START); - gdma_start(s_tx_channel, (intptr_t)(uart_env.tx.link_head)); - return 0; - } else { + if (seg_count == 0) { return -1; } + + if (!last_frame) { + /* array_size is capped by max_transmit_buffer_count; a gap before the rest may look like idle EOF on the peer. */ + ESP_LOGW(TAG, "HCI TX packet exceeds max_transmit_buffer_count (%d), sending partial packet", + HCI_TX_MAX_SEGMENT_COUNT); + } + + /* Mark BUSY first so a completion ISR cannot be observed as still-idle. */ + hci_driver_uart_dma_txstate_set(HCI_TRANS_TX_BUSY); + err = uhci_multi_buffer_transmit(s_hci_driver_uart_dma_env.uhci_ctrl, + s_hci_driver_uart_dma_env.tx_segments, seg_count); + /* Transmit must not fail; assert directly. */ + assert(err == ESP_OK); + + return 0; +} + +/** + * @brief Create the UHCI controller, register ISR callbacks and start continuous RX. + * + * UART pins/baud must already be programmed (uart_param_config / uart_set_pin). + * UHCI and BT HCI share the same hardware; this driver is the exclusive UHCI user. + */ +static int +hci_driver_uart_dma_uhci_install(void) +{ + uhci_controller_config_t uhci_cfg = { + .uart_port = s_hci_driver_uart_dma_env.hci_uart_params->hci_uart_port, /* Attach this UART to UHCI. */ + .tx_trans_queue_depth = 2, /* One in-flight HCI packet is enough; +1 for slack. */ + .max_transmit_size = HCI_TX_MAX_SIZE, /* Total bytes of all segments in one transaction. */ + .max_transmit_buffer_count = HCI_TX_MAX_SEGMENT_COUNT, /* Caps uhci_multi_buffer_transmit() array_size. */ + .max_receive_internal_mem = HCI_UHCI_RX_DESC_MEM, /* Sizes the RX DMA descriptor chain, not the ring. */ + .dma_burst_size = 32, /* Power-of-two burst; 0 would disable burst. */ + .rx_eof_flags.idle_eof = 1, /* Frame ends when the UART RX line goes idle. */ + }; + uhci_event_callbacks_t uhci_cbs = { + .on_rx_trans_event = hci_driver_uart_dma_rx_event_cb, /* Partial node and/or frame EOF. */ + .on_tx_trans_done = hci_driver_uart_dma_tx_done_cb, + }; + + ESP_LOGI(TAG, "uart attach uhci"); + if (uhci_new_controller(&uhci_cfg, &s_hci_driver_uart_dma_env.uhci_ctrl) != ESP_OK) { + return -1; + } + + /* Register before start_receive_continuous so the first frame is not dropped. */ + if (uhci_register_event_callbacks(s_hci_driver_uart_dma_env.uhci_ctrl, &uhci_cbs, NULL) != ESP_OK) { + uhci_del_controller(s_hci_driver_uart_dma_env.uhci_ctrl); + s_hci_driver_uart_dma_env.uhci_ctrl = NULL; + return -1; + } + + /* Arm once. Do not call uhci_receive() / start again until uhci_stop_receive(). */ + if (uhci_start_receive_continuous(s_hci_driver_uart_dma_env.uhci_ctrl, + s_hci_driver_uart_dma_env.rx_dma_ring, + s_hci_driver_uart_dma_env.rx_dma_ring_size) != ESP_OK) { + uhci_del_controller(s_hci_driver_uart_dma_env.uhci_ctrl); + s_hci_driver_uart_dma_env.uhci_ctrl = NULL; + return -1; + } + + return 0; +} + +/** + * @brief Stop continuous RX so UHCI RX callbacks cannot run anymore. + * + * Does not delete the controller: the process task may still submit TX until it + * is torn down. No-op if UHCI was never installed (init error path). + */ +static void +hci_driver_uart_dma_uhci_stop_rx(void) +{ + if (!s_hci_driver_uart_dma_env.uhci_ctrl) { + return; + } + + uhci_stop_receive(s_hci_driver_uart_dma_env.uhci_ctrl); +} + +/** + * @brief Wait for in-flight TX and delete the UHCI controller. + * + * Call only after the process task is gone so it cannot uhci_multi_buffer_transmit() + * on a handle that is being deleted. The DMA ring may be freed only after this returns. + */ +static void +hci_driver_uart_dma_uhci_del(void) +{ + if (!s_hci_driver_uart_dma_env.uhci_ctrl) { + return; + } + + uhci_wait_all_tx_transaction_done(s_hci_driver_uart_dma_env.uhci_ctrl, 100); + uhci_del_controller(s_hci_driver_uart_dma_env.uhci_ctrl); + s_hci_driver_uart_dma_env.uhci_ctrl = NULL; +} + +static void +hci_driver_uart_dma_task_delete(void) +{ + if (s_hci_driver_uart_dma_env.task_handler) { + vTaskDelete(s_hci_driver_uart_dma_env.task_handler); + s_hci_driver_uart_dma_env.task_handler = NULL; + } } -static void hci_driver_uart_dma_install(void) -{ - periph_module_enable(PERIPH_UHCI0_MODULE); - periph_module_reset(PERIPH_UHCI0_MODULE); - // install DMA driver - gdma_channel_alloc_config_t channel_config = {0}; - - ESP_ERROR_CHECK(gdma_new_ahb_channel(&channel_config, &s_tx_channel, &s_rx_channel)); - gdma_connect(s_tx_channel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_UHCI, 0)); - gdma_connect(s_rx_channel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_UHCI, 0)); - gdma_strategy_config_t strategy_config = { - .auto_update_desc = false, - .owner_check = false - }; - - gdma_apply_strategy(s_tx_channel, &strategy_config); - gdma_apply_strategy(s_rx_channel, &strategy_config); - gdma_rx_event_callbacks_t rx_cbs = { - .on_recv_eof = hci_uart_tl_rx_eof_callback - }; - - gdma_register_rx_event_callbacks(s_rx_channel, &rx_cbs, NULL); - gdma_tx_event_callbacks_t tx_cbs = { - .on_trans_eof = hci_uart_tl_tx_eof_callback - }; - - gdma_register_tx_event_callbacks(s_tx_channel, &tx_cbs, NULL); - // configure UHCI - uhci_ll_init((uhci_dev_t *)s_uhci_hw); - // uhci_ll_rx_set_eof_mode((uhci_dev_t *)s_uhci_hw, UHCI_RX_LEN_EOF); - uhci_ll_rx_set_eof_mode((uhci_dev_t *)s_uhci_hw, UHCI_RX_IDLE_EOF); - // disable software flow control - s_uhci_hw->escape_conf.val = 0; - uhci_ll_attach_uart_port((uhci_dev_t *)s_uhci_hw, s_hci_driver_uart_dma_env.hci_uart_params->hci_uart_port); -} - +/** + * @brief Enqueue a controller-to-host HCI packet and wake the process task. + * + * The actual UART DMA send happens in hci_driver_uart_dma_tx_submit() when TX is idle. + */ static int hci_driver_uart_dma_tx(hci_driver_data_type_t data_type, uint8_t *data, uint32_t length, - hci_driver_direction_t dir) + hci_driver_direction_t dir) { /* By now, this layer is only used by controller. */ assert(dir == HCI_DRIVER_DIR_C2H); @@ -482,10 +366,14 @@ hci_driver_uart_dma_tx(hci_driver_data_type_t data_type, uint8_t *data, uint32_t return 0; } +/** + * @brief H4 has assembled one host-bound HCI packet; forward it upward. + */ static int hci_driver_uart_dma_h4_frame_cb(uint8_t pkt_type, void *data) { hci_driver_forward_fn *forward_cb; + forward_cb = s_hci_driver_uart_dma_env.forward_cb; if (!forward_cb) { return -1; @@ -494,63 +382,59 @@ hci_driver_uart_dma_h4_frame_cb(uint8_t pkt_type, void *data) return forward_cb(pkt_type, data, 0, HCI_DRIVER_DIR_H2C); } +/** + * @brief Drain the software RX ringbuf into the H4 state machine. + * + * Timeout is 0: the process task is already woken by process_sem. Chunks may be + * one DMA node or a short EOF tail; H4 concatenates them into HCI packets. + */ +static void +hci_driver_uart_dma_process_rx(void) +{ + size_t item_size; + uint8_t *rx_data; + int ret; + + if (s_hci_driver_uart_dma_env.rx_copy_overflow) { + ESP_LOGE(TAG, "RX software ring buffer overflow, HCI stream may lose sync"); + s_hci_driver_uart_dma_env.rx_copy_overflow = false; + r_ble_ll_hci_ev_hw_err(ESP_HCI_SYNC_LOSS_ERR); + } + + while ((rx_data = xRingbufferReceive(s_hci_driver_uart_dma_env.rx_copy_ringbuf, &item_size, 0)) != NULL) { + ESP_LOGD(TAG, "uart rx"); + ESP_LOG_BUFFER_HEXDUMP(TAG, rx_data, item_size, ESP_LOG_DEBUG); + ret = hci_h4_sm_rx(s_hci_driver_uart_dma_env.h4_sm, rx_data, (uint16_t)item_size); + /* Return the item before parsing the next slice so the ringbuf can accept more ISR copies. */ + vRingbufferReturnItem(s_hci_driver_uart_dma_env.rx_copy_ringbuf, rx_data); + if (ret < 0) { + ESP_LOGW(TAG, "parse rx data error!\n"); + r_ble_ll_hci_ev_hw_err(ESP_HCI_SYNC_LOSS_ERR); + } + } +} + +/** + * @brief Serialized TX kick + RX parse. + * + * Woken by: controller TX enqueue, UHCI TX done, or UHCI RX event. A binary + * semaphore is enough because both sides are polled every wake-up. + */ static void hci_driver_uart_dma_process_task(void *p) { - hci_message_t *rxinfo_container; - os_sr_t sr; - int ret; - uint8_t* rx_data; - uint32_t rx_len; + (void)p; while (true) { xSemaphoreTake(s_hci_driver_uart_dma_env.process_sem, portMAX_DELAY); - ESP_LOGD(TAG, "task run:%d\n",s_hci_driver_uart_dma_env.hci_tx_state); - /* Process Tx data */ + ESP_LOGD(TAG, "task run:%d\n", s_hci_driver_uart_dma_env.hci_tx_state); + + /* Do not start another uhci_multi_buffer_transmit() while one is in flight. */ if (s_hci_driver_uart_dma_env.hci_tx_state == HCI_TRANS_TX_IDLE) { - hci_driver_uart_dma_tx_start(hci_driver_uart_dma_send_callback, (void*)&uart_env); + hci_driver_uart_dma_tx_submit(); } - if (s_hci_driver_uart_dma_env.rxinfo_mem_exhausted) { - rx_data = (void *)uart_env.rx.link_head->buf; - rx_len = uart_env.rx.link_head->length; - ESP_LOGD(TAG, "rxinfo exhausted:"); - ESP_LOG_BUFFER_HEXDUMP(TAG, rx_data, rx_len, ESP_LOG_DEBUG); - ret = hci_h4_sm_rx(s_hci_driver_uart_dma_env.h4_sm, rx_data, rx_len); - hci_driver_uart_dma_rx_start(rx_data, HCI_RX_DATA_BLOCK_SIZE); - hci_driver_uart_dma_rxinfo_mem_exhausted_set(false); - if (ret < 0) { - ESP_LOGW(TAG, "parse rx data error!\n"); - r_ble_ll_hci_ev_hw_err(ESP_HCI_SYNC_LOSS_ERR); - } - } - - while (!STAILQ_EMPTY(&g_hci_rxinfo_head)) { - OS_ENTER_CRITICAL(sr); - rxinfo_container = STAILQ_FIRST(&g_hci_rxinfo_head); - STAILQ_REMOVE_HEAD(&g_hci_rxinfo_head, next); - OS_EXIT_CRITICAL(sr); - - rx_data = rxinfo_container->ptr; - rx_len = rxinfo_container->length; - ESP_LOGD(TAG, "uart rx"); - ESP_LOG_BUFFER_HEXDUMP(TAG, rx_data, rx_len, ESP_LOG_DEBUG); - ret = hci_h4_sm_rx(s_hci_driver_uart_dma_env.h4_sm, rx_data, rx_len); - if (ret < 0) { - ESP_LOGW(TAG, "parse rx data error!\n"); - r_ble_ll_hci_ev_hw_err(ESP_HCI_SYNC_LOSS_ERR); - } - - os_memblock_put(s_hci_driver_uart_dma_env.hci_rxinfo_pool, rxinfo_container); - /* No need to enter CRITICAL */ - if (s_hci_driver_uart_dma_env.is_continue_rx) { - /* We should set continux rx flag first, RX interrupted may happened when rx start soon */ - hci_driver_uart_dma_continue_rx_enable(false); - hci_driver_uart_dma_rx_start(rx_data, HCI_RX_DATA_BLOCK_SIZE); - } else { - os_memblock_put(s_hci_driver_uart_dma_env.hci_rx_data_pool, rx_data); - } - } + hci_driver_uart_dma_process_rx(); } } @@ -570,26 +454,39 @@ hci_driver_uart_dma_task_create(void) return 0; } - +/** + * @brief Tear down in reverse-dependency order, with UHCI RX stopped before the task. + * + * 1. uhci_stop_receive: RX ISR must not write a ringbuf / give a sem we are about to free. + * 2. Delete the process task: it must not call UHCI TX APIs during del_controller. + * 3. Wait TX + uhci_del_controller: DMA ring is then unused. + * 4. UART, working buffers, process_sem, TX list. + */ static void hci_driver_uart_dma_deinit(void) { - if (s_hci_driver_uart_dma_env.task_handler) { - vTaskDelete(s_hci_driver_uart_dma_env.task_handler); - s_hci_driver_uart_dma_env.task_handler = NULL; + hci_driver_uart_dma_uhci_stop_rx(); + hci_driver_uart_dma_task_delete(); + hci_driver_uart_dma_uhci_del(); + + if (s_hci_driver_uart_dma_env.hci_uart_params) { + /* uart_param_config/set_pin do not uart_driver_install(); delete may be a no-op. */ + (void)uart_driver_delete(s_hci_driver_uart_dma_env.hci_uart_params->hci_uart_port); } - ESP_ERROR_CHECK(uart_driver_delete(s_hci_driver_uart_dma_env.hci_uart_params->hci_uart_port)); hci_driver_uart_dma_memory_deinit(); if (s_hci_driver_uart_dma_env.process_sem) { vSemaphoreDelete(s_hci_driver_uart_dma_env.process_sem); + s_hci_driver_uart_dma_env.process_sem = NULL; } hci_driver_util_deinit(); memset(&s_hci_driver_uart_dma_env, 0, sizeof(hci_driver_uart_dma_env_t)); } - +/** + * @brief Bring up H4, TX list, UART, UHCI continuous RX and the process task. + */ static int hci_driver_uart_dma_init(hci_driver_forward_fn *cb) { @@ -598,7 +495,8 @@ hci_driver_uart_dma_init(hci_driver_forward_fn *cb) memset(&s_hci_driver_uart_dma_env, 0, sizeof(hci_driver_uart_dma_env_t)); s_hci_driver_uart_dma_env.h4_sm = &s_hci_driver_uart_h4_sm; - hci_h4_sm_init(s_hci_driver_uart_dma_env.h4_sm, &s_hci_driver_mem_alloc, &s_hci_driver_mem_free, hci_driver_uart_dma_h4_frame_cb); + hci_h4_sm_init(s_hci_driver_uart_dma_env.h4_sm, &s_hci_driver_mem_alloc, &s_hci_driver_mem_free, + hci_driver_uart_dma_h4_frame_cb); rc = hci_driver_util_init(); if (rc) { @@ -617,12 +515,13 @@ hci_driver_uart_dma_init(hci_driver_forward_fn *cb) s_hci_driver_uart_dma_env.forward_cb = cb; s_hci_driver_uart_dma_env.hci_uart_params = hci_driver_uart_config_param_get(); + /* UART must be parameterized before uhci_new_controller() attaches the port. */ hci_driver_uart_config(s_hci_driver_uart_dma_env.hci_uart_params); - ESP_LOGI(TAG, "uart attach uhci!"); - hci_driver_uart_dma_install(); - - STAILQ_INIT(&g_hci_rxinfo_head); + rc = hci_driver_uart_dma_uhci_install(); + if (rc) { + goto error; + } rc = hci_driver_uart_dma_task_create(); if (rc) { @@ -630,10 +529,6 @@ hci_driver_uart_dma_init(hci_driver_forward_fn *cb) } s_hci_driver_uart_dma_env.hci_tx_state = HCI_TRANS_TX_IDLE; - s_hci_driver_uart_dma_env.rxinfo_mem_exhausted = false; - s_hci_driver_uart_dma_env.is_continue_rx = false; - hci_driver_uart_dma_rx_start(os_memblock_get(s_hci_driver_uart_dma_env.hci_rx_data_pool), - HCI_RX_DATA_BLOCK_SIZE); return 0; error: @@ -644,10 +539,10 @@ error: int hci_driver_uart_dma_reconfig_pin(int tx_pin, int rx_pin, int cts_pin, int rts_pin) { + /* UHCI stays attached to the same UART port; only the GPIO mapping changes. */ return hci_driver_uart_pin_update(tx_pin, rx_pin, cts_pin, rts_pin); } - hci_driver_ops_t hci_driver_uart_dma_ops = { .hci_driver_tx = hci_driver_uart_dma_tx, .hci_driver_init = hci_driver_uart_dma_init,