# UART DMA (UHCI)

[中文]

This document describes the functionality of the UART DMA(UHCI) driver in ESP-IDF. The table of contents is as follows:

## Introduction

This document shows how to use UART and DMA together for transmitting or receiving large data volumes using high baud rates. 2 HP UART controllers on ESP32-C6 share one group of DMA TX/RX channels via host controller interface (HCI). This document assumes that UART DMA is controlled by UHCI entity.

Note

The UART DMA shares the HCI hardware with Bluetooth, so please don't use BT HCI together with UART DMA, even if they use different UART ports.

## Quick Start

This section will quickly guide you on how to use the UHCI driver. Through a simple example including transmitting and receiving, it demonstrates how to create and start a UHCI, initiate a transmit and receive transactions, and register event callback functions. The general usage process is as follows:

### Creating and Enabling the UHCI controller

UHCI controller requires the configuration specified by `uhci_controller_config_t`.

If the configurations in `uhci_controller_config_t` is specified, users can call `uhci_new_controller()` to allocate and initialize a uhci controller. This function will return a uhci controller handle if it runs correctly. Besides, UHCI must work with the installed UART driver. As a reference, see the code below.

    #define EX_UART_NUM 1       // Define UART port number

    // For uart port configuration, please refer to UART programming guide.
    // Please double-check as the baud rate might be limited by serial port chips.
    uart_config_t uart_config = {
        .baud_rate = 1 * 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_DEFAULT,
    };

    //UART parameter config
    ESP_ERROR_CHECK(uart_param_config(EX_UART_NUM, &uart_config));
    ESP_ERROR_CHECK(uart_set_pin(EX_UART_NUM, UART_TX_IO, UART_RX_IO, -1, -1));

    uhci_controller_config_t uhci_cfg = {
        .uart_port = EX_UART_NUM,                 // Connect uart port to UHCI hardware.
        .tx_trans_queue_depth = 30,               // Queue depth of transaction queue.
        .max_receive_internal_mem = 10 * 1024,    // internal memory usage, for more information, please refer to API reference.
        .max_transmit_size = 10 * 1024,           // Maximum transfer size in one transaction, in bytes.
        .dma_burst_size = 32,                     // Burst size.
        .rx_eof_flags.idle_eof = 1,               // When to trigger a end of frame event, you can choose `idle_eof`, `rx_brk_eof`, `length_eof`, for more information, please refer to API reference.
    };

    uhci_controller_handle_t uhci_ctrl;

    ESP_ERROR_CHECK(uhci_new_controller(&uhci_cfg, &uhci_ctrl));

### Register Event Callbacks

When an event occurs on the UHCI controller (e.g., transmission or receiving is completed), the CPU is notified of this event via an interrupt. If there is a function that needs to be called when a particular events occur, you can register a callback for that event with the ISR for UHCI (Interrupt Service Routine) by calling `uhci_register_event_callbacks()` for both TX and RX respectively. Since the registered callback functions are called in the interrupt context, the user should ensure that the callback function is non-blocking, e.g., by making sure that only FreeRTOS APIs with the `FromISR` suffix are called from within the function. The callback function has a boolean return value used to indicate whether a higher priority task has been unblocked by the callback.

The UHCI event callbacks are listed in the `uhci_event_callbacks_t`:

- `uhci_event_callbacks_t::on_tx_trans_done` sets a callback function for the "trans-done" event. The function prototype is declared in `uhci_tx_done_callback_t`.

- `uhci_event_callbacks_t::on_rx_trans_event` sets a callback function for "receive" event. The function prototype is declared in `uhci_rx_event_callback_t`.

Note

The "rx-trans-event" is not equivalent to "receive-finished". This callback can also be called at a "partial-received" time, for many times during one receive transaction, which can be notified by `uhci_rx_event_data_t::flags::totally_received`.

Users can save their own context in `uhci_register_event_callbacks()` as well, via the parameter `user_data`. The user data is directly passed to each callback function.

In the callback function, users can fetch the event-specific data that is filled by the driver in the `edata`. Note that the `edata` pointer is only valid during the callback, please do not try to save this pointer and use that outside of the callback function.

