You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
UART has no callback registration. It signals only through the FreeRTOS event queue handed to uart_driver_install(), so a task has to sit blocked on that queue. A queue set can multiplex several ports, or other queues, onto one task, but a task is still required.
It is the only driver in ESP-IDF v6.0 still working that way. Grepping components/esp_driver_*/include/ for callback registration turns up 31 APIs:
In a single-threaded event-loop design the other peripherals integrate without a task of their own. i2c_master_register_event_callbacks and twai_node_register_event_callbacks fire in ISR context, and getting from there to the loop costs a few instructions and no copy.
With UART the same path needs:
a 4096-byte FreeRTOS task whose entire body is a blocking queue receive followed by uart_read_bytes(), existing only to get bytes onto the loop;
a heap allocation and copy of every parsed frame, because a view into the task's own buffer cannot outlive the task boundary.
Both exist because the driver's only notification mechanism is a queue that something has to block on. In a wrapper layer over IDF peripherals, UART then needs a thread hop that nothing else does.
Describe the solution you'd like.
uart_register_event_callbacks(), shaped like the ones the other drivers already have:
typedefstruct {
uart_rx_data_callback_ton_rx_data; /* bytes available in the ring buffer */uart_rx_break_callback_ton_rx_break;
uart_pattern_callback_ton_pattern; /* pattern detect, as UART_PATTERN_DET */uart_rx_error_callback_ton_rx_error; /* frame/parity error, FIFO overflow, buffer full */uart_tx_done_callback_ton_tx_done; /* TX drained, or space available */
} uart_event_callbacks_t;
esp_err_tuart_register_event_callbacks(uart_port_tuart_num,
constuart_event_callbacks_t*cbs,
void*user_ctx);
Following the same conventions:
called from ISR context, returning bool for "a higher-priority task was woken", as twai_node and i2c_master do;
mutually exclusive with the event queue, registered after uart_driver_install(..., queue = NULL, ...);
carrying the same information as the matching uart_event_t, so existing code ports field for field.
A handle-based UART driver, in the style of the other esp_driver_* components, would be the obvious home for this, but the rewrite is not what we are asking for. Adding callback registration to the current driver would be enough to drop the mandatory task, and the ISR already has the hook points (see below).
As an alternative worth exploring, going a step further than the callbacks listed above: with callbacks in place, neither driver-side buffer is strictly necessary.
Every callback above is a notification about a buffer: on_rx_data reports bytes that landed in the RX ring buffer, on_tx_done reports space returned in the TX one. If they carried the data instead, the buffers would have little left to do.
On RX that is a read from within the callback — the caller supplies a buffer, the driver fills it — in the shape of twai_node_receive_from_isr(). The ISR already stages the FIFO contents in p_uart->rx_data_buf, and the volume per callback is bounded by UART_HW_FIFO_LEN, so the caller has a known worst case. On TX it is the reverse: a submit-and-notify call like twai_node_transmit() or uhci_transmit(), the caller keeping the buffer valid until on_tx_done. uart_write_bytes() blocks in both configurations today, so there is no way to start a transmission and return.
That would make rx_buffer_size == 0 and tx_buffer_size == 0 a supported combination rather than an error, with uart_read_bytes(), uart_flush_input() and uart_get_buffered_data_len() returning ESP_ERR_NOT_SUPPORTED, and pattern detection unavailable, since its positions are offsets into buffered data.
Describe alternatives you've considered.
1. The event queue and a task. What we do today. A queue set keeps that to one task rather than one per port, but the task and its stack remain, as does a copy of every message that leaves it. On a high-rate port that copy dominates an otherwise allocation-free path.
2. uart_set_select_notif_callback() (driver/uart_select.h). This is the hook we want, and it already fires from the ISR on every UART_DATA event, not only while a select() is in flight. In esp_driver_uart/src/uart.c:
if (uart_event.type==UART_DATA) {
UART_ENTER_CRITICAL_ISR(&uart_selectlock);
if (p_uart->uart_select_notif_callback) {
p_uart->uart_select_notif_callback(uart_num, UART_SELECT_READ_NOTIF, &HPTaskAwoken);
with UART_SELECT_ERROR_NOTIF on FIFO overflow, and a BaseType_t *task_woken out-parameter. Functionally it is a working ISR callback.
We would rather not build on it: it is documented only as select() plumbing, there is one slot per port, and taking it would silently break any VFS select() on that UART. The hook already exists and works; what is missing is a supported way to use it.
3. UHCI (uhci_register_event_callbacks). A modern callback API with DMA behind it, and presumably the intended high-throughput path. SOC_UHCI_SUPPORTED is defined only for ESP32-S3 and ESP32-P4 though, and SOC_UHCI_NUM is 1, so one UART per chip at most and nothing on ESP32 or the C-series. That rules it out as a general answer.
4. VFS + select(). Pulls in the VFS layer and still needs a task blocked in select(). The task moves; it does not go away.
UART RX/TX Interrupt Example (IDFGH-10219) #11485 (IDFGH-10219) asks for much the same thing: how to register a UART interrupt handler and learn when there is buffer space, without polling and while coexisting with uart_driver_install(). Opened May 2023, still open with no reply, so we are filing something more specific rather than adding to it.
If a public callback API is not planned, it would help to know whether uart_set_select_notif_callback() is usable by application code, or whether it is private in all but placement. As it stands, anyone wrapping UART asynchronously has to spend a task on it.
Is your feature request related to a problem?
UART has no callback registration. It signals only through the FreeRTOS event queue handed to
uart_driver_install(), so a task has to sit blocked on that queue. A queue set can multiplex several ports, or other queues, onto one task, but a task is still required.It is the only driver in ESP-IDF v6.0 still working that way. Grepping
components/esp_driver_*/include/for callback registration turns up 31 APIs:driver/uart.hhas none.In a single-threaded event-loop design the other peripherals integrate without a task of their own.
i2c_master_register_event_callbacksandtwai_node_register_event_callbacksfire in ISR context, and getting from there to the loop costs a few instructions and no copy.With UART the same path needs:
uart_read_bytes(), existing only to get bytes onto the loop;Both exist because the driver's only notification mechanism is a queue that something has to block on. In a wrapper layer over IDF peripherals, UART then needs a thread hop that nothing else does.
Describe the solution you'd like.
uart_register_event_callbacks(), shaped like the ones the other drivers already have:Following the same conventions:
boolfor "a higher-priority task was woken", astwai_nodeandi2c_masterdo;uart_driver_install(..., queue = NULL, ...);uart_event_t, so existing code ports field for field.A handle-based UART driver, in the style of the other
esp_driver_*components, would be the obvious home for this, but the rewrite is not what we are asking for. Adding callback registration to the current driver would be enough to drop the mandatory task, and the ISR already has the hook points (see below).As an alternative worth exploring, going a step further than the callbacks listed above: with callbacks in place, neither driver-side buffer is strictly necessary.
Every callback above is a notification about a buffer:
on_rx_datareports bytes that landed in the RX ring buffer,on_tx_donereports space returned in the TX one. If they carried the data instead, the buffers would have little left to do.On RX that is a read from within the callback — the caller supplies a buffer, the driver fills it — in the shape of
twai_node_receive_from_isr(). The ISR already stages the FIFO contents inp_uart->rx_data_buf, and the volume per callback is bounded byUART_HW_FIFO_LEN, so the caller has a known worst case. On TX it is the reverse: a submit-and-notify call liketwai_node_transmit()oruhci_transmit(), the caller keeping the buffer valid untilon_tx_done.uart_write_bytes()blocks in both configurations today, so there is no way to start a transmission and return.That would make
rx_buffer_size == 0andtx_buffer_size == 0a supported combination rather than an error, withuart_read_bytes(),uart_flush_input()anduart_get_buffered_data_len()returningESP_ERR_NOT_SUPPORTED, and pattern detection unavailable, since its positions are offsets into buffered data.Describe alternatives you've considered.
1. The event queue and a task. What we do today. A queue set keeps that to one task rather than one per port, but the task and its stack remain, as does a copy of every message that leaves it. On a high-rate port that copy dominates an otherwise allocation-free path.
2.
uart_set_select_notif_callback()(driver/uart_select.h). This is the hook we want, and it already fires from the ISR on everyUART_DATAevent, not only while aselect()is in flight. Inesp_driver_uart/src/uart.c:with
UART_SELECT_ERROR_NOTIFon FIFO overflow, and aBaseType_t *task_wokenout-parameter. Functionally it is a working ISR callback.We would rather not build on it: it is documented only as
select()plumbing, there is one slot per port, and taking it would silently break any VFSselect()on that UART. The hook already exists and works; what is missing is a supported way to use it.3. UHCI (
uhci_register_event_callbacks). A modern callback API with DMA behind it, and presumably the intended high-throughput path.SOC_UHCI_SUPPORTEDis defined only for ESP32-S3 and ESP32-P4 though, andSOC_UHCI_NUMis 1, so one UART per chip at most and nothing on ESP32 or the C-series. That rules it out as a general answer.4. VFS +
select(). Pulls in the VFS layer and still needs a task blocked inselect(). The task moves; it does not go away.Additional context.
uart_driver_install(). Opened May 2023, still open with no reply, so we are filing something more specific rather than adding to it.uart_set_select_notif_callback()is usable by application code, or whether it is private in all but placement. As it stands, anyone wrapping UART asynchronously has to spend a task on it.