Finishing PS/2 bus driver

This commit is contained in:
Daniel Samson
2026-07-11 14:12:33 +01:00
parent d218d93f79
commit 2a583d55a8
11 changed files with 793 additions and 15 deletions
+12
View File
@@ -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 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 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 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 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 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 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.addFileArg(hpet_exe.getEmittedBin());
mk_run.addArg("bus"); mk_run.addArg("bus");
mk_run.addFileArg(bus_exe.getEmittedBin()); 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.addArg("device-manager");
mk_run.addFileArg(device_manager_exe.getEmittedBin()); mk_run.addFileArg(device_manager_exe.getEmittedBin());
mk_run.addArg("args-echo"); mk_run.addArg("args-echo");
@@ -330,6 +339,9 @@ pub fn build(b: *std.Build) void {
.{ device_manager_exe, "system/services" }, .{ device_manager_exe, "system/services" },
.{ hpet_exe, "system/drivers" }, .{ hpet_exe, "system/drivers" },
.{ bus_exe, "system/drivers" }, .{ bus_exe, "system/drivers" },
.{ ps2_bus_exe, "system/drivers" },
.{ ps2_keyboard_exe, "system/drivers" },
.{ ps2_mouse_exe, "system/drivers" },
}) |entry| { }) |entry| {
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
b.getInstallStep().dependOn(&step.step); b.getInstallStep().dependOn(&step.step);
+17
View File
@@ -3,6 +3,7 @@
//! ownership of its hardware; the claim is the capability the kernel checks before //! ownership of its hardware; the claim is the capability the kernel checks before
//! mapping registers or routing an IRQ. //! mapping registers or routing an IRQ.
const std = @import("std");
const abi = @import("abi"); const abi = @import("abi");
const device_abi = @import("device-abi"); const device_abi = @import("device-abi");
const sc = @import("system-call.zig"); 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 { 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)); 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;
}
+11
View File
@@ -2,6 +2,7 @@
//! stubs, one per kernel call. Numbers come from `abi.SystemCall`, the single //! stubs, one per kernel call. Numbers come from `abi.SystemCall`, the single
//! source of truth shared with the kernel dispatcher. //! source of truth shared with the kernel dispatcher.
const std = @import("std");
const abi = @import("abi"); const abi = @import("abi");
const sc = @import("system-call.zig"); 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); 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; /// 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). /// 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 /// Delivery is prompt but asynchronous, like a signal: a target caught running on
+1 -1
View File
@@ -3,7 +3,7 @@
//! Discovery backends (ACPI today, device-tree later) translate their native //! Discovery backends (ACPI today, device-tree later) translate their native
//! hardware description into this one shape, so the rest of the kernel walks a //! hardware description into this one shape, so the rest of the kernel walks a
//! plain `Device` tree without knowing which firmware described the machine — //! 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. //! applies to the CPU.
//! //!
//! This is deliberately minimal: enough to *describe* what was discovered (a //! This is deliberately minimal: enough to *describe* what was discovered (a
+46
View File
@@ -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;
}
+50
View File
@@ -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;
}
+175
View File
@@ -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;
}
+437
View File
@@ -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;
}
};
+16 -1
View File
@@ -700,6 +700,11 @@ fn systemIrqAck(state: *architecture.CpuState) void {
if (irq.ack(gsi)) architecture.setSystemCallResult(state, 0) else fail(state); 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. /// 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. /// 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 /// 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 /// 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. /// 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 { fn systemDebugWrite(state: *architecture.CpuState) void {
const ptr = architecture.systemCallArg(state, 0); const ptr = architecture.systemCallArg(state, 0);
const len = architecture.systemCallArg(state, 1); const len = architecture.systemCallArg(state, 1);
if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) { if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) {
const source: [*]const u8 = @ptrFromInt(ptr); 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 @memcpy(write_buffer[0..len], source[0..len]); // keep the latest message
write_len = len; write_len = len;
write_from_user = architecture.fromUser(state); write_from_user = architecture.fromUser(state);
write_count += 1; write_count += 1;
log.write("DANOS-INIT: "); if (write_at_line_start) log.write("DANOS-INIT: ");
log.write(source[0..len]); log.write(source[0..len]);
if (len != 0) write_at_line_start = source[len - 1] == '\n';
architecture.setSystemCallResult(state, len); architecture.setSystemCallResult(state, len);
} else { } else {
fail(state); fail(state);
+1 -1
View File
@@ -4,7 +4,7 @@
//! hiding the trade-offs. Keeping them here makes them visible at a glance and gives //! 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); //! 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 //! 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. //! 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 /// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping
@@ -12,19 +12,34 @@
//! HPET, decides `hpet` serves it, and brings that driver all the way up. (The //! 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 //! 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.) //! that redundancy so the manager is the sole owner of driver spawning.)
const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const device = runtime.device; 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 /// 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 /// /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. /// manager reads the tree and decides. `null` = no driver for this class yet.
fn driverFor(class: u64) ?[]const u8 { fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
if (class == @intFromEnum(device.DeviceClass.timer)) return "hpet"; // the HPET // 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; return null;
} }
pub fn main() void { pub fn main() void {
// Enumerate into a heap buffer (too big for the one-page user stack). // Enumerate into a heap buffer (too big for the one-page user stack).
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
@@ -36,16 +51,16 @@ pub fn main() void {
var matched: usize = 0; var matched: usize = 0;
for (buffer[0..count]) |descriptor| { for (buffer[0..count]) |descriptor| {
const driver_name = driverFor(descriptor.class) orelse continue; const driver_name = driverFor(descriptor) orelse continue;
matched += 1; matched += 1;
if (runtime.system.spawn(driver_name) != null) { if (!system.isProcessRunning(driver_name)) {
_ = runtime.system.write("device-manager: spawned "); if (runtime.system.spawn(driver_name) != null) {
_ = runtime.system.write(driver_name); writeLine("device-manager: spawned {s}\n", .{driver_name});
_ = runtime.system.write("\n"); } else {
writeLine("device-manager: failed to spawn {s}\n", .{driver_name});
}
} else { } else {
_ = runtime.system.write("device-manager: failed to spawn "); writeLine("device-manager: already spawned {s}\n", .{driver_name});
_ = runtime.system.write(driver_name);
_ = runtime.system.write("\n");
} }
} }