From 2a583d55a8c93f7a5a4fbe47a9d2b3f182d0eb43 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:12:33 +0100 Subject: [PATCH] Finishing PS/2 bus driver --- build.zig | 12 + library/runtime/device.zig | 17 + library/runtime/system.zig | 11 + system/devices/device-model.zig | 2 +- system/drivers/ps2-bus/keyboard.zig | 46 ++ system/drivers/ps2-bus/mouse.zig | 50 ++ system/drivers/ps2-bus/ps2-bus.zig | 175 +++++++ system/drivers/ps2-bus/ps2-library.zig | 437 ++++++++++++++++++ system/kernel/process.zig | 17 +- system/parameters.zig | 2 +- .../device-manager/device-manager.zig | 39 +- 11 files changed, 793 insertions(+), 15 deletions(-) create mode 100644 system/drivers/ps2-bus/keyboard.zig create mode 100644 system/drivers/ps2-bus/mouse.zig create mode 100644 system/drivers/ps2-bus/ps2-bus.zig create mode 100644 system/drivers/ps2-bus/ps2-library.zig diff --git a/build.zig b/build.zig index 55a5f4d..823aa09 100644 --- a/build.zig +++ b/build.zig @@ -298,6 +298,9 @@ pub fn build(b: *std.Build) void { 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 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"); @@ -316,6 +319,12 @@ pub fn build(b: *std.Build) void { mk_run.addFileArg(hpet_exe.getEmittedBin()); mk_run.addArg("bus"); mk_run.addFileArg(bus_exe.getEmittedBin()); + mk_run.addArg("ps2-bus"); + mk_run.addFileArg(ps2_bus_exe.getEmittedBin()); + mk_run.addArg("ps2-keyboard"); + mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin()); + mk_run.addArg("ps2-mouse"); + mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); mk_run.addArg("device-manager"); mk_run.addFileArg(device_manager_exe.getEmittedBin()); mk_run.addArg("args-echo"); @@ -330,6 +339,9 @@ pub fn build(b: *std.Build) void { .{ device_manager_exe, "system/services" }, .{ hpet_exe, "system/drivers" }, .{ bus_exe, "system/drivers" }, + .{ ps2_bus_exe, "system/drivers" }, + .{ ps2_keyboard_exe, "system/drivers" }, + .{ ps2_mouse_exe, "system/drivers" }, }) |entry| { const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); b.getInstallStep().dependOn(&step.step); diff --git a/library/runtime/device.zig b/library/runtime/device.zig index 6502314..0bc1525 100644 --- a/library/runtime/device.zig +++ b/library/runtime/device.zig @@ -3,6 +3,7 @@ //! ownership of its hardware; the claim is the capability the kernel checks before //! mapping registers or routing an IRQ. +const std = @import("std"); const abi = @import("abi"); const device_abi = @import("device-abi"); const sc = @import("system-call.zig"); @@ -108,3 +109,19 @@ pub fn ioRead(device_id: u64, resource_index: u64, offset: u64, width: u8) ?u32 pub fn ioWrite(device_id: u64, resource_index: u64, offset: u64, width: u8, value: u32) bool { return !failed(sc.systemCall5(.io_write, device_id, resource_index, offset, width, value)); } + +/// Find DeviceDescription by hid +/// +/// Utility function for driver development +pub fn findDeviceDescriptorByHid(buffer: []DeviceDescriptor, hid_needle: []const u8) ?DeviceDescriptor { + const total = enumerate(buffer); + const n = @min(total, buffer.len); + for (@as([]DeviceDescriptor, buffer[0..n])) |d| { + const hid_haystack = d.hid[0..@intCast(d.hid_len)]; + if (std.mem.eql(u8, hid_haystack, hid_needle)) { + return d; + } + } + + return null; +} \ No newline at end of file diff --git a/library/runtime/system.zig b/library/runtime/system.zig index 8561e9c..3b32a8e 100644 --- a/library/runtime/system.zig +++ b/library/runtime/system.zig @@ -2,6 +2,7 @@ //! stubs, one per kernel call. Numbers come from `abi.SystemCall`, the single //! source of truth shared with the kernel dispatcher. +const std = @import("std"); const abi = @import("abi"); const sc = @import("system-call.zig"); @@ -98,6 +99,16 @@ pub fn processes(out: []abi.ProcessDescriptor) usize { return sc.systemCall2(.process_enumerate, @intFromPtr(out.ptr), out.len); } +/// Whether a process spawned under `name` (its argv[0]) is currently alive. +pub fn isProcessRunning(name: []const u8) bool { + var table: [32]ProcessDescriptor = undefined; + const total = processes(&table); + for (table[0..@min(total, table.len)]) |descriptor| { + if (std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name)) return true; + } + return false; +} + /// End process `id`. Only its supervisor — the process that spawned it — may; /// anyone else gets false, as does a stale or unknown id (ids are never reused). /// Delivery is prompt but asynchronous, like a signal: a target caught running on diff --git a/system/devices/device-model.zig b/system/devices/device-model.zig index e9eb0d6..84f63d0 100644 --- a/system/devices/device-model.zig +++ b/system/devices/device-model.zig @@ -3,7 +3,7 @@ //! Discovery backends (ACPI today, device-tree later) translate their native //! hardware description into this one shape, so the rest of the kernel walks a //! plain `Device` tree without knowing which firmware described the machine — -//! the same discipline `root.zig`'s `MemoryKind` applies to memory and `architecture` +//! the same discipline `ps2-library.zig`'s `MemoryKind` applies to memory and `architecture` //! applies to the CPU. //! //! This is deliberately minimal: enough to *describe* what was discovered (a diff --git a/system/drivers/ps2-bus/keyboard.zig b/system/drivers/ps2-bus/keyboard.zig new file mode 100644 index 0000000..38649bb --- /dev/null +++ b/system/drivers/ps2-bus/keyboard.zig @@ -0,0 +1,46 @@ +//! PS/2 Keyboard Driver +//! +//! Spawned by the ps2-bus driver once the controller is initialized and port 1 +//! 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. + +const std = @import("std"); +const runtime = @import("runtime"); +const ps2 = @import("ps2-library.zig"); +const device = runtime.device; + +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); + if (hid.len == 0) { + _ = runtime.system.write("system/drivers/ps2-bus/keyboard: no HID argument\n"); + return; + } + writeLine("system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid}); + + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("system/drivers/ps2-bus/keyboard: out of memory\n"); + return; + }; + if (device.findDeviceDescriptorByHid(buffer, hid) == null) { + writeLine("system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid}); + 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"); + + _ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n"); + while (true) runtime.system.sleep(1000); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; +} diff --git a/system/drivers/ps2-bus/mouse.zig b/system/drivers/ps2-bus/mouse.zig new file mode 100644 index 0000000..23286f4 --- /dev/null +++ b/system/drivers/ps2-bus/mouse.zig @@ -0,0 +1,50 @@ +//! PS/2 Mouse Driver +//! +//! Spawned by the ps2-bus driver once the controller is initialized and port 2 +//! 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. + +const std = @import("std"); +const runtime = @import("runtime"); +const ps2 = @import("ps2-library.zig"); +const device = runtime.device; + +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); + if (hid.len == 0) { + _ = runtime.system.write("system/drivers/ps2-bus/mouse: no HID argument\n"); + return; + } + writeLine("system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid}); + + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("system/drivers/ps2-bus/mouse: out of memory\n"); + return; + }; + const mouse_device_descriptor = device.findDeviceDescriptorByHid(buffer, hid) orelse { + 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}); + return; + } + writeLine("system/drivers/ps2-bus/mouse: claimed device for hid {s}\n", .{hid}); + + _ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n"); + while (true) runtime.system.sleep(1000); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; +} diff --git a/system/drivers/ps2-bus/ps2-bus.zig b/system/drivers/ps2-bus/ps2-bus.zig new file mode 100644 index 0000000..98c985e --- /dev/null +++ b/system/drivers/ps2-bus/ps2-bus.zig @@ -0,0 +1,175 @@ +//! The PS/2 Controller is located on the mainboard. +//! In the early days the controller was a single chip (Intel 8042). +//! As of today it is part of the Advanced Integrated Peripheral. +//! +//! It shows up in the device discovery as: +//! KBD_ [acpi_device] hid=PNP0303 (PS/2 Keyboard) +//! - io_port 0x60 len 0x1 +//! - io_port 0x64 len 0x1 +//! - irq 0x1 len 0x1 +//! MOU_ [acpi_device] hid=PNP0F13 (PS/2 Mouse) +//! - irq 0xc len 0x1 +const std = @import("std"); +const runtime = @import("runtime"); +const ps2 = @import("ps2-library.zig"); +const device = runtime.device; + +/// 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. +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); +} + +/// 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 { + const device_type = controller.identifyDevice(port) orelse { + writeLine("system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)}); + return; + }; + const driver_name = device_type.driverName() orelse { + writeLine("system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)}); + return; + }; + 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}); + } +} + +pub fn main() void { + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("system/drivers/ps2-bus: out of memory\n"); + return; + }; + + var has_two_channels = false; + // 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"); + 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"); + + if (!device.claim(controller_device_descriptor.id)) { + _ = runtime.system.write("system/drivers/ps2-bus: unable to claim controller \n"); + return; + } + + const controller = ps2.Controller.init(controller_device_descriptor) orelse { + _ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IO ports\n"); + return; + }; + + controller.disablePort(.One); + controller.disablePort(.Two); + controller.flushOutputBuffer(); + + const current = controller.readConfigurationByte() orelse { + _ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n"); + return; + }; + + const update = current & ~(ps2.configuration_first_port_interrupt | + ps2.configuration_second_port_interrupt | + ps2.configuration_first_port_translation); + + if (controller.writeConfigurationByte(update) == null) { + _ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n"); + return; + } + + if (controller.performSelfTest()) | reply | { + if (reply != ps2.response_controller_test_passed) { + _ = runtime.system.write("system/drivers/ps2-bus: perform controller self test failed\n"); + return; + } + } else { + _ = runtime.system.write("system/drivers/ps2-bus: controller self test timed out\n"); + return; + } + + has_two_channels = controller.hasTwoChannels() orelse { + _ = runtime.system.write("system/drivers/ps2-bus: controller channels timed out\n"); + return; + }; + + if (has_two_channels) { + _ = runtime.system.write("system/drivers/ps2-bus: has two channels\n"); + // keep the bus quiet until we have tested the ports and are ready to use them + controller.disablePort(.Two); + } else { + _ = runtime.system.write("system/drivers/ps2-bus: has one channel\n"); + } + + // interface tests: always test port 1, test port 2 only if it exists + const port_one_works = (controller.testPort(.One) orelse { + _ = runtime.system.write("system/drivers/ps2-bus: port 1 test timed out\n"); + return; + }) == ps2.response_port_test_passed; + + var port_two_works = false; + if (has_two_channels) { + port_two_works = (controller.testPort(.Two) orelse { + _ = runtime.system.write("system/drivers/ps2-bus: port 2 test timed out\n"); + return; + }) == ps2.response_port_test_passed; + } + + if (!port_one_works and !port_two_works) { + _ = runtime.system.write("system/drivers/ps2-bus: no usable ports\n"); + return; + } + + // enable the working ports and, via a read-modify-write, their interrupts + 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) { + if (controller.resetDevice(.One)) |passed| { + if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset failed\n"); + } else { + _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset timed out\n"); + } + } + if (port_two_works) { + if (controller.resetDevice(.Two)) |passed| { + if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset failed\n"); + } else { + _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset timed out\n"); + } + } + + // 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); + } else { + _ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n"); + return; + } + + _ = runtime.system.write("system/drivers/ps2-bus: ok\n"); + while (true) runtime.system.sleep(1000); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; +} diff --git a/system/drivers/ps2-bus/ps2-library.zig b/system/drivers/ps2-bus/ps2-library.zig new file mode 100644 index 0000000..7b60c06 --- /dev/null +++ b/system/drivers/ps2-bus/ps2-library.zig @@ -0,0 +1,437 @@ +//! shared definitions between the different PS/2 drivers +const std = @import("std"); +const runtime = @import("runtime"); +const device = runtime.device; +const system = runtime.system; + +/// PS-2 io ports: +/// The PS/2 Controller itself uses 2 IO ports (usually, IO ports 0x60 and 0x64). Like many IO +/// ports, reads and writes may access different internal registers. +/// +/// Historical note: The PC-XT PPI had used port 0x61 to reset the keyboard interrupt request +/// signal (among other unrelated functions). Port 0x61 has no keyboard related functions on AT and +/// PS/2 compatibles. +/// +/// The Data Port (typically IO Port 0x60) is used for reading data that was received from a PS/2 +/// device or from the PS/2 controller itself and writing data to a PS/2 device or to the PS/2 +/// controller itself. +// Access type: Read/Write +pub const dataPort = 0x60; +// Access type: Read +pub const statusRegisterPort = 0x64; +// Access type: Write +pub const CommandRegisterPort = 0x64; + +/// How long to poll the status register before giving up. PS/2 controller +/// responses normally arrive within a few milliseconds. +pub const default_wait_timeout_nanoseconds: u64 = 10_000_000; // 10 ms + +/// A PS/2 device reset (0xFF) runs the device's self-test (BAT), whose reply can +/// take far longer than an ordinary controller response. +pub const device_reset_timeout_nanoseconds: u64 = 750_000_000; // 750 ms + +/// PS/2 controller commands, written to the command register (port 0x64). +pub const cmd_read_configuration_byte: u8 = 0x20; // read controller configuration byte (internal RAM byte 0) +pub const cmd_write_configuration_byte: u8 = 0x60; // write controller configuration byte (internal RAM byte 0) +pub const cmd_disable_second_port: u8 = 0xA7; // disable second PS/2 port (dual-channel controllers only) +pub const cmd_enable_second_port: u8 = 0xA8; // enable second PS/2 port (dual-channel controllers only) +pub const cmd_test_second_port: u8 = 0xA9; // test second PS/2 port +pub const cmd_test_controller: u8 = 0xAA; // controller self-test +pub const cmd_test_first_port: u8 = 0xAB; // test first PS/2 port +pub const cmd_diagnostic_dump: u8 = 0xAC; // read all bytes of internal RAM +pub const cmd_disable_first_port: u8 = 0xAD; // disable first PS/2 port +pub const cmd_enable_first_port: u8 = 0xAE; // enable first PS/2 port +pub const cmd_read_controller_input_port: u8 = 0xC0; // read controller input port +pub const cmd_read_controller_output_port: u8 = 0xD0; // read controller output port +pub const cmd_write_controller_output_port: u8 = 0xD1; // write next data byte to the controller output port +pub const cmd_write_first_port_output: u8 = 0xD2; // write next data byte to the first port output buffer +pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to the second port output buffer +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. +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 +pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error +pub const status_parity_error: u8 = 1 << 7; // 1 = parity error + +/// Controller configuration byte bits (internal RAM byte 0; read/written via 0x20/0x60). +pub const configuration_first_port_interrupt: u8 = 1 << 0; // 1 = first port IRQ (IRQ1) enabled +pub const configuration_second_port_interrupt: u8 = 1 << 1; // 1 = second port IRQ (IRQ12) enabled +pub const configuration_system_flag: u8 = 1 << 2; // 1 = system passed POST +pub const configuration_first_port_clock_disabled: u8 = 1 << 4; // 1 = first port clock disabled +pub const configuration_second_port_clock_disabled: u8 = 1 << 5; // 1 = second port clock disabled +pub const configuration_first_port_translation: u8 = 1 << 6; // 1 = first port scancode translation enabled + +/// Controller output port bits (read/written via 0xD0/0xD1). +pub const output_port_system_reset: u8 = 1 << 0; // WARNING: keep this 1; writing 0 can lock the machine +pub const output_port_a20_gate: u8 = 1 << 1; // A20 gate +pub const output_port_second_port_clock: u8 = 1 << 2; // dual-channel controllers only +pub const output_port_second_port_data: u8 = 1 << 3; // dual-channel controllers only +pub const output_port_first_port_output_full: u8 = 1 << 4; // output buffer full from first port (IRQ1) +pub const output_port_second_port_output_full: u8 = 1 << 5; // output buffer full from second port (IRQ12) +pub const output_port_first_port_clock: u8 = 1 << 6; // first port clock +pub const output_port_first_port_data: u8 = 1 << 7; // first port data + +/// Controller self-test (0xAA) result codes. +pub const response_controller_test_passed: u8 = 0x55; +pub const response_controller_test_failed: u8 = 0xFC; + +/// Port test (0xAB / 0xA9) result codes. +pub const response_port_test_passed: u8 = 0x00; +pub const response_port_test_clock_stuck_low: u8 = 0x01; +pub const response_port_test_clock_stuck_high: u8 = 0x02; +pub const response_port_test_data_stuck_low: u8 = 0x03; +pub const response_port_test_data_stuck_high: u8 = 0x04; + +/// PS/2 device commands, written to the data port (0x60) to reach the attached device. +pub const device_cmd_identify: u8 = 0xF2; // identify device +pub const device_cmd_enable_scanning: u8 = 0xF4; +pub const device_cmd_disable_scanning: u8 = 0xF5; +pub const device_cmd_reset: u8 = 0xFF; // reset and run the device self-test (BAT) + +/// PS/2 device response bytes, read from the data port (0x60). +pub const device_response_self_test_passed: u8 = 0xAA; // BAT succeeded after a reset +pub const device_response_echo: u8 = 0xEE; +pub const device_response_acknowledge: u8 = 0xFA; // ACK +pub const device_response_self_test_failed_1: u8 = 0xFC; // BAT failure +pub const device_response_self_test_failed_2: u8 = 0xFD; // BAT failure +pub const device_response_resend: u8 = 0xFE; // ask the host to resend the last byte + +/// PS/2 device identify (0xF2) reply bytes. A keyboard returns a two-byte id +/// beginning with 0xAB; a mouse returns a single-byte id (0x00/0x03/0x04); an +/// ancient AT keyboard returns nothing at all. +pub const identify_keyboard_mf2: u8 = 0xAB; // first byte of a MF2 keyboard id (a subtype byte follows) +pub const identify_mouse_standard: u8 = 0x00; +pub const identify_mouse_scroll: u8 = 0x03; // mouse with scroll wheel +pub const identify_mouse_five_button: u8 = 0x04; // 5-button mouse + +fn waitReadable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool { + const deadline = system.clock() + wait_timeout_nanoseconds; + while (system.clock() < deadline) { + if (status(id, cmd_index) & status_output_buffer_full != 0) return true; // OBF set -> data ready + } + return false; +} + +fn waitWritable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool { + const deadline = system.clock() + wait_timeout_nanoseconds; + while (system.clock() < deadline) { + if (status(id, cmd_index) & status_input_buffer_full == 0) return true; // IBF clear -> ok to write + } + return false; // timed out +} + +pub fn status(id: u64, cmd_index: u64) u8 { + return @intCast(device.ioRead(id, cmd_index, 0, 1) orelse 0); +} + +pub fn sendCommand(id: u64, cmd_index: u64, byte: u8, timeout_nanoseconds: u64) bool { + // wait IBF clear + if (!waitWritable(id, cmd_index, timeout_nanoseconds)) return false; + return device.ioWrite(id, cmd_index, 0, 1, byte); +} + +pub fn readData(id: u64, status_index: u64, data_index: u64, timeout_nanoseconds: u64) ?u8 { + // OBF lives in the status register (0x64); wait for it there, then read the data port (0x60) + if (!waitReadable(id, status_index, timeout_nanoseconds)) return null; + return @intCast(device.ioRead(id, data_index, 0, 1) orelse 0); +} + +pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool { + // IBF lives in the status register (0x64); wait for it to clear there, then write the data port (0x60) + if (!waitWritable(id, status_index, timeout_nanoseconds)) return false; + return device.ioWrite(id, data_index, 0, 1, byte); +} + +pub const Port = enum(u2) { + One, + Two, + + /// Command register byte that disables this port. + fn disableCommand(self: Port) u8 { + return switch (self) { + .One => cmd_disable_first_port, + .Two => cmd_disable_second_port, + }; + } + + /// Command register byte that enables this port (and its clock). + fn enableCommand(self: Port) u8 { + return switch (self) { + .One => cmd_enable_first_port, + .Two => cmd_enable_second_port, + }; + } + + /// Command register byte that runs this port's interface test. + fn testCommand(self: Port) u8 { + return switch (self) { + .One => cmd_test_first_port, + .Two => cmd_test_second_port, + }; + } + + /// Configuration-byte bit that, when set, disables this port's clock. + pub fn clockDisabledBit(self: Port) u8 { + return switch (self) { + .One => configuration_first_port_clock_disabled, + .Two => configuration_second_port_clock_disabled, + }; + } + + /// Configuration-byte bit that, when set, enables this port's interrupt. + pub fn interruptBit(self: Port) u8 { + return switch (self) { + .One => configuration_first_port_interrupt, + .Two => configuration_second_port_interrupt, + }; + } + + /// Command register byte that writes the next data byte into this port's + /// output buffer (makes a byte appear as if it came from the device). + pub fn writeOutputBufferCommand(self: Port) u8 { + return switch (self) { + .One => cmd_write_first_port_output, + .Two => cmd_write_second_port_output, + }; + } + + /// Controller command that must prefix a byte destined for this port's + /// device. Port 1 is the default target of the data port, so it needs no + /// prefix (null); port 2 requires the "write second port input" command. + pub fn deviceInputCommand(self: Port) ?u8 { + return switch (self) { + .One => null, + .Two => cmd_write_second_port_input, + }; + } + + /// Controller output-port bit driving this port's clock line. + pub fn outputPortClockBit(self: Port) u8 { + return switch (self) { + .One => output_port_first_port_clock, + .Two => output_port_second_port_clock, + }; + } + + /// Controller output-port bit driving this port's data line. + pub fn outputPortDataBit(self: Port) u8 { + return switch (self) { + .One => output_port_first_port_data, + .Two => output_port_second_port_data, + }; + } + + /// Controller output-port bit set when this port's output buffer is full + /// (wired to the port's IRQ line). + pub fn outputPortBufferFullBit(self: Port) u8 { + return switch (self) { + .One => output_port_first_port_output_full, + .Two => output_port_second_port_output_full, + }; + } +}; + +/// 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, + + /// Initial-ramdisk name of the driver that serves this device type, or null + /// if we could not classify it. + pub fn driverName(self: DeviceType) ?[]const u8 { + return switch (self) { + .keyboard => "ps2-keyboard", + .mouse => "ps2-mouse", + .unknown => null, + }; + } + + /// Canonical ACPI HID for this device type, handed to the spawned driver as + /// 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", + .unknown => null, + }; + } +}; + +/// 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 +/// call sites free of global state. +pub const Controller = struct { + device_id: u64, + /// Resource index of the command/status port (0x64). + status_index: u64, + /// Resource index of the data port (0x60). + data_index: u64, + + /// Resolve the controller's IO-port resource indices from its device + /// descriptor. Returns null if either the data or command/status port is + /// missing from the descriptor. + pub fn init(device_descriptor: device.DeviceDescriptor) ?Controller { + var data_index: ?u64 = null; + var status_index: ?u64 = null; + + for (device_descriptor.resources, 0..device_descriptor.resource_count) |resource, resource_index| { + if (resource.kind != @intFromEnum(device.ResourceKind.io_port)) continue; + if (resource.start == dataPort) { + data_index = @intCast(resource_index); + } else if (resource.start == statusRegisterPort) { + status_index = @intCast(resource_index); + } + } + + return .{ + .device_id = device_descriptor.id, + .data_index = data_index orelse return null, + .status_index = status_index orelse return null, + }; + } + + pub fn disablePort(self: Controller, port: Port) void { + // port enable/disable are controller commands and go to the command register (0x64) + _ = sendCommand(self.device_id, self.status_index, port.disableCommand(), default_wait_timeout_nanoseconds); + } + + pub fn enablePort(self: Controller, port: Port) void { + // enabling a port also starts its clock + _ = sendCommand(self.device_id, self.status_index, port.enableCommand(), default_wait_timeout_nanoseconds); + } + + /// Run a port's interface test. Returns the controller's reply — compare it + /// to `response_port_test_passed` (0x00) — or null on timeout. + pub fn testPort(self: Controller, port: Port) ?u8 { + if (!sendCommand(self.device_id, self.status_index, port.testCommand(), default_wait_timeout_nanoseconds)) return null; + return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds); + } + + pub fn flushOutputBuffer(self: Controller) void { + // flush any stale byte the controller buffered + _ = device.ioRead(self.device_id, self.data_index, 0, 1); + } + + pub fn readConfigurationByte(self: Controller) ?u8 { + // ask the controller to place its configuration byte in the output buffer, then read it + if (!sendCommand(self.device_id, self.status_index, cmd_read_configuration_byte, default_wait_timeout_nanoseconds)) return null; + return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds); + } + + pub fn writeConfigurationByte(self: Controller, update_byte: u8) ?u8 { + // command 0x60 makes the controller store the next data-port byte as its configuration byte + if (!sendCommand(self.device_id, self.status_index, cmd_write_configuration_byte, default_wait_timeout_nanoseconds)) return null; + if (!writeData(self.device_id, self.status_index, self.data_index, update_byte, default_wait_timeout_nanoseconds)) return null; + return update_byte; + } + + /// Run the controller self-test. Returns the reply — compare it to + /// `response_controller_test_passed` (0x55) — or null on timeout. + pub fn performSelfTest(self: Controller) ?u8 { + if (!sendCommand(self.device_id, self.status_index, cmd_test_controller, default_wait_timeout_nanoseconds)) return null; + return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds); + } + + /// Detect whether this is a dual-channel controller by temporarily enabling + /// port 2 and checking whether its clock turned on. Note: this leaves port 2 + /// enabled; the caller should disable it again to keep the bus quiet until + /// device bring-up. + pub fn hasTwoChannels(self: Controller) ?bool { + self.enablePort(.Two); + const configuration = self.readConfigurationByte() orelse return null; + return (configuration & Port.Two.clockDisabledBit()) == 0; + } + + /// Reset the device attached to `port` (device command 0xFF) and wait for + /// its power-on self-test (BAT) result. Returns true if the device both + /// acknowledged and passed, false if it reported a self-test failure, or + /// null on timeout. The BAT reply can be slow, so the response reads use + /// `device_reset_timeout_nanoseconds`. + pub fn resetDevice(self: Controller, port: Port) ?bool { + // A byte destined for port 2 must be prefixed with the "write to second + // port input buffer" controller command (0xD4); port 1 is the default. + if (port.deviceInputCommand()) |prefix| { + if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null; + } + if (!writeData(self.device_id, self.status_index, self.data_index, device_cmd_reset, default_wait_timeout_nanoseconds)) return null; + + // A successful reset yields both an ACK (0xFA) and a self-test-passed + // byte (0xAA). Their order is not guaranteed, so accept either ordering. + var saw_acknowledge = false; + var saw_self_test_passed = false; + var reads: u8 = 0; + while (reads < 2) : (reads += 1) { + const reply = readData(self.device_id, self.status_index, self.data_index, device_reset_timeout_nanoseconds) orelse return null; + switch (reply) { + device_response_acknowledge => saw_acknowledge = true, + device_response_self_test_passed => saw_self_test_passed = true, + device_response_self_test_failed_1, device_response_self_test_failed_2 => return false, + else => {}, + } + } + return saw_acknowledge and saw_self_test_passed; + } + + /// Send one command byte to the device on `port` (applying the port-2 prefix + /// as needed) and consume its acknowledgement. Returns true on ACK (0xFA), + /// false on any other reply, or null on timeout. + pub fn sendToDevice(self: Controller, port: Port, byte: u8) ?bool { + if (port.deviceInputCommand()) |prefix| { + if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null; + } + if (!writeData(self.device_id, self.status_index, self.data_index, byte, default_wait_timeout_nanoseconds)) return null; + const reply = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds) orelse return null; + return reply == device_response_acknowledge; + } + + /// Discard any bytes sitting in the output buffer (for example the device-id + /// byte a mouse emits after a reset) so they cannot be mistaken for the reply + /// to a subsequent command. + pub fn drainOutputBuffer(self: Controller) void { + var guard: u8 = 0; + while (guard < 16) : (guard += 1) { + if (status(self.device_id, self.status_index) & status_output_buffer_full == 0) return; + _ = device.ioRead(self.device_id, self.data_index, 0, 1); + } + } + + /// Ask the device on `port` what it is (command 0xF2) and classify the reply. + /// Scanning is disabled around the query so a streaming device cannot inject + /// data bytes that look like the identifier. Returns the device type, or null + /// if the identify command itself timed out. + pub fn identifyDevice(self: Controller, port: Port) ?DeviceType { + // Clear any leftover bytes (e.g. a post-reset mouse id) before we start. + self.drainOutputBuffer(); + + // Stop the device reporting so its data can't be mistaken for the reply. + if (self.sendToDevice(port, device_cmd_disable_scanning) == null) return null; + + if (self.sendToDevice(port, device_cmd_identify) == null) return null; + + // After the ACK, the device sends 0, 1, or 2 identifier bytes. + const first = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds); + const device_type: DeviceType = if (first) |id| switch (id) { + identify_keyboard_mf2 => blk: { + // A MF2 keyboard sends a second subtype byte; consume and ignore it. + _ = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds); + break :blk .keyboard; + }, + identify_mouse_standard, identify_mouse_scroll, identify_mouse_five_button => .mouse, + else => .unknown, + } else + // No identifier bytes at all is a legacy AT keyboard. + .keyboard; + + // Resume scanning so the device works once its driver takes over. + _ = self.sendToDevice(port, device_cmd_enable_scanning); + return device_type; + } +}; \ No newline at end of file diff --git a/system/kernel/process.zig b/system/kernel/process.zig index a11bd80..f3f2b5b 100644 --- a/system/kernel/process.zig +++ b/system/kernel/process.zig @@ -700,6 +700,11 @@ fn systemIrqAck(state: *architecture.CpuState) void { if (irq.ack(gsi)) architecture.setSystemCallResult(state, 0) else fail(state); } +/// Whether the debug_write stream sits at the start of a line — the last emitted +/// byte was a newline (true at boot: nothing emitted yet). Guarded by the kernel +/// lock in `systemDebugWrite`, like the stream it describes. +var write_at_line_start: bool = true; + /// debug_write(ptr, len): copy bytes from user memory into the kernel log. /// A bring-up diagnostic — real output goes through the VFS/console later. /// @@ -709,17 +714,27 @@ fn systemIrqAck(state: *architecture.CpuState) void { /// Known gap (fine for trusted user code): a pointer into an *unmapped* hole in /// the user half passes the check and the read #PFs -> on_fault halts — a /// self-DoS, not an isolation break. Fault-recovering copy-in is a later item. +/// +/// The emit runs under the kernel lock, so a message is atomic on the wire — two +/// processes writing from different cores can interleave *messages*, never bytes. +/// The "DANOS-INIT: " marker is emitted only at the start of a line (not per +/// call), so a process may assemble a line from several writes without the marker +/// (or, with the lock, another byte) landing in the middle. Cleanly-terminated +/// lines from concurrent writers stay whole either way. fn systemDebugWrite(state: *architecture.CpuState) void { const ptr = architecture.systemCallArg(state, 0); const len = architecture.systemCallArg(state, 1); if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) { const source: [*]const u8 = @ptrFromInt(ptr); + const flags = sync.enter(); + defer sync.leave(flags); @memcpy(write_buffer[0..len], source[0..len]); // keep the latest message write_len = len; write_from_user = architecture.fromUser(state); write_count += 1; - log.write("DANOS-INIT: "); + 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); } else { fail(state); diff --git a/system/parameters.zig b/system/parameters.zig index 2f01b31..a17e556 100644 --- a/system/parameters.zig +++ b/system/parameters.zig @@ -4,7 +4,7 @@ //! hiding the trade-offs. Keeping them here makes them visible at a glance and gives //! one spot to change them. They're plain `comptime` constants (zero runtime cost); //! any one can later be promoted to a `-D` build option if a target needs to vary it -//! (see build.zig's `-Dtest-case` for the pattern). This keeps root.zig to what it +//! (see build.zig's `-Dtest-case` for the pattern). This keeps ps2-library.zig to what it //! actually is — the bootloader↔kernel handoff *contract* — with tunables living here. /// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 310120a..938ed4c 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -12,19 +12,34 @@ //! HPET, decides `hpet` serves it, and brings that driver all the way up. (The //! kernel still auto-spawns the whole initial-ramdisk at boot; increment 3 removes //! that redundancy so the manager is the sole owner of driver spawning.) - +const std = @import("std"); const runtime = @import("runtime"); const device = runtime.device; +const system = runtime.system; -/// The driver that serves each device class — the policy table. In a fuller system +/// Format one whole log line and emit it in a single `debug_write`, so output +/// from the drivers this manager starts (which run concurrently) can never land +/// in the middle of it. +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); +} + +/// The driver that serves each device — the policy table. In a fuller system /// this comes from the drivers describing what they bind (or a manifest under /// /system/drivers); for now it is a small static map, which is enough to prove the /// manager reads the tree and decides. `null` = no driver for this class yet. -fn driverFor(class: u64) ?[]const u8 { - if (class == @intFromEnum(device.DeviceClass.timer)) return "hpet"; // the HPET +fn driverFor(d: device.DeviceDescriptor) ?[]const u8 { + // detect device via DeviceClass + 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; } + pub fn main() void { // Enumerate into a heap buffer (too big for the one-page user stack). const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { @@ -36,16 +51,16 @@ pub fn main() void { var matched: usize = 0; for (buffer[0..count]) |descriptor| { - const driver_name = driverFor(descriptor.class) orelse continue; + const driver_name = driverFor(descriptor) orelse continue; matched += 1; - if (runtime.system.spawn(driver_name) != null) { - _ = runtime.system.write("device-manager: spawned "); - _ = runtime.system.write(driver_name); - _ = runtime.system.write("\n"); + if (!system.isProcessRunning(driver_name)) { + if (runtime.system.spawn(driver_name) != null) { + writeLine("device-manager: spawned {s}\n", .{driver_name}); + } else { + writeLine("device-manager: failed to spawn {s}\n", .{driver_name}); + } } else { - _ = runtime.system.write("device-manager: failed to spawn "); - _ = runtime.system.write(driver_name); - _ = runtime.system.write("\n"); + writeLine("device-manager: already spawned {s}\n", .{driver_name}); } }