From eb8ef76ceef626a0a8dcfdff11f20e9fd8faa715 Mon Sep 17 00:00:00 2001 From: Lingao Meng Date: Fri, 24 Jul 2026 14:13:06 +0800 Subject: [PATCH] arch/sim: Add PTY mode for simulated UART The simulated UART driver currently opens the host path configured by CONFIG_SIM_UARTx_NAME directly. This requires the host-side serial endpoint to exist before NuttX opens the UART, so users and CI jobs need an external setup step such as socat to create a PTY pair. It also means the host endpoint path is effectively a build-time choice: changing the host device path requires changing configuration and rebuilding, which is inconvenient for tests that allocate a fresh PTY path on each run. Add CONFIG_SIM_UART_PTY for Linux sim builds. When enabled, non-console simulated UART ports allocate a host pseudoterminal from /dev/ptmx, put the host master side in raw mode, and print the host slave path when the NuttX UART is opened. The NuttX-side device name remains CONFIG_SIM_UARTx_NAME, for example /dev/ttySIM0, so applications continue to use the normal NuttX serial API. Keep the option disabled by default so existing configurations still open the configured host path directly. Console UART handling is also left on the existing host-open path. Also make host_uart_checkin() and host_uart_checkout() check the actual POLLIN/POLLOUT bits returned by poll(). This avoids treating error-only or unrelated poll events as readable or writable serial readiness. The main benefit is simpler and more deterministic simulator integration: a simulated UART can expose a real host-visible /dev/pts/N endpoint by itself, without pre-creating a matching host device and without rebuilding NuttX when the host PTY path changes. This is useful for host-side test scripts and external protocol tools while keeping application code on the standard NuttX UART interface. Companion apps-side test branch: https://github.com/LingaoM/nuttx-apps/tree/sim_uart_tester Testing: Host: Ubuntu 22.04 x86_64 Board/config: sim:nsh Apps test code: https://github.com/LingaoM/nuttx-apps/tree/sim_uart_tester Common test configuration: CONFIG_NSH_BUILTIN_APPS=y CONFIG_EXAMPLES_HELLO=y CONFIG_SIM_UART_NUMBER=1 CONFIG_SIM_UART0_NAME="/dev/ttySIM0" CONFIG_SIM_UART_PTY=y DMA-mode build and test: 1. Configure sim:nsh with the companion apps tree: ./tools/configure.sh -a ../nuttx-apps sim:nsh 2. Enable the common test configuration above and keep DMA enabled: CONFIG_SIM_UART_DMA=y CONFIG_SERIAL_TXDMA=y CONFIG_SERIAL_RXDMA=y 3. Build: make clean make -j16 4. Start NuttX: ./nuttx 5. In NSH, run the hello test app: nsh> hello /dev/ttySIM0 connected to pseudotty: /dev/pts/73 6. In another terminal, run the host-side tester from the companion apps branch with the printed PTY path: cd ../nuttx-apps ./examples/hello/test_sim_uart_pty.py /dev/pts/73 7. The host-side tester sends 32768 bytes from the host to NuttX and receives 49152 bytes from NuttX to the host. The payload includes non-text binary bytes. Both sides validate deterministic payload contents and checksums, then exchange an ACK. 8. Observed host-side output: HOST_OPEN: /dev/pts/73 HOST_TX: 32768 bytes checksum=0x1f9989f4 HOST_RX: 49152 bytes checksum=0x06a45c69 HOST_TX: ACK TEST PASSED 9. Observed NuttX output: sim_uart_pty_test: binary RX 32768 TX 49152 passed Non-DMA build and test: 1. Disable DMA for the same sim:nsh configuration: # CONFIG_SIM_UART_DMA is not set # CONFIG_SERIAL_TXDMA is not set # CONFIG_SERIAL_RXDMA is not set 2. Refresh the configuration and rebuild. On this host, the installed Python olddefconfig shim is broken, so I refreshed Kconfig directly with kconfig-conf and the same environment that the NuttX Makefile passes to Kconfig: APPSDIR=/mnt/ssd/code/code/nuttx-apps \ APPSBINDIR=/mnt/ssd/code/code/nuttx-apps \ BINDIR=/mnt/ssd/code/code/nuttx \ EXTERNALDIR=/mnt/ssd/code/code/nuttx/dummy \ kconfig-conf --olddefconfig Kconfig make clean make -j16 3. Repeated the same runtime steps as the DMA test: ./nuttx nsh> hello cd ../nuttx-apps ./examples/hello/test_sim_uart_pty.py /dev/pts/73 4. Observed the same successful binary transfer result: HOST_TX: 32768 bytes checksum=0x1f9989f4 HOST_RX: 49152 bytes checksum=0x06a45c69 HOST_TX: ACK TEST PASSED sim_uart_pty_test: binary RX 32768 TX 49152 passed After the non-DMA test, I restored the default DMA configuration, rebuilt, and reran the same binary PTY test successfully so the final local build state was back on CONFIG_SIM_UART_DMA=y. Assisted-by: Claude:Claude-Fable-5 Signed-off-by: Lingao Meng --- .../platforms/sim/sim/boards/sim/index.rst | 15 ++++- arch/sim/Kconfig | 60 +++++++++++++++---- arch/sim/src/sim/posix/sim_hostuart.c | 57 +++++++++++++++++- arch/sim/src/sim/sim_internal.h | 1 + arch/sim/src/sim/sim_uart.c | 17 ++++-- arch/sim/src/sim/win/sim_hostuart.c | 10 ++++ 6 files changed, 141 insertions(+), 19 deletions(-) diff --git a/Documentation/platforms/sim/sim/boards/sim/index.rst b/Documentation/platforms/sim/sim/boards/sim/index.rst index 3f080122eb799..01c82908f9e1a 100644 --- a/Documentation/platforms/sim/sim/boards/sim/index.rst +++ b/Documentation/platforms/sim/sim/boards/sim/index.rst @@ -2173,10 +2173,23 @@ nxscope Configuration demonstrating NxScope stream over simulated UART interface. -The simulated UART must be created on host before running NuttX:: +If ``CONFIG_SIM_UART_PTY`` is disabled, the simulated UART peer must be +created on the host before running NuttX:: socat PTY,link=/dev/ttySIM0 PTY,link=/dev/ttyNX0 +In that mode ``CONFIG_SIM_UART0_NAME`` is both the NuttX device name and +the host path that the sim UART backend opens. This works when the host +path is stable, but it is inconvenient for automated tests and generated +PTYs: the peer device must exist before the UART is opened, and changing +the host path requires changing the NuttX configuration. + +If ``CONFIG_SIM_UART_PTY`` is enabled, NuttX creates the host +pseudoterminal when the simulated UART is opened and prints the host +PTY slave path. The configured ``SIM_UARTx_NAME`` remains the NuttX +device name, while the host PTY path is allocated at runtime and can be +passed to the external simulator or test program. + See :doc:`/applications/examples/nxscope/index` and :doc:`/applications/logging/nxscope/index` for more details. diff --git a/arch/sim/Kconfig b/arch/sim/Kconfig index 7ad1e801c28be..b1e78e1b60e22 100644 --- a/arch/sim/Kconfig +++ b/arch/sim/Kconfig @@ -774,6 +774,11 @@ config SIM_UART_NUMBER Anything sent to the one of these ports will be relayed automatically to the other, and vice-versa. + If SIM_UART_PTY is enabled, NuttX creates the simulated + host pseudoterminal itself. In that mode SIM_UARTx_NAME is + only the NuttX device name, and the host pseudoterminal path + is printed when the port is opened. + config SIM_UART_BUFFER_SIZE int "UART buffer size" default 256 @@ -784,16 +789,41 @@ config SIM_UART_BUFFER_SIZE Note that all ports will have the same buffer size. +config SIM_UART_PTY + bool "Create host pseudoterminals for simulated UART ports" + default n + depends on HOST_LINUX && SIM_UART_NUMBER >= 1 + ---help--- + Open a new host pseudoterminal for each simulated UART + instead of opening an existing host serial device. The + UART remains registered in NuttX using SIM_UARTx_NAME, and + the host PTY slave path is printed when the port is opened. + + The existing behavior requires the host endpoint named by + SIM_UARTx_NAME to exist before the simulated UART is opened. + This makes automated tests awkward when the peer endpoint is + a generated PTY with a host-dependent path, because the host + path must be prepared up front and encoded in the NuttX + configuration. With this option enabled, the sim UART + backend owns the host PTY allocation and reports the actual + slave path at runtime, while keeping the NuttX device name + stable for applications. + + If disabled, SIM UART keeps the existing behavior and opens + the host device path specified by SIM_UARTx_NAME directly. + config SIM_UART0_NAME string "UART port 0 name" default "/dev/ttySIM0" depends on SIM_UART_NUMBER >= 1 ---help--- This is the name of the simulated UART port. - The port will be mounted in NuttX under this name. + The port will be registered in NuttX under this name. - A UART port must also exist on the host system - with the exact same name specified here. + If SIM_UART_PTY is disabled, a UART port must also exist on + the host system with the exact same name specified here. If + SIM_UART_PTY is enabled, the host pseudoterminal is allocated + automatically and printed when this port is opened. config SIM_UART1_NAME string "UART port 1 name" @@ -801,10 +831,12 @@ config SIM_UART1_NAME depends on SIM_UART_NUMBER >= 2 ---help--- This is the name of the simulated UART port. - The port will be mounted in NuttX under this name. + The port will be registered in NuttX under this name. - A UART port must also exist on the host system - with the exact same name specified here. + If SIM_UART_PTY is disabled, a UART port must also exist on + the host system with the exact same name specified here. If + SIM_UART_PTY is enabled, the host pseudoterminal is allocated + automatically and printed when this port is opened. config SIM_UART2_NAME string "UART port 2 name" @@ -812,10 +844,12 @@ config SIM_UART2_NAME depends on SIM_UART_NUMBER >= 3 ---help--- This is the name of the simulated UART port. - The port will be mounted in NuttX under this name. + The port will be registered in NuttX under this name. - A UART port must also exist on the host system - with the exact same name specified here. + If SIM_UART_PTY is disabled, a UART port must also exist on + the host system with the exact same name specified here. If + SIM_UART_PTY is enabled, the host pseudoterminal is allocated + automatically and printed when this port is opened. config SIM_UART3_NAME string "UART port 3 name" @@ -823,10 +857,12 @@ config SIM_UART3_NAME depends on SIM_UART_NUMBER >= 4 ---help--- This is the name of the simulated UART port. - The port will be mounted in NuttX under this name. + The port will be registered in NuttX under this name. - A UART port must also exist on the host system - with the exact same name specified here. + If SIM_UART_PTY is disabled, a UART port must also exist on + the host system with the exact same name specified here. If + SIM_UART_PTY is enabled, the host pseudoterminal is allocated + automatically and printed when this port is opened. config SIM_RAM_UART bool "SIM RAM UART Device" diff --git a/arch/sim/src/sim/posix/sim_hostuart.c b/arch/sim/src/sim/posix/sim_hostuart.c index facc21e467e0e..04254bc882848 100644 --- a/arch/sim/src/sim/posix/sim_hostuart.c +++ b/arch/sim/src/sim/posix/sim_hostuart.c @@ -24,6 +24,7 @@ * Included Files ****************************************************************************/ +#include #include #include #include @@ -130,6 +131,58 @@ int host_uart_open(const char *pathname) return fd; } +/**************************************************************************** + * Name: host_uart_openpty + ****************************************************************************/ + +int host_uart_openpty(const char *name) +{ +#ifdef CONFIG_HOST_LINUX + unsigned int ptyno; + int lock = 0; + int oflags; + int ret; + int fd; + + oflags = O_RDWR | O_NOCTTY | O_NONBLOCK; +#ifdef O_CLOEXEC + oflags |= O_CLOEXEC; +#endif + fd = open("/dev/ptmx", oflags); + if (fd < 0) + { + return -errno; + } + + ret = ioctl(fd, TIOCGPTN, &ptyno); + if (ret < 0) + { + ret = -errno; + goto errout; + } + + ret = ioctl(fd, TIOCSPTLCK, &lock); + if (ret < 0) + { + ret = -errno; + goto errout; + } + + setrawmode(fd); + + printf("%s connected to pseudotty: /dev/pts/%u\n", name, ptyno); + + return fd; + +errout: + close(fd); + return ret; +#else + (void)name; + return -ENOSYS; +#endif +} + /**************************************************************************** * Name: host_uart_close ****************************************************************************/ @@ -229,7 +282,7 @@ bool host_uart_checkin(int fd) pfd.fd = fd; pfd.events = POLLIN; - return poll(&pfd, 1, 0) == 1; + return poll(&pfd, 1, 0) == 1 && (pfd.revents & POLLIN) != 0; } /**************************************************************************** @@ -242,7 +295,7 @@ bool host_uart_checkout(int fd) pfd.fd = fd; pfd.events = POLLOUT; - return poll(&pfd, 1, 0) == 1; + return poll(&pfd, 1, 0) == 1 && (pfd.revents & POLLOUT) != 0; } /**************************************************************************** diff --git a/arch/sim/src/sim/sim_internal.h b/arch/sim/src/sim/sim_internal.h index 3ca70b65e9421..82349ada14c29 100644 --- a/arch/sim/src/sim/sim_internal.h +++ b/arch/sim/src/sim/sim_internal.h @@ -284,6 +284,7 @@ void sim_uartinit(void); void host_uart_start(void); int host_uart_open(const char *pathname); +int host_uart_openpty(const char *name); void host_uart_close(int fd); int host_uart_puts(int fd, const char *buf, size_t size); int host_uart_gets(int fd, char *buf, size_t size); diff --git a/arch/sim/src/sim/sim_uart.c b/arch/sim/src/sim/sim_uart.c index 4f8a3b6de0b80..98e5ed53c65ff 100644 --- a/arch/sim/src/sim/sim_uart.c +++ b/arch/sim/src/sim/sim_uart.c @@ -307,9 +307,8 @@ static void uart_nputs(int fd, const char *buf, size_t size) * Name: tty_setup * * Description: - * Configure the UART baud, bits, parity, fifos, etc. This - * method is called the first time that the serial port is - * opened. + * Open and configure the host endpoint for the simulated UART. + * This method is called the first time that the serial port is opened. * ****************************************************************************/ @@ -317,7 +316,17 @@ static int tty_setup(struct uart_dev_s *dev) { struct tty_priv_s *priv = dev->priv; - priv->fd = host_uart_open(priv->path); +#ifdef CONFIG_SIM_UART_PTY + if (!dev->isconsole) + { + priv->fd = host_uart_openpty(priv->path); + } + else +#endif + { + priv->fd = host_uart_open(priv->path); + } + return priv->fd; } diff --git a/arch/sim/src/sim/win/sim_hostuart.c b/arch/sim/src/sim/win/sim_hostuart.c index 331daa7ad8ee2..220fa997f2e30 100644 --- a/arch/sim/src/sim/win/sim_hostuart.c +++ b/arch/sim/src/sim/win/sim_hostuart.c @@ -74,6 +74,16 @@ int host_uart_open(const char *pathname) return -ENOSYS; } +/**************************************************************************** + * Name: host_uart_openpty + ****************************************************************************/ + +int host_uart_openpty(const char *name) +{ + (void)name; + return -ENOSYS; +} + /**************************************************************************** * Name: host_uart_close ****************************************************************************/