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:
stdin
is a buffered stream for reading input from the user, wrapping file descriptorSTDIN_FILENO
(0).stdout
is a buffered stream for writing output to the user, wrappingSTDOUT_FILENO
(1).stderr
is 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-C5, 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.
Configuration
Built-in implementations of standard I/O can be selected using several Kconfig options:
CONFIG_ESP_CONSOLE_UART_DEFAULT — Enables UART with default options (pin numbers, baud rate) for standard I/O.
CONFIG_ESP_CONSOLE_UART_CUSTOM — Enables UART for standard I/O, with TX/RX pin numbers and baud rate configurable via Kconfig.
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG — Enables USB Serial/JTAG for standard I/O. See USB Serial/JTAG Controller Console for details about hardware connections required.
CONFIG_ESP_CONSOLE_NONE — Disables standard I/O. If this option is selected,
stdin
,stdout
, andstderr
will be mapped to/dev/null
and won't produce any output or generate any input.
Enabling one of these option 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
stdout
andstderr
by the application will be sent to both primary and secondary consoles
stdin
will only contain data sent by the host to the primary console.
The following secondary console options are available:
Standard Streams and FreeRTOS Tasks
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
, andstderr
for any given task without affecting other tasks, e.g., by doingstdin = fopen("/dev/uart/1", "r")
.To change the default
stdin
,stdout
,stderr
streams for new tasks, modify_GLOBAL_REENT->_stdin
(_stdout
,_stderr
) before creating the task.Closing default
stdin
,stdout
, orstderr
usingfclose
closes theFILE
stream object, which will affect all other tasks.
Each stream (stdin
, stdout
, stderr
) has a mutex associated with it. This mutex is used 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 will ensure that the outputs from each task are not mixed together.
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 USB Serial/JTAG driver using a call to the usb_serial_jtag_vfs_use_nonblocking()
function. 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:
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.