pci-bus joins usb-xhci-bus in receiving its device from the manager rather
than claiming the id it found in argv[1]. Its hello moves ahead of the ECAM
mapping, since that is where the bridge now arrives, and its hello was
already mandatory so nothing about its failure behaviour changes.
isDelegated compared whole strings, which silently missed this driver: the
manager records the boot-snapshot match as the bare "pci-bus" and a
devices.csv match as the full "/system/drivers/pci-bus". pci-bus was then
neither claiming nor delegated and died on "ECAM mmio_map failed". It now
matches on the last path component. Reintroducing the whole-string compare
breaks usb-xhci-bus instead of pci-bus — the two spellings swap which driver
loses — so usb-hid is the case that catches it, not pci-scan.
pci-scan asserts the delegation on the initial bring-up AND after the
restart drill, with the device id backreferenced so both must name the same
device. That is what proves the manager re-takes a device when its driver
dies and hands it to the replacement, which is the property the whole
supervision design rests on.
The remaining three claimants are NOT converted, and the plan records why
rather than working around it. ps2-bus and the acpi service never hello at
all, which device-manager.md states deliberately ("legacy drivers ... not
yet required to hello"), so delegating to them means either promoting them
out of legacy or giving the grant a delivery point that is not hello.
virtio-gpu hellos best-effort by design — "standalone bring-up has no
manager" — and delegation would make it mandatory. Both are decisions, not
mechanical steps.
Consequence: D6 is blocked, because device_claim cannot be closed off while
three claimants still depend on it. D7-D9 are unaffected — they concern what
the kernel stores and how its table is sized.
Suite 118/118.
288 lines
15 KiB
Zig
288 lines
15 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 device = @import("driver");
|
|
const ipc = @import("ipc");
|
|
const process = @import("process");
|
|
const service = @import("service");
|
|
const device_manager = @import("driver");
|
|
const memory = @import("memory");
|
|
const logging = @import("logging");
|
|
const device_manager_protocol = @import("device-manager-protocol");
|
|
const pci_class = @import("pci-class");
|
|
|
|
/// Log a discovered function as its would-be /system/configuration/devices.csv columns (bus, base,
|
|
/// class, prog_if, vendor, device, subsystem) followed by the human-readable
|
|
/// class/subclass/prog-IF names — so a row for a new driver reads straight off the
|
|
/// boot log. `subsystem` prints as `*` when the function has none, matching the CSV
|
|
/// wildcard. All read unclaimed, through the bridge's ECAM: the enumerator never
|
|
/// claims the functions it probes (pci.zig's header — the device-owned pci.Function
|
|
/// view is what needs a claim, not this one). A wide buffer: the names are long.
|
|
fn logFunction(bus: u64, dev: u64, function: u64, class_triple: u32, vendor_id: u16, product_id: u16, subsystem: u32) void {
|
|
const cc = pci_class.ClassCode.unpack(@truncate(class_triple));
|
|
const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
|
|
var sub_buffer: [8]u8 = undefined;
|
|
const sub = if (subsystem == 0) "*" else std.fmt.bufPrint(&sub_buffer, "{X:0>8}", .{subsystem}) catch "*";
|
|
var line: [320]u8 = undefined;
|
|
const text = std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} bus=pci base={X:0>2} class={X:0>2} prog_if={X:0>2} vendor={X:0>4} device={X:0>4} subsystem={s} — {s} / {s}{s}{s}\n", .{ bus, dev, function, cc.base, cc.subclass, cc.prog_if, vendor_id, product_id, sub, pci_class.className(cc.base), pci_class.subclassName(cc.base, cc.subclass), if (pif.len != 0) " / " else "", pif }) catch return;
|
|
_ = logging.write(text);
|
|
}
|
|
|
|
var bridge_id: u64 = device_manager_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: ipc.Handle = 0;
|
|
/// Functions this scan discovered but could not publish. Counted so the end of the
|
|
/// scan can reconcile "found" against "registered" — a scan that silently returns a
|
|
/// subset of the machine is the failure this driver is most able to hide.
|
|
var refused: u32 = 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));
|
|
}
|
|
|
|
/// Hello the manager (which is where the bridge arrives), map the ECAM, then scan.
|
|
fn initialise(endpoint: ipc.Handle) bool {
|
|
_ = endpoint;
|
|
// **The handshake first, because it is where the device arrives.** This driver
|
|
// used to claim `bridge_id` here — first-come-first-served, so the manager's
|
|
// matching was advisory and any process could have claimed the bridge by naming
|
|
// the same id. The manager now holds it and transfers it in the hello reply
|
|
// (docs/os-development/device-authority.md). `hello` is synchronous, so the
|
|
// transfer has completed by the time this returns.
|
|
//
|
|
// Keep the manager handle to report children through; a supervised bus that
|
|
// cannot reach its manager has nothing to serve.
|
|
manager_handle = device_manager.hello(.bus, bridge_id) orelse {
|
|
std.log.info("no hello with the device manager; bridge {d} not delegated", .{bridge_id});
|
|
return false;
|
|
};
|
|
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = logging.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)) {
|
|
_ = logging.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 {
|
|
_ = logging.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 {
|
|
_ = logging.write("/system/drivers/pci-bus: ECAM mmio_map failed\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;
|
|
registerAndReport(bus, dev, function, class_revision >> 8);
|
|
}
|
|
}
|
|
}
|
|
// Reconcile: "found" alone reads as success even when most of the machine was
|
|
// refused. If the two disagree, say so at a level that survives a scrollback.
|
|
if (refused == 0) {
|
|
std.log.info("{d} functions found, all registered", .{found});
|
|
} else {
|
|
std.log.warn("{d} functions found, {d} REFUSED — {d} registered", .{ found, refused, found - refused });
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
// Vendor/device from the first config dword (0x00): low half vendor, high half
|
|
// device. These carry to the manager's /system/configuration/devices.csv matcher so a function
|
|
// can bind on its exact 1AF4:1050 identity, not just its class triple.
|
|
const vendor_device = configRead(bus, dev, function, 0x00);
|
|
descriptor.vendor = @truncate(vendor_device);
|
|
descriptor.device = @truncate(vendor_device >> 16);
|
|
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) {
|
|
// Subsystem id lives at 0x2C only on type-0 (device) headers, not on
|
|
// bridges: dword low half is subsystem-vendor, high half subsystem-device.
|
|
// Repack vendor-first so it reads like the CSV's `ssvid:ssid`.
|
|
const subsystem_dword = configRead(bus, dev, function, 0x2C);
|
|
descriptor.subsystem = (@as(u32, @truncate(subsystem_dword)) << 16) | @as(u32, @truncate(subsystem_dword >> 16));
|
|
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);
|
|
}
|
|
|
|
// The devices.csv-column + friendly-name breadcrumb, now that vendor/device/
|
|
// subsystem are read. Every discovered function is logged, matched or not.
|
|
logFunction(bus, dev, function, class_triple, descriptor.vendor, descriptor.device, descriptor.subsystem);
|
|
|
|
// Name the rule that refused. `ParentFull` and `TableFull` are different ceilings
|
|
// with different fixes, and `NotContained` is not a ceiling at all — it means the
|
|
// BAR escaped the bridge's own window (docs/fixed-bounds-audit.md).
|
|
const registered = device.register(bridge_id, &descriptor) catch |e| {
|
|
refused += 1;
|
|
std.log.warn("register refused for {d}:{d}.{d}: {s}", .{ bus, dev, function, @errorName(e) });
|
|
return;
|
|
};
|
|
// The registered device id is the packet's target — the manager's object
|
|
// addressing — so the report body carries only where on the bus it sits and
|
|
// what it is.
|
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
const framed = device_manager_protocol.Protocol.encodeRequest(.child_added, registered, .{
|
|
.bus = @intFromEnum(device_manager_protocol.BusKind.pci),
|
|
.parent = bridge_id,
|
|
.bus_address = (bus << 8) | (dev << 3) | function,
|
|
.identity = class_triple,
|
|
.vendor = descriptor.vendor,
|
|
.device = descriptor.device,
|
|
.subsystem = descriptor.subsystem,
|
|
}, &.{}, &packet) orelse return;
|
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
_ = ipc.call(manager_handle, framed, &reply) catch {
|
|
std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
|
|
};
|
|
}
|
|
|
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
|
_ = message;
|
|
_ = reply;
|
|
_ = sender;
|
|
_ = arrived; // nothing here takes a capability: the harness closes what arrives
|
|
return 0;
|
|
}
|
|
|
|
pub fn main(init: 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;
|
|
};
|
|
service.run(device_manager_protocol.message_maximum, .{
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
});
|
|
}
|