From 1caf2709111c41c14b8cdc153ddc51f7d50702a3 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Mon, 16 Mar 2026 20:43:56 +0100 Subject: [PATCH 1/4] [Feature] add dtgen device tree compile-time code generator --- Cargo.lock | 14 + xtasks/crates/dtgen/Cargo.lock | 193 ++++++++++++++ xtasks/crates/dtgen/Cargo.toml | 16 ++ xtasks/crates/dtgen/README.md | 282 ++++++++++++++++++++ xtasks/crates/dtgen/src/codegen.rs | 407 +++++++++++++++++++++++++++++ xtasks/crates/dtgen/src/ir.rs | 145 ++++++++++ xtasks/crates/dtgen/src/lib.rs | 13 + xtasks/crates/dtgen/src/main.rs | 31 +++ xtasks/crates/dtgen/src/parser.rs | 232 ++++++++++++++++ 9 files changed, 1333 insertions(+) create mode 100644 xtasks/crates/dtgen/Cargo.lock create mode 100644 xtasks/crates/dtgen/Cargo.toml create mode 100644 xtasks/crates/dtgen/README.md create mode 100644 xtasks/crates/dtgen/src/codegen.rs create mode 100644 xtasks/crates/dtgen/src/ir.rs create mode 100644 xtasks/crates/dtgen/src/lib.rs create mode 100644 xtasks/crates/dtgen/src/main.rs create mode 100644 xtasks/crates/dtgen/src/parser.rs diff --git a/Cargo.lock b/Cargo.lock index bdc12ce..3ca1696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,6 +523,14 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dtgen" +version = "0.1.0" +dependencies = [ + "clap", + "fdt", +] + [[package]] name = "dtor" version = "0.1.1" @@ -613,6 +621,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784a4df722dc6267a04af36895398f59d21d07dce47232adf31ec0ff2fa45e67" + [[package]] name = "find-msvc-tools" version = "0.1.7" diff --git a/xtasks/crates/dtgen/Cargo.lock b/xtasks/crates/dtgen/Cargo.lock new file mode 100644 index 0000000..337b987 --- /dev/null +++ b/xtasks/crates/dtgen/Cargo.lock @@ -0,0 +1,193 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "dtgen" +version = "0.1.0" +dependencies = [ + "clap", + "fdt", +] + +[[package]] +name = "fdt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784a4df722dc6267a04af36895398f59d21d07dce47232adf31ec0ff2fa45e67" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/xtasks/crates/dtgen/Cargo.toml b/xtasks/crates/dtgen/Cargo.toml new file mode 100644 index 0000000..184d21c --- /dev/null +++ b/xtasks/crates/dtgen/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dtgen" +version = "0.1.0" +edition = "2021" + +[lib] +name = "dtgen" +path = "src/lib.rs" + +[[bin]] +name = "dtgen" +path = "src/main.rs" + +[dependencies] +fdt = "0.1.5" +clap = { version = "4", features = ["derive"] } \ No newline at end of file diff --git a/xtasks/crates/dtgen/README.md b/xtasks/crates/dtgen/README.md new file mode 100644 index 0000000..39104f5 --- /dev/null +++ b/xtasks/crates/dtgen/README.md @@ -0,0 +1,282 @@ +# dtgen + +`dtgen` parses a Device Tree Source (`.dts`) file and emits a `dt.rs` file containing a complete static representation of the device tree. This file is included via `include!` and provides a query API usable both at compile time (proc macros) and at runtime. + +--- + +## Including the generated file + +In your crate: + +```rust +include!(concat!(env!("OUT_DIR"), "/dt.rs")); +``` + +Or if dtgen was invoked with a custom output path: + +```rust +include!("path/to/dt.rs"); +``` + +--- + +## Types + +### `PropValue` + +Represents a raw DTS property value. + +```rust +pub enum PropValue { + Empty, // boolean flag property, e.g. gpio-controller + U32(u32), // single cell, e.g. current-speed = <115200> + U32Array(&'static [u32]), // cell array, e.g. clocks = <&rcc 1 0x4000> + Str(&'static str), // string, e.g. status = "okay" + StringList(&'static [&'static str]), // string list, e.g. compatible = "a", "b" + Bytes(&'static [u8]), // raw byte array +} +``` + +### `Peripheral` + +Every node with at least one `compatible` string is emitted as a `Peripheral`. + +```rust +pub struct Peripheral { + pub node: usize, // index into NODES[] + pub compatible: &'static [&'static str], // all compatible strings + pub reg: Option<(usize, usize)>, // (base_addr, size) + pub interrupts: &'static [u32], // interrupt numbers + pub phandle: Option, // phandle value if present + pub props: &'static [(&'static str, PropValue)], // all extra properties +} +``` + +### `TreeNode` + +Topology-only node - every node in the tree including structural ones. + +```rust +pub struct TreeNode { + pub name: &'static str, + pub phandle: Option, + pub parent: Option, + pub children: &'static [usize], +} +``` + +--- + +## Static arrays + +```rust +NODES: &[TreeNode] // every node in the tree, depth-first order +PERIPHERALS: &[Peripheral] // every node that has a compatible string +MODEL: &str // /model property or first root compatible +STDOUT: Option<&str> // first compatible of the /chosen stdout-path target +``` + +--- + +## Peripheral methods + +### Compatible matching + +```rust +// exact match against any compatible string +p.is_compatible("st,stm32-uart") + +// substring match - useful for class-level matching +p.compatible_contains("uart") +``` + +### Property access + +```rust +// raw PropValue +p.prop("current-speed") // Option + +// typed convenience accessors +p.prop_u32("current-speed") // Option +p.prop_str("status") // Option<&'static str> +p.prop_u32_array("clocks") // Option<&'static [u32]> +``` + +### Register / interrupt access + +```rust +p.reg_base() // Option base address +p.reg_size() // Option mapped size +p.interrupts // &[u32] all interrupt numbers +``` + +### Phandle resolution + +Phandle arrays are stored as raw `U32Array` props. +The first element of each phandle entry is the phandle value of the provider node. + +```rust +// resolve a phandle to its Peripheral +if let Some(PropValue::U32Array(cells)) = p.prop("clocks") { + let clock_phandle = cells[0]; + if let Some(clock) = p.resolve_phandle(clock_phandle) { + let freq = clock.prop_u32("clock-frequency"); + } +} +``` + +### Status / enabled + +```rust +// returns true if status is absent or "okay" +// returns false if status = "disabled" +p.is_enabled() +``` + +### Tree navigation + +```rust +p.tree_node() // &'static TreeNode +p.tree_node().parent_node() // Option<&'static TreeNode> +p.tree_node().iter_children() // impl Iterator +``` + +--- + +## Free query functions + +### By compatible string + +```rust +// first enabled match +peripheral_by_compatible("st,stm32-uart") // Option<&'static Peripheral> + +// all enabled matches - e.g. multiple UARTs +peripherals_by_compatible("st,stm32-uart") // impl Iterator +``` + +### By phandle + + +```rust +peripheral_by_phandle(1) // Option<&'static Peripheral> +``` + +### By node index + +```rust +peripheral_by_node(7) // Option<&'static Peripheral> +``` + +### By name + +Matches with or without unit address suffix. + +```rust +peripheral_by_name("serial") // matches "serial@40013800" - note this then works via first founds +peripheral_by_name("serial@40013800") // exact match also works +``` + +--- + +## `chosen` submodule + +```rust +// resolves /chosen stdout-path to the target Peripheral +chosen::stdout_path() // Option<&'static Peripheral> +``` + +--- + +## Common query patterns + +### Find the console UART + +```rust +let console = chosen::stdout_path() + .expect("no stdout-path in /chosen"); + +let base = console.reg_base().expect("console has no reg"); +let baud = console.prop_u32("current-speed").unwrap_or(115200); +``` + +### Find all enabled UARTs + +```rust +for uart in peripherals_by_compatible("st,stm32-uart") { + let base = uart.reg_base().unwrap(); + let irq = uart.interrupts.first().copied(); +} +``` + +### Resolve a clock dependency + +```rust +let uart = peripheral_by_compatible("st,stm32-uart").unwrap(); + +if let Some(PropValue::U32Array(cells)) = uart.prop("clocks") { + // cells = [phandle, ...clock specifier cells...] + let phandle = cells[0]; + let rcc = peripheral_by_phandle(phandle).expect("clock provider not found"); + let freq = rcc.prop_u32("clock-frequency").unwrap_or(0); +} +``` + +### Find a GPIO controller by phandle + +```rust +// DTS: led-gpios = <&gpioa 5 0> +// emitted as: PropValue::U32Array(&[gpioa_phandle, 5, 0]) + +if let Some(PropValue::U32Array(cells)) = node.prop("led-gpios") { + let gpio = peripheral_by_phandle(cells[0]).unwrap(); + let pin = cells[1]; + let flags = cells[2]; +} +``` + +### Walk children of a node + +```rust +// find all child nodes of the "leds" node +if let Some(leds) = peripheral_by_name("leds") { + for (child_idx, child_node) in leds.tree_node().iter_children() { + if let Some(child_periph) = peripheral_by_node(child_idx) { + // process each LED child peripheral + } + } +} +``` + +### Filter by compatible then check a prop + +```rust +// find an SPI controller with a specific bus frequency +let spi = peripherals_by_compatible("st,stm32-spi") + .find(|p| p.prop_u32("clock-frequency") == Some(1_000_000)); +``` + +--- + +## CLI invocation + +``` +dtgen [-I ...] +``` + +```bash +dtgen board.dts src/dt.rs +dtgen board.dts out/dt.rs -I vendor/stm32/include -I vendor/cmsis/include +``` + +## `build.rs` integration + +```rust +fn main() { + let dts = std::path::Path::new("board.dts"); + let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()) + .join("dt.rs"); + dtgen::run(dts, &[], &out).expect("dtgen failed"); + println!("cargo:rerun-if-changed=board.dts"); +} +``` diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs new file mode 100644 index 0000000..ac17348 --- /dev/null +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -0,0 +1,407 @@ +use crate::ir::{DeviceTree, PropValue}; + +pub fn generate_rust(dt: &DeviceTree) -> String { + let mut out = String::new(); + emit_header(&mut out); + emit_prop_value_type(&mut out); + emit_topology_type(&mut out); + emit_peripheral_type(&mut out); + emit_nodes(&mut out, dt); + emit_peripherals(&mut out, dt); + emit_board_identity(&mut out, dt); + emit_query_api(&mut out, dt); + out +} + +// ================================================================================================ +// File header +// ================================================================================================ + +fn emit_header(out: &mut String) { + out.push_str( + r#"// ================================ +// GENERATED BY dtgen — DO NOT EDIT +// ================================ + +#![allow(dead_code)] + +"#, + ); +} + +// ================================================================================================ +// Type defitions +// ================================================================================================ + +fn emit_prop_value_type(out: &mut String) { + out.push_str( + r#"#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PropValue { + Empty, + U32(u32), + U32Array(&'static [u32]), + Str(&'static str), + StringList(&'static [&'static str]), + Bytes(&'static [u8]), +} + +"#, + ); +} + +fn emit_topology_type(out: &mut String) { + out.push_str( + r#"#[derive(Debug, Clone, Copy)] +pub struct TreeNode { + pub name: &'static str, + pub phandle: Option, + pub parent: Option, + pub children: &'static [usize], +} + +impl TreeNode { + /// Returns the parent TreeNode, if any. + pub fn parent_node(&self) -> Option<&'static TreeNode> { + self.parent.map(|idx| &NODES[idx]) + } + + /// Iterate children as (node_index, &TreeNode). + pub fn iter_children(&self) -> impl Iterator { + self.children.iter().map(|&idx| (idx, &NODES[idx])) + } +} + +"#, + ); +} + +fn emit_peripheral_type(out: &mut String) { + out.push_str( + r#"#[derive(Debug, Clone, Copy)] +pub struct Peripheral { + pub node: usize, + pub compatible: &'static [&'static str], + pub reg: Option<(usize, usize)>, + pub interrupts: &'static [u32], + pub phandle: Option, + pub props: &'static [(&'static str, PropValue)], +} + +impl Peripheral { + /// Returns true if any compatible string exactly matches `c`. + pub fn is_compatible(&self, c: &str) -> bool { + self.compatible.iter().any(|&s| s == c) + } + + /// Returns true if any compatible string contains `fragment` as a substring. + pub fn compatible_contains(&self, fragment: &str) -> bool { + self.compatible.iter().any(|&s| s.contains(fragment)) + } + + /// Look up a prop by key, returning the raw PropValue. + pub fn prop(&self, key: &str) -> Option { + self.props.iter().find(|(k, _)| *k == key).map(|(_, v)| *v) + } + + /// Convenience: get a u32 prop. + pub fn prop_u32(&self, key: &str) -> Option { + match self.prop(key) { + Some(PropValue::U32(v)) => Some(v), + _ => None, + } + } + + /// Convenience: get a str prop. + pub fn prop_str(&self, key: &str) -> Option<&'static str> { + match self.prop(key) { + Some(PropValue::Str(s)) => Some(s), + _ => None, + } + } + + /// Convenience: get a u32 array prop. + pub fn prop_u32_array(&self, key: &str) -> Option<&'static [u32]> { + match self.prop(key) { + Some(PropValue::U32Array(arr)) => Some(arr), + _ => None, + } + } + + /// Returns the base address from reg, if present. + pub fn reg_base(&self) -> Option { + self.reg.map(|(base, _)| base) + } + + /// Returns the size from reg, if present. + pub fn reg_size(&self) -> Option { + self.reg.map(|(_, size)| size) + } + + /// Resolve a phandle value (e.g. from a clocks prop) to another Peripheral. + pub fn resolve_phandle(&self, ph: u32) -> Option<&'static Peripheral> { + peripheral_by_phandle(ph) + } + + /// Returns the TreeNode for this peripheral. + pub fn tree_node(&self) -> &'static TreeNode { + &NODES[self.node] + } + + /// Returns true if status prop is absent or set to "okay". + pub fn is_enabled(&self) -> bool { + match self.prop_str("status") { + Some(s) => s == "okay", + None => true, + } + } +} + +"#, + ); +} + +// ================================================================================================ +// Nodes +// ================================================================================================ + +fn emit_nodes(out: &mut String, dt: &DeviceTree) { + out.push_str("pub const NODES: &[TreeNode] = &[\n"); + for (i, node) in dt.nodes.iter().enumerate() { + let phandle = opt_u32(node.phandle); + let parent = opt_usize(node.parent); + let children = if node.children.is_empty() { + "&[]".to_string() + } else { + let inner: Vec = node.children.iter().map(|c| c.to_string()).collect(); + format!("&[{}]", inner.join(", ")) + }; + out.push_str(&format!( + " // {i}\n TreeNode {{ name: {:?}, phandle: {phandle}, parent: {parent}, children: {children} }},\n", + node.name, + )); + } + out.push_str("];\n\n"); +} + +// ================================================================================================ +// Peripherals +// ================================================================================================ + +fn emit_peripherals(out: &mut String, dt: &DeviceTree) { + let mut indices: Vec = Vec::new(); + dt.walk(|idx, node| { + if !node.compatible.is_empty() { + indices.push(idx); + } + }); + + // emit per-node prop sub-consts (only for types that cannot be inlined) + for (i, &idx) in indices.iter().enumerate() { + let node = &dt.nodes[idx]; + + let mut sorted_extra: Vec<(&String, &PropValue)> = node.extra.iter().collect(); + sorted_extra.sort_by_key(|(k, _)| k.as_str()); + + for (j, (_, val)) in sorted_extra.iter().enumerate() { + match val { + PropValue::U32Array(arr) => { + let vals: Vec = arr.iter().map(|v| v.to_string()).collect(); + out.push_str(&format!( + "const PERIPH_{i}_PROP_{j}: &[u32] = &[{}];\n", + vals.join(", ") + )); + } + PropValue::StringList(list) => { + let strs: Vec = list.iter().map(|s| format!("{:?}", s)).collect(); + out.push_str(&format!( + "const PERIPH_{i}_PROP_{j}_STRS: &[&str] = &[{}];\n", + strs.join(", ") + )); + } + PropValue::Bytes(b) => { + let bytes: Vec = b.iter().map(|v| format!("{:#04x}", v)).collect(); + out.push_str(&format!( + "const PERIPH_{i}_PROP_{j}_BYTES: &[u8] = &[{}];\n", + bytes.join(", ") + )); + } + _ => {} + } + } + + // props array + let mut prop_entries: Vec = Vec::new(); + for (j, (key, val)) in sorted_extra.iter().enumerate() { + let pv = match val { + PropValue::Empty => "PropValue::Empty".to_string(), + PropValue::U32(v) => format!("PropValue::U32({v})"), + PropValue::U32Array(_) => format!("PropValue::U32Array(PERIPH_{i}_PROP_{j})"), + PropValue::Str(s) => format!("PropValue::Str({:?})", s), + PropValue::StringList(_) => { + format!("PropValue::StringList(PERIPH_{i}_PROP_{j}_STRS)") + } + PropValue::Bytes(_) => format!("PropValue::Bytes(PERIPH_{i}_PROP_{j}_BYTES)"), + }; + prop_entries.push(format!(" ({:?}, {})", key, pv)); + } + + out.push_str(&format!( + "const PERIPH_{i}_PROPS: &[(&str, PropValue)] = &[\n{}\n];\n", + prop_entries.join(",\n") + )); + } + + out.push('\n'); + out.push_str("pub const PERIPHERALS: &[Peripheral] = &[\n"); + + for (i, &idx) in indices.iter().enumerate() { + let node = &dt.nodes[idx]; + + let compats: Vec = node.compatible.iter().map(|c| format!("{:?}", c)).collect(); + let compat_inline = format!("&[{}]", compats.join(", ")); + + let reg = match node.reg { + Some((base, size)) => format!("Some(({:#010x}, {:#x}))", base, size), + None => "None".to_string(), + }; + + let irqs = if node.interrupts.is_empty() { + "&[]".to_string() + } else { + let vals: Vec = node.interrupts.iter().map(|v| v.to_string()).collect(); + format!("&[{}]", vals.join(", ")) + }; + + let phandle = opt_u32(node.phandle); + + out.push_str(&format!( + " // {i} — node {idx}: {:?}\n Peripheral {{ node: {idx}, compatible: {compat_inline}, reg: {reg}, interrupts: {irqs}, phandle: {phandle}, props: PERIPH_{i}_PROPS }},\n", + node.name, + )); + } + + out.push_str("];\n\n"); +} + +// ================================================================================================ +// Board identity +// ================================================================================================ + +fn emit_board_identity(out: &mut String, dt: &DeviceTree) { + out.push_str(&format!("pub const MODEL: &str = {:?};\n\n", dt.model())); + let stdout = dt + .stdout_compat() + .as_deref() + .map(|s| format!("Some({s:?})")) + .unwrap_or_else(|| "None".to_string()); + out.push_str(&format!("pub const STDOUT: Option<&str> = {stdout};\n\n")); +} + +// ================================================================================================ +// Query API +// ================================================================================================ + +fn emit_query_api(out: &mut String, dt: &DeviceTree) { + out.push_str( + r#"/// Find the first enabled peripheral whose compatible list exactly matches `c`. +pub fn peripheral_by_compatible(c: &str) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| p.is_compatible(c) && p.is_enabled()) +} + +/// Iterate all enabled peripherals whose compatible list exactly matches `c`. +pub fn peripherals_by_compatible(c: &str) -> impl Iterator { + PERIPHERALS.iter().filter(move |p| p.is_compatible(c) && p.is_enabled()) +} + +/// Find a peripheral by its phandle value. +/// Ignores enabled status — phandle targets like clock providers may have no status prop. +pub fn peripheral_by_phandle(ph: u32) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| p.phandle == Some(ph)) +} + +/// Find a peripheral by its NODES index. +pub fn peripheral_by_node(idx: usize) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| p.node == idx) +} + +/// Find a peripheral by node name, with or without unit address. +/// e.g. "serial" matches "serial@40013800". +pub fn peripheral_by_name(name: &str) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| { + let n = NODES[p.node].name; + n == name || n.split('@').next() == Some(name) + }) +} + +"#, + ); + + emit_aliases_module(out, dt); +} + +fn emit_aliases_module(out: &mut String, dt: &DeviceTree) { + let pairs: Vec<(String, String)> = dt + .by_name + .get("aliases") + .map(|&idx| { + let node = &dt.nodes[idx]; + let mut v: Vec<(String, String)> = node + .extra + .iter() + .filter_map(|(k, val)| { + if let crate::ir::PropValue::Str(s) = val { + let name = s.split('/').last().unwrap_or(s); + Some((k.clone(), name.to_string())) + } else { + None + } + }) + .collect(); + v.sort_by_key(|(k, _)| k.clone()); + v + }) + .unwrap_or_default(); + + out.push_str("\npub mod aliases {\n"); + out.push_str(" use super::*;\n\n"); + + // emit ALIASES slice + let entries: Vec = pairs + .iter() + .map(|(k, v)| format!(" ({:?}, {:?})", k, v)) + .collect(); + out.push_str(&format!( + " pub const ALIASES: &[(&str, &str)] = &[\n{}\n ];\n\n", + entries.join(",\n") + )); + + // emit resolve function + out.push_str( + r#" /// Resolve an alias name to its Peripheral. + pub fn resolve(alias: &str) -> Option<&'static Peripheral> { + ALIASES + .iter() + .find(|(k, _)| *k == alias) + .and_then(|(_, name)| peripheral_by_name(name)) + } +"#, + ); + + out.push_str("}\n"); +} + +// ─── Formatting helpers ─────────────────────────────────────────────────────── + +fn opt_u32(v: Option) -> String { + match v { + Some(n) => format!("Some({n})"), + None => "None".to_string(), + } +} + +fn opt_usize(v: Option) -> String { + match v { + Some(n) => format!("Some({n:#010x})"), + None => "None".to_string(), + } +} diff --git a/xtasks/crates/dtgen/src/ir.rs b/xtasks/crates/dtgen/src/ir.rs new file mode 100644 index 0000000..f4d1222 --- /dev/null +++ b/xtasks/crates/dtgen/src/ir.rs @@ -0,0 +1,145 @@ +use std::collections::HashMap; + +// ================================================================================================ +// DTS object attribute types +// ================================================================================================ + +#[derive(Debug, Clone)] +pub enum PropValue { + Empty, + U32(u32), + U32Array(Vec), + Str(String), + StringList(Vec), + Bytes(Vec), +} + +#[derive(Debug, Clone)] +pub struct Node { + pub name: String, + pub compatible: Vec, + pub reg: Option<(u64, u64)>, // (base, size) + pub interrupts: Vec, + pub phandle: Option, + pub extra: HashMap, + pub children: Vec, // indices into DeviceTree::nodes + pub parent: Option, +} + +#[allow(dead_code)] +impl Node { + pub fn reg_base(&self) -> Option { + self.reg.map(|(base, _)| base) + } + + pub fn reg_size(&self) -> Option { + self.reg.map(|(_, size)| size) + } + + pub fn primary_compatible(&self) -> Option<&str> { + self.compatible.first().map(|s| s.as_str()) + } + + pub fn is_compatible(&self, prefix: &str) -> bool { + self.compatible.iter().any(|c| c.starts_with(prefix)) + } + + pub fn extra_u32(&self, key: &str) -> Option { + match self.extra.get(key) { + Some(PropValue::U32(v)) => Some(*v), + _ => None, + } + } + + pub fn extra_u32_array(&self, key: &str) -> Option<&[u32]> { + match self.extra.get(key) { + Some(PropValue::U32Array(v)) => Some(v), + _ => None, + } + } + + pub fn extra_str(&self, key: &str) -> Option<&str> { + match self.extra.get(key) { + Some(PropValue::Str(s)) => Some(s.as_str()), + _ => None, + } + } + + pub fn extra_string_list(&self, key: &str) -> Option<&[String]> { + match self.extra.get(key) { + Some(PropValue::StringList(v)) => Some(v), + _ => None, + } + } +} + +// ================================================================================================ +// Raw devicetree as output from parsing in-memory DTB +// ================================================================================================ + +#[derive(Debug)] +pub struct DeviceTree { + pub nodes: Vec, + pub by_phandle: HashMap, + pub by_name: HashMap, + pub root: usize, +} + +#[allow(dead_code)] +impl DeviceTree { + pub fn node(&self, idx: usize) -> &Node { + &self.nodes[idx] + } + + pub fn resolve_phandle(&self, phandle: u32) -> Option<&Node> { + self.by_phandle.get(&phandle).map(|&idx| &self.nodes[idx]) + } + + pub fn resolve_phandle_idx(&self, phandle: u32) -> Option { + self.by_phandle.get(&phandle).copied() + } + + // iterate only direct children of a node. + pub fn children(&self, idx: usize) -> impl Iterator { + self.nodes[idx] + .children + .iter() + .map(|&child_idx| (child_idx, &self.nodes[child_idx])) + } + + // walk all nodes depth-first, calling f for each (idx, node). + pub fn walk(&self, mut f: impl FnMut(usize, &Node)) { + self.walk_from(self.root, &mut f); + } + + fn walk_from(&self, idx: usize, f: &mut impl FnMut(usize, &Node)) { + f(idx, &self.nodes[idx]); + for &child in &self.nodes[idx].children { + self.walk_from(child, f); + } + } + + // model string from /model property or first compatible string. + pub fn model(&self) -> String { + let root = &self.nodes[self.root]; + if let Some(s) = root.extra_str("model") { + return s.to_string(); + } + root.compatible + .first() + .cloned() + .unwrap_or_else(|| "unknown".to_string()) + } + + // resolve stdout-path in /chosen to the first compatible string of that node. + pub fn stdout_compat(&self) -> Option { + let chosen_idx = *self.by_name.get("chosen")?; + let path = self.nodes[chosen_idx].extra_str("stdout-path")?.to_string(); + // strip optional baud suffix: "/soc/serial@deadbeef:115200" -> "/soc/serial@deadbeef" + let path = path.split(':').next()?; + // match by last path component + let name = path.split('/').last()?; + let idx = self.by_name.get(name)?; + self.nodes[*idx].compatible.first().cloned() + } +} diff --git a/xtasks/crates/dtgen/src/lib.rs b/xtasks/crates/dtgen/src/lib.rs new file mode 100644 index 0000000..36d6917 --- /dev/null +++ b/xtasks/crates/dtgen/src/lib.rs @@ -0,0 +1,13 @@ +mod codegen; +mod ir; +mod parser; + +use std::path::Path; + +pub fn run(dts_path: &Path, include_dirs: &[&Path], out_path: &Path) -> Result<(), String> { + let dtb = parser::dts_to_dtb(dts_path, include_dirs)?; + let dt = parser::dtb_to_devicetree(&dtb)?; + let src = codegen::generate_rust(&dt); + std::fs::write(out_path, src) + .map_err(|e| format!("dtgen: failed to write {}: {e}", out_path.display())) +} diff --git a/xtasks/crates/dtgen/src/main.rs b/xtasks/crates/dtgen/src/main.rs new file mode 100644 index 0000000..370bf88 --- /dev/null +++ b/xtasks/crates/dtgen/src/main.rs @@ -0,0 +1,31 @@ +use clap::Parser; +use std::path::PathBuf; + +// dtgen CLI — thin wrapper over lib::run +// +// Usage: +// dtgen [-I ...] +// +// Examples: +// dtgen board.dts out/device.rs +// dtgen board.dts out/device.rs -I vendor/stm32/include -I vendor/cmsis/include + +#[derive(Parser)] +#[command(name = "dtgen", version, about)] +struct Args { + input: PathBuf, // input .dts file + output: PathBuf, // output .rs file + + #[arg(short = 'I', value_name = "DIR")] + include_dirs: Vec, // extra include directories, forwarded to cpp preprocessor +} + +fn main() { + let args = Args::parse(); + let refs: Vec<&std::path::Path> = args.include_dirs.iter().map(|p| p.as_path()).collect(); + + dtgen::run(&args.input, &refs, &args.output).unwrap_or_else(|e| { + eprintln!("dtgen error: {e}"); + std::process::exit(1); + }); +} diff --git a/xtasks/crates/dtgen/src/parser.rs b/xtasks/crates/dtgen/src/parser.rs new file mode 100644 index 0000000..268c730 --- /dev/null +++ b/xtasks/crates/dtgen/src/parser.rs @@ -0,0 +1,232 @@ +use crate::ir::{DeviceTree, Node, PropValue}; +use std::collections::HashMap; + +// ================================================================================================ +// DTB construction from compiling DTS +// ================================================================================================ + +pub fn dts_to_dtb( + dts_path: &std::path::Path, + include_dirs: &[&std::path::Path], +) -> Result, String> { + let preprocessed_path = dts_path.with_extension("preprocessed.dts"); + let dtb_path = dts_path.with_extension("dtb"); + + // stage 1 - preprocessing + // -E: preprocess only + // -nostdinc: caller provides all needed headers + // -undef: don't predefine macros + // -x assembler-with-cpp: preprocessor interpreter + let mut cpp_cmd = std::process::Command::new("cpp"); + cpp_cmd.args(["-E", "-nostdinc", "-undef", "-x", "assembler-with-cpp"]); + + for dir in include_dirs { + cpp_cmd.arg("-I").arg(dir); + } + + cpp_cmd.arg(dts_path).arg("-o").arg(&preprocessed_path); + let cpp_status = cpp_cmd + .status() + .map_err(|e| format!("cpp not found: {e}. Install with: apt install gcc"))?; + + if !cpp_status.success() { + return Err("cpp preprocessing failed".to_string()); + } + + // stage 2 - dts compilation + let dtc_status = std::process::Command::new("dtc") + .args([ + "-I", + "dts", + "-O", + "dtb", + "-o", + dtb_path.to_str().unwrap(), + preprocessed_path.to_str().unwrap(), + ]) + .status() + .map_err(|e| { + format!("dtc not found: {e}. Install with: apt install device-tree-compiler") + })?; + + if !dtc_status.success() { + return Err("dtc failed".to_string()); + } + + std::fs::read(&dtb_path).map_err(|e| format!("cannot read DTB: {e}")) +} + +// ================================================================================================ +// DeviceTree construction from walk through DTB in-memory blob via FDT crate +// ================================================================================================ + +pub fn dtb_to_devicetree(dtb: &[u8]) -> Result { + let fdt = fdt::Fdt::new(dtb).map_err(|e| format!("fdt parse error: {e}"))?; + let mut tree = DeviceTree { + nodes: Vec::new(), + by_phandle: HashMap::new(), + by_name: HashMap::new(), + root: 0, + }; + + let root = fdt.find_node("/").ok_or("cannot find root node")?; + let addr_cells = read_cell_count(&root, "#address-cells").unwrap_or(1); + let size_cells = read_cell_count(&root, "#size-cells").unwrap_or(1); + + tree.root = walk(root, None, &mut tree, addr_cells, size_cells); + Ok(tree) +} + +fn walk<'a>( + node: fdt::node::FdtNode<'a, '_>, + parent: Option, + tree: &mut DeviceTree, + addr_cells: u32, + size_cells: u32, +) -> usize { + let name = node.name.to_string(); + + let compatible: Vec = node + .compatible() + .map(|c| c.all().map(|s| s.to_string()).collect()) + .unwrap_or_default(); + + let phandle = node + .property("phandle") + .filter(|p| p.value.len() == 4) + .map(|p| u32::from_be_bytes(p.value.try_into().unwrap())); + + let child_addr_cells = read_cell_count(&node, "#address-cells").unwrap_or(addr_cells); + let child_size_cells = read_cell_count(&node, "#size-cells").unwrap_or(size_cells); + + let reg = parse_reg(&node, addr_cells, size_cells); + let interrupts: Vec = node + .property("interrupts") + .map(|p| { + p.value + .chunks(4) + .map(|b| u32::from_be_bytes(b.try_into().unwrap())) + .collect() + }) + .unwrap_or_default(); + + const SKIP: &[&str] = &[ + "compatible", + "reg", + "phandle", + "linux,phandle", + "interrupts", + "#address-cells", + "#size-cells", + "name", + ]; + + let mut extra = HashMap::new(); + for prop in node.properties() { + if SKIP.contains(&prop.name) { + continue; + } + extra.insert(prop.name.to_string(), parse_prop_value(prop.value)); + } + + let idx = tree.nodes.len(); + tree.nodes.push(Node { + name: name.clone(), + compatible, + reg, + interrupts, + phandle, + extra, + children: Vec::new(), + parent, + }); + + if let Some(ph) = phandle { + tree.by_phandle.insert(ph, idx); + } + tree.by_name.insert(name, idx); + + for child in node.children() { + let child_idx = walk(child, Some(idx), tree, child_addr_cells, child_size_cells); + tree.nodes[idx].children.push(child_idx); + } + + idx +} + +// ================================================================================================ +// Helpers +// ================================================================================================ + +fn read_cell_count<'a>(node: &fdt::node::FdtNode<'a, '_>, prop: &str) -> Option { + node.property(prop) + .filter(|p| p.value.len() == 4) + .map(|p| u32::from_be_bytes(p.value.try_into().unwrap())) +} + +fn parse_reg<'a>( + node: &fdt::node::FdtNode<'a, '_>, + addr_cells: u32, + size_cells: u32, +) -> Option<(u64, u64)> { + let prop = node.property("reg")?; + let words: Vec = prop + .value + .chunks(4) + .map(|b| u32::from_be_bytes(b.try_into().unwrap())) + .collect(); + + let addr = match addr_cells { + 1 => *words.first()? as u64, + 2 => ((*words.first()? as u64) << 32) | *words.get(1)? as u64, + _ => return None, + }; + + let size = match size_cells { + 0 => 0u64, + 1 => *words.get(addr_cells as usize)? as u64, + 2 => { + let i = addr_cells as usize; + ((*words.get(i)? as u64) << 32) | *words.get(i + 1)? as u64 + } + _ => return None, + }; + + Some((addr, size)) +} + +fn parse_prop_value(bytes: &[u8]) -> PropValue { + if bytes.is_empty() { + return PropValue::Empty; + } + + if bytes.last() == Some(&0) { + let is_printable_ascii = bytes[..bytes.len() - 1] + .iter() + .all(|&b| b == 0 || (b >= 0x20 && b <= 0x7e)); + + if is_printable_ascii { + let s = std::str::from_utf8(&bytes[..bytes.len() - 1]).unwrap(); + let parts: Vec<&str> = s.split('\0').collect(); + return if parts.len() == 1 { + PropValue::Str(parts[0].to_string()) + } else { + PropValue::StringList(parts.iter().map(|s| s.to_string()).collect()) + }; + } + } + + if bytes.len().is_multiple_of(4) { + let words: Vec = bytes + .chunks(4) + .map(|b| u32::from_be_bytes(b.try_into().unwrap())) + .collect(); + return if words.len() == 1 { + PropValue::U32(words[0]) + } else { + PropValue::U32Array(words) + }; + } + + PropValue::Bytes(bytes.to_vec()) +} From 9993751a6597ad5c5977e587744eb152ffeddfa5 Mon Sep 17 00:00:00 2001 From: EvilHedge <101642745+LuisRuisinger@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:02:57 +0100 Subject: [PATCH 2/4] Fix Passing &dtb_path to Command::arg directly to not trigger potential panic on non-utf8 paths Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- xtasks/crates/dtgen/src/parser.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/xtasks/crates/dtgen/src/parser.rs b/xtasks/crates/dtgen/src/parser.rs index 268c730..bcd104c 100644 --- a/xtasks/crates/dtgen/src/parser.rs +++ b/xtasks/crates/dtgen/src/parser.rs @@ -34,16 +34,16 @@ pub fn dts_to_dtb( } // stage 2 - dts compilation - let dtc_status = std::process::Command::new("dtc") - .args([ - "-I", - "dts", - "-O", - "dtb", - "-o", - dtb_path.to_str().unwrap(), - preprocessed_path.to_str().unwrap(), - ]) + let mut dtc_cmd = std::process::Command::new("dtc"); + dtc_cmd + .arg("-I") + .arg("dts") + .arg("-O") + .arg("dtb") + .arg("-o") + .arg(&dtb_path) + .arg(&preprocessed_path); + let dtc_status = dtc_cmd .status() .map_err(|e| { format!("dtc not found: {e}. Install with: apt install device-tree-compiler") From 5f029f9645cd67dd5636770ec1b3cd66604d5a61 Mon Sep 17 00:00:00 2001 From: EvilHedge <101642745+LuisRuisinger@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:06:36 +0100 Subject: [PATCH 3/4] [Fix] Codegen for treenodes should output decimals as node indices rather than hex which would potentially not imply indexing but rather addressing. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- xtasks/crates/dtgen/src/codegen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index ac17348..b434373 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -401,7 +401,7 @@ fn opt_u32(v: Option) -> String { fn opt_usize(v: Option) -> String { match v { - Some(n) => format!("Some({n:#010x})"), + Some(n) => format!("Some({n})"), None => "None".to_string(), } } From 52d910111f0e56e6afe1d14164be5d0da945b4fb Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Wed, 18 Mar 2026 21:32:19 +0100 Subject: [PATCH 4/4] [Fix] * changed allow(dead_code) to not yield a compil e error when including the code generated file anywhere except as first file * fixed cargo edition * fixed temporary artifacts being emitted during dts preprocessing and compilation * fixed interrupt parsing to not panic * extending the by_name map to be a multimap since the device tree specification only states that names must be unique among sibling nodes of the same subtree * added additional query functions aswell as adapting to the multiset of name references * added modules for memory regions aswell as chosen * emitted core chosen attributes --- xtasks/crates/dtgen/Cargo.toml | 4 +- xtasks/crates/dtgen/README.md | 101 +++++--- xtasks/crates/dtgen/src/codegen.rs | 401 +++++++++++++++++++++++------ xtasks/crates/dtgen/src/ir.rs | 22 +- xtasks/crates/dtgen/src/parser.rs | 66 +++-- 5 files changed, 430 insertions(+), 164 deletions(-) diff --git a/xtasks/crates/dtgen/Cargo.toml b/xtasks/crates/dtgen/Cargo.toml index 184d21c..55fe54e 100644 --- a/xtasks/crates/dtgen/Cargo.toml +++ b/xtasks/crates/dtgen/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dtgen" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] name = "dtgen" @@ -13,4 +13,4 @@ path = "src/main.rs" [dependencies] fdt = "0.1.5" -clap = { version = "4", features = ["derive"] } \ No newline at end of file +clap = { version = "4", features = ["derive"] } diff --git a/xtasks/crates/dtgen/README.md b/xtasks/crates/dtgen/README.md index 39104f5..e5128b3 100644 --- a/xtasks/crates/dtgen/README.md +++ b/xtasks/crates/dtgen/README.md @@ -143,65 +143,84 @@ p.tree_node().iter_children() // impl Iterator +peripheral_by_compatible("st,stm32-uart") // Option<&'static Peripheral> // all enabled matches - e.g. multiple UARTs -peripherals_by_compatible("st,stm32-uart") // impl Iterator +peripherals_by_compatible("st,stm32-uart") // impl Iterator ``` - ### By phandle - - ```rust -peripheral_by_phandle(1) // Option<&'static Peripheral> +peripheral_by_phandle(1) // Option<&'static Peripheral> ``` - ### By node index - ```rust -peripheral_by_node(7) // Option<&'static Peripheral> +peripheral_by_node(7) // Option<&'static Peripheral> ``` - ### By name +Node names are only unique among siblings. Use the scoped or path forms when the +name may appear under multiple parents (e.g. `channel@0` under multiple ADC nodes). +```rust +// all matches across the entire tree - name with or without unit address +peripherals_by_name("channel") // impl Iterator +peripherals_by_name("channel@0") // exact unit-address match also works -Matches with or without unit address suffix. +// scoped to a specific parent - safe when names are not globally unique +peripheral_by_name_under("channel", adc1.node) // Option<&'static Peripheral> -```rust -peripheral_by_name("serial") // matches "serial@40013800" - note this then works via first founds -peripheral_by_name("serial@40013800") // exact match also works +// unambiguous full path - with or without unit addresses at each segment +peripheral_by_path("soc/adc@50040000/channel@17") // Option<&'static Peripheral> +peripheral_by_path("soc/adc/channel") // first match at each level ``` - --- ## `chosen` submodule - ```rust -// resolves /chosen stdout-path to the target Peripheral -chosen::stdout_path() // Option<&'static Peripheral> +// O(1) direct index into PERIPHERALS, resolved at codegen time from /chosen stdout-path +chosen::stdout() // Option<&'static Peripheral> + +// raw constants - all are Option<_>, None if absent from /chosen +chosen::STDOUT // Option — index into PERIPHERALS +chosen::BOOTARGS // Option<&str> +chosen::INITRD_START // Option +chosen::INITRD_END // Option ``` - --- +## `aliases` submodule +```rust +// resolve an alias name to its Peripheral +aliases::resolve("serial1") // Option<&'static Peripheral> +// raw table if you need to iterate +aliases::ALIASES // &[(&str, usize)] — (alias_name, node_index) +``` +--- +## `memory` submodule +```rust +// all declared memory regions, sorted by base address +// entries are (node_name, base_address, size_in_bytes) +memory::REGIONS // &[(&str, usize, usize)] +memory::region_by_name("memory@20000000") // Option<(usize, usize)> — (base, size) +memory::total_bytes() // usize +``` +--- ## Common query patterns ### Find the console UART ```rust -let console = chosen::stdout_path() +let console = chosen::stdout() .expect("no stdout-path in /chosen"); - let base = console.reg_base().expect("console has no reg"); let baud = console.prop_u32("current-speed").unwrap_or(115200); ``` ### Find all enabled UARTs - ```rust for uart in peripherals_by_compatible("st,stm32-uart") { let base = uart.reg_base().unwrap(); @@ -210,36 +229,29 @@ for uart in peripherals_by_compatible("st,stm32-uart") { ``` ### Resolve a clock dependency - ```rust let uart = peripheral_by_compatible("st,stm32-uart").unwrap(); - if let Some(PropValue::U32Array(cells)) = uart.prop("clocks") { // cells = [phandle, ...clock specifier cells...] - let phandle = cells[0]; - let rcc = peripheral_by_phandle(phandle).expect("clock provider not found"); + let rcc = peripheral_by_phandle(cells[0]).expect("clock provider not found"); let freq = rcc.prop_u32("clock-frequency").unwrap_or(0); } ``` ### Find a GPIO controller by phandle - ```rust // DTS: led-gpios = <&gpioa 5 0> // emitted as: PropValue::U32Array(&[gpioa_phandle, 5, 0]) - if let Some(PropValue::U32Array(cells)) = node.prop("led-gpios") { - let gpio = peripheral_by_phandle(cells[0]).unwrap(); - let pin = cells[1]; + let gpio = peripheral_by_phandle(cells[0]).unwrap(); + let pin = cells[1]; let flags = cells[2]; } ``` ### Walk children of a node - ```rust -// find all child nodes of the "leds" node -if let Some(leds) = peripheral_by_name("leds") { +if let Some(leds) = peripherals_by_name("leds").next() { for (child_idx, child_node) in leds.tree_node().iter_children() { if let Some(child_periph) = peripheral_by_node(child_idx) { // process each LED child peripheral @@ -248,29 +260,36 @@ if let Some(leds) = peripheral_by_name("leds") { } ``` -### Filter by compatible then check a prop +### Find an ADC channel by path +```rust +// unambiguous even though "channel@17" appears under multiple ADC nodes +let vbat = peripheral_by_path("soc/adc@50040000/channel@17") + .expect("VBAT channel not found"); +``` +### Filter by compatible then check a prop ```rust -// find an SPI controller with a specific bus frequency let spi = peripherals_by_compatible("st,stm32-spi") .find(|p| p.prop_u32("clock-frequency") == Some(1_000_000)); ``` ---- +### Set up memory regions +```rust +for (name, base, size) in memory::REGIONS { + mpu_configure_region(base, size); +} +``` +--- ## CLI invocation - ``` dtgen [-I ...] ``` - ```bash dtgen board.dts src/dt.rs dtgen board.dts out/dt.rs -I vendor/stm32/include -I vendor/cmsis/include ``` - ## `build.rs` integration - ```rust fn main() { let dts = std::path::Path::new("board.dts"); diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index b434373..c870c45 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -8,34 +8,35 @@ pub fn generate_rust(dt: &DeviceTree) -> String { emit_peripheral_type(&mut out); emit_nodes(&mut out, dt); emit_peripherals(&mut out, dt); - emit_board_identity(&mut out, dt); emit_query_api(&mut out, dt); + emit_aliases_module(&mut out, dt); + emit_memory_module(&mut out, dt); + emit_chosen_module(&mut out, dt); out } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // File header -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ fn emit_header(out: &mut String) { out.push_str( - r#"// ================================ -// GENERATED BY dtgen — DO NOT EDIT -// ================================ - -#![allow(dead_code)] + r#"// ------------------------------------------------------------------------------------------------ +// AUTOGENERATED BY dtgen - DO NOT EDIT FILE +// ------------------------------------------------------------------------------------------------ "#, ); } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // Type defitions -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ fn emit_prop_value_type(out: &mut String) { out.push_str( - r#"#[derive(Debug, Clone, Copy, PartialEq)] + r#"#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum PropValue { Empty, U32(u32), @@ -51,7 +52,8 @@ pub enum PropValue { fn emit_topology_type(out: &mut String) { out.push_str( - r#"#[derive(Debug, Clone, Copy)] + r#"#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] pub struct TreeNode { pub name: &'static str, pub phandle: Option, @@ -60,12 +62,12 @@ pub struct TreeNode { } impl TreeNode { - /// Returns the parent TreeNode, if any. + // returns the parent TreeNode, if any. pub fn parent_node(&self) -> Option<&'static TreeNode> { self.parent.map(|idx| &NODES[idx]) } - /// Iterate children as (node_index, &TreeNode). + // iterate children as (node_index, &TreeNode). pub fn iter_children(&self) -> impl Iterator { self.children.iter().map(|&idx| (idx, &NODES[idx])) } @@ -77,7 +79,8 @@ impl TreeNode { fn emit_peripheral_type(out: &mut String) { out.push_str( - r#"#[derive(Debug, Clone, Copy)] + r#"#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] pub struct Peripheral { pub node: usize, pub compatible: &'static [&'static str], @@ -88,22 +91,22 @@ pub struct Peripheral { } impl Peripheral { - /// Returns true if any compatible string exactly matches `c`. + // returns true if any compatible string exactly matches `c` pub fn is_compatible(&self, c: &str) -> bool { self.compatible.iter().any(|&s| s == c) } - /// Returns true if any compatible string contains `fragment` as a substring. + // returns true if any compatible string contains `fragment` as a substring pub fn compatible_contains(&self, fragment: &str) -> bool { self.compatible.iter().any(|&s| s.contains(fragment)) } - /// Look up a prop by key, returning the raw PropValue. + // look up a prop by key, returning the raw PropValue pub fn prop(&self, key: &str) -> Option { self.props.iter().find(|(k, _)| *k == key).map(|(_, v)| *v) } - /// Convenience: get a u32 prop. + // get a u32 prop pub fn prop_u32(&self, key: &str) -> Option { match self.prop(key) { Some(PropValue::U32(v)) => Some(v), @@ -111,7 +114,7 @@ impl Peripheral { } } - /// Convenience: get a str prop. + // get a str prop pub fn prop_str(&self, key: &str) -> Option<&'static str> { match self.prop(key) { Some(PropValue::Str(s)) => Some(s), @@ -119,7 +122,7 @@ impl Peripheral { } } - /// Convenience: get a u32 array prop. + // get a u32 array prop pub fn prop_u32_array(&self, key: &str) -> Option<&'static [u32]> { match self.prop(key) { Some(PropValue::U32Array(arr)) => Some(arr), @@ -127,27 +130,27 @@ impl Peripheral { } } - /// Returns the base address from reg, if present. + // returns the base address from reg, if present pub fn reg_base(&self) -> Option { self.reg.map(|(base, _)| base) } - /// Returns the size from reg, if present. + // returns the size from reg, if present pub fn reg_size(&self) -> Option { self.reg.map(|(_, size)| size) } - /// Resolve a phandle value (e.g. from a clocks prop) to another Peripheral. + // resolve a phandle value (e.g. from a clocks prop) to another Peripheral pub fn resolve_phandle(&self, ph: u32) -> Option<&'static Peripheral> { peripheral_by_phandle(ph) } - /// Returns the TreeNode for this peripheral. + // returns the TreeNode for this peripheral. pub fn tree_node(&self) -> &'static TreeNode { &NODES[self.node] } - /// Returns true if status prop is absent or set to "okay". + // returns true if status prop is absent or set to "okay" pub fn is_enabled(&self) -> bool { match self.prop_str("status") { Some(s) => s == "okay", @@ -160,9 +163,9 @@ impl Peripheral { ); } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // Nodes -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ fn emit_nodes(out: &mut String, dt: &DeviceTree) { out.push_str("pub const NODES: &[TreeNode] = &[\n"); @@ -183,9 +186,9 @@ fn emit_nodes(out: &mut String, dt: &DeviceTree) { out.push_str("];\n\n"); } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // Peripherals -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ fn emit_peripherals(out: &mut String, dt: &DeviceTree) { let mut indices: Vec = Vec::new(); @@ -283,106 +286,158 @@ fn emit_peripherals(out: &mut String, dt: &DeviceTree) { out.push_str("];\n\n"); } -// ================================================================================================ -// Board identity -// ================================================================================================ - -fn emit_board_identity(out: &mut String, dt: &DeviceTree) { - out.push_str(&format!("pub const MODEL: &str = {:?};\n\n", dt.model())); - let stdout = dt - .stdout_compat() - .as_deref() - .map(|s| format!("Some({s:?})")) - .unwrap_or_else(|| "None".to_string()); - out.push_str(&format!("pub const STDOUT: Option<&str> = {stdout};\n\n")); -} - -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // Query API -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ fn emit_query_api(out: &mut String, dt: &DeviceTree) { out.push_str( - r#"/// Find the first enabled peripheral whose compatible list exactly matches `c`. + r#"// find the first enabled peripheral for which any compatible string equals `c`. pub fn peripheral_by_compatible(c: &str) -> Option<&'static Peripheral> { PERIPHERALS.iter().find(|p| p.is_compatible(c) && p.is_enabled()) } -/// Iterate all enabled peripherals whose compatible list exactly matches `c`. +// iterate all enabled peripherals for which any compatible string equals `c` pub fn peripherals_by_compatible(c: &str) -> impl Iterator { PERIPHERALS.iter().filter(move |p| p.is_compatible(c) && p.is_enabled()) } -/// Find a peripheral by its phandle value. -/// Ignores enabled status — phandle targets like clock providers may have no status prop. +// find a peripheral by its phandle value +// ignores enabled status - phandle targets like clock providers may have no status prop +// phandle values are unique pub fn peripheral_by_phandle(ph: u32) -> Option<&'static Peripheral> { PERIPHERALS.iter().find(|p| p.phandle == Some(ph)) } -/// Find a peripheral by its NODES index. +// find a peripheral by its NODES index +// NODES indices are unique pub fn peripheral_by_node(idx: usize) -> Option<&'static Peripheral> { PERIPHERALS.iter().find(|p| p.node == idx) } -/// Find a peripheral by node name, with or without unit address. -/// e.g. "serial" matches "serial@40013800". -pub fn peripheral_by_name(name: &str) -> Option<&'static Peripheral> { - PERIPHERALS.iter().find(|p| { +// iterate all peripherals whose node name matches, with or without unit address +// e.g. "channel" matches "channel@0", "channel@1", etc. across all parents +// node names are only unique among siblings - use peripheral_by_name_under or +// peripheral_by_path when you need an unambiguous match +pub fn peripherals_by_name(name: &str) -> impl Iterator { + PERIPHERALS.iter().filter(move |p| { let n = NODES[p.node].name; n == name || n.split('@').next() == Some(name) }) } +// find a peripheral by name scoped to a specific parent node index +// safe form of name lookup since node names are only unique among siblings +// e.g. peripheral_by_name_under("channel", adc1.node) returns adc1's channel@0 +pub fn peripheral_by_name_under(name: &str, parent_node: usize) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| { + let n = NODES[p.node].name; + let name_matches = n == name || n.split('@').next() == Some(name); + let parent_matches = NODES[p.node].parent == Some(parent_node); + + name_matches && parent_matches + }) +} + +// find a peripheral by its full path from root, with or without unit addresses +// e.g. "soc/i2c@40005400/lsm6dsl@6a" or "soc/i2c/lsm6dsl" +// at each level the first sibling whose name matches is taken +pub fn peripheral_by_path(path: &str) -> Option<&'static Peripheral> { + let segments: Vec<&str> = path.trim_start_matches('/').split('/').collect(); + let mut current = 0usize; + for segment in &segments { + let child = NODES[current].children.iter().find(|&&idx| { + let n = NODES[idx].name; + n == *segment || n.split('@').next() == Some(*segment) + })?; + + current = *child; + } + + peripheral_by_node(current) +} "#, ); +} + +fn resolve_path(dt: &DeviceTree, path: &str) -> Option { + if path == "/" { + return Some(dt.root); + } + + let mut current_idx = dt.root; + + // split "/soc/serial@40013800" into ["soc", "serial@40013800"] + let segments = path.split('/').filter(|s| !s.is_empty()); + for segment in segments { + let current_node = &dt.nodes[current_idx]; + + // look through children of the current node for a matching name + let found_child = current_node + .children + .iter() + .find(|&&child_idx| dt.nodes[child_idx].name == segment); - emit_aliases_module(out, dt); + if let Some(&next_idx) = found_child { + current_idx = next_idx; + } else { + return None; // Path broken + } + } + Some(current_idx) } +// ------------------------------------------------------------------------------------------------ +// Alias module +// ------------------------------------------------------------------------------------------------ + fn emit_aliases_module(out: &mut String, dt: &DeviceTree) { - let pairs: Vec<(String, String)> = dt + let mut pairs: Vec<(String, usize)> = dt .by_name .get("aliases") + .and_then(|indices| indices.first()) // there is usually only one /aliases node .map(|&idx| { - let node = &dt.nodes[idx]; - let mut v: Vec<(String, String)> = node + dt.nodes[idx] .extra .iter() .filter_map(|(k, val)| { - if let crate::ir::PropValue::Str(s) = val { - let name = s.split('/').last().unwrap_or(s); - Some((k.clone(), name.to_string())) + if let crate::ir::PropValue::Str(path) = val { + // resolve the full path to a unique index + resolve_path(dt, path).map(|target_idx| (k.clone(), target_idx)) } else { None } }) - .collect(); - v.sort_by_key(|(k, _)| k.clone()); - v + .collect() }) .unwrap_or_default(); - out.push_str("\npub mod aliases {\n"); + // sort by alias name (e.g., serial0, serial1) for deterministic output + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + out.push('\n'); + + out.push_str(&format!("// {}\n", "-".repeat(96))); + out.push_str("// Aliases\n"); + out.push_str(&format!("// {}\n\n", "-".repeat(96))); + + out.push_str("pub mod aliases {\n"); out.push_str(" use super::*;\n\n"); - // emit ALIASES slice - let entries: Vec = pairs - .iter() - .map(|(k, v)| format!(" ({:?}, {:?})", k, v)) - .collect(); - out.push_str(&format!( - " pub const ALIASES: &[(&str, &str)] = &[\n{}\n ];\n\n", - entries.join(",\n") - )); + // emit the slice of tuples: (alias_name, node_index) + out.push_str(" pub const ALIASES: &[(&str, usize)] = &[\n"); + for (name, idx) in &pairs { + out.push_str(&format!(" ({:?}, {}),\n", name, idx)); + } - // emit resolve function + out.push_str(" ];\n\n"); out.push_str( - r#" /// Resolve an alias name to its Peripheral. + r#" // resolve an alias name to its Peripheral pub fn resolve(alias: &str) -> Option<&'static Peripheral> { ALIASES .iter() .find(|(k, _)| *k == alias) - .and_then(|(_, name)| peripheral_by_name(name)) + .and_then(|(_, idx)| peripheral_by_node(*idx)) } "#, ); @@ -390,7 +445,195 @@ fn emit_aliases_module(out: &mut String, dt: &DeviceTree) { out.push_str("}\n"); } -// ─── Formatting helpers ─────────────────────────────────────────────────────── +// ------------------------------------------------------------------------------------------------ +// Memory module +// ------------------------------------------------------------------------------------------------ + +fn emit_memory_module(out: &mut String, dt: &DeviceTree) { + // the device tree speciciation states that for a memory node this field must exist and be set + // to "memory" + let mut regions: Vec<(&str, u64, u64)> = dt + .nodes + .iter() + .filter(|n| { + n.extra + .get("device_type") + .map(|v| matches!(v, crate::ir::PropValue::Str(s) if s == "memory")) + .unwrap_or(false) + }) + .filter_map(|n| { + let (base, size) = n.reg?; + Some((n.name.as_str(), base, size)) + }) + .collect(); + + // sort by base address for deterministic, predictable output + regions.sort_by_key(|&(_, base, _)| base); + + out.push('\n'); + + out.push_str(&format!("// {}\n", "-".repeat(96))); + out.push_str("// Memory regions\n"); + out.push_str(&format!("// {}\n\n", "-".repeat(96))); + + out.push_str("pub mod memory {\n"); + out.push_str(" use super::*;\n\n"); + out.push_str(" pub const REGIONS: &[(&str, usize, usize)] = &[\n"); + + for (name, base, size) in ®ions { + out.push_str(&format!( + " ({:?}, {:#010x}, {:#010x}),\n", + name, base, size + )); + } + + out.push_str(" ];\n\n"); + out.push_str( + r#" // find a memory region by node name + pub fn region_by_name(name: &str) -> Option<(usize, usize)> { + REGIONS + .iter() + .find(|(n, _, _)| *n == name) + .map(|(_, base, size)| (*base, *size)) + } + + // total memory in bytes across all declared regions + pub fn total_bytes() -> usize { + REGIONS.iter().map(|(_, _, size)| size).sum() + } +"#, + ); + out.push_str("}\n"); +} + +// ------------------------------------------------------------------------------------------------ +// Chosen module +// ------------------------------------------------------------------------------------------------ + +fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { + let chosen = dt + .by_name + .get("chosen") + .and_then(|indices| indices.first()) + .map(|&idx| &dt.nodes[idx]); + + // reconstruct the same peripheral indices vec that emit_peripherals uses + // so we can map a node index to a PERIPHERALS array index + // used as Peripheral index retrieval for stdout path resolve + let mut periph_indices: Vec = Vec::new(); + dt.walk(|idx, node| { + if !node.compatible.is_empty() { + periph_indices.push(idx); + } + }); + + let stdout_periph_idx: Option = chosen.and_then(|n| { + if let Some(crate::ir::PropValue::Str(raw)) = n.extra.get("stdout-path") { + let path = raw.split(':').next().unwrap_or(raw); + + let node_idx = if path.contains('/') { + resolve_path(dt, path) + } else { + dt.by_name + .get("aliases") + .and_then(|ids| ids.first()) + .and_then(|&aidx| { + if let Some(crate::ir::PropValue::Str(resolved)) = + dt.nodes[aidx].extra.get(path) + { + resolve_path(dt, resolved) + } else { + None + } + }) + }?; + + // position in PERIPHERALS = position of node_idx in periph_indices + periph_indices.iter().position(|&idx| idx == node_idx) + } else { + None + } + }); + + let bootargs: Option<&str> = chosen.and_then(|n| { + if let Some(crate::ir::PropValue::Str(s)) = n.extra.get("bootargs") { + Some(s.as_str()) + } else { + None + } + }); + + let initrd_start: Option = chosen.and_then(|n| { + if let Some(crate::ir::PropValue::U32(v)) = n.extra.get("linux,initrd-start") { + Some(*v) + } else { + None + } + }); + + let initrd_end: Option = chosen.and_then(|n| { + if let Some(crate::ir::PropValue::U32(v)) = n.extra.get("linux,initrd-end") { + Some(*v) + } else { + None + } + }); + + out.push('\n'); + + out.push_str(&format!("// {}\n", "-".repeat(96))); + out.push_str("// Chosen\n"); + out.push_str(&format!("// {}\n\n", "-".repeat(96))); + out.push_str("pub mod chosen {\n"); + out.push_str(" use super::*;\n\n"); + + match stdout_periph_idx { + Some(idx) => out.push_str(&format!( + " // index into PERIPHERALS of the stdout peripheral, resolved from stdout-path\n pub const STDOUT: Option = Some({idx});\n" + )), + None => out.push_str( + " // no stdout-path declared in /chosen.\n pub const STDOUT: Option = None;\n", + ), + } + + match bootargs { + Some(s) => out.push_str(&format!( + " pub const BOOTARGS: Option<&str> = Some({:?});\n", + s + )), + None => out.push_str(" pub const BOOTARGS: Option<&str> = None;\n"), + } + + match initrd_start { + Some(v) => out.push_str(&format!( + " pub const INITRD_START: Option = Some({:#010x});\n", + v + )), + None => out.push_str(" pub const INITRD_START: Option = None;\n"), + } + + match initrd_end { + Some(v) => out.push_str(&format!( + " pub const INITRD_END: Option = Some({:#010x});\n\n", + v + )), + None => out.push_str(" pub const INITRD_END: Option = None;\n\n"), + } + + out.push_str( + r#" // resolve stdout to its Peripheral directly via PERIPHERALS index + pub fn stdout() -> Option<&'static Peripheral> { + STDOUT.map(|idx| &PERIPHERALS[idx]) + } +} + +"#, + ); +} + +// ------------------------------------------------------------------------------------------------ +// Formatting helpers +// ------------------------------------------------------------------------------------------------ fn opt_u32(v: Option) -> String { match v { diff --git a/xtasks/crates/dtgen/src/ir.rs b/xtasks/crates/dtgen/src/ir.rs index f4d1222..7de1d5c 100644 --- a/xtasks/crates/dtgen/src/ir.rs +++ b/xtasks/crates/dtgen/src/ir.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // DTS object attribute types -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ #[derive(Debug, Clone)] pub enum PropValue { @@ -73,15 +73,15 @@ impl Node { } } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // Raw devicetree as output from parsing in-memory DTB -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ #[derive(Debug)] pub struct DeviceTree { pub nodes: Vec, pub by_phandle: HashMap, - pub by_name: HashMap, + pub by_name: HashMap>, pub root: usize, } @@ -130,16 +130,4 @@ impl DeviceTree { .cloned() .unwrap_or_else(|| "unknown".to_string()) } - - // resolve stdout-path in /chosen to the first compatible string of that node. - pub fn stdout_compat(&self) -> Option { - let chosen_idx = *self.by_name.get("chosen")?; - let path = self.nodes[chosen_idx].extra_str("stdout-path")?.to_string(); - // strip optional baud suffix: "/soc/serial@deadbeef:115200" -> "/soc/serial@deadbeef" - let path = path.split(':').next()?; - // match by last path component - let name = path.split('/').last()?; - let idx = self.by_name.get(name)?; - self.nodes[*idx].compatible.first().cloned() - } } diff --git a/xtasks/crates/dtgen/src/parser.rs b/xtasks/crates/dtgen/src/parser.rs index bcd104c..c602370 100644 --- a/xtasks/crates/dtgen/src/parser.rs +++ b/xtasks/crates/dtgen/src/parser.rs @@ -1,16 +1,26 @@ use crate::ir::{DeviceTree, Node, PropValue}; use std::collections::HashMap; -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // DTB construction from compiling DTS -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ pub fn dts_to_dtb( dts_path: &std::path::Path, include_dirs: &[&std::path::Path], ) -> Result, String> { - let preprocessed_path = dts_path.with_extension("preprocessed.dts"); - let dtb_path = dts_path.with_extension("dtb"); + let out_dir = std::env::var_os("OUT_DIR") + .map(std::path::PathBuf::from) + .unwrap_or_else(std::env::temp_dir); + let base_name = dts_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("dtgen"); + let suffix = std::process::id(); + + // emit files in spefic build dir / or default back to temp dir + let preprocessed_path = out_dir.join(format!("{base_name}.{suffix}.preprocessed.dts")); + let dtb_path = out_dir.join(format!("{base_name}.{suffix}.dtb")); // stage 1 - preprocessing // -E: preprocess only @@ -43,22 +53,25 @@ pub fn dts_to_dtb( .arg("-o") .arg(&dtb_path) .arg(&preprocessed_path); - let dtc_status = dtc_cmd - .status() - .map_err(|e| { - format!("dtc not found: {e}. Install with: apt install device-tree-compiler") - })?; + + let dtc_status = dtc_cmd.status().map_err(|e| { + format!("dtc not found: {e}. Install with: apt install device-tree-compiler") + })?; if !dtc_status.success() { return Err("dtc failed".to_string()); } - std::fs::read(&dtb_path).map_err(|e| format!("cannot read DTB: {e}")) + let dtb_bytes = std::fs::read(&dtb_path).map_err(|e| format!("cannot read DTB: {e}"))?; + let _ = std::fs::remove_file(&preprocessed_path); + let _ = std::fs::remove_file(&dtb_path); + + Ok(dtb_bytes) } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // DeviceTree construction from walk through DTB in-memory blob via FDT crate -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ pub fn dtb_to_devicetree(dtb: &[u8]) -> Result { let fdt = fdt::Fdt::new(dtb).map_err(|e| format!("fdt parse error: {e}"))?; @@ -104,8 +117,8 @@ fn walk<'a>( .property("interrupts") .map(|p| { p.value - .chunks(4) - .map(|b| u32::from_be_bytes(b.try_into().unwrap())) + .chunks_exact(4) + .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])) .collect() }) .unwrap_or_default(); @@ -144,7 +157,9 @@ fn walk<'a>( if let Some(ph) = phandle { tree.by_phandle.insert(ph, idx); } - tree.by_name.insert(name, idx); + + // names are only unique compared to their siblings of the same subtree + tree.by_name.entry(name).or_default().push(idx); for child in node.children() { let child_idx = walk(child, Some(idx), tree, child_addr_cells, child_size_cells); @@ -154,9 +169,9 @@ fn walk<'a>( idx } -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ // Helpers -// ================================================================================================ +// ------------------------------------------------------------------------------------------------ fn read_cell_count<'a>(node: &fdt::node::FdtNode<'a, '_>, prop: &str) -> Option { node.property(prop) @@ -199,14 +214,17 @@ fn parse_prop_value(bytes: &[u8]) -> PropValue { if bytes.is_empty() { return PropValue::Empty; } - if bytes.last() == Some(&0) { - let is_printable_ascii = bytes[..bytes.len() - 1] - .iter() - .all(|&b| b == 0 || (b >= 0x20 && b <= 0x7e)); + let inner = &bytes[..bytes.len() - 1]; + + // the device tree specification states the following constraints for valid strings + let is_printable = inner.iter().all(|&b| (0x20..=0x7e).contains(&b) || b == 0); + let has_printable = inner.iter().any(|&b| (0x20..=0x7e).contains(&b)); + let no_leading_null = !inner.starts_with(&[0]); + let no_consecutive_nulls = !inner.windows(2).any(|w| w == [0, 0]); - if is_printable_ascii { - let s = std::str::from_utf8(&bytes[..bytes.len() - 1]).unwrap(); + if is_printable && has_printable && no_leading_null && no_consecutive_nulls { + let s = std::str::from_utf8(inner).unwrap(); let parts: Vec<&str> = s.split('\0').collect(); return if parts.len() == 1 { PropValue::Str(parts[0].to_string()) @@ -215,7 +233,6 @@ fn parse_prop_value(bytes: &[u8]) -> PropValue { }; } } - if bytes.len().is_multiple_of(4) { let words: Vec = bytes .chunks(4) @@ -227,6 +244,5 @@ fn parse_prop_value(bytes: &[u8]) -> PropValue { PropValue::U32Array(words) }; } - PropValue::Bytes(bytes.to_vec()) }