use std::{string::String, vec::Vec};
#[cfg(target_arch = "loongarch64")]
use ax_std::os::arceos::driver as ax_driver;
use axdevice::{
DeviceFirmwareBinding, DeviceFirmwareProperty, DeviceFirmwareSpec, ResolvedDeviceGraph,
};
use axdevice_base::AccessWidth;
use super::GuestMmioRegion;
use crate::{AxVmError, AxVmResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GuestSerialModel {
Uart16550,
Pl011,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GuestSerialTransport {
Port { base: u16, length: u16 },
Mmio {
base: usize,
length: usize,
register_shift: u8,
register_width: AccessWidth,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GuestSerialProfile {
pub model: GuestSerialModel,
pub transport: GuestSerialTransport,
pub irq: usize,
pub clock_hz: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestClockReference {
pub provider_phandle: u32,
pub specifier: Vec<u32>,
pub provider_regions: Vec<GuestMmioRegion>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestSerialFdtIdentity {
pub node_path: String,
pub node_phandle: Option<u32>,
pub interrupt_parent: u32,
pub interrupt_specifier: Vec<u32>,
pub stdout_path: String,
pub clock_references: Vec<GuestClockReference>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GuestSerialAcpiIdentity {
pub namespace_path: Option<String>,
pub source_table: [u8; 4],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GuestSerialFirmwareIdentity {
Fdt(GuestSerialFdtIdentity),
Acpi(GuestSerialAcpiIdentity),
}
impl GuestSerialFirmwareIdentity {
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
pub(crate) const fn fdt(&self) -> Option<&GuestSerialFdtIdentity> {
match self {
Self::Fdt(identity) => Some(identity),
Self::Acpi(_) => None,
}
}
pub(crate) fn binding(&self) -> DeviceFirmwareBinding {
match self {
Self::Fdt(identity) => DeviceFirmwareBinding::FdtNode(identity.node_path.clone()),
Self::Acpi(identity) => identity
.namespace_path
.clone()
.map(DeviceFirmwareBinding::AcpiDevice)
.unwrap_or(DeviceFirmwareBinding::None),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HostSerialSnapshot {
pub profile: GuestSerialProfile,
pub identity: GuestSerialFirmwareIdentity,
}
#[cfg(any(
target_arch = "loongarch64",
all(target_arch = "x86_64", feature = "host-fs")
))]
pub(crate) fn host_serial_from_acpi(
serial: ax_driver::probe::acpi::AcpiSerialConsole,
fallback: GuestSerialProfile,
) -> AxVmResult<HostSerialSnapshot> {
use ax_driver::probe::acpi::{AcpiSerialAddressSpace, AcpiSerialInterface};
if serial.registers.size == 0 {
return Err(AxVmError::invalid_config(
"host SPCR serial register range is empty",
));
}
let model = match serial.interface {
AcpiSerialInterface::Uart16550 => GuestSerialModel::Uart16550,
AcpiSerialInterface::Pl011 => GuestSerialModel::Pl011,
};
let register_width = match serial.access_size {
0 | 1 => AccessWidth::Byte,
2 => AccessWidth::Word,
3 => AccessWidth::Dword,
4 => AccessWidth::Qword,
value => {
return Err(AxVmError::invalid_config(std::format!(
"host SPCR serial access size {value} is invalid"
)));
}
};
let transport = match serial.address_space {
AcpiSerialAddressSpace::Memory => GuestSerialTransport::Mmio {
base: usize::try_from(serial.registers.base).map_err(|_| {
AxVmError::invalid_config("host SPCR serial address exceeds the target width")
})?,
length: usize::try_from(serial.registers.size).map_err(|_| {
AxVmError::invalid_config("host SPCR serial range exceeds the target width")
})?,
register_shift: 0,
register_width: if model == GuestSerialModel::Pl011 {
AccessWidth::Dword
} else {
register_width
},
},
AcpiSerialAddressSpace::Io => GuestSerialTransport::Port {
base: u16::try_from(serial.registers.base).map_err(|_| {
AxVmError::invalid_config("host SPCR serial I/O port exceeds 16 bits")
})?,
length: u16::try_from(serial.registers.size).map_err(|_| {
AxVmError::invalid_config("host SPCR serial I/O range exceeds 16 bits")
})?,
},
};
let irq = serial.irq.ok_or_else(|| {
AxVmError::invalid_config("host SPCR selected a serial console without an interrupt")
})?;
Ok(HostSerialSnapshot {
profile: GuestSerialProfile {
model,
transport,
irq: usize::try_from(irq)
.map_err(|_| AxVmError::invalid_config("host SPCR IRQ exceeds usize"))?,
clock_hz: serial.clock_hz.unwrap_or(fallback.clock_hz),
},
identity: GuestSerialFirmwareIdentity::Acpi(GuestSerialAcpiIdentity {
namespace_path: serial.namespace_path,
source_table: *b"SPCR",
}),
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GuestSerialFdtInterrupt {
GicSpi,
PlicSource,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ResolvedSerialDevice {
id: String,
profile: GuestSerialProfile,
firmware_binding: DeviceFirmwareBinding,
}
impl ResolvedSerialDevice {
pub(crate) fn id(&self) -> &str {
&self.id
}
pub(crate) const fn profile(&self) -> GuestSerialProfile {
self.profile
}
pub(crate) const fn firmware_binding(&self) -> &DeviceFirmwareBinding {
&self.firmware_binding
}
}
pub(crate) fn resolved_serial_devices(
graph: &ResolvedDeviceGraph,
) -> AxVmResult<Vec<ResolvedSerialDevice>> {
graph
.nodes()
.filter_map(|node| {
let firmware = node.firmware();
serial_model(&firmware).map(|model| (node, firmware, model))
})
.map(|(node, firmware, model)| {
let registers = single_slot(&firmware, firmware.register_slots(), "register")?;
let interrupt = single_slot(&firmware, firmware.interrupt_slots(), "interrupt")?;
let resources = graph.resources_for(node.id())?;
let transport = if let Some((_, base, length)) =
resources.pio_ranges().find(|(slot, ..)| *slot == registers)
{
GuestSerialTransport::Port { base, length }
} else {
let (base, length) = resources.mmio(registers)?;
GuestSerialTransport::Mmio {
base: usize::try_from(base)
.map_err(|_| serial_range_error(node.id().as_str()))?,
length: usize::try_from(length)
.map_err(|_| serial_range_error(node.id().as_str()))?,
register_shift: u8::try_from(u32_property(&firmware, "reg-shift").unwrap_or(0))
.map_err(|_| serial_property_error(node.id().as_str(), "reg-shift"))?,
register_width: AccessWidth::try_from(
usize::try_from(u32_property(&firmware, "reg-io-width").unwrap_or(1))
.map_err(|_| {
serial_property_error(node.id().as_str(), "reg-io-width")
})?,
)
.map_err(|()| serial_property_error(node.id().as_str(), "reg-io-width"))?,
}
};
let irq = resources.wired_irq(interrupt)?.input().value();
Ok(ResolvedSerialDevice {
id: node.id().as_str().into(),
profile: GuestSerialProfile {
model,
transport,
irq,
clock_hz: u32_property(&firmware, "clock-frequency").unwrap_or(1_843_200),
},
firmware_binding: node.firmware_binding().clone(),
})
})
.collect()
}
fn serial_model(firmware: &DeviceFirmwareSpec) -> Option<GuestSerialModel> {
if firmware
.compatible()
.iter()
.any(|compatible| compatible == "arm,pl011")
{
Some(GuestSerialModel::Pl011)
} else if firmware
.compatible()
.iter()
.any(|compatible| matches!(compatible.as_str(), "ns16550" | "ns16550a"))
{
Some(GuestSerialModel::Uart16550)
} else {
None
}
}
fn single_slot<'a>(
firmware: &DeviceFirmwareSpec,
slots: &'a [axdevice::ResourceSlot],
kind: &'static str,
) -> AxVmResult<&'a axdevice::ResourceSlot> {
let [slot] = slots else {
return Err(AxVmError::invalid_config(std::format!(
"serial firmware model {:?} must declare exactly one {kind} slot",
firmware.node_name()
)));
};
Ok(slot)
}
fn u32_property(firmware: &DeviceFirmwareSpec, name: &str) -> Option<u32> {
firmware
.properties()
.iter()
.find_map(|property| match property {
DeviceFirmwareProperty::U32 {
name: property_name,
value,
} if property_name == name => Some(*value),
_ => None,
})
}
fn serial_range_error(device: &str) -> AxVmError {
AxVmError::invalid_config(std::format!(
"resolved serial range for {device} exceeds the target address width"
))
}
fn serial_property_error(device: &str, property: &'static str) -> AxVmError {
AxVmError::invalid_config(std::format!(
"resolved serial {device} has invalid {property}"
))
}
#[cfg(all(test, target_arch = "x86_64", feature = "host-fs"))]
mod tests;