feat(uhci): uhci receive can be called in isr

This commit is contained in:
C.S.M
2026-07-23 16:38:03 +08:00
parent 8550997ada
commit fcd5c55194
9 changed files with 594 additions and 59 deletions

View File

@@ -21,6 +21,15 @@ menu "ESP-Driver:UHCI Configurations"
If this option is not selected, UHCI interrupt will be disabled for a long time and
may cause data lost when doing spi flash operation.
config UHCI_RECV_FUNC_IN_IRAM
bool "Place UHCI receive function into IRAM"
default n
select GDMA_CTRL_FUNC_IN_IRAM if SOC_GDMA_SUPPORTED
help
Place uhci_receive() into IRAM for better performance and fewer cache misses.
This also allows uhci_receive() to be called from the RX-done callback (ISR context),
for example to re-arm reception with a new buffer and minimize the RX gap.
config UHCI_ISR_CACHE_SAFE
bool "Allow UHCI ISR to execute when cache is disabled" if !SPI_FLASH_AUTO_SUSPEND
select UHCI_ISR_HANDLER_IN_IRAM
@@ -30,6 +39,8 @@ menu "ESP-Driver:UHCI Configurations"
Enable this option to allow the ISR for UHCI to execute even when the cache is disabled.
This can be useful in scenarios where the cache might be turned off, but the UHCI
functionality is still required to operate correctly.
To also call uhci_receive() from the ISR while the cache is disabled, enable
UHCI_RECV_FUNC_IN_IRAM as well.
config UHCI_ENABLE_DEBUG_LOG
bool "Enable debug log"

View File

