Files
LithosAnanake/kernel/src/hal/console.c
T
rajamesandClaude Opus 5.5 a78cc56c31 fix(v4): nothing of a session left on screen after logout
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>
2026-10-26 19:14:22 -04:00

520 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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);
}