Files
nt-lite/code/hal/hal_keyboard.c
T
2026-05-04 09:17:00 +02:00

304 lines
11 KiB
C

// hal_keyboard.c - PS/2 keyboard, interrupt-driven, 256-entry ring buffer
//
// Scan-code set 1 (XT-compatible), as output by the 8042's built-in
// translation layer. Supports make/break for all standard keys, Shift /
// Ctrl / Alt / CapsLock / NumLock modifiers, and the E0-prefixed extended
// key set (arrows, Home/End/PgUp/PgDn/Ins/Del, RCtrl, RAlt, numpad-Enter,
// numpad-divide).
//
// Threading model
// ---------------
// Single-producer (ISR on IRQ 1) / single-consumer ring buffer. All
// ISR-written state is declared volatile. On a uniprocessor, volatile
// plus the natural atomicity of aligned int reads/writes is sufficient.
// On SMP, add acquire/release memory barriers around gHead and gTail.
//
// Caller responsibilities
// -----------------------
// 1. Register KbdIsr with your interrupt dispatcher for IRQ 1.
// 2. Send the APIC/PIC EOI *after* KbdIsr returns, in the surrounding
// wrapper - not inside KbdIsr itself.
// 3. Unmask IRQ 1 in the APIC or 8259 before expecting any interrupts.
//
#include <stdint.h>
#include <stdbool.h>
#include <intrin.h>
#include <hal_api.h>
#include <hal_apic.h>
#include <hal_debug.h>
// -- Port addresses ------------------------------------------------------------
#define PS2_DATA 0x60u
#define PS2_STAT 0x64u
#define PS2_CMD 0x64u
// -- Ring buffer ---------------------------------------------------------------
// KBD_BUF_SIZE must be a power of two; KBD_BUF_MASK replaces % with &.
#define KBD_BUF_SIZE 256u
#define KBD_BUF_MASK (KBD_BUF_SIZE - 1u)
static uint8_t gBuf[KBD_BUF_SIZE];
static volatile int gHead = 0; // consumer read pointer (updated by reader)
static volatile int gTail = 0; // producer write pointer (updated by ISR)
// -- Scan-code set 1 constants -------------------------------------------------
#define SC1_BREAK_BIT 0x80u // OR'd into every break (key-up) scan code
#define SC1_MAKE_MASK 0x7Fu // complement of SC1_BREAK_BIT; avoids ~uint cast
#define SC1_EXT_PREFIX 0xE0u // one-byte prefix for extended keys
// -- Modifier flags ------------------------------------------------------------
#define MOD_LSHIFT (1u << 0)
#define MOD_RSHIFT (1u << 1)
#define MOD_LCTRL (1u << 2)
#define MOD_RCTRL (1u << 3)
#define MOD_LALT (1u << 4)
#define MOD_RALT (1u << 5)
#define MOD_CAPS (1u << 6) // toggle, not a held key
#define MOD_NUMLOCK (1u << 7) // toggle, not a held key
#define MOD_SHIFT (MOD_LSHIFT | MOD_RSHIFT)
#define MOD_CTRL (MOD_LCTRL | MOD_RCTRL)
#define MOD_ALT (MOD_LALT | MOD_RALT)
static volatile uint32_t gModifiers = 0;
static volatile bool gNextExtended = false; // previous byte was 0xE0
// -- Raw-scancode callback (invoked from ISR context) --------------------------
// The pointer must be volatile so the compiler re-reads it on every ISR entry
// rather than caching the value seen at startup.
static void (* volatile gCallback)(uint8_t) = NULL;
// -- ASCII translation tables - scan-code set 1, non-extended keys only --------
// Index = 7-bit make code (SC1_BREAK_BIT cleared). 0 = no printable glyph.
// Numpad digits yield their digit characters; a NumLock-aware wrapper can
// substitute cursor-movement codes (e.g. HAL_KEY_HOME) when NumLock is clear.
//
// Layout verified against the IBM PC/AT scan-code set 1 reference.
static const char gUnshifted[128] = {
/*00*/ 0,
/*01*/ 0, // ESC
/*02*/ '1','2','3','4','5','6','7','8','9','0','-','=', // 02-0D
/*0E*/ '\b',
/*0F*/ '\t',
/*10*/ 'q','w','e','r','t','y','u','i','o','p','[',']', // 10-1B
/*1C*/ '\n',
/*1D*/ 0, // Left Ctrl
/*1E*/ 'a','s','d','f','g','h','j','k','l',';','\'','`', // 1E-29
/*2A*/ 0, // Left Shift
/*2B*/ '\\',
/*2C*/ 'z','x','c','v','b','n','m',',','.','/', // 2C-35
/*36*/ 0, // Right Shift
/*37*/ '*', // Numpad * (also PrtSc on some keyboards)
/*38*/ 0, // Left Alt
/*39*/ ' ',
/*3A*/ 0, // Caps Lock
/*3B*/ 0,0,0,0,0,0,0,0,0,0, // F1-F10 (3B-44)
/*45*/ 0, // Num Lock
/*46*/ 0, // Scroll Lock
/*47*/ '7','8','9', // Numpad 7-9
/*4A*/ '-', // Numpad -
/*4B*/ '4','5','6', // Numpad 4-6
/*4E*/ '+', // Numpad +
/*4F*/ '1','2','3', // Numpad 1-3
/*52*/ '0', // Numpad 0
/*53*/ '.', // Numpad .
/*54*/ 0,0,0,
/*57*/ 0, // F11
/*58*/ 0, // F12
/*59*/ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 59-68
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 69-78
0,0,0,0,0,0,0, // 79-7F
};
static const char gShifted[128] = {
/*00*/ 0,
/*01*/ 0, // ESC
/*02*/ '!','@','#','$','%','^','&','*','(',')','_','+', // 02-0D
/*0E*/ '\b',
/*0F*/ '\t',
/*10*/ 'Q','W','E','R','T','Y','U','I','O','P','{','}', // 10-1B
/*1C*/ '\n',
/*1D*/ 0, // Left Ctrl
/*1E*/ 'A','S','D','F','G','H','J','K','L',':','"','~', // 1E-29
/*2A*/ 0, // Left Shift
/*2B*/ '|',
/*2C*/ 'Z','X','C','V','B','N','M','<','>','?', // 2C-35
/*36*/ 0, // Right Shift
/*37*/ '*',
/*38*/ 0, // Left Alt
/*39*/ ' ',
/*3A*/ 0, // Caps Lock
/*3B*/ 0,0,0,0,0,0,0,0,0,0, // F1-F10
/*45*/ 0,
/*46*/ 0,
/*47*/ '7','8','9',
/*4A*/ '-',
/*4B*/ '4','5','6',
/*4E*/ '+',
/*4F*/ '1','2','3',
/*52*/ '0',
/*53*/ '.',
/*54*/ 0,0,0,
/*57*/ 0, // F11
/*58*/ 0, // F12
/*59*/ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,
};
// -- Interrupt service routine --------------------------------------------------
// Called with interrupts disabled (inside the hardware IRQ handler).
// The caller's wrapper must send the APIC/PIC EOI after this returns.
static void KbdIsr(void) {
uint8_t sc = __inbyte(PS2_DATA);
// E0 prefix: the following byte is an extended make/break code.
if (sc == SC1_EXT_PREFIX) {
gNextExtended = true;
return;
}
bool extended = gNextExtended;
bool is_break = (sc & SC1_BREAK_BIT) != 0;
uint8_t make = sc & SC1_MAKE_MASK;
gNextExtended = false;
// -- Update modifier state -------------------------------------------------
if (!extended) {
switch (make) {
case 0x2A: // Left Shift
gModifiers = is_break ? (gModifiers & ~MOD_LSHIFT)
: (gModifiers | MOD_LSHIFT);
return;
case 0x36: // Right Shift
gModifiers = is_break ? (gModifiers & ~MOD_RSHIFT)
: (gModifiers | MOD_RSHIFT);
return;
case 0x1D: // Left Ctrl
gModifiers = is_break ? (gModifiers & ~MOD_LCTRL)
: (gModifiers | MOD_LCTRL);
return;
case 0x38: // Left Alt
gModifiers = is_break ? (gModifiers & ~MOD_LALT)
: (gModifiers | MOD_LALT);
return;
case 0x3A: // Caps Lock - toggle on make only
if (!is_break) gModifiers ^= MOD_CAPS;
return;
case 0x45: // Num Lock - toggle on make only
if (!is_break) gModifiers ^= MOD_NUMLOCK;
return;
default:
break;
}
} else {
// Extended make codes that map to modifier keys.
switch (make) {
case 0x1D: // Right Ctrl (E0 1D)
gModifiers = is_break ? (gModifiers & ~MOD_RCTRL)
: (gModifiers | MOD_RCTRL);
return;
case 0x38: // Right Alt / AltGr (E0 38)
gModifiers = is_break ? (gModifiers & ~MOD_RALT)
: (gModifiers | MOD_RALT);
return;
default:
break;
}
// All other extended keys (arrows, Home/End, etc.) fall through
// and are enqueued as make codes for consumer-side interpretation.
}
// -- Discard break codes for non-modifier keys -----------------------------
if (is_break) return;
// -- Enqueue make code; silently drop if buffer is full --------------------
int next = (gTail + 1) & (int)KBD_BUF_MASK;
if (next != gHead) {
gBuf[gTail] = make;
gTail = next;
}
// -- Notify optional callback with the raw make code -----------------------
// Take a local snapshot so the compiler cannot reorder the NULL check and
// the call across a volatile barrier, and to avoid double-fetching.
void (*cb)(uint8_t) = (void (*)(uint8_t))gCallback;
if (cb) cb(make);
}
// -- Consumer API --------------------------------------------------------------
static bool KbdPoll(void) {
return gHead != gTail;
}
// Blocking read. Spins with PAUSE until a make code is available.
// On a preemptive kernel, replace the _mm_pause() loop with a
// condition variable or sti/hlt to avoid wasting CPU cycles.
static uint8_t KbdReadScancode(void) {
while (gHead == gTail) {
_mm_pause();
}
uint8_t sc = gBuf[gHead];
gHead = (gHead + 1) & (int)KBD_BUF_MASK;
return sc;
}
// Returns the ASCII character for the next keypress, applying the current
// Shift and CapsLock state. Returns 0 for keys with no ASCII mapping
// (function keys, arrows, unmodified Ctrl/Alt keys, etc.).
//
// CapsLock inverts shift only for alphabetic keys (a-z); digit and symbol
// keys are always governed solely by the physical Shift state.
static char KbdReadChar(void) {
uint8_t make = KbdReadScancode();
if (make >= 128) return 0;
uint32_t mods = gModifiers; // snapshot; safe outside ISR
bool shifted = (mods & MOD_SHIFT) != 0;
// CapsLock toggles the effective shift for alphabetic keys only.
if (mods & MOD_CAPS) {
char base = gUnshifted[make];
if (base >= 'a' && base <= 'z') {
shifted = !shifted;
}
}
return shifted ? gShifted[make] : gUnshifted[make];
}
static void KbdSetCallback(void (*cb)(uint8_t)) {
gCallback = cb;
}
// Returns the current modifier bitmask (OR of MOD_* flags above).
// Snapshot is taken outside ISR and may lag by at most one interrupt.
static uint32_t KbdGetModifiers(void) {
return gModifiers;
}
// -- Initializer ---------------------------------------------------------------
void HalKeyboardInitialize(HAL_KEYBOARD *out) {
// Enable PS/2 auxiliary port so the mouse driver can claim IRQ 12.
// The 8042 must have an empty input buffer before we can issue commands.
while (__inbyte(PS2_STAT) & 2) { /* wait for input buffer empty */ }
__outbyte(PS2_CMD, 0xA8); // enable aux port
// Drain any stale bytes sitting in the PS/2 output buffer before
// enabling interrupts, so the first ISR call sees a clean state.
while (__inbyte(PS2_STAT) & 1) {
__inbyte(PS2_DATA);
}
out->Poll = KbdPoll;
out->ReadScancode = KbdReadScancode;
out->ReadChar = KbdReadChar;
out->SetCallback = KbdSetCallback;
out->GetModifiers = KbdGetModifiers;
out->Isr = KbdIsr;
out->IrqNumber = 1;
}