From 9b9758e5ee2564b04685eadf2b9ce23dd1060b86 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 19:34:40 +0200 Subject: rsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mouse (IRQ12, stream mode): ByteRing generalized for both PS/2 devices, ISR routes data-port bytes by the controller's aux bit; mouse_init with bounded-ACK handshake (boots without a mouse); packet decoder with bit-3 resync, screen-relative deltas, buttons. Exposed as /dev/mouse, the `mouse` stream command, and `make test-mouse`. Message queues (ADR 0003's GUI-era primitive): every task gets a static 16-slot TaskMsg ring; send/recv/recv_blocking with direct wake + tick scan safety net for the lost-wakeup race; `msgrecv`/`send` commands, `mouse ` routes real IRQ12 events into a task's queue, jobs shows q=. `make test-msg` + `make test-disk` regression pass. --- rsnix/src/ps2.rs | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) (limited to 'rsnix/src/ps2.rs') diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 35848b6..5f4a804 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs @@ -16,6 +16,7 @@ const MOD_RALT: u8 = 0x20; const MOD_CAPS: u8 = 0x40; /// A decoded key event. +#[derive(Clone, Copy, Debug)] pub enum KeyEvent { Char(char), Enter, @@ -400,3 +401,185 @@ fn ctrl_held() -> bool { let state = kbd(); state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0 } + +// --------------------------------------------------------------------------- +// PS/2 mouse — auxiliary channel, IRQ12, stream mode (3-byte packets) +// --------------------------------------------------------------------------- + +// PS/2 controller commands (written to port 0x64). +const CMD_AUX_ENABLE: u8 = 0xA8; // enable the auxiliary (second) port +const CMD_READ_CFG: u8 = 0x20; // read Configuration Byte → port 0x60 +const CMD_WRITE_CFG: u8 = 0x60; // write Configuration Byte ← port 0x60 +const CMD_SEND_TO_AUX: u8 = 0xD4; // next byte on 0x60 goes to the mouse + +// Configuration Byte bits (as sent via CMD_READ_CFG / CMD_WRITE_CFG). +// Bit 0 (IRQ1/keyboard) is set by `init`; this driver preserves it by +// reading the byte and only ORing in the two mouse bits below. +const CFG_AUX_IRQ: u8 = 0x02; // bit 1 — IRQ12 (mouse) +const CFG_AUX_CLOCK: u8 = 0x20; // bit 5 — auxiliary port clock enable + +// Mouse device commands (sent via CMD_SEND_TO_AUX). +const AUX_SET_DEFAULTS: u8 = 0xF6; // 100 Hz sample, 4 counts/mm, 1:1 +const AUX_ENABLE: u8 = 0xF4; // start data reporting (stream mode) +const AUX_ACK: u8 = 0xFA; + +/// A decoded PS/2 mouse event (one 3-byte packet). +/// +/// Movement is **screen-relative**: `dx` positive = right, `dy` positive = +/// down. (The PS/2 wire format is Y-up-positive; the driver negates it so +/// consumers can apply the deltas to framebuffer coordinates directly.) +#[derive(Clone, Copy, Debug)] +pub struct MouseEvent { + pub dx: i16, + pub dy: i16, + pub left: bool, + pub right: bool, + pub middle: bool, +} + +/// Packet assembly state — lives in main context (consumer side), fed by +/// the raw byte ring that the IRQ12 handler fills. +struct MouseState { + phase: u8, // 0 = awaiting start byte, 1 = await X, 2 = await Y + pkt: [u8; 3], +} + +static mut MOUSE: MouseState = MouseState { + phase: 0, + pkt: [0; 3], +}; +static MOUSE_PRESENT: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); + +/// True once `mouse_init` found a device that ACKed its setup commands. +pub fn mouse_present() -> bool { + MOUSE_PRESENT.load(core::sync::atomic::Ordering::Relaxed) +} + +/// Enable the auxiliary (mouse) channel and put the device into stream +/// mode. Returns false when no device ACKs (controller without a second +/// port, or a missing mouse) — the kernel then just runs keyboard-only. +/// +/// Must run *before* `interrupts::init` unmasks IRQ12: the init handshake +/// reads the data port directly, so any stray mouse bytes would otherwise +/// be split across the ISR path mid-handshake. +pub fn mouse_init() -> bool { + drain_input(); + unsafe { + // 1. Enable the auxiliary port itself. + wait_write(); + core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_AUX_ENABLE, + options(nostack, preserves_flags)); + + // 2. Configuration Byte: enable IRQ12 + the aux port clock. Read, + // OR the two bits, write back (preserves the keyboard IRQ bit + // that ps2::init set). + wait_write(); + core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_READ_CFG, + options(nostack, preserves_flags)); + wait_read(); + let mut cfg: u8; + core::arch::asm!("in al, dx", out("al") cfg, in("dx") DATA_PORT, + options(nostack, preserves_flags)); + cfg |= CFG_AUX_IRQ | CFG_AUX_CLOCK; + wait_write(); + core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_WRITE_CFG, + options(nostack, preserves_flags)); + wait_write(); + core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cfg, + options(nostack, preserves_flags)); + + // 3. Device setup. `set defaults` normalises the sample rate / + // resolution before enabling reporting, so the packet stream + // starts at a known cadence. Both commands need a device ACK; + // a timeout here means no mouse — bail out cleanly. + if !aux_command(AUX_SET_DEFAULTS) || !aux_command(AUX_ENABLE) { + return false; + } + } + MOUSE_PRESENT.store(true, core::sync::atomic::Ordering::Relaxed); + true +} + +/// Send one byte to the mouse via the 0xD4 channel and wait for its ACK. +/// The wait is bounded so a missing device cannot hang the boot. +fn aux_command(cmd: u8) -> bool { + unsafe { + wait_write(); + core::arch::asm!("out dx, al", in("dx") STATUS_PORT, in("al") CMD_SEND_TO_AUX, + options(nostack, preserves_flags)); + wait_write(); + core::arch::asm!("out dx, al", in("dx") DATA_PORT, in("al") cmd, + options(nostack, preserves_flags)); + for _ in 0..100_000 { + let status: u8; + core::arch::asm!("in al, dx", out("al") status, in("dx") STATUS_PORT, + options(nostack, preserves_flags)); + if status & STAT_OUTPUT_FULL != 0 { + let b: u8; + core::arch::asm!("in al, dx", out("al") b, in("dx") DATA_PORT, + options(nostack, preserves_flags)); + return b == AUX_ACK; + } + } + } + false +} + +/// Non-blocking: decode the next pending mouse event, if a full packet has +/// arrived. Raw bytes are drained from the IRQ ring; incomplete packets +/// stay buffered in `MouseState` until the remaining bytes land. +pub fn try_poll_mouse() -> Option { + while let Some(b) = crate::interrupts::read_mouse_byte() { + if let Some(ev) = mouse_byte(b) { + return Some(ev); + } + } + None +} + +/// Feed one raw byte into the packet assembler. Byte 0 of every standard +/// PS/2 mouse packet has bit 3 set (the sync marker), so a lost byte (ring +/// overrun, init response) is detected on the next packet and the stream +/// re-syncs — the marker is only accepted as a packet start. +fn mouse_byte(b: u8) -> Option { + let m = unsafe { &mut *core::ptr::addr_of_mut!(MOUSE) }; + match m.phase { + 0 => { + if b & 0x08 != 0 { + m.pkt[0] = b; + m.phase = 1; + } + None + } + 1 => { + m.pkt[1] = b; + m.phase = 2; + None + } + _ => { + m.pkt[2] = b; + m.phase = 0; + Some(MouseEvent { + // 9-bit signed: low 8 bits in the data byte, sign bit in + // byte 0 (bit 4 = X, bit 5 = Y). + dx: signed9(m.pkt[0], m.pkt[1], 0x10), + // PS/2 reports Y positive = up; flip to screen convention. + dy: -signed9(m.pkt[0], m.pkt[2], 0x20), + left: m.pkt[0] & 0x01 != 0, + right: m.pkt[0] & 0x02 != 0, + middle: m.pkt[0] & 0x04 != 0, + }) + } + } +} + +/// Sign-extend an 8-bit movement byte to 9 bits using the packet's sign +/// flag (bit `sign` of byte 0), widening to i16 for consumers. +fn signed9(b0: u8, b: u8, sign: u8) -> i16 { + let mut v = (b as i8) as i16; + if b0 & sign != 0 { + // Set the 9th bit (0xFF00 = sign-extension mask for i16). + v |= 0xFF00u16 as i16; + } + v +} -- cgit v1.2.3