]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
fix: serial input blocking — timer-polled UART RX fallback
authorTulio A M Mendes <[email protected]>
Sat, 14 Feb 2026 22:27:14 +0000 (19:27 -0300)
committerTulio A M Mendes <[email protected]>
Sat, 14 Feb 2026 22:27:14 +0000 (19:27 -0300)
Root cause: IOAPIC edge-triggered delivery for COM1 IRQ 4 never
fires in QEMU i440FX. The UART IRQ line state during the PIC→IOAPIC
transition is undefined — if the line is already HIGH when the
IOAPIC starts watching, no rising edge is ever detected, permanently
blocking serial input.

Attempted fixes that did NOT work:
- hal_uart_drain_rx() after IOAPIC routing (drain FIFO + IIR + MSR)
- FIFO trigger level 14→1 byte (eliminate character timeout dependency)
- IER disable→drain→re-enable sequencing around IOAPIC route

Fix: poll UART RX in the timer tick handler (100Hz). hal_uart_poll_rx()
checks LSR bit 0 and dispatches pending characters through the existing
rx_callback chain (tty_input_char). This gives ≤10ms latency for serial
input — imperceptible for interactive use.

The IRQ-driven path (uart_irq_handler at vector 36) remains active as
a fast path for platforms where IOAPIC edge detection works correctly.

Also adds tests/test_serial_input.exp: automated expect-based test that
boots /bin/sh with console=serial and verifies typed commands execute.

Tests: 20/20 smoke (8s), 16/16 battery, serial input PASS, cppcheck clean

include/hal/uart.h
src/drivers/timer.c
src/hal/arm/uart.c
src/hal/mips/uart.c
src/hal/riscv/uart.c
src/hal/x86/uart.c
tests/test_serial_input.exp [new file with mode: 0755]

index 0efb76f007baea832ee1bbe82f1f32d78741002e..7548ce9c3b7e55f054a085a38543719d9ca5433f 100644 (file)
@@ -3,6 +3,7 @@
 
 void hal_uart_init(void);
 void hal_uart_drain_rx(void);
+void hal_uart_poll_rx(void);
 void hal_uart_putc(char c);
 int  hal_uart_try_getc(void);
 void hal_uart_set_rx_callback(void (*cb)(char));
index 6027b16b8516c357389f39366f049593ad468b23..3176247f7437266d1adf6e9d5b917a1f6f231564 100644 (file)
@@ -5,6 +5,7 @@
 #include "vga_console.h"
 
 #include "hal/timer.h"
+#include "hal/uart.h"
 
 static uint32_t tick = 0;
 
@@ -16,6 +17,7 @@ static void hal_tick_bridge(void) {
     tick++;
     vdso_update_tick(tick);
     vga_flush();
+    hal_uart_poll_rx();
     process_wake_check(tick);
     schedule();
 }