The TX event data is defined in `uhci_tx_done_event_data_t`:

- `uhci_tx_done_event_data_t::buffer` indicates the buffer has been sent out.

The RX event data is defined in `uhci_rx_event_data_t`:

- `uhci_rx_event_data_t::data` points to the received data. The data is stored in the buffer specified by the `buffer` parameter of `uhci_receive()`, so users should not free this receive buffer before the callback returns. Data pointed to by `edata->data` is typically only guaranteed to be readable during the callback. If application code needs to use the received data after callback returns, copy it to a external buffer first.

- `uhci_rx_event_data_t::recv_size` indicates the number of received data. This value is not larger than the `buffer_size` parameter of `uhci_receive()` function.

- `uhci_rx_event_data_t::flags::totally_received` indicates whether the current received buffer is the last one in the transaction.

Note

Forwarding `edata->data` pointer to another task without copying is an advanced zero-copy usage. To keep it safe, user code must understand the chunking and overwrite behavior of the underlying circular DMA buffer, and guarantee the consumer can process data before it gets overwritten.

### Initiating UHCI Transmission

`uhci_transmit()` is a non-blocking function, which means this function will immediately return after you call it. The related callback can be obtained via `uhci_event_callbacks_t::on_tx_trans_done` to indicate that the transaction is done. The function `uhci_wait_all_tx_transaction_done()` can be used to block the thread until all transactions are finished.

Data can be transmitted via UHCI as follows:

    uint8_t data_wr[DATA_LENGTH];
    for (int i = 0; i < DATA_LENGTH; i++) {
        data_wr[i] = i;
    }
    ESP_ERROR_CHECK(uhci_transmit(uhci_ctrl, data_wr, DATA_LENGTH));
    // Wait all transaction finishes
    ESP_ERROR_CHECK(uhci_wait_all_tx_transaction_done(uhci_ctrl, -1));

### Initiating UHCI Reception

`uhci_receive()` is a non-blocking function, which means this function will immediately return after it is called. The related callback can be obtained via `uhci_event_callbacks_t::on_rx_trans_event` to indicate the receive event. It can be useful to determine if a transaction has been finished.

Data can be received via UHCI as follows:

    // global variable: handle of queue.
    QueueHandle_t uhci_queue;

    IRAM_ATTR static bool s_uhci_rx_event_cbs(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)
    {
        // parameter `user_ctx` is parsed by the third parameter of function `uhci_register_event_callbacks`
        uhci_context_t *ctx = (uhci_context_t *)user_ctx;
        BaseType_t xTaskWoken = 0;
        uhci_event_t evt = 0;
        if (edata->flags.totally_received) {
            evt = UHCI_EVT_EOF;
            ctx->receive_size += edata->recv_size;
            memcpy(ctx->p_receive_data, edata->data, edata->recv_size);
        } else {
            evt = UHCI_EVT_PARTIAL_DATA;
            ctx->receive_size += edata->recv_size;
            memcpy(ctx->p_receive_data, edata->data, edata->recv_size);
            ctx->p_receive_data += edata->recv_size;
        }

        xQueueSendFromISR(uhci_queue, &evt, &xTaskWoken);
        return xTaskWoken;
    }

    // In task
    uhci_event_callbacks_t uhci_cbs = {
        .on_rx_trans_event = s_uhci_rx_event_cbs,
    };

    // Register callback and start reception.
    ESP_ERROR_CHECK(uhci_register_event_callbacks(uhci_ctrl, &uhci_cbs, ctx));
    ESP_ERROR_CHECK(uhci_receive(uhci_ctrl, pdata, 100));

    uhci_event_t evt;
    while (1) {
        // A queue in task for receiving event triggered by UHCI.
        if (xQueueReceive(uhci_queue, &evt, portMAX_DELAY) == pdTRUE) {
            if (evt == UHCI_EVT_EOF) {
                printf("Received size: %d\n", ctx->receive_size);
                break;
            }
        }
    }

