Files
LithosAnanke/src/starkernel/arch/amd64/i8042.c
T
Robert Allan JamesandClaude Sonnet 5 88eb73cfe8 starkernel: item 4.3.5 -- amd64 I/O APIC + i8042 keyboard, interrupt-driven
Punch list §25 item 4.3.5 complete.

New ioapic.c/i8042.c drivers (MADT-derived I/O APIC base, no hardcoded
constants) plus a KBD-SCAN/KBD-DEBUG diagnostic word pair. Three real
bugs found and fixed en route, all blocking this item's own acceptance:
a fatal LAPIC spurious-vector crash (nothing had driven a real external
interrupt through the I/O APIC before), OVMF leaving the keyboard device
itself scanning-disabled (0xF4 fix), and isr.S's stub table only having
individually-numbered stubs through vector 32 -- everything above that,
including our IRQ1 vector 33, silently reported as vector 255 regardless
of which IDT slot actually fired. Verified live via QEMU sendkey against
KBD-SCAN: correct XT Set-1 make/break codes for two different keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 00:18:48 -04:00

151 lines
5.2 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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.
*/
/**
* i8042.c - PS/2 keyboard controller driver (amd64)
*
* Item 4.3.5 (FABRIC.md §27.5).
*/
#ifndef __STARKERNEL__
#error "i8042.c is kernel-only"
#endif
#include <stdint.h>
#include "starkernel/i8042.h"
#include "console.h"
#define I8042_DATA_PORT 0x60
#define I8042_STATUS_PORT 0x64
#define I8042_CMD_PORT 0x64
#define I8042_STATUS_OUTPUT_FULL (1u << 0)
#define I8042_STATUS_INPUT_FULL (1u << 1)
#define I8042_CMD_READ_CONFIG 0x20
#define I8042_CMD_WRITE_CONFIG 0x60
#define I8042_CONFIG_IRQ1_ENABLE (1u << 0)
#define I8042_DEV_CMD_ENABLE_SCANNING 0xF4
#define I8042_DEV_ACK 0xFA
/* Bounded wait -- command handshakes during one-time init only, never in
* the interrupt path or a per-keystroke loop. */
#define I8042_INIT_WAIT_ITERS 100000
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 val;
__asm__ volatile ("inb %1, %0" : "=a"(val) : "Nd"(port));
return val;
}
void i8042_init(void) {
/* Drain any stale output byte left by firmware before enabling IRQ1 —
* otherwise the first "interrupt" would really be this leftover byte
* read via polling, not a real IRQ1 delivery. */
while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
(void)inb(I8042_DATA_PORT);
}
outb(I8042_CMD_PORT, I8042_CMD_READ_CONFIG);
uint8_t config = inb(I8042_DATA_PORT);
config |= I8042_CONFIG_IRQ1_ENABLE;
outb(I8042_CMD_PORT, I8042_CMD_WRITE_CONFIG);
outb(I8042_DATA_PORT, config);
/* Tell the keyboard device itself to (re)enable scanning. OVMF's own
* PS/2 driver commonly sends 0xF5 (disable scanning) to the device as
* part of its own teardown before handoff, the same way it stops
* driving the PIT/PIC -- the controller's IRQ1-enable bit above only
* governs the controller, not whether the device generates scancodes
* at all. Bounded waits: one-time init handshake, not steady-state
* polling. */
{
int i;
for (i = 0; i < I8042_INIT_WAIT_ITERS; i++) {
if (!(inb(I8042_STATUS_PORT) & I8042_STATUS_INPUT_FULL)) break;
}
outb(I8042_DATA_PORT, I8042_DEV_CMD_ENABLE_SCANNING);
for (i = 0; i < I8042_INIT_WAIT_ITERS; i++) {
if (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
uint8_t resp = inb(I8042_DATA_PORT);
if (resp == I8042_DEV_ACK) {
console_println("i8042: keyboard ACKed enable-scanning");
} else {
console_println("i8042: keyboard responded (not ACK) to enable-scanning");
}
break;
}
}
}
console_println("i8042: IRQ1 enabled in controller config byte");
}
void i8042_drain_stale(void) {
/* Edge-triggered IRQ1 + OBF already set at unmask time == no rising
* edge ever again (the textbook "fires once, then never" pattern).
* i8042_init()'s drain happens at M4; VM bootstrap runs for real wall
* time between then and unmask, so re-drain immediately before. */
while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
(void)inb(I8042_DATA_PORT);
}
}
/* Ring buffer: single producer (the ISR), single reader (i8042_pop_scancode(),
* called from ordinary non-interrupt context, e.g. a diagnostic FORTH word).
* 32 entries is far more than one QEMU test keypress burst can fill. */
#define RING_SIZE 32
static volatile uint8_t ring[RING_SIZE];
static volatile uint8_t ring_head = 0; /* next slot the ISR writes */
static volatile uint8_t ring_tail = 0; /* next slot the reader takes */
/* Count of real keyboard IRQs serviced since boot -- a plain counter, no
* I/O, is cheap enough to keep in the ISR itself and doubles as the
* quickest way to confirm the interrupt path is alive without touching
* the ring buffer at all. */
volatile uint32_t g_i8042_isr_count = 0;
void i8042_handle_irq(void) {
/* Must always read 0x60, even to discard — otherwise OBF never clears
* and the controller stops delivering further interrupts. */
uint8_t scancode = inb(I8042_DATA_PORT);
g_i8042_isr_count++;
uint8_t next_head = (uint8_t)((ring_head + 1) % RING_SIZE);
if (next_head != ring_tail) { /* drop the byte if the ring is full */
ring[ring_head] = scancode;
ring_head = next_head;
}
}
int i8042_pop_scancode(uint8_t *out) {
if (ring_tail == ring_head) return 0; /* empty */
*out = ring[ring_tail];
ring_tail = (uint8_t)((ring_tail + 1) % RING_SIZE);
return 1;
}