index f9ca6a0f3cb0ce2028bb3b3fc7a47bec33e06020..477f6af449e80ac10cadc078a13eb8728d83e7a6 100644 (file)
@@ -9,6 +9,9 @@ void hal_uart_init(void) {
 void hal_uart_drain_rx(void) {
 }
 
+void hal_uart_poll_rx(void) {
+}
+
 void hal_uart_putc(char c) {
     volatile uint32_t* uart = (volatile uint32_t*)UART_BASE;
     while (uart[6] & (1 << 5)) { }
index 96b55d66422d604dbed303a3ecee018e4048362f..cfaaf9ba851426618f9b2f2d4a80fdd9bc6cfc42 100644 (file)
@@ -16,6 +16,9 @@ void hal_uart_drain_rx(void) {
         (void)mmio_read8(UART_BASE);
 }
 
+void hal_uart_poll_rx(void) {
+}
+
 void hal_uart_putc(char c) {
     while ((mmio_read8(UART_BASE + 5) & 0x20) == 0) { }
     mmio_write8(UART_BASE, (uint8_t)c);
index 4ca44ac112c29df79cd1bff936ef67100208fd68..e39d71728c9226d35f3446686bf1cdf9d148f8a6 100644 (file)
@@ -14,6 +14,9 @@ void hal_uart_drain_rx(void) {
         (void)mmio_read8(UART_BASE);
 }
 
+void hal_uart_poll_rx(void) {
+}
+
 void hal_uart_putc(char c) {
     while ((mmio_read8(UART_BASE + 5) & 0x20) == 0) { }
     mmio_write8(UART_BASE, (uint8_t)c);
index 4935d5e1c049dec12ba1da748be036761043fcd6..b62b94c18a67cd45beed8cf370056f7165bf5e1d 100644 (file)
@@ -22,7 +22,7 @@ void hal_uart_init(void) {
     outb(UART_BASE + 0, 0x03);    /* Baud 38400 */
     outb(UART_BASE + 1, 0x00);
     outb(UART_BASE + 3, 0x03);    /* 8N1 */
-    outb(UART_BASE + 2, 0xC7);    /* Enable FIFO */
+    outb(UART_BASE + 2, 0x07);    /* Enable FIFO, clear both, 1-byte trigger */
     outb(UART_BASE + 4, 0x0B);    /* DTR + RTS + OUT2 */
 
     /* Register IRQ 4 handler (IDT vector 36 = 32 + 4) */
@@ -33,13 +33,50 @@ void hal_uart_init(void) {
 }
 
 void hal_uart_drain_rx(void) {
-    /* Drain any pending characters from the UART FIFO.
-     * This de-asserts the IRQ line so that the next character
-     * produces a clean rising edge for the IOAPIC (edge-triggered). */
-    (void)inb(UART_BASE + 2);          /* Read IIR to ack any pending */
-    while (inb(UART_BASE + 5) & 0x01)  /* Drain RX FIFO */
+    /* Full UART interrupt reinitialisation for IOAPIC hand-off.
+     *
+     * hal_uart_init() runs under the legacy PIC and enables IER bit 0
+     * (RX interrupt).  By the time the IOAPIC routes IRQ 4 as
+     * edge-triggered, the UART IRQ line may already be asserted —
+     * the IOAPIC will never see a rising edge and serial input is
+     * permanently dead.
+     *
+     * Fix: temporarily disable ALL UART interrupts so the IRQ line
+     * goes LOW, drain every pending condition, then re-enable IER.
+     * The next character will produce a clean LOW→HIGH edge. */
+
+    /* 1. Disable all UART interrupts — IRQ line goes LOW */
+    outb(UART_BASE + 1, 0x00);
+
+    /* 2. Drain the RX FIFO */
+    while (inb(UART_BASE + 5) & 0x01)
         (void)inb(UART_BASE);
-    (void)inb(UART_BASE + 6);          /* Read MSR to clear delta bits */
+
+    /* 3. Read IIR until "no interrupt pending" (bit 0 set) */
+    for (int i = 0; i < 16; i++) {
+        uint8_t iir = inb(UART_BASE + 2);
+        if (iir & 0x01) break;
+    }
+
+    /* 4. Clear modem-status delta bits */
+    (void)inb(UART_BASE + 6);
+
+    /* 5. Clear line-status error bits */
+    (void)inb(UART_BASE + 5);
+
+    /* 6. Re-enable RX interrupt — next character will assert a clean edge */
+    outb(UART_BASE + 1, 0x01);
+}
+
+void hal_uart_poll_rx(void) {
+    /* Timer-driven fallback: drain any pending characters from the
+     * UART FIFO via polling.  Called from the timer tick handler so
+     * serial input works even if the IOAPIC edge-triggered IRQ for
+     * COM1 is never delivered (observed in QEMU i440FX). */
+    while (inb(UART_BASE + 5) & 0x01) {
+        char c = (char)inb(UART_BASE);
+        if (uart_rx_cb) uart_rx_cb(c);
+    }
 }
 
 void hal_uart_set_rx_callback(void (*cb)(char)) {
diff --git a/tests/test_serial_input.exp b/tests/test_serial_input.exp
new file mode 100755 (executable)
index 0000000..53691df
--- /dev/null
@@ -0,0 +1,100 @@
+#!/usr/bin/expect -f
+#
+# AdrOS Serial Input Test
+# Verifies that typing on the serial console works with /bin/sh.
+#
+# Usage: expect tests/test_serial_input.exp [timeout_sec]
+#   timeout_sec: max seconds (default: 30)
+#
+# Exit codes:
+#   0 = serial input works (echo received)
+#   1 = serial input broken (no echo / no response)
+#   2 = timeout (boot did not complete)
+
+set timeout_sec [lindex $argv 0]
+if {$timeout_sec eq ""} { set timeout_sec 30 }
+
+set iso "adros-x86.iso"
+set disk "disk.img"
+
+# Ensure disk image exists
+if {![file exists $disk]} {
+    exec dd if=/dev/zero of=$disk bs=1M count=4 2>/dev/null
+}
+
+# Create a temporary grub.cfg that boots directly into shell+serial
+set tmpdir [exec mktemp -d]
+file mkdir "$tmpdir/boot/grub"
+set fd [open "$tmpdir/boot/grub/grub.cfg" w]
+puts $fd {set timeout=0
+set default=0
+menuentry "AdrOS serial shell" {
+  multiboot2 /boot/adros-x86.bin init=/bin/sh console=serial
+  module2 /boot/initrd.img
+  boot
+}}
+close $fd
+
+# Build a temporary ISO with the modified grub.cfg
+set tmp_iso "$tmpdir/adros-serial-test.iso"
+# Copy original ISO contents
+file mkdir "$tmpdir/iso"
+exec cp -r iso/boot "$tmpdir/iso/"
+file copy -force "$tmpdir/boot/grub/grub.cfg" "$tmpdir/iso/boot/grub/grub.cfg"
+exec grub-mkrescue -o $tmp_iso "$tmpdir/iso" 2>/dev/null
+
+set timeout $timeout_sec
+
+# Spawn QEMU with serial on stdio so expect can interact
+spawn qemu-system-i386 \
+    -smp 4 -boot d -cdrom $tmp_iso -m 128M -display none \
+    -drive file=$disk,if=ide,format=raw \
+    -nic user,model=e1000 \
+    -serial mon:stdio -monitor none \
+    -no-reboot -no-shutdown
+
+set qemu_id $spawn_id
+
+# Wait for shell prompt ($ )
+expect {
+    -re {\$ } {
+        # Shell prompt appeared — good
+    }
+    timeout {
+        puts "\n\[SERIAL-INPUT\] FAIL: Timeout waiting for shell prompt"
+        catch {exec kill [exp_pid]}
+        file delete -force $tmpdir
+        exit 2
+    }
+    eof {
+        puts "\n\[SERIAL-INPUT\] FAIL: QEMU exited before shell prompt"
+        file delete -force $tmpdir
+        exit 1
+    }
+}
+
+# Send a test command
+send "echo SERIAL_INPUT_OK\r"
+
+# Wait for the output
+expect {
+    "SERIAL_INPUT_OK" {
+        puts "\n\[SERIAL-INPUT\] PASS: Serial input works"
+        send "exit\r"
+        after 500
+        catch {exec kill [exp_pid]}
+        file delete -force $tmpdir
+        exit 0
+    }
+    timeout {
+        puts "\n\[SERIAL-INPUT\] FAIL: No response to typed command"
+        catch {exec kill [exp_pid]}
+        file delete -force $tmpdir
+        exit 1
+    }
+    eof {
+        puts "\n\[SERIAL-INPUT\] FAIL: QEMU exited unexpectedly"
+        file delete -force $tmpdir
+        exit 1
+    }
+}