Files
LithosAnanake/kernel/src/hal/console.c
T
rajamesandClaude Opus 5.5 cab0ffb487 feat(v4): the screen and the keyboards
The stick, increment 1, task 2 (plan 2026-10-26-stick-1-screen-and-keyboard;
docs/v4.0.0/STICK.md 2, 5.3, 5.4). On the v4 path:
- the framebuffer is brought up after POST, cleared and dark: console text
  goes to serial only ("splash only, log on serial") until Zuse logs in;
  then the screen is the console (TTF, a blinking cursor, as v3's), and
  when she logs out it is cleared and dark again (console_fb_mirror(),
  console_fb_clear(); the mirror is on by default, so v3 is unchanged);
- the line editor and the node's KEY read every keyboard -- PS/2, USB, the
  virtio keyboard -- through the shared bridge (sk_input_getc()); the
  headless wait drops keyboard input as well as serial bytes;
- an EMERGENCY_CONSOLE_ENABLED build has the screen from the start.
The framebuffer setup is one helper both paths call (kernel_main.c).

Red (before): screen_dark_until_login 153051 (10835 lit), console/keys
153309, logout 153609, kbd_typeahead 153918, virtio_kbd aarch64 154245
and riscv64 154718; no_framebuffer 155546 passed before and after.
Green: 155926 (0 lit), 160156, 160409 (0 lit after logout), 160630,
virtio_kbd 161414 and 161820, no_framebuffer 161206; usb_kbd with the USB
keyboard the only one (-machine i8042=off) 164548, v3 control 164757;
the identity gate's sessions 162245-163802 all pass; v3 typed by keyboard
164009, 164115. (usb_kbd 160857 and 164313 failed in the harness:
input-send-event's device names a display, not a keyboard.)
Accepted: suite EXIT 0; three clean qemu boots, POST 538/538, dict_hash
0xfec8c47cfc6f437f unchanged (174704, 174911, 175315); test-32, 39
modules, 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:02:27 -04:00

517 lines
17 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_puts("\x1b[2J\x1b[H");
}
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)
{
if (fb_is_available()) {
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);
}