Wire real PS/2 mouse packets through to input events
Replace the mouse driver's synthetic stream with the real path, the way the keyboard was wired: - The auxiliary port's IRQ12 is enumerated on the mouse's own ACPI node (PNP0F13), and the kernel only lets a device's claimer bind or ack its IRQs — so ps2-bus now claims that node alongside the controller whenever port 2 carries a device, binds IRQ12 to its one endpoint, and re-arms whichever line the notification's badge names. The forwarding loop already routed auxiliary bytes by status bit 5. - mouse-packet.zig (new, pure, host-tested): three-byte stream-mode packet assembly — bit-3 sync with resynchronization, ACK/BAT bytes dropped at packet start, nine-bit two's-complement movement, overflow packets discarded, and PS/2 positive-Y-up converted to the screen convention (positive down). - mouse.zig mirrors the keyboard driver: no hardware claim, attaches to the bus as its mouse, and publishes button_down/button_up per changed button plus motion events with the pressed-button mask. Verified end to end in QEMU via monitor mouse_move/mouse_button: motion round-trips in screen coordinates, buttons transition with the right mask, and keyboard events keep flowing alongside. The follow-up is the IntelliMouse magic-knock for a scroll wheel (four-byte packets) and scroll events.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
//! PS/2 mouse packet assembly — the byte stream a streaming mouse sends, turned
|
||||
//! into decoded movement/button reports.
|
||||
//!
|
||||
//! A standard PS/2 mouse in stream mode sends three-byte packets:
|
||||
//!
|
||||
//! byte 0: | Y ovf | X ovf | Y sign | X sign | 1 | middle | right | left |
|
||||
//! byte 1: X movement (low eight bits; the sign bit lives in byte 0)
|
||||
//! byte 2: Y movement (likewise)
|
||||
//!
|
||||
//! Movement is nine-bit two's complement, PS/2 convention: positive X right,
|
||||
//! positive Y **up**. The decoded packet converts Y to the screen convention
|
||||
//! (positive down), matching what every consumer of relative motion expects.
|
||||
//! Bit 3 of byte 0 is always set — the resynchronization anchor: a byte at
|
||||
//! packet start with bit 3 clear cannot be a packet header and is dropped.
|
||||
//!
|
||||
//! Everything here is pure (no imports beyond `std`, no IO), so it is
|
||||
//! host-testable: the tests at the bottom run under `zig build test`.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// One decoded movement/button report, in screen convention (positive dy down).
|
||||
pub const Packet = struct {
|
||||
left: bool,
|
||||
right: bool,
|
||||
middle: bool,
|
||||
dx: i16,
|
||||
dy: i16,
|
||||
};
|
||||
|
||||
const header_always_set: u8 = 1 << 3;
|
||||
const header_left: u8 = 1 << 0;
|
||||
const header_right: u8 = 1 << 1;
|
||||
const header_middle: u8 = 1 << 2;
|
||||
const header_x_sign: u8 = 1 << 4;
|
||||
const header_y_sign: u8 = 1 << 5;
|
||||
const header_x_overflow: u8 = 1 << 6;
|
||||
const header_y_overflow: u8 = 1 << 7;
|
||||
|
||||
/// Device protocol bytes that can reach the packet stream around bring-up (the
|
||||
/// acknowledge to enable-reporting, a reset's self-test result). Both have bit 3
|
||||
/// set, so the header check alone cannot reject them; they are recognized only
|
||||
/// at packet start, where a real header cannot be one of them in practice.
|
||||
const response_acknowledge: u8 = 0xFA;
|
||||
const response_self_test_passed: u8 = 0xAA;
|
||||
|
||||
/// Accumulates the byte stream into `Packet`s. Feed it every byte the mouse
|
||||
/// sends; the third byte of each well-formed packet returns one.
|
||||
pub const Assembler = struct {
|
||||
bytes: [3]u8 = undefined,
|
||||
count: u8 = 0,
|
||||
|
||||
pub fn feed(self: *Assembler, byte: u8) ?Packet {
|
||||
if (self.count == 0) {
|
||||
// Resynchronize: a packet must start with a plausible header.
|
||||
if (byte & header_always_set == 0) return null;
|
||||
if (byte == response_acknowledge or byte == response_self_test_passed) return null;
|
||||
}
|
||||
self.bytes[self.count] = byte;
|
||||
self.count += 1;
|
||||
if (self.count < 3) return null;
|
||||
self.count = 0;
|
||||
|
||||
const header = self.bytes[0];
|
||||
// An overflowed count is garbage by definition; discard the packet.
|
||||
if (header & (header_x_overflow | header_y_overflow) != 0) return null;
|
||||
|
||||
return .{
|
||||
.left = header & header_left != 0,
|
||||
.right = header & header_right != 0,
|
||||
.middle = header & header_middle != 0,
|
||||
.dx = movement(self.bytes[1], header & header_x_sign != 0),
|
||||
// PS/2 positive Y is up; screen positive Y is down.
|
||||
.dy = -movement(self.bytes[2], header & header_y_sign != 0),
|
||||
};
|
||||
}
|
||||
|
||||
/// Nine-bit two's complement: the eight movement bits plus the header's sign.
|
||||
fn movement(low: u8, negative: bool) i16 {
|
||||
const value: i16 = low;
|
||||
return if (negative) value - 256 else value;
|
||||
}
|
||||
};
|
||||
|
||||
// --- tests (host-run via `zig build test`) ------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
fn feedAll(assembler: *Assembler, bytes: []const u8) ?Packet {
|
||||
var result: ?Packet = null;
|
||||
for (bytes) |byte| {
|
||||
if (assembler.feed(byte)) |packet| result = packet;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
test "plain motion decodes with screen-convention y" {
|
||||
var assembler = Assembler{};
|
||||
const packet = feedAll(&assembler, &.{ 0x08, 5, 3 }).?;
|
||||
try testing.expectEqual(@as(i16, 5), packet.dx);
|
||||
try testing.expectEqual(@as(i16, -3), packet.dy); // PS/2 up 3 -> screen -3
|
||||
try testing.expect(!packet.left and !packet.right and !packet.middle);
|
||||
}
|
||||
|
||||
test "negative movement sign-extends through the header bits" {
|
||||
var assembler = Assembler{};
|
||||
// X sign and Y sign set: dx = 0xFB - 256 = -5, dy raw = 0xFE - 256 = -2 -> screen +2.
|
||||
const packet = feedAll(&assembler, &.{ 0x08 | 0x10 | 0x20, 0xFB, 0xFE }).?;
|
||||
try testing.expectEqual(@as(i16, -5), packet.dx);
|
||||
try testing.expectEqual(@as(i16, 2), packet.dy);
|
||||
}
|
||||
|
||||
test "buttons decode from the header" {
|
||||
var assembler = Assembler{};
|
||||
const packet = feedAll(&assembler, &.{ 0x08 | 0x01 | 0x02, 0, 0 }).?;
|
||||
try testing.expect(packet.left);
|
||||
try testing.expect(packet.right);
|
||||
try testing.expect(!packet.middle);
|
||||
}
|
||||
|
||||
test "a byte with bit 3 clear at packet start is dropped" {
|
||||
var assembler = Assembler{};
|
||||
// The stray 0x02 cannot be a header; the following packet still decodes.
|
||||
try testing.expectEqual(@as(?Packet, null), assembler.feed(0x02));
|
||||
const packet = feedAll(&assembler, &.{ 0x09, 1, 0 }).?;
|
||||
try testing.expect(packet.left);
|
||||
try testing.expectEqual(@as(i16, 1), packet.dx);
|
||||
}
|
||||
|
||||
test "protocol bytes at packet start are dropped" {
|
||||
var assembler = Assembler{};
|
||||
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xFA)); // enable-reporting ACK
|
||||
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xAA)); // self-test passed
|
||||
const packet = feedAll(&assembler, &.{ 0x08, 7, 0 }).?;
|
||||
try testing.expectEqual(@as(i16, 7), packet.dx);
|
||||
}
|
||||
|
||||
test "an overflowed packet is discarded whole" {
|
||||
var assembler = Assembler{};
|
||||
try testing.expectEqual(@as(?Packet, null), feedAll(&assembler, &.{ 0x08 | 0x40, 0xFF, 0xFF }));
|
||||
// The assembler is back at packet start.
|
||||
const packet = feedAll(&assembler, &.{ 0x08, 1, 1 }).?;
|
||||
try testing.expectEqual(@as(i16, 1), packet.dx);
|
||||
}
|
||||
Reference in New Issue
Block a user