use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::{Arc, Weak};
use alloc::vec::Vec;
use crate::core::error::{Error, Result};
use crate::core::hosts::{HostKind, HostObjects};
use crate::core::space::{AddressSpace, RegionKind, RegionRef};
use crate::core::sync::{LockRank, Mutex};
use crate::core::wire::WireId;
use super::fdt::FdtWriter;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeKind {
Clint {
timebase_hz: u32,
},
Plic {
ndev: u32,
},
Syscon {
poweroff: u32,
reboot: u32,
},
Peripheral,
}
#[derive(Debug, Clone)]
pub struct NodeSpec {
pub kind: NodeKind,
pub name: &'static str,
pub compatible: &'static [&'static str],
pub cells: Vec<(&'static str, Vec<u32>)>,
pub strings: Vec<(&'static str, String)>,
pub irq_wire: Option<WireId>,
}
impl NodeSpec {
#[must_use]
pub fn peripheral(name: &'static str, compatible: &'static [&'static str]) -> NodeSpec {
NodeSpec {
kind: NodeKind::Peripheral,
name,
compatible,
cells: Vec::new(),
strings: Vec::new(),
irq_wire: None,
}
}
#[must_use]
pub fn with_cells(mut self, name: &'static str, cells: Vec<u32>) -> NodeSpec {
self.cells.push((name, cells));
self
}
}
pub trait DtSource: Send + Sync {
fn dt_spec(&self) -> NodeSpec;
fn dt_plic_source(&self, wire: WireId) -> Option<u32> {
let _ = wire;
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CpuSpec {
pub harts: u32,
pub isa: String,
pub mmu: String,
pub boot_hart: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConfig {
pub model: String,
pub bootargs: String,
pub initrd: Option<(u64, u64)>,
pub cpus: CpuSpec,
pub default_timebase_hz: u32,
}
struct Entry {
key: usize,
region: Weak<crate::core::space::Region>,
source: Weak<dyn DtSource>,
}
#[derive(Default)]
pub struct Publications {
entries: Mutex<Vec<Entry>>,
}
impl core::fmt::Debug for Publications {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.entries.try_lock() {
Some(entries) => f
.debug_struct("Publications")
.field("published", &entries.len())
.finish(),
None => f
.debug_struct("Publications")
.field("published", &"<in use>")
.finish(),
}
}
}
impl Publications {
#[must_use]
pub fn new() -> Publications {
Publications {
entries: Mutex::with_rank(LockRank::LEAF, Vec::new()),
}
}
pub fn publish(&self, region: &RegionRef, source: Weak<dyn DtSource>) {
let key = key_of(region);
let mut table = self.entries.lock();
table.retain(|e| e.region.strong_count() > 0 && e.key != key);
table.push(Entry {
key,
region: Arc::downgrade(region),
source,
});
}
#[must_use]
pub fn lookup(&self, region: &RegionRef) -> Option<Arc<dyn DtSource>> {
let key = key_of(region);
let table = self.entries.lock();
table
.iter()
.find(|e| e.key == key)
.and_then(|e| e.source.upgrade())
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.lock().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.lock().is_empty()
}
}
pub const KIND: HostKind = HostKind::new("riscv.dt");
const TABLE_NAME: &str = "dt";
pub fn table(hosts: &HostObjects) -> Result<Arc<Publications>> {
hosts.open(KIND, TABLE_NAME, Publications::new)
}
pub fn table_for(props: &crate::core::props::Props) -> Result<Arc<Publications>> {
props.host(KIND, TABLE_NAME, Publications::new)
}
pub fn publish(hosts: &HostObjects, region: &RegionRef, source: Weak<dyn DtSource>) -> Result<()> {
table(hosts)?.publish(region, source);
Ok(())
}
fn key_of(region: &RegionRef) -> usize {
Arc::as_ptr(region) as *const u8 as usize
}
#[derive(Debug)]
struct Placed {
base: u64,
size: u64,
spec: NodeSpec,
}
fn leaf_of(region: &RegionRef) -> RegionRef {
let mut here = Arc::clone(region);
for _ in 0..16 {
let Some(alias) = here.as_alias() else {
return here;
};
here = Arc::clone(alias.target());
}
here
}
fn survey(dt: &Publications, space: &AddressSpace) -> (Vec<Placed>, Vec<(u64, u64)>) {
let mut placed = Vec::new();
let mut memory = Vec::new();
let view = space.view();
for (_, mapping) in view.mappings() {
let leaf = leaf_of(&mapping.region);
let size = mapping.region.len();
if let Some(source) = dt.lookup(&leaf) {
placed.push(Placed {
base: mapping.base,
size,
spec: source.dt_spec(),
});
} else if matches!(leaf.kind(), RegionKind::Ram(_)) {
memory.push((mapping.base, size));
}
}
placed.sort_by_key(|p| p.base);
memory.sort_by_key(|m| m.0);
(placed, memory)
}
pub fn generate(dt: &Publications, space: &AddressSpace, cfg: &TreeConfig) -> Result<Vec<u8>> {
let (placed, memory) = survey(dt, space);
if memory.is_empty() {
return Err(Error::Config {
at: "device tree".to_string(),
message: format!(
"the address space `{}` has no RAM mapped, so the tree would have no \
`memory` node and nothing could be loaded",
space.name()
),
});
}
let mut next_phandle = 1u32;
let intc: Vec<u32> = (0..cfg.cpus.harts)
.map(|_| {
let p = next_phandle;
next_phandle += 1;
p
})
.collect();
let plic_phandle = placed
.iter()
.find(|p| matches!(p.spec.kind, NodeKind::Plic { .. }))
.map(|_| {
let p = next_phandle;
next_phandle += 1;
p
});
let syscon_phandle = placed
.iter()
.find(|p| matches!(p.spec.kind, NodeKind::Syscon { .. }))
.map(|_| {
let p = next_phandle;
next_phandle += 1;
p
});
let plic_pins: Option<Arc<dyn DtSource>> = {
let view = space.view();
let mut found = None;
for (_, mapping) in view.mappings() {
let leaf = leaf_of(&mapping.region);
if let Some(source) = dt.lookup(&leaf)
&& matches!(source.dt_spec().kind, NodeKind::Plic { .. })
{
found = Some(source);
break;
}
}
found
};
let irq_of = |spec: &NodeSpec| -> Option<u32> {
let wire = spec.irq_wire?;
plic_pins.as_ref()?.dt_plic_source(wire)
};
let timebase = placed
.iter()
.find_map(|p| match p.spec.kind {
NodeKind::Clint { timebase_hz } => Some(timebase_hz),
_ => None,
})
.unwrap_or(cfg.default_timebase_hz);
let stdout = placed
.iter()
.find(|p| p.spec.name == "serial")
.map(|p| format!("/soc/serial@{:x}", p.base));
let mut w = FdtWriter::new();
w.set_boot_cpu(cfg.cpus.boot_hart);
w.begin_node("");
w.prop_u32("#address-cells", 2);
w.prop_u32("#size-cells", 2);
w.prop_str_list("compatible", &["riscv-virtio"]);
w.prop_str("model", &cfg.model);
w.begin_node("chosen");
if !cfg.bootargs.is_empty() {
w.prop_str("bootargs", &cfg.bootargs);
}
if let Some(path) = &stdout {
w.prop_str("stdout-path", path);
}
if let Some((start, end)) = cfg.initrd {
w.prop_u64("linux,initrd-start", start);
w.prop_u64("linux,initrd-end", end);
}
w.end_node()?;
w.begin_node("cpus");
w.prop_u32("#address-cells", 1);
w.prop_u32("#size-cells", 0);
w.prop_u32("timebase-frequency", timebase);
for hart in 0..cfg.cpus.harts {
w.begin_node(&format!("cpu@{hart}"));
w.prop_str("device_type", "cpu");
w.prop_u32("reg", hart);
w.prop_str("status", "okay");
w.prop_str_list("compatible", &["riscv"]);
w.prop_str("riscv,isa", &cfg.cpus.isa);
w.prop_str("riscv,isa-base", base_isa(&cfg.cpus.isa));
let mut extensions = Vec::new();
for name in isa_extensions(&cfg.cpus.isa) {
extensions.extend_from_slice(name.as_bytes());
extensions.push(0);
}
w.prop_bytes("riscv,isa-extensions", &extensions);
if !cfg.cpus.mmu.is_empty() {
w.prop_str("mmu-type", &format!("riscv,{}", cfg.cpus.mmu));
}
w.begin_node("interrupt-controller");
w.prop_u32("#interrupt-cells", 1);
w.prop_empty("interrupt-controller");
w.prop_str_list("compatible", &["riscv,cpu-intc"]);
w.prop_u32("phandle", intc[hart as usize]);
w.end_node()?;
w.end_node()?;
}
w.end_node()?;
for (base, size) in &memory {
w.begin_node(&format!("memory@{base:x}"));
w.prop_str("device_type", "memory");
w.prop_reg64(&[(*base, *size)]);
w.end_node()?;
}
w.begin_node("soc");
w.prop_u32("#address-cells", 2);
w.prop_u32("#size-cells", 2);
w.prop_str_list("compatible", &["simple-bus"]);
w.prop_empty("ranges");
for item in &placed {
w.begin_node(&format!("{}@{:x}", item.spec.name, item.base));
w.prop_str_list("compatible", item.spec.compatible);
w.prop_reg64(&[(item.base, item.size)]);
match item.spec.kind {
NodeKind::Clint { .. } => {
let mut cells = Vec::with_capacity(cfg.cpus.harts as usize * 4);
for phandle in &intc {
cells.extend_from_slice(&[*phandle, 3, *phandle, 7]);
}
w.prop_cells("interrupts-extended", &cells);
}
NodeKind::Plic { ndev } => {
let mut cells = Vec::with_capacity(cfg.cpus.harts as usize * 4);
for phandle in &intc {
cells.extend_from_slice(&[*phandle, 11, *phandle, 9]);
}
w.prop_cells("interrupts-extended", &cells);
w.prop_empty("interrupt-controller");
w.prop_u32("#interrupt-cells", 1);
w.prop_u32("#address-cells", 0);
w.prop_u32("riscv,ndev", ndev);
if let Some(p) = plic_phandle {
w.prop_u32("phandle", p);
}
}
NodeKind::Syscon { .. } => {
if let Some(p) = syscon_phandle {
w.prop_u32("phandle", p);
}
}
NodeKind::Peripheral => {}
}
if let (Some(irq), Some(parent)) = (irq_of(&item.spec), plic_phandle) {
w.prop_u32("interrupt-parent", parent);
w.prop_u32("interrupts", irq);
}
for (name, cells) in &item.spec.cells {
w.prop_cells(name, cells);
}
for (name, value) in &item.spec.strings {
w.prop_str(name, value);
}
w.end_node()?;
}
w.end_node()?;
if let (Some(phandle), Some(syscon)) = (
syscon_phandle,
placed.iter().find_map(|p| match p.spec.kind {
NodeKind::Syscon { poweroff, reboot } => Some((poweroff, reboot)),
_ => None,
}),
) {
let (poweroff, reboot) = syscon;
for (node, value, compatible) in [
("poweroff", poweroff, "syscon-poweroff"),
("reboot", reboot, "syscon-reboot"),
] {
w.begin_node(node);
w.prop_u32("value", value);
w.prop_u32("offset", 0);
w.prop_u32("regmap", phandle);
w.prop_str_list("compatible", &[compatible]);
w.end_node()?;
}
}
w.end_node()?;
w.finish()
}
fn base_isa(isa: &str) -> &'static str {
if isa.starts_with("rv32") {
"rv32i"
} else {
"rv64i"
}
}
fn isa_extensions(isa: &str) -> Vec<String> {
let letters = isa
.strip_prefix("rv64")
.or_else(|| isa.strip_prefix("rv32"))
.unwrap_or(isa);
let mut out: Vec<String> = Vec::new();
for letter in letters.chars().filter(char::is_ascii_alphabetic) {
let name = letter.to_ascii_lowercase().to_string();
if !out.contains(&name) {
out.push(name);
}
}
for always in ["zicsr", "zifencei", "zicntr"] {
out.push(always.to_string());
}
out
}
pub fn describe(dtb: &[u8]) -> Result<String> {
let word = |at: usize| -> Result<u32> {
dtb.get(at..at + 4)
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
.ok_or_else(|| Error::State("device tree is truncated".to_string()))
};
if word(0)? != super::fdt::FDT_MAGIC {
return Err(Error::State("not a flattened device tree".to_string()));
}
let off_struct = word(8)? as usize;
let len_struct = word(36)? as usize;
let off_strings = word(12)? as usize;
let name_at = |at: usize| -> String {
let end = dtb[at..].iter().position(|b| *b == 0).unwrap_or(0) + at;
String::from_utf8_lossy(&dtb[at..end]).into_owned()
};
let mut out = String::new();
let mut at = off_struct;
let end = off_struct + len_struct;
let mut depth = 0usize;
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
while at + 4 <= end {
let token = word(at)?;
at += 4;
match token {
1 => {
let name = name_at(at);
at += name.len() + 1;
at = at.next_multiple_of(4);
for _ in 0..depth {
out.push_str(" ");
}
out.push_str(if name.is_empty() { "/" } else { &name });
out.push_str(" {\n");
depth += 1;
*counts.entry(name).or_default() += 1;
}
2 => {
depth = depth.saturating_sub(1);
for _ in 0..depth {
out.push_str(" ");
}
out.push_str("};\n");
}
3 => {
let len = word(at)? as usize;
let name_off = word(at + 4)? as usize;
at += 8;
let name = name_at(off_strings + name_off);
for _ in 0..=depth {
out.push_str(" ");
}
out.push_str(&format!("{name} [{len}]\n"));
at += len;
at = at.next_multiple_of(4);
}
4 => {}
9 => break,
other => {
return Err(Error::State(format!("unknown device tree token {other}")));
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::{MemOps, RamStore, Region};
#[derive(Debug)]
struct Fake;
impl DtSource for Fake {
fn dt_spec(&self) -> NodeSpec {
NodeSpec::peripheral("fake", &["rsemu,fake"])
}
}
impl MemOps for Fake {
fn read(
&self,
_offset: u64,
_dst: &mut [u8],
_attrs: crate::core::space::MemAttrs,
) -> crate::core::space::MemResult {
Ok(())
}
fn write(
&self,
_offset: u64,
_src: &[u8],
_attrs: crate::core::space::MemAttrs,
) -> crate::core::space::MemResult {
Ok(())
}
fn constraints(&self) -> crate::core::space::AccessConstraints {
crate::core::space::AccessConstraints::IO
}
}
fn published(dt: &Publications) -> (RegionRef, Arc<Fake>) {
let ops = Arc::new(Fake);
let region: RegionRef = Arc::new(Region::io(
"fake",
0x100,
Arc::clone(&ops) as Arc<dyn MemOps>,
));
dt.publish(®ion, Arc::downgrade(&ops) as Weak<dyn DtSource>);
(region, ops)
}
#[test]
fn a_published_region_finds_its_describer_again() {
let dt = Publications::new();
let (region, _ops) = published(&dt);
let found = dt.lookup(®ion).expect("published");
assert_eq!(found.dt_spec().name, "fake");
let other: RegionRef = Arc::new(Region::ram("ram", Arc::new(RamStore::new(0x100))));
assert!(dt.lookup(&other).is_none());
assert!(Publications::new().lookup(®ion).is_none());
}
#[test]
fn an_entry_dies_with_its_device() {
let dt = Publications::new();
let key = {
let (region, _ops) = published(&dt);
let key = key_of(®ion);
assert!(dt.lookup(®ion).is_some());
key
};
let (_region, _ops) = published(&dt);
assert!(
!dt.entries.lock().iter().any(|e| e.key == key),
"a dropped device leaves nothing behind"
);
assert_eq!(dt.len(), 1, "and only the live publication is kept");
}
#[test]
fn a_builds_table_is_opened_once_and_shared() {
let hosts = crate::core::HostObjects::new();
let a = table(&hosts).expect("a fresh table");
let b = table(&hosts).expect("the same one");
assert!(Arc::ptr_eq(&a, &b));
assert!(a.is_empty());
let elsewhere = crate::core::HostObjects::new();
assert!(!Arc::ptr_eq(
&a,
&table(&elsewhere).expect("another build's")
));
}
#[test]
fn a_space_with_no_ram_says_so_rather_than_emitting_a_useless_tree() {
let space = AddressSpace::new("mem", 64);
let cfg = TreeConfig {
model: "test".to_string(),
bootargs: String::new(),
initrd: None,
cpus: CpuSpec {
harts: 1,
isa: "rv64imac".to_string(),
mmu: "sv39".to_string(),
boot_hart: 0,
},
default_timebase_hz: 10_000_000,
};
let e = generate(&Publications::new(), &space, &cfg)
.expect_err("no memory")
.to_string();
assert!(e.contains("no RAM"), "{e}");
}
}