Initial commit

Signed-off-by: Robert Allan James <robert.allan.james@gmail.com>
This commit is contained in:
Robert Allan James
2026-09-01 12:07:32 -04:00
parent d2a0305703
commit 58c59e87e5
11 changed files with 218 additions and 16 deletions
+49 -1
View File
@@ -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);
}
+29
View File
@@ -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
*/
+51 -3
View File
@@ -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");
+1 -1
View File
@@ -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. */