/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * console.c - Serial console implementation (UART 16550) * Supports amd64 via I/O ports */ #include "console.h" #include "framebuffer.h" #include "vt100.h" #include "arch.h" #if defined(__x86_64__) || defined(__i386__) #define SERIAL_SUPPORTED 1 /* UART 16550 I/O ports (COM1) */ #define SERIAL_PORT_BASE 0x3F8 #define SERIAL_DATA_PORT (SERIAL_PORT_BASE + 0) #define SERIAL_INT_ENABLE_PORT (SERIAL_PORT_BASE + 1) #define SERIAL_FIFO_CTRL_PORT (SERIAL_PORT_BASE + 2) #define SERIAL_LINE_CTRL_PORT (SERIAL_PORT_BASE + 3) #define SERIAL_MODEM_CTRL_PORT (SERIAL_PORT_BASE + 4) #define SERIAL_LINE_STATUS_PORT (SERIAL_PORT_BASE + 5) /* Line Status Register bits */ #define SERIAL_LSR_DATA_READY (1 << 0) #define SERIAL_LSR_THR_EMPTY (1 << 5) static inline void outb(uint16_t port, uint8_t val) { __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port)); } static inline uint8_t inb(uint16_t port) { uint8_t ret; __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port)); return ret; } #elif defined(__aarch64__) #define SERIAL_SUPPORTED 1 /* PL011 UART — QEMU virt machine UART0 at 0x09000000 */ #define PL011_BASE ((volatile unsigned int *)0x09000000UL) #define PL011_DR (PL011_BASE + 0) /* Data Register (offset 0x000) */ #define PL011_FR (PL011_BASE + 6) /* Flag Register (offset 0x018) */ #define PL011_FR_TXFF (1u << 5) /* TX FIFO full */ static inline void pl011_putc(char c) { while (*PL011_FR & PL011_FR_TXFF) { } *PL011_DR = (unsigned int)(unsigned char)c; } #elif defined(__riscv) #define SERIAL_SUPPORTED 1 /* NS16550 UART — QEMU virt machine UART0 at 0x10000000 (MMIO, byte-wide) */ #define NS16550_BASE ((volatile unsigned char *)0x10000000UL) #define NS16550_THR (NS16550_BASE + 0) /* Transmit Holding Register */ #define NS16550_LSR (NS16550_BASE + 5) /* Line Status Register */ #define NS16550_LSR_THRE (1u << 5) /* Transmit Holding Register Empty */ #define NS16550_LSR_DR (1u << 0) /* Data Ready */ static inline void ns16550_putc(char c) { while (!(*NS16550_LSR & NS16550_LSR_THRE)) { } *NS16550_THR = (unsigned char)c; } #else #define SERIAL_SUPPORTED 0 /* Stubs for other architectures */ static inline void outb(uint16_t port, uint8_t val) { (void)port; (void)val; } static inline uint8_t inb(uint16_t port) { (void)port; return 0; } #endif /** * Initialize serial console * x86: programs 16550 UART. * aarch64: PL011 is already initialized by UEFI firmware on QEMU virt. */ void console_init(void) { #if defined(__x86_64__) || defined(__i386__) outb(SERIAL_INT_ENABLE_PORT, 0x00); outb(SERIAL_LINE_CTRL_PORT, 0x80); outb(SERIAL_DATA_PORT, 0x01); outb(SERIAL_INT_ENABLE_PORT, 0x00); outb(SERIAL_LINE_CTRL_PORT, 0x03); outb(SERIAL_FIFO_CTRL_PORT, 0xC7); outb(SERIAL_MODEM_CTRL_PORT, 0x0B); outb(SERIAL_MODEM_CTRL_PORT, 0x1E); outb(SERIAL_DATA_PORT, 0xAE); if (inb(SERIAL_DATA_PORT) != 0xAE) { return; } outb(SERIAL_MODEM_CTRL_PORT, 0x0F); #elif defined(__aarch64__) /* PL011 already enabled by AAVMF; nothing to do */ (void)0; #elif defined(__riscv) /* NS16550 already enabled by UEFI firmware on QEMU virt; nothing to do */ (void)0; #else (void)SERIAL_SUPPORTED; #endif } #if defined(__x86_64__) || defined(__i386__) static int serial_transmit_empty(void) { return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_THR_EMPTY; } #endif /* Active VM name for [Name] line prefix; NULL = no prefix. * * g_active_vm_name_buf owns the storage -- console_set_vm_name() copies * into it rather than storing the caller's own pointer. Found live * 2026-08-28 (FABRIC-3.md Phase F): mama_word_use() (USE) passes * entry.name, a local VMRegistryEntry's own field -- once USE returns, * that stack frame is reused and the old raw-pointer version left * g_active_vm_name dangling, corrupting every console tag after the * first USE (observed as garbled "[[]"/binary-looking prefixes). A * caller passing a string literal (e.g. console_set_vm_name("Hermes")) * was always safe; this fixes every caller uniformly instead of relying * on each one happening to pass static storage. */ #define CONSOLE_VM_NAME_BUF 64 static char g_active_vm_name_buf[CONSOLE_VM_NAME_BUF]; static const char *g_active_vm_name = (void *)0; static int g_line_start = 1; void console_set_vm_name(const char *name) { /* Empty string treated the same as NULL: console_save_vm_name() * writes "" for "there was no active name," so this keeps that * round-trip correct (save-empty then restore-empty must mean * "still no prefix," not "prefix is now the empty string"). */ if (!name || !name[0]) { g_active_vm_name = (void *)0; return; } size_t i; for (i = 0; i < CONSOLE_VM_NAME_BUF - 1u && name[i]; i++) g_active_vm_name_buf[i] = name[i]; g_active_vm_name_buf[i] = '\0'; g_active_vm_name = g_active_vm_name_buf; } const char *console_get_vm_name(void) { return g_active_vm_name; } void console_save_vm_name(char *out, size_t cap) { if (!out || cap == 0) return; size_t i = 0; if (g_active_vm_name) { for (; i < cap - 1u && g_active_vm_name[i]; i++) out[i] = g_active_vm_name[i]; } out[i] = '\0'; } /* Raw single-character write — no prefix logic, called by emit_prefix() */ static void raw_putc(char c) { #if defined(__aarch64__) if (c == '\n') pl011_putc('\r'); pl011_putc(c); #elif defined(__riscv) if (c == '\n') ns16550_putc('\r'); ns16550_putc(c); #elif defined(__x86_64__) || defined(__i386__) if (c == '\n') { while (!serial_transmit_empty()) { arch_relax(); } outb(SERIAL_DATA_PORT, '\r'); } while (!serial_transmit_empty()) { arch_relax(); } outb(SERIAL_DATA_PORT, (uint8_t)c); #else (void)c; #endif } /* Emit "[VMName] " to both serial (raw_putc) and, when available, the * framebuffer (vt100_putc) -- mirrors console_putc()'s own serial/framebuffer * split so the prefix reaches both outputs, not serial only. No recursion * into console_putc itself (would re-trigger the line-start prefix check). */ /* FABRIC.md 4.4: the bracketed VM name (brackets included) renders in * standard web orange, 0xFFA500 -- not in the classic 16-color ANSI * palette, so sent as a literal 24-bit SGR sequence rather than a palette * index. Same dual serial+framebuffer send pattern as the rest of this * function. */ static void emit_prefix(void) { const char *p; int fb = fb_is_available(); static const char *color_on = "\x1b[38;2;255;165;0m"; static const char *color_off = "\x1b[39m"; for (p = color_on; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); } raw_putc('['); if (fb) vt100_putc('['); for (p = g_active_vm_name; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); } raw_putc(']'); if (fb) vt100_putc(']'); for (p = color_off; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); } raw_putc(' '); if (fb) vt100_putc(' '); } /** * Write a single character to serial console. * Emits "[VMName] " at the start of each new line when a VM name is set. * Also mirrors output to the framebuffer VT100 terminal when available. * Serial output is ALWAYS active regardless of framebuffer state. */ void console_putc(char c) { /* --- serial UART path (always on) --- */ if (g_active_vm_name && g_line_start && c != '\n') { emit_prefix(); g_line_start = 0; } raw_putc(c); if (c == '\n') { g_line_start = 1; } /* --- framebuffer VT100 path (when available) --- */ if (fb_is_available()) { if (c == '\n') vt100_putc('\r'); vt100_putc(c); } } /** * Write a null-terminated string to serial console */ void console_puts(const char *s) { if (!s) return; while (*s) { console_putc(*s++); } } /** * Write a string with newline to serial console */ void console_println(const char *s) { console_puts(s); console_putc('\n'); } /** * Check if data is available to read */ int console_poll(void) { #if defined(__x86_64__) || defined(__i386__) return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_DATA_READY; #elif defined(__aarch64__) /* PL011_FR bit 4 = RXFE (RX FIFO empty); data available when RXFE == 0 */ #define PL011_FR_RXFE (1u << 4) return !(*PL011_FR & PL011_FR_RXFE); #elif defined(__riscv) return *NS16550_LSR & NS16550_LSR_DR; #else return 0; #endif } /** * Initialize the framebuffer VT100 terminal and attach it to the console. * Safe to call with info == NULL or when the framebuffer base is zero (no-op). * After this call every console_putc / console_puts output is mirrored to * the screen; the serial UART continues to function concurrently. */ void console_fb_init(const FramebufferInfo *info, FbPixelFormat fmt) { if (!info || !info->base) return; fb_init(info, fmt); if (fb_is_available()) { vt100_init(); } } void console_fb_enable_ttf(void) { if (fb_is_available()) { vt100_enable_ttf(); } } void console_fb_scroll_back(uint32_t n) { if (fb_is_available()) { vt100_scroll_back(n); } } void console_fb_scroll_fwd(uint32_t n) { if (fb_is_available()) { vt100_scroll_fwd(n); } } void console_fb_toggle_graphics(void) { if (fb_is_available()) { vt100_toggle_graphics(); } } void console_fb_draw_cursor(void) { if (fb_is_available()) { vt100_draw_cursor(); } } void console_fb_erase_cursor(void) { if (fb_is_available()) { vt100_erase_cursor(); } } /** * Read a single character from serial console (non-blocking) * Returns -1 if no character available */ int console_getc(void) { #if defined(__x86_64__) || defined(__i386__) if (!console_poll()) return -1; return (int)(unsigned char)inb(SERIAL_DATA_PORT); #elif defined(__aarch64__) if (!console_poll()) return -1; return (int)(*PL011_DR & 0xFF); #elif defined(__riscv) if (!console_poll()) return -1; return (int)(*NS16550_THR & 0xFF); #else return -1; #endif }