Merge branch 'feat/refactor_stdio_vfs_v6.1' into 'release/v6.1'

feat(storage/stdio): refactor stdio vfs (v6.1)

See merge request espressif/esp-idf!48957
This commit is contained in:
Jiang Jiang Jian
2026-07-03 17:47:31 +08:00
8 changed files with 420 additions and 108 deletions
@@ -588,7 +588,6 @@ static esp_err_t uart_end_select(void *end_select_args)
if (s_uart_select_count[i] == 0) {
uart_set_select_notif_callback(i, NULL);
}
break;
}
}
portEXIT_CRITICAL(uart_get_selectlock());
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -138,32 +138,38 @@ void esp_reent_cleanup(void)
* Initialize stdin, stdout, and stderr using static memory allocation.
* Creating them with fopen() would call malloc() internally.
*/
static char write_buf[BUFSIZ];
static char read_buf[BUFSIZ];
static char in_buf[BUFSIZ];
static char out_buf[BUFSIZ];
static char err_buf[BUFSIZ];
static struct __file_bufio __stdin = FDEV_SETUP_BUFIO(0, read_buf, BUFSIZ, read, write, lseek, close, __SRD, 0);
static struct __file_bufio __stdout = FDEV_SETUP_BUFIO(1, write_buf, BUFSIZ, read, write, lseek, close, __SWR, __BLBF);
static struct __file_bufio __stdin = FDEV_SETUP_BUFIO(0, in_buf, BUFSIZ, read, write, lseek, close, __SRD, 0);
static struct __file_bufio __stdout = FDEV_SETUP_BUFIO(1, out_buf, BUFSIZ, read, write, lseek, close, __SWR, __BLBF);
static struct __file_bufio __stderr = FDEV_SETUP_BUFIO(2, err_buf, BUFSIZ, read, write, lseek, close, __SWR, __BLBF);
FILE *stdin = &__stdin.xfile.cfile.file;
FILE *stdout = &__stdout.xfile.cfile.file;
FILE *stderr = &__stdout.xfile.cfile.file;
FILE *stderr = &__stderr.xfile.cfile.file;
#if CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY
__thread FILE* tls_stdin = &__stdin.xfile.cfile.file;
__thread FILE* tls_stdout = &__stdout.xfile.cfile.file;
__thread FILE* tls_stderr = &__stdout.xfile.cfile.file;
__thread FILE* tls_stderr = &__stderr.xfile.cfile.file;
#endif
#if CONFIG_VFS_SUPPORT_IO
void esp_libc_init_global_stdio(const char *stdio_dev)
{
int stdin_fd = open(stdio_dev, O_RDONLY);
assert(stdin_fd > 0);
assert(stdin_fd >= 0); // Should be 0
__stdin.ptr = (void *)(intptr_t)(stdin_fd);
int stdout_fd = open(stdio_dev, O_WRONLY);
assert(stdout_fd > 0);
assert(stdout_fd >= 0); // Should be 1
__stdout.ptr = (void *)(intptr_t)(stdout_fd);
int stderr_fd = open(stdio_dev, O_WRONLY);
assert(stderr_fd >= 0); // Should be 2
__stderr.ptr = (void *)(intptr_t)(stderr_fd);
}
#else /* CONFIG_VFS_SUPPORT_IO */
void esp_libc_init_global_stdio(void)
+4 -2
View File
@@ -7,7 +7,6 @@ if(non_os_build)
idf_component_register()
return()
endif()
set(srcs)
set(includes)
@@ -18,10 +17,13 @@ if(${target} STREQUAL "linux")
"linux/include")
else()
list(APPEND srcs "stdio_port.c"
"stdio_vfs.c"
"stdio_simple.c"
"stdio_syscalls_simple.c")
list(APPEND includes "include")
if(CONFIG_VFS_SUPPORT_IO)
list(APPEND srcs "stdio_vfs.c")
endif()
endif()
idf_component_register(SRCS ${srcs}
+8
View File
@@ -185,4 +185,12 @@ menu "ESP-STDIO"
If enabled, esp_rom_printf and ESP_EARLY_LOG output will also be sent over USB CDC.
Disabling this option saves about 1kB or RAM.
config ESP_STDIO_MAX_VFS_ENTRIES
int "Maximum number of registerable VFS sinks"
depends on VFS_SUPPORT_IO
range 1 10
default 2
help
Maximum number of filesystems that can be registered as sinks for console output.
endmenu
+259 -94
View File
@@ -5,10 +5,15 @@
*/
#include "sdkconfig.h"
#include <assert.h>
#include <stdbool.h>
#include <fcntl.h>
#include <string.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "esp_stdio.h"
#include "esp_vfs.h"
#include <sys/errno.h>
#if CONFIG_VFS_SUPPORT_IO
@@ -30,6 +35,8 @@
#include "esp_private/startup_internal.h"
#include "esp_private/nullfs.h"
#include <sys/lock.h>
#endif
#define STRINGIFY(s) STRINGIFY2(s)
#define STRINGIFY2(s) #s
@@ -42,135 +49,233 @@
* while the secondary is only used for output.
*/
#if CONFIG_VFS_SUPPORT_IO
typedef struct {
int fd_primary;
int fd_secondary;
} vfs_console_context_t;
const esp_vfs_fs_ops_t *ops; /* NULL = slot is free */
void *vfs_ctx;
const char *path;
int fd; /* -1 if not opened (used by auxiliaries) */
} mux_entry_t;
// Secondary register part.
#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG
const static esp_vfs_fs_ops_t *secondary_vfs = NULL;
#endif // Secondary part
/* Primary backend (all ops forwarded except open + write + fsync) */
static mux_entry_t s_primary = { .fd = -1 };
const static esp_vfs_fs_ops_t *primary_vfs = NULL;
/* Auxiliary sinks (write-only fan-out) */
#define STDIO_MAX_AUXILIARY (CONFIG_ESP_STDIO_MAX_VFS_ENTRIES - 1)
static mux_entry_t s_auxiliary[STDIO_MAX_AUXILIARY] = { [0 ... STDIO_MAX_AUXILIARY - 1] = { .fd = -1 } };
static int s_open_count = 0;
static _lock_t s_lock;
static vfs_console_context_t vfs_console = {0};
static const char *TAG = "esp_stdio";
static size_t s_open_count = 0;
#ifdef CONFIG_VFS_SUPPORT_TERMIOS
static const mux_entry_t *get_primary_termios_entry(int fd, const esp_vfs_termios_ops_t **out_ops)
{
const mux_entry_t *entry = &s_primary;
assert(entry);
const esp_vfs_termios_ops_t *termios = entry->ops->termios;
if (!termios) {
errno = ENOSYS;
return NULL;
}
if (out_ops) {
*out_ops = termios;
}
return entry;
}
#endif // CONFIG_VFS_SUPPORT_TERMIOS
static esp_err_t __attribute__((unused)) register_auxiliary(const esp_vfs_fs_ops_t *ops, void *ctx, const char *path)
{
if (!ops || !path || !ops->write_p) {
return ESP_ERR_INVALID_ARG;
}
_lock_acquire(&s_lock);
for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) {
if (s_auxiliary[i].ops == NULL) {
s_auxiliary[i] = (mux_entry_t) {
.ops = ops,
.vfs_ctx = ctx,
.path = path,
.fd = -1,
};
_lock_release(&s_lock);
return ESP_OK;
}
}
_lock_release(&s_lock);
ESP_EARLY_LOGE(TAG, "Too many auxiliary sinks");
return ESP_ERR_NO_MEM;
}
int console_open(__attribute__((unused)) void *ctx, const char * path, int flags, int mode)
{
if (s_open_count > 0) {
// Underlying fd is already open, so just increment the open count
// and return the same fd
s_open_count++;
return 0;
(void)path;
const mux_entry_t *entry = &s_primary;
if (!entry->ops || !entry->ops->open_p) {
errno = ENOSYS;
return -1;
}
// Primary port open
#if CONFIG_ESP_CONSOLE_UART
vfs_console.fd_primary = open("/dev/uart/"STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), flags, mode);
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
vfs_console.fd_primary = open("/dev/usbserjtag", flags, mode);
#elif CONFIG_ESP_CONSOLE_USB_CDC
vfs_console.fd_primary = open("/dev/cdcacm", flags, mode);
#else
vfs_console.fd_primary = open("/dev/null", flags, mode);
#endif
// Secondary port open
#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG
vfs_console.fd_secondary = open("/dev/secondary", flags, mode);
#endif
int local_fd = entry->ops->open_p(entry->vfs_ctx, entry->path, flags, mode);
if (local_fd < 0) {
return -1;
}
/* Lazily open auxiliaries on first console open */
_lock_acquire(&s_lock);
if (s_open_count == 0) {
for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) {
mux_entry_t *aux = &s_auxiliary[i];
if (aux->ops && aux->ops->open_p && aux->fd < 0) {
aux->fd = aux->ops->open_p(aux->vfs_ctx, aux->path, O_WRONLY, 0);
}
}
}
s_open_count++;
_lock_release(&s_lock);
return local_fd;
}
int console_close(__attribute__((unused)) void *ctx, int fd)
{
const mux_entry_t *entry = &s_primary;
if (!entry->ops || !entry->ops->close_p) {
errno = ENOSYS;
return -1;
}
int ret = entry->ops->close_p(entry->vfs_ctx, fd);
if (ret != 0) {
return ret;
}
/* Close auxiliaries when last console fd is closed */
_lock_acquire(&s_lock);
if (s_open_count > 0) {
s_open_count--;
}
if (s_open_count == 0) {
for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) {
mux_entry_t *aux = &s_auxiliary[i];
if (aux->ops && aux->ops->close_p && aux->fd >= 0) {
aux->ops->close_p(aux->vfs_ctx, aux->fd);
aux->fd = -1;
}
}
}
_lock_release(&s_lock);
return 0;
}
ssize_t console_write(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size)
{
write(vfs_console.fd_primary, data, size);
#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG
write(vfs_console.fd_secondary, data, size);
#endif
return size;
const mux_entry_t *primary = &s_primary;
ssize_t ret_val = primary->ops->write_p(primary->vfs_ctx, fd, data, size);
_lock_acquire(&s_lock);
for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) {
const mux_entry_t *entry = &s_auxiliary[i];
if (entry->ops && entry->ops->write_p && entry->fd >= 0) {
(void) entry->ops->write_p(entry->vfs_ctx, entry->fd, data, size);
}
}
_lock_release(&s_lock);
return ret_val;
}
int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat * st)
{
return fstat(vfs_console.fd_primary, st);
}
int console_close(__attribute__((unused)) void *ctx, int fd)
{
if (s_open_count == 0) {
errno = EBADF;
const mux_entry_t *entry = &s_primary;
if (!entry->ops->fstat_p) {
errno = ENOSYS;
return -1;
}
s_open_count--;
// We don't actually close the underlying fd until the open count reaches 0
if (s_open_count > 0) {
return 0;
}
// All function calls are to primary, except from write and close, which will be forwarded to both primary and secondary.
close(vfs_console.fd_primary);
#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG
close(vfs_console.fd_secondary);
#endif
return 0;
return entry->ops->fstat_p(entry->vfs_ctx, fd, st);
}
ssize_t console_read(__attribute__((unused)) void *ctx, int fd, void * dst, size_t size)
{
return read(vfs_console.fd_primary, dst, size);
const mux_entry_t *entry = &s_primary;
if (!entry->ops->read_p) {
errno = ENOSYS;
return -1;
}
return entry->ops->read_p(entry->vfs_ctx, fd, dst, size);
}
int console_fcntl(__attribute__((unused)) void *ctx, int fd, int cmd, int arg)
{
return fcntl(vfs_console.fd_primary, cmd, arg);
const mux_entry_t *entry = &s_primary;
if (!entry->ops->fcntl_p) {
errno = ENOSYS;
return -1;
}
return entry->ops->fcntl_p(entry->vfs_ctx, fd, cmd, arg);
}
int console_fsync(__attribute__((unused)) void *ctx, int fd)
{
const int ret_val = fsync(vfs_console.fd_primary);
#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG
(void)fsync(vfs_console.fd_secondary);
#endif
const mux_entry_t *primary = &s_primary;
if (!primary->ops->fsync_p) {
errno = ENOSYS;
return -1;
}
int ret_val = primary->ops->fsync_p(primary->vfs_ctx, fd);
_lock_acquire(&s_lock);
for (size_t i = 0; i < STDIO_MAX_AUXILIARY; i++) {
const mux_entry_t *entry = &s_auxiliary[i];
if (entry->ops && entry->ops->fsync_p && entry->fd >= 0) {
(void) entry->ops->fsync_p(entry->vfs_ctx, entry->fd);
}
}
_lock_release(&s_lock);
return ret_val;
}
#ifdef CONFIG_VFS_SUPPORT_DIR
int console_access(__attribute__((unused)) void *ctx, const char *path, int amode)
{
// currently only UART support DIR.
return access("/dev/uart/"STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), amode);
const mux_entry_t *entry = &s_primary;
assert(entry);
if (!entry->ops->dir || !entry->ops->dir->access_p) {
errno = ENOSYS;
return -1;
}
(void) path;
return entry->ops->dir->access_p(entry->vfs_ctx, entry->path, amode);
}
#endif // CONFIG_VFS_SUPPORT_DIR
#ifdef CONFIG_VFS_SUPPORT_SELECT
static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
esp_vfs_select_sem_t select_sem, void **end_select_args)
{
// start_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig
if (primary_vfs->select->start_select) {
return primary_vfs->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args);
const mux_entry_t *entry = &s_primary;
if (!entry->ops || !entry->ops->select || !entry->ops->select->start_select) {
return ESP_ERR_NOT_SUPPORTED;
}
return ESP_ERR_NOT_SUPPORTED;
return entry->ops->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args);
}
esp_err_t console_end_select(void *end_select_args)
static esp_err_t console_end_select(void *end_select_args)
{
// end_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig
if (primary_vfs->select->end_select) {
return primary_vfs->select->end_select(end_select_args);
const mux_entry_t *entry = &s_primary;
if (!entry->ops || !entry->ops->select || !entry->ops->select->end_select) {
return ESP_ERR_NOT_SUPPORTED;
}
return ESP_ERR_NOT_SUPPORTED;
return entry->ops->select->end_select(end_select_args);
}
#endif // CONFIG_VFS_SUPPORT_SELECT
@@ -179,22 +284,58 @@ esp_err_t console_end_select(void *end_select_args)
int console_tcsetattr(__attribute__((unused)) void *ctx, int fd, int optional_actions, const struct termios *p)
{
return tcsetattr(vfs_console.fd_primary, optional_actions, p);
const esp_vfs_termios_ops_t *termios = NULL;
const mux_entry_t *entry = get_primary_termios_entry(fd, &termios);
if (!entry) {
return -1;
}
if (!termios->tcsetattr_p) {
errno = ENOSYS;
return -1;
}
return entry->ops->termios->tcsetattr_p(entry->vfs_ctx, fd, optional_actions, p);
}
int console_tcgetattr(__attribute__((unused)) void *ctx, int fd, struct termios *p)
{
return tcgetattr(vfs_console.fd_primary, p);
const esp_vfs_termios_ops_t *termios = NULL;
const mux_entry_t *entry = get_primary_termios_entry(fd, &termios);
if (!entry) {
return -1;
}
if (!termios->tcgetattr_p) {
errno = ENOSYS;
return -1;
}
return entry->ops->termios->tcgetattr_p(entry->vfs_ctx, fd, p);
}
int console_tcdrain(__attribute__((unused)) void *ctx, int fd)
{
return tcdrain(vfs_console.fd_primary);
const esp_vfs_termios_ops_t *termios = NULL;
const mux_entry_t *entry = get_primary_termios_entry(fd, &termios);
if (!entry) {
return -1;
}
if (!termios->tcdrain_p) {
errno = ENOSYS;
return -1;
}
return entry->ops->termios->tcdrain_p(entry->vfs_ctx, fd);
}
int console_tcflush(__attribute__((unused)) void *ctx, int fd, int select)
{
return tcflush(vfs_console.fd_primary, select);
const esp_vfs_termios_ops_t *termios = NULL;
const mux_entry_t *entry = get_primary_termios_entry(fd, &termios);
if (!entry) {
return -1;
}
if (!termios->tcflush_p) {
errno = ENOSYS;
return -1;
}
return entry->ops->termios->tcflush_p(entry->vfs_ctx, fd, select);
}
#endif // CONFIG_VFS_SUPPORT_TERMIOS
@@ -242,31 +383,55 @@ static const esp_vfs_fs_ops_t s_vfs_console = {
#endif // CONFIG_VFS_SUPPORT_TERMIOS
};
static esp_err_t esp_vfs_dev_console_register(void)
{
return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL);
}
esp_err_t esp_stdio_register(void)
{
esp_err_t err = ESP_OK;
_lock_init(&s_lock);
// Primary vfs part.
#if CONFIG_ESP_CONSOLE_UART
primary_vfs = esp_vfs_uart_get_vfs();
s_primary = (mux_entry_t) {
.ops = esp_vfs_uart_get_vfs(),
.vfs_ctx = NULL,
.path = "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM),
.fd = -1,
};
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
primary_vfs = esp_vfs_usb_serial_jtag_get_vfs();
s_primary = (mux_entry_t) {
.ops = esp_vfs_usb_serial_jtag_get_vfs(),
.vfs_ctx = NULL,
.path = "/",
.fd = -1,
};
#elif CONFIG_ESP_CONSOLE_USB_CDC
primary_vfs = esp_vfs_cdcacm_get_vfs();
s_primary = (mux_entry_t) {
.ops = esp_vfs_cdcacm_get_vfs(),
.vfs_ctx = NULL,
.path = "/",
.fd = -1,
};
#else
primary_vfs = esp_vfs_null_get_vfs();
s_primary = (mux_entry_t) {
.ops = esp_vfs_null_get_vfs(),
.vfs_ctx = NULL,
.path = "/",
.fd = -1,
};
#endif
// Secondary vfs part.
if (!s_primary.ops) {
ESP_EARLY_LOGE(TAG, "No primary console backend available");
return ESP_FAIL;
}
// Auxiliary sinks (write-only fan-out).
#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG
secondary_vfs = esp_vfs_usb_serial_jtag_get_vfs();
esp_err_t err = register_auxiliary(esp_vfs_usb_serial_jtag_get_vfs(), NULL, "/");
if (err != ESP_OK) {
return err;
}
#endif
err = esp_vfs_dev_console_register();
return err;
return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL);
}
ESP_SYSTEM_INIT_FN(init_vfs_console, CORE, BIT(0), 119)
@@ -1,11 +1,12 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
#include "sdkconfig.h"
#include "esp_rom_serial_output.h"
@@ -18,6 +19,7 @@
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#if CONFIG_ESP_CONSOLE_NONE
/* Set up UART on UART_0 (console) to be able to
@@ -50,6 +52,9 @@ static void console_none_print(void)
#endif
#if CONFIG_VFS_SUPPORT_IO
#define ESP_STDIO_RUN_OPEN_CLOSE_CHECK 1
#if ESP_STDIO_RUN_OPEN_CLOSE_CHECK
static void console_open_close_check(void)
{
printf("Opening /dev/console\n");
@@ -65,6 +70,79 @@ static void console_open_close_check(void)
printf("This should be printed to stdout\n");
}
#endif
static void stdio_fd_mode_behavior_check(void)
{
printf("STDIO_TEST:MODE=FORWARDING\n");
int fd0 = open("/dev/console", O_RDWR);
assert(fd0 >= 0);
int fd1 = open("/dev/console", O_RDWR);
assert(fd1 >= 0);
assert(fd0 != fd1);
printf("STDIO_TEST:UNIQUE_FDS_OK\n");
const char *msg = "STDIO_TEST:WRITE_OK\n";
ssize_t wr = write(fd0, msg, strlen(msg));
assert(wr == (ssize_t) strlen(msg));
assert(fsync(fd0) == 0);
printf("STDIO_TEST:FSYNC_OK\n");
assert(close(fd0) == 0);
assert(close(fd1) == 0);
printf("STDIO_TEST:CLOSE_OK\n");
}
static void stdio_fd_init_check(void)
{
int in_fd = fileno(stdin);
int out_fd = fileno(stdout);
int err_fd = fileno(stderr);
assert(in_fd == 0);
assert(out_fd == 1);
assert(err_fd == 2);
printf("STDIO_TEST:FD_INIT:IN=%d,OUT=%d,ERR=%d\n", in_fd, out_fd, err_fd);
fprintf(stdout, "STDIO_TEST:STREAM:STDOUT\n");
fflush(stdout);
fprintf(stderr, "STDIO_TEST:STREAM:STDERR\n");
fflush(stderr);
}
#if CONFIG_VFS_SUPPORT_SELECT && CONFIG_ESP_CONSOLE_UART
static void stdio_select_fd_mapping_check(void)
{
bool driver_installed_here = false;
esp_err_t err = uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0);
if (err == ESP_OK) {
driver_installed_here = true;
} else {
assert(err == ESP_ERR_INVALID_STATE);
}
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fileno(stdout), &writefds);
FD_SET(fileno(stderr), &writefds);
struct timeval tv = {
.tv_sec = 0,
.tv_usec = 0,
};
errno = 0;
int ret = select(fileno(stderr) + 1, NULL, &writefds, NULL, &tv);
assert(ret >= 0);
if (driver_installed_here) {
assert(uart_driver_delete(CONFIG_ESP_CONSOLE_UART_NUM) == ESP_OK);
}
printf("STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK\n");
}
#endif
#endif // CONFIG_VFS_SUPPORT_IO
void app_main(void)
@@ -80,6 +158,11 @@ void app_main(void)
assert(fd > 2 && "Incorrect file descriptor returned, stdin, stdout, stderr were not correctly assigned");
close(fd);
stdio_fd_init_check();
#if CONFIG_VFS_SUPPORT_SELECT && CONFIG_ESP_CONSOLE_UART
stdio_select_fd_mapping_check();
#endif
#endif // CONFIG_VFS_SUPPORT_IO
#if CONFIG_ESP_CONSOLE_NONE
@@ -87,7 +170,12 @@ void app_main(void)
#endif // CONFIG_ESP_CONSOLE_NONE
#if CONFIG_VFS_SUPPORT_IO
#if ESP_STDIO_RUN_OPEN_CLOSE_CHECK
console_open_close_check();
#else
printf("STDIO_TEST:SKIP:OPEN_CLOSE_CHECK\n");
#endif
stdio_fd_mode_behavior_check();
#endif // CONFIG_VFS_SUPPORT_IO
}
@@ -1,5 +1,9 @@
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
#
# HW runs: use --embedded-services esp,idf (not idf alone) so serial is attached.
# See README.md in this directory for QEMU vs hardware commands and common failures.
#
import pexpect
import pytest
from pytest_embedded import Dut
@@ -7,6 +11,11 @@ from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
def _expect_app_main_banner(dut: Dut) -> None:
"""Sync to the latest app boot (avoids matching a stale 'Hello World' left in the serial buffer)."""
dut.expect('Calling app_main()')
def not_expect(dut: Dut, output_regex: str) -> None:
try:
dut.expect(output_regex, timeout=5)
@@ -34,7 +43,11 @@ def test_esp_system_stdio_no_output_uart(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_system_stdio_only_serial_jtag(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:STREAM:STDOUT')
dut.expect('STDIO_TEST:STREAM:STDERR')
dut.expect('Opening /dev/console')
dut.expect('This should be printed to stdout')
dut.expect('Closing /dev/console')
@@ -50,6 +63,7 @@ def test_esp_system_stdio_only_serial_jtag(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_system_stdio_only_serial_jtag_no_vfs(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
@@ -62,6 +76,7 @@ def test_esp_system_stdio_only_serial_jtag_no_vfs(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='lack of runner # TODO: IDFCI-10703')
def test_esp_system_stdio_secondary_serial_jtag(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
@@ -71,8 +86,37 @@ def test_esp_system_stdio_secondary_serial_jtag(dut: Dut) -> None:
@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='p4 rev3 migration')
def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None:
dut.expect('2nd stage bootloader')
_expect_app_main_banner(dut)
dut.expect('Hello World')
# Order matches test_app_main.c: stdio_fd_init_check() runs before console_open_close_check()
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:STREAM:STDOUT')
dut.expect('STDIO_TEST:STREAM:STDERR')
dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK')
dut.expect('Opening /dev/console')
dut.expect('This should be printed to stdout')
dut.expect('Closing /dev/console')
dut.expect('This should be printed to stdout')
dut.expect('STDIO_TEST:MODE=FORWARDING')
dut.expect('STDIO_TEST:UNIQUE_FDS_OK')
dut.expect('STDIO_TEST:WRITE_OK')
dut.expect('STDIO_TEST:FSYNC_OK')
dut.expect('STDIO_TEST:CLOSE_OK')
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'config, embedded_services',
[('simple', 'idf,qemu')],
)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None:
dut.expect('Hello World')
dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2')
dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK')
dut.expect('STDIO_TEST:MODE=FORWARDING')
dut.expect('STDIO_TEST:UNIQUE_FDS_OK')
dut.expect('STDIO_TEST:WRITE_OK')
dut.expect('STDIO_TEST:FSYNC_OK')
dut.expect('STDIO_TEST:CLOSE_OK')
@@ -9,7 +9,7 @@
#include "esp_heap_caps.h"
// Some resources are lazy allocated in mspi bus test, the threshold is left for that case
#define TEST_MEMORY_LEAK_THRESHOLD (-600)
#define TEST_MEMORY_LEAK_THRESHOLD (-650)
static size_t before_free_8bit;
static size_t before_free_32bit;