408 lines
12 KiB
Rust
408 lines
12 KiB
Rust
use std::fs::File;
|
|
use std::io::{ErrorKind, Read};
|
|
|
|
#[cfg(target_os = "redox")]
|
|
use std::fs::OpenOptions;
|
|
#[cfg(target_os = "redox")]
|
|
use std::io::Write;
|
|
|
|
use crate::memory::{CacheType, MmioProt, MmioRegion};
|
|
use crate::pci::{MsixCapability, PciDevice, PciDeviceInfo};
|
|
use crate::{DriverError, Result};
|
|
|
|
const MSIX_ENTRY_SIZE: usize = 16;
|
|
const MSIX_VECTOR_CTRL_OFFSET: usize = 12;
|
|
const MSIX_MASK_BIT: u32 = 1;
|
|
#[cfg(target_os = "redox")]
|
|
const X86_MSI_ADDRESS_BASE: u64 = 0x0000_0000_FEE0_0000;
|
|
|
|
pub struct IrqHandle {
|
|
fd: File,
|
|
irq: u32,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct IrqEvent {
|
|
pub irq: u32,
|
|
}
|
|
|
|
pub struct MsixTable {
|
|
pub base: MmioRegion,
|
|
pub pba: MmioRegion,
|
|
pub table_size: u16,
|
|
pub bar_addr: u64,
|
|
}
|
|
|
|
pub struct MsixVector {
|
|
pub index: u16,
|
|
pub irq: u32,
|
|
pub fd: File,
|
|
}
|
|
|
|
impl IrqHandle {
|
|
#[cfg(target_os = "redox")]
|
|
pub fn request(irq: u32) -> Result<Self> {
|
|
let path = format!("/scheme/irq/{irq}");
|
|
let fd = File::open(&path).map_err(|e| {
|
|
log::warn!("failed to open IRQ {irq} at {path}: {e}");
|
|
e
|
|
})?;
|
|
log::debug!("IRQ {irq} acquired via {path}");
|
|
Ok(Self { fd, irq })
|
|
}
|
|
|
|
#[cfg(not(target_os = "redox"))]
|
|
pub fn request(irq: u32) -> Result<Self> {
|
|
Err(DriverError::Irq(format!(
|
|
"IRQ {irq} is only available on target_os=redox"
|
|
)))
|
|
}
|
|
|
|
pub fn wait(&mut self) -> Result<IrqEvent> {
|
|
let mut buf = [0u8; 8];
|
|
self.fd.read_exact(&mut buf)?;
|
|
Ok(IrqEvent { irq: self.irq })
|
|
}
|
|
|
|
pub fn try_wait(&mut self) -> Result<Option<IrqEvent>> {
|
|
let mut buf = [0u8; 8];
|
|
|
|
loop {
|
|
match self.fd.read(&mut buf) {
|
|
Ok(0) => return Ok(None),
|
|
Ok(_) => return Ok(Some(IrqEvent { irq: self.irq })),
|
|
Err(err) if err.kind() == ErrorKind::WouldBlock => return Ok(None),
|
|
Err(err) if err.kind() == ErrorKind::Interrupted => continue,
|
|
Err(err) => return Err(err.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
pub fn set_affinity(&self, cpu_mask: u64) -> Result<()> {
|
|
let path = format!("/scheme/irq/{}/affinity", self.irq);
|
|
let mut fd = OpenOptions::new().write(true).open(&path).map_err(|err| {
|
|
DriverError::Irq(format!("failed to open IRQ affinity control {path}: {err}"))
|
|
})?;
|
|
fd.write_all(&cpu_mask.to_le_bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(target_os = "redox"))]
|
|
pub fn set_affinity(&self, _cpu_mask: u64) -> Result<()> {
|
|
Err(DriverError::Irq(
|
|
"IRQ affinity control is only available on target_os=redox".into(),
|
|
))
|
|
}
|
|
|
|
pub fn irq(&self) -> u32 {
|
|
self.irq
|
|
}
|
|
}
|
|
|
|
impl MsixTable {
|
|
pub fn map(device_info: &PciDeviceInfo, cap: &MsixCapability) -> Result<Self> {
|
|
let table_bar = lookup_msix_bar(device_info, cap.table_bar, "table")?;
|
|
let pba_bar = lookup_msix_bar(device_info, cap.pba_bar, "PBA")?;
|
|
|
|
let table_len = usize::from(cap.table_size) * MSIX_ENTRY_SIZE;
|
|
let pba_len = usize::from(cap.table_size).div_ceil(64) * core::mem::size_of::<u64>();
|
|
|
|
let table_phys =
|
|
checked_bar_window(table_bar.addr, table_bar.size, cap.table_offset, table_len)?;
|
|
let pba_phys = checked_bar_window(pba_bar.addr, pba_bar.size, cap.pba_offset, pba_len)?;
|
|
|
|
let base = MmioRegion::map(
|
|
table_phys,
|
|
table_len,
|
|
CacheType::DeviceMemory,
|
|
MmioProt::READ_WRITE,
|
|
)?;
|
|
let pba = MmioRegion::map(
|
|
pba_phys,
|
|
pba_len,
|
|
CacheType::DeviceMemory,
|
|
MmioProt::READ_WRITE,
|
|
)?;
|
|
|
|
Ok(Self {
|
|
base,
|
|
pba,
|
|
table_size: cap.table_size,
|
|
bar_addr: table_bar.addr,
|
|
})
|
|
}
|
|
|
|
pub fn mask_all(&self) {
|
|
for index in 0..self.table_size {
|
|
self.mask_vector(index);
|
|
}
|
|
}
|
|
|
|
pub fn enable(&mut self, pci_device: &mut PciDevice, cap_offset: u8) -> Result<()> {
|
|
pci_device.enable_msix(cap_offset)
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
pub fn request_vector(&self, index: u16) -> Result<MsixVector> {
|
|
let cpu_id = read_bsp_cpu_id()?;
|
|
let (irq, fd) = allocate_irq_vector(cpu_id)?;
|
|
self.program_x86_message(index, cpu_id, irq)?;
|
|
self.unmask_vector(index);
|
|
log::info!(
|
|
"redox-driver-sys: allocated MSI-X vector {} -> irq {} on cpu {}",
|
|
index,
|
|
irq,
|
|
cpu_id
|
|
);
|
|
Ok(MsixVector { fd, index, irq })
|
|
}
|
|
|
|
#[cfg(not(target_os = "redox"))]
|
|
pub fn request_vector(&self, index: u16) -> Result<MsixVector> {
|
|
Err(DriverError::Irq(format!(
|
|
"MSI-X vector {index} allocation is only available on target_os=redox"
|
|
)))
|
|
}
|
|
|
|
pub fn mask_vector(&self, index: u16) {
|
|
if let Ok(offset) = self.entry_offset(index) {
|
|
self.base
|
|
.write32(offset + MSIX_VECTOR_CTRL_OFFSET, MSIX_MASK_BIT);
|
|
}
|
|
}
|
|
|
|
pub fn unmask_vector(&self, index: u16) {
|
|
if let Ok(offset) = self.entry_offset(index) {
|
|
self.base.write32(offset + MSIX_VECTOR_CTRL_OFFSET, 0);
|
|
}
|
|
}
|
|
|
|
pub fn is_pending(&self, index: u16) -> bool {
|
|
if index >= self.table_size {
|
|
return false;
|
|
}
|
|
|
|
let word_index = usize::from(index / 64) * core::mem::size_of::<u64>();
|
|
let bit = u32::from(index % 64);
|
|
(self.pba.read64(word_index) & (1u64 << bit)) != 0
|
|
}
|
|
|
|
fn entry_offset(&self, index: u16) -> Result<usize> {
|
|
if index >= self.table_size {
|
|
return Err(DriverError::Irq(format!(
|
|
"MSI-X vector index {index} is outside table size {}",
|
|
self.table_size
|
|
)));
|
|
}
|
|
Ok(usize::from(index) * MSIX_ENTRY_SIZE)
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
fn program_x86_message(&self, index: u16, cpu_id: u8, irq: u32) -> Result<()> {
|
|
let offset = self.entry_offset(index)?;
|
|
let vector = irq
|
|
.checked_add(32)
|
|
.ok_or_else(|| DriverError::Irq(format!("IRQ {irq} overflowed x86 vector space")))?;
|
|
let vector = u8::try_from(vector).map_err(|_| {
|
|
DriverError::Irq(format!("IRQ {irq} does not fit in an x86 MSI-X vector"))
|
|
})?;
|
|
let message_addr = X86_MSI_ADDRESS_BASE | (u64::from(cpu_id) << 12);
|
|
|
|
self.base.write32(offset, message_addr as u32);
|
|
self.base.write32(offset + 4, (message_addr >> 32) as u32);
|
|
self.base.write32(offset + 8, u32::from(vector));
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn lookup_msix_bar<'a>(
|
|
device_info: &'a PciDeviceInfo,
|
|
bar_index: u8,
|
|
label: &str,
|
|
) -> Result<&'a crate::pci::PciBarInfo> {
|
|
device_info
|
|
.find_memory_bar(bar_index as usize)
|
|
.ok_or_else(|| DriverError::CapabilityNotFound(format!("MSI-X {label} BAR {}", bar_index)))
|
|
}
|
|
|
|
fn checked_bar_window(bar_addr: u64, bar_size: u64, offset: u32, len: usize) -> Result<u64> {
|
|
let len_u64 = u64::try_from(len)
|
|
.map_err(|_| DriverError::InvalidParam("MSI-X BAR window length overflow"))?;
|
|
let start = bar_addr
|
|
.checked_add(u64::from(offset))
|
|
.ok_or(DriverError::InvalidParam("MSI-X BAR address overflow"))?;
|
|
let end = u64::from(offset)
|
|
.checked_add(len_u64)
|
|
.ok_or(DriverError::InvalidParam("MSI-X BAR range overflow"))?;
|
|
|
|
if end > bar_size {
|
|
return Err(DriverError::Irq(format!(
|
|
"MSI-X BAR window offset {:#x} len {:#x} exceeds BAR size {:#x}",
|
|
offset, len, bar_size
|
|
)));
|
|
}
|
|
|
|
Ok(start)
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
fn read_bsp_cpu_id() -> Result<u8> {
|
|
let mut fd = File::open("/scheme/irq/bsp")
|
|
.map_err(|err| DriverError::Irq(format!("failed to open /scheme/irq/bsp: {err}")))?;
|
|
let mut buf = [0u8; 8];
|
|
let bytes_read = fd.read(&mut buf)?;
|
|
|
|
let raw = match bytes_read {
|
|
8 => u64::from_le_bytes(buf),
|
|
4 => u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as u64,
|
|
_ => {
|
|
return Err(DriverError::Irq(format!(
|
|
"unexpected /scheme/irq/bsp payload size {bytes_read}"
|
|
)))
|
|
}
|
|
};
|
|
|
|
u8::try_from(raw).map_err(|_| DriverError::Irq(format!("BSP CPU id {raw} does not fit in u8")))
|
|
}
|
|
|
|
#[cfg(target_os = "redox")]
|
|
fn allocate_irq_vector(cpu_id: u8) -> Result<(u32, File)> {
|
|
let dir = format!("/scheme/irq/cpu-{cpu_id:02x}");
|
|
let entries = std::fs::read_dir(&dir).map_err(|err| {
|
|
DriverError::Irq(format!("failed to enumerate IRQ vectors in {dir}: {err}"))
|
|
})?;
|
|
|
|
let mut candidates = Vec::new();
|
|
for entry in entries {
|
|
let entry = entry?;
|
|
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
continue;
|
|
};
|
|
let Ok(irq) = name.parse::<u32>() else {
|
|
continue;
|
|
};
|
|
candidates.push(irq);
|
|
}
|
|
candidates.sort_unstable();
|
|
|
|
for irq in candidates {
|
|
let path = format!("{dir}/{irq}");
|
|
match OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(&path)
|
|
{
|
|
Ok(fd) => {
|
|
log::debug!(
|
|
"redox-driver-sys: reserved irq vector {} from {} for cpu {}",
|
|
irq,
|
|
path,
|
|
cpu_id
|
|
);
|
|
return Ok((irq, fd));
|
|
}
|
|
Err(err) if err.kind() == ErrorKind::AlreadyExists => continue,
|
|
Err(err) if err.kind() == ErrorKind::NotFound => continue,
|
|
Err(err) => {
|
|
return Err(DriverError::Irq(format!(
|
|
"failed to allocate MSI-X IRQ vector via {path}: {err}"
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(DriverError::Irq(format!(
|
|
"no free IRQ vectors available in {dir}"
|
|
)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::pci::{PciBarInfo, PciBarKind, PciLocation};
|
|
|
|
fn test_device_info() -> PciDeviceInfo {
|
|
PciDeviceInfo {
|
|
location: PciLocation {
|
|
segment: 0,
|
|
bus: 0,
|
|
device: 0,
|
|
function: 0,
|
|
},
|
|
vendor_id: 0x1002,
|
|
device_id: 0x1234,
|
|
subsystem_vendor_id: 0xffff,
|
|
subsystem_device_id: 0xffff,
|
|
revision: 0,
|
|
class_code: 0,
|
|
subclass: 0,
|
|
prog_if: 0,
|
|
header_type: 0,
|
|
irq: None,
|
|
bars: Vec::new(),
|
|
capabilities: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn checked_bar_window_accepts_in_range_region() {
|
|
let start = checked_bar_window(0x1000, 0x400, 0x80, 0x40).expect("window in range");
|
|
assert_eq!(start, 0x1080);
|
|
}
|
|
|
|
#[test]
|
|
fn checked_bar_window_rejects_region_past_bar_end() {
|
|
let error = checked_bar_window(0x1000, 0x100, 0xf0, 0x20).expect_err("window past end");
|
|
assert!(matches!(error, DriverError::Irq(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn checked_bar_window_rejects_address_overflow() {
|
|
let error = checked_bar_window(u64::MAX - 0x10, 0x200, 0x40, 0x20)
|
|
.expect_err("overflow should be rejected");
|
|
assert!(matches!(error, DriverError::InvalidParam("MSI-X BAR address overflow")));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_msix_bar_requires_memory_bar() {
|
|
let mut device = test_device_info();
|
|
device.bars.push(PciBarInfo {
|
|
index: 0,
|
|
kind: PciBarKind::Io,
|
|
addr: 0x3f8,
|
|
size: 8,
|
|
prefetchable: false,
|
|
});
|
|
|
|
let error = lookup_msix_bar(&device, 0, "table").expect_err("I/O BAR should be rejected");
|
|
assert!(matches!(error, DriverError::CapabilityNotFound(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_msix_bar_returns_matching_memory_bar() {
|
|
let mut device = test_device_info();
|
|
device.bars.push(PciBarInfo {
|
|
index: 2,
|
|
kind: PciBarKind::Memory64,
|
|
addr: 0x20_0000,
|
|
size: 0x1000,
|
|
prefetchable: true,
|
|
});
|
|
|
|
let bar = lookup_msix_bar(&device, 2, "table").expect("memory BAR should be found");
|
|
assert_eq!(bar.index, 2);
|
|
assert_eq!(bar.addr, 0x20_0000);
|
|
}
|
|
|
|
#[cfg(not(target_os = "redox"))]
|
|
#[test]
|
|
fn irq_request_reports_non_redox_platform_limit() {
|
|
match IrqHandle::request(5) {
|
|
Ok(_) => panic!("host builds should reject IRQ requests"),
|
|
Err(error) => assert!(matches!(error, DriverError::Irq(_))),
|
|
}
|
|
}
|
|
}
|