mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 10:40:47 +03:00
Merge branch 'feat/uhci_receive_isr_ctn_v6.1' into 'release/v6.1'
feat(uhci): ISR receive and continuous API example/docs (backport v6.1) See merge request espressif/esp-idf!51641
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)
|
||||
|
||||
@@ -38,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.
|
||||
@@ -114,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.
|
||||
@@ -122,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 = UHCI_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) {
|
||||
@@ -236,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,
|
||||
@@ -309,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;
|
||||
|
||||
@@ -329,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;
|
||||
@@ -342,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++) {
|
||||
@@ -354,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) {
|
||||
@@ -366,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,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);
|
||||
@@ -399,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");
|
||||
@@ -523,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) {
|
||||
@@ -624,6 +678,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:
|
||||
|
||||
@@ -90,6 +90,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
|
||||
|
||||
@@ -200,6 +200,30 @@ This mechanism allows the user to achieve continuous and fast reception using a
|
||||
|
||||
The parameter ``read_buffer`` of :cpp:func:`uhci_receive` cannot be freed until receive finishes.
|
||||
|
||||
Continuous Reception
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:cpp:func:`uhci_receive` is a one-shot API: it stops the DMA after a single frame and has to be re-armed for the next one. During that re-arm gap incoming bytes can be lost. For continuous streaming, use :cpp:func:`uhci_start_receive_continuous` instead. It keeps the GDMA running across EOFs, so the reception never idles and no data is dropped between frames.
|
||||
|
||||
The provided ``read_buffer`` is split across the DMA nodes and used as a ring: each finished frame (for example, a UART idle or length EOF) is delivered through the :cpp:member:`uhci_event_callbacks_t::on_rx_trans_event` callback with :cpp:member:`uhci_rx_event_data_t::flags::totally_received` set to 1, while a filled-but-not-yet-complete node is delivered with it set to 0. The callback hands out a pointer directly into the ring buffer (zero-copy), so the application must consume the data before the DMA wraps around and overwrites it. Size the buffer according to the expected throughput and consumer latency; overrun protection is not provided.
|
||||
|
||||
Call :cpp:func:`uhci_stop_receive` to end the session and return the controller to the idle state, after which it can be re-armed or deleted.
|
||||
|
||||
.. code:: c
|
||||
|
||||
// Register callback and start continuous reception.
|
||||
ESP_ERROR_CHECK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, ctx));
|
||||
ESP_ERROR_CHECK(uhci_start_receive_continuous(uhci_ctrl, pdata, buffer_size));
|
||||
|
||||
// ... consume the frames delivered through on_rx_trans_event ...
|
||||
|
||||
// Stop the session before freeing pdata or deleting the controller.
|
||||
ESP_ERROR_CHECK(uhci_stop_receive(uhci_ctrl));
|
||||
|
||||
.. note::
|
||||
|
||||
The parameter ``read_buffer`` of :cpp:func:`uhci_start_receive_continuous` must stay valid until :cpp:func:`uhci_stop_receive` returns.
|
||||
|
||||
Uninstall UHCI controller
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -200,6 +200,30 @@ RX 事件数据在 :cpp:type:`uhci_rx_event_data_t` 中定义:
|
||||
|
||||
在接收完成之前,:cpp:func:`uhci_receive` 的参数 ``read_buffer`` 不可被释放。
|
||||
|
||||
连续接收
|
||||
^^^^^^^^
|
||||
|
||||
:cpp:func:`uhci_receive` 是一次性 (one-shot) 接口:它在接收完一帧后便会停止 DMA,需要重新调用才能接收下一帧,而在这个重新装载的间隙里可能会丢失到来的数据。若需要持续接收数据流,请改用 :cpp:func:`uhci_start_receive_continuous`。它会让 GDMA 跨越 EOF 持续运行,接收过程不会中断,因此帧与帧之间不会丢数据。
|
||||
|
||||
传入的 ``read_buffer`` 会被拆分到各个 DMA 节点上,并作为环形缓冲区循环使用:每接收完一帧(例如 UART idle 或 length EOF),都会通过 :cpp:member:`uhci_event_callbacks_t::on_rx_trans_event` 回调交付,且 :cpp:member:`uhci_rx_event_data_t::flags::totally_received` 置为 1;而某个节点填满但整帧尚未结束时,则以该标志为 0 交付。回调直接给出指向环形缓冲区内部的指针(零拷贝),因此应用必须在 DMA 绕回并覆盖数据之前将其消费掉。请根据预期的吞吐量与消费延迟来确定缓冲区大小;驱动不提供溢出保护。
|
||||
|
||||
调用 :cpp:func:`uhci_stop_receive` 可结束本次接收会话,并使控制器回到空闲状态,之后便可重新装载或删除控制器。
|
||||
|
||||
.. code:: c
|
||||
|
||||
// 注册回调并启动连续接收。
|
||||
ESP_ERROR_CHECK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, ctx));
|
||||
ESP_ERROR_CHECK(uhci_start_receive_continuous(uhci_ctrl, pdata, buffer_size));
|
||||
|
||||
// ... 消费通过 on_rx_trans_event 交付的各帧数据 ...
|
||||
|
||||
// 在释放 pdata 或删除控制器之前,先停止接收会话。
|
||||
ESP_ERROR_CHECK(uhci_stop_receive(uhci_ctrl));
|
||||
|
||||
.. note::
|
||||
|
||||
:cpp:func:`uhci_start_receive_continuous` 的参数 ``read_buffer`` 必须保持有效,直到 :cpp:func:`uhci_stop_receive` 返回。
|
||||
|
||||
卸载 UHCI 控制器
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Build the project and flash it to the board, then run monitor tool to view seria
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
Then transmit the prepared bin to ESP-chips. Please note that the bin should be in one packet transaction which means there should not have any interval during the transaction in this example because it uses idle to judge whether the packet finishes or not.
|
||||
Then transmit the prepared bin to ESP-chips. This example uses `uhci_start_receive_continuous()`, so the DMA keeps running across UART idle gaps and no data is dropped between frames. The transfer is considered finished once the RX line stays idle for about one second (`UART_DMA_OTA_IDLE_TIMEOUT_MS`), so make sure the whole bin is sent without such a long pause in the middle.
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
|
||||
@@ -26,9 +26,11 @@ static const char *TAG = "uhci-example";
|
||||
#define UART_DMA_OTA_BUFFER_SIZE (10 * 1024)
|
||||
#define UART_DMA_OTA_RINGBUF_SIZE (10 * 1024)
|
||||
|
||||
// One second without any new data is treated as end-of-transfer.
|
||||
#define UART_DMA_OTA_IDLE_TIMEOUT_MS 1000
|
||||
|
||||
typedef struct {
|
||||
RingbufHandle_t ringbuf;
|
||||
volatile bool rx_eof;
|
||||
volatile bool rx_overflow;
|
||||
} ota_rx_context_t;
|
||||
|
||||
@@ -37,22 +39,15 @@ static bool s_uhci_rx_event_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_r
|
||||
ota_rx_context_t *ctx = (ota_rx_context_t *)user_ctx;
|
||||
BaseType_t xTaskWoken = pdFALSE;
|
||||
|
||||
// Copy each chunk out of the DMA storage buffer before it gets overwritten. In continuous mode
|
||||
// the driver keeps the DMA running across frames, so this fires for every partial node and every
|
||||
// frame EOF without any gap in between.
|
||||
if (xRingbufferSendFromISR(ctx->ringbuf, edata->data, edata->recv_size, &xTaskWoken) != pdTRUE) {
|
||||
ctx->rx_overflow = true;
|
||||
}
|
||||
if (edata->flags.totally_received) {
|
||||
ctx->rx_eof = true;
|
||||
}
|
||||
return xTaskWoken == pdTRUE;
|
||||
}
|
||||
|
||||
static bool rx_ringbuf_is_empty(RingbufHandle_t ringbuf)
|
||||
{
|
||||
UBaseType_t items_waiting = 0;
|
||||
vRingbufferGetInfo(ringbuf, NULL, NULL, NULL, NULL, &items_waiting);
|
||||
return items_waiting == 0;
|
||||
}
|
||||
|
||||
static void perform_ota_update(uhci_controller_handle_t uhci_ctrl, ota_rx_context_t *ctx)
|
||||
{
|
||||
const esp_partition_t *ota_partition = esp_ota_get_next_update_partition(NULL);
|
||||
@@ -65,29 +60,35 @@ static void perform_ota_update(uhci_controller_handle_t uhci_ctrl, ota_rx_contex
|
||||
ESP_ERROR_CHECK(esp_ota_begin(ota_partition, OTA_SIZE_UNKNOWN, &ota_handle));
|
||||
ESP_LOGI(TAG, "OTA process started");
|
||||
|
||||
// Storage buffer for continuous reception. The driver splits it across the DMA nodes and uses it
|
||||
// as a ring, so the OTA image can be much larger than the buffer itself.
|
||||
uint8_t *pdata = heap_caps_calloc(1, UART_DMA_OTA_BUFFER_SIZE, MALLOC_CAP_DEFAULT);
|
||||
assert(pdata);
|
||||
ESP_ERROR_CHECK(uhci_receive(uhci_ctrl, pdata, UART_DMA_OTA_BUFFER_SIZE));
|
||||
ESP_ERROR_CHECK(uhci_start_receive_continuous(uhci_ctrl, pdata, UART_DMA_OTA_BUFFER_SIZE));
|
||||
|
||||
size_t total_received_size = 0;
|
||||
while (1) {
|
||||
size_t item_size = 0;
|
||||
uint8_t *data = xRingbufferReceive(ctx->ringbuf, &item_size, pdMS_TO_TICKS(1000));
|
||||
uint8_t *data = xRingbufferReceive(ctx->ringbuf, &item_size, pdMS_TO_TICKS(UART_DMA_OTA_IDLE_TIMEOUT_MS));
|
||||
if (data) {
|
||||
ESP_ERROR_CHECK(esp_ota_write(ota_handle, data, item_size));
|
||||
vRingbufferReturnItem(ctx->ringbuf, data);
|
||||
total_received_size += item_size;
|
||||
} else if (total_received_size > 0) {
|
||||
// The ring buffer stayed empty for the whole timeout after data had started flowing:
|
||||
// the sender is done. A NULL return only happens when the ring buffer is drained, so no
|
||||
// received bytes are left behind here.
|
||||
break;
|
||||
}
|
||||
|
||||
if (ctx->rx_overflow) {
|
||||
ESP_LOGE(TAG, "RX ring buffer overflow, please reduce the baud rate or increase the ring buffer size");
|
||||
abort();
|
||||
}
|
||||
|
||||
if (ctx->rx_eof && rx_ringbuf_is_empty(ctx->ringbuf)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the continuous reception before releasing the storage buffer.
|
||||
ESP_ERROR_CHECK(uhci_stop_receive(uhci_ctrl));
|
||||
free(pdata);
|
||||
|
||||
ESP_LOGI(TAG, "Total received size: %zu", total_received_size);
|
||||
@@ -115,7 +116,7 @@ void app_main(void)
|
||||
.max_receive_internal_mem = UART_DMA_OTA_BUFFER_SIZE,
|
||||
.max_transmit_size = UART_DMA_OTA_BUFFER_SIZE,
|
||||
.dma_burst_size = 32,
|
||||
.rx_eof_flags.idle_eof = 1, // receive finishes when rx line turns idle.
|
||||
.rx_eof_flags.idle_eof = 1, // deliver a frame (EOF) whenever the rx line turns idle.
|
||||
};
|
||||
|
||||
uhci_controller_handle_t uhci_ctrl;
|
||||
@@ -125,7 +126,6 @@ void app_main(void)
|
||||
|
||||
ota_rx_context_t ctx = {
|
||||
.ringbuf = xRingbufferCreate(UART_DMA_OTA_RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF),
|
||||
.rx_eof = false,
|
||||
.rx_overflow = false,
|
||||
};
|
||||
assert(ctx.ringbuf);
|
||||
|
||||
Reference in New Issue
Block a user