From the fresh review of the stick's increment 1 (With fixes): - Critical: after logout Zuse's last screen, her prompt included, could be drawn back on a headless machine. The logout clear (ESC[2J) blanked the pixels only: Alt+TAB, still read while headless, redrew the terminal from its saved text; and if she had Alt+TABbed to the graphics view before the pull, the clear did nothing and her session stayed on screen (and the console did not draw at her next login). Now vt100_reset() forgets everything the terminal has shown -- the on-screen text, the scrollback and their boot snapshots -- makes it visible again and fills the screen; and Alt+TAB does nothing while the screen is off (v3's never is, so v3 is unchanged). - Important: an EMERGENCY_CONSOLE_ENABLED build no longer clears the screen when Zuse's drive is pulled: its console goes on running. Red: alttab_after_logout 183308 (4515 lit), alttab_before_pull 183529 (4539 lit; the console not redrawn), emergency_pull 183759 (0 lit). Green: 184115, 184328, 184552; regression 184805-185956 (screens, keyboard, hotplug, emergency, v3 typed by keyboard) all pass. Accepted: 190111, 190312, 190710, POST 538/538, dict_hash 0xfec8c47cfc6f437f; test-32, 39 modules, 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
520 lines
18 KiB
C
520 lines
18 KiB
C
/*
|
||
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);
|
||
}
|