diff --git a/build.zig b/build.zig index b163acc..3dbc40b 100644 --- a/build.zig +++ b/build.zig @@ -339,6 +339,7 @@ pub fn build(b: *std.Build) void { 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 usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); + const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); // A test fixture, not a real driver: hellos to the device manager, then faults — // what the driver-restart scenario drives the crash-loop cap with. const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig"); @@ -388,6 +389,8 @@ pub fn build(b: *std.Build) void { mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); mk_run.addArg("usb-xhci-bus"); mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); + mk_run.addArg("pci-bus"); + mk_run.addFileArg(pci_bus_exe.getEmittedBin()); mk_run.addArg("crash-test"); mk_run.addFileArg(crash_test_exe.getEmittedBin()); mk_run.addArg("device-list"); diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index 47fa5a5..95340c0 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -84,10 +84,11 @@ branch is green; keep branches; push everything. caught by the new every-BAR-contained assert in `discovery`; idempotent `device_register` proven in `bus`; `ChildAdded.device_id`; m17-m18-plan.md archived; suite 54/54). -- [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the - ECAM window, walk bus/device/function headers, log what it finds. - Scenario `pci-scan`: the kernel test compares the driver's reported count - against the broker table's `pci_device` count — equivalence, per class. +- [x] **M19.1** — pci-bus driver, scan only (claims the bridge, maps ECAM + through its grant, brute-force walk with the multifunction rule; the + manager matches pci_host_bridge → pci-bus per device with the full + protocol contract; `pci-scan` builds its expected marker from the + kernel's own count — equivalence on the first run; suite 55/55). - [ ] **M19.2** — register + report: each function registered under the bridge (config-space slice + BARs, `pci_class` in the descriptor), reported with `child_added { device_id, identity = class triple }`. Manager mirrors; diff --git a/system/drivers/pci-bus/pci-bus.zig b/system/drivers/pci-bus/pci-bus.zig new file mode 100644 index 0000000..4a24277 --- /dev/null +++ b/system/drivers/pci-bus/pci-bus.zig @@ -0,0 +1,147 @@ +//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0 +//! (docs/m19-m20-plan.md, M19). The device manager matches the `pci_host_bridge` +//! node and spawns one instance per bridge, the bridge's device id as argv[1] — +//! the same per-device contract as usb-xhci-bus. +//! +//! M19.1 (this increment): claim the bridge, map its ECAM window (resource 0; +//! the bus range and the MMIO apertures follow it), walk every +//! bus/device/function config header, and log what the walk finds — ending +//! with "pci-bus: N functions found", which the `pci-scan` scenario compares +//! against the kernel's own enumeration. Registration and reports (M19.2), and +//! the kernel walk's retirement (M19.3), build on this proven-equivalent scan. + +const std = @import("std"); +const runtime = @import("runtime"); +const protocol = runtime.device_manager_protocol; +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); +} + +var bridge_id: u64 = protocol.no_device; +var ecam_base: usize = 0; +var start_bus: u64 = 0; +var bus_count: u64 = 0; + +/// One aligned 32-bit read from a function's configuration space. +fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 { + const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset); + const register: *volatile u32 = @ptrFromInt(address); + return register.*; +} + +/// Claim the bridge, map the ECAM, hello the manager, then scan. +fn initialise(endpoint: runtime.ipc.Handle) bool { + _ = endpoint; + if (!device.claim(bridge_id)) { + writeLine("pci-bus: unable to claim bridge device {d}\n", .{bridge_id}); + return false; + } + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("pci-bus: out of memory\n"); + return false; + }; + const total = device.enumerate(buffer); + const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| { + if (d.id == bridge_id) break d; + } else { + writeLine("pci-bus: device {d} not in the device tree\n", .{bridge_id}); + return false; + }; + // Resource 0 is the ECAM window (1 MiB of config space per bus); the bus + // range rides beside it. The MMIO apertures (M19.0) come after both. + if (descriptor.resource_count < 2 or descriptor.resources[0].kind != @intFromEnum(device.ResourceKind.memory)) { + _ = runtime.system.write("pci-bus: bridge has no ECAM window\n"); + return false; + } + const bus_range = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| { + if (resource.kind == @intFromEnum(device.ResourceKind.bus_range)) break resource; + } else { + _ = runtime.system.write("pci-bus: bridge has no bus range\n"); + return false; + }; + start_bus = bus_range.start; + bus_count = bus_range.len; + ecam_base = device.mmioMap(bridge_id, 0) orelse { + _ = runtime.system.write("pci-bus: ECAM mmio_map failed\n"); + return false; + }; + + // The handshake, then the scan (reports join in M19.2). + var manager: ?runtime.ipc.Handle = null; + var tries: u32 = 0; + while (manager == null and tries < 100) : (tries += 1) { + manager = runtime.ipc.lookup(.device_manager); + if (manager == null) runtime.system.sleep(20); + } + const h = manager orelse { + _ = runtime.system.write("pci-bus: no device manager to hello\n"); + return false; + }; + const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id }; + var reply: [protocol.message_maximum]u8 = undefined; + const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch { + _ = runtime.system.write("pci-bus: hello call failed\n"); + return false; + }; + if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) { + _ = runtime.system.write("pci-bus: hello refused\n"); + return false; + } + + scan(); + return true; +} + +/// The brute-force walk the kernel does today, from ring 3: every bus in the +/// range, 32 devices, 8 functions; vendor id FFFFh means nothing decodes there, +/// and only multifunction devices get their functions 1..7 probed. +fn scan() void { + var found: u32 = 0; + var bus: u64 = start_bus; + while (bus < start_bus + bus_count) : (bus += 1) { + var dev: u64 = 0; + while (dev < 32) : (dev += 1) { + const first = configRead(bus, dev, 0, 0); + if (first & 0xFFFF == 0xFFFF) continue; + const multifunction = (configRead(bus, dev, 0, 0x0C) >> 16) & 0x80 != 0; + var function: u64 = 0; + while (function < 8) : (function += 1) { + if (function != 0 and !multifunction) break; + const vendor_device = configRead(bus, dev, function, 0); + if (vendor_device & 0xFFFF == 0xFFFF) continue; + const class_revision = configRead(bus, dev, function, 0x08); + found += 1; + writeLine("pci-bus: {d}:{d}.{d} class 0x{x:0>6}\n", .{ bus, dev, function, class_revision >> 8 }); + } + } + } + writeLine("pci-bus: {d} functions found\n", .{found}); +} + +fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { + _ = message; + _ = reply; + _ = sender; + _ = capability; + return 0; +} + +pub fn main(init: runtime.process.Init) void { + const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent + bridge_id = std.fmt.parseInt(u64, argument, 10) catch { + writeLine("pci-bus: malformed bridge device id '{s}'\n", .{argument}); + return; + }; + runtime.service.run(protocol.message_maximum, .{ + .init = initialise, + .on_message = onMessage, + }); +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; // pull the runtime entry shim into the image +} diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 2477f26..8b09a80 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -144,6 +144,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { usbReportTest(boot_information); } else if (eql(case, "device-list")) { deviceListTest(boot_information); + } else if (eql(case, "pci-scan")) { + pciScanTest(boot_information); } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1793,6 +1795,64 @@ fn deviceListTest(boot_information: *const BootInformation) void { result(); } +/// M19.1: the ring-3 PCI scan agrees with the kernel's. The manager spawns +/// pci-bus for the host bridge; the driver walks the same ECAM window through +/// its mmio_map grant and must find exactly the functions the kernel's own +/// enumeration recorded — the equivalence that licenses retiring the kernel +/// walk in M19.3. +fn pciScanTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: pci-scan\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + + // What the kernel found: the expected marker is built from its own count. + var buffer: [64]device_abi.DeviceDescriptor = undefined; + const n = @min(devices_broker.enumerate(&buffer), buffer.len); + var kernel_count: u32 = 0; + for (buffer[0..n]) |d| { + if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) kernel_count += 1; + } + check("the kernel enumerated PCI functions to compare against", kernel_count >= 1); + var marker_buffer: [48]u8 = undefined; + const marker = std.fmt.bufPrint(&marker_buffer, "pci-bus: {d} functions found", .{kernel_count}) catch { + check("marker formatted", false); + result(); + return; + }; + + process.setInitialRamdisk(image); + process.write_count = 0; + var manager: u32 = 0; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "device-manager")) continue; + manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0; + break; + } + check("device-manager spawned", manager != 0); + + scheduler.setPriority(1); + const deadline = architecture.millis() + 15000; + var seen = false; + while (architecture.millis() < deadline and !seen) { + if (process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker)) seen = true; + scheduler.yield(); + } + scheduler.setPriority(4); + check("the ring-3 scan found exactly the kernel's function count", seen); + result(); +} + /// The whole user-side surface at once: spawn process-test's supervisor role, /// which — entirely from ring 3 — creates an exit endpoint, spawns its two /// children supervised, sees them in process_enumerate, kills them (one blocked, diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 262f026..a579bbe 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -318,6 +318,13 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { var matched: usize = 0; for (buffer[0..count]) |descriptor| { + if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) { + // The PCI bus driver: enumeration in ring 3 (M19), one instance + // per bridge, the bridge id as its assignment. + matched += 1; + addDriver("pci-bus", descriptor.id, true); + continue; + } if (pciDriverFor(descriptor)) |driver_name| { matched += 1; addDriver(driver_name, descriptor.id, true); diff --git a/test/qemu_test.py b/test/qemu_test.py index 79e168d..9d3a0a4 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -274,6 +274,13 @@ CASES = [ r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: child added", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map + # grant) finds exactly the functions the kernel's own walk recorded. + {"name": "pci-scan", + "smp": 4, + "timeout": 60, + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M18.3: the application surface — device-list enumerates the tree over IPC, # subscribes (endpoint as capability), and observes the removed/added events # the reporter's test-kill produces (docs/device-manager.md).