diff --git a/Makefile b/Makefile index cd3347a8..28b209f8 100644 --- a/Makefile +++ b/Makefile @@ -215,6 +215,37 @@ ifeq ($(call has, VIRTIOGPU), 1) OBJS_EXTRA += vgpu-display.o endif +# console +SEMU_CONSOLE ?= both +SEMU_CONSOLE := $(strip $(SEMU_CONSOLE)) +override ENABLE_VIRTIOCONSOLE := 0 +override ENABLE_UART8250 := 0 +ifeq ($(SEMU_CONSOLE),virtio) + override ENABLE_VIRTIOCONSOLE := 1 +else ifeq ($(SEMU_CONSOLE),both) + override ENABLE_VIRTIOCONSOLE := 1 + override ENABLE_UART8250 := 1 +else ifeq ($(SEMU_CONSOLE),uart8250) + override ENABLE_UART8250 := 1 +else ifeq ($(SEMU_CONSOLE),uart) + override ENABLE_UART8250 := 1 +else + $(error SEMU_CONSOLE must be virtio, uart8250, or both) +endif + +$(call set-feature, VIRTIOCONSOLE) +$(call set-feature, UART8250) + +ifeq ($(call has, VIRTIOCONSOLE), 1) + OBJS_EXTRA += virtio-console.o + VIRTIO_CONSOLE_PATH ?= /tmp/semu-hvc0 + OPTS += --virtio-console-path $(VIRTIO_CONSOLE_PATH) +endif + +ifeq ($(call has, UART8250), 1) + OBJS_EXTRA += uart.o +endif + ifneq ($(filter 1,$(call has, VIRTIOGPU) $(call has, VIRTIOINPUT)),) OBJS_EXTRA += window-sw.o endif @@ -227,7 +258,6 @@ OBJS := \ ram.o \ utils.o \ plic.o \ - uart.o \ main.o \ aclint.o \ coro.o \ diff --git a/README.md b/README.md index e0ede423..806e6b43 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - Privilege levels: S and U modes - Control and status registers (CSR) - Virtual memory system: RV32 MMU -- UART: 8250/16550 +- UART: 8250/16550 console - PLIC (platform-level interrupt controller): 32 interrupts, no priority - Standard SBI, with the timer extension - I/O support using VirtIO standard: - virtio-blk acquires disk image from the host. + - virtio-console exposes an additional guest console as `/dev/hvc0`. - 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 @@ -67,7 +68,7 @@ With the default external-root build, `make check` uses `Image`, `minimal.dtb`, and `ext4.img`, and boots `semu` headlessly with an equivalent command line: ```shell -$ ./semu -k Image -c 1 -b minimal.dtb -H -d ext4.img +$ ./semu -k Image -c 1 -b minimal.dtb -H -d ext4.img --virtio-console-path /tmp/semu-hvc0 ``` If `ENABLE_EXTERNAL_ROOT=0` is used, `make check` switches to the legacy @@ -90,6 +91,33 @@ Enter `root` to access shell. You can exit the emulator using: \. (press Ctrl+A, leave it, afterwards press X) +By default, `make check` enables both the 8250 UART console and +virtio-console. The UART is the primary console, so boot logs and the main +login prompt appear in the terminal that ran `make check`. The virtio-console +device is also exposed to the guest as `/dev/hvc0`, and `make check` creates a +host endpoint at `/tmp/semu-hvc0`. + +Open another host terminal to attach to the virtio-console: + +```shell +$ screen /tmp/semu-hvc0 +``` + +Inside the guest, confirm both consoles are active with: + +```shell +# cat /proc/consoles +# grep hvc0 /etc/inittab +``` + +To use only one console implementation at build time, override +`SEMU_CONSOLE`: + +```shell +$ make check SEMU_CONSOLE=uart8250 +$ make check SEMU_CONSOLE=virtio +``` + To test virtio-gpu with a visible SDL window, run `semu` manually without `-H`. Make sure `sdl2-config` is in `PATH`, then build the emulator, DTB, kernel, and test tools disk. Press `Ctrl+Alt+G` to release the mouse cursor from the SDL @@ -119,7 +147,7 @@ project and can be listed in the guest with: ## Usage ```shell -./semu -k linux-image [-b dtb-file] [-d disk-image] [-i initrd-image] [-s shared-directory] [-H] +./semu -k linux-image [-b dtb-file] [-d disk-image] [-i initrd-image] [-s shared-directory] [-H] [--virtio-console-path path] ``` * `linux-image` is the path to the Linux kernel `Image`. @@ -129,6 +157,9 @@ project and can be listed in the guest with: from `rootfs.cpio` via `scripts/rootfs_ext4.sh`. * `shared-directory` is optional, as it specifies the path of a directory on the host that will be shared with the guest operating system through virtio-fs, enabling file access from the guest via a virtual filesystem mount. * `-H` (or `--headless`) skips SDL window creation; useful for CI and `make check`. +* `virtio-console-path` is optional, as it creates a host-side symlink to the + pseudo-terminal connected to the guest `/dev/hvc0`. The default used by + `make check` is `/tmp/semu-hvc0`. * `initrd-image` is optional and only used on the *legacy* boot path. The default `minimal.dtb` built with `ENABLE_EXTERNAL_ROOT=1` does not advertise initrd placement, so `-i` there requires either diff --git a/configs/buildroot.config b/configs/buildroot.config index 6c9b179d..1f47c1b1 100644 --- a/configs/buildroot.config +++ b/configs/buildroot.config @@ -47,4 +47,5 @@ BR2_TARGET_ROOTFS_CPIO_FULL=y # BR2_TARGET_ROOTFS_CPIO_DRACUT is not set BR2_TARGET_ROOTFS_CPIO_NONE=y # BR2_TARGET_ROOTFS_TAR is not set +BR2_ROOTFS_POST_BUILD_SCRIPT="../target/post-build.sh" BR2_PACKAGE_BUSYBOX_CONFIG="busybox.config" 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 diff --git a/device.h b/device.h index 02198be4..21548648 100644 --- a/device.h +++ b/device.h @@ -50,6 +50,8 @@ 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) @@ -85,6 +87,7 @@ void u8250_write(hart_t *core, void u8250_check_ready(u8250_state_t *uart); void u8250_flush_out(u8250_state_t *uart); void capture_keyboard_input(); +#endif /* SEMU_HAS(UART8250) */ /* virtio-net */ @@ -347,6 +350,54 @@ 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_init(virtio_console_state_t *vcon); +void virtio_console_refresh_rx(virtio_console_state_t *vcon); + +#endif /* SEMU_HAS(VIRTIOCONSOLE) */ + /* ACLINT MTIMER */ typedef struct { /* A MTIMER device has two separate base addresses: one for the MTIME @@ -558,7 +609,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 @@ -581,6 +634,9 @@ 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 diff --git a/feature.h b/feature.h index bca97602..26b57682 100644 --- a/feature.h +++ b/feature.h @@ -37,5 +37,15 @@ #define SEMU_FEATURE_VIRTIOGPU 1 #endif +/* virtio-console */ +#ifndef SEMU_FEATURE_VIRTIOCONSOLE +#define SEMU_FEATURE_VIRTIOCONSOLE 1 +#endif + +/* 8250/16550 UART console */ +#ifndef SEMU_FEATURE_UART8250 +#define SEMU_FEATURE_UART8250 0 +#endif + /* Feature test macro */ #define SEMU_HAS(x) SEMU_FEATURE_##x diff --git a/main.c b/main.c index a0b53334..db8b21a6 100644 --- a/main.c +++ b/main.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,13 @@ #else #include #endif +#if SEMU_HAS(VIRTIOCONSOLE) +#if defined(__linux__) +#include +#else +#include +#endif +#endif #include "coro.h" #include "device.h" @@ -76,6 +84,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]); @@ -86,6 +95,7 @@ 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(VIRTIONET) static void emu_update_vnet_interrupts(vm_t *vm) @@ -157,6 +167,18 @@ static void emu_update_vgpu_interrupts(vm_t *vm) } #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 void emu_update_timer_interrupt(hart_t *hart) { emu_state_t *data = PRIV(hart); @@ -223,10 +245,18 @@ static inline void emu_tick_peripherals(emu_state_t *emu) if (emu->peripheral_update_ctr-- == 0) { emu->peripheral_update_ctr = 64; +#if SEMU_HAS(VIRTIOCONSOLE) + virtio_console_refresh_rx(&emu->vconsole); + if (emu->vconsole.InterruptStatus) + emu_update_vconsole_interrupts(vm); +#endif + +#if SEMU_HAS(UART8250) u8250_check_ready(&emu->uart); u8250_flush_out(&emu->uart); if (emu->uart.in_ready) emu_update_uart_interrupts(vm); +#endif #if SEMU_HAS(VIRTIONET) virtio_net_refresh_queue(&emu->vnet); @@ -271,6 +301,10 @@ static inline void emu_tick_peripherals(emu_state_t *emu) /* A closed window is treated like a frontend shutdown request. */ if (g_window.window_is_closed()) emu->stopped = true; +#endif +#if SEMU_HAS(VIRTIOCONSOLE) + if (emu->vconsole.InterruptStatus) + emu_update_vconsole_interrupts(vm); #endif } } @@ -294,10 +328,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); @@ -349,6 +385,13 @@ 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 0x52: /* virtio-console */ + virtio_console_read(hart, &data->vconsole, addr & 0xFFFFF, width, + value); + emu_update_vconsole_interrupts(hart->vm); + return; #endif } } @@ -375,10 +418,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); @@ -442,6 +487,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 0x52: /* virtio-console */ + virtio_console_write(hart, &data->vconsole, addr & 0xFFFFF, width, + value); + emu_update_vconsole_interrupts(hart->vm); + return; #endif } } @@ -685,6 +737,56 @@ struct mapper { static struct mapper mapper[N_MAPPERS] = {0}; static int map_index = 0; + +#if SEMU_HAS(VIRTIOCONSOLE) +static char vconsole_link_path[PATH_MAX]; +static bool vconsole_link_created = false; + +static void cleanup_vconsole_link(void) +{ + if (!vconsole_link_created) + return; + unlink(vconsole_link_path); + vconsole_link_created = false; +} + +static bool semu_init_vconsole_pty(virtio_console_state_t *vcon, + const char *path) +{ + int master_fd = -1; + int slave_fd = -1; + char slave_name[PATH_MAX]; + + if (openpty(&master_fd, &slave_fd, slave_name, NULL, NULL) < 0) { + perror("openpty"); + return false; + } + + if (path) { + unlink(path); + if (symlink(slave_name, path) < 0) { + perror("symlink"); + close(master_fd); + close(slave_fd); + return false; + } + + snprintf(vconsole_link_path, sizeof(vconsole_link_path), "%s", path); + vconsole_link_created = true; + atexit(cleanup_vconsole_link); + fprintf(stderr, "virtio-console host endpoint: %s -> %s\n", path, + slave_name); + } else { + fprintf(stderr, "virtio-console host endpoint: %s\n", slave_name); + } + + vcon->in_fd = master_fd; + vcon->out_fd = master_fd; + close(slave_fd); + return true; +} +#endif + static void unmap_files(void) { while (map_index--) { @@ -731,7 +833,8 @@ static void usage(const char *execpath) { fprintf(stderr, "Usage: %s -k linux-image [-b dtb] [-i initrd-image] [-d " - "disk-image] [-s shared-directory] [-H]\n", + "disk-image] [-s shared-directory] [-H] " + "[--virtio-console-path path]\n", execpath); } @@ -745,10 +848,11 @@ static void handle_options(int argc, int *hart_count, bool *debug, bool *headless, - char **shared_dir) + char **shared_dir, + char **vconsole_path) { *kernel_file = *dtb_file = *initrd_file = *disk_file = *net_dev = - *shared_dir = NULL; + *shared_dir = *vconsole_path = NULL; int optidx = 0; struct option opts[] = { @@ -756,10 +860,12 @@ static void handle_options(int argc, {"initrd", 1, NULL, 'i'}, {"disk", 1, NULL, 'd'}, {"netdev", 1, NULL, 'n'}, {"smp", 1, NULL, 'c'}, {"gdbstub", 0, NULL, 'g'}, {"help", 0, NULL, 'h'}, - {"shared_dir", 1, NULL, 's'}, {"headless", 0, NULL, 'H'}}; + {"shared_dir", 1, NULL, 's'}, {"headless", 0, NULL, 'H'}, + {"virtio-console-path", 1, NULL, 'p'}}; int c; - while ((c = getopt_long(argc, argv, "k:b:i:d:n:c:s:ghH", opts, &optidx)) != + while ((c = + getopt_long(argc, argv, "k:b:i:d:n:c:s:p:ghH", opts, &optidx)) != -1) { switch (c) { case 'k': @@ -799,6 +905,9 @@ static void handle_options(int argc, case 's': *shared_dir = optarg; break; + case 'p': + *vconsole_path = optarg; + break; case 'g': *debug = true; break; @@ -866,6 +975,7 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) char *disk_file; char *netdev; char *shared_dir; + char *vconsole_path; int hart_count = 1; bool debug = false; bool headless = false; @@ -875,7 +985,7 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) vm_t *vm = &emu->vm; handle_options(argc, argv, &kernel_file, &dtb_file, &initrd_file, &disk_file, &netdev, &hart_count, &debug, &headless, - &shared_dir); + &shared_dir, &vconsole_path); #if !SEMU_HAS(VIRTIOINPUT) && !SEMU_HAS(VIRTIOGPU) (void) headless; #endif @@ -939,6 +1049,17 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) * (legacy boot path; not used when the guest boots from /dev/vda). */ if (initrd_file) { + struct stat st; + if (stat(initrd_file, &st) < 0) { + fprintf(stderr, "could not stat %s\n", initrd_file); + return 2; + } + if ((uint64_t) st.st_size > INITRD_SIZE) { + fprintf(stderr, + "initrd image %s is too large: %zu bytes > %u bytes\n", + initrd_file, (size_t) st.st_size, INITRD_SIZE); + return 2; + } uint32_t initrd_addr = dtb_addr - INITRD_SIZE; ram_loc = ((char *) emu->ram) + initrd_addr; map_file(&ram_loc, initrd_file); @@ -976,10 +1097,12 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) } /* Set up peripherals */ +#if SEMU_HAS(UART8250) emu->uart.in_fd = 0, emu->uart.out_fd = 1; emu->uart.waiting_hart_id = UINT32_MAX; emu->uart.has_waiting_hart = false; capture_keyboard_input(); /* set up uart */ +#endif #if SEMU_HAS(VIRTIONET) /* Always set ram pointer, even if netdev is not configured. * Device tree may still expose the device to guest. @@ -1036,6 +1159,13 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) vgpu_display_set_scanout_count(scanout_id + 1U); #endif +#if SEMU_HAS(VIRTIOCONSOLE) + emu->vconsole.ram = emu->ram; + if (!semu_init_vconsole_pty(&emu->vconsole, vconsole_path)) + return 1; + virtio_console_init(&(emu->vconsole)); +#endif + #if SEMU_HAS(VIRTIOINPUT) || SEMU_HAS(VIRTIOGPU) g_window.window_init(headless, SCREEN_WIDTH, SCREEN_HEIGHT); @@ -1444,6 +1574,7 @@ static void semu_run(emu_state_t *emu) return; } +#if SEMU_HAS(UART8250) if (isatty(emu->uart.in_fd)) { struct kevent kev_uart; EV_SET(&kev_uart, emu->uart.in_fd, EVFILT_READ, EV_ADD | EV_ENABLE, @@ -1455,6 +1586,7 @@ static void semu_run(emu_state_t *emu) return; } } +#endif #else int wfi_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if (wfi_timer_fd < 0) { @@ -1477,7 +1609,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 + * - UART fd: 1 descriptor for keyboard input, when enabled */ struct pollfd *pfds = NULL; size_t poll_capacity = 0; @@ -1488,10 +1620,13 @@ 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 timer and optional UART (no coroutine I/O), * plus an optional wake pipe when a window backend is enabled. */ - size_t needed = 2; + size_t needed = 1; +#if SEMU_HAS(UART8250) + needed++; +#endif #if SEMU_HAS(VIRTIOINPUT) || SEMU_HAS(VIRTIOGPU) if (emu->wake_fd[0] >= 0) needed++; @@ -1520,7 +1655,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; @@ -1529,10 +1664,13 @@ 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 */ - if (vm->hart[i]->in_wfi || - (emu->uart.has_waiting_hart && - emu->uart.waiting_hart_id == i)) { + bool waiting_for_console = false; +#if SEMU_HAS(UART8250) + waiting_for_console = emu->uart.has_waiting_hart && + emu->uart.waiting_hart_id == i; +#endif + /* Count hart as idle if it's in WFI or waiting for console */ + if (vm->hart[i]->in_wfi || waiting_for_console) { idle_harts++; } } @@ -1573,6 +1711,7 @@ static void semu_run(emu_state_t *emu) } #endif +#if SEMU_HAS(UART8250) /* Add UART input fd (stdin for keyboard input). * Only add UART when: * 1. Single-hart configuration (n_hart == 1), OR @@ -1592,6 +1731,7 @@ static void semu_run(emu_state_t *emu) pfds[pfd_count] = (struct pollfd) {emu->uart.in_fd, POLLIN, 0}; pfd_count++; } +#endif #if SEMU_HAS(VIRTIOINPUT) || SEMU_HAS(VIRTIOGPU) /* Always watch the wake pipe so that backend work such as input diff --git a/minimal.dts b/minimal.dts index 2b604236..cd5f7b3c 100644 --- a/minimal.dts +++ b/minimal.dts @@ -10,26 +10,39 @@ #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 - * - lpj=260000: skip the BogoMIPS calibration loop; the value - * matches what calibration would print at the current - * timebase-frequency + * lpj=260000 skips 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. */ - bootargs = "earlycon console=ttyS0 root=/dev/vda rw rootwait quiet loglevel=3 lpj=260000"; +#if SEMU_FEATURE_VIRTIOCONSOLE && SEMU_FEATURE_UART8250 + bootargs = "earlycon console=hvc0 console=ttyS0 root=/dev/vda rw rootwait lpj=260000"; + stdout-path = "serial0"; +#elif SEMU_FEATURE_VIRTIOCONSOLE + bootargs = "console=hvc0 root=/dev/vda rw rootwait lpj=260000"; +#elif SEMU_FEATURE_UART8250 + bootargs = "earlycon console=ttyS0 root=/dev/vda rw rootwait lpj=260000"; stdout-path = "serial0"; +#endif #else +#if SEMU_FEATURE_VIRTIOCONSOLE && SEMU_FEATURE_UART8250 + bootargs = "earlycon console=hvc0 console=ttyS0"; + stdout-path = "serial0"; +#elif SEMU_FEATURE_VIRTIOCONSOLE + bootargs = "console=hvc0"; +#elif SEMU_FEATURE_UART8250 bootargs = "earlycon console=ttyS0"; stdout-path = "serial0"; +#endif linux,initrd-start = <0x1f700000>; /* @403 MiB (503 * 1024 * 1024) */ linux,initrd-end = <0x1fefffff>; /* @511 MiB (511 * 1024 * 1024 - 1) */ #endif @@ -54,6 +67,7 @@ ranges = <0x0 0xF0000000 0x10000000>; interrupt-parent = <&plic0>; +#if SEMU_FEATURE_UART8250 serial@4000000 { compatible = "ns16550"; reg = <0x4000000 0x100000>; @@ -61,6 +75,7 @@ no-loopback-test; clock-frequency = <5000000>; /* the baudrate divisor is ignored */ }; +#endif #if SEMU_FEATURE_VIRTIONET net0: virtio@4100000 { @@ -123,5 +138,13 @@ interrupts = <9>; }; #endif + +#if SEMU_FEATURE_VIRTIOCONSOLE + console0: virtio@5200000 { + compatible = "virtio,mmio"; + reg = <0x5200000 0x200>; + interrupts = <10>; + }; +#endif }; }; diff --git a/target/post-build.sh b/target/post-build.sh new file mode 100755 index 00000000..bb185597 --- /dev/null +++ b/target/post-build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh +set -eu + +TARGET_DIR="${1:?TARGET_DIR is required}" +INITTAB="${TARGET_DIR}/etc/inittab" +HVC0_LINE='hvc0::respawn:/sbin/getty -L hvc0 0 vt100 # SEMU_VIRTIO_CONSOLE' + +if [ ! -f "${INITTAB}" ]; then + exit 0 +fi + +if ! grep -q 'SEMU_VIRTIO_CONSOLE' "${INITTAB}"; then + printf '\n%s\n' "${HVC0_LINE}" >> "${INITTAB}" +fi diff --git a/virtio-console.c b/virtio-console.c new file mode 100644 index 00000000..3d4a7945 --- /dev/null +++ b/virtio-console.c @@ -0,0 +1,413 @@ +#include +#include +#include +#include +#include +#include + +#include "common.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_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 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 void virtio_console_update_status(virtio_console_state_t *vcon, + uint32_t status) +{ + vcon->Status |= status; + if (status) + return; + + /* Reset while preserving host wiring and config space. */ + 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_read_le16(uint32_t *ram, uint32_t base, uint16_t idx) +{ + uint32_t word = ram[base + 1 + idx / 2]; + return (word >> (16 * (idx % 2))) & MASK(16); +} + +static bool vcon_validate_guest_range(uint64_t addr, + uint32_t len) +{ + return addr < RAM_SIZE && len <= (uint64_t) RAM_SIZE - addr; +} + +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) { + if (errno == EINTR) + continue; + return false; + } + + cursor += written; + len -= written; + } + + return true; +} + +static void vcon_push_used(virtio_console_state_t *vcon, + virtio_console_queue_t *queue, + uint16_t head, + uint32_t written_len) +{ + uint32_t *ram = vcon->ram; + uint16_t used = ram[queue->QueueUsed] >> 16; + uint32_t used_addr = queue->QueueUsed + 1 + (used % queue->QueueNum) * 2; + + ram[used_addr] = head; + ram[used_addr + 1] = written_len; + used++; + + ram[queue->QueueUsed] &= MASK(16); + ram[queue->QueueUsed] |= ((uint32_t) used) << 16; + + if (!(ram[queue->QueueAvail] & 1)) + vcon->InterruptStatus |= VIRTIO_INT__USED_RING; +} + +static bool vcon_fd_ready(int fd) +{ + struct pollfd pfd = {fd, POLLIN, 0}; + + if (poll(&pfd, 1, 0) < 0) + return false; + return (pfd.revents & POLLIN) != 0; +} + +static bool vcon_process_tx(virtio_console_state_t *vcon, + virtio_console_queue_t *queue) +{ + uint32_t *ram = vcon->ram; + uint16_t avail = ram[queue->QueueAvail] >> 16; + + while (queue->last_avail != avail) { + uint16_t slot = queue->last_avail % queue->QueueNum; + uint16_t head = vcon_read_le16(ram, queue->QueueAvail, slot); + uint16_t desc_idx = head; + uint32_t total_len = 0; + bool saw_end = false; + + queue->last_avail++; + + for (uint16_t steps = 0; steps < queue->QueueNum; steps++) { + struct virtq_desc *desc; + const void *buf; + + if (desc_idx >= queue->QueueNum) { + virtio_console_set_fail(vcon); + return false; + } + + desc = (struct virtq_desc *) &ram[queue->QueueDesc + desc_idx * 4]; + if (desc->flags & VIRTIO_DESC_F_WRITE) { + virtio_console_set_fail(vcon); + return false; + } + + if (!vcon_validate_guest_range(desc->addr, desc->len)) { + virtio_console_set_fail(vcon); + return false; + } + + buf = (const void *) ((uintptr_t) vcon->ram + (uintptr_t) desc->addr); + if (!vcon_write_all(vcon->out_fd, buf, desc->len)) { + fprintf(stderr, "failed to write virtio-console output\n"); + virtio_console_set_fail(vcon); + return false; + } + total_len += desc->len; + + if (!(desc->flags & VIRTIO_DESC_F_NEXT)) { + saw_end = true; + break; + } + desc_idx = desc->next; + } + + if (!saw_end) { + virtio_console_set_fail(vcon); + return false; + } + + vcon_push_used(vcon, queue, head, total_len); + + avail = ram[queue->QueueAvail] >> 16; + } + + return true; +} + +void virtio_console_refresh_rx(virtio_console_state_t *vcon) +{ + virtio_console_queue_t *queue = &vcon->queues[VCON_QUEUE_RX]; + uint32_t *ram = vcon->ram; + uint16_t avail; + + if (!queue->ready || !(vcon->Status & VIRTIO_STATUS__DRIVER_OK)) + return; + if (!vcon_fd_ready(vcon->in_fd)) + return; + + avail = ram[queue->QueueAvail] >> 16; + while (queue->last_avail != avail && vcon_fd_ready(vcon->in_fd)) { + uint16_t slot = queue->last_avail % queue->QueueNum; + uint16_t head = vcon_read_le16(ram, queue->QueueAvail, slot); + uint16_t desc_idx = head; + uint32_t total_len = 0; + + for (uint16_t steps = 0; steps < queue->QueueNum; steps++) { + struct virtq_desc *desc; + uint8_t *buf; + ssize_t nread; + + if (desc_idx >= queue->QueueNum) { + virtio_console_set_fail(vcon); + return; + } + + desc = (struct virtq_desc *) &ram[queue->QueueDesc + desc_idx * 4]; + if (!(desc->flags & VIRTIO_DESC_F_WRITE)) { + virtio_console_set_fail(vcon); + return; + } + if (!vcon_validate_guest_range(desc->addr, desc->len)) { + virtio_console_set_fail(vcon); + return; + } + + buf = (uint8_t *) ((uintptr_t) vcon->ram + (uintptr_t) desc->addr); + nread = read(vcon->in_fd, buf, desc->len); + if (nread < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) + return; + virtio_console_set_fail(vcon); + return; + } + if (nread == 0) + return; + total_len += (uint32_t) nread; + + if (total_len || !(desc->flags & VIRTIO_DESC_F_NEXT)) + break; + desc_idx = desc->next; + } + + queue->last_avail++; + vcon_push_used(vcon, queue, head, total_len); + + avail = ram[queue->QueueAvail] >> 16; + } +} + +static bool virtio_console_reg_read(virtio_console_state_t *vcon, + uint32_t addr, + uint32_t *value) +{ +#define _(reg) VIRTIO_##reg + switch (addr >> 2) { + 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->QueueSel < ARRAY_SIZE(vcon->queues) ? VCON_QUEUE_NUM_MAX + : 0; + return true; + case _(QueueReady): + *value = VCON_QUEUE.ready ? 1 : 0; + 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 >> 2) { + 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 (value > 0 && value <= VCON_QUEUE_NUM_MAX) + VCON_QUEUE.QueueNum = value; + else + virtio_console_set_fail(vcon); + return true; + case _(QueueReady): + VCON_QUEUE.ready = value & 1; + if (value & 1) + VCON_QUEUE.last_avail = vcon->ram[VCON_QUEUE.QueueAvail] >> 16; + return true; + case _(QueueDescLow): + VCON_QUEUE.QueueDesc = vcon_preprocess(vcon, value); + return true; + case _(QueueDescHigh): + if (value) + virtio_console_set_fail(vcon); + return true; + case _(QueueDriverLow): + VCON_QUEUE.QueueAvail = vcon_preprocess(vcon, value); + return true; + case _(QueueDriverHigh): + if (value) + virtio_console_set_fail(vcon); + return true; + case _(QueueDeviceLow): + VCON_QUEUE.QueueUsed = vcon_preprocess(vcon, value); + return true; + case _(QueueDeviceHigh): + if (value) + virtio_console_set_fail(vcon); + return true; + case _(QueueNotify): + if (value == VCON_QUEUE_TX) + return vcon_process_tx(vcon, &vcon->queues[VCON_QUEUE_TX]); + if (value == VCON_QUEUE_RX) + return true; + virtio_console_set_fail(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, value)) + vm_set_exception(vm, RV_EXC_LOAD_FAULT, vm->exc_val); + break; + 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); + return; + } +} + +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, value)) + vm_set_exception(vm, RV_EXC_STORE_FAULT, vm->exc_val); + break; + 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); + return; + } +} + +void virtio_console_init(virtio_console_state_t *vcon) +{ + (void) vcon; +}