diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index eecf0a3..d5b81c6 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -211,7 +211,7 @@ Output: `build//kernel/starkernel_loader.efi` + `build//kernel/stark Two independently tracked version strings flow into the generated `include/version.h`: `VERSION` (`Makefile.starkernel` — the embedded StarForth engine version, currently `3.1.0`; note this does **not** auto-sync with the standalone StarForth repo's own version) and -`LITHOS_VERSION` (`Makefile.starkernel` — the kernel version, currently `2.0.0`). +`LITHOS_VERSION` (`Makefile.starkernel` — the kernel version, currently `2.0.1`). ### Build configuration (Kconfig — real, wired, not vestigial) diff --git a/Makefile.starkernel b/Makefile.starkernel index 8faac39..e936fa3 100644 --- a/Makefile.starkernel +++ b/Makefile.starkernel @@ -76,9 +76,11 @@ endif # v1.0.x — serial-only production (released) # v1.5.x — framebuffer VT100 terminal/console milestone (released) # v2.0.0 — QEMU release (even major = LTS): three-arch QEMU story complete +# v2.0.1 — SER5 hardware-track line: RDRAND backend + generic thumbdrive image goal +# v2.2.0 — amd64 bare-metal (Beelink SER5) — see ROADMAP "Board-by-board rollout" # v2.5.0 — hardware bare-metal release: real per-arch RNG + real-board boot VERSION ?= 3.1.0 -LITHOS_VERSION ?= 2.0.0 +LITHOS_VERSION ?= 2.0.1 # ============================================================================== # BUILD PATHS @@ -556,7 +558,7 @@ KERNEL_OBJS := \ .PHONY: all clean clean-kernel kernel kernel-all .PHONY: qemu qemu-esp qemu-gdb -.PHONY: thumbdrive +.PHONY: thumbdrive iso-usb .PHONY: info help # ============================================================================== @@ -1022,16 +1024,23 @@ thumbdrive: all riscv64) BOOTNAME="BOOTRISCV64.EFI" ;; \ *) echo "Error: no thumbdrive EFI boot name for ARCH=$(ARCH)"; exit 1 ;; \ esac; \ + # Geometry: 128 MiB disk (262144 sectors), GPT partition 1 spans sectors \ + # 2048..262110 (the GPT last-usable sector for this disk size), i.e. \ + # 260063 sectors. The FAT32 ESP image MUST match the partition size \ + # exactly; a larger ESP overruns the disk (GPT grows corrupt) or \ + # spills past the partition end, both of which made earlier builds \ + # unbootable on hardware. \ DISK=$(BUILD_DIR)/starkernel-thumbdrive.img; \ + ESP_SECTORS=260063; \ rm -f $$DISK $(BUILD_DIR)/thumbdrive-esp.img; \ - dd if=/dev/zero of=$(BUILD_DIR)/thumbdrive-esp.img bs=512 count=131072 2>/dev/null; \ + dd if=/dev/zero of=$(BUILD_DIR)/thumbdrive-esp.img bs=512 count=$$ESP_SECTORS 2>/dev/null; \ mkfs.fat -F 32 -n "STARKERNEL" $(BUILD_DIR)/thumbdrive-esp.img >/dev/null 2>&1; \ mmd -i $(BUILD_DIR)/thumbdrive-esp.img ::/EFI; \ mmd -i $(BUILD_DIR)/thumbdrive-esp.img ::/EFI/BOOT; \ mcopy -i $(BUILD_DIR)/thumbdrive-esp.img $(LOADER_EFI) ::/EFI/BOOT/$$BOOTNAME; \ printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$$BOOTNAME" > $(BUILD_DIR)/thumbdrive-startup.nsh; \ mcopy -i $(BUILD_DIR)/thumbdrive-esp.img $(BUILD_DIR)/thumbdrive-startup.nsh ::/startup.nsh; \ - dd if=/dev/zero of=$$DISK bs=1M count=64 2>/dev/null; \ + dd if=/dev/zero of=$$DISK bs=1M count=128 2>/dev/null; \ sgdisk -n 1:2048:0 -t 1:ef00 -c 1:"EFI System" $$DISK >/dev/null 2>&1; \ dd if=$(BUILD_DIR)/thumbdrive-esp.img of=$$DISK bs=512 seek=2048 conv=notrunc 2>/dev/null; \ rm -f $(BUILD_DIR)/thumbdrive-esp.img $(BUILD_DIR)/thumbdrive-startup.nsh; \ @@ -1039,6 +1048,46 @@ thumbdrive: all echo "Write to a USB stick with: dd if=$$DISK of=/dev/sdX bs=4M status=progress" +# iso-usb — build a PURE UEFI isohybrid ISO for the Iso Image Writer workflow +# (GNOME Disks "Restore Disk Image...", or dd). Writes a single .iso to a USB +# stick as a raw image; the ISO carries a clean GPT with an EFI System +# Partition holding EFI/BOOT/BOOT.EFI, so real UEFI firmware (e.g. +# Beelink SER5) scans the ESP and boots it. No GRUB, no isolinux, no MBR boot +# code — the protective MBR template is zeroed (partition metadata only). +# Also still boots under QEMU via -cdrom. This is the novice path: one ISO, +# picked with a GUI, written to the stick. +iso-usb: all + @mkdir -p $(BUILD_DIR) + @if ! which xorriso >/dev/null 2>&1; then echo "Error: xorriso not found. Install xorriso (apt-get install xorriso)."; exit 1; fi + @if ! which mformat >/dev/null 2>&1 || ! which mmd >/dev/null 2>&1 || ! which mcopy >/dev/null 2>&1; then echo "Error: mtools not found. Install mtools (apt-get install mtools)."; exit 1; fi + @case "$(ARCH)" in \ + amd64) BOOTNAME="BOOTX64.EFI" ;; \ + aarch64) BOOTNAME="BOOTAA64.EFI" ;; \ + *) echo "Error: iso-usb EFI boot name only defined for amd64/aarch64 yet"; exit 1 ;; \ + esac; \ + ISODIR=$(BUILD_DIR)/iso-usb; \ + rm -rf $$ISODIR; mkdir -p $$ISODIR; \ + dd if=/dev/zero of=$$ISODIR/efi.img bs=512 count=8192 2>/dev/null; \ + mformat -i $$ISODIR/efi.img :: >/dev/null 2>&1; \ + mmd -i $$ISODIR/efi.img ::/EFI; mmd -i $$ISODIR/efi.img ::/EFI/BOOT; \ + mcopy -i $$ISODIR/efi.img $(LOADER_EFI) ::/EFI/BOOT/$$BOOTNAME; \ + printf 'FS0:\\EFI\\BOOT\\%s\r\n' "$$BOOTNAME" > $$ISODIR/startup.nsh; \ + mcopy -i $$ISODIR/efi.img $$ISODIR/startup.nsh ::/startup.nsh; \ + head -c 432 /dev/zero > $$ISODIR/protmbr.bin; \ + ISO=$(BUILD_DIR)/starkernel-iso-usb.iso; \ + rm -f $$ISO; \ + xorriso -as mkisofs \ + -V STARKERNEL -r -J \ + -isohybrid-mbr $$ISODIR/protmbr.bin \ + -eltorito-alt-boot -e efi.img -no-emul-boot \ + -isohybrid-gpt-basdat \ + -o $$ISO $$ISODIR >/dev/null 2>&1; \ + rm -rf $$ISODIR; \ + echo "Iso Image Writer ISO (pure UEFI): $$ISO"; \ + echo " In GNOME Disks, pick the ISO with 'Restore Disk Image...' and select your USB stick."; \ + echo " Or: dd if=$$ISO of=/dev/sdX bs=4M status=progress" + + # qemu-esp — interactive dev boot from FAT directory (no disk image rebuild, # no auto-kill/timeout/DOE-injection logic — stays up until you quit it # yourself with Ctrl-A X or by closing the window). @@ -1172,6 +1221,7 @@ help: @echo " qemu-esp — quick boot from FAT directory (amd64, aarch64)" @echo " qemu-gdb — boot with GDB stub on :1234" @echo " thumbdrive — build generic GPT/FAT32 UEFI disk image (write to a USB stick with dd; real-hardware/SER5 path)" + @echo " iso-usb — build UEFI isohybrid ISO for Iso Image Writer / GNOME Disks 'Restore Disk Image...' (novice path; real-hardware/SER5)" @echo "" @echo "QEMU display:" @echo " QEMU_DISPLAY=gtk — framebuffer window backend for 'qemu' goal (default: gtk)" diff --git a/README.md b/README.md index 1b228b6..f3d8355 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LithosAnanke v2.0.0 +# LithosAnanke v2.0.1 **UEFI-bootable FORTH microkernel.** Boots from firmware, initialises memory and interrupts, then runs the StarForth VM as its sole userspace runtime. No libc. No OS. Just stone and necessity. diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index 897edf1..7e3911d 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,6 @@ # Capsule Block Manifest — Auto-generated - + + diff --git a/disk/artemis.img b/disk/artemis.img index 263040c..0ab09a5 100644 Binary files a/disk/artemis.img and b/disk/artemis.img differ diff --git a/include/starkernel/console.h b/include/starkernel/console.h index aa3f45f..071fb9a 100644 --- a/include/starkernel/console.h +++ b/include/starkernel/console.h @@ -128,6 +128,24 @@ void console_puts(const char *s); */ void console_println(const char *s); +/** + * console_ensure_line_start - Make sure the next console_putc() starts at + * the beginning of a fresh output line, emitting a newline if output is + * currently mid-line (e.g. dangling behind a re-anchored prompt). + * No-op if already at line start. Mirrors console_putc()'s dual + * serial+framebuffer behavior (a bare '\n' reaches both). + */ +void console_ensure_line_start(void); + +/** + * console_tx_count - Monotonic count of console_putc() calls delivered to + * either output (serial and/or framebuffer). In use by the REPL to detect + * that an idle bottom half (heartbeat, USB attach/detach) wrote to the + * console while the top-level prompt was showing, so it can re-anchor the + * prompt afterward. Never decreases. + */ +uint64_t console_tx_count(void); + /** * Read a single character from serial console (non-blocking) * Returns -1 if no character available diff --git a/include/starkernel/repl.h b/include/starkernel/repl.h index dbf2358..82825bb 100644 --- a/include/starkernel/repl.h +++ b/include/starkernel/repl.h @@ -120,12 +120,20 @@ int sk_console_key_available(void); * uses internally, so a mid-word EXPECT behaves identically to typing at * "ok>" itself. * - * @param buf Destination buffer - * @param size Buffer capacity, including the NUL terminator - * @param active_vm VM whose idle dispatch runs while waiting + * @param buf Destination buffer + * @param size Buffer capacity, including the NUL terminator + * @param active_vm VM whose idle dispatch runs while waiting + * @param reanchor_prompt Nonzero to re-print the "ok> " prompt whenever + * an idle bottom half (heartbeat, USB attach/detach) writes + * to the console while this readline blocks at a bare, + * untyped prompt -- keeps the top-level prompt as the last + * thing shown once the chatter dies down. Callers whose + * prompt line is their own (shim.c's fgets(), i.e. + * QUERY/EXPECT/ACCEPT) pass 0 so "ok> " never gets stamped + * onto their mid-word input context. * @return number of characters placed in buf, not counting the NUL */ -int sk_console_readline(char *buf, int size, VM *active_vm); +int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt); #ifdef __cplusplus } diff --git a/src/starkernel/boot/uefi_loader.c b/src/starkernel/boot/uefi_loader.c index 05f3aae..c72a05e 100644 --- a/src/starkernel/boot/uefi_loader.c +++ b/src/starkernel/boot/uefi_loader.c @@ -68,6 +68,18 @@ extern void kernel_main(BootInfo *boot_info); #if defined(ARCH_AMD64) #define COM1_BASE 0x3F8 + +/* Set by raw_serial_init() only after a 16550 presence probe succeeds. + * When 0, raw_serial_putc() drops bytes immediately so boot can never + * wedge on a board whose 0x3F8 decode has no UART behind it (real + * mini-PCs like the Beelink SER5 typically expose no legacy COM port, + * whereas QEMU always emulates one — this is why this hang only bites + * on hardware). */ +static int raw_serial_ready = 0; + +/* Bounded THRE poll bound: provides the same "drop, don't hang" safety + * even if the probe above passes on a phantom decode. */ +#define RAW_SERIAL_THRE_MAX_SPIN 100000u /** * @brief Write a byte to an x86 I/O port via @c OUT (early-boot raw path). * @@ -137,6 +149,29 @@ static void raw_serial_init(void) raw_outb(COM1_BASE + 2, 0xC7); /* RTS/DSR set */ raw_outb(COM1_BASE + 4, 0x0B); + + /* 16550 presence probe: the scratch register (COM1+7) reads back what + * was written only when a real UART owns this decode. Unclaimed ports + * (or no I/O bridge routing) return garbage/0xFF, so the probe fails + * and serial is disabled — otherwise raw_serial_putc()'s THRE poll + * could spin forever on a board with no COM port. */ + raw_outb(COM1_BASE + 7, 0x5A); + raw_serial_ready = (raw_inb(COM1_BASE + 7) == 0x5A) ? 1 : 0; + + if (raw_serial_ready) + { + /* Sanity-check that THRE can actually be observed; if not, the + * decode is a phantom — disable serial rather than risk a spin. */ + unsigned int spin = 0; + while ((raw_inb(COM1_BASE + 5) & 0x20) == 0 && spin < RAW_SERIAL_THRE_MAX_SPIN) + { + ++spin; + } + if (spin >= RAW_SERIAL_THRE_MAX_SPIN) + { + raw_serial_ready = 0; + } + } } /** @@ -154,7 +189,20 @@ static void raw_serial_init(void) */ static void raw_serial_putc(char c) { - while ((raw_inb(COM1_BASE + 5) & 0x20) == 0) { } + /* No UART behind 0x3F8 (or probe failed): drop the byte, never block. */ + if (!raw_serial_ready) return; + + unsigned int spin = 0; + while ((raw_inb(COM1_BASE + 5) & 0x20) == 0) + { + /* Bounded: on hardware whose probe passed but THRE never asserts + * (phantom decode), drop the character instead of hanging boot. */ + if (++spin >= RAW_SERIAL_THRE_MAX_SPIN) + { + raw_serial_ready = 0; + return; + } + } raw_outb(COM1_BASE + 0, (uint8_t)c); } diff --git a/src/starkernel/hal/console.c b/src/starkernel/hal/console.c index 3d73e43..d56b58a 100644 --- a/src/starkernel/hal/console.c +++ b/src/starkernel/hal/console.c @@ -176,6 +176,7 @@ static int serial_transmit_empty(void) { 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; +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() @@ -258,8 +259,23 @@ static void emit_prefix(void) { * 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. + * console_ensure_line_start() uses the inner form so its format-only + * newline does not read as "real output" to the REPL's prompt re-anchor + * (repl.c) -- a silent idle beat that merely closes a dangling prompt line + * must not be mistaken for chatter and trigger a prompt repaint. The + * counter counts characters the caller actually intended to emit. */ +static void console_putc_inner(char c); + void console_putc(char c) { + 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(); @@ -288,6 +304,19 @@ void console_puts(const char *s) { } } +uint64_t console_tx_count(void) +{ + return g_console_tx_count; +} + +void console_ensure_line_start(void) +{ + if (!g_line_start) + { + console_putc_inner('\n'); + } +} + /** * Write a string with newline to serial console */ diff --git a/src/starkernel/repl.c b/src/starkernel/repl.c index 2b8ac68..2543c67 100644 --- a/src/starkernel/repl.c +++ b/src/starkernel/repl.c @@ -141,6 +141,16 @@ static int g_idle_pump_active; /* zero-initialized (BSS) */ static void sk_repl_idle(VM *active_vm) { + /* Close any dangling output line before this bottom half emits its own + * chatter (xhci attach/detach progress, block-subsystem notices). If we + * are mid-prompt-line -- the REPL started the attach while sitting at + * "ok> " -- a bare console_println() would otherwise glue its text onto + * the prompt and inherit no {VMName} prefix (g_line_start is 0). A + * fresh line first keeps every idle line prefix-tagged and readable, + * matching what an interactive typing session expects. No-op when the + * console is already at a line boundary. */ + console_ensure_line_start(); + /* Artemis Milestone 2d: xHCI Event Ring servicing. This is exactly the * "interrupt-driven, coarse cadence, cheap early-exit" trigger Section * U item 6 asked for -- xhci_poll_events() is a no-op read (loop @@ -498,11 +508,22 @@ int sk_console_key_available(void) * for the REPL's own top-level prompt, since it's the same underlying * console. Any g_console_pending_key left over from a ?TERMINAL peek is * consumed first so a line read never drops a byte ?TERMINAL already saw. + * @param reanchor_prompt nonzero from the REPL's own prompt sites (which + * print SK_PROMPT_TEXT immediately before): re-print the prompt whenever an + * idle bottom half wrote to the console while this call blocked at the bare + * prompt (see the re-anchor block in the idle branch). shim.c's fgets() + * passes 0 -- its prompt context is caller-owned. *===========================================================================*/ -int sk_console_readline(char *buf, int size, VM *active_vm) +int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt) { int n = 0; + /* TX counter value right after the caller printed its prompt. Any + * console output that lands while this readline blocks (heartbeat + * status, sk_repl_idle()'s USB attach/detach chatter) pushes the + * counter past this mark and away from a bare prompt; when that + * happens, re-anchor the prompt (below). */ + uint64_t prompt_tx_mark = console_tx_count(); buf[0] = '\0'; console_fb_draw_cursor(); /* show the cursor at the bare prompt, before any input */ @@ -531,6 +552,33 @@ int sk_console_readline(char *buf, int size, VM *active_vm) g_last_beat_tick = now; sk_repl_idle(active_vm); } + + /* + * Re-anchor the prompt (FABRIC.md 4.4a unified prompt: print + * only "ok> " here -- console_putc() auto-prefixes the current + * [VMName] on a fresh line). When an idle bottom half above + * pushed output past prompt_tx_mark, the console cursor is now + * below/after new lines and the "ok> " the caller printed has + * been scrolled or buried -- once the flood passes, the screen + * and serial log would end on a stale line with no prompt + * (FABRIC-3.md: the bare prompt must be the last thing shown + * while the REPL sits idle). Reprinting it restores that + * invariant. Skipped while a line is being edited (n > 0) so + * partial echo stays attached to its own prompt; shim.c's + * fgets() (QUERY/EXPECT/ACCEPT) calls in with reanchor_prompt + * == 0 for the same reason -- its prompt line is caller-owned + * text, not the REPL's. Each silent beat leaves the mark + * unchanged, so the final state after the chatter dies down is + * a fresh prompt on the last visible line, cursor on it. + */ + if (reanchor_prompt && n == 0 && + console_tx_count() != prompt_tx_mark) + { + console_puts(SK_PROMPT_TEXT); + console_fb_draw_cursor(); + prompt_tx_mark = console_tx_count(); + } + /* * Do NOT use hlt here: QEMU single-threaded TCG can't process * its APIC timer callbacks while the guest CPU is halted (the @@ -720,7 +768,7 @@ int sk_repl_step(VM *vm) console_puts(SK_PROMPT_TEXT); } - sk_console_readline(input, sizeof(input), vm); + sk_console_readline(input, sizeof(input), vm, 1); if (input[0] == '\0') { console_puts(" ok\n"); @@ -763,7 +811,7 @@ void sk_repl_run(VM *vm) * step()'s matching comment above. */ console_puts(SK_PROMPT_TEXT); - sk_console_readline(input, sizeof(input), active); + sk_console_readline(input, sizeof(input), active, 1); if (input[0] == '\0') { console_puts(" ok\n"); diff --git a/src/starkernel/vm/host/shim.c b/src/starkernel/vm/host/shim.c index 5dbf78a..0c8e188 100644 --- a/src/starkernel/vm/host/shim.c +++ b/src/starkernel/vm/host/shim.c @@ -1154,7 +1154,7 @@ static VM *shim_console_vm(void) { char *fgets(char *s, int size, FILE *stream) { (void)stream; if (!s || size <= 0) return NULL; - sk_console_readline(s, size, shim_console_vm()); + sk_console_readline(s, size, shim_console_vm(), 0); return s; } /** @brief Kernel @c fputc(): ignores stream; emits @p c to kernel console. */