/* 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" #include "starkernel/fdt.h" #include "starkernel/fdt_uart.h" /* THE SERIAL LINE. A board has one serial line that is its console, the * lowest common denominator: everything printed goes out on it and what is * typed on it comes in, beside the screen and the keyboard where a board * has those. Which line it is, is found once, by console_attach_serial(): * on a PC, COM1 if a UART answers there; elsewhere the UART the board's * devicetree names as its console (fdt_uart.h), which the firmware has set * up; and with no devicetree at all -- QEMU under UEFI firmware -- the UART * of QEMU's virt machine. Until it is found, and on a board that has none, * there is no line: nothing is written anywhere and nothing is read. * * Before this the addresses were QEMU's, fixed. On a Raspberry Pi 5 the * aarch64 one is ordinary RAM, and on a Milk-V Mars the riscv64 one is the * board's UART with its registers laid out otherwise (boards/BRINGUP.md). */ enum { SERIAL_NONE = 0, SERIAL_PORT16550, SERIAL_MMIO16550, SERIAL_PL011 }; static int g_serial_kind; /* SERIAL_NONE until console_attach_serial() */ static uintptr_t g_serial_base; /* register 0: an I/O port, or an address */ static unsigned g_serial_shift; /* register n is at base + (n << shift) */ static unsigned g_serial_width; /* bytes at a time, by memory: 1 or 4 */ static int g_serial_from_dt; /* the devicetree named it */ /* How long to wait for the line to take a character before giving the line * up for dead. At 115200 a character takes 87 microseconds; this is many * thousand times that on any of the boards. A console that cannot print * must not stop the boot. */ #define SERIAL_TX_MAX_SPIN 4000000u /* 16550: transmit and receive are register 0, line status is register 5 */ #define UART16550_LSR 5u #define UART16550_LSR_DR (1u << 0) #define UART16550_LSR_THRE (1u << 5) /* PL011: data is register 0, flags are register 6 (offset 0x18) */ #define PL011_FR 6u #define PL011_FR_RXFE (1u << 4) #define PL011_FR_TXFF (1u << 5) #if defined(__x86_64__) || defined(__i386__) #define COM1_PORT 0x3F8 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; } #endif static uint32_t serial_reg_read(unsigned n) { #if defined(__x86_64__) || defined(__i386__) if (g_serial_kind == SERIAL_PORT16550) return inb((uint16_t)(g_serial_base + n)); #endif if (g_serial_width == 4) return *(volatile uint32_t *)(g_serial_base + ((uintptr_t)n << g_serial_shift)); return *(volatile uint8_t *)(g_serial_base + ((uintptr_t)n << g_serial_shift)); } static void serial_reg_write(unsigned n, uint32_t v) { #if defined(__x86_64__) || defined(__i386__) if (g_serial_kind == SERIAL_PORT16550) { outb((uint16_t)(g_serial_base + n), (uint8_t)v); return; } #endif if (g_serial_width == 4) *(volatile uint32_t *)(g_serial_base + ((uintptr_t)n << g_serial_shift)) = v; else *(volatile uint8_t *)(g_serial_base + ((uintptr_t)n << g_serial_shift)) = (uint8_t)v; } static int serial_tx_ready(void) { if (g_serial_kind == SERIAL_PL011) return !(serial_reg_read(PL011_FR) & PL011_FR_TXFF); return (serial_reg_read(UART16550_LSR) & UART16550_LSR_THRE) != 0; } static int serial_rx_ready(void) { if (g_serial_kind == SERIAL_NONE) return 0; if (g_serial_kind == SERIAL_PL011) return !(serial_reg_read(PL011_FR) & PL011_FR_RXFE); return (serial_reg_read(UART16550_LSR) & UART16550_LSR_DR) != 0; } /* One character out on the line, if there is one. A line that does not * come ready in SERIAL_TX_MAX_SPIN tries is given up: from then on there is * no line, and nothing waits on it again. */ static void serial_putc(char c) { unsigned spin = 0; if (g_serial_kind == SERIAL_NONE) return; while (!serial_tx_ready()) { if (++spin >= SERIAL_TX_MAX_SPIN) { g_serial_kind = SERIAL_NONE; return; } arch_relax(); } serial_reg_write(0, (uint8_t)c); } void console_attach_serial(const void *dtb) { g_serial_kind = SERIAL_NONE; g_serial_from_dt = 0; #if defined(__x86_64__) || defined(__i386__) (void)dtb; /* COM1, if a UART answers there: its scratch register gives back what * was written to it. A port no UART owns reads all ones, which would * otherwise look like a character waiting, for ever. The loader makes * the same probe (boot/uefi_loader.c, raw_serial_init). */ outb(COM1_PORT + 7, 0x5A); if (inb(COM1_PORT + 7) != 0x5A) return; outb(COM1_PORT + 7, 0xA5); if (inb(COM1_PORT + 7) != 0xA5) return; g_serial_base = COM1_PORT; g_serial_shift = 0; g_serial_width = 1; g_serial_kind = SERIAL_PORT16550; #elif defined(__aarch64__) || defined(__riscv) { fdt_uart_t u; if (fdt_console_uart(dtb, &u)) { g_serial_base = (uintptr_t)u.base; g_serial_shift = u.reg_shift; g_serial_width = u.io_width; g_serial_kind = (u.kind == FDT_UART_PL011) ? SERIAL_PL011 : SERIAL_MMIO16550; g_serial_from_dt = 1; return; } /* A devicetree that names no console this can use: no line. It is * a real board's, and QEMU's addresses are not to be tried on it. */ if (fdt_valid(dtb)) return; } /* No devicetree at all: QEMU's virt machine under UEFI firmware, which * has set its UART up. */ #if defined(__aarch64__) g_serial_base = 0x09000000UL; g_serial_shift = 2; g_serial_width = 4; g_serial_kind = SERIAL_PL011; #else g_serial_base = 0x10000000UL; g_serial_shift = 0; g_serial_width = 1; g_serial_kind = SERIAL_MMIO16550; #endif #else (void)dtb; #endif } /** * Initialize serial console * x86: programs the 16550 at COM1, if console_attach_serial() found one. * aarch64, riscv64: the line was set up by the firmware; nothing to do. */ void console_init(void) { #if defined(__x86_64__) || defined(__i386__) if (g_serial_kind != SERIAL_PORT16550) return; outb(COM1_PORT + 1, 0x00); outb(COM1_PORT + 3, 0x80); outb(COM1_PORT + 0, 0x01); outb(COM1_PORT + 1, 0x00); outb(COM1_PORT + 3, 0x03); outb(COM1_PORT + 2, 0xC7); outb(COM1_PORT + 4, 0x0B); outb(COM1_PORT + 4, 0x1E); outb(COM1_PORT + 0, 0xAE); if (inb(COM1_PORT + 0) != 0xAE) { g_serial_kind = SERIAL_NONE; /* it does not loop back: not a UART to be trusted */ return; } outb(COM1_PORT + 4, 0x0F); #endif } void console_describe_serial(void) { static const char hex[] = "0123456789abcdef"; char buf[19]; int i; uint64_t v = (uint64_t)g_serial_base; if (g_serial_kind == SERIAL_NONE) { console_println("Console: no serial line"); return; } buf[0] = '0'; buf[1] = 'x'; for (i = 0; i < 16; i++) buf[2 + i] = hex[(v >> (60 - 4 * i)) & 0xF]; buf[18] = '\0'; console_puts("Console: serial line is a "); console_puts(g_serial_kind == SERIAL_PL011 ? "PL011" : "16550"); console_puts(g_serial_kind == SERIAL_PORT16550 ? " at port " : " at "); console_puts(buf); console_println(g_serial_kind == SERIAL_PORT16550 ? " (COM1, probed)" : g_serial_from_dt ? " (named by the devicetree)" : " (no devicetree: QEMU's)"); } /* 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-2.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; /* Whether console text is drawn on the framebuffer as well as sent to the * serial line. On unless a boot turns it off: v3's console mirrors from * console_fb_init() on, as always; the v4 boot keeps the screen dark until * Zuse logs in ("splash only, log on serial", docs/v4.0.0/STICK.md 2). */ static int g_fb_mirror = 1; static int fb_mirrored(void) { return g_fb_mirror && fb_is_available(); } void console_fb_mirror(int on) { g_fb_mirror = on ? 1 : 0; } void console_fb_clear(void) { if (fb_is_available()) vt100_reset(); } static uint64_t g_console_tx_count; /* monotonic console_putc() counter (BSS) */ 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; } /* FABRIC-3.md SXXV follow-up: see console.h's own doc comment on * console_set_user_prefix_provider() for why this is a registered * callback rather than console.c calling into capsule/WIREBIND logic * directly. NULL (the default, until repl.c registers one at boot) means * "no provider yet" -- emit_prefix() falls back to the bare "[VMName] " * form, identical to today's behavior. */ static console_user_prefix_fn g_user_prefix_fn = (void *)0; void console_set_user_prefix_provider(console_user_prefix_fn fn) { g_user_prefix_fn = fn; } 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 (c == '\n') serial_putc('\r'); serial_putc(c); } /* 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-0.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_mirrored(); static const char *color_on = "\x1b[38;2;255;165;0m"; static const char *color_off = "\x1b[39m"; const char *user = g_user_prefix_fn ? g_user_prefix_fn() : (void *)0; for (p = color_on; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); } raw_putc('['); if (fb) vt100_putc('['); if (user && user[0]) { for (p = user; *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. * * console_putc_inner(): the same body minus the tx-counter increment -- * used internally for the newline console_putc() itself realizes out of * g_pending_line_close below (see console_ensure_line_start()), so that * one real character write emits at most one counted "start" (the '\n' * plus the character together read as a single unit of real output, not * two, to console_tx_count()'s callers). */ static void console_putc_inner(char c); /* FABRIC-2.md §I.9 fix, 2026-09-05: set by console_ensure_line_start(), * cleared either by console_cancel_deferred_line_start() (no output * followed -- drop the newline silently) or here in console_putc() (real * output followed -- realize it for real, counted, immediately before the * character that triggered it). See console.h's doc comments on both * functions for the full rationale; the short version is that the old * unconditional-and-immediate version of this newline fired every idle * heartbeat regardless of whether anything was actually about to be * printed, visibly snapping a bare REPL prompt to a fresh blank line on * its own. */ static int g_pending_line_close = 0; void console_putc(char c) { if (g_pending_line_close) { g_pending_line_close = 0; g_console_tx_count++; console_putc_inner('\n'); } g_console_tx_count++; console_putc_inner(c); } static void console_putc_inner(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, and mirrored) --- */ if (fb_mirrored()) { 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++); } } uint64_t console_tx_count(void) { return g_console_tx_count; } void console_ensure_line_start(void) { if (!g_line_start) { g_pending_line_close = 1; } } void console_cancel_deferred_line_start(void) { g_pending_line_close = 0; } /** * 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) { return serial_rx_ready(); } /** * 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) { /* Not while the screen is off (the v4 boot, headless): Alt+TAB would * draw the terminal back (fixed 2026-10-26). v3's is always on. */ if (fb_mirrored()) { vt100_toggle_graphics(); } } void console_fb_draw_cursor(void) { if (fb_mirrored()) { vt100_draw_cursor(); } } void console_fb_erase_cursor(void) { if (fb_mirrored()) { vt100_erase_cursor(); } } /** * Read a single character from serial console (non-blocking) * Returns -1 if no character available */ int console_getc(void) { if (!serial_rx_ready()) return -1; return (int)(serial_reg_read(0) & 0xFF); }