mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(uhci): uhci receive can be called in isr
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user