use consortium_dts::utils::irq_method::IrqMethod;
use consortium_dts::{Cell, Node, Property, Reference};
use crate::error::CfgError;
pub const GENERIC_UIO_COMPATIBLE: &str = "generic-uio";
pub trait ToUio {
fn to_uio(&self) -> Result<Option<UioDevice>, CfgError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UioBacking {
#[default]
Registers,
ReservedMemory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RegLayout {
#[default]
Wide,
Narrow,
}
#[derive(Debug, Clone, Default)]
pub struct UioOptions {
pub layout: RegLayout,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UioClock {
pub provider: String,
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UioDevice {
pub mmap: Vec<(u32, u32)>,
pub irq: Option<(u32, IrqMethod)>,
pub backing: UioBacking,
pub dma_coherent: Vec<bool>,
pub interrupt_parent: Option<String>,
pub clock: Option<UioClock>,
}
impl UioDevice {
pub fn to_node(&self, name: impl Into<String>) -> Node {
self.to_node_with_options(name, &UioOptions::default())
}
pub fn to_node_with_options(&self, name: impl Into<String>, options: &UioOptions) -> Node {
let name = name.into();
let mut node = match self.mmap.first() {
Some((base, _)) => Node::with_unit_address(name.clone(), hex(*base)),
None => Node::new(name.clone()),
};
node = node.property(Property::string("compatible", GENERIC_UIO_COMPATIBLE));
node = node.property(Property::string("linux,uio-name", name.clone()));
if !self.mmap.is_empty() {
match self.backing {
UioBacking::Registers => {
let cells = self
.mmap
.iter()
.flat_map(|(base, length)| reg_cells(*base, *length, options.layout));
node = node.property(Property::cells("reg", cells));
}
UioBacking::ReservedMemory => {
let refs = (0..self.mmap.len()).map(|index| {
Cell::reference(Reference::label(reserved_label(
&name,
index,
self.mmap.len(),
)))
});
node = node.property(Property::cells("memory-region", refs));
}
}
}
if let Some((spi, method)) = self.irq {
if let Some(parent) = &self.interrupt_parent {
node = node.property(Property::cells(
"interrupt-parent",
[Cell::reference(Reference::label(parent.as_str()))],
));
}
node = node.property(Property::cells(
"interrupts",
[
Cell::raw("GIC_SPI"),
Cell::number(u64::from(spi)),
Cell::raw(irq_trigger_macro(method)),
],
));
}
if let Some(clock) = &self.clock {
node = node.property(Property::cells(
"clocks",
[
Cell::reference(Reference::label(clock.provider.as_str())),
Cell::raw(clock.id.as_str()),
],
));
}
node.status_okay()
}
pub fn reserved_memory_nodes(&self, name: impl Into<String>) -> Vec<Node> {
self.reserved_memory_nodes_with_options(name, &UioOptions::default())
}
pub fn reserved_memory_nodes_with_options(
&self,
name: impl Into<String>,
options: &UioOptions,
) -> Vec<Node> {
if self.backing != UioBacking::ReservedMemory {
return Vec::new();
}
let name = name.into();
let count = self.mmap.len();
self.mmap
.iter()
.enumerate()
.map(|(index, (base, length))| {
let mut node = Node::with_unit_address(name.clone(), hex(*base))
.label(reserved_label(&name, index, count))
.property(Property::cells(
"reg",
reg_cells(*base, *length, options.layout),
));
if self.dma_coherent.get(index).copied().unwrap_or(false) {
node = node.property(Property::boolean("dma-coherent"));
}
node
.property(Property::boolean("no-map"))
})
.collect()
}
}
pub fn reserved_memory_container(
children: impl IntoIterator<Item = Node>,
layout: RegLayout,
) -> Option<Node> {
let children: Vec<Node> = children.into_iter().collect();
if children.is_empty() {
return None;
}
let cells = match layout {
RegLayout::Wide => 2u64,
RegLayout::Narrow => 1u64,
};
let mut node = Node::new("reserved-memory")
.property(Property::cells("#address-cells", [Cell::number(cells)]))
.property(Property::cells("#size-cells", [Cell::number(cells)]))
.property(Property::boolean("ranges"));
for child in children {
node = node.child(child);
}
Some(node)
}
fn reserved_label(name: &str, index: usize, count: usize) -> String {
let base = name.replace('-', "_");
if count > 1 {
format!("{base}_{index}")
} else {
base
}
}
fn reg_cells(base: u32, length: u32, layout: RegLayout) -> Vec<Cell> {
match layout {
RegLayout::Wide => vec![
Cell::raw("0x0"),
Cell::raw(hex(base)),
Cell::raw("0x0"),
Cell::raw(hex(length)),
],
RegLayout::Narrow => vec![Cell::raw(hex(base)), Cell::raw(hex(length))],
}
}
fn irq_trigger_macro(method: IrqMethod) -> &'static str {
match method {
IrqMethod::None => "IRQ_TYPE_NONE",
IrqMethod::EdgeRising => "IRQ_TYPE_EDGE_RISING",
IrqMethod::EdgeFalling => "IRQ_TYPE_EDGE_FALLING",
IrqMethod::EdgeBoth => "IRQ_TYPE_EDGE_BOTH",
IrqMethod::LevelHigh => "IRQ_TYPE_LEVEL_HIGH",
IrqMethod::LevelLow => "IRQ_TYPE_LEVEL_LOW",
}
}
fn hex(value: u32) -> String {
format!("0x{value:x}")
}
#[cfg(test)]
mod tests {
use super::*;
use consortium_dts::ToDts;
fn registers(mmap: Vec<(u32, u32)>, irq: Option<(u32, IrqMethod)>) -> UioDevice {
UioDevice {
mmap,
irq,
backing: UioBacking::Registers,
dma_coherent: Vec::new(),
interrupt_parent: None,
clock: None,
}
}
fn reserved(mmap: Vec<(u32, u32)>) -> UioDevice {
UioDevice {
mmap,
irq: None,
backing: UioBacking::ReservedMemory,
dma_coherent: Vec::new(),
interrupt_parent: None,
clock: None,
}
}
#[test]
fn renders_single_window_generic_uio() {
let device = registers(vec![(0x0049_0000, 0x400)], None);
let dts = device.to_node("ipcc1").to_dts();
assert!(dts.contains("ipcc1@490000 {"));
assert!(dts.contains("compatible = \"generic-uio\";"));
assert!(dts.contains("reg = <0x0 0x490000 0x0 0x400>;"));
assert!(dts.contains("status = \"okay\";"));
assert!(!dts.contains("interrupts"));
}
#[test]
fn aggregates_multiple_register_mappings() {
let device = registers(vec![(0x0040_0000, 0x8000), (0x0040_8000, 0x2000)], None);
let dts = device.to_node("regs").to_dts();
assert!(dts.contains("reg = <0x0 0x400000 0x0 0x8000 0x0 0x408000 0x0 0x2000>;"));
}
#[test]
fn dma_coherent_is_only_on_reserved_memory_node() {
let mut device = reserved(vec![(0x0040_0000, 0x8000), (0x0040_8000, 0x2000)]);
device.dma_coherent = vec![false, true];
let uio = device.to_node("ipc-shm").to_dts();
let reserved = device.reserved_memory_nodes("ipc-shm");
assert!(!uio.contains("dma-coherent"));
assert!(!reserved[0].to_dts().contains("dma-coherent"));
assert!(reserved[1].to_dts().contains("dma-coherent;"));
assert!(
reserved
.iter()
.all(|node| node.to_dts().contains("no-map;"))
);
}
#[test]
fn narrow_layout_uses_single_cells() {
let device = registers(vec![(0x0040_0000, 0x8000)], None);
let options = UioOptions {
layout: RegLayout::Narrow,
};
let dts = device.to_node_with_options("regs", &options).to_dts();
assert!(dts.contains("reg = <0x400000 0x8000>;"));
}
#[test]
fn renders_gic_spi_interrupt() {
let mut device = registers(
vec![(0x0049_0000, 0x400)],
Some((171, IrqMethod::LevelHigh)),
);
device.interrupt_parent = Some("intc".to_string());
let dts = device.to_node("ipcc1").to_dts();
assert!(dts.contains("interrupt-parent = <&intc>;"));
assert!(dts.contains("interrupts = <GIC_SPI 171 IRQ_TYPE_LEVEL_HIGH>;"));
}
#[test]
fn renders_doorbell_clock() {
let mut device = registers(
vec![(0x0049_0000, 0x400)],
Some((171, IrqMethod::LevelHigh)),
);
device.interrupt_parent = Some("intc".to_string());
device.clock = Some(UioClock {
provider: "scmi_clk".to_string(),
id: "CK_SCMI_IPCC1".to_string(),
});
let dts = device.to_node("ipcc1").to_dts();
assert!(dts.contains("clocks = <&scmi_clk CK_SCMI_IPCC1>;"));
}
#[test]
fn omits_unit_address_without_mappings() {
let device = registers(vec![], None);
let node = device.to_node("uio");
assert_eq!(node.unit_address, None);
}
#[test]
fn reserved_memory_device_references_carveout_not_reg() {
let device = reserved(vec![(0x2040_0000, 0x8000)]);
let dts = device.to_node("ipc-shm").to_dts();
assert!(dts.contains("ipc-shm@20400000 {"));
assert!(dts.contains("memory-region = <&ipc_shm>;"));
assert!(!dts.contains("reg ="));
let nodes = device.reserved_memory_nodes("ipc-shm");
assert_eq!(nodes.len(), 1);
let carveout = nodes[0].to_dts();
assert!(carveout.contains("ipc_shm: ipc-shm@20400000 {"));
assert!(carveout.contains("reg = <0x0 0x20400000 0x0 0x8000>;"));
assert!(carveout.contains("no-map;"));
}
#[test]
fn reserved_memory_multiple_windows_index_labels() {
let device = reserved(vec![(0x2040_0000, 0x8000), (0x2040_8000, 0x2000)]);
let dts = device.to_node("ipc-shm").to_dts();
assert!(dts.contains("memory-region = <&ipc_shm_0 &ipc_shm_1>;"));
let nodes = device.reserved_memory_nodes("ipc-shm");
assert_eq!(nodes.len(), 2);
assert!(nodes[0].to_dts().contains("ipc_shm_0: ipc-shm@20400000 {"));
assert!(nodes[1].to_dts().contains("ipc_shm_1: ipc-shm@20408000 {"));
}
#[test]
fn register_device_has_no_reserved_memory_nodes() {
let device = registers(vec![(0x0049_0000, 0x400)], None);
assert!(device.reserved_memory_nodes("ipcc1").is_empty());
}
#[test]
fn reserved_memory_container_wraps_children() {
let device = reserved(vec![(0x2040_0000, 0x8000)]);
let children = device.reserved_memory_nodes("ipc-shm");
let container = reserved_memory_container(children, RegLayout::Wide)
.expect("children present yields a container");
let dts = container.to_dts();
assert!(dts.contains("reserved-memory {"));
assert!(dts.contains("#address-cells = <2>;"));
assert!(dts.contains("#size-cells = <2>;"));
assert!(dts.contains("ranges;"));
assert!(dts.contains("ipc_shm: ipc-shm@20400000 {"));
assert!(reserved_memory_container(Vec::new(), RegLayout::Wide).is_none());
}
}