| git.druid.rocks | index | druid520 | kaboom | src/ | drivers/ | serial.nsc |
src/drivers/serial.nsc
/*
* com1 (0x3f8) 16550 uart driver. started as a test/debug output
* channel only (boot.s already proved the pipeline got this far with
* one raw outb before kmain even ran) -- now also a real INPUT
* channel (serial_handle_irq, below), because -nographic makes it
* the ONLY channel: there's no separate graphical window with its
* own ps/2-backed keyboard focus, just one real terminal whose
* keystrokes arrive as raw bytes over this same uart, not as ps/2
* scancodes. found for real: every keystroke this whole project's
* testing ever used was qemu's own QMP send-key, which injects real
* ps/2 scancodes regardless of display mode -- a real user typing
* into a real -nographic session was never actually exercised until
* someone tried it and nothing happened at all, because kbd.nsc's
* irq1 handler has nothing to fire for a byte that arrived over com1.
*/
void outb(u16 port, u8 val);
u8 inb(u16 port);
void kbd_push(u8 ch);
global void
serial_init(void)
{
outb((u16)0x3f9, (u8)0x00);
outb((u16)0x3fb, (u8)0x80);
outb((u16)0x3f8, (u8)0x03);
outb((u16)0x3f9, (u8)0x00);
outb((u16)0x3fb, (u8)0x03);
outb((u16)0x3fa, (u8)0xc7);
outb((u16)0x3fc, (u8)0x0b);
/* enable the uart's own "received data available" interrupt
* (interrupt enable register, bit 0) -- without this the uart
* never asserts irq4 at all, no matter how many bytes arrive. */
outb((u16)0x3f9, (u8)0x01);
}
u8
serial_tx_ready(void)
{
u8 status;
status = inb((u16)0x3fd);
return status & (u8)0x20;
}
/* translates a bare '\n' to "\r\n" -- qemu's -nographic puts the
* host terminal itself into raw mode so keystrokes pass straight
* through to the guest, which also means the terminal's own output
* side does none of its usual \n->\r\n translation (that's normally
* termios's job, disabled along with everything else raw mode turns
* off): a real serial device is expected to send both bytes itself.
* without this, a bare \n just moves down a row without returning to
* column 0, so anything with more than one line (cat'ing any file
* more than a few words long) walks the cursor right with every
* line and leaves the next prompt printed mid-line, looking exactly
* like a broken cursor -- found by actually running kaboom over
* -nographic and cat'ing a multi-line file, not from reading the
* code (a captured-to-a-file serial log, this kernel's only test
* method until now, never shows this: the file just accumulates
* bytes with no live terminal cursor to confuse). doing this once,
* here, fixes every caller (serial_puts, fd_write's console output,
* the keyboard echo) instead of needing the same fix repeated at
* each one. */
global void
serial_putc(u8 ch)
{
if(ch == (u8)10)
{
while(serial_tx_ready() == (u8)0)
{
}
outb((u16)0x3f8, (u8)13);
}
while(serial_tx_ready() == (u8)0)
{
}
outb((u16)0x3f8, ch);
}
global void
serial_puts(ptr str)
{
ptr p;
u64 word;
u8 ch;
p = str;
while(1)
{
word = (u64)*p;
ch = (u8)(word & (u64)0xff);
if(ch == (u8)0)
{
return;
}
serial_putc(ch);
p = p + 1;
}
}
/* clears whatever real terminal is on the other end of com1, via the
* same ansi "clear screen + cursor home" sequence any ordinary
* terminal-based `clear` sends -- found missing by testing the clear
* command over -nographic specifically: vga_clear (vga.nsc) only ever
* touched the vga hardware framebuffer, which -nographic has no
* window onto at all, so the serial-only console it actually gives a
* real user just... never got cleared, with nothing sent over the
* one channel that mattered. */
global void
serial_clear(void)
{
serial_puts("\x1b[2J\x1b[H");
}
u8
serial_rx_ready(void)
{
u8 status;
status = inb((u16)0x3fd);
return status & (u8)0x01;
}
/* irq4 (com1): a byte arrived from whatever real terminal is
* actually talking to this machine. two translations happen here,
* neither of which a real ps/2 keystroke ever needed, both of which
* a raw-mode host terminal needs done FOR it since raw mode is
* exactly "the terminal does none of its usual translation, the far
* end is on its own":
*
* - enter usually arrives as a bare '\r' (13), not '\n' (10) --
* fd_read_stdin's own end-of-line check is ch==10 only, so without
* this a typed line would simply never be seen as finished.
* - backspace can arrive as either the "backspace" byte (8, what a
* ps/2 keystroke already produces) or "delete" (127, what a lot of
* real terminals actually send for the same physical key) --
* folded to 8 so fd_read_stdin's existing ch==8 handling (see its
* own note on why backspace's real erase logic lives there, not in
* an irq handler) covers both without needing to know two byte
* values mean the same key.
*
* pushed through the exact same kbd_push kbd.nsc's own irq1 handler
* uses -- one shared buffer, one shared echo rule, regardless of
* which physical path a byte actually arrived through. */
global void
serial_handle_irq(void)
{
u8 ch;
if(serial_rx_ready() == (u8)0)
{
return;
}
ch = inb((u16)0x3f8);
if(ch == (u8)13)
{
ch = (u8)10;
}
else if(ch == (u8)127)
{
ch = (u8)8;
}
/* drop anything else outside the handful of control bytes this
* console actually understands (8 backspace, 9 tab, 10 newline)
* -- found for real by testing arrow keys and the delete key over
* -nographic: both start with esc (27), which fd_read_stdin has
* no concept of at all, so it always went into the line buffer
* (and got echoed back) as an ordinary character. echoing esc back
* is the actual problem, not just cosmetic: the user's REAL
* terminal on the other end sees an escaped byte sequence coming
* FROM kaboom (indistinguishable from one of kaboom's own real
* ansi sequences, like serial_clear's) and faithfully executes it
* as a genuine cursor move, letting them navigate up into and
* "edit" output that's already scrolled past -- purely a trick of
* the real terminal's own rendering, kaboom's own line buffer was
* never actually touched by any of it. dropping esc (and every
* other unhandled control byte) before it's ever echoed or pushed
* closes that off entirely; whatever printable bytes followed
* (arrow keys are esc+[+A/B/C/D) still come through afterward as
* plain, harmless characters with no leading esc to give them any
* special meaning. */
if(ch != (u8)10 && ch != (u8)8 && ch != (u8)9 && (ch < (u8)32 || ch > (u8)126))
{
return;
}
kbd_push(ch);
}