Standard I/O and Console Output
ESP-IDF provides C standard I/O facilities, such as stdin, stdout, and stderr streams, as well as C standard library functions such as printf() which operate on these streams.
As common in POSIX systems, these streams are buffering wrappers around file descriptors:
stdinis a buffered stream for reading input from the user, wrapping file descriptorSTDIN_FILENO(0).stdoutis a buffered stream for writing output to the user, wrappingSTDOUT_FILENO(1).stderris a buffered stream for writing error messages to the user, wrappingSTDERR_FILENO(2).
In ESP-IDF, there is no practical distinction between stdout and stderr, as both streams are sent to the same physical interface. Most applications will use only stdout. For example, ESP-IDF logging functions always write to stdout regardless of the log level.
The underlying stdin, stdout, and stderr file descriptors are implemented based on VFS drivers.
On ESP32-H4, ESP-IDF provides implementations of VFS drivers for I/O over:
UART
USB Serial/JTAG
"Null" (no output)
Standard I/O is not limited to these options, though. See below on enabling custom destinations for standard I/O.
Note
The console path is separate from an application UART that you open with the UART driver on other pins. If a UART peripheral provides console output, a USB-to-UART bridge chip is required. Many development boards already include this bridge chip onboard for console communication between the PC and the default UART0. If a USB or USB Serial/JTAG peripheral provides console output, no adapter chip is needed. The USB connector on the development board connects directly to the chip's USB D+/D- pins. Traffic on other UART ports does not appear in idf.py monitor unless a given port is configured as the console. For details, see Establish Serial Connection with ESP32-H4.
Configuration
Built-in implementations of standard I/O can be selected using several Kconfig options:
— Enables UART with default options (pin numbers, baud rate) for standard I/O.
— Enables UART for standard I/O, with TX/RX pin numbers and baud rate configurable via Kconfig.
— Enables USB Serial/JTAG for standard I/O. See USB Serial/JTAG Controller Console for details about hardware connections required.
— Disables standard I/O. If this option is selected,
stdin,stdout, andstderrwill be mapped to/dev/nulland won't produce any output or generate any input.
Enabling one of these options will cause the corresponding VFS driver to be built into the application and used to open stdin, stdout, and stderr streams. Data written to stdout and stderr will be sent over the selected interface, and input from the selected interface will be available on stdin.
Secondary output
ESP-IDF has built-in support for sending standard output to a secondary destination. This option makes the application output visible on two interfaces at once, for example on both UART and USB Serial/JTAG.
Note that secondary console is output-only:
data written to
stdoutandstderrby the application will be sent to both primary and secondary consoles
stdinwill only contain data sent by the host to the primary console.
The following secondary console options are available:
The secondary console is the compile-time equivalent of a runtime auxiliary sink described below. It is a fixed, output-only destination.
Standard Streams and FreeRTOS Tasks
ESP-IDF provides two different implementations of standard I/O streams based on the selected LibC implementation defined by . The behavior of stdin, stdout, and stderr streams differs between these implementations, particularly regarding how they are shared across FreeRTOS tasks.
Common to both implementations, each stream (stdin, stdout, stderr) has a mutex associated with it to protect the stream from concurrent access by multiple tasks. For example, if two tasks are writing to stdout at the same time, the mutex ensures that the outputs from each task are not mixed together.
Newlib
In ESP-IDF, to save RAM, FILE objects for stdin, stdout, and stderr are shared between all FreeRTOS tasks, but the pointers to these objects are unique for every task. This means that:
It is possible to change
stdin,stdout, andstderrfor any given task without affecting other tasks, e.g., by doingstdin = fopen("/dev/uart/1", "r").To change the default
stdin,stdout,stderrstreams for new tasks, modify_GLOBAL_REENT->_stdin(_stdout,_stderr) before creating the task.Closing default
stdin,stdout, orstderrusingfclosecloses theFILEstream object, which will affect all other tasks.
Picolibc
According to the POSIX standard, all default stdin, stdout, and stderr streams are global and shared between all FreeRTOS tasks. This means that:
Modifying
stdin,stdout, orstderrwill affect all other tasks. It is not possible to change standard I/O streams for specific tasks.If a thread-local stream is needed, it should be implemented in the application code by opening a file stream and using it within tasks, e.g.,
fscanf(),fprintf(), etc.
Blocking and non-blocking I/O
UART
By default, UART VFS uses simplified functions for reading from and writing to UART. Writes busy-wait until all data is put into UART FIFO, and reads are non-blocking, returning only the data present in the FIFO. Due to this non-blocking read behavior, higher level C library calls, such as fscanf("%d\n", &var);, might not have desired results.
Applications which use the UART driver can instruct VFS to use the driver's interrupt driven, blocking read and write functions instead. This can be done using a call to the uart_vfs_dev_use_driver() function. It is also possible to revert to the basic non-blocking functions using a call to uart_vfs_dev_use_nonblocking().
When the interrupt-driven driver is installed, it is also possible to enable/disable non-blocking behavior using fcntl function with O_NONBLOCK flag.
USB Serial/JTAG
Similar to UART, the VFS driver for USB Serial/JTAG defaults to a simplified implementation: writes are blocking (busy-wait until all the data has been sent) and reads are non-blocking, returning only the data present in the FIFO. This behavior can be changed to use the interrupt-driven, blocking read and write functions of the USB Serial/JTAG driver by calling usb_serial_jtag_vfs_use_nonblocking(). Note that the USB Serial/JTAG driver has to be initialized using usb_serial_jtag_driver_install() beforehand. It is also possible to revert to the basic non-blocking functions using a call to usb_serial_jtag_vfs_use_nonblocking().
When the interrupt-driven driver is installed, it is also possible to enable/disable non-blocking behavior using fcntl function with O_NONBLOCK flag.
Newline conversion
VFS drivers provide an optional newline conversion feature for input and output. Internally, most applications send and receive lines terminated by the LF (\n) character. Different terminal programs may require different line termination, such as CR or CRLF.
Applications can configure this behavior globally using the following Kconfig options:
- For output
- For input
It is also possible to configure line ending conversion for the specific VFS driver:
For UART:
uart_vfs_dev_port_set_rx_line_endings()anduart_vfs_dev_port_set_tx_line_endings()For USB Serial/JTAG:
usb_serial_jtag_vfs_set_rx_line_endings()andusb_serial_jtag_vfs_set_tx_line_endings()
Buffering
By default, standard I/O streams are line buffered. This means that data written to the stream is not sent to the underlying device until a newline character is written, or the buffer is full. This means, for example, that if you call printf("Hello"), the text will not be sent to the UART until you call printf("\n") or the stream buffer fills up due to other prints.
This behavior can be changed using the setvbuf() function. For example, to disable buffering for stdout:
setvbuf(stdout, NULL, _IONBF, 0);
You can also use setvbuf() to increase the buffer size, or switch to fully buffered mode.
Custom channels for standard I/O
To send application output to a custom channel (for example, a WebSocket connection), it is possible to create a custom VFS driver. See the VFS documentation for details. The VFS driver has to implement at least the following functions:
open()andclose()
write()
read()— only if the custom channel is also used for input
fstat()— recommended, to provide correct buffering behavior for the I/O streams
fcntl()— only if non-blocking I/O has to be supported
Once you have created a custom VFS driver, use esp_vfs_register_fs() to register it with VFS. Then, use fopen() to redirect stdout and stderr to the custom channel. For example:
FILE *f = fopen("/dev/mychannel", "w");
if (f == NULL) {
// handle the error here
}
stdout = f;
stderr = f;
Note that logging functions (ESP_LOGE(), etc.) write their output to stdout. Keep this in mind when using logging within the implementation of your custom VFS (or any components which it calls). For example, if the custom VFS driver's write() operation fails and uses ESP_LOGE() to log the error, this will cause the output to be sent to stdout, which would again call the custom VFS driver's write() operation. This would result in an infinite loop. It is recommended to keep track of this re-entry condition in the VFS driver's write() implementation, and return immediately if the write operation is still in progress.
Console I/O multiplexer
In addition to redirecting stdout and stderr manually (as shown above), ESP-IDF provides a console I/O multiplexer that lets an application register one or more custom VFS backends and switch between them at runtime, without reassigning the stdin/stdout/stderr streams itself.
The multiplexer is exposed on /dev/console and manages backends in two roles:
Primary — the active read and write backend. Both application input (
stdin) and output (stdout/stderr) go through it. Exactly one backend is the primary at any time. Useesp_stdio_set_primary_driver()to choose it; the call returns the previously active primary so the application can restore it later. The previous primary is not removed — it stays registered as a write-only auxiliary. The default console selected in Kconfig is registered automatically at boot and starts as the primary.Auxiliary — a write-only sink. Every byte written to
stdoutandstderris fanned out to all registered auxiliaries in addition to the primary. This is useful, for example, to mirror console output to a log file or a network connection while keeping the interactive console on the physical interface.
A backend is added with esp_stdio_register_driver() (initially as an auxiliary) and removed with esp_stdio_unregister_driver(). The backend currently acting as the primary cannot be unregistered; promote another one with esp_stdio_set_primary_driver() first.
The maximum number of backends that can be registered at once (the primary plus all auxiliaries, including the Kconfig default) is set by CONFIG_ESP_STDIO_MAX_VFS_ENTRIES.
The multiplexer API is declared in esp_stdio.h and is available when CONFIG_VFS_SUPPORT_IO is enabled. See the API reference below for the full behavior of each call.
Example
The system/esp_stdio/runtime_registration example shows the full flow: it registers a custom backend, makes it the active console, and later reverts to the previous one.
The same re-entrancy caveat described above for logging applies here: the backend's write() implementation must not trigger logging that would recursively write to the console.
API Reference
Header File
This header file can be included with:
#include "esp_stdio.h"
Functions
-
esp_err_t esp_stdio_register(void)
Register the default console VFS backend(s) selected in Kconfig.
Sets up the primary (and any Kconfig-selected secondary) console sink and mounts them under /dev/console. This function is automatically called from startup code to enable serial output; applications normally do not need to call it themselves.
- Returns:
ESP_OK on success, or an error code from the underlying VFS registration.
-
esp_err_t esp_stdio_register_driver(const esp_stdio_driver_config_t *config, esp_stdio_handle_t *out_handle)
Register a VFS driver as a write-only auxiliary sink in the console mux.
The driver is added to the multiplexer as an auxiliary: once /dev/console is open, every byte written to stdout/stderr is fanned out to it in addition to the primary. To use it as the read+write primary instead, promote it with esp_stdio_set_primary_driver().
The caller is responsible for initialising the driver before calling this function. The ops table, context, and path string must remain valid until esp_stdio_unregister_driver() is called.
- Parameters:
config -- Pointer to the driver configuration (must not be NULL; vfs_ops and path must not be NULL).
out_handle -- Receives the opaque handle on success (must not be NULL).
- Returns:
ESP_OK on success ESP_ERR_INVALID_ARG if any required pointer is NULL ESP_ERR_NO_MEM if the entry pool is exhausted
-
esp_err_t esp_stdio_unregister_driver(esp_stdio_handle_t handle)
Unregister a driver from the console mux.
The driver's backend fd is closed if it was open and its pool slot is released. A driver that is currently the active primary cannot be removed: promote a different driver with esp_stdio_set_primary_driver() first. After a successful call the handle is invalid.
Note
Unregistering the Kconfig-selected boot console (the handle returned by the very first esp_stdio_set_primary_driver() call) is a one-way transition: its pool slot is freed and the saved handle becomes stale, so it cannot be restored later, and the internal "user owns the
primary" state stays latched for the remaining lifetime. Prefer demoting the boot console (promote another driver) over unregistering it if you may need to restore it.
- Parameters:
handle -- A valid handle returned by esp_stdio_register_driver().
- Returns:
ESP_OK on success ESP_ERR_INVALID_ARG if handle is NULL or already unregistered ESP_ERR_INVALID_STATE if handle is the active primary
-
esp_err_t esp_stdio_set_primary_driver(esp_stdio_handle_t handle, esp_stdio_handle_t *out_old_primary)
Select the active read+write primary driver.
The given driver becomes the source for console reads and the target for console writes. The previously active primary is not removed — it stays registered as a write-only auxiliary and keeps receiving the fan-out.
The previous primary's handle is returned through
out_old_primaryso the caller can restore it later (a simple save/restore that composes across nested overrides). The very first call returns the handle of the Kconfig-selected default console.- Parameters:
handle -- Handle of the driver to make primary (must be a valid, registered handle returned by esp_stdio_register_driver()).
out_old_primary -- Receives the handle of the previously active primary, or NULL if the caller does not need it.
- Returns:
ESP_OK on success ESP_ERR_INVALID_ARG if handle is NULL or already unregistered
-
esp_err_t esp_stdio_install_io_driver(void)
Install and enable the stdio driver.
Initializes the selected console backend and registers it as the active input and output stream source. After calling this function, standard I/O functions such as
printfand terminal input will use the configured backend.Line endings are automatically configured for typical terminal use:
RX: CR (terminals send CR when Enter is pressed)
TX: CRLF (move cursor to beginning of next line on newline)
If a user has taken over the primary console with esp_stdio_set_primary_driver(), this function is a no-op (the user backend is assumed to be already initialised by the caller).
- Returns:
ESP_OK if the driver is successfully installed, or an appropriate error code otherwise.
-
void esp_stdio_uninstall_io_driver(void)
Uninstall the active stdio driver.
Restores driver state and detaches the console backend from the standard I/O streams. Call this when shutting down or when replacing the current console driver with a different one.
If a user has taken over the primary console with esp_stdio_set_primary_driver(), this function is a no-op.
Structures
-
struct esp_stdio_driver_config_t
Configuration for registering a console I/O driver in the mux.
The caller must keep the ops table, context, and path string valid until esp_stdio_unregister_driver() is called.
Public Members
-
const esp_vfs_fs_ops_t *vfs_ops
VFS operations table (must not be NULL)
-
void *vfs_ctx
Context pointer forwarded to every VFS callback
-
const char *path
Path used for open() inside the driver (e.g. "/0", "/")
-
const esp_vfs_fs_ops_t *vfs_ops
Macros
-
ESP_VFS_DEV_CONSOLE
Type Definitions
-
typedef struct esp_stdio_entry *esp_stdio_handle_t
Opaque handle representing a registered console I/O driver.