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

This commit is contained in:
C.S.M
2026-07-30 11:41:24 +08:00
parent 8550997ada
commit fcd5c55194
9 changed files with 594 additions and 59 deletions
@@ -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
)
@@ -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 = {
@@ -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]);
}
}
@@ -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