// --------------------------------------------------------------------------- // PCI config space — legacy port-based access (0xCF8 / 0xCFC). // // Enumeration walks buses 0-255 × devices 0-31 × functions 0-7. Port I/O // stays usable after ExitBootServices, so the shell can list devices and // future drivers (AHCI, network, ...) can find their controllers. // --------------------------------------------------------------------------- const CONFIG_ADDRESS: u16 = 0xCF8; const CONFIG_DATA: u16 = 0xCFC; #[derive(Debug, Clone, Copy)] pub struct DeviceInfo { pub bus: u8, pub device: u8, pub func: u8, pub vendor: u16, pub device_id: u16, pub class: u8, pub subclass: u8, pub prog_if: u8, pub header_type: u8, } fn outl(port: u16, val: u32) { unsafe { core::arch::asm!( "out dx, eax", in("dx") port, in("eax") val, options(nomem, nostack, preserves_flags) ); } } fn inl(port: u16) -> u32 { let val: u32; unsafe { core::arch::asm!( "in eax, dx", out("eax") val, in("dx") port, options(nomem, nostack, preserves_flags) ); } val } /// Encode the PCI config-space port address for (bus, dev, fn, reg); /// `reg` must be dword-aligned. fn config_addr(bus: u8, device: u8, func: u8, reg: u8) -> u32 { 0x8000_0000 | ((bus as u32) << 16) | ((device as u32) << 11) | ((func as u32) << 8) | ((reg as u32) & 0xFC) } /// Read a dword from a device's config space (`reg` must be dword-aligned). pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 { outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); inl(CONFIG_DATA) } /// Write a dword to a device's config space (`reg` must be dword-aligned). pub fn config_write(bus: u8, device: u8, func: u8, reg: u8, val: u32) { outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); outl(CONFIG_DATA, val) } fn outb(port: u16, val: u8) { unsafe { core::arch::asm!( "out dx, al", in("dx") port, in("al") val, options(nomem, nostack, preserves_flags) ); } } /// Write a single byte to config space (any byte offset). The PCI /// byte-enable cycle goes out on port 0xCFC + lane, exactly like `pci_ /// write_config_byte` everywhere else; QEMU and real chipsets both decode /// the low two address bits of the config-data port. pub fn config_write_byte(bus: u8, device: u8, func: u8, reg: u8, val: u8) { outl(CONFIG_ADDRESS, config_addr(bus, device, func, reg)); outb(CONFIG_DATA + ((reg & 3) as u16), val) } /// Read a memory BAR (index 0-5) as a 64-bit address, handling 32/64-bit /// BARs. Returns None for I/O BARs or invalid values. pub fn bar64(bus: u8, device: u8, func: u8, bar_index: u8) -> Option { let reg_off = 0x10 + bar_index * 4; let lo = config_read(bus, device, func, reg_off); if lo & 1 == 1 { return None; // I/O BAR } let kind = (lo >> 1) & 0x3; let hi = if kind == 2 { config_read(bus, device, func, reg_off + 4) } else { 0 }; let base = ((hi as u64) << 32) | (lo as u64 & 0xFFFF_FFF0); if base == 0 { None } else { Some(base) } } /// Claim USB Legacy ownership from the BIOS on a controller whose MMIO base /// exposes the USB Legacy Support extended capability (USBLEGSUP, cap id 1). /// /// EHCI/xHCI “USB Legacy Support” ownership handoff (EHCI spec §5.1). /// /// The 2012-era PCH BIOS drives USB keyboards through SMM USB-legacy /// emulation while it owns the controller: its SMI handler polls the EHCI /// over USB-error/port-change events. If the OS reprograms the controller /// and runs async DMA underneath the still-active BIOS module, the SMM poll /// collides with the OS transfer at an unpredictable moment — a classic /// real-silicon Host System Error (SE) that QEMU cannot reproduce (no SMM). /// /// CRITICAL: the extended-capability pointer is a PCI *config-space* /// offset, not an MMIO offset. Read it from an MMIO register (`eccp_src` /// = MMIO dword offset; EHCI: HCCPARAMS @+0x08 bits 15:8; xHCI: HCSPARAMS1 /// @+0x04 bits 31:16), then walk config space — exactly Linux's /// `quirk_usb_handoff_ehci` (pci-quirks.c) and FreeBSD's `ehci_pci_attach`. /// Our previous implementation treated EECP as an MMIO offset, read a /// reserved register (0), and silently never claimed ownership — the /// observed “claim skipped” on the 7010. /// /// Handshake (Linux/FreeBSD-identical): if the BIOS-owned semaphore /// (bit 16 of the cap dword) is set, write the OS-owned semaphore byte /// (cap+3) = 1 and spin (bounded ~1 s) for bit 16 to clear. USBLEGCTLSTS /// (cap+4) is deliberately left alone — Linux notes some systems “get /// upset” if it's touched. /// /// Returns: 0 = already released/owned (no claim needed), 1 = handed over, /// 2 = BIOS never released, 3 = no legacy capability / chain out of range. pub fn claim_usb_legacy( bus: u8, dev: u8, func: u8, mmio_base: usize, eccp_src: usize, eccp_bits_high: bool, ) -> u8 { let src = unsafe { (mmio_base as *const u32).add(eccp_src).read_volatile() }; crate::serial_println!( "usb-legacy: probe mmio=+{:#x} hcc={:#010x} high={}", eccp_src, src, eccp_bits_high ); let mut offset = if eccp_bits_high { ((src >> 16) & 0xFFFF) as u8 // xHCI: XECP (u8 caps the walk at 0xFF config) } else { ((src >> 8) & 0xFF) as u8 // EHCI: EECP }; if offset == 0 { return 3; } let mut guard = 32; // bounded walk, no panics while offset != 0 && guard > 0 { guard -= 1; let cap = config_read(bus, dev, func, offset); crate::serial_println!( "usb-legacy: cap@{:#x} id={:#04x} next={:#04x} d={:#010x}", offset, cap & 0xFF, (cap >> 8) & 0xFF, cap ); let id = cap & 0xFF; let next = ((cap >> 8) & 0xFF) as u8; match id { 0 => return 3, // illegal reserved cap terminates the chain 1 => { // USB Legacy Support capability. let ctlsts = config_read(bus, dev, func, offset + 4); crate::serial_println!( "usb-legacy: cap@{:#x} bios_sem={} os_sem={} ctlsts={:#x}", offset, (cap >> 16) & 1, (cap >> 24) & 1, ctlsts ); if cap & (1 << 24) != 0 { return 0; // OS already owns } if cap & (1 << 16) == 0 { return 0; // BIOS not claiming it } crate::serial_println!("usb-legacy: BIOS owns controller - claiming (OS sem)"); config_write_byte(bus, dev, func, offset + 3, 1); let t0 = crate::interrupts::uptime_ms(); let mut handed = false; while crate::interrupts::uptime_ms().wrapping_sub(t0) < 1000 { if config_read(bus, dev, func, offset) & (1 << 16) == 0 { handed = true; break; } core::hint::spin_loop(); } return if handed { 1 } else { 2 }; } _ => { offset = next; } } } 3 } /// Enumerate every device/function present on all buses. pub fn probe() -> alloc::vec::Vec { let mut out = alloc::vec::Vec::new(); for bus in 0..=255u8 { for dev in 0..32u8 { for func in 0..8u8 { let id = config_read(bus, dev, func, 0); let vendor = (id & 0xFFFF) as u16; if vendor == 0xFFFF { if func == 0 { break; // nothing on this device number } continue; } let hdr = config_read(bus, dev, func, 0x0C); let header_type = (hdr & 0xFF) as u8; let cc = config_read(bus, dev, func, 0x08); out.push(DeviceInfo { bus, device: dev, func, vendor, device_id: (id >> 16) as u16, class: (cc >> 24) as u8, subclass: (cc >> 16) as u8, prog_if: (cc >> 8) as u8, header_type, }); // A non-multifunction device has no functions 1-7. if func == 0 && header_type & 0x80 == 0 { break; } } } } out } /// Short human name for a (class, subclass) pair. pub fn class_name(class: u8, subclass: u8) -> &'static str { match (class, subclass) { (0x00, 0x01) => "VGA-compatible", (0x00, _) => "Unclassified", (0x01, 0x01) => "IDE controller", (0x01, 0x06) => "SATA controller", (0x01, 0x08) => "NVM controller", (0x01, _) => "Mass storage", (0x02, 0x00) => "Ethernet", (0x02, _) => "Network", (0x03, 0x00) => "VGA", (0x03, _) => "Display", (0x04, _) => "Multimedia", (0x05, _) => "Memory controller", (0x06, 0x00) => "Host bridge", (0x06, 0x01) => "ISA bridge", (0x06, 0x04) => "PCI-PCI bridge", (0x06, _) => "Bridge", (0x08, 0x80) => "PIC", (0x0C, 0x03) => "USB controller", (0x0C, 0x05) => "SMBus", (0x0C, _) => "Serial bus", (0x11, _) => "Signal processing", (0xFF, _) => "Unassigned", _ => "Other", } }