Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization: - Move StarForth v3 engine to v3/ (src/, include/, Makefile) - Move kernel to kernel/ (src/, include/, linker/, Makefile) - Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md) - Move FABRIC-0..4.md to docs/fabric/ - Move ONTOLOGY.md and ROADMAP.md to docs/ Board infrastructure: - Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/ - Each board has board.mk (ISA, CPU flags, boot recipe) and README.md - Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET - make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board - ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet) - scripts/mkdiskimage.sh builds disk images for all boards Docs pipeline: - docs/book/ with LaTeX master (main.tex) and Makefile - pandoc converts Markdown to LaTeX at build time - Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks) - make docs builds single PDF (754 pages, 0 missing characters) - make docs TARGET=<board> adds board appendix - build/docs/<book|board>/meta.tex stamps git commit into PDF Bug fixes: - 42 include paths that only worked by accident now use correct relative paths - clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build) - Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved - Doxyfile, .clang-tidy, README.md, Kconfig paths updated Verified: - Hosted v3 build passes 1012 tests, 0 failures - SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE) - Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes - make clean TARGET=<board> removes only that board and its ISA objects - make all builds all boards, hosted v3, and docs in one run Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
#define SERIAL_SUPPORTED 1
|
||||
|
||||
/* UART 16550 I/O ports (COM1) */
|
||||
#define SERIAL_PORT_BASE 0x3F8
|
||||
|
||||
#define SERIAL_DATA_PORT (SERIAL_PORT_BASE + 0)
|
||||
#define SERIAL_INT_ENABLE_PORT (SERIAL_PORT_BASE + 1)
|
||||
#define SERIAL_FIFO_CTRL_PORT (SERIAL_PORT_BASE + 2)
|
||||
#define SERIAL_LINE_CTRL_PORT (SERIAL_PORT_BASE + 3)
|
||||
#define SERIAL_MODEM_CTRL_PORT (SERIAL_PORT_BASE + 4)
|
||||
#define SERIAL_LINE_STATUS_PORT (SERIAL_PORT_BASE + 5)
|
||||
|
||||
/* Line Status Register bits */
|
||||
#define SERIAL_LSR_DATA_READY (1 << 0)
|
||||
#define SERIAL_LSR_THR_EMPTY (1 << 5)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#elif defined(__aarch64__)
|
||||
#define SERIAL_SUPPORTED 1
|
||||
|
||||
/* PL011 UART — QEMU virt machine UART0 at 0x09000000 */
|
||||
#define PL011_BASE ((volatile unsigned int *)0x09000000UL)
|
||||
#define PL011_DR (PL011_BASE + 0) /* Data Register (offset 0x000) */
|
||||
#define PL011_FR (PL011_BASE + 6) /* Flag Register (offset 0x018) */
|
||||
#define PL011_FR_TXFF (1u << 5) /* TX FIFO full */
|
||||
|
||||
static inline void pl011_putc(char c)
|
||||
{
|
||||
while (*PL011_FR & PL011_FR_TXFF) { }
|
||||
*PL011_DR = (unsigned int)(unsigned char)c;
|
||||
}
|
||||
|
||||
#elif defined(__riscv)
|
||||
#define SERIAL_SUPPORTED 1
|
||||
|
||||
/* NS16550 UART — QEMU virt machine UART0 at 0x10000000 (MMIO, byte-wide) */
|
||||
#define NS16550_BASE ((volatile unsigned char *)0x10000000UL)
|
||||
#define NS16550_THR (NS16550_BASE + 0) /* Transmit Holding Register */
|
||||
#define NS16550_LSR (NS16550_BASE + 5) /* Line Status Register */
|
||||
#define NS16550_LSR_THRE (1u << 5) /* Transmit Holding Register Empty */
|
||||
#define NS16550_LSR_DR (1u << 0) /* Data Ready */
|
||||
|
||||
static inline void ns16550_putc(char c)
|
||||
{
|
||||
while (!(*NS16550_LSR & NS16550_LSR_THRE)) { }
|
||||
*NS16550_THR = (unsigned char)c;
|
||||
}
|
||||
|
||||
#else
|
||||
#define SERIAL_SUPPORTED 0
|
||||
|
||||
/* Stubs for other architectures */
|
||||
static inline void outb(uint16_t port, uint8_t val) {
|
||||
(void)port;
|
||||
(void)val;
|
||||
}
|
||||
|
||||
static inline uint8_t inb(uint16_t port) {
|
||||
(void)port;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Initialize serial console
|
||||
* x86: programs 16550 UART.
|
||||
* aarch64: PL011 is already initialized by UEFI firmware on QEMU virt.
|
||||
*/
|
||||
void console_init(void) {
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
outb(SERIAL_INT_ENABLE_PORT, 0x00);
|
||||
outb(SERIAL_LINE_CTRL_PORT, 0x80);
|
||||
outb(SERIAL_DATA_PORT, 0x01);
|
||||
outb(SERIAL_INT_ENABLE_PORT, 0x00);
|
||||
outb(SERIAL_LINE_CTRL_PORT, 0x03);
|
||||
outb(SERIAL_FIFO_CTRL_PORT, 0xC7);
|
||||
outb(SERIAL_MODEM_CTRL_PORT, 0x0B);
|
||||
outb(SERIAL_MODEM_CTRL_PORT, 0x1E);
|
||||
outb(SERIAL_DATA_PORT, 0xAE);
|
||||
if (inb(SERIAL_DATA_PORT) != 0xAE) {
|
||||
return;
|
||||
}
|
||||
outb(SERIAL_MODEM_CTRL_PORT, 0x0F);
|
||||
#elif defined(__aarch64__)
|
||||
/* PL011 already enabled by AAVMF; nothing to do */
|
||||
(void)0;
|
||||
#elif defined(__riscv)
|
||||
/* NS16550 already enabled by UEFI firmware on QEMU virt; nothing to do */
|
||||
(void)0;
|
||||
#else
|
||||
(void)SERIAL_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
static int serial_transmit_empty(void) {
|
||||
return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_THR_EMPTY;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 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;
|
||||
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 defined(__aarch64__)
|
||||
if (c == '\n') pl011_putc('\r');
|
||||
pl011_putc(c);
|
||||
#elif defined(__riscv)
|
||||
if (c == '\n') ns16550_putc('\r');
|
||||
ns16550_putc(c);
|
||||
#elif defined(__x86_64__) || defined(__i386__)
|
||||
if (c == '\n') {
|
||||
while (!serial_transmit_empty()) { arch_relax(); }
|
||||
outb(SERIAL_DATA_PORT, '\r');
|
||||
}
|
||||
while (!serial_transmit_empty()) { arch_relax(); }
|
||||
outb(SERIAL_DATA_PORT, (uint8_t)c);
|
||||
#else
|
||||
(void)c;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* 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_is_available();
|
||||
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) --- */
|
||||
if (fb_is_available()) {
|
||||
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) {
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_DATA_READY;
|
||||
#elif defined(__aarch64__)
|
||||
/* PL011_FR bit 4 = RXFE (RX FIFO empty); data available when RXFE == 0 */
|
||||
#define PL011_FR_RXFE (1u << 4)
|
||||
return !(*PL011_FR & PL011_FR_RXFE);
|
||||
#elif defined(__riscv)
|
||||
return *NS16550_LSR & NS16550_LSR_DR;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_is_available()) {
|
||||
vt100_draw_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
void console_fb_erase_cursor(void)
|
||||
{
|
||||
if (fb_is_available()) {
|
||||
vt100_erase_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single character from serial console (non-blocking)
|
||||
* Returns -1 if no character available
|
||||
*/
|
||||
int console_getc(void) {
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
if (!console_poll()) return -1;
|
||||
return (int)(unsigned char)inb(SERIAL_DATA_PORT);
|
||||
#elif defined(__aarch64__)
|
||||
if (!console_poll()) return -1;
|
||||
return (int)(*PL011_DR & 0xFF);
|
||||
#elif defined(__riscv)
|
||||
if (!console_poll()) return -1;
|
||||
return (int)(*NS16550_THR & 0xFF);
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user