use consortium_cfg_common::config::DtsMode;
use consortium_data::dts::{local_include_closure, resolve_base};
use consortium_dts::utils::edit;
use consortium_dts::{
Cell, Deletion, File, Node, NodeOverlay, Property, Reference, ToDts, parse_dts,
};
use crate::bridge::LoweredIr;
use crate::components::uio::{RegLayout, UioBacking, reserved_memory_container};
use crate::error::CfgError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedDts {
pub file_name: String,
pub contents: String,
pub warnings: Vec<String>,
}
pub fn render_linux_dts(
chip: &str,
devkit: Option<&str>,
base_override: Option<&str>,
mode: DtsMode,
ir: &LoweredIr,
target_cores: &[String],
) -> Result<Option<RenderedDts>, CfgError> {
if ir.uio.is_empty() {
consortium_log::trace!("no UIO devices in IR; skipping Linux DTS render");
return Ok(None);
}
consortium_log::trace!("resolving base device tree for chip '{}'", chip);
let base = resolve_base(chip, devkit, base_override)?;
consortium_log::trace!("collecting local include closure for '{}'", base.entry);
let sources = local_include_closure(&base)?;
let mut files = Vec::with_capacity(sources.len());
for (name, contents) in &sources {
consortium_log::trace!("parsing device-tree source '{}'", name);
let file = parse_dts(contents).map_err(|e| {
consortium_log::trace!("device-tree source '{}' failed to parse: {}", name, e);
CfgError::DtsParse {
file: name.clone(),
detail: e.to_string(),
}
})?;
files.push(file);
}
let base_file = files.first().cloned().unwrap_or_default();
let analysis = analyze(&files, ir, target_cores);
let rendered = if mode.is_overlay() {
let (contents, warnings) = render_overlay(&analysis, ir);
RenderedDts {
file_name: overlay_file_name(&base),
contents,
warnings,
}
} else {
RenderedDts {
file_name: merge_file_name(&base),
contents: render_merge(&base_file, &analysis, ir),
warnings: Vec::new(),
}
};
Ok(Some(rendered))
}
fn merge_file_name(base: &consortium_data::dts::BaseDts) -> String {
format!("consortium-{}.dts", stem(&base.entry))
}
fn overlay_file_name(base: &consortium_data::dts::BaseDts) -> String {
format!("consortium-{}.dtso", stem(&base.entry))
}
fn stem(entry: &str) -> &str {
entry
.strip_suffix(".dtsi")
.or_else(|| entry.strip_suffix(".dts"))
.unwrap_or(entry)
}
const RPROC_MARKERS: &[&str] = &["-cm33", "-cm7", "-cm4", "cortex-m"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RprocFix {
pub label: String,
pub delete_mboxes: bool,
pub delete_mbox_names: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Analysis {
pub conflicts: Vec<String>,
pub rprocs: Vec<RprocFix>,
}
pub fn analyze(files: &[File], ir: &LoweredIr, target_cores: &[String]) -> Analysis {
let mut conflicts = Vec::new();
for name in doorbell_labels(ir) {
if !conflicts.contains(&name) && files.iter().any(|f| edit::label_exists(f, &name)) {
conflicts.push(name);
}
}
let mut rprocs: Vec<RprocFix> = Vec::new();
for file in files {
edit::for_each_node(file, |node| {
let Some(label) = node.label.as_deref() else {
return;
};
let is_rproc = RPROC_MARKERS.iter().any(|m| node.compatible_contains(m));
let targets_our_core = target_cores
.iter()
.any(|core| label.contains(core.as_str()) || node.name.contains(core.as_str()));
if is_rproc && targets_our_core && !rprocs.iter().any(|r| r.label == label) {
rprocs.push(RprocFix {
label: label.to_string(),
delete_mboxes: label_has_property(files, label, "mboxes"),
delete_mbox_names: label_has_property(files, label, "mbox-names"),
});
}
});
}
Analysis { conflicts, rprocs }
}
pub fn render_merge(base: &File, analysis: &Analysis, ir: &LoweredIr) -> String {
let mut file = base.clone();
file.version = true;
file.plugin = false;
if let Some(root) = ir_root_node(ir) {
file.items.push(root.into());
}
for label in &analysis.conflicts {
file.items.push(
NodeOverlay::label(label)
.property(Property::string("status", "disabled"))
.into(),
);
}
for fix in &analysis.rprocs {
file.items.push(rproc_override(fix).into());
}
file.to_dts()
}
pub fn render_overlay(analysis: &Analysis, ir: &LoweredIr) -> (String, Vec<String>) {
let mut root = Node::root();
let mut fragment = 0u64;
if let Some(payload) = ir_root_node(ir) {
let overlay = Node::new("__overlay__").with_children(payload.children);
root = root.child(
Node::with_unit_address("fragment", fragment.to_string())
.property(Property::string("target-path", "/"))
.child(overlay),
);
fragment += 1;
}
for label in &analysis.conflicts {
let overlay = Node::new("__overlay__").property(Property::string("status", "disabled"));
root = root.child(
Node::with_unit_address("fragment", fragment.to_string())
.property(Property::cells(
"target",
[Cell::reference(Reference::label(label.clone()))],
))
.child(overlay),
);
fragment += 1;
}
let warnings = analysis
.rprocs
.iter()
.filter(|r| r.delete_mboxes || r.delete_mbox_names)
.map(|r| {
format!(
"remoteproc node `&{}` needs its `mboxes`/`mbox-names` removed, but an overlay \
cannot delete properties; use `[dts] mode = \"merge\"` so remoteproc loads",
r.label
)
})
.collect();
(File::dtso().item(root).to_dts(), warnings)
}
fn rproc_override(fix: &RprocFix) -> NodeOverlay {
let mut overlay = NodeOverlay::label(fix.label.clone());
if fix.delete_mboxes {
overlay = overlay.deletion(Deletion::Property("mboxes".to_string()));
}
if fix.delete_mbox_names {
overlay = overlay.deletion(Deletion::Property("mbox-names".to_string()));
}
overlay.status_okay()
}
fn ir_root_node(ir: &LoweredIr) -> Option<Node> {
if ir.uio.is_empty() {
return None;
}
let mut root = Node::root();
let carveouts: Vec<Node> = ir
.uio
.iter()
.flat_map(|(name, device)| device.reserved_memory_nodes(name.as_str()))
.collect();
if let Some(container) = reserved_memory_container(carveouts, RegLayout::Wide) {
root = root.child(container);
}
for (name, device) in &ir.uio {
root = root.child(device.to_node(name.as_str()));
}
Some(root)
}
fn doorbell_labels(ir: &LoweredIr) -> Vec<String> {
ir.uio
.iter()
.filter(|(_, device)| device.backing == UioBacking::Registers)
.map(|(name, _)| name.clone())
.collect()
}
trait WithChildren {
fn with_children(self, children: impl IntoIterator<Item = Node>) -> Self;
}
impl WithChildren for Node {
fn with_children(mut self, children: impl IntoIterator<Item = Node>) -> Self {
for child in children {
self = self.child(child);
}
self
}
}
fn label_has_property(files: &[File], label: &str, property: &str) -> bool {
files.iter().any(|file| {
edit::find_by_label(file, label).is_some_and(|n| n.has_property(property))
|| edit::overlays(file)
.any(|o| o.target_label() == Some(label) && o.has_property(property))
})
}
#[cfg(test)]
mod tests {
use super::*;
use consortium_dts::parse_dts;
use crate::components::uio::{UioBacking, UioDevice};
fn ir_with(uio: Vec<(String, UioDevice)>) -> LoweredIr {
LoweredIr::empty_with_uio(uio)
}
fn shm(name: &str, base: u32, len: u32) -> (String, UioDevice) {
(
name.to_string(),
UioDevice {
mmap: vec![(base, len)],
irq: None,
backing: UioBacking::ReservedMemory,
dma_coherent: Vec::new(),
interrupt_parent: None,
clock: None,
},
)
}
fn doorbell(name: &str, base: u32) -> (String, UioDevice) {
(
name.to_string(),
UioDevice {
mmap: vec![(base, 0x1000)],
irq: None,
backing: UioBacking::Registers,
dma_coherent: Vec::new(),
interrupt_parent: None,
clock: None,
},
)
}
const BASE: &str = r#"/dts-v1/;
#include "soc.dtsi"
/ {
model = "Board";
soc {
mu7: mailbox@42430000 {
compatible = "fsl,imx95-mu-v3.0";
status = "okay";
};
cm33: remoteproc-cm33 {
compatible = "fsl,imx93-cm33";
status = "disabled";
};
};
};
&cm33 {
mboxes = <&mu1 0 1>;
mbox-names = "tx", "rx";
status = "okay";
};
"#;
#[test]
fn analysis_finds_conflict_and_rproc() {
let base = parse_dts(BASE).unwrap();
let ir = ir_with(vec![
shm("ipc-shm", 0x2040_0000, 0x8000),
doorbell("mu7", 0x4243_0000),
]);
let analysis = analyze(std::slice::from_ref(&base), &ir, &["cm33".to_string()]);
assert_eq!(analysis.conflicts, vec!["mu7".to_string()]);
assert_eq!(analysis.rprocs.len(), 1);
let fix = &analysis.rprocs[0];
assert_eq!(fix.label, "cm33");
assert!(fix.delete_mboxes);
assert!(fix.delete_mbox_names);
}
#[test]
fn merge_appends_overrides_and_uio() {
let base = parse_dts(BASE).unwrap();
let ir = ir_with(vec![
shm("ipc-shm", 0x2040_0000, 0x8000),
doorbell("mu7", 0x4243_0000),
]);
let analysis = analyze(std::slice::from_ref(&base), &ir, &["cm33".to_string()]);
let dts = render_merge(&base, &analysis, &ir);
assert!(dts.contains("model = \"Board\""));
assert!(dts.contains("#include \"soc.dtsi\""));
assert!(dts.contains("compatible = \"generic-uio\""));
assert!(dts.contains("reserved-memory {"));
assert!(dts.contains("&mu7 {"));
assert!(dts.contains("status = \"disabled\""));
assert!(dts.contains("&cm33 {"));
assert!(dts.contains("/delete-property/ mboxes;"));
assert!(dts.contains("/delete-property/ mbox-names;"));
}
#[test]
fn overlay_uses_fragment_form_and_warns_on_rproc() {
let base = parse_dts(BASE).unwrap();
let ir = ir_with(vec![
shm("ipc-shm", 0x2040_0000, 0x8000),
doorbell("mu7", 0x4243_0000),
]);
let analysis = analyze(std::slice::from_ref(&base), &ir, &["cm33".to_string()]);
let (dtso, warnings) = render_overlay(&analysis, &ir);
assert!(dtso.contains("/plugin/;"));
assert!(dtso.contains("fragment@0 {"));
assert!(dtso.contains("target-path = \"/\""));
assert!(dtso.contains("__overlay__ {"));
assert!(dtso.contains("fragment@1 {"));
assert!(dtso.contains("target = <&mu7>"));
assert!(dtso.contains("status = \"disabled\""));
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("&cm33"));
}
#[test]
fn merge_on_real_imx95_devkit_disables_predeclared_mailbox() {
let ir = ir_with(vec![
shm("ipc-shm", 0x2040_0000, 0x8000),
doorbell("mu7", 0x4243_0000),
]);
let rendered = render_linux_dts(
"imx95",
Some("imx95-19x19-evk"),
None,
DtsMode::Merge,
&ir,
&["cm7".to_string()],
)
.expect("render succeeds")
.expect("uio present");
assert_eq!(rendered.file_name, "consortium-imx95-19x19-evk.dts");
assert!(rendered.contents.contains("compatible = \"generic-uio\""));
assert!(rendered.contents.contains("&mu7 {"));
assert!(rendered.contents.contains("#include"));
assert!(rendered.warnings.is_empty());
}
#[test]
fn overlay_on_real_stm32_devkit_emits_fragments() {
let ir = ir_with(vec![shm("ipc-shm", 0x0a02_0000, 0x8000)]);
let rendered = render_linux_dts(
"stm32mp257fak3",
Some("stm32mp257f-dk"),
None,
DtsMode::Overlay,
&ir,
&["cm33".to_string()],
)
.expect("render succeeds")
.expect("uio present");
assert_eq!(rendered.file_name, "consortium-stm32mp257f-dk.dtso");
assert!(rendered.contents.contains("/plugin/;"));
assert!(rendered.contents.contains("fragment@0"));
assert!(rendered.contents.contains("target-path = \"/\""));
}
#[test]
fn unused_coprocessor_is_not_touched() {
let base = parse_dts(BASE).unwrap();
let ir = ir_with(vec![shm("ipc-shm", 0x2040_0000, 0x8000)]);
let analysis = analyze(std::slice::from_ref(&base), &ir, &["cm7".to_string()]);
assert!(analysis.rprocs.is_empty());
}
}