From 8b76af34737aea6628bbedd21c77e70d981be3ab Mon Sep 17 00:00:00 2001 From: Shengwen Cheng Date: Mon, 7 Sep 2026 22:41:07 +0800 Subject: [PATCH 1/4] linux.config: Enable virtio-console --- configs/linux.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/linux.config b/configs/linux.config index 0a32ab3c..5e1ca2dc 100644 --- a/configs/linux.config +++ b/configs/linux.config @@ -834,7 +834,7 @@ CONFIG_SERIAL_CORE_CONSOLE=y # CONFIG_NULL_TTY is not set # CONFIG_SERIAL_DEV_BUS is not set # CONFIG_TTY_PRINTK is not set -# CONFIG_VIRTIO_CONSOLE is not set +CONFIG_VIRTIO_CONSOLE=y # CONFIG_IPMI_HANDLER is not set CONFIG_HW_RANDOM=y CONFIG_HW_RANDOM_VIRTIO=y From 5b126246b66c371076d55fc1faa257c3f56ebb73 Mon Sep 17 00:00:00 2001 From: Shengwen Cheng Date: Mon, 7 Sep 2026 23:19:21 +0800 Subject: [PATCH 2/4] Refactor host console handling --- Makefile | 1 + console.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ console.h | 7 +++++++ device.h | 1 - main.c | 6 ++++-- uart.c | 50 ++-------------------------------------------- 6 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 console.c create mode 100644 console.h diff --git a/Makefile b/Makefile index 5c4bd4b3..148bef37 100644 --- a/Makefile +++ b/Makefile @@ -227,6 +227,7 @@ OBJS := \ ram.o \ utils.o \ plic.o \ + console.o \ uart.o \ main.o \ aclint.o \ diff --git a/console.c b/console.c new file mode 100644 index 00000000..275a89da --- /dev/null +++ b/console.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +#include "console.h" + +#define CONSOLE_ESCAPE 1 /* Ctrl-a */ + +static struct termios saved_termios; +static int terminal_fd = -1; +static int output_fd = -1; +static bool escape_pending; + +static void host_console_restore(void) +{ + if (terminal_fd >= 0) + tcsetattr(terminal_fd, TCSANOW, &saved_termios); +} + +void host_console_setup(int in_fd, int out_fd) +{ + struct termios termios; + + output_fd = out_fd; + if (!isatty(in_fd) || tcgetattr(in_fd, &saved_termios) < 0) + return; + + termios = saved_termios; + termios.c_lflag &= ~(ICANON | ECHO | ISIG); + if (tcsetattr(in_fd, TCSANOW, &termios) < 0) + return; + + terminal_fd = in_fd; + atexit(host_console_restore); +} + +ssize_t host_console_read(int fd, void *buf, size_t len) +{ + ssize_t nread = read(fd, buf, len); + uint8_t *bytes = buf; + + if (nread <= 0) + return nread; + + for (ssize_t i = 0; i < nread; i++) { + if (escape_pending && bytes[i] == 'x') { + if (output_fd >= 0) { + ssize_t written = write(output_fd, "\n", 1); + (void) written; + } + exit(0); + } + escape_pending = bytes[i] == CONSOLE_ESCAPE; + } + + return nread; +} diff --git a/console.h b/console.h new file mode 100644 index 00000000..d8195ac7 --- /dev/null +++ b/console.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +void host_console_setup(int in_fd, int out_fd); +ssize_t host_console_read(int fd, void *buf, size_t len); diff --git a/device.h b/device.h index 02198be4..58cf3c29 100644 --- a/device.h +++ b/device.h @@ -84,7 +84,6 @@ void u8250_write(hart_t *core, uint32_t value); void u8250_check_ready(u8250_state_t *uart); void u8250_flush_out(u8250_state_t *uart); -void capture_keyboard_input(); /* virtio-net */ diff --git a/main.c b/main.c index c09c2b57..884447e0 100644 --- a/main.c +++ b/main.c @@ -23,6 +23,7 @@ #include #endif +#include "console.h" #include "coro.h" #include "device.h" #include "mini-gdbstub/include/gdbstub.h" @@ -1009,10 +1010,11 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) } /* Set up peripherals */ - emu->uart.in_fd = 0, emu->uart.out_fd = 1; + emu->uart.in_fd = STDIN_FILENO; + emu->uart.out_fd = STDOUT_FILENO; emu->uart.waiting_hart_id = UINT32_MAX; emu->uart.has_waiting_hart = false; - capture_keyboard_input(); /* set up uart */ + host_console_setup(STDIN_FILENO, STDOUT_FILENO); #if SEMU_HAS(VIRTIONET) /* Always set ram pointer, even if netdev is not configured. * Device tree may still expose the device to guest. diff --git a/uart.c b/uart.c index 2e80cded..641ea2cf 100644 --- a/uart.c +++ b/uart.c @@ -1,58 +1,19 @@ #include #include #include -#include #include -#include #include +#include "console.h" #include "coro.h" #include "device.h" #include "riscv.h" #include "riscv_private.h" -/* - * The control mode flag for keyboard. - * - * ICANON: Enable canonical mode. - * ECHO: Echo input characters. - * ISIG: When any of the characters INTR, QUIT, - * SUSP, or DSUSP are received, generate the - * corresponding signal. - * - * It is essential to re-enable ISIG upon exit. - * Otherwise, the default signal handler will - * not catch the signal. E.g., SIGINT generated by - * CTRL + c. - * - */ -#define TERMIOS_C_CFLAG (ICANON | ECHO | ISIG) - /* Emulate 8250 (plain, without loopback mode support) */ #define U8250_INT_THRE 1 -static void reset_keyboard_input() -{ - /* Re-enable echo, etc. on keyboard. */ - struct termios term; - tcgetattr(0, &term); - term.c_lflag |= TERMIOS_C_CFLAG; - tcsetattr(0, TCSANOW, &term); -} - -/* Asynchronous communication to capture all keyboard input for the VM. */ -void capture_keyboard_input() -{ - /* Hook exit, because we want to re-enable keyboard. */ - atexit(reset_keyboard_input); - - struct termios term; - tcgetattr(0, &term); - term.c_lflag &= ~TERMIOS_C_CFLAG; /* Disable echo as well */ - tcsetattr(0, TCSANOW, &term); -} - void u8250_update_interrupts(u8250_state_t *uart) { /* Some interrupts are level-generated. */ @@ -164,18 +125,11 @@ static uint8_t u8250_handle_in(u8250_state_t *uart) return value; /* Spurious wakeup - still no data */ } - if (read(uart->in_fd, &value, 1) < 0) + if (host_console_read(uart->in_fd, &value, 1) < 0) fprintf(stderr, "failed to read UART input: %s\n", strerror(errno)); uart->in_ready = false; u8250_check_ready(uart); - if (value == 1) { /* start of heading (Ctrl-a) */ - if (getchar() == 120) { /* keyboard x */ - printf("\n"); /* end emulator with newline */ - exit(0); - } - } - return value; } From cf35dd37ae87f412d570c77b5310e25154489b66 Mon Sep 17 00:00:00 2001 From: Shengwen Cheng Date: Mon, 7 Sep 2026 23:33:05 +0800 Subject: [PATCH 3/4] Implement virtio-console --- Makefile | 22 ++- device.h | 64 ++++++- feature.h | 8 + main.c | 119 +++++++++--- minimal.dts | 24 ++- virtio-console.c | 474 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 679 insertions(+), 32 deletions(-) create mode 100644 virtio-console.c diff --git a/Makefile b/Makefile index 148bef37..d5026c98 100644 --- a/Makefile +++ b/Makefile @@ -215,6 +215,27 @@ ifeq ($(call has, VIRTIOGPU), 1) OBJS_EXTRA += vgpu-display.o endif +# Guest console: UART-8250 or virtio-console +SEMU_CONSOLE ?= virtio +SEMU_CONSOLE := $(strip $(SEMU_CONSOLE)) +override ENABLE_UART8250 := 0 +override ENABLE_VIRTIOCONSOLE := 0 +ifeq ($(SEMU_CONSOLE),uart8250) + override ENABLE_UART8250 := 1 +else ifeq ($(SEMU_CONSOLE),virtio) + override ENABLE_VIRTIOCONSOLE := 1 +else + $(error SEMU_CONSOLE must be either uart8250 or virtio) +endif +$(call set-feature, UART8250) +$(call set-feature, VIRTIOCONSOLE) + +ifeq ($(call has, UART8250), 1) + OBJS_EXTRA += uart.o +else + OBJS_EXTRA += virtio-console.o +endif + ifneq ($(filter 1,$(call has, VIRTIOGPU) $(call has, VIRTIOINPUT)),) OBJS_EXTRA += window-sw.o endif @@ -228,7 +249,6 @@ OBJS := \ utils.o \ plic.o \ console.o \ - uart.o \ main.o \ aclint.o \ coro.o \ diff --git a/device.h b/device.h index 58cf3c29..1fa8eb8d 100644 --- a/device.h +++ b/device.h @@ -50,6 +50,7 @@ void plic_write(hart_t *core, uint32_t value); /* UART */ +#if SEMU_HAS(UART8250) #define IRQ_UART 1 #define IRQ_UART_BIT (1 << IRQ_UART) @@ -84,6 +85,7 @@ void u8250_write(hart_t *core, uint32_t value); void u8250_check_ready(u8250_state_t *uart); void u8250_flush_out(u8250_state_t *uart); +#endif /* SEMU_HAS(UART8250) */ /* virtio-net */ @@ -346,6 +348,57 @@ uint32_t virtio_gpu_register_scanout(virtio_gpu_state_t *vgpu, uint32_t height); #endif /* SEMU_HAS(VIRTIOGPU) */ +/* VirtIO-Console */ + +#if SEMU_HAS(VIRTIOCONSOLE) + +#define IRQ_VCONSOLE 10 +#define IRQ_VCONSOLE_BIT (1 << IRQ_VCONSOLE) + +typedef struct { + uint32_t QueueNum; + uint32_t QueueDesc; + uint32_t QueueAvail; + uint32_t QueueUsed; + uint16_t last_avail; + bool ready; +} virtio_console_queue_t; + +typedef struct { + /* feature negotiation */ + uint32_t DeviceFeaturesSel; + uint32_t DriverFeatures; + uint32_t DriverFeaturesSel; + /* queue config */ + uint32_t QueueSel; + virtio_console_queue_t queues[2]; + /* status */ + uint32_t Status; + uint32_t InterruptStatus; + /* supplied by environment */ + uint32_t *ram; + int in_fd; + int out_fd; +} virtio_console_state_t; + +void virtio_console_read(hart_t *core, + virtio_console_state_t *vcon, + uint32_t addr, + uint8_t width, + uint32_t *value); +void virtio_console_write(hart_t *core, + virtio_console_state_t *vcon, + uint32_t addr, + uint8_t width, + uint32_t value); +void virtio_console_refresh(virtio_console_state_t *vcon); +void virtio_console_init(virtio_console_state_t *vcon, + uint32_t *ram, + int in_fd, + int out_fd); + +#endif /* SEMU_HAS(VIRTIOCONSOLE) */ + /* ACLINT MTIMER */ typedef struct { /* A MTIMER device has two separate base addresses: one for the MTIME @@ -557,7 +610,9 @@ typedef struct { uint32_t *disk; vm_t vm; plic_state_t plic; +#if SEMU_HAS(UART8250) u8250_state_t uart; +#endif #if SEMU_HAS(VIRTIONET) virtio_net_state_t vnet; #endif @@ -580,13 +635,16 @@ typedef struct { #if SEMU_HAS(VIRTIOGPU) virtio_gpu_state_t vgpu; #endif +#if SEMU_HAS(VIRTIOCONSOLE) + virtio_console_state_t vconsole; +#endif #if SEMU_HAS(VIRTIOINPUT) || SEMU_HAS(VIRTIOGPU) /* Use self-pipe trick to unblock the emulator loop when the window backend * has queued work, such as input events or window shutdown. When all harts * are idle, 'semu_run()' can call 'poll(-1)' and block indefinitely - * waiting for timer or UART events. The window-event thread has no way to - * wake that blocked 'poll()' other than writing to a file descriptor it is - * watching. + * waiting for timer or console events. The window-event thread has no way + * to wake that blocked 'poll()' other than writing to a file descriptor it + * is watching. * * 'wake_fd[0]' (read end) is added to 'pfds[]' so 'poll()' monitors it. * 'wake_fd[1]' (write end) is handed to the window backend, which diff --git a/feature.h b/feature.h index bca97602..717c1085 100644 --- a/feature.h +++ b/feature.h @@ -2,6 +2,14 @@ /* enable/disable (compile time) features in this header */ +#ifndef SEMU_FEATURE_VIRTIOCONSOLE +#define SEMU_FEATURE_VIRTIOCONSOLE 1 +#endif + +#ifndef SEMU_FEATURE_UART8250 +#define SEMU_FEATURE_UART8250 0 +#endif + /* virtio-blk */ #ifndef SEMU_FEATURE_VIRTIOBLK #define SEMU_FEATURE_VIRTIOBLK 1 diff --git a/main.c b/main.c index 884447e0..2225e382 100644 --- a/main.c +++ b/main.c @@ -77,6 +77,7 @@ static uint32_t *mem_page_table(const hart_t *hart, uint32_t ppn) return NULL; } +#if SEMU_HAS(UART8250) static void emu_update_uart_interrupts(vm_t *vm) { emu_state_t *data = PRIV(vm->hart[0]); @@ -87,6 +88,40 @@ static void emu_update_uart_interrupts(vm_t *vm) data->plic.active &= ~IRQ_UART_BIT; plic_update_interrupts(vm, &data->plic); } +#endif + +#if SEMU_HAS(VIRTIOCONSOLE) +static void emu_update_vconsole_interrupts(vm_t *vm) +{ + emu_state_t *data = PRIV(vm->hart[0]); + if (data->vconsole.InterruptStatus) + data->plic.active |= IRQ_VCONSOLE_BIT; + else + data->plic.active &= ~IRQ_VCONSOLE_BIT; + plic_update_interrupts(vm, &data->plic); +} +#endif + +static int emu_console_input_fd(const emu_state_t *emu) +{ +#if SEMU_HAS(UART8250) + return emu->uart.in_fd; +#else + return emu->vconsole.in_fd; +#endif +} + +static bool emu_hart_waiting_for_console(const emu_state_t *emu, + uint32_t hart_id) +{ +#if SEMU_HAS(UART8250) + return emu->uart.has_waiting_hart && emu->uart.waiting_hart_id == hart_id; +#else + (void) emu; + (void) hart_id; + return false; +#endif +} #if SEMU_HAS(VIRTIONET) static void emu_update_vnet_interrupts(vm_t *vm) @@ -224,10 +259,16 @@ static inline void emu_tick_peripherals(emu_state_t *emu) if (emu->peripheral_update_ctr-- == 0) { emu->peripheral_update_ctr = 64; +#if SEMU_HAS(UART8250) u8250_check_ready(&emu->uart); u8250_flush_out(&emu->uart); if (emu->uart.in_ready) emu_update_uart_interrupts(vm); +#else + virtio_console_refresh(&emu->vconsole); + if (emu->vconsole.InterruptStatus) + emu_update_vconsole_interrupts(vm); +#endif #if SEMU_HAS(VIRTIONET) virtio_net_refresh_queue(&emu->vnet); @@ -295,10 +336,12 @@ static void mem_load(hart_t *hart, case 0x2: /* PLIC (0 - 0x3F) */ plic_read(hart, &data->plic, addr & 0x3FFFFFF, width, value); return; +#if SEMU_HAS(UART8250) case 0x40: /* UART */ u8250_read(hart, &data->uart, addr & 0xFFFFF, width, value); emu_update_uart_interrupts(hart->vm); return; +#endif #if SEMU_HAS(VIRTIONET) case 0x41: /* virtio-net */ virtio_net_read(hart, &data->vnet, addr & 0xFFFFF, width, value); @@ -350,6 +393,12 @@ static void mem_load(hart_t *hart, case 0x4B: /* virtio-gpu */ virtio_gpu_read(hart, &data->vgpu, addr & 0xFFFFF, width, value); return; +#endif +#if SEMU_HAS(VIRTIOCONSOLE) + case 0x4C: /* virtio-console */ + virtio_console_read(hart, &data->vconsole, addr & 0xFFFFF, width, + value); + return; #endif } } @@ -376,10 +425,12 @@ static void mem_store(hart_t *hart, plic_write(hart, &data->plic, addr & 0x3FFFFFF, width, value); plic_update_interrupts(hart->vm, &data->plic); return; +#if SEMU_HAS(UART8250) case 0x40: /* UART */ u8250_write(hart, &data->uart, addr & 0xFFFFF, width, value); emu_update_uart_interrupts(hart->vm); return; +#endif #if SEMU_HAS(VIRTIONET) case 0x41: /* virtio-net */ virtio_net_write(hart, &data->vnet, addr & 0xFFFFF, width, value); @@ -443,6 +494,13 @@ static void mem_store(hart_t *hart, virtio_gpu_write(hart, &data->vgpu, addr & 0xFFFFF, width, value); emu_update_vgpu_interrupts(hart->vm); return; +#endif +#if SEMU_HAS(VIRTIOCONSOLE) + case 0x4C: /* virtio-console */ + virtio_console_write(hart, &data->vconsole, addr & 0xFFFFF, width, + value); + emu_update_vconsole_interrupts(hart->vm); + return; #endif } } @@ -1010,10 +1068,14 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) } /* Set up peripherals */ +#if SEMU_HAS(UART8250) emu->uart.in_fd = STDIN_FILENO; emu->uart.out_fd = STDOUT_FILENO; emu->uart.waiting_hart_id = UINT32_MAX; emu->uart.has_waiting_hart = false; +#else + virtio_console_init(&emu->vconsole, emu->ram, STDIN_FILENO, STDOUT_FILENO); +#endif host_console_setup(STDIN_FILENO, STDOUT_FILENO); #if SEMU_HAS(VIRTIONET) /* Always set ram pointer, even if netdev is not configured. @@ -1444,14 +1506,14 @@ static void semu_run(emu_state_t *emu) * * Architecture: * - Each hart runs as an independent coroutine - * - Peripherals (VirtIO-Net, UART, etc.) use inline polling + * - Peripherals (VirtIO-Net, console, etc.) use inline polling * - Main loop acts as scheduler, resuming hart coroutines round-robin - * - poll() monitors timer and UART for power management + * - poll() monitors timer and console input for power management * * Power management optimization: * - When all harts execute WFI (Wait For Interrupt), scheduler blocks * in poll() with timeout=-1 (indefinite) until: - * * UART input arrives (keyboard) + * * Console input arrives * * Timer expires (1ms periodic timer for guest timer emulation) * - This avoids busy-waiting when guest OS is idle * @@ -1479,12 +1541,13 @@ static void semu_run(emu_state_t *emu) return; } - if (isatty(emu->uart.in_fd)) { - struct kevent kev_uart; - EV_SET(&kev_uart, emu->uart.in_fd, EVFILT_READ, EV_ADD | EV_ENABLE, - 0, 0, NULL); - if (kevent(kq, &kev_uart, 1, NULL, 0, NULL) < 0) { - perror("kevent uart setup"); + int console_fd = emu_console_input_fd(emu); + if (isatty(console_fd)) { + struct kevent kev_console; + EV_SET(&kev_console, console_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, + 0, NULL); + if (kevent(kq, &kev_console, 1, NULL, 0, NULL) < 0) { + perror("kevent console setup"); close(kq); emu->exit_code = -1; return; @@ -1512,7 +1575,7 @@ static void semu_run(emu_state_t *emu) /* Poll-based event loop for I/O monitoring: * - Timer fd: 1 descriptor for periodic timer (kqueue/timerfd) - * - UART fd: 1 descriptor for keyboard input + * - Console fd: 1 descriptor for host input */ struct pollfd *pfds = NULL; size_t poll_capacity = 0; @@ -1523,7 +1586,7 @@ static void semu_run(emu_state_t *emu) */ if (signal_received) break; - /* Only need fds for timer and UART (no coroutine I/O), + /* Only need fds for the timer and console (no coroutine I/O), * plus an optional wake pipe when a window backend is enabled. */ size_t needed = 2; @@ -1555,7 +1618,7 @@ static void semu_run(emu_state_t *emu) * modifies flags. * * - If no harts are STARTED, block indefinitely (wait for IPI) - * - If all STARTED harts are idle (WFI or UART waiting), block + * - If all STARTED harts are idle (WFI or console waiting), block * - Otherwise, use non-blocking poll (timeout=0) */ int poll_timeout = 0; @@ -1564,10 +1627,11 @@ static void semu_run(emu_state_t *emu) for (uint32_t i = 0; i < vm->n_hart; i++) { if (vm->hart[i]->hsm_status == SBI_HSM_STATE_STARTED) { started_harts++; - /* Count hart as idle if it's in WFI or waiting for UART */ + /* Count a hart as idle while it is in WFI or blocked on the + * selected console frontend. + */ if (vm->hart[i]->in_wfi || - (emu->uart.has_waiting_hart && - emu->uart.waiting_hart_id == i)) { + emu_hart_waiting_for_console(emu, i)) { idle_harts++; } } @@ -1608,23 +1672,26 @@ static void semu_run(emu_state_t *emu) } #endif - /* Add UART input fd (stdin for keyboard input). - * Only add UART when: + /* Add the selected console input fd. + * Only add it when: * 1. Single-hart configuration (n_hart == 1), OR * 2. Boot not complete (!boot_complete), OR * 3. All harts are active (idle_harts == 0), OR - * 4. A hart is actively waiting for UART input + * 4. A hart is actively waiting for console input * - * This prevents UART (which is always "readable" on TTY) from + * This prevents an always-readable input from * preventing poll() sleep when harts are idle. Trade-off: user * input (Ctrl+A x) may be delayed by up to poll_timeout (10ms) * when harts are idle, which is acceptable for an emulator. */ - bool need_uart = (vm->n_hart == 1) || !boot_complete || - (idle_harts == 0) || emu->uart.has_waiting_hart; - if (emu->uart.in_fd >= 0 && pfd_count < poll_capacity && - need_uart) { - pfds[pfd_count] = (struct pollfd) {emu->uart.in_fd, POLLIN, 0}; + bool need_console = + (vm->n_hart == 1) || !boot_complete || (idle_harts == 0); +#if SEMU_HAS(UART8250) + need_console |= emu->uart.has_waiting_hart; +#endif + int console_fd = emu_console_input_fd(emu); + if (console_fd >= 0 && pfd_count < poll_capacity && need_console) { + pfds[pfd_count] = (struct pollfd) {console_fd, POLLIN, 0}; pfd_count++; } @@ -1665,7 +1732,7 @@ static void semu_run(emu_state_t *emu) /* Execute poll() to wait for I/O events. * - timeout=0: non-blocking poll when harts are active * - timeout=10: short sleep when some harts idle - * - timeout=-1: blocking poll when all harts idle (WFI or UART + * - timeout=-1: blocking poll when all harts idle (WFI or console * wait) * * When pfd_count==0, poll() acts as a pure sleep mechanism. @@ -1715,7 +1782,7 @@ static void semu_run(emu_state_t *emu) /* Resume all hart coroutines (round-robin scheduling). * Each hart executes a batch of instructions, then yields back. * Harts in WFI will have their in_wfi flag cleared by interrupt - * handlers (ACLINT, PLIC, UART) when interrupts are injected. + * handlers (ACLINT, PLIC, console) when interrupts are injected. * * Note: We must always resume harts after poll() returns, even if * all harts appear idle. The in_wfi flag is only cleared when diff --git a/minimal.dts b/minimal.dts index 2b604236..5e9d4f6f 100644 --- a/minimal.dts +++ b/minimal.dts @@ -10,26 +10,36 @@ #size-cells = <1>; model = "semu"; +#if SEMU_FEATURE_UART8250 aliases { serial0 = "/soc@F0000000/serial@4000000"; }; +#endif chosen { #if SEMU_FEATURE_EXTERNAL_ROOT && SEMU_FEATURE_VIRTIOBLK /* External rootfs on virtio-blk; skip initrd unpack entirely. - * - quiet/loglevel=3: suppress non-error printk so the UART path - * (MMIO -> device handler -> fputc) does not dominate boot + * - quiet/loglevel=3: suppress non-error printk so console traffic + * does not dominate boot * - lpj=260000: skip the BogoMIPS calibration loop; the value * matches what calibration would print at the current * timebase-frequency * Note: rootflags=noatime makes ext4 root mount fail under the * prebuilt Linux Image; do not add it back without re-testing. */ +#if SEMU_FEATURE_UART8250 bootargs = "earlycon console=ttyS0 root=/dev/vda rw rootwait quiet loglevel=3 lpj=260000"; stdout-path = "serial0"; +#elif SEMU_FEATURE_VIRTIOCONSOLE + bootargs = "console=hvc0 root=/dev/vda rw rootwait quiet loglevel=3 lpj=260000"; +#endif #else +#if SEMU_FEATURE_UART8250 bootargs = "earlycon console=ttyS0"; stdout-path = "serial0"; +#elif SEMU_FEATURE_VIRTIOCONSOLE + bootargs = "console=hvc0"; +#endif linux,initrd-start = <0x1f700000>; /* @403 MiB (503 * 1024 * 1024) */ linux,initrd-end = <0x1fefffff>; /* @511 MiB (511 * 1024 * 1024 - 1) */ #endif @@ -54,6 +64,7 @@ ranges = <0x0 0xF0000000 0x10000000>; interrupt-parent = <&plic0>; +#if SEMU_FEATURE_UART8250 serial@4000000 { compatible = "ns16550"; reg = <0x4000000 0x100000>; @@ -61,6 +72,7 @@ no-loopback-test; clock-frequency = <5000000>; /* the baudrate divisor is ignored */ }; +#endif #if SEMU_FEATURE_VIRTIONET net0: virtio@4100000 { @@ -123,5 +135,13 @@ interrupts = <9>; }; #endif + +#if SEMU_FEATURE_VIRTIOCONSOLE + virtio@4c00000 { + compatible = "virtio,mmio"; + reg = <0x4c00000 0x200>; + interrupts = <10>; + }; +#endif }; }; diff --git a/virtio-console.c b/virtio-console.c new file mode 100644 index 00000000..a3e0e8ee --- /dev/null +++ b/virtio-console.c @@ -0,0 +1,474 @@ +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "console.h" +#include "device.h" +#include "riscv.h" +#include "riscv_private.h" +#include "virtio.h" + +#define VCON_FEATURES_0 0 +#define VCON_FEATURES_1 1 /* VIRTIO_F_VERSION_1 */ +#define VCON_QUEUE_NUM_MAX 1024 +#define VCON_INPUT_CHUNK 256 +#define VCON_QUEUE (vcon->queues[vcon->QueueSel]) + +enum { VCON_QUEUE_RX = 0, VCON_QUEUE_TX = 1 }; + +static void virtio_console_set_fail(virtio_console_state_t *vcon) +{ + vcon->Status |= VIRTIO_STATUS__DEVICE_NEEDS_RESET; + if (vcon->Status & VIRTIO_STATUS__DRIVER_OK) + vcon->InterruptStatus |= VIRTIO_INT__CONF_CHANGE; +} + +static bool vcon_guest_range_valid(uint64_t addr, uint64_t len) +{ + return addr <= RAM_SIZE && len <= (uint64_t) RAM_SIZE - addr; +} + +static inline uint32_t vcon_preprocess(virtio_console_state_t *vcon, + uint32_t addr) +{ + if (addr >= RAM_SIZE || (addr & 0b11)) + return virtio_console_set_fail(vcon), 0; + + return addr >> 2; +} + +static bool vcon_queue_layout_valid(const virtio_console_queue_t *queue) +{ + uint64_t num = queue->QueueNum; + uint64_t desc = (uint64_t) queue->QueueDesc << 2; + uint64_t avail = (uint64_t) queue->QueueAvail << 2; + uint64_t used = (uint64_t) queue->QueueUsed << 2; + + if (!num || num > VCON_QUEUE_NUM_MAX || (num & (num - 1))) + return false; + if ((desc & 0xf) || (avail & 0x1) || (used & 0x3)) + return false; + + return vcon_guest_range_valid(desc, num * 16) && + vcon_guest_range_valid(avail, 4 + num * 2) && + vcon_guest_range_valid(used, 4 + num * 8); +} + +static void virtio_console_update_status(virtio_console_state_t *vcon, + uint32_t status) +{ + vcon->Status |= status; + if (status) + return; + + uint32_t *ram = vcon->ram; + int in_fd = vcon->in_fd; + int out_fd = vcon->out_fd; + + memset(vcon, 0, sizeof(*vcon)); + vcon->ram = ram; + vcon->in_fd = in_fd; + vcon->out_fd = out_fd; +} + +static uint16_t vcon_avail_entry(const uint32_t *ram, + const virtio_console_queue_t *queue, + uint16_t slot) +{ + uint32_t word = ram[queue->QueueAvail + 1 + slot / 2]; + return word >> (16 * (slot % 2)); +} + +static bool vcon_get_chain(virtio_console_state_t *vcon, + const virtio_console_queue_t *queue, + uint16_t head, + bool device_writes, + struct virtq_desc *chain, + uint16_t *chain_len) +{ + uint16_t desc_idx = head; + + *chain_len = 0; + for (uint32_t steps = 0; steps < queue->QueueNum; steps++) { + struct virtq_desc desc; + + if (desc_idx >= queue->QueueNum) { + virtio_console_set_fail(vcon); + return false; + } + + memcpy(&desc, &vcon->ram[queue->QueueDesc + desc_idx * 4], + sizeof(desc)); + if ((!!(desc.flags & VIRTIO_DESC_F_WRITE)) != device_writes || + (desc.flags & ~(VIRTIO_DESC_F_NEXT | VIRTIO_DESC_F_WRITE)) || + !vcon_guest_range_valid(desc.addr, desc.len)) { + virtio_console_set_fail(vcon); + return false; + } + + chain[(*chain_len)++] = desc; + if (!(desc.flags & VIRTIO_DESC_F_NEXT)) + return true; + desc_idx = desc.next; + } + + /* A chain with more descriptors than the queue contains has a cycle. */ + virtio_console_set_fail(vcon); + return false; +} + +static void vcon_push_used(virtio_console_state_t *vcon, + virtio_console_queue_t *queue, + uint16_t head, + uint32_t len) +{ + uint16_t used = vcon->ram[queue->QueueUsed] >> 16; + uint32_t elem = queue->QueueUsed + 1 + (used % queue->QueueNum) * 2; + + vcon->ram[elem] = head; + vcon->ram[elem + 1] = len; + used++; + vcon->ram[queue->QueueUsed] &= MASK(16); + vcon->ram[queue->QueueUsed] |= (uint32_t) used << 16; + + if (!(vcon->ram[queue->QueueAvail] & 1)) + vcon->InterruptStatus |= VIRTIO_INT__USED_RING; +} + +static bool vcon_queue_pending(virtio_console_state_t *vcon, + virtio_console_queue_t *queue, + uint16_t *avail) +{ + *avail = vcon->ram[queue->QueueAvail] >> 16; + if ((uint16_t) (*avail - queue->last_avail) <= queue->QueueNum) + return queue->last_avail != *avail; + + virtio_console_set_fail(vcon); + return false; +} + +static bool vcon_write_all(int fd, const void *buf, size_t len) +{ + const uint8_t *cursor = buf; + + while (len) { + ssize_t written = write(fd, cursor, len); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) + return false; + cursor += written; + len -= written; + } + + return true; +} + +static void vcon_process_tx(virtio_console_state_t *vcon) +{ + virtio_console_queue_t *queue = &vcon->queues[VCON_QUEUE_TX]; + struct virtq_desc chain[VCON_QUEUE_NUM_MAX]; + uint16_t avail; + + if (vcon->Status & VIRTIO_STATUS__DEVICE_NEEDS_RESET) + return; + if (!(vcon->Status & VIRTIO_STATUS__DRIVER_OK) || !queue->ready) { + virtio_console_set_fail(vcon); + return; + } + + if (!vcon_queue_pending(vcon, queue, &avail)) + return; + + while (queue->last_avail != avail) { + uint16_t slot = queue->last_avail % queue->QueueNum; + uint16_t head = vcon_avail_entry(vcon->ram, queue, slot); + uint16_t chain_len; + uint32_t total = 0; + + if (!vcon_get_chain(vcon, queue, head, false, chain, &chain_len)) + return; + + for (uint16_t i = 0; i < chain_len; i++) { + if (chain[i].len > UINT32_MAX - total) { + virtio_console_set_fail(vcon); + return; + } + const void *buf = + (const uint8_t *) vcon->ram + (uintptr_t) chain[i].addr; + if (!vcon_write_all(vcon->out_fd, buf, chain[i].len)) { + fprintf(stderr, "virtio-console: output failed: %s\n", + strerror(errno)); + virtio_console_set_fail(vcon); + return; + } + total += chain[i].len; + } + + queue->last_avail++; + vcon_push_used(vcon, queue, head, total); + if (!vcon_queue_pending(vcon, queue, &avail)) + return; + } +} + +static bool vcon_input_ready(int fd) +{ + struct pollfd pfd = {fd, POLLIN, 0}; + + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} + +void virtio_console_refresh(virtio_console_state_t *vcon) +{ + virtio_console_queue_t *queue = &vcon->queues[VCON_QUEUE_RX]; + struct virtq_desc chain[VCON_QUEUE_NUM_MAX]; + uint8_t input[VCON_INPUT_CHUNK]; + uint16_t avail; + + if ((vcon->Status & VIRTIO_STATUS__DEVICE_NEEDS_RESET) || + !(vcon->Status & VIRTIO_STATUS__DRIVER_OK) || !queue->ready || + !vcon_input_ready(vcon->in_fd) || + !vcon_queue_pending(vcon, queue, &avail)) + return; + + while (queue->last_avail != avail && vcon_input_ready(vcon->in_fd)) { + uint16_t slot = queue->last_avail % queue->QueueNum; + uint16_t head = vcon_avail_entry(vcon->ram, queue, slot); + uint16_t chain_len; + size_t capacity = 0; + + if (!vcon_get_chain(vcon, queue, head, true, chain, &chain_len)) + return; + + for (uint16_t i = 0; i < chain_len && capacity < sizeof(input); i++) + capacity += MIN((size_t) chain[i].len, sizeof(input) - capacity); + if (!capacity) { + virtio_console_set_fail(vcon); + return; + } + + ssize_t nread = host_console_read(vcon->in_fd, input, capacity); + if (nread < 0 && + (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) + return; + if (nread < 0) { + fprintf(stderr, "virtio-console: input failed: %s\n", + strerror(errno)); + virtio_console_set_fail(vcon); + return; + } + if (!nread) + return; + + size_t copied = 0; + for (uint16_t i = 0; i < chain_len && copied < (size_t) nread; i++) { + size_t len = MIN((size_t) chain[i].len, (size_t) nread - copied); + void *buf = (uint8_t *) vcon->ram + (uintptr_t) chain[i].addr; + memcpy(buf, input + copied, len); + copied += len; + } + + queue->last_avail++; + vcon_push_used(vcon, queue, head, nread); + if (!vcon_queue_pending(vcon, queue, &avail)) + return; + } +} + +static bool virtio_console_reg_read(virtio_console_state_t *vcon, + uint32_t addr, + uint32_t *value) +{ +#define _(reg) VIRTIO_##reg + switch (addr) { + case _(MagicValue): + *value = 0x74726976; + return true; + case _(Version): + *value = 2; + return true; + case _(DeviceID): + *value = 3; + return true; + case _(VendorID): + *value = VIRTIO_VENDOR_ID; + return true; + case _(DeviceFeatures): + *value = vcon->DeviceFeaturesSel == 0 + ? VCON_FEATURES_0 + : (vcon->DeviceFeaturesSel == 1 ? VCON_FEATURES_1 : 0); + return true; + case _(QueueNumMax): + *value = VCON_QUEUE_NUM_MAX; + return true; + case _(QueueReady): + *value = VCON_QUEUE.ready; + return true; + case _(InterruptStatus): + *value = vcon->InterruptStatus; + return true; + case _(Status): + *value = vcon->Status; + return true; + case _(ConfigGeneration): + *value = 0; + return true; + default: + return false; + } +#undef _ +} + +static bool virtio_console_reg_write(virtio_console_state_t *vcon, + uint32_t addr, + uint32_t value) +{ +#define _(reg) VIRTIO_##reg + switch (addr) { + case _(DeviceFeaturesSel): + vcon->DeviceFeaturesSel = value; + return true; + case _(DriverFeatures): + if (vcon->DriverFeaturesSel == 0) + vcon->DriverFeatures = value; + return true; + case _(DriverFeaturesSel): + vcon->DriverFeaturesSel = value; + return true; + case _(QueueSel): + if (value < ARRAY_SIZE(vcon->queues)) + vcon->QueueSel = value; + else + virtio_console_set_fail(vcon); + return true; + case _(QueueNum): + if (VCON_QUEUE.ready) + virtio_console_set_fail(vcon); + else if (value && value <= VCON_QUEUE_NUM_MAX && !(value & (value - 1))) + VCON_QUEUE.QueueNum = value; + else + virtio_console_set_fail(vcon); + return true; + case _(QueueReady): + if (value & ~1U || (value && !vcon_queue_layout_valid(&VCON_QUEUE))) { + virtio_console_set_fail(vcon); + return true; + } + VCON_QUEUE.ready = value; + return true; + case _(QueueDescLow): + if (VCON_QUEUE.ready) + virtio_console_set_fail(vcon); + else + VCON_QUEUE.QueueDesc = vcon_preprocess(vcon, value); + return true; + case _(QueueDescHigh): + if (VCON_QUEUE.ready || value) + virtio_console_set_fail(vcon); + return true; + case _(QueueDriverLow): + if (VCON_QUEUE.ready) + virtio_console_set_fail(vcon); + else + VCON_QUEUE.QueueAvail = vcon_preprocess(vcon, value); + return true; + case _(QueueDriverHigh): + if (VCON_QUEUE.ready || value) + virtio_console_set_fail(vcon); + return true; + case _(QueueDeviceLow): + if (VCON_QUEUE.ready) + virtio_console_set_fail(vcon); + else + VCON_QUEUE.QueueUsed = vcon_preprocess(vcon, value); + return true; + case _(QueueDeviceHigh): + if (VCON_QUEUE.ready || value) + virtio_console_set_fail(vcon); + return true; + case _(QueueNotify): + if (value >= ARRAY_SIZE(vcon->queues)) + virtio_console_set_fail(vcon); + else if (!(vcon->Status & VIRTIO_STATUS__DEVICE_NEEDS_RESET) && + (!(vcon->Status & VIRTIO_STATUS__DRIVER_OK) || + !vcon->queues[value].ready)) + virtio_console_set_fail(vcon); + else if (value == VCON_QUEUE_TX) + vcon_process_tx(vcon); + return true; + case _(InterruptACK): + vcon->InterruptStatus &= ~value; + return true; + case _(Status): + virtio_console_update_status(vcon, value); + return true; + default: + return false; + } +#undef _ +} + +void virtio_console_read(hart_t *vm, + virtio_console_state_t *vcon, + uint32_t addr, + uint8_t width, + uint32_t *value) +{ + switch (width) { + case RV_MEM_LW: + if (addr & 0x3) { + vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); + return; + } + if (!virtio_console_reg_read(vcon, addr >> 2, value)) + vm_set_exception(vm, RV_EXC_LOAD_FAULT, vm->exc_val); + return; + case RV_MEM_LBU: + case RV_MEM_LB: + case RV_MEM_LHU: + case RV_MEM_LH: + vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); + return; + default: + vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); + } +} + +void virtio_console_write(hart_t *vm, + virtio_console_state_t *vcon, + uint32_t addr, + uint8_t width, + uint32_t value) +{ + switch (width) { + case RV_MEM_SW: + if (addr & 0x3) { + vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); + return; + } + if (!virtio_console_reg_write(vcon, addr >> 2, value)) + vm_set_exception(vm, RV_EXC_STORE_FAULT, vm->exc_val); + return; + case RV_MEM_SB: + case RV_MEM_SH: + vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); + return; + default: + vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); + } +} + +void virtio_console_init(virtio_console_state_t *vcon, + uint32_t *ram, + int in_fd, + int out_fd) +{ + vcon->ram = ram; + vcon->in_fd = in_fd; + vcon->out_fd = out_fd; +} From d1bfc1d93149e2f860ceaf5094c83e7a7e743384 Mon Sep 17 00:00:00 2001 From: Shengwen Cheng Date: Mon, 7 Sep 2026 23:43:15 +0800 Subject: [PATCH 4/4] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3193a138..1aec3301 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - Standard SBI, with the timer extension - I/O support using VirtIO standard: - virtio-blk acquires disk image from the host. + - virtio-console provides a paravirtualized console. + - virtio-rng provides entropy to the guest. - virtio-net is mapped as TAP interface. - virtio-snd uses [PortAudio](https://github.com/PortAudio/portaudio) for sound playback on the host with one limitations: - As some unknown issues in guest Linux OS (confirmed in v6.7 and v6.12), you need