In the API `uhci_receive()` interface, the parameter read_buffer is a buffer that must be provided by the user, and parameter buffer_size represents the size of the buffer supplied by the user. In the configuration structure of the UHCI controller, the parameter `uhci_controller_config_t::max_receive_internal_mem` specifies the desired size of the internal DMA working space. The software allocates a certain number of DMA nodes based on this working space size. These nodes form a circular linked list.

When a node is filled, but the reception has not yet completed, the event `uhci_event_callbacks_t::on_rx_trans_event` will be triggered, accompanied by `uhci_rx_event_data_t::flags::totally_received` set to 0. When all the data has been fully received, the `uhci_event_callbacks_t::on_rx_trans_event` event will be triggered again with `uhci_rx_event_data_t::flags::totally_received` set to 1.

This mechanism allows the user to achieve continuous and fast reception using a relatively small buffer, without needing to allocate a buffer the same size as the total data being received.

Note

The parameter read_buffer of `uhci_receive()` cannot be freed until receive finishes.

### Uninstall UHCI controller

If a previously installed UHCI controller is no longer needed, it's recommended to recycle the resource by calling `uhci_del_controller()`, so that the underlying hardware is released.

    ESP_ERROR_CHECK(uhci_del_controller(uhci_ctrl));

## Advanced Features

As the basic usage has been covered, it's time to explore more advanced features of the UHCI driver.

### Power Management

When power management is enabled, i.e., CONFIG_PM_ENABLE is on, the system may adjust or disable the clock source before going to sleep. As a result, the FIFO inside the UHCI can't work as expected.

The driver can prevent the above issue by creating a power management lock. The lock type is set based on different clock sources. The driver will acquire the lock in `uhci_receive()` or `uhci_transmit()`, and release it in the transaction-done interrupt. That means, any UHCI transactions between these two functions are guaranteed to work correctly and stably.

### Cache Safe

By default, the interrupt on which UHCI relies is deferred when the Cache is disabled for reasons such as writing or erasing the main flash. Thus, the transaction-done interrupt fails to be handled in time, which is unacceptable in a real-time application. What is worse, when the UHCI transaction relies on ping-pong interrupt to successively encode or copy the UHCI buffer, a delayed interrupt can lead to an unpredictable result.

There is a Kconfig option CONFIG_UHCI_ISR_CACHE_SAFE that has the following features:

1. Enable the interrupt being serviced even when the cache is disabled

2. Place all functions used by the ISR into IRAM [1]

3. Place the driver object into DRAM in case it is mapped to PSRAM by accident

This Kconfig option allows the interrupt handler to run while the cache is disabled but comes at the cost of increased IRAM consumption.

### Resource Consumption

Use the IDF Size tool to check the code and data consumption of the UHCI driver. The following are the test results under 2 different conditions (using ESP32-C3 as an example):

Note that the following data are not exact values and are for reference only; they may differ on different chip models.

Resource consumption when CONFIG_UHCI_ISR_CACHE_SAFE is enabled:

Resource Consumption
Component Layer | Total Size | DIRAM | .bss | .data | .text | Flash Code | Flash Data | .rodata
--- | --- | --- | --- | --- | --- | --- | --- | ---
UHCI | 5733 | 680 | 8 | 34 | 638 | 4878 | 175 | 175

Resource consumption when CONFIG_UHCI_ISR_CACHE_SAFE is disabled:

Resource Consumption
Component Layer | Total Size | DIRAM | .bss | .data | .text | Flash Code | .text | Flash Data | .rodata
--- | --- | --- | --- | --- | --- | --- | --- | --- | ---
UHCI | 5479 | 42 | 8 | 34 | 0 | 5262 | 5262 | 175 | 175

### Performance

To improve the real-time response capability of interrupt handling, the UHCI driver provides the CONFIG_UHCI_ISR_HANDLER_IN_IRAM option. Enabling this option will place the interrupt handler in internal RAM, reducing the latency caused by cache misses when loading instructions from Flash.

Note

However, user callback functions and context data called by the interrupt handler may still be located in Flash, and cache miss issues will still exist. Users need to place callback functions and data in internal RAM, for example, using `IRAM_ATTR` and `DRAM_ATTR`.

### Thread Safety

