From: Tulio A M Mendes Date: Sat, 14 Feb 2026 22:27:14 +0000 (-0300) Subject: fix: serial input blocking — timer-polled UART RX fallback X-Git-Url: https://projects.tadryanom.me/docs/POSIX_ROADMAP.md?a=commitdiff_plain;h=d696dfe1d2741a8e1a19597e60da0ccdc238ce22;p=AdrOS.git fix: serial input blocking — timer-polled UART RX fallback 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 --- diff --git a/include/hal/uart.h b/include/hal/uart.h index 0efb76f..7548ce9 100644 --- a/include/hal/uart.h +++ b/include/hal/uart.h @@ -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)); diff --git a/src/drivers/timer.c b/src/drivers/timer.c index 6027b16..3176247 100644 --- a/src/drivers/timer.c +++ b/src/drivers/timer.c @@ -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(); } diff --git a/src/hal/arm/uart.c b/src/hal/arm/uart.c index f9ca6a0..477f6af 100644 --- a/src/hal/arm/uart.c +++ b/src/hal/arm/uart.c @@ -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)) { } diff --git a/src/hal/mips/uart.c b/src/hal/mips/uart.c index 96b55d6..cfaaf9b 100644 --- a/src/hal/mips/uart.c +++ b/src/hal/mips/uart.c @@ -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); diff --git a/src/hal/riscv/uart.c b/src/hal/riscv/uart.c index 4ca44ac..e39d717 100644 --- a/src/hal/riscv/uart.c +++ b/src/hal/riscv/uart.c @@ -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); diff --git a/src/hal/x86/uart.c b/src/hal/x86/uart.c index 4935d5e..b62b94c 100644 --- a/src/hal/x86/uart.c +++ b/src/hal/x86/uart.c @@ -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 index 0000000..53691df --- /dev/null +++ b/tests/test_serial_input.exp @@ -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 + } +}