Skip to main content

strat9_kernel/arch/x86_64/
msi.rs

1//! MSI / MSI-X interrupt support for PCI devices (x86_64).
2//!
3//! MSI (Message Signalled Interrupts) bypass the I/O APIC entirely:
4//! the device writes a message directly to the LAPIC's system bus.
5//!
6//! ## Reference
7//! - PCI Local Bus Spec 3.0, §6.8
8//! - Intel 64 and IA-32 SDM, Vol. 3A, §10.11
9
10use crate::{arch::x86_64::pci::*, hardware::pci_client::PciDevice};
11
12/// Try to enable MSI on `pci_dev` with `vector`.
13///
14/// Returns `true` on success, `false` if MSI is not supported or
15/// programming failed.  On success the device will deliver interrupts
16/// via MSI instead of legacy INTx.
17///
18/// # Note
19///
20/// This does **not** clear the INTx_DISABLE bit in the PCI command
21/// register.  The caller should set `COMMAND.INTx_DISABLE` after
22/// enabling MSI to prevent the device from also asserting its legacy
23/// interrupt pin.
24fn enable_msi(pci_dev: &PciDevice, vector: u8) -> bool {
25    let Some(cap) = pci_dev.msi_cap_offset() else {
26        return false;
27    };
28
29    let ctrl = pci_dev.read_config_u16(cap + msi_cap::CONTROL);
30    let addr64 = (ctrl & msi_ctrl::ADDR64) != 0;
31    let lapic_id = crate::arch::x86_64::apic::lapic_id();
32
33    // Message Address = 0xFEE0_0000 | (LAPIC ID << 12)
34    let msg_addr = MSI_ADDR_BASE | (lapic_id << MSI_ADDR_DEST_SHIFT);
35
36    // Message Data = vector (fixed delivery, edge-triggered)
37    let msg_data = vector as u16;
38
39    // Program the MSI registers (all 32-bit writes).
40    pci_dev.write_config_u32(cap + msi_cap::ADDR_LOW, msg_addr);
41
42    if addr64 {
43        // 64-bit: upper addr at +8, data at +12
44        pci_dev.write_config_u32(cap + msi_cap::ADDR_HIGH, 0);
45        pci_dev.write_config_u16(cap + msi_cap::DATA + 4, msg_data);
46    } else {
47        // 32-bit: data at +8
48        pci_dev.write_config_u16(cap + msi_cap::DATA, msg_data);
49    }
50
51    // Enable MSI: set bit 0 in the control register.
52    // Keep Multi-Message Enable at 0 (single vector).
53    let new_ctrl = ctrl & !msi_ctrl::MME_MASK | msi_ctrl::ENABLE;
54    pci_dev.write_config_u16(cap + msi_cap::CONTROL, new_ctrl);
55
56    // Verify that MSI Enable stuck.
57    let verify = pci_dev.read_config_u16(cap + msi_cap::CONTROL);
58    if verify & msi_ctrl::ENABLE == 0 {
59        log::warn!(
60            "MSI: enable bit did not stick on {:04x}:{:04x}",
61            pci_dev.vendor_id,
62            pci_dev.device_id
63        );
64        return false;
65    }
66
67    // Disable legacy INTx in the PCI command register so the device does
68    // not assert its interrupt pin alongside the MSI message.
69    let mut cmd = pci_dev.read_config_u16(config::COMMAND);
70    cmd |= command::INTERRUPT_DISABLE;
71    pci_dev.write_config_u16(config::COMMAND, cmd);
72
73    log::info!(
74        "MSI: enabled on {:04x}:{:04x} → vector {:#x} ({} addr)",
75        pci_dev.vendor_id,
76        pci_dev.device_id,
77        vector,
78        if addr64 { "64-bit" } else { "32-bit" },
79    );
80    true
81}
82
83/// Try to enable MSI-X on `pci_dev` with `vector` using the first table entry.
84///
85/// MSI-X requires access to the device's **BAR space** for the vector table.
86/// This function maps the table BAR if not already mapped, programs the
87/// first table entry with the MSI message address/data, then enables MSI-X.
88///
89/// Returns `true` on success, `false` if MSI-X is not supported or
90/// programming failed.
91///
92/// # Safety
93///
94/// The caller must ensure that the BAR containing the MSI-X table is
95/// already identity-mapped (or mapped via the kernel's phys-to-virt
96/// window), otherwise the table writes will fault.
97fn enable_msix(pci_dev: &PciDevice, vector: u8) -> bool {
98    let Some(cap) = pci_dev.msix_cap_offset() else {
99        return false;
100    };
101
102    let ctrl = pci_dev.read_config_u16(cap + msix_cap::CONTROL);
103    let table_size = (ctrl & msix_ctrl::TABLE_SIZE_MASK) as usize;
104    if table_size == 0 {
105        log::warn!(
106            "MSI-X: {:04x}:{:04x} has zero-sized table",
107            pci_dev.vendor_id,
108            pci_dev.device_id
109        );
110        return false;
111    }
112
113    // Parse Table BAR indicator + offset.
114    let table_reg = pci_dev.read_config_u32(cap + msix_cap::TABLE);
115    let bar_index = (table_reg & 0x7) as u8;
116    let tbl_offset = (table_reg & 0xFFFF_FFF8) as u64;
117
118    // Map the table BAR if needed.
119    let bar = match pci_dev.read_bar(bar_index) {
120        Some(bar) => bar,
121        None => {
122            log::warn!(
123                "MSI-X: {:04x}:{:04x} table BAR {} invalid",
124                pci_dev.vendor_id,
125                pci_dev.device_id,
126                bar_index
127            );
128            return false;
129        }
130    };
131
132    let (bar_phys, _bar_size) = match bar {
133        crate::hardware::pci_client::Bar::Memory32 { addr, .. } => (addr as u64, 0x1000),
134        crate::hardware::pci_client::Bar::Memory64 { addr, .. } => (addr, 0x1000),
135        _ => {
136            log::warn!(
137                "MSI-X: {:04x}:{:04x} table BAR is I/O space",
138                pci_dev.vendor_id,
139                pci_dev.device_id
140            );
141            return false;
142        }
143    };
144
145    // Ensure the BAR + table region is identity-mapped.
146    let table_phys = bar_phys + tbl_offset;
147    crate::memory::paging::ensure_identity_map_range(table_phys, 0x1000);
148    let table_virt = crate::memory::phys_to_virt(table_phys) as *mut u32;
149
150    let lapic_id = crate::arch::x86_64::apic::lapic_id();
151    let msg_addr = MSI_ADDR_BASE | (lapic_id << MSI_ADDR_DEST_SHIFT);
152    let msg_data = vector as u32; // bits 7:0 = vector
153
154    // Program the first MSI-X table entry (16 bytes per entry).
155    // Entry layout: [addr_lo:32, addr_hi:32, data:32, ctrl:32]
156    // Entry 0 offset in table = 0
157    // SAFETY: we mapped the BAR region.
158    unsafe {
159        // Message Address (low 32 bits)
160        core::ptr::write_volatile(table_virt, msg_addr);
161        // Message Address (high 32 bits = 0 for x86_64)
162        core::ptr::write_volatile(table_virt.add(1), 0u32);
163        // Message Data
164        core::ptr::write_volatile(table_virt.add(2), msg_data);
165        // Vector Control: 0 = unmasked
166        core::ptr::write_volatile(table_virt.add(3), 0u32);
167    }
168
169    // Enable MSI-X in the capability control register.
170    let new_ctrl = (ctrl | msix_ctrl::ENABLE) & !msix_ctrl::FUNC_MASK;
171    pci_dev.write_config_u16(cap + msix_cap::CONTROL, new_ctrl);
172
173    // Disable legacy INTx.
174    let mut cmd = pci_dev.read_config_u16(config::COMMAND);
175    cmd |= command::INTERRUPT_DISABLE;
176    pci_dev.write_config_u16(config::COMMAND, cmd);
177
178    log::info!(
179        "MSI-X: enabled on {:04x}:{:04x} → vector {:#x} ({} entries, table BAR{}+{:#x})",
180        pci_dev.vendor_id,
181        pci_dev.device_id,
182        vector,
183        table_size + 1,
184        bar_index,
185        tbl_offset,
186    );
187    true
188}
189
190/// Probe a PCI device and enable the best available interrupt mode.
191///
192/// Priority: **MSI-X** => **MSI** => **INTx** (legacy I/O APIC routing).
193///
194/// `vector` is the IDT vector to associate with the interrupt
195/// (typically `PIC1_OFFSET + irq` for legacy IRQs, or a free vector
196/// for MSI/MSI-X).
197///
198/// Returns `(irq_line, vector)` suitable for `register_nic_irq()`.
199/// When MSI/MSI-X is active the `irq_line` value is only used for EOI
200/// in the no-APIC fallback path (which MSI requires APIC, so the value
201/// is cosmetic).
202pub fn probe_and_enable(pci_dev: &PciDevice, prefer_msix: bool) -> (u8, u8) {
203    let irq_line = pci_dev.interrupt_line;
204    let vector = if irq_line < 16 && irq_line != 0 && irq_line != 0xFF {
205        0x20 + irq_line
206    } else if irq_line != 0 && irq_line != 0xFF {
207        irq_line
208    } else {
209        0x20 // fallback vector
210    };
211
212    // 1. Try MSI-X first (if preferred and available).
213    if prefer_msix && enable_msix(pci_dev, vector) {
214        return (irq_line, vector);
215    }
216
217    // 2. Try MSI.
218    if enable_msi(pci_dev, vector) {
219        return (irq_line, vector);
220    }
221
222    // 3. Fall back to INTx : caller must route via I/O APIC.
223    log::info!(
224        "MSI/MSI-X unavailable on {:04x}:{:04x}, falling back to INTx IRQ {}",
225        pci_dev.vendor_id,
226        pci_dev.device_id,
227        irq_line,
228    );
229    (irq_line, vector)
230}