Every supervised driver owes the device manager a hello at startup, and the retry-lookup-call-check for it had been copied into five drivers: usb.helloManager (misfiled in the USB client) plus hand-rolled twins in display, virtio-gpu, pci-bus, and usb-xhci-bus. Extract it once as a runtime client, runtime.device_manager.hello(role, device_id) ?Handle — returns the manager endpoint (bus drivers keep it to report children through), null when there is no manager or it refused the handshake, and logs the outcome itself so each call site is one line. Also correct two roles while collapsing their calls: virtio-gpu and the display driver each claim one PCI function and report no children, so they are Role.device, not Role.bus. The manager ignores role today, so this is cosmetic, but it matches the protocol's own definition (bus = reports children via child_added). Behavior-preserving otherwise: pci-bus and usb-xhci-bus move their hello logging from raw serial writes to std.log, which the kernel renders with the same "<path>: " prefix, so driver-restart still matches "usb-xhci-bus: hello acknowledged". zig build clean; 8 QEMU cases pass (driver-restart, pci-scan, usb-hid, usb-storage, virtio-gpu, display-reattach, device-list, device-manager).
243 lines
12 KiB
Zig
243 lines
12 KiB
Zig
//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0
|
|
//! (docs/discovery.md). 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 "/system/drivers/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;
|
|
const pci_class = @import("pci-class");
|
|
|
|
/// Log a discovered function with its (class / subclass / prog-IF) triple decoded
|
|
/// to human names — the boot-log breadcrumb that says *what* the hardware is, so
|
|
/// "class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
|
|
/// progif 0x01 (AHCI 1.0)" reads straight off the log when writing a new driver.
|
|
/// A dedicated wider buffer than `writeLine`'s, since the decoded names are long.
|
|
fn logFunction(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
|
const cc = pci_class.ClassCode.unpack(@truncate(class_triple));
|
|
const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
|
|
var line: [200]u8 = undefined;
|
|
const text = if (pif.len != 0)
|
|
std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2} ({s})\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if, pif }) catch return
|
|
else
|
|
std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2}\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
|
|
_ = runtime.system.write(text);
|
|
}
|
|
|
|
var bridge_id: u64 = protocol.no_device;
|
|
var ecam_base: usize = 0;
|
|
var ecam_physical: u64 = 0;
|
|
var start_bus: u64 = 0;
|
|
var bus_count: u64 = 0;
|
|
var manager_handle: runtime.ipc.Handle = 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.*;
|
|
}
|
|
|
|
fn configWrite(bus: u64, dev: u64, function: u64, offset: u64, value: u32) void {
|
|
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
|
const register: *volatile u32 = @ptrFromInt(address);
|
|
register.* = value;
|
|
}
|
|
|
|
fn configRead16(bus: u64, dev: u64, function: u64, offset: u64) u16 {
|
|
const word = configRead(bus, dev, function, offset & ~@as(u64, 3));
|
|
return @truncate(word >> @intCast((offset & 3) * 8));
|
|
}
|
|
|
|
fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) void {
|
|
const aligned = offset & ~@as(u64, 3);
|
|
const shift: u5 = @intCast((offset & 3) * 8);
|
|
const word = configRead(bus, dev, function, aligned);
|
|
const mask = @as(u32, 0xFFFF) << shift;
|
|
configWrite(bus, dev, function, aligned, (word & ~mask) | (@as(u32, value) << shift));
|
|
}
|
|
|
|
/// Claim the bridge, map the ECAM, hello the manager, then scan.
|
|
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|
_ = endpoint;
|
|
if (!device.claim(bridge_id)) {
|
|
std.log.info("unable to claim bridge device {d}", .{bridge_id});
|
|
return false;
|
|
}
|
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = runtime.system.write("/system/drivers/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 {
|
|
std.log.info("device {d} not in the device tree", .{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("/system/drivers/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("/system/drivers/pci-bus: bridge has no bus range\n");
|
|
return false;
|
|
};
|
|
start_bus = bus_range.start;
|
|
bus_count = bus_range.len;
|
|
ecam_physical = descriptor.resources[0].start;
|
|
ecam_base = device.mmioMap(bridge_id, 0) orelse {
|
|
_ = runtime.system.write("/system/drivers/pci-bus: ECAM mmio_map failed\n");
|
|
return false;
|
|
};
|
|
|
|
// The handshake (role: bus — we enumerate PCI and report the functions we
|
|
// find), then the scan. Keep the manager handle to report children through;
|
|
// a supervised bus that cannot reach its manager has nothing to serve.
|
|
manager_handle = runtime.device_manager.hello(.bus, bridge_id) orelse 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;
|
|
logFunction(bus, dev, function, class_revision >> 8);
|
|
registerAndReport(bus, dev, function, class_revision >> 8);
|
|
}
|
|
}
|
|
}
|
|
std.log.info("{d} functions found", .{found});
|
|
}
|
|
|
|
/// Register one function under the bridge and report it to the manager. The
|
|
/// descriptor mirrors the kernel's own recording byte for byte — config slice
|
|
/// as resource 0, then the sized BARs — so during coexistence the idempotent
|
|
/// device_register (M19.0) returns the kernel's existing node id rather than
|
|
/// growing a duplicate, and the report carries the id drivers already use.
|
|
fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
|
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
|
descriptor.class = @intFromEnum(device.DeviceClass.pci_device);
|
|
descriptor.pci_class = class_triple;
|
|
descriptor.resources[0] = .{
|
|
.kind = @intFromEnum(device.ResourceKind.memory),
|
|
.start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)),
|
|
.len = 4096,
|
|
};
|
|
descriptor.resource_count = 1;
|
|
|
|
// The standard BAR-sizing probe, exactly as the kernel does it: decode off,
|
|
// write all-ones, read the writable mask back, restore. Header type 0 only.
|
|
const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F;
|
|
if (header_type == 0) {
|
|
const command = configRead16(bus, dev, function, 0x04);
|
|
configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11));
|
|
var i: u64 = 0;
|
|
while (i < 6) : (i += 1) {
|
|
if (descriptor.resource_count >= 8) break;
|
|
const off = 0x10 + i * 4;
|
|
const original = configRead(bus, dev, function, off);
|
|
if (original == 0) continue;
|
|
const slot: usize = @intCast(descriptor.resource_count);
|
|
if (original & 1 != 0) {
|
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
|
const readback = configRead(bus, dev, function, off);
|
|
configWrite(bus, dev, function, off, original);
|
|
const mask = readback & 0xFFFF_FFFC;
|
|
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
|
if (size == 0) continue; // unimplemented BAR — nothing to register
|
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.io_port), .start = original & 0xFFFF_FFFC, .len = size };
|
|
descriptor.resource_count += 1;
|
|
} else if ((original >> 1) & 0x3 == 2) {
|
|
const original_high = configRead(bus, dev, function, off + 4);
|
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
|
configWrite(bus, dev, function, off + 4, 0xFFFF_FFFF);
|
|
const lo = configRead(bus, dev, function, off);
|
|
const hi = configRead(bus, dev, function, off + 4);
|
|
configWrite(bus, dev, function, off, original);
|
|
configWrite(bus, dev, function, off + 4, original_high);
|
|
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
|
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
|
i += 1; // consumed the high half regardless
|
|
if (size == 0) continue;
|
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = (@as(u64, original_high) << 32) | (original & 0xFFFF_FFF0), .len = size };
|
|
descriptor.resource_count += 1;
|
|
} else {
|
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
|
const readback = configRead(bus, dev, function, off);
|
|
configWrite(bus, dev, function, off, original);
|
|
const mask = readback & 0xFFFF_FFF0;
|
|
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
|
if (size == 0) continue;
|
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = original & 0xFFFF_FFF0, .len = size };
|
|
descriptor.resource_count += 1;
|
|
}
|
|
}
|
|
configWrite16(bus, dev, function, 0x04, command);
|
|
}
|
|
|
|
const registered = device.register(bridge_id, &descriptor) orelse {
|
|
std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
|
|
return;
|
|
};
|
|
const report = protocol.ChildAdded{
|
|
.parent = bridge_id,
|
|
.bus_address = (bus << 8) | (dev << 3) | function,
|
|
.identity = class_triple,
|
|
.device_id = registered,
|
|
};
|
|
var reply: [protocol.message_maximum]u8 = undefined;
|
|
_ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
|
|
std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
|
|
};
|
|
}
|
|
|
|
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 {
|
|
std.log.info("malformed bridge device id '{s}'", .{argument});
|
|
return;
|
|
};
|
|
runtime.service.run(protocol.message_maximum, .{
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
});
|
|
}
|