@@ -85,6 +85,10 @@ esp_err_t uhci_new_controller(const uhci_controller_config_t *config, uhci_contr
* The return from the function doesn't mean a finished receive. You need to register corresponding
* callback function to get notification.
*
* @note This function can be called from the RX-done callback (i.e. ISR context), e.g. to re-arm
* reception with a new buffer and minimize the RX gap. To call it while the cache is disabled,
* enable CONFIG_UHCI_RECV_FUNC_IN_IRAM so this function is placed in IRAM.
*
* @return
* - `ESP_OK`: The driver is ready for data reception.
* - `ESP_ERR_INVALID_STATE`: The controller is not in enable state.
@@ -92,6 +96,46 @@ esp_err_t uhci_new_controller(const uhci_controller_config_t *config, uhci_contr
*/
esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size);
/**
* @brief Start the receive continuously.
*
* Unlike `uhci_receive()` (which stops the DMA after one frame and must be re-armed), this keeps
* the GDMA running across EOFs. `read_buffer` is split across the DMA nodes and used as a circular
* ring: each finished frame (UART idle/length EOF) is delivered through the registered
* `on_rx_trans_event` callback with `flags.totally_received = true`, and the following frame lands
* in the next buffer without any re-arm. Call `uhci_stop_receive()` to end the session.
*
* @param[in] uhci_ctrl Handle to the UHCI controller.
* @param[in] read_buffer Caller-provided storage buffer to receive into. Must stay valid until `uhci_stop_receive()`.
* @param[in] buffer_size The size of the storage buffer, in bytes.
*
* @note The callback delivers a pointer into the storage buffer (zero-copy). The application must
* consume the data before the DMA wraps around and overwrites it, so size the storage buffer for the
* expected throughput and consumer latency. Overrun protection is not provided in this version.
*
* @return
* - `ESP_OK`: Continuous reception started.
* - `ESP_ERR_INVALID_ARG`: Invalid arguments (e.g., null buffer or invalid controller handle).
* - `ESP_ERR_INVALID_STATE`: A reception is already in progress.
*/
esp_err_t uhci_start_receive_continuous(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size);
/**
* @brief Stop an ongoing reception.
*
* Stops the RX DMA and returns the controller to the idle state so it can be re-armed with
* `uhci_receive()` / `uhci_start_receive_continuous()` or deleted with `uhci_del_controller()`. Mainly
* used to end a `uhci_start_receive_continuous()` session; it is a no-op if no reception is in progress.
*
* @param[in] uhci_ctrl Handle to the UHCI controller.
*
* @return
* - `ESP_OK`: Reception stopped (or already idle).
* - `ESP_ERR_INVALID_ARG`: The provided `uhci_ctrl` handle is invalid or null.
* - `ESP_ERR_INVALID_STATE`: A reception is concurrently being armed; retry after it completes.
*/
esp_err_t uhci_stop_receive(uhci_controller_handle_t uhci_ctrl);
/**
* @brief Transmit data using the UHCI controller.
*

View File

@@ -17,11 +17,15 @@ entries:
uhci: uhci_gdma_rx_callback_done (noflash)
uhci: uhci_gdma_tx_callback_eof (noflash)
uhci: uhci_do_transmit (noflash)
if UHCI_RECV_FUNC_IN_IRAM = y:
uhci: uhci_receive (noflash)
uhci: uhci_receive_internal (noflash)
[mapping:uhci_driver_gdma_link]
archive: libesp_driver_dma.a
entries:
if UHCI_ISR_HANDLER_IN_IRAM = y:
gdma_link: gdma_link_count_buffer_size_till_eof (noflash)
if UHCI_ISR_HANDLER_IN_IRAM = y || UHCI_RECV_FUNC_IN_IRAM = y:
gdma_link: gdma_link_mount_buffers (noflash)
gdma_link: gdma_link_get_head_addr (noflash)

View File

@@ -15,6 +15,7 @@
#include "esp_attr.h"
#include "esp_log.h"
#include "esp_check.h"
#include "esp_macros.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
@@ -37,7 +38,7 @@
#include "esp_memory_utils.h"
#include "esp_cache.h"
static const char* TAG = "uhci";
#define TAG "uhci"
typedef struct uhci_platform_t {
_lock_t mutex; // platform level mutex lock.
@@ -113,7 +114,9 @@ static bool uhci_gdma_rx_callback_done(gdma_channel_handle_t dma_chan, gdma_even
return false;
}
if (event_data->flags.abnormal_eof || event_data->flags.normal_eof) {
const bool frame_end = event_data->flags.normal_eof || event_data->flags.abnormal_eof;
const bool rx_terminal = frame_end && !uhci_ctrl->rx_dir.continuous;
if (rx_terminal) {
// An EOF signal does not automatically stop the DMA transfer, so we need to stop it manually.
gdma_stop(uhci_ctrl->rx_dir.dma_chan);
// stop() cannot prevent already prefetched DMA descriptors from being processed.
@@ -121,50 +124,56 @@ static bool uhci_gdma_rx_callback_done(gdma_channel_handle_t dma_chan, gdma_even
gdma_reset(uhci_ctrl->rx_dir.dma_chan);
}
uhci_rx_event_data_t evt_data = {0};
if (!event_data->flags.abnormal_eof) {
const size_t cache_line = uhci_ctrl->rx_dir.cache_line;
size_t rx_size, sync_size;
if (!event_data->flags.normal_eof) {
rx_size = uhci_ctrl->rx_dir.buffer_size_per_desc_node[uhci_ctrl->rx_dir.node_index];
sync_size = rx_size;
} else {
rx_size = gdma_link_count_buffer_size_till_eof(uhci_ctrl->rx_dir.dma_link, uhci_ctrl->rx_dir.node_index);
sync_size = UHCI_ALIGN_UP(rx_size, cache_line); // round up to the next cache line
}
evt_data = (uhci_rx_event_data_t) {
.data = uhci_ctrl->rx_dir.buffer_pointers[uhci_ctrl->rx_dir.node_index],
.recv_size = rx_size,
.flags.totally_received = event_data->flags.normal_eof,
};
if (esp_ptr_external_ram(evt_data.data)) {
esp_psram_mspi_mb();
}
// DMA just finished writing the node's buffer. Because the descriptor link is circular,
// the same buffer region gets overwritten on every loop. On targets where the buffer is
// backed by a cache, the CPU must invalidate the range before reading, otherwise it will
// return stale data from a previous loop.
if (cache_line > 0) {
esp_cache_msync((void *)evt_data.data, sync_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C);
}
const size_t cache_line = uhci_ctrl->rx_dir.cache_line;
size_t rx_size, sync_size;
if (!frame_end) {
rx_size = uhci_ctrl->rx_dir.buffer_size_per_desc_node[uhci_ctrl->rx_dir.node_index];
sync_size = rx_size;
} else {
rx_size = gdma_link_count_buffer_size_till_eof(uhci_ctrl->rx_dir.dma_link, uhci_ctrl->rx_dir.node_index);
// Round the invalidate size up to a full cache line. Each node buffer is itself cache-line
// aligned and a whole multiple of the cache line, so the extra bytes stay inside this same
// node buffer (never a neighbor) and only discard DMA scratch past the frame end.
sync_size = ESP_ALIGN_UP(rx_size, cache_line);
}
if (event_data->flags.abnormal_eof || event_data->flags.normal_eof) {
uhci_ctrl->rx_dir.node_index = 0;
uhci_rx_event_data_t evt_data = {
.data = uhci_ctrl->rx_dir.buffer_pointers[uhci_ctrl->rx_dir.node_index],
.recv_size = rx_size,
.flags.totally_received = frame_end,
};
if (esp_ptr_external_ram(evt_data.data)) {
esp_psram_mspi_mb();
}
// DMA just finished writing the node's buffer. Because the descriptor link is circular,
// the same buffer region gets overwritten on every loop. On targets where the buffer is
// backed by a cache, the CPU must invalidate the range before reading, otherwise it will
// return stale data from a previous loop.
if (cache_line > 0) {
esp_cache_msync((void *)evt_data.data, sync_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C);
}
if (rx_terminal) {
// One-shot completion (or abnormal EOF): return to idle so uhci_receive() can re-arm and
// the controller can be deleted. Atomically claim the RUN->ENABLE transition; only the
// winner releases the PM lock, so a concurrent uhci_stop_receive() (possibly on another
// core) cannot double-release the single pm_lock shared with TX.
uhci_ctrl->rx_dir.node_index = 0;
uhci_rx_fsm_t expected = UHCI_RX_FSM_RUN;
if (atomic_compare_exchange_strong(&uhci_ctrl->rx_dir.rx_fsm, &expected, UHCI_RX_FSM_ENABLE)) {
#if CONFIG_PM_ENABLE
// release power manager lock
if (uhci_ctrl->pm_lock) {
esp_pm_lock_release(uhci_ctrl->pm_lock);
}
// release power manager lock
if (uhci_ctrl->pm_lock) {
esp_pm_lock_release(uhci_ctrl->pm_lock);
}
#endif
atomic_store(&uhci_ctrl->rx_dir.rx_fsm, UHCI_RX_FSM_ENABLE);
}
} else {
// A filled node (any mode) or a completed frame in continuous mode: advance to the next
// node of the circular link and keep the DMA running. In continuous mode the PM lock stays
// held until uhci_stop_receive().
uhci_ctrl->rx_dir.node_index++;
// Go back to 0 as its a circle descriptor link
if (uhci_ctrl->rx_dir.node_index >= uhci_ctrl->rx_dir.rx_num_dma_nodes) {
@@ -235,11 +244,6 @@ static esp_err_t uhci_gdma_initialize(uhci_controller_handle_t uhci_ctrl, const
ESP_RETURN_ON_ERROR(gdma_new_link_list(&dma_link_config, &uhci_ctrl->rx_dir.dma_link), TAG, "DMA rx link list alloc failed");
ESP_LOGD(TAG, "rx_dma node number is %d", uhci_ctrl->rx_dir.rx_num_dma_nodes);
uhci_ctrl->rx_dir.buffer_size_per_desc_node = heap_caps_calloc(uhci_ctrl->rx_dir.rx_num_dma_nodes, sizeof(*uhci_ctrl->rx_dir.buffer_size_per_desc_node), UHCI_MEM_ALLOC_CAPS);
ESP_RETURN_ON_FALSE(uhci_ctrl->rx_dir.buffer_size_per_desc_node, ESP_ERR_NO_MEM, TAG, "no memory for recording buffer size for desc node");
uhci_ctrl->rx_dir.buffer_pointers = heap_caps_calloc(uhci_ctrl->rx_dir.rx_num_dma_nodes, sizeof(*uhci_ctrl->rx_dir.buffer_pointers), UHCI_MEM_ALLOC_CAPS);
ESP_RETURN_ON_FALSE(uhci_ctrl->rx_dir.buffer_pointers, ESP_ERR_NO_MEM, TAG, "no memory for recording buffer pointers for desc node");
// Register callbacks
gdma_tx_event_callbacks_t tx_cbk = {
.on_trans_eof = uhci_gdma_tx_callback_eof,
@@ -308,13 +312,15 @@ static void uhci_do_transmit(uhci_controller_handle_t uhci_ctrl, uhci_transactio
gdma_start(uhci_ctrl->tx_dir.dma_chan, gdma_link_get_head_addr(uhci_ctrl->tx_dir.dma_link));
}
esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size)
static esp_err_t uhci_receive_internal(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size, bool continuous)
{
ESP_RETURN_ON_FALSE(uhci_ctrl, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE(read_buffer != NULL && buffer_size > 0, ESP_ERR_INVALID_ARG, TAG, "read buffer null or buffer size is 0");
// Use the ISR-safe check variants: uhci_receive() is documented to be callable from the RX-done
// callback (ISR context), where the plain ESP_LOGE-based macros would take the log mutex.
ESP_RETURN_ON_FALSE_ISR(uhci_ctrl, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE_ISR(read_buffer != NULL && buffer_size > 0, ESP_ERR_INVALID_ARG, TAG, "read buffer null or buffer size is 0");
uhci_rx_fsm_t expected_fsm = UHCI_RX_FSM_ENABLE;
ESP_RETURN_ON_FALSE(atomic_compare_exchange_strong(&uhci_ctrl->rx_dir.rx_fsm, &expected_fsm, UHCI_RX_FSM_RUN_WAIT), ESP_ERR_INVALID_STATE, TAG, "controller not in enable state");
ESP_RETURN_ON_FALSE_ISR(atomic_compare_exchange_strong(&uhci_ctrl->rx_dir.rx_fsm, &expected_fsm, UHCI_RX_FSM_RUN_WAIT), ESP_ERR_INVALID_STATE, TAG, "controller not in enable state");
esp_err_t ret = ESP_OK;
@@ -328,7 +334,7 @@ esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer,
uintptr_t aligned_address = ((uintptr_t)read_buffer + max_alignment_needed - 1) & ~(max_alignment_needed - 1);
size_t offset = aligned_address - (uintptr_t)read_buffer;
ESP_GOTO_ON_FALSE(buffer_size > offset, ESP_ERR_INVALID_ARG, err, TAG, "buffer size too small to align");
ESP_GOTO_ON_FALSE_ISR(buffer_size > offset, ESP_ERR_INVALID_ARG, err, TAG, "buffer size too small to align");
read_buffer = (uint8_t *)aligned_address;
buffer_size -= offset;
@@ -341,7 +347,9 @@ esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer,
size_t remaining_size = usable_size - (base_size * node_count);
{
gdma_buffer_mount_config_t mount_configs[node_count];
// Reuse the pre-allocated scratch array instead of a VLA: this function may run in ISR
// context, where a large node_count on the stack could overflow the small ISR stack.
gdma_buffer_mount_config_t *mount_configs = uhci_ctrl->rx_dir.mount_configs;
memset(mount_configs, 0, node_count * sizeof(gdma_buffer_mount_config_t));
for (size_t i = 0; i < node_count; i++) {
@@ -353,8 +361,8 @@ esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer,
} else {
uhci_ctrl->rx_dir.buffer_size_per_desc_node[i] = base_size;
}
ESP_GOTO_ON_FALSE(uhci_ctrl->rx_dir.buffer_size_per_desc_node[i] != 0 && uhci_ctrl->rx_dir.buffer_size_per_desc_node[i] <= DMA_DESCRIPTOR_BUFFER_MAX_SIZE,
ESP_ERR_INVALID_ARG, err, TAG, "buffer_size is too small or too large");
ESP_GOTO_ON_FALSE_ISR(uhci_ctrl->rx_dir.buffer_size_per_desc_node[i] != 0 && uhci_ctrl->rx_dir.buffer_size_per_desc_node[i] <= DMA_DESCRIPTOR_BUFFER_MAX_SIZE,
ESP_ERR_INVALID_ARG, err, TAG, "buffer_size is too small or too large");
size_t buffer_alignment = esp_ptr_internal(read_buffer) ? uhci_ctrl->rx_dir.int_mem_align : uhci_ctrl->rx_dir.ext_mem_align;
mount_configs[i] = (gdma_buffer_mount_config_t) {
@@ -365,18 +373,18 @@ esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer,
.mark_final = GDMA_FINAL_LINK_TO_DEFAULT,
}
};
ESP_LOGD(TAG, "The DMA node %d has %d byte", i, uhci_ctrl->rx_dir.buffer_size_per_desc_node[i]);
ESP_DRAM_LOGD(TAG, "The DMA node %d has %d byte", i, uhci_ctrl->rx_dir.buffer_size_per_desc_node[i]);
read_buffer += uhci_ctrl->rx_dir.buffer_size_per_desc_node[i];
}
ESP_GOTO_ON_ERROR(gdma_link_mount_buffers(uhci_ctrl->rx_dir.dma_link, 0, mount_configs, node_count, NULL), err, TAG, "DMA link mount buffers failed");
ESP_GOTO_ON_ERROR_ISR(gdma_link_mount_buffers(uhci_ctrl->rx_dir.dma_link, 0, mount_configs, node_count, NULL), err, TAG, "DMA link mount buffers failed");
// Invalidate cache before DMA starts to ensure no dirty cache lines.
// All DMA nodes (mount_configs) share the same contiguous user buffer, so checking mount_configs[0].buffer is sufficient.
bool need_cache_sync = esp_ptr_internal(mount_configs[0].buffer) ? (uhci_ctrl->int_mem_cache_line_size > 0) : (uhci_ctrl->ext_mem_cache_line_size > 0);
if (need_cache_sync) {
ESP_GOTO_ON_ERROR(esp_cache_msync(mount_configs[0].buffer, usable_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C), err, TAG, "cache sync failed");
ESP_GOTO_ON_ERROR_ISR(esp_cache_msync(mount_configs[0].buffer, usable_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C), err, TAG, "cache sync failed");
}
}
@@ -387,6 +395,7 @@ esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer,
}
#endif
uhci_ctrl->rx_dir.continuous = continuous;
atomic_store(&uhci_ctrl->rx_dir.rx_fsm, UHCI_RX_FSM_RUN);
gdma_reset(uhci_ctrl->rx_dir.dma_chan);
@@ -398,6 +407,49 @@ err:
return ret;
}
esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size)
{
return uhci_receive_internal(uhci_ctrl, read_buffer, buffer_size, false);
}
esp_err_t uhci_start_receive_continuous(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size)
{
return uhci_receive_internal(uhci_ctrl, read_buffer, buffer_size, true);
}
esp_err_t uhci_stop_receive(uhci_controller_handle_t uhci_ctrl)
{
ESP_RETURN_ON_FALSE(uhci_ctrl, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
// Atomically claim the RUN->ENABLE transition. If the RX EOF ISR already ended the session
// (state is ENABLE) or wins this race, we must not stop the DMA or release the shared PM lock
// again, otherwise the single pm_lock (shared with TX) would be double-released.
uhci_rx_fsm_t expected = UHCI_RX_FSM_RUN;
if (!atomic_compare_exchange_strong(&uhci_ctrl->rx_dir.rx_fsm, &expected, UHCI_RX_FSM_ENABLE)) {
// RUN_WAIT means a receive is concurrently being armed (e.g. re-armed from the RX-done ISR).
// Report it instead of silently returning ESP_OK, which would let that start win the race and
// keep the DMA running after the caller believes it stopped.
if (expected == UHCI_RX_FSM_RUN_WAIT) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
gdma_stop(uhci_ctrl->rx_dir.dma_chan);
gdma_reset(uhci_ctrl->rx_dir.dma_chan);
uhci_ctrl->rx_dir.node_index = 0;
uhci_ctrl->rx_dir.continuous = false;
#if CONFIG_PM_ENABLE
// In continuous mode the PM lock is held for the whole session; release it here.
if (uhci_ctrl->pm_lock) {
esp_pm_lock_release(uhci_ctrl->pm_lock);
}
#endif
return ESP_OK;
}
esp_err_t uhci_multi_buffer_transmit(uhci_controller_handle_t uhci_ctrl, const uhci_transmit_buffer_info_t *buffer_info_array, size_t array_size)
{
ESP_RETURN_ON_FALSE(uhci_ctrl, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
@@ -522,6 +574,9 @@ esp_err_t uhci_del_controller(uhci_controller_handle_t uhci_ctrl)
if (uhci_ctrl->rx_dir.buffer_pointers) {
free(uhci_ctrl->rx_dir.buffer_pointers);
}
if (uhci_ctrl->rx_dir.mount_configs) {
heap_caps_free(uhci_ctrl->rx_dir.mount_configs);
}
#if CONFIG_PM_ENABLE
if (uhci_ctrl->pm_lock) {
@@ -619,6 +674,16 @@ esp_err_t uhci_new_controller(const uhci_controller_config_t *config, uhci_contr
ESP_GOTO_ON_ERROR(uhci_gdma_initialize(uhci_ctrl, config), err, TAG, "uhci gdma initialize failed");
// rx_num_dma_nodes is only known after uhci_gdma_initialize() queried the DMA alignment, so the
// per-node RX scratch arrays are allocated here (mirroring how tx_dir.mount_configs is allocated).
uhci_ctrl->rx_dir.buffer_size_per_desc_node = heap_caps_calloc(uhci_ctrl->rx_dir.rx_num_dma_nodes, sizeof(*uhci_ctrl->rx_dir.buffer_size_per_desc_node), UHCI_MEM_ALLOC_CAPS);
ESP_GOTO_ON_FALSE(uhci_ctrl->rx_dir.buffer_size_per_desc_node, ESP_ERR_NO_MEM, err, TAG, "no memory for recording buffer size for desc node");
uhci_ctrl->rx_dir.buffer_pointers = heap_caps_calloc(uhci_ctrl->rx_dir.rx_num_dma_nodes, sizeof(*uhci_ctrl->rx_dir.buffer_pointers), UHCI_MEM_ALLOC_CAPS);
ESP_GOTO_ON_FALSE(uhci_ctrl->rx_dir.buffer_pointers, ESP_ERR_NO_MEM, err, TAG, "no memory for recording buffer pointers for desc node");
// Pre-allocate the mount config scratch array so uhci_receive() never puts a VLA on the (small) ISR stack.
uhci_ctrl->rx_dir.mount_configs = heap_caps_calloc(uhci_ctrl->rx_dir.rx_num_dma_nodes, sizeof(gdma_buffer_mount_config_t), UHCI_MEM_ALLOC_CAPS);
ESP_GOTO_ON_FALSE(uhci_ctrl->rx_dir.mount_configs, ESP_ERR_NO_MEM, err, TAG, "no memory for rx buffer mount config array");
*ret_uhci_ctrl = uhci_ctrl;
return ESP_OK;
err:

View File

@@ -22,7 +22,6 @@ extern "C" {
typedef struct uhci_controller_t uhci_controller_t;
#define UHCI_ALIGN_UP(num, align) (((num) + ((align) - 1)) & ~((align) - 1))
#define UHCI_MAX(a, b) (((a)>(b))?(a):(b))
#define UHCI_PM_LOCK_NAME_LEN_MAX 16
@@ -90,6 +89,8 @@ typedef struct {
size_t int_mem_align; // Alignment for internal memory
size_t ext_mem_align; // Alignment for external memory
size_t rx_num_dma_nodes; // rx dma number nodes
gdma_buffer_mount_config_t *mount_configs; // scratch array (capacity rx_num_dma_nodes) reused by every receive to mount buffer segments; avoids a VLA in ISR context
bool continuous; // continuous mode: keep DMA running across EOFs instead of stopping
} uhci_rx_dir;
struct uhci_controller_t {

View File

@@ -1,8 +1,15 @@
# In order for the cases defined by `TEST_CASE` to be linked into the final elf,
# the component can be registered as WHOLE_ARCHIVE
set(srcs "test_app_main.c"
"test_uhci.c")
# The cache-safe case needs uhci_receive() in IRAM, only build it when that path is enabled
if(CONFIG_UHCI_ISR_CACHE_SAFE)
list(APPEND srcs "test_uhci_cache_safe.c")
endif()
idf_component_register(
SRCS "test_app_main.c"
"test_uhci.c"
SRCS ${srcs}
REQUIRES esp_driver_uart unity test_utils esp_psram
WHOLE_ARCHIVE
)

View File

@@ -8,9 +8,12 @@
#include <sys/param.h>
#include "unity.h"
#include "test_utils.h"
#include "unity_test_utils_cache.h"
#include "esp_rom_sys.h"
#include "driver/uart.h"
#include "driver/uhci.h"
#include "hal/gdma_periph.h"
#include "hal/uart_ll.h"
#define DATA_LENGTH 1024
#define EX_UART_NUM 1
@@ -240,6 +243,10 @@ TEST_CASE("UHCI write and receive with idle eof", "[uhci]")
TEST_ESP_OK(uart_param_config(EX_UART_NUM, &uart_config));
// Connect TX and RX together for testing self send-receive
TEST_ESP_OK(uart_set_pin(EX_UART_NUM, UART_TX_IO, UART_TX_IO, -1, -1));
// Tying TX to RX through the GPIO matrix can latch a spurious byte into the RX FIFO. Let the
// line settle then drop it, otherwise it prepends a bogus 0x00 to the received data.
vTaskDelay(pdMS_TO_TICKS(20));
uart_ll_rxfifo_rst(UART_LL_GET_HW(EX_UART_NUM));
uhci_controller_config_t uhci_cfg = {
.uart_port = EX_UART_NUM,
@@ -286,6 +293,10 @@ TEST_CASE("UHCI write and receive with length eof", "[uhci]")
TEST_ESP_OK(uart_param_config(EX_UART_NUM, &uart_config));
// Connect TX and RX together for testing self send-receive
TEST_ESP_OK(uart_set_pin(EX_UART_NUM, UART_TX_IO, UART_TX_IO, -1, -1));
// Tying TX to RX through the GPIO matrix can latch a spurious byte into the RX FIFO. Let the
// line settle then drop it, otherwise it prepends a bogus 0x00 to the received data.
vTaskDelay(pdMS_TO_TICKS(20));
uart_ll_rxfifo_rst(UART_LL_GET_HW(EX_UART_NUM));
uhci_controller_config_t uhci_cfg = {
.uart_port = EX_UART_NUM,
@@ -324,6 +335,260 @@ static void uhci_fill_pattern(uint8_t *buf, size_t len, uint8_t start)
}
}
// ---------------------------------------------------------------------------
// Re-arm uhci_receive() from the RX-done callback (ISR context)
// ---------------------------------------------------------------------------
#define REARM_BURST_SIZE 64
#define REARM_BURST_COUNT 4
typedef struct {
QueueHandle_t evt_queue; // carries the index of the just-filled buffer
uint8_t *bufs[2]; // double buffer, alternately armed
size_t buf_size;
int cur; // buffer index currently armed
int done; // number of completed receptions
} uhci_rearm_ctx_t;
typedef struct {
int buf_idx;
size_t size;
const uint8_t *data; // actual DMA data pointer (may be cache-line aligned inside the buffer)
} rearm_evt_t;
// This callback runs in ISR context. On EOF it immediately re-arms reception with the
// other buffer (so RX never idles) and hands the filled buffer to the task for processing.
IRAM_ATTR static bool s_uhci_rx_rearm_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
{
uhci_rearm_ctx_t *ctx = (uhci_rearm_ctx_t *)user_ctx;
BaseType_t xTaskWoken = pdFALSE;
if (edata->flags.totally_received) {
rearm_evt_t evt = { .buf_idx = ctx->cur, .size = edata->recv_size, .data = edata->data };
// Re-arm with the alternate buffer from ISR (except after the last expected burst, so the
// controller can be deleted cleanly), then let the task consume the just-filled one.
if (++ctx->done < REARM_BURST_COUNT) {
ctx->cur ^= 1;
uhci_receive(uhci_ctrl, ctx->bufs[ctx->cur], ctx->buf_size);
}
xQueueSendFromISR(ctx->evt_queue, &evt, &xTaskWoken);
}
return xTaskWoken == pdTRUE;
}
static void uhci_rearm_receive_test(void *arg)
{
void **args = (void **)arg;
uhci_controller_handle_t uhci_ctrl = (uhci_controller_handle_t)args[0];
SemaphoreHandle_t exit_sema = (SemaphoreHandle_t)args[1];
uhci_rearm_ctx_t *ctx = heap_caps_calloc(1, sizeof(uhci_rearm_ctx_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
assert(ctx);
ctx->evt_queue = xQueueCreate(REARM_BURST_COUNT + 2, sizeof(rearm_evt_t));
assert(ctx->evt_queue);
ctx->buf_size = DATA_LENGTH / 4;
for (int i = 0; i < 2; i++) {
ctx->bufs[i] = heap_caps_calloc(1, ctx->buf_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
assert(ctx->bufs[i]);
}
uhci_event_callbacks_t uhci_cbs = {
.on_rx_trans_event = s_uhci_rx_rearm_cbs,
};
TEST_ESP_OK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, ctx));
// Arm the first buffer from task context; subsequent re-arms happen inside the ISR callback.
ctx->cur = 0;
TEST_ESP_OK(uhci_receive(uhci_ctrl, ctx->bufs[0], ctx->buf_size));
rearm_evt_t evt;
for (int i = 0; i < REARM_BURST_COUNT; i++) {
TEST_ASSERT(xQueueReceive(ctx->evt_queue, &evt, portMAX_DELAY) == pdTRUE);
printf("burst %d filled buffer %d, size %d\n", i, evt.buf_idx, (int)evt.size);
TEST_ASSERT_EQUAL(REARM_BURST_SIZE, evt.size);
for (int j = 0; j < evt.size; j++) {
TEST_ASSERT(evt.data[j] == (uint8_t)j);
}
}
vQueueDelete(ctx->evt_queue);
for (int i = 0; i < 2; i++) {
free(ctx->bufs[i]);
}
free(ctx);
xSemaphoreGive(exit_sema);
vTaskDelete(NULL);
}
TEST_CASE("UHCI re-arm receive from ISR callback", "[uhci]")
{
uart_config_t uart_config = {
.baud_rate = 2 * 1000 * 1000,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_XTAL,
};
TEST_ESP_OK(uart_param_config(EX_UART_NUM, &uart_config));
// Connect TX and RX together for testing self send-receive
TEST_ESP_OK(uart_set_pin(EX_UART_NUM, UART_TX_IO, UART_TX_IO, -1, -1));
// Tying TX to RX through the GPIO matrix can latch a spurious byte into the RX FIFO. Let the
// line settle then drop it, otherwise it becomes a bogus 1-byte "frame 0" ahead of the real data.
vTaskDelay(pdMS_TO_TICKS(20));
uart_ll_rxfifo_rst(UART_LL_GET_HW(EX_UART_NUM));
uhci_controller_config_t uhci_cfg = {
.uart_port = EX_UART_NUM,
.tx_trans_queue_depth = 30,
.max_receive_internal_mem = 10 * 1024,
.max_transmit_size = 10 * 1024,
.dma_burst_size = 32,
.rx_eof_flags.idle_eof = 1,
};
uhci_controller_handle_t uhci_ctrl;
SemaphoreHandle_t exit_sema = xSemaphoreCreateBinary();
TEST_ESP_OK(uhci_new_controller(&uhci_cfg, &uhci_ctrl));
void *args[] = { uhci_ctrl, exit_sema };
xTaskCreate(uhci_rearm_receive_test, "uhci_rearm_receive_test", 4096 * 2, args, 5, NULL);
// Give the receiver task time to arm the first buffer before transmitting.
vTaskDelay(100 / portTICK_PERIOD_MS);
uint8_t data_wr[REARM_BURST_SIZE];
for (int i = 0; i < REARM_BURST_SIZE; i++) {
data_wr[i] = i;
}
// Each burst is followed by an idle gap so the RX side generates an idle EOF and the
// ISR callback re-arms reception with the next buffer.
for (int i = 0; i < REARM_BURST_COUNT; i++) {
TEST_ESP_OK(uhci_transmit(uhci_ctrl, data_wr, REARM_BURST_SIZE));
uhci_wait_all_tx_transaction_done(uhci_ctrl, portMAX_DELAY);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
xSemaphoreTake(exit_sema, portMAX_DELAY);
vTaskDelay(2);
TEST_ESP_OK(uhci_del_controller(uhci_ctrl));
vSemaphoreDelete(exit_sema);
}
// ---------------------------------------------------------------------------
// Continuous reception: arm once with uhci_start_receive_continuous(), the driver keeps the DMA running
// across EOFs and each frame lands in the next slot of the ring, no re-arm between frames.
// ---------------------------------------------------------------------------
#define CONT_BURST_SIZE 600 // larger than a single RX DMA node, so each frame spans several nodes
#define CONT_BURST_COUNT 6 // > RX DMA node count, so the ring wraps at least once
typedef struct {
QueueHandle_t done_queue; // carries the reassembled frame length
uint8_t *reasm; // linear reassembly buffer for the current frame
size_t reasm_len; // bytes accumulated so far for the current frame
} cont_ctx_t;
// Runs in ISR context. A frame larger than one DMA node arrives as several node-sized "partial"
// events (totally_received == false) followed by the EOF event, so copy every chunk into a linear
// buffer to reassemble the frame in order (also covers frames crossing the ring wrap-around).
IRAM_ATTR static bool s_uhci_rx_continuous_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
{
cont_ctx_t *ctx = (cont_ctx_t *)user_ctx;
BaseType_t xTaskWoken = pdFALSE;
if (ctx->reasm_len + edata->recv_size <= DATA_LENGTH) {
memcpy(ctx->reasm + ctx->reasm_len, edata->data, edata->recv_size);
// Only count bytes actually copied so the reported length stays consistent with the buffer.
ctx->reasm_len += edata->recv_size;
}
if (edata->flags.totally_received) {
size_t total = ctx->reasm_len;
ctx->reasm_len = 0;
xQueueSendFromISR(ctx->done_queue, &total, &xTaskWoken);
}
return xTaskWoken == pdTRUE;
}
TEST_CASE("UHCI continuous receive keeps DMA running across frames", "[uhci]")
{
uart_config_t uart_config = {
.baud_rate = 2 * 1000 * 1000,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_XTAL,
};
TEST_ESP_OK(uart_param_config(EX_UART_NUM, &uart_config));
// Connect TX and RX together for testing self send-receive
TEST_ESP_OK(uart_set_pin(EX_UART_NUM, UART_TX_IO, UART_TX_IO, -1, -1));
// Tying TX to RX through the GPIO matrix can latch a spurious byte into the RX FIFO (seen when
// this test re-runs). Let the line settle then drop it, otherwise it becomes a bogus 1-byte
// "frame 0" ahead of the real data.
vTaskDelay(pdMS_TO_TICKS(20));
uart_ll_rxfifo_rst(UART_LL_GET_HW(EX_UART_NUM));
uhci_controller_config_t uhci_cfg = {
.uart_port = EX_UART_NUM,
.tx_trans_queue_depth = 30,
.max_receive_internal_mem = 10 * 1024,
.max_transmit_size = 10 * 1024,
.dma_burst_size = 32,
.rx_eof_flags.idle_eof = 1,
};
uhci_controller_handle_t uhci_ctrl;
TEST_ESP_OK(uhci_new_controller(&uhci_cfg, &uhci_ctrl));
cont_ctx_t ctx = {0};
ctx.done_queue = xQueueCreate(CONT_BURST_COUNT + 2, sizeof(size_t));
TEST_ASSERT_NOT_NULL(ctx.done_queue);
// Sized to the whole ring so a buggy over-long frame can't overflow it.
ctx.reasm = heap_caps_calloc(1, DATA_LENGTH, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
TEST_ASSERT_NOT_NULL(ctx.reasm);
uhci_event_callbacks_t uhci_cbs = {
.on_rx_trans_event = s_uhci_rx_continuous_cbs,
};
TEST_ESP_OK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, &ctx));
// A single ring buffer, split across the RX DMA nodes. Each frame is larger than one node so it
// spans several, and consecutive frames wrap the ring around.
uint8_t *ring = heap_caps_calloc(1, DATA_LENGTH, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
TEST_ASSERT_NOT_NULL(ring);
// Arm continuous reception ONCE. Note: no uhci_receive() call between frames below.
TEST_ESP_OK(uhci_start_receive_continuous(uhci_ctrl, ring, DATA_LENGTH));
uint8_t data_wr[CONT_BURST_SIZE];
for (int i = 0; i < CONT_BURST_COUNT; i++) {
// Distinct content per frame so we can verify ordering and correctness.
for (int j = 0; j < CONT_BURST_SIZE; j++) {
data_wr[j] = (uint8_t)(i + j);
}
TEST_ESP_OK(uhci_transmit(uhci_ctrl, data_wr, CONT_BURST_SIZE));
uhci_wait_all_tx_transaction_done(uhci_ctrl, portMAX_DELAY);
// Idle gap so the RX side raises an idle EOF for this frame.
vTaskDelay(pdMS_TO_TICKS(50));
// The whole frame must arrive (reassembled from its node chunks) though RX was never re-armed.
size_t total = 0;
TEST_ASSERT(xQueueReceive(ctx.done_queue, &total, pdMS_TO_TICKS(1000)) == pdTRUE);
printf("frame %d received, size %d\n", i, (int)total);
TEST_ASSERT_EQUAL(CONT_BURST_SIZE, total);
for (int j = 0; j < CONT_BURST_SIZE; j++) {
TEST_ASSERT_EQUAL_HEX8((uint8_t)(i + j), ctx.reasm[j]);
}
}
TEST_ESP_OK(uhci_stop_receive(uhci_ctrl));
vTaskDelay(2);
TEST_ESP_OK(uhci_del_controller(uhci_ctrl));
free(ring);
free(ctx.reasm);
vQueueDelete(ctx.done_queue);
}
TEST_CASE("UHCI single buffer and multi buffer transmit interleaved", "[uhci]")
{
uart_config_t uart_config = {

View File

@@ -0,0 +1,137 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
// Cache-safe UHCI test. Built only when CONFIG_UHCI_ISR_CACHE_SAFE is set (see main/CMakeLists.txt),
// so uhci_receive() can be called from the RX-done callback while the flash cache is disabled.
#include <assert.h>
#include "unity.h"
#include "test_utils.h"
#include "unity_test_utils_cache.h"
#include "esp_attr.h"
#include "esp_rom_sys.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/uhci.h"
#define DATA_LENGTH 1024
#define EX_UART_NUM 1
#define UART_TX_IO 2
#define CACHE_SAFE_BURST_SIZE 64
typedef struct {
TaskHandle_t task_to_notify;
uint8_t *bufs[2]; // double buffer, alternately armed
size_t buf_size;
int cur; // buffer index currently armed
volatile bool stop_rearm; // stop re-arming reception from the ISR
volatile size_t recv_size;
const uint8_t *recv_data;
} uhci_cache_safe_ctx_t;
// Runs in ISR context with the cache disabled. Re-arm reception with the alternate buffer
// (this is the uhci_receive() call under test) and notify the task.
IRAM_ATTR static bool s_uhci_rx_cache_safe_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
{
uhci_cache_safe_ctx_t *ctx = (uhci_cache_safe_ctx_t *)user_ctx;
BaseType_t xTaskWoken = pdFALSE;
if (edata->flags.totally_received) {
ctx->recv_size = edata->recv_size;
ctx->recv_data = edata->data;
if (!ctx->stop_rearm) {
ctx->cur ^= 1;
uhci_receive(uhci_ctrl, ctx->bufs[ctx->cur], ctx->buf_size);
}
vTaskNotifyGiveFromISR(ctx->task_to_notify, &xTaskWoken);
}
return xTaskWoken == pdTRUE;
}
// Holds the cache disabled long enough for the primed transmission to loop back, so the RX
// idle-EOF interrupt fires (and re-arms reception) entirely within this window.
IRAM_ATTR static void s_uhci_hold_cache_disabled(void *args)
{
esp_rom_delay_us(5000);
}
TEST_CASE("UHCI receive from ISR works with cache disabled", "[uhci]")
{
uart_config_t uart_config = {
.baud_rate = 2 * 1000 * 1000,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_XTAL,
};
TEST_ESP_OK(uart_param_config(EX_UART_NUM, &uart_config));
// Connect TX and RX together for testing self send-receive
TEST_ESP_OK(uart_set_pin(EX_UART_NUM, UART_TX_IO, UART_TX_IO, -1, -1));
uhci_controller_config_t uhci_cfg = {
.uart_port = EX_UART_NUM,
.tx_trans_queue_depth = 30,
.max_receive_internal_mem = 10 * 1024,
.max_transmit_size = 10 * 1024,
.dma_burst_size = 32,
.rx_eof_flags.idle_eof = 1,
};
uhci_controller_handle_t uhci_ctrl;
TEST_ESP_OK(uhci_new_controller(&uhci_cfg, &uhci_ctrl));
uhci_cache_safe_ctx_t ctx = {
.task_to_notify = xTaskGetCurrentTaskHandle(),
.buf_size = DATA_LENGTH / 4,
.cur = 0,
};
for (int i = 0; i < 2; i++) {
ctx.bufs[i] = heap_caps_calloc(1, ctx.buf_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
assert(ctx.bufs[i]);
}
uhci_event_callbacks_t uhci_cbs = {
.on_rx_trans_event = s_uhci_rx_cache_safe_cbs,
};
TEST_ESP_OK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, &ctx));
uint8_t data_wr[CACHE_SAFE_BURST_SIZE];
for (int i = 0; i < CACHE_SAFE_BURST_SIZE; i++) {
data_wr[i] = i;
}
// Arm reception, then start a transmission that will loop back. Disable the cache right away:
// the transmit finishes and the RX idle-EOF interrupt fires within the cache-disabled window,
// so uhci_receive() runs from the ISR while the cache is off.
TEST_ESP_OK(uhci_receive(uhci_ctrl, ctx.bufs[0], ctx.buf_size));
TEST_ESP_OK(uhci_transmit(uhci_ctrl, data_wr, CACHE_SAFE_BURST_SIZE));
unity_utils_run_cache_disable_stub(s_uhci_hold_cache_disabled, NULL);
TEST_ASSERT_NOT_EQUAL(0, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000)));
TEST_ASSERT_EQUAL(CACHE_SAFE_BURST_SIZE, ctx.recv_size);
for (int i = 0; i < CACHE_SAFE_BURST_SIZE; i++) {
TEST_ASSERT_EQUAL(data_wr[i], ctx.recv_data[i]);
}
// A second receive was re-armed from the ISR. Stop re-arming and feed it once more so it
// finishes naturally, then wait for it to complete before deleting the controller.
ctx.stop_rearm = true;
TEST_ESP_OK(uhci_transmit(uhci_ctrl, data_wr, CACHE_SAFE_BURST_SIZE));
TEST_ASSERT_NOT_EQUAL(0, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000)));
TEST_ASSERT_EQUAL(CACHE_SAFE_BURST_SIZE, ctx.recv_size);
for (int i = 0; i < CACHE_SAFE_BURST_SIZE; i++) {
TEST_ASSERT_EQUAL(data_wr[i], ctx.recv_data[i]);
}
TEST_ESP_OK(uhci_del_controller(uhci_ctrl));
for (int i = 0; i < 2; i++) {
free(ctx.bufs[i]);
}
}

View File

@@ -1,5 +1,6 @@
CONFIG_COMPILER_DUMP_RTL_FILES=y
CONFIG_UHCI_ISR_CACHE_SAFE=y
CONFIG_UHCI_RECV_FUNC_IN_IRAM=y
CONFIG_GPIO_CTRL_FUNC_IN_IRAM=y
CONFIG_COMPILER_OPTIMIZATION_NONE=y
# silent the error check, as the error string are stored in rodata, causing RTL check failure