The factory function `uhci_new_controller()`, `uhci_register_event_callbacks()` and `uhci_del_controller()` are guaranteed to be thread safe by the driver, which means, user can call them from different RTOS tasks without protection by extra locks.

### Other Kconfig Options

- CONFIG_UHCI_ENABLE_DEBUG_LOG is allowed for the forced enabling of all debug logs for the UHCI driver, regardless of the global log level setting. Enabling this option can help developers obtain more detailed log information during the debugging process, making it easier to locate and resolve issues, but it will increase the size of the firmware binary.

## Application Examples

- peripherals/uart/uart_dma_ota demonstrates how to use the uart dma for fast OTA the chip firmware with 1M baud rate speed.

## API Reference

### Header File

- components/esp_driver_uart/include/driver/uhci.h

- This header file can be included with:

  >     #include "driver/uhci.h"

- This header file is a part of the API provided by the `esp_driver_uart` component. To declare that your component depends on `esp_driver_uart`, add the following to your CMakeLists.txt:

  >     REQUIRES esp_driver_uart

  > or

  >     PRIV_REQUIRES esp_driver_uart

### Functions

esp_err_t uhci_new_controller(const uhci_controller_config_t *config, uhci_controller_handle_t *ret_uhci_ctrl)

  Create and initialize a new UHCI controller.

  This function initializes a new UHCI controller instance based on the provided configuration. It allocates and configures resources required for the UHCI controller, such as DMA and communication settings. The created controller handle is returned through the output parameter.

  Parameters:

    - config -- [in] Pointer to a `uhci_controller_config_t` structure containing the configuration parameters for the UHCI controller.

    - ret_uhci_ctrl -- [out] Pointer to a variable where the handle to the newly created UHCI controller will be stored. This handle is used in subsequent operations involving the controller.

  Returns:

    - `ESP_OK`: Controller successfully created and initialized.

    - `ESP_ERR_INVALID_ARG`: One or more arguments are invalid (e.g., null pointers or invalid config).

    - `ESP_ERR_NO_MEM`: Memory allocation for the controller failed.

    - Other error codes: Indicate failure in the underlying hardware or driver initialization.

esp_err_t uhci_receive(uhci_controller_handle_t uhci_ctrl, uint8_t *read_buffer, size_t buffer_size)

  Receive data from the UHCI controller.

  This function retrieves data from the UHCI controller into the provided buffer. It is typically used for receiving data that was transmitted via UART and processed by the UHCI DMA controller.

  Note

  Note

  The function is non-blocking, it just mounts the user buffer to the DMA. The return from the function doesn't mean a finished receive. You need to register corresponding callback function to get notification.

  Parameters:

    - uhci_ctrl -- [in] Handle to the UHCI controller, which was previously created using `uhci_new_controller()`.

    - read_buffer -- [out] Pointer to the buffer where the received data will be stored. The buffer must be pre-allocated by the caller.

    - buffer_size -- [in] The size of read buffer.

  Returns:

    - `ESP_OK`: Data successfully received and written to the buffer.

    - `ESP_ERR_INVALID_ARG`: Invalid arguments (e.g., null buffer or invalid controller handle).

esp_err_t uhci_transmit(uhci_controller_handle_t uhci_ctrl, uint8_t *write_buffer, size_t write_size)

  Transmit data using the UHCI controller.

  This function sends data from the provided buffer through the UHCI controller. It uses the DMA capabilities of UHCI to efficiently handle data transmission via UART.

  Note

  The function is an non-blocking api, which means this function will return immediately. You can get corresponding event from callbacks.

  Parameters:

    - uhci_ctrl -- [in] Handle to the UHCI controller, which was previously created using `uhci_new_controller()`.

    - write_buffer -- [in] Pointer to the buffer containing the data to be transmitted. The buffer must remain valid until the transmission is complete.

    - write_size -- [in] The number of bytes to transmit from the buffer.

  Returns:

    - `ESP_OK`: Data successfully queued for transmission.

    - `ESP_ERR_INVALID_ARG`: Invalid arguments (e.g., null buffer, invalid handle, or zero `write_size`).

