6 Commits
Author SHA1 Message Date
Daniel Samson 88e92254e9 Name ACPI hardware IDs instead of magic _HID strings
Turn acpi-ids.zig's flat name table into a HardwareId enum modeled on
ps2-library's Port: one entry() switch holds the registry (variant ->
_HID string + human-readable name), with hid(), description(), and
fromHid() methods. The free description(hid) lookup the kernel's
device-tree dump uses survives, implemented over the enum, and a new
test round-trips every variant through fromHid.

Callers now name the device instead of quoting its id:

- ps2-library's DeviceType.hid() and ps2-bus's descriptor lookups use
  HardwareId.ps2_keyboard / .ps2_mouse.
- device-manager's driverFor parses the HID once with fromHid and
  switches on named values.
- acpi.zig's isPciRootNode carried the same ids twice, as strings and
  as packed-EISA integers (0x030AD041/0x080AD041); both branches now
  decode to the string form and answer through one isPciRootHid helper
  using .pci_bus / .pci_express_root_bridge.
- build.zig threads the acpi-ids module (previously kernel-only) into
  every user binary, like xkeyboard-config.
2026-07-11 23:23:16 +01:00
Daniel Samson 5725d35e5b Name the attach reply statuses instead of magic numbers
Add an AttachStatus enum to ps2-library.zig for AttachReply.status,
distinguishing the three failure causes handleAttach previously
collapsed into a bare -1: invalid_request (message too short),
missing_endpoint (no capability passed), and no_such_device (no port
identified the requested device type). The keyboard and mouse drivers
check against AttachStatus.ok rather than a literal 0.
2026-07-11 23:11:45 +01:00
Daniel Samson 8c95525793 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.
2026-07-11 23:09:04 +01:00
Daniel Samson 5bba5d3363 Wire real PS/2 scancodes through to input events and characters
Replace the keyboard driver's synthetic stream with the real path:

- ps2-bus binds IRQ1 (interrupt bits set only after the bind), drains
  port 0x60 on each interrupt, and forwards every byte to the attached
  child driver over async ipc_send, routed by the status register's
  auxiliary-output bit. Children attach via the new well-known ps2_bus
  service, handing over their endpoint as a capability.
- scancode.zig (new, pure, host-tested): scancode set 2 -> USB HID
  usage decoding (F0/E0/E1 prefix state machine) plus keyboard state —
  pressed-key bitmap, typematic-repeat classification, modifier and
  caps-lock tracking.
- keyboard.zig decodes the forwarded stream and publishes real
  key_down/key_press/key_up events, filling key_press characters via
  xkeyboard-config (layout from argv[2], default us) and synthesizing
  ASCII control characters for Enter/Tab/Backspace/Escape.
- protocol.zig names the full HID usage set in Keycode; build.zig
  threads the xkeyboard-config module into user binaries.