esp_err_t uhci_del_controller(uhci_controller_handle_t uhci_ctrl)

  Uninstall the UHCI (UART Host Controller Interface) driver and release resources.

  This function deinitializes the UHCI controller and frees any resources allocated during its initialization. It ensures proper cleanup and prevents resource leaks when the UHCI controller is no longer needed.

  Parameters:

    uhci_ctrl -- [in] Handle to the UHCI controller, which was previously created using `uhci_new_controller()`. Passing an invalid or uninitialized handle may result in undefined behavior.

  Returns:

    - `ESP_OK`: The UHCI driver was successfully uninstalled, and resources were released.

    - `ESP_ERR_INVALID_ARG`: The provided `uhci_ctrl` handle is invalid or null.

esp_err_t uhci_register_event_callbacks(uhci_controller_handle_t uhci_ctrl, const uhci_event_callbacks_t *cbs, void *user_data)

  Register event callback functions for a UHCI controller.

  This function allows the user to register callback functions to handle specific UHCI events, such as transmission or reception completion. The callbacks provide a mechanism to handle asynchronous events generated by the UHCI controller.

  Parameters:

    - uhci_ctrl -- [in] Handle to the UHCI controller, which was previously created using `uhci_new_controller()`.

    - cbs -- [in] Pointer to a `uhci_event_callbacks_t` structure that defines the callback functions to be registered. This structure includes pointers to the callback functions for handling UHCI events.

    - user_data -- [in] Pointer to user-defined data that will be passed to the callback functions when they are invoked. This can be used to provide context or state information specific to the application.

  Returns:

    - `ESP_OK`: Event callbacks were successfully registered.

    - `ESP_ERR_INVALID_ARG`: Invalid arguments (e.g., null `uhci_ctrl` handle or `cbs` pointer).

esp_err_t uhci_wait_all_tx_transaction_done(uhci_controller_handle_t uhci_ctrl, int timeout_ms)

  Wait for all pending TX transactions done.

  Parameters:

    - uhci_ctrl -- [in] UHCI controller that created by `uhci_new_controller`

    - timeout_ms -- [in] Timeout in milliseconds, `-1` means to wait forever

  Returns:

    - ESP_OK: All pending TX transactions is finished and recycled

    - ESP_ERR_INVALID_ARG: Wait for all pending TX transactions done failed because of invalid argument

    - ESP_ERR_TIMEOUT: Wait for all pending TX transactions done timeout

    - ESP_FAIL: Wait for all pending TX transactions done failed because of other error

### Structures

struct uhci_controller_config_t

  UHCI controller specific configurations.

  Public Members

  uart_port_t uart_port

    UART port that connect to UHCI controller

  size_t tx_trans_queue_depth

    Depth of internal transfer queue, increase this value can support more transfers pending in the background

  size_t max_transmit_size

    Maximum transfer size in one transaction, in bytes. This decides the number of DMA nodes will be used for each transaction

  size_t max_receive_internal_mem

    Internal DMA usage memory. Each DMA node can point to a maximum of x bytes (depends on chip). This value determines the number of DMA nodes used for each transaction. When your transfer size is large enough, it is recommended to set this value greater than x to facilitate efficient ping-pong operations, such as 2 * x.

  size_t dma_burst_size

    DMA burst size, in bytes. Set to 0 to disable data burst. Otherwise, use a power of 2.

  size_t max_packet_receive

    Max receive size, auto stop receiving after reach this value, only valid when `length_eof` set true

  uint16_t rx_brk_eof

    UHCI will end payload receive process when NULL frame is received by UART.

  uint16_t idle_eof

    UHCI will end payload receive process when UART has been in idle state.

  uint16_t length_eof

    UHCI will end payload receive process when the receiving byte count has reached the specific value.

  struct uhci_controller_config_t rx_eof_flags

    UHCI eof flags

struct uhci_event_callbacks_t

  Structure for defining callback functions for UHCI events.

  Public Members

  uhci_rx_event_callback_t on_rx_trans_event

    Callback function for handling the completion of a reception.

  uhci_tx_done_callback_t on_tx_trans_done

    Callback function for handling the completion of a transmission.

### Header File

- components/esp_driver_uart/include/driver/uhci_types.h

- This header file can be included with:

  >     #include "driver/uhci_types.h"

- This header file is a part of the API provided by the `esp_driver_uart` component. To declare that your component depends on `esp_driver_uart`, add the following to your CMakeLists.txt:

  >     REQUIRES esp_driver_uart

  > or

  >     PRIV_REQUIRES esp_driver_uart

### Structures

struct uhci_tx_done_event_data_t

  UHCI TX Done Event Data.

  Public Members

  uint8_t *buffer

    Pointer to the which data buffer has been finished the transaction

  size_t sent_size

    Size has been sent out

struct uhci_rx_event_data_t

  UHCI RX Done Event Data Structure.

  Public Members

  const uint8_t *data

    Pointer to the received data buffer. Data pointed to by this pointer is typically only guaranteed to be readable during the callback. If you need to use it after callback returns, copy it to external buffer first or refer to advanced zero-copy usage.

  size_t recv_size

    Number of bytes received

  uint32_t totally_received

    When callback is invoked, while this bit is not set, means the current event gives partial of whole data, the transaction has not been finished. If set, means the current event gives whole data, the transaction finished.

  struct uhci_rx_event_data_t flags

    UHCI RX event flags

### Type Definitions

typedef struct uhci_controller_t *uhci_controller_handle_t

  UHCI Controller Handle Type.

typedef bool (*uhci_tx_done_callback_t)(uhci_controller_handle_t uhci_ctrl, const uhci_tx_done_event_data_t *edata, void *user_ctx)

  UHCI TX Done Callback Function Type.

  Param uhci_ctrl:

    Handle to the UHCI controller that initiated the transmission.

  Param edata:

    Pointer to a structure containing event data related to the completed transmission. This structure provides details such as the number of bytes transmitted and any status information relevant to the operation.

  Param user_ctx:

    User-defined context passed during the callback registration. It can be used to maintain application-specific state or data.

  Return:

    Whether a high priority task has been waken up by this callback function

typedef bool (*uhci_rx_event_callback_t)(uhci_controller_handle_t uhci_ctrl, const uhci_rx_event_data_t *edata, void *user_ctx)

  UHCI RX Done Callback Function Type.

  Param uhci_ctrl:

    Handle to the UHCI controller that initiated the transmission.

  Param edata:

    Pointer to a structure containing event data related to receive event. This structure provides details such as the number of bytes received and any status information relevant to the operation. The `edata` pointer is only valid during the callback. So do not save this pointer and use it outside the callback.

  Param user_ctx:

    User-defined context passed during the callback registration. It can be used to maintain application-specific state or data.

  Return:

    Whether a high priority task has been waken up by this callback function

### Header File

- components/hal/include/hal/uhci_types.h

- This header file can be included with:

  >     #include "hal/uhci_types.h"

### Structures

struct uhci_seper_chr_t

  UHCI escape sequence.

  Public Members

  uint8_t seper_chr

    escape sequence character

  uint8_t sub_chr1

    escape sequence sub-character 1

  uint8_t sub_chr2

    escape sequence sub-character 2

  bool sub_chr_en

    enable use of sub-chaacter of escape sequence

struct uhci_swflow_ctrl_sub_chr_t

  UHCI software flow control.

  Public Members

  uint8_t xon_chr

    character for XON

  uint8_t xon_sub1

    sub-character 1 for XON

  uint8_t xon_sub2

    sub-character 2 for XON

  uint8_t xoff_chr

    character 2 for XOFF

  uint8_t xoff_sub1

    sub-character 1 for XOFF

  uint8_t xoff_sub2

    sub-character 2 for XOFF

  uint8_t flow_en

    enable use of software flow control

[1]

The callback function, e.g., `uhci_event_callbacks_t::on_tx_trans_done`, `uhci_event_callbacks_t::on_rx_trans_event` and the functions invoked by itself should also reside in IRAM, users need to take care of this by themselves.

Was this page helpful?

- Thank you! We received your feedback.
- If you have any comments, fill in Espressif Documentation Feedback Form.

- We value your feedback.
- Let us know how we can improve this page by filling in Espressif Documentation Feedback Form.