Verified end to end in QEMU via monitor sendkey: shift, caps lock,
and control-character synthesis all decode correctly. The mouse
driver still publishes its synthetic stream; attaching it to the
bus the same way is the follow-up.
2026-07-11 22:48:59 +01:00
Daniel Samson 80b72db676 Removing DAN-INIT 2026-07-11 21:43:49 +01:00
Daniel Samson aa0c97353a fixing arguments 2026-07-11 21:39:27 +01:00
14 changed files with 1290 additions and 154 deletions
+24 -15
View File
@@ -60,6 +60,8 @@ fn addUserBinary(
runtime_module: *std.Build.Module,
posix_module: *std.Build.Module,
mmio_module: *std.Build.Module,
xkeyboard_config_module: *std.Build.Module,
acpi_ids_module: *std.Build.Module,
name: []const u8,
root: []const u8,
) *std.Build.Step.Compile {
@@ -81,6 +83,12 @@ fn addUserBinary(
.{ .name = "posix", .module = posix_module },
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
.{ .name = "mmio", .module = mmio_module },
// Keyboard layouts (keycode + modifiers -> keysym/character), available
// to any program that wants it. See library/xkeyboard-config/.
.{ .name = "xkeyboard-config", .module = xkeyboard_config_module },
// ACPI/PnP hardware-ID registry, so drivers name devices
// (HardwareId.ps2_keyboard) instead of magic "_HID" strings.
.{ .name = "acpi-ids", .module = acpi_ids_module },
},
}),
});
@@ -224,7 +232,6 @@ pub fn build(b: *std.Build) void {
.{ .name = "layouts", .module = xkb_layouts_module },
},
});
_ = xkeyboard_config_module;
// The POSIX / C compatibility layer, a separate library layered strictly over the
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
@@ -309,7 +316,7 @@ pub fn build(b: *std.Build) void {
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the `runtime` runtime library, and
// started in ring 3 by the kernel's user-ELF loader.
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "init", "system/services/init/init.zig");
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
b.getInstallStep().dependOn(&init_install.step);
@@ -317,21 +324,21 @@ pub fn build(b: *std.Build) void {
// Each is built by the same user-binary recipe, then packed into one image by
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs", "system/services/vfs/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs-test", "system/services/vfs/vfs-test.zig");
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "hpet", "system/drivers/hpet/hpet.zig");
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/bus/bus.zig");
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/ps2-bus/ps2-bus.zig");
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "device-manager", "system/services/device-manager/device-manager.zig");
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "hpet", "system/drivers/hpet/hpet.zig");
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "bus", "system/drivers/bus/bus.zig");
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "bus", "system/drivers/ps2-bus/ps2-bus.zig");
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input", "system/services/input/input.zig");
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-source", "system/services/input-source/input-source.zig");
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-test", "system/services/input-test/input-test.zig");
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "args-echo", "system/services/args-echo/args-echo.zig");
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "process-test", "system/services/process-test/process-test.zig");
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig");
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
// (the container format is trivial, and Python sidesteps std API churn). Args:
@@ -507,6 +514,8 @@ pub fn build(b: *std.Build) void {
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
"system/devices/acpi-ids.zig", // _HID name decoding
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
}) |root| {
const mod_tests = b.addTest(.{
.root_module = b.createModule(.{
+30 -16
View File
@@ -108,22 +108,36 @@ the service delivers to its endpoint, which only the same thread could receive).
## Status and follow-ups
- **Synthetic sources, for now.** The `ps2-bus` driver owns PNP0303, which carries *both*
the 0x60/0x64 ports and IRQ1, so reading real scancodes/packets has to live in the bus,
not in [keyboard.zig](../system/drivers/ps2-bus/keyboard.zig) /
[mouse.zig](../system/drivers/ps2-bus/mouse.zig). Until that lands, the keyboard driver
publishes a synthetic key stream, the mouse driver a synthetic motion/click stream, and
the hardware-free `input-source` rotates through all three classes (including a synthetic
joystick, which has no driver yet) — all via the `input.synthetic*Event` helpers. The
fan-out and per-device routing are real; only the bytes are placeholder. **Follow-up:** the
bus binds IRQ1/IRQ12, reads port 0x60, and `ps2-library` decodes scan-set-1 →
keycodes and mouse packets; the drivers publish decoded events.
- **Keycode → character** is a keymap, and danos has one:
[`library/xkeyboard-config`](../library/xkeyboard-config/README.md) compiles the X11
xkeyboard-config layouts (us, gb, de, fr, …) to native Zig —
`xkb.map(layout, keycode, mods)` → keysym + Unicode character. Wiring it in to fill a
`key_press` event's `character` (in the keyboard driver, or a small keymap service) is the
natural next step.
- **The keyboard is real.** The `ps2-bus` driver owns PNP0303, which carries *both* the
0x60/0x64 ports and IRQ1, so reading the hardware lives in the bus, not in
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig): the bus binds IRQ1 and, on each
interrupt, drains port 0x60, routing every byte by the status register's
auxiliary-output bit to whichever child driver **attached** for that device (an
`AttachRequest` to the well-known `ps2_bus` service, carrying the child's endpoint as a
capability; the bytes then arrive as asynchronous `ForwardedByte` messages, so the IRQ
path never blocks on a child). The keyboard driver decodes the stream — scancode **set 2**,
what the keyboard sends with the 8042's legacy translation off, decoded by
[scancode.zig](../system/drivers/ps2-bus/scancode.zig) into USB HID usage keycodes with
make/break, typematic-repeat, and modifier tracking (host-tested under `zig build test`) —
and publishes real `key_down`/`key_press`/`key_up` events.
- **Keycode → character** is wired in: the keyboard driver fills a `key_press` event's
`character` through [`library/xkeyboard-config`](../library/xkeyboard-config/README.md)
(`xkb.map(layout, keycode, mods)` → keysym + Unicode character), synthesizing the ASCII
control characters for Enter/Tab/Backspace/Escape, whose keysyms map to no Unicode. The
layout defaults to `us`; the bus can pass another as the driver's argv[2] — the seam for
a future settings source.
- **The mouse is real too.** IRQ12 is enumerated on the auxiliary device's own ACPI node
(PNP0F13), so the bus claims that node alongside the controller and routes both IRQs to
its one endpoint, acking whichever line the notification's badge names.
[mouse.zig](../system/drivers/ps2-bus/mouse.zig) attaches the way the keyboard does and
assembles the forwarded bytes with
[mouse-packet.zig](../system/drivers/ps2-bus/mouse-packet.zig) (three-byte stream-mode
packets: sync/overflow handling, nine-bit movement, screen-convention `dy` — host-tested
under `zig build test`) into `button_down`/`button_up` transitions and `motion` events.
**Follow-up:** the IntelliMouse magic-knock for a scroll wheel (four-byte packets) and
`scroll` events. The hardware-free `input-source` still rotates through all three classes
synthetically (including a joystick, which has no driver yet) via the
`input.synthetic*Event` helpers.
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
Real backpressure/flow-control is future work.
- **`publish` is unauthenticated** — any process may publish, consistent with the current
+1
View File
@@ -124,6 +124,7 @@ pub const ProcessDescriptor = extern struct {
pub const ServiceId = enum(u32) {
vfs = 1,
input = 2,
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
_,
};
+116 -46
View File
@@ -1,60 +1,120 @@
//! ACPI / PnP hardware-ID (`_HID`) names: the flat analog of pci-class.zig for
//! `acpi_device` nodes. Unlike PCI, ACPI has no class/subclass/prog-IF taxonomy — a
//! device's identity *is* its `_HID` string (`PNP0303` simply means "PS/2 keyboard"),
//! so this is a plain id -> description registry rather than a hierarchical decoder.
//! so this is a plain id <-> name registry rather than a hierarchical decoder.
//! The well-known PnP/ACPI IDs; vendor-specific ids (e.g. `QEMU0002`, `INTC1234`) have
//! no standard name and return "". Pure reference data, so it is shared by kernel
//! discovery (the device-tree dump) and any user-space tool.
//! no standard name and decode to nothing. Pure reference data, so it is shared by
//! kernel discovery (the device-tree dump) and any user-space driver or tool.
//!
//! Code that means a specific device names the `HardwareId` variant instead of its
//! `_HID` string — `HardwareId.ps2_keyboard.hid()` reads without a registry lookup,
//! where a bare `"PNP0303"` does not.
const std = @import("std");
const Entry = struct { hid: []const u8, name: []const u8 };
/// The common standard PnP/ACPI hardware IDs, as named values. Prefix ranges hint at
/// the grouping (PNP03xx keyboards, PNP0Fxx pointing devices, PNP0Cxx ACPI
/// power/thermal, PNP0Axx buses), but there is no formal hierarchy — hence a flat
/// enum over a flat registry.
pub const HardwareId = enum {
programmable_interrupt_controller,
system_timer,
high_precision_event_timer,
dma_controller,
ps2_keyboard,
parallel_port,
ecp_parallel_port,
serial_port,
floppy_disk_controller,
system_speaker,
pci_bus,
generic_container,
/// The second id the ACPI spec assigns the same "Generic Container Device" name.
generic_container_extended,
pci_express_root_bridge,
real_time_clock,
system_board,
motherboard_reserved_resources,
math_coprocessor,
acpi_system_board,
embedded_controller,
control_method_battery,
fan,
power_button,
lid,
sleep_button,
pci_interrupt_link,
microsoft_ps2_mouse,
ps2_mouse,
ac_adapter,
processor_device,
processor_aggregator,
processor_container,
/// The common standard PnP/ACPI hardware IDs. Prefix ranges hint at the grouping
/// (PNP03xx keyboards, PNP0Fxx pointing devices, PNP0Cxx ACPI power/thermal,
/// PNP0Axx buses), but there is no formal hierarchy — hence a flat table.
const table = [_]Entry{
.{ .hid = "PNP0000", .name = "Programmable Interrupt Controller (PIC)" },
.{ .hid = "PNP0100", .name = "System Timer (PIT)" },
.{ .hid = "PNP0103", .name = "High Precision Event Timer (HPET)" },
.{ .hid = "PNP0200", .name = "DMA Controller" },
.{ .hid = "PNP0303", .name = "PS/2 Keyboard" },
.{ .hid = "PNP0400", .name = "Standard LPT Parallel Port" },
.{ .hid = "PNP0401", .name = "ECP Parallel Port" },
.{ .hid = "PNP0501", .name = "16550A-compatible Serial Port" },
.{ .hid = "PNP0700", .name = "PC Floppy Disk Controller" },
.{ .hid = "PNP0800", .name = "System Speaker" },
.{ .hid = "PNP0A03", .name = "PCI Bus" },
.{ .hid = "PNP0A05", .name = "Generic Container Device" },
.{ .hid = "PNP0A06", .name = "Generic Container Device" },
.{ .hid = "PNP0A08", .name = "PCI Express Root Bridge" },
.{ .hid = "PNP0B00", .name = "Real-Time Clock (RTC)" },
.{ .hid = "PNP0C01", .name = "System Board" },
.{ .hid = "PNP0C02", .name = "Motherboard Reserved Resources" },
.{ .hid = "PNP0C04", .name = "Math Coprocessor" },
.{ .hid = "PNP0C08", .name = "ACPI System Board" },
.{ .hid = "PNP0C09", .name = "ACPI Embedded Controller" },
.{ .hid = "PNP0C0A", .name = "ACPI Control Method Battery" },
.{ .hid = "PNP0C0B", .name = "ACPI Fan" },
.{ .hid = "PNP0C0C", .name = "ACPI Power Button" },
.{ .hid = "PNP0C0D", .name = "ACPI Lid" },
.{ .hid = "PNP0C0E", .name = "ACPI Sleep Button" },
.{ .hid = "PNP0C0F", .name = "PCI Interrupt Link Device" },
.{ .hid = "PNP0F03", .name = "Microsoft PS/2 Mouse" },
.{ .hid = "PNP0F13", .name = "PS/2 Mouse" },
.{ .hid = "ACPI0003", .name = "AC Adapter" },
.{ .hid = "ACPI0007", .name = "Processor Device" },
.{ .hid = "ACPI000C", .name = "Processor Aggregator" },
.{ .hid = "ACPI0010", .name = "Processor Container" },
const Entry = struct { hid: []const u8, name: []const u8 };
/// The registry row for this id: its `_HID` string and human-readable name.
fn entry(self: HardwareId) Entry {
return switch (self) {
.programmable_interrupt_controller => .{ .hid = "PNP0000", .name = "Programmable Interrupt Controller (PIC)" },
.system_timer => .{ .hid = "PNP0100", .name = "System Timer (PIT)" },
.high_precision_event_timer => .{ .hid = "PNP0103", .name = "High Precision Event Timer (HPET)" },
.dma_controller => .{ .hid = "PNP0200", .name = "DMA Controller" },
.ps2_keyboard => .{ .hid = "PNP0303", .name = "PS/2 Keyboard" },
.parallel_port => .{ .hid = "PNP0400", .name = "Standard LPT Parallel Port" },
.ecp_parallel_port => .{ .hid = "PNP0401", .name = "ECP Parallel Port" },
.serial_port => .{ .hid = "PNP0501", .name = "16550A-compatible Serial Port" },
.floppy_disk_controller => .{ .hid = "PNP0700", .name = "PC Floppy Disk Controller" },
.system_speaker => .{ .hid = "PNP0800", .name = "System Speaker" },
.pci_bus => .{ .hid = "PNP0A03", .name = "PCI Bus" },
.generic_container => .{ .hid = "PNP0A05", .name = "Generic Container Device" },
.generic_container_extended => .{ .hid = "PNP0A06", .name = "Generic Container Device" },
.pci_express_root_bridge => .{ .hid = "PNP0A08", .name = "PCI Express Root Bridge" },
.real_time_clock => .{ .hid = "PNP0B00", .name = "Real-Time Clock (RTC)" },
.system_board => .{ .hid = "PNP0C01", .name = "System Board" },
.motherboard_reserved_resources => .{ .hid = "PNP0C02", .name = "Motherboard Reserved Resources" },
.math_coprocessor => .{ .hid = "PNP0C04", .name = "Math Coprocessor" },
.acpi_system_board => .{ .hid = "PNP0C08", .name = "ACPI System Board" },
.embedded_controller => .{ .hid = "PNP0C09", .name = "ACPI Embedded Controller" },
.control_method_battery => .{ .hid = "PNP0C0A", .name = "ACPI Control Method Battery" },
.fan => .{ .hid = "PNP0C0B", .name = "ACPI Fan" },
.power_button => .{ .hid = "PNP0C0C", .name = "ACPI Power Button" },
.lid => .{ .hid = "PNP0C0D", .name = "ACPI Lid" },
.sleep_button => .{ .hid = "PNP0C0E", .name = "ACPI Sleep Button" },
.pci_interrupt_link => .{ .hid = "PNP0C0F", .name = "PCI Interrupt Link Device" },
.microsoft_ps2_mouse => .{ .hid = "PNP0F03", .name = "Microsoft PS/2 Mouse" },
.ps2_mouse => .{ .hid = "PNP0F13", .name = "PS/2 Mouse" },
.ac_adapter => .{ .hid = "ACPI0003", .name = "AC Adapter" },
.processor_device => .{ .hid = "ACPI0007", .name = "Processor Device" },
.processor_aggregator => .{ .hid = "ACPI000C", .name = "Processor Aggregator" },
.processor_container => .{ .hid = "ACPI0010", .name = "Processor Container" },
};
}
/// This id's `_HID` string (e.g. `.ps2_keyboard` -> "PNP0303").
pub fn hid(self: HardwareId) []const u8 {
return self.entry().hid;
}
/// This id's human-readable name (e.g. `.ps2_keyboard` -> "PS/2 Keyboard").
pub fn description(self: HardwareId) []const u8 {
return self.entry().name;
}
/// The named value for a `_HID` string, or null if it is not a known standard
/// id (vendor-specific ids are not in the registry).
pub fn fromHid(hid_string: []const u8) ?HardwareId {
for (std.enums.values(HardwareId)) |id| {
if (std.mem.eql(u8, id.hid(), hid_string)) return id;
}
return null;
}
};
/// The human-readable name for a `_HID`, or "" if it is not a known standard id
/// (vendor-specific ids have no registry name — callers just print the raw HID).
/// The human-readable name for a `_HID` string, or "" if it is not a known standard
/// id (vendor-specific ids have no registry name — callers just print the raw HID).
pub fn description(hid: []const u8) []const u8 {
for (table) |entry| {
if (std.mem.eql(u8, entry.hid, hid)) return entry.name;
}
return "";
return (HardwareId.fromHid(hid) orelse return "").description();
}
test "decodes standard PnP/ACPI ids and leaves the rest alone" {
@@ -66,3 +126,13 @@ test "decodes standard PnP/ACPI ids and leaves the rest alone" {
try eq("", description("QEMU0002")); // vendor-specific: no standard name
try eq("", description("")); // no HID at all
}
test "named values round-trip through their _HID strings" {
const testing = std.testing;
try testing.expectEqualStrings("PNP0303", HardwareId.ps2_keyboard.hid());
try testing.expectEqual(@as(?HardwareId, .ps2_mouse), HardwareId.fromHid("PNP0F13"));
try testing.expectEqual(@as(?HardwareId, null), HardwareId.fromHid("QEMU0002"));
for (std.enums.values(HardwareId)) |id| {
try testing.expectEqual(@as(?HardwareId, id), HardwareId.fromHid(id.hid()));
}
}
+12 -6
View File
@@ -17,6 +17,7 @@
const std = @import("std");
const boot_handoff = @import("boot-handoff");
const abi = @import("abi");
const acpi_ids = @import("acpi-ids");
const parameters = @import("parameters");
const device_model = @import("device-model.zig");
const aml = @import("aml/aml.zig");
@@ -914,7 +915,14 @@ fn readAdr(node: *aml.Node) ?u32 {
return @truncate(readIntObj(n.value, &p) orelse return null);
}
/// Whether a namespace device is a PCI(e) host bridge (`PNP0A03` / `PNP0A08`).
/// Whether a `_HID` string names a PCI(e) host bridge.
fn isPciRootHid(hid: []const u8) bool {
const id = acpi_ids.HardwareId.fromHid(hid) orelse return false;
return id == .pci_bus or id == .pci_express_root_bridge;
}
/// Whether a namespace device is a PCI(e) host bridge. A packed EISA id is decoded
/// to its string form first, so both encodings answer through the one registry.
fn isPciRootNode(node: *aml.Node) bool {
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return false;
if (hid.kind != .name or hid.value.len == 0) return false;
@@ -922,12 +930,10 @@ fn isPciRootNode(node: *aml.Node) bool {
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
var p: usize = 0;
const n = readIntObj(hid.value, &p) orelse return false;
return n == 0x030AD041 or n == 0x080AD041; // PNP0A03 / PNP0A08
},
0x0D => {
const s = cstr(hid.value[1..]);
return std.mem.eql(u8, s, "PNP0A03") or std.mem.eql(u8, s, "PNP0A08");
var buffer: [8]u8 = undefined;
return isPciRootHid(eisaIdToStr(@truncate(n), &buffer));
},
0x0D => return isPciRootHid(cstr(hid.value[1..])),
else => return false,
}
}
+141 -16
View File
@@ -1,21 +1,76 @@
//! PS/2 Keyboard Driver
//!
//! Spawned by the ps2-bus driver once the controller is initialized and port 1
//! Spawned by the ps2-bus driver once the controller is initialized and the port
//! has passed its interface test and device reset. The bus driver hands us our
//! device HID as argv[1]; we use it to locate our own device descriptor.
//! device HID as argv[1] and, optionally, a layout name (`"us"`, `"gb"`, ...) as
//! argv[2].
//!
//! The 8042's ports (0x60/0x64) and IRQ1 live on the PNP0303 node, which the
//! ps2-bus driver exclusively owns — so this driver never touches the hardware.
//! Instead it **attaches** to the bus (handing over its endpoint as a capability)
//! and receives every scancode byte as a forwarded asynchronous message. Each byte
//! feeds the set-2 decoder; a decoded key becomes input-protocol events:
//!
//! scancode byte -> HID usage keycode -> key_down / key_up
//! -> xkeyboard-config -> character -> key_press
const std = @import("std");
const runtime = @import("runtime");
const xkb = @import("xkeyboard-config");
const ps2 = @import("ps2-library.zig");
const scancode = @import("scancode.zig");
const device = runtime.device;
const ipc = runtime.ipc;
const protocol = runtime.input_protocol;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
pub fn main() void {
const hid = runtime.argument(1);
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
/// registering) is still coming up.
fn lookupBus() ?ipc.Handle {
var attempts: usize = 0;
while (attempts < 100) : (attempts += 1) {
if (ipc.lookup(.ps2_bus)) |handle| return handle;
runtime.system.sleep(50);
}
return null;
}
/// The character a pressed key produces under `modifiers`, or 0 for none. The
/// layout lookup answers for printable keys; the keys whose keysym has no Unicode
/// mapping but that every consumer still expects as a character (Enter, Tab,
/// Backspace, Escape) are given their ASCII control characters here.
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: scancode.ModifierSnapshot) u32 {
const mapping = xkb.map(layout, usage, .{
.shift = modifiers.shift,
.caps_lock = modifiers.caps_lock,
.level3 = modifiers.right_alt,
.control = modifiers.control,
});
if (mapping.character) |character| return character;
return switch (@as(protocol.Keycode, @enumFromInt(usage))) {
.enter, .keypad_enter => '\n',
.tab => '\t',
.backspace => 0x08,
.escape => 0x1B,
else => 0,
};
}
/// The input protocol's modifier word for a snapshot.
fn modifierWord(modifiers: scancode.ModifierSnapshot) u32 {
var word: u32 = 0;
if (modifiers.shift) word |= protocol.modifier_shift;
if (modifiers.control) word |= protocol.modifier_control;
if (modifiers.alt) word |= protocol.modifier_alt;
return word;
}
pub fn main(init: runtime.process.Init) void {
const hid = init.arguments.get(1).?;
if (hid.len == 0) {
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no HID argument\n");
return;
@@ -31,25 +86,95 @@ pub fn main() void {
return;
}
// The 8042 ports (0x60/0x64) and this keyboard's IRQ1 both live on the same
// PNP0303 node, which the ps2-bus driver exclusively owns — so the keyboard is
// served through the bus and does not claim the controller itself.
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: served by ps2-bus (controller owned by bus)\n");
// The layout is a spawn argument so a later settings source can choose it;
// absent (as today) it defaults to us.
const layout_name = init.arguments.get(2) orelse "us";
const layout = xkb.byName(layout_name) orelse xkb.us;
writeLine("system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
// Broadcast keyboard events through the input service so programs can listen for them
// (docs/input.md). Until the bus reads real IRQ1 scancodes and hands them here (a
// follow-up), we publish the same synthetic stand-in stream the demo source uses — the
// fan-out path is real, only the source of the bytes is placeholder.
// Attach to the bus: hand it our endpoint, and it forwards every byte the
// keyboard sends (it owns the controller; we own the decoding).
const bus = lookupBus() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
return;
};
const endpoint = ipc.createIpcEndpoint() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no endpoint\n");
return;
};
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: attach call failed\n");
return;
};
if (attached.len < @sizeOf(ps2.AttachReply) or
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
{
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: attach refused\n");
return;
}
// Broadcast keyboard events through the input service so programs can listen
// for them (docs/input.md).
var source = runtime.input.connectSource() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: input service unavailable\n");
return;
};
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n");
var step: usize = 0;
while (true) : (step +%= 1) {
_ = source.publishKeyboardEvent(runtime.input.syntheticKeyEvent(step));
runtime.system.sleep(200);
var decoder = scancode.Decoder{};
var state = scancode.KeyboardState{};
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
while (true) {
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
const key = decoder.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
const transition = state.apply(key);
const modifiers = modifierWord(transition.modifiers);
switch (transition.action) {
.pressed => {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(protocol.EventKind.key_down),
.keycode = key.usage,
.character = 0,
.modifiers = modifiers,
});
const character = characterFor(layout, key.usage, transition.modifiers);
if (character != 0) {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(protocol.EventKind.key_press),
.keycode = key.usage,
.character = character,
.modifiers = modifiers,
});
}
},
// Typematic repeat: the key did not physically go down again, so no
// key_down — but it keeps producing its character.
.repeated => {
const character = characterFor(layout, key.usage, transition.modifiers);
if (character != 0) {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(protocol.EventKind.key_press),
.keycode = key.usage,
.character = character,
.modifiers = modifiers,
});
}
},
.released => {
_ = source.publishKeyboardEvent(.{
.kind = @intFromEnum(protocol.EventKind.key_up),
.keycode = key.usage,
.character = 0,
.modifiers = modifiers,
});
},
}
}
}
+143
View File
@@ -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);
}
+101 -20
View File
@@ -1,21 +1,55 @@
//! PS/2 Mouse Driver
//!
//! Spawned by the ps2-bus driver once the controller is initialized and port 2
//! Spawned by the ps2-bus driver once the controller is initialized and the port
//! has passed its interface test and device reset. The bus driver hands us our
//! device HID as argv[1]; we use it to locate our own device descriptor.
//! device HID as argv[1].
//!
//! Like the keyboard, this driver never touches the hardware: the 8042's ports
//! and both port IRQs are owned by the ps2-bus driver (the auxiliary port's
//! IRQ12 lives on the PNP0F13 node, which the bus claims alongside the
//! controller). The driver **attaches** to the bus and receives every byte the
//! mouse sends as a forwarded asynchronous message. The bytes assemble into
//! three-byte packets, and each packet becomes input-protocol events:
//!
//! packet -> button transitions -> button_down / button_up
//! -> movement -> motion (dx/dy, screen convention)
const std = @import("std");
const runtime = @import("runtime");
const ps2 = @import("ps2-library.zig");
const mouse_packet = @import("mouse-packet.zig");
const device = runtime.device;
const ipc = runtime.ipc;
const protocol = runtime.input_protocol;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
pub fn main() void {
const hid = runtime.argument(1);
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
/// registering) is still coming up.
fn lookupBus() ?ipc.Handle {
var attempts: usize = 0;
while (attempts < 100) : (attempts += 1) {
if (ipc.lookup(.ps2_bus)) |handle| return handle;
runtime.system.sleep(50);
}
return null;
}
/// The protocol's pressed-button bitmask for a packet.
fn buttonMask(packet: mouse_packet.Packet) u32 {
var mask: u32 = 0;
if (packet.left) mask |= protocol.mouse_button_left;
if (packet.right) mask |= protocol.mouse_button_right;
if (packet.middle) mask |= protocol.mouse_button_middle;
return mask;
}
pub fn main(init: runtime.process.Init) void {
const hid = init.arguments.get(1).?;
if (hid.len == 0) {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: no HID argument\n");
return;
@@ -26,34 +60,81 @@ pub fn main() void {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: out of memory\n");
return;
};
const mouse_device_descriptor = device.findDeviceDescriptorByHid(buffer, hid) orelse {
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
writeLine("system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
return;
};
}
// The mouse's own node (PNP0F13) carries IRQ12 and is not claimed by the bus,
// so this driver takes exclusive ownership of it. Port IO still goes through
// the bus, which owns the shared 8042 ports.
if (!device.claim(mouse_device_descriptor.id)) {
writeLine("system/drivers/ps2-bus/mouse: unable to claim device for hid {s}\n", .{hid});
// Attach to the bus: hand it our endpoint, and it forwards every byte the
// mouse sends (it owns the controller; we own the decoding).
const bus = lookupBus() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ps2-bus service unavailable\n");
return;
};
const endpoint = ipc.createIpcEndpoint() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: no endpoint\n");
return;
};
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.mouse) };
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: attach call failed\n");
return;
};
if (attached.len < @sizeOf(ps2.AttachReply) or
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
{
_ = runtime.system.write("system/drivers/ps2-bus/mouse: attach refused\n");
return;
}
writeLine("system/drivers/ps2-bus/mouse: claimed device for hid {s}\n", .{hid});
// Broadcast mouse events through the input service so programs can listen for them
// (docs/input.md). As with the keyboard, decoding real PS/2 mouse packets is a
// follow-up; for now we publish the synthetic stand-in stream. The fan-out path is
// real, only the source of the movement is placeholder.
// Broadcast mouse events through the input service so programs can listen
// for them (docs/input.md).
var source = runtime.input.connectSource() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: input service unavailable\n");
return;
};
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n");
var step: usize = 0;
while (true) : (step +%= 1) {
_ = source.publishMouseEvent(runtime.input.syntheticMouseEvent(step));
runtime.system.sleep(200);
var assembler = mouse_packet.Assembler{};
var buttons: u32 = 0;
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
while (true) {
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
const packet = assembler.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
const new_buttons = buttonMask(packet);
// A button transition per changed button, carrying the new whole mask.
const changed = buttons ^ new_buttons;
for ([_]u32{ protocol.mouse_button_left, protocol.mouse_button_right, protocol.mouse_button_middle }) |button| {
if (changed & button == 0) continue;
const kind: protocol.MouseEventKind = if (new_buttons & button != 0) .button_down else .button_up;
_ = source.publishMouseEvent(.{
.kind = @intFromEnum(kind),
.button = button,
.dx = 0,
.dy = 0,
.scroll_x = 0,
.scroll_y = 0,
.buttons = new_buttons,
});
}
buttons = new_buttons;
if (packet.dx != 0 or packet.dy != 0) {
_ = source.publishMouseEvent(.{
.kind = @intFromEnum(protocol.MouseEventKind.motion),
.button = 0,
.dx = packet.dx,
.dy = packet.dy,
.scroll_x = 0,
.scroll_y = 0,
.buttons = new_buttons,
});
}
}
}
+159 -19
View File
@@ -11,8 +11,10 @@
//! - irq 0xc len 0x1
const std = @import("std");
const runtime = @import("runtime");
const acpi_ids = @import("acpi-ids");
const ps2 = @import("ps2-library.zig");
const device = runtime.device;
const ipc = runtime.ipc;
/// Format one whole log line and emit it in a single `debug_write`, so output
/// from the child drivers (which run concurrently) can never interleave with it.
@@ -23,22 +25,68 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
/// Ask the device on `port` what it is, then spawn the matching driver from the
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
/// from what the device reports, not from the port number.
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) void {
/// from what the device reports, not from the port number. Returns the identified
/// type so the forwarding loop can route that port's bytes to the driver once it
/// attaches, or null if nothing was spawned.
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
const device_type = controller.identifyDevice(port) orelse {
writeLine("system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
return;
return null;
};
const driver_name = device_type.driverName() orelse {
writeLine("system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
return;
return null;
};
const hid = device_type.hid() orelse "";
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
writeLine("system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
} else {
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
return device_type;
}
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
return null;
}
/// Resource index of the controller's IRQ (IRQ1) on the PNP0303 descriptor, found
/// the way the ports are found in `Controller.init`.
fn findInterruptResourceIndex(descriptor: device.DeviceDescriptor) ?u64 {
for (0..descriptor.resource_count) |index| {
if (descriptor.resources[index].kind == @intFromEnum(device.ResourceKind.irq)) return index;
}
return null;
}
/// Forwarding endpoints of the attached child drivers, indexed by `ps2.Port`.
/// Written when a child's `AttachRequest` arrives, read on every forwarded byte.
var port_endpoints = [_]?ipc.Handle{ null, null };
/// Which device type each port identified as, so an attaching child (which knows
/// its type, not its port) can be matched to the right port's byte stream.
var port_device_types = [_]?ps2.DeviceType{ null, null };
/// Handle a child driver's `AttachRequest`: record the endpoint capability it
/// passed as the forwarding target for the port whose device matches its type.
/// Writes an `AttachReply` into `out` and returns its length.
fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
const reply = struct {
fn write(buffer: []u8, status: ps2.AttachStatus) usize {
const header = ps2.AttachReply{ .status = @intFromEnum(status) };
@memcpy(buffer[0..@sizeOf(ps2.AttachReply)], std.mem.asBytes(&header));
return @sizeOf(ps2.AttachReply);
}
};
if (message.len < @sizeOf(ps2.AttachRequest)) return reply.write(out, .invalid_request);
const request = std.mem.bytesToValue(ps2.AttachRequest, message[0..@sizeOf(ps2.AttachRequest)]);
const endpoint = got.cap orelse return reply.write(out, .missing_endpoint);
for (&port_device_types, 0..) |maybe_type, port_index| {
const device_type = maybe_type orelse continue;
if (@intFromEnum(device_type) != request.device_type) continue;
port_endpoints[port_index] = endpoint;
writeLine("system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
return reply.write(out, .ok);
}
return reply.write(out, .no_such_device);
}
pub fn main() void {
@@ -48,10 +96,12 @@ pub fn main() void {
};
var has_two_channels = false;
var maybe_controller: ?ps2.Controller = null;
var maybe_interrupt_index: ?u64 = null;
// The 8042's IO ports (0x60/0x64) are enumerated under the keyboard ACPI node
// (PNP0303), so we init the controller from that descriptor — but which device
// is on which port is decided later by identify, not by this HID.
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, "PNP0303");
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_keyboard.hid());
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
_ = runtime.system.write("system/drivers/ps2-bus: found PS/2 controller\n");
_ = runtime.system.write("system/drivers/ps2-bus: initializing controller\n");
@@ -65,6 +115,8 @@ pub fn main() void {
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IO ports\n");
return;
};
maybe_controller = controller;
maybe_interrupt_index = findInterruptResourceIndex(controller_device_descriptor);
controller.disablePort(.One);
controller.disablePort(.Two);
@@ -126,18 +178,12 @@ pub fn main() void {
return;
}
// enable the working ports and, via a read-modify-write, their interrupts
// Enable the working ports. Their interrupts stay off until IRQ1 is bound
// below — reset and identify use polled reads, which must never race the
// interrupt-driven drain loop for bytes.
controller.enablePort(.One);
if (port_two_works) controller.enablePort(.Two);
var configuration = controller.readConfigurationByte() orelse {
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
return;
};
if (port_one_works) configuration |= ps2.Port.One.interruptBit();
if (port_two_works) configuration |= ps2.Port.Two.interruptBit();
_ = controller.writeConfigurationByte(configuration);
// reset each working device; a failing device is logged but does not
// abort bring-up of the other one
if (port_one_works) {
@@ -158,15 +204,109 @@ pub fn main() void {
// Identify the device on each working port and hand it off to the driver
// that matches what it reported — a port is not assumed to be a keyboard
// or a mouse by its number.
if (port_one_works) spawnIdentifiedDriver(controller, .One);
if (port_two_works) spawnIdentifiedDriver(controller, .Two);
if (port_one_works) port_device_types[@intFromEnum(ps2.Port.One)] = spawnIdentifiedDriver(controller, .One);
if (port_two_works) port_device_types[@intFromEnum(ps2.Port.Two)] = spawnIdentifiedDriver(controller, .Two);
} else {
_ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n");
return;
}
const controller = maybe_controller.?;
const interrupt_index = maybe_interrupt_index orelse {
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IRQ\n");
return;
};
// The endpoint the child drivers attach to and IRQ1 wakes. Registered under a
// well-known id so the children can find it, the way input subscribers find
// the input service.
const endpoint = ipc.createIpcEndpoint() orelse {
_ = runtime.system.write("system/drivers/ps2-bus: no endpoint\n");
return;
};
if (!ipc.register(.ps2_bus, endpoint)) {
_ = runtime.system.write("system/drivers/ps2-bus: register failed\n");
return;
}
// From here on, only the interrupt path reads the data port. Drop anything a
// device sent between enable-scanning and now, bind the IRQs, and only then
// let the controller raise them — an interrupt with nobody bound is lost.
controller.drainOutputBuffer();
if (!device.irqBind(controller.device_id, interrupt_index, endpoint)) {
_ = runtime.system.write("system/drivers/ps2-bus: irq_bind failed\n");
return;
}
// Port 2's interrupt (IRQ12) is enumerated on the auxiliary device's own ACPI
// node (PNP0F13), not on the controller's — so if port 2 carries a device,
// claim that node too and route its IRQ to the same endpoint. The IRQ belongs
// to the *port*, whatever device identify found on it.
var maybe_auxiliary_interrupt: ?struct { device_id: u64, interrupt_index: u64, gsi: u64 } = null;
if (port_device_types[@intFromEnum(ps2.Port.Two)] != null) {
if (device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_mouse.hid())) |descriptor| {
if (findInterruptResourceIndex(descriptor)) |auxiliary_index| {
if (device.claim(descriptor.id) and device.irqBind(descriptor.id, auxiliary_index, endpoint)) {
maybe_auxiliary_interrupt = .{
.device_id = descriptor.id,
.interrupt_index = auxiliary_index,
.gsi = descriptor.resources[auxiliary_index].start,
};
} else {
_ = runtime.system.write("system/drivers/ps2-bus: auxiliary irq_bind failed\n");
}
}
}
}
var configuration = controller.readConfigurationByte() orelse {
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
return;
};
if (port_device_types[@intFromEnum(ps2.Port.One)] != null) configuration |= ps2.Port.One.interruptBit();
if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.Two.interruptBit();
_ = controller.writeConfigurationByte(configuration);
_ = runtime.system.write("system/drivers/ps2-bus: ok\n");
while (true) runtime.system.sleep(1000);
// The forwarding loop: an IRQ1 notification drains the output buffer, routing
// each byte to the attached driver of the port it came from; a client message
// is a child driver's AttachRequest.
var reply_buffer: [@sizeOf(ps2.AttachReply)]u8 = undefined;
var reply_len: usize = 0;
var receive: [@sizeOf(ps2.AttachRequest)]u8 = undefined;
while (true) {
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
if (got.isNotification()) {
reply_len = 0;
if (got.isMessage() or got.isChildExit()) continue; // nothing sends us these
while (true) {
const current_status = ps2.status(controller.device_id, controller.status_index);
if (current_status & ps2.status_output_buffer_full == 0) break;
const byte = device.ioRead(controller.device_id, controller.data_index, 0, 1) orelse break;
const port: ps2.Port = if (current_status & ps2.status_auxiliary_output != 0) .Two else .One;
if (port_endpoints[@intFromEnum(port)]) |child| {
const forwarded = ps2.ForwardedByte{ .port = @intFromEnum(port), .byte = byte };
_ = ipc.send(child, std.mem.asBytes(&forwarded));
}
// An unattached port's byte is dropped — e.g. a keystroke before
// the keyboard driver has attached.
}
// Re-arm the line that woke us: the notification badge carries the
// GSI, and IRQ1 and IRQ12 are acked through different device claims.
if (maybe_auxiliary_interrupt) |auxiliary| {
if (got.source() == auxiliary.gsi) {
_ = device.irqAck(auxiliary.device_id, auxiliary.interrupt_index);
} else {
_ = device.irqAck(controller.device_id, interrupt_index);
}
} else {
_ = device.irqAck(controller.device_id, interrupt_index);
}
continue;
}
reply_len = handleAttach(receive[0..got.len], got, &reply_buffer);
}
}
pub const panic = runtime.panic;
+56 -9
View File
@@ -1,6 +1,7 @@
//! shared definitions between the different PS/2 drivers
const std = @import("std");
const runtime = @import("runtime");
const acpi_ids = @import("acpi-ids");
const device = runtime.device;
const system = runtime.system;
@@ -49,12 +50,15 @@ pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to th
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU
/// PS/2 status register bits (read from the status port, 0x64). Bits 4 and 5
/// are chipset-specific and intentionally omitted.
/// PS/2 status register bits (read from the status port, 0x64). Bit 4 is
/// chipset-specific and intentionally omitted.
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
/// Chipset-specific in the original spec, universal in practice on dual-channel
/// controllers: set = the waiting byte came from the second port (the mouse).
pub const status_auxiliary_output: u8 = 1 << 5;
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error
@@ -237,11 +241,12 @@ pub const Port = enum(u2) {
};
/// The kind of device attached to a port, as reported by the device itself in
/// response to the identify command — not assumed from the port number.
pub const DeviceType = enum {
keyboard,
mouse,
unknown,
/// response to the identify command — not assumed from the port number. Fixed
/// `u32` values because the type also travels in an `AttachRequest`.
pub const DeviceType = enum(u32) {
keyboard = 0,
mouse = 1,
unknown = 2,
/// Initial-ramdisk name of the driver that serves this device type, or null
/// if we could not classify it.
@@ -257,13 +262,55 @@ pub const DeviceType = enum {
/// its command-line argument, or null if we could not classify it.
pub fn hid(self: DeviceType) ?[]const u8 {
return switch (self) {
.keyboard => "PNP0303",
.mouse => "PNP0F13",
.keyboard => acpi_ids.HardwareId.ps2_keyboard.hid(),
.mouse => acpi_ids.HardwareId.ps2_mouse.hid(),
.unknown => null,
};
}
};
// --- the bus <-> child-driver forwarding protocol -----------------------------
//
// The 8042's ports and IRQ1 live on the PNP0303 node that only the ps2-bus driver
// claims, so the child device drivers (ps2-keyboard, ps2-mouse) cannot read port
// 0x60 themselves. Instead each child **attaches**: it calls the bus's well-known
// `ps2_bus` endpoint with an `AttachRequest`, handing over its own endpoint as the
// call's capability. From then on the bus forwards every byte the device sends as
// a `ForwardedByte` via the asynchronous `ipc.send` — the IRQ path in the bus can
// never block on a slow child, and the child never touches the controller.
/// A child driver registering for its device's bytes. `device_type` is a
/// `DeviceType` value; the child's receive endpoint travels as the call's
/// capability (`send_cap`).
pub const AttachRequest = extern struct {
device_type: u32,
};
/// How the bus answered an `AttachRequest` (`AttachReply.status`).
pub const AttachStatus = enum(i32) {
ok = 0,
/// The request was malformed (too short to be an `AttachRequest`).
invalid_request = -1,
/// The call carried no endpoint capability to forward to.
missing_endpoint = -2,
/// No port identified a device of the requested type.
no_such_device = -3,
};
/// Reply to an `AttachRequest`. `status` is an `AttachStatus` value.
pub const AttachReply = extern struct {
status: i32,
_padding: u32 = 0,
};
/// One raw byte read from the data port, forwarded to the attached child whose
/// port it came from (routed by the status register's auxiliary-output bit).
pub const ForwardedByte = extern struct {
/// The `Port` the byte came from, as `@intFromEnum`.
port: u32,
byte: u32,
};
/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
/// drive the controller through its methods; there is only ever one 8042 per
/// machine, but holding the resolved resource indices in an instance keeps the
+389
View File
@@ -0,0 +1,389 @@
//! PS/2 scancode set 2 → USB HID usage decoding, plus the keyboard state a driver
//! needs on top of it (pressed keys, modifier tracking, caps-lock toggle).
//!
//! Set 2 is what a keyboard sends when the 8042's legacy set-1 translation is off —
//! which is how ps2-bus.zig deliberately configures the controller. A key's **make**
//! code is one byte (two with an `E0` prefix for the "extended" keys added after the
//! original AT layout); its **break** code is the same code behind an `F0` prefix.
//! Pause alone is an eight-byte `E1` sequence with no break.
//!
//! The output vocabulary is USB HID keyboard-page usages (a=4, enter=40, ...), the
//! same numbering the input protocol's `Keycode` and the xkeyboard-config layout
//! tables use — so a decoded usage indexes a layout directly.
//!
//! 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");
// --- USB HID usages the state machine itself needs to recognize --------------
pub const usage_caps_lock: u8 = 0x39;
pub const usage_left_control: u8 = 0xE0;
pub const usage_left_shift: u8 = 0xE1;
pub const usage_left_alt: u8 = 0xE2;
pub const usage_right_control: u8 = 0xE4;
pub const usage_right_shift: u8 = 0xE5;
pub const usage_right_alt: u8 = 0xE6; // AltGr — selects XKB level 3
// --- scancode set 2 → HID usage tables ---------------------------------------
/// Single-byte (non-`E0`) make codes. Zero means "no key" — protocol bytes (ACK,
/// BAT results) and reserved codes land there and decode to nothing.
pub const set2_base: [256]u8 = blk: {
var table = [_]u8{0} ** 256;
// function row
table[0x01] = 0x42; // F9
table[0x03] = 0x3E; // F5
table[0x04] = 0x3C; // F3
table[0x05] = 0x3A; // F1
table[0x06] = 0x3B; // F2
table[0x07] = 0x45; // F12
table[0x09] = 0x43; // F10
table[0x0A] = 0x41; // F8
table[0x0B] = 0x3F; // F6
table[0x0C] = 0x3D; // F4
table[0x78] = 0x44; // F11
table[0x83] = 0x40; // F7
// letters
table[0x1C] = 0x04; // A
table[0x32] = 0x05; // B
table[0x21] = 0x06; // C
table[0x23] = 0x07; // D
table[0x24] = 0x08; // E
table[0x2B] = 0x09; // F
table[0x34] = 0x0A; // G
table[0x33] = 0x0B; // H
table[0x43] = 0x0C; // I
table[0x3B] = 0x0D; // J
table[0x42] = 0x0E; // K
table[0x4B] = 0x0F; // L
table[0x3A] = 0x10; // M
table[0x31] = 0x11; // N
table[0x44] = 0x12; // O
table[0x4D] = 0x13; // P
table[0x15] = 0x14; // Q
table[0x2D] = 0x15; // R
table[0x1B] = 0x16; // S
table[0x2C] = 0x17; // T
table[0x3C] = 0x18; // U
table[0x2A] = 0x19; // V
table[0x1D] = 0x1A; // W
table[0x22] = 0x1B; // X
table[0x35] = 0x1C; // Y
table[0x1A] = 0x1D; // Z
// digit row
table[0x16] = 0x1E; // 1
table[0x1E] = 0x1F; // 2
table[0x26] = 0x20; // 3
table[0x25] = 0x21; // 4
table[0x2E] = 0x22; // 5
table[0x36] = 0x23; // 6
table[0x3D] = 0x24; // 7
table[0x3E] = 0x25; // 8
table[0x46] = 0x26; // 9
table[0x45] = 0x27; // 0
// control and whitespace
table[0x5A] = 0x28; // Enter
table[0x76] = 0x29; // Escape
table[0x66] = 0x2A; // Backspace
table[0x0D] = 0x2B; // Tab
table[0x29] = 0x2C; // Space
// punctuation
table[0x4E] = 0x2D; // - _
table[0x55] = 0x2E; // = +
table[0x54] = 0x2F; // [ {
table[0x5B] = 0x30; // ] }
table[0x5D] = 0x31; // \ | (non-US hash on ISO boards, same position)
table[0x4C] = 0x33; // ; :
table[0x52] = 0x34; // ' "
table[0x0E] = 0x35; // ` ~
table[0x41] = 0x36; // , <
table[0x49] = 0x37; // . >
table[0x4A] = 0x38; // / ?
table[0x61] = 0x64; // non-US backslash (the extra ISO key between shift and Z)
// locks
table[0x58] = usage_caps_lock;
table[0x77] = 0x53; // Num Lock
table[0x7E] = 0x47; // Scroll Lock
// keypad
table[0x7C] = 0x55; // keypad *
table[0x7B] = 0x56; // keypad -
table[0x79] = 0x57; // keypad +
table[0x69] = 0x59; // keypad 1
table[0x72] = 0x5A; // keypad 2
table[0x7A] = 0x5B; // keypad 3
table[0x6B] = 0x5C; // keypad 4
table[0x73] = 0x5D; // keypad 5
table[0x74] = 0x5E; // keypad 6
table[0x6C] = 0x5F; // keypad 7
table[0x75] = 0x60; // keypad 8
table[0x7D] = 0x61; // keypad 9
table[0x70] = 0x62; // keypad 0
table[0x71] = 0x63; // keypad .
// modifiers
table[0x14] = usage_left_control;
table[0x12] = usage_left_shift;
table[0x11] = usage_left_alt;
table[0x59] = usage_right_shift;
break :blk table;
};
/// `E0`-prefixed make codes. `E0 12` is the "fake shift" the keyboard wraps around
/// Print Screen and navigation keys when a real shift is involved; it maps to zero
/// here, so it decodes to nothing and only the real key comes through.
pub const set2_extended: [256]u8 = blk: {
var table = [_]u8{0} ** 256;
table[0x11] = usage_right_alt;
table[0x14] = usage_right_control;
table[0x1F] = 0xE3; // left GUI
table[0x27] = 0xE7; // right GUI
table[0x2F] = 0x65; // application (menu)
table[0x7C] = 0x46; // Print Screen (arrives as E0 12 E0 7C; the E0 12 decodes to nothing)
table[0x4A] = 0x54; // keypad /
table[0x5A] = 0x58; // keypad Enter
table[0x70] = 0x49; // Insert
table[0x6C] = 0x4A; // Home
table[0x7D] = 0x4B; // Page Up
table[0x71] = 0x4C; // Delete
table[0x69] = 0x4D; // End
table[0x7A] = 0x4E; // Page Down
table[0x74] = 0x4F; // right arrow
table[0x6B] = 0x50; // left arrow
table[0x72] = 0x51; // down arrow
table[0x75] = 0x52; // up arrow
break :blk table;
};
// --- the byte-stream decoder --------------------------------------------------
/// One decoded key transition: which key (as a USB HID usage) and whether this is
/// a make (press or typematic repeat) or a break (release).
pub const DecodedKey = struct {
usage: u8,
make: bool,
};
/// Turns the raw set-2 byte stream into `DecodedKey`s. Feed it every byte the
/// keyboard sends; most bytes complete a key and return one, prefix bytes return
/// null and arm the state machine for the next byte.
pub const Decoder = struct {
const State = enum {
idle,
extended, // saw E0
break_prefix, // saw F0
extended_break, // saw E0 F0
pause_skip, // inside the 8-byte E1 Pause sequence
};
state: State = .idle,
/// Bytes still to swallow in `pause_skip`.
skip: u8 = 0,
/// The whole Pause make sequence is `E1 14 77 E1 F0 14 F0 77` — seven bytes
/// after the leading `E1`, and no break sequence ever follows.
const pause_bytes_after_e1: u8 = 7;
pub fn feed(self: *Decoder, byte: u8) ?DecodedKey {
switch (self.state) {
.idle => switch (byte) {
0xE0 => self.state = .extended,
0xF0 => self.state = .break_prefix,
0xE1 => {
self.state = .pause_skip;
self.skip = pause_bytes_after_e1;
},
// Anything else is a make code — or a protocol byte (0xFA ACK,
// 0xAA BAT-passed, 0xEE echo, ...), which the tables map to zero.
else => return decoded(set2_base[byte], true),
},
.extended => switch (byte) {
0xF0 => self.state = .extended_break,
else => {
self.state = .idle;
return decoded(set2_extended[byte], true);
},
},
.break_prefix => {
self.state = .idle;
return decoded(set2_base[byte], false);
},
.extended_break => {
self.state = .idle;
return decoded(set2_extended[byte], false);
},
.pause_skip => {
self.skip -= 1;
if (self.skip == 0) self.state = .idle;
},
}
return null;
}
fn decoded(usage: u8, make: bool) ?DecodedKey {
if (usage == 0) return null; // unmapped or a protocol byte
return .{ .usage = usage, .make = make };
}
};
// --- driver-side keyboard state -----------------------------------------------
/// What a key transition did, plus the modifier state to stamp on the resulting
/// events (snapshotted after the transition was applied).
pub const Transition = struct {
pub const Action = enum {
pressed, // physical make of a key that was up
repeated, // typematic make of a key already down — no new key_down
released, // physical break
};
action: Action,
modifiers: ModifierSnapshot,
};
/// The modifier state at one instant, in both vocabularies a driver needs: the
/// input protocol's coarse bits (shift/control/alt) and the level-selection
/// inputs xkeyboard-config takes (shift, caps_lock, AltGr as level3).
pub const ModifierSnapshot = struct {
shift: bool, // either shift held
control: bool, // either control held
alt: bool, // either alt held (including AltGr)
right_alt: bool, // AltGr specifically — the XKB level-3 selector
caps_lock: bool, // the toggle, not the key
};
/// Tracks which keys are physically down and the caps-lock toggle, and classifies
/// each decoded transition. Pure state — no IO — so repeat detection and modifier
/// snapshots are host-testable.
pub const KeyboardState = struct {
/// One bit per HID usage: set while the key is physically down.
pressed: [32]u8 = [_]u8{0} ** 32,
caps_lock: bool = false,
pub fn apply(self: *KeyboardState, key: DecodedKey) Transition {
const already_down = self.isPressed(key.usage);
if (key.make) {
if (!already_down) {
self.setPressed(key.usage, true);
if (key.usage == usage_caps_lock) self.caps_lock = !self.caps_lock;
}
return .{
.action = if (already_down) .repeated else .pressed,
.modifiers = self.snapshot(),
};
}
self.setPressed(key.usage, false);
return .{ .action = .released, .modifiers = self.snapshot() };
}
pub fn isPressed(self: *const KeyboardState, usage: u8) bool {
return self.pressed[usage / 8] & (@as(u8, 1) << @intCast(usage % 8)) != 0;
}
fn setPressed(self: *KeyboardState, usage: u8, down: bool) void {
const bit = @as(u8, 1) << @intCast(usage % 8);
if (down) {
self.pressed[usage / 8] |= bit;
} else {
self.pressed[usage / 8] &= ~bit;
}
}
fn snapshot(self: *const KeyboardState) ModifierSnapshot {
const right_alt = self.isPressed(usage_right_alt);
return .{
.shift = self.isPressed(usage_left_shift) or self.isPressed(usage_right_shift),
.control = self.isPressed(usage_left_control) or self.isPressed(usage_right_control),
.alt = self.isPressed(usage_left_alt) or right_alt,
.right_alt = right_alt,
.caps_lock = self.caps_lock,
};
}
};
// --- tests (host-run via `zig build test`) ------------------------------------
const testing = std.testing;
/// Feed `bytes` and return the single DecodedKey they should produce (fails the
/// test if they produce none or more than one).
fn feedOne(decoder: *Decoder, bytes: []const u8) !DecodedKey {
var result: ?DecodedKey = null;
for (bytes) |byte| {
if (decoder.feed(byte)) |key| {
try testing.expect(result == null);
result = key;
}
}
return result orelse error.TestExpectedResult;
}
fn feedNone(decoder: *Decoder, bytes: []const u8) !void {
for (bytes) |byte| try testing.expectEqual(@as(?DecodedKey, null), decoder.feed(byte));
}
test "base make and break: A" {
var decoder = Decoder{};
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = false }, try feedOne(&decoder, &.{ 0xF0, 0x1C }));
}
test "extended make and break: right arrow" {
var decoder = Decoder{};
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x74 }));
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = false }, try feedOne(&decoder, &.{ 0xE0, 0xF0, 0x74 }));
}
test "pause: the E1 sequence is consumed silently" {
var decoder = Decoder{};
try feedNone(&decoder, &.{ 0xE1, 0x14, 0x77, 0xE1, 0xF0, 0x14, 0xF0, 0x77 });
// The decoder is back in idle: an ordinary key still decodes.
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
}
test "protocol bytes decode to nothing" {
var decoder = Decoder{};
try feedNone(&decoder, &.{ 0xFA, 0xAA, 0xEE }); // ACK, BAT-passed, echo
}
test "print screen: the fake-shift E0 12 decodes to nothing" {
var decoder = Decoder{};
try feedNone(&decoder, &.{ 0xE0, 0x12 });
try testing.expectEqual(DecodedKey{ .usage = 0x46, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x7C }));
}
test "typematic repeat is classified, not re-pressed" {
var state = KeyboardState{};
const a = DecodedKey{ .usage = 0x04, .make = true };
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
try testing.expectEqual(Transition.Action.released, state.apply(.{ .usage = 0x04, .make = false }).action);
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
}
test "shift held shows in the snapshot of other keys" {
var state = KeyboardState{};
_ = state.apply(.{ .usage = usage_left_shift, .make = true });
const transition = state.apply(.{ .usage = 0x04, .make = true });
try testing.expect(transition.modifiers.shift);
try testing.expect(!transition.modifiers.control);
_ = state.apply(.{ .usage = usage_left_shift, .make = false });
_ = state.apply(.{ .usage = 0x04, .make = false });
try testing.expect(!state.apply(.{ .usage = 0x04, .make = true }).modifiers.shift);
}
test "right alt reports both alt and the level-3 selector" {
var state = KeyboardState{};
_ = state.apply(.{ .usage = usage_right_alt, .make = true });
const transition = state.apply(.{ .usage = 0x04, .make = true });
try testing.expect(transition.modifiers.alt);
try testing.expect(transition.modifiers.right_alt);
}
test "caps lock toggles on make, not on repeat or break" {
var state = KeyboardState{};
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock);
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // repeat
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = false }).modifiers.caps_lock);
try testing.expect(!state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // second press: off
}
-1
View File
@@ -745,7 +745,6 @@ fn systemDebugWrite(state: *architecture.CpuState) void {
write_len = len;
write_from_user = architecture.fromUser(state);
write_count += 1;
if (write_at_line_start) log.write("DANOS-INIT: ");
log.write(source[0..len]);
if (len != 0) write_at_line_start = source[len - 1] == '\n';
architecture.setSystemCallResult(state, len);
@@ -14,6 +14,7 @@
//! that redundancy so the manager is the sole owner of driver spawning.)
const std = @import("std");
const runtime = @import("runtime");
const acpi_ids = @import("acpi-ids");
const device = runtime.device;
const system = runtime.system;
@@ -34,9 +35,11 @@ fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
// detect device via hid
const hid = d.hid[0..@intCast(d.hid_len)];
if (std.mem.eql(u8, hid, "PNP0303") or std.mem.eql(u8, hid, "PNP0F13")) return "ps2-bus";
return null;
const id = acpi_ids.HardwareId.fromHid(hid) orelse return null;
return switch (id) {
.ps2_keyboard, .ps2_mouse => "ps2-bus",
else => null,
};
}
+112 -3
View File
@@ -73,16 +73,125 @@ pub const modifier_shift: u32 = 1 << 0;
pub const modifier_control: u32 = 1 << 1;
pub const modifier_alt: u32 = 1 << 2;
/// A minimal danos-native keycode namespace — enough for the synthetic source and to show
/// the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
/// The danos-native keycode namespace: USB HID keyboard-page usages (page 0x07), the
/// numbering the PS/2 scancode decoder emits and the xkeyboard-config layout tables are
/// indexed by. Non-exhaustive, so an unnamed usage still travels as a valid value.
pub const Keycode = enum(u32) {
unknown = 0,
a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this
// letters
a = 4,
b = 5,
c = 6,
d = 7,
e = 8,
f = 9,
g = 10,
h = 11,
i = 12,
j = 13,
k = 14,
l = 15,
m = 16,
n = 17,
o = 18,
p = 19,
q = 20,
r = 21,
s = 22,
t = 23,
u = 24,
v = 25,
w = 26,
x = 27,
y = 28,
z = 29,
// digit row
one = 30,
two = 31,
three = 32,
four = 33,
five = 34,
six = 35,
seven = 36,
eight = 37,
nine = 38,
zero = 39,
// control and whitespace
enter = 40,
escape = 41,
backspace = 42,
tab = 43,
spacebar = 44,
// punctuation
minus = 45,
equal = 46,
left_bracket = 47,
right_bracket = 48,
backslash = 49,
non_us_hash = 50,
semicolon = 51,
apostrophe = 52,
grave = 53,
comma = 54,
period = 55,
slash = 56,
caps_lock = 57,
// function row
f1 = 58,
f2 = 59,
f3 = 60,
f4 = 61,
f5 = 62,
f6 = 63,
f7 = 64,
f8 = 65,
f9 = 66,
f10 = 67,
f11 = 68,
f12 = 69,
print_screen = 70,
scroll_lock = 71,
pause = 72,
// navigation
insert = 73,
home = 74,
page_up = 75,
delete = 76,
end = 77,
page_down = 78,
right_arrow = 79,
left_arrow = 80,
down_arrow = 81,
up_arrow = 82,
// keypad
num_lock = 83,
keypad_slash = 84,
keypad_asterisk = 85,
keypad_minus = 86,
keypad_plus = 87,
keypad_enter = 88,
keypad_one = 89,
keypad_two = 90,
keypad_three = 91,
keypad_four = 92,
keypad_five = 93,
keypad_six = 94,
keypad_seven = 95,
keypad_eight = 96,
keypad_nine = 97,
keypad_zero = 98,
keypad_period = 99,
non_us_backslash = 100,
application = 101,
// modifiers
left_control = 224,
left_shift = 225,
left_alt = 226,
left_gui = 227,
right_control = 228,
right_shift = 229,
right_alt = 230,
right_gui = 231,
_,
};