From 1caf2709111c41c14b8cdc153ddc51f7d50702a3 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Mon, 16 Mar 2026 20:43:56 +0100 Subject: [PATCH 01/21] [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 02/21] 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 03/21] [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 04/21] [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()) } From 30e155fe50dc6f81f4f1e2c86dc8904439fd2f3b Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 22 Mar 2026 14:58:59 +0100 Subject: [PATCH 05/21] [Fix] chosen node * since some projects do not tend to adhere to the device tree specifications it is needed to support the cdoe generation of additional non-standard properties which could be contained in the chosen node * let initrd attributes be also handled by the non-standard properties handler since these are linux specific and not default properties defined by the device tree standard --- xtasks/crates/dtgen/src/codegen.rs | 99 +++++++++++++++++------------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index c870c45..ea78a35 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -278,7 +278,7 @@ fn emit_peripherals(out: &mut String, dt: &DeviceTree) { 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", + " // {i} - node {idx}: {:?}\n Peripheral {{ node: {idx}, compatible: {compat_inline}, reg: {reg}, interrupts: {irqs}, phandle: {phandle}, props: PERIPH_{i}_PROPS }},\n", node.name, )); } @@ -506,10 +506,6 @@ fn emit_memory_module(out: &mut String, dt: &DeviceTree) { out.push_str("}\n"); } -// ------------------------------------------------------------------------------------------------ -// Chosen module -// ------------------------------------------------------------------------------------------------ - fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { let chosen = dt .by_name @@ -517,9 +513,6 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { .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() { @@ -527,6 +520,10 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { } }); + // -------------------------------------------------------------------------------------------- + // Standard properties + // -------------------------------------------------------------------------------------------- + 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); @@ -548,7 +545,6 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { }) }?; - // position in PERIPHERALS = position of node_idx in periph_indices periph_indices.iter().position(|&idx| idx == node_idx) } else { None @@ -563,27 +559,46 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { } }); - 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 - } - }); + // -------------------------------------------------------------------------------------------- + // Extra properties + // -------------------------------------------------------------------------------------------- - 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 - } - }); + const KNOWN: &[&str] = &["stdout-path", "bootargs"]; + let extras: Vec<(&str, &crate::ir::PropValue)> = chosen + .map(|n| { + let mut v: Vec<_> = n + .extra + .iter() + .filter(|(k, _)| !KNOWN.contains(&k.as_str())) + .map(|(k, v)| (k.as_str(), v)) + .collect(); + v.sort_by_key(|(k, _)| *k); + v + }) + .unwrap_or_default(); - out.push('\n'); + // -------------------------------------------------------------------------------------------- + // Emit + // -------------------------------------------------------------------------------------------- + out.push('\n'); out.push_str(&format!("// {}\n", "-".repeat(96))); out.push_str("// Chosen\n"); out.push_str(&format!("// {}\n\n", "-".repeat(96))); + + // U32Array and Bytes backing statics must live outside the mod block + for (i, (_, val)) in extras.iter().enumerate() { + match val { + crate::ir::PropValue::U32Array(arr) => { + out.push_str(&format!("static CHOSEN_EXTRA_{i}: &[u32] = &{arr:?};\n")); + } + crate::ir::PropValue::Bytes(arr) => { + out.push_str(&format!("static CHOSEN_EXTRA_{i}: &[u8] = &{arr:?};\n")); + } + _ => {} + } + } + out.push_str("pub mod chosen {\n"); out.push_str(" use super::*;\n\n"); @@ -591,9 +606,7 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { 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", - ), + None => out.push_str(" pub const STDOUT: Option = None;\n"), } match bootargs { @@ -604,22 +617,26 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { 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('\n'); + out.push_str(" // non-standard properties of the chosen node\n"); + out.push_str(" pub const EXTRAS: &[(&str, PropValue)] = &[\n"); + for (i, (key, val)) in extras.iter().enumerate() { + let expr = match val { + crate::ir::PropValue::Empty => "PropValue::Empty".to_string(), + crate::ir::PropValue::U32(v) => format!("PropValue::U32({v:#010x})"), + crate::ir::PropValue::U32Array(_) => { + format!("PropValue::U32Array(CHOSEN_EXTRA_{i}.to_vec())") + } + crate::ir::PropValue::Str(s) => format!("PropValue::Str({s:?})"), + crate::ir::PropValue::StringList(sl) => format!("PropValue::StringList(vec!{sl:?})"), + crate::ir::PropValue::Bytes(_) => { + format!("PropValue::Bytes(CHOSEN_EXTRA_{i}.to_vec())") + } + }; + out.push_str(&format!(" ({key:?}, {expr}),\n")); } + out.push_str(" ];\n\n"); out.push_str( r#" // resolve stdout to its Peripheral directly via PERIPHERALS index pub fn stdout() -> Option<&'static Peripheral> { From 517c27e7b0436b1fee13f4616a5630a56f962d24 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Mon, 23 Mar 2026 18:32:55 +0100 Subject: [PATCH 06/21] [Rewrite] * rewritten code generation to use quote, syn, prettyplease --- Cargo.lock | 4 + xtasks/crates/dtgen/Cargo.toml | 4 + xtasks/crates/dtgen/src/codegen.rs | 812 ++++++++++++++--------------- 3 files changed, 413 insertions(+), 407 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ca1696..f06e3f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,6 +529,10 @@ version = "0.1.0" dependencies = [ "clap", "fdt", + "prettyplease", + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/xtasks/crates/dtgen/Cargo.toml b/xtasks/crates/dtgen/Cargo.toml index 55fe54e..2cedf04 100644 --- a/xtasks/crates/dtgen/Cargo.toml +++ b/xtasks/crates/dtgen/Cargo.toml @@ -14,3 +14,7 @@ path = "src/main.rs" [dependencies] fdt = "0.1.5" clap = { version = "4", features = ["derive"] } +quote = "1" +proc-macro2 = "1" +prettyplease = "0.2" +syn = { version = "2", features = ["full"] } diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index ea78a35..0feae7c 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -1,17 +1,29 @@ use crate::ir::{DeviceTree, PropValue}; +use proc_macro2::TokenStream; +use quote::quote; 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_query_api(&mut out, dt); - emit_aliases_module(&mut out, dt); - emit_memory_module(&mut out, dt); - emit_chosen_module(&mut out, dt); + let segments: &[TokenStream] = &[ + emit_prop_value_type(), + emit_topology_type(), + emit_peripheral_type(), + emit_nodes(dt), + emit_peripherals(dt), + emit_query_api(), + emit_aliases_module(dt), + emit_memory_module(dt), + emit_chosen_module(dt), + ]; + + let mut out = emit_header(); + for tokens in segments { + let file: syn::File = syn::parse2(tokens.clone()).unwrap(); + + // concat each segment with empty line inbetween + out.push_str(&prettyplease::unparse(&file)); + out.push('\n'); + } + out } @@ -19,178 +31,178 @@ pub fn generate_rust(dt: &DeviceTree) -> String { // File header // ------------------------------------------------------------------------------------------------ -fn emit_header(out: &mut String) { - out.push_str( - r#"// ------------------------------------------------------------------------------------------------ +fn emit_header() -> String { + r#"// ------------------------------------------------------------------------------------------------ // AUTOGENERATED BY dtgen - DO NOT EDIT FILE // ------------------------------------------------------------------------------------------------ -"#, - ); +"# + .to_string() } // ------------------------------------------------------------------------------------------------ // Type defitions // ------------------------------------------------------------------------------------------------ -fn emit_prop_value_type(out: &mut String) { - out.push_str( - r#"#[allow(dead_code)] -#[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_prop_value_type() -> TokenStream { + quote! { + #[allow(dead_code)] + #[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#"#[allow(dead_code)] -#[derive(Debug, Clone, Copy)] -pub struct TreeNode { - pub name: &'static str, - pub phandle: Option, - pub parent: Option, - pub children: &'static [usize], -} +fn emit_topology_type() -> TokenStream { + quote! { + #[allow(dead_code)] + #[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]) - } + impl TreeNode { + #[doc = "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])) + #[doc = "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#"#[allow(dead_code)] -#[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)], -} +fn emit_peripheral_type() -> TokenStream { + quote! { + #[allow(dead_code)] + #[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) - } + impl Peripheral { + #[doc = "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)) - } + #[doc = "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) - } + #[doc = "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) + } - // get a u32 prop - pub fn prop_u32(&self, key: &str) -> Option { - match self.prop(key) { - Some(PropValue::U32(v)) => Some(v), - _ => None, - } - } + #[doc = "get a u32 prop"] + pub fn prop_u32(&self, key: &str) -> Option { + match self.prop(key) { + Some(PropValue::U32(v)) => Some(v), + _ => None, + } + } - // 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, - } - } + #[doc = "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, + } + } - // 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, - } - } + #[doc = "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) - } + #[doc = "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) - } + #[doc = "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) - } + #[doc = "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] - } + #[doc = "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, + #[doc = "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, - )); +fn emit_nodes(dt: &DeviceTree) -> TokenStream { + let entries = dt.nodes.iter().map(|node| { + let name = node.name.as_str(); + let phandle = node + .phandle + .map_or(quote! { None }, |v| quote! { Some(#v) }); + let parent = node.parent.map_or(quote! { None }, |v| quote! { Some(#v) }); + let children = &node.children; + + quote! { + TreeNode { + name: #name, + phandle: #phandle, + parent: #parent, + children: &[#(#children),*], + }, + } + }); + + quote! { + pub const NODES: &[TreeNode] = &[ + #(#entries)* + ]; } - out.push_str("];\n\n"); } // ------------------------------------------------------------------------------------------------ // Peripherals // ------------------------------------------------------------------------------------------------ -fn emit_peripherals(out: &mut String, dt: &DeviceTree) { +fn emit_peripherals(dt: &DeviceTree) -> TokenStream { let mut indices: Vec = Vec::new(); dt.walk(|idx, node| { if !node.compatible.is_empty() { @@ -198,166 +210,184 @@ fn emit_peripherals(out: &mut String, dt: &DeviceTree) { } }); - // emit per-node prop sub-consts (only for types that cannot be inlined) + let mut backing_statics = TokenStream::new(); + let mut peripheral_entries = Vec::new(); + 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()); + // backing statics for non-inline prop types 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(", ") - )); + let ident = quote::format_ident!("PERIPH_{}_PROP_{}", i, j); + backing_statics.extend(quote! { + const #ident: &[u32] = &[#(#arr),*]; + }); } 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(", ") - )); + let ident = quote::format_ident!("PERIPH_{}_PROP_{}_STRS", i, j); + backing_statics.extend(quote! { + const #ident: &[&str] = &[#(#list),*]; + }); } 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(", ") - )); + let ident = quote::format_ident!("PERIPH_{}_PROP_{}_BYTES", i, j); + backing_statics.extend(quote! { + const #ident: &[u8] = &[#(#b),*]; + }); } _ => {} } } // 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), + let props_ident = quote::format_ident!("PERIPH_{}_PROPS", i); + let prop_entries = sorted_extra.iter().enumerate().map(|(j, (key, val))| { + let val_tokens = match val { + PropValue::Empty => quote! { PropValue::Empty }, + PropValue::U32(v) => quote! { PropValue::U32(#v) }, + PropValue::U32Array(_) => { + let ident = quote::format_ident!("PERIPH_{}_PROP_{}", i, j); + quote! { PropValue::U32Array(#ident) } + } + PropValue::Str(s) => quote! { PropValue::Str(#s) }, PropValue::StringList(_) => { - format!("PropValue::StringList(PERIPH_{i}_PROP_{j}_STRS)") + let ident = quote::format_ident!("PERIPH_{}_PROP_{}_STRS", i, j); + quote! { PropValue::StringList(#ident) } + } + PropValue::Bytes(_) => { + let ident = quote::format_ident!("PERIPH_{}_PROP_{}_BYTES", i, j); + quote! { PropValue::Bytes(#ident) } } - 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(), + quote! { (#key, #val_tokens), } + }); + + backing_statics.extend(quote! { + const #props_ident: &[(&str, PropValue)] = &[ + #(#prop_entries)* + ]; + }); + + // peripheral entry + let compat_strs = node.compatible.iter().map(|c| c.as_str()); + let reg_tokens = match node.reg { + Some((base, size)) => { + let base_lit = + syn::LitInt::new(&format!("{base:#010x}"), proc_macro2::Span::call_site()); + let size_lit = + syn::LitInt::new(&format!("{size:#x}"), proc_macro2::Span::call_site()); + + quote! { Some((#base_lit, #size_lit)) } + } + None => quote! { None }, }; - 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(", ")) + // automatic fallback to &[] on empty + let irqs = &node.interrupts; + let phandle_tokens = match node.phandle { + Some(v) => quote! { Some(#v) }, + None => quote! { None }, }; - 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, - )); + peripheral_entries.push(quote! { + Peripheral { + node: #idx, + compatible: &[#(#compat_strs),*], + reg: #reg_tokens, + interrupts: &[#(#irqs),*], + phandle: #phandle_tokens, + props: #props_ident, + }, + }); } - out.push_str("];\n\n"); -} + quote! { + #backing_statics + pub const PERIPHERALS: &[Peripheral] = &[ + #(#peripheral_entries)* + ]; + } +} // ------------------------------------------------------------------------------------------------ // Query API // ------------------------------------------------------------------------------------------------ -fn emit_query_api(out: &mut String, dt: &DeviceTree) { - out.push_str( - 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()) -} +fn emit_query_api() -> TokenStream { + quote! { + #[doc = "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 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()) -} + #[doc = "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 -// phandle values are unique -pub fn peripheral_by_phandle(ph: u32) -> Option<&'static Peripheral> { - PERIPHERALS.iter().find(|p| p.phandle == Some(ph)) -} + #[doc = "find a peripheral by its phandle value"] + #[doc = "ignores enabled status - phandle targets like clock providers may have no status prop"] + #[doc = "phandle values are unique"] + pub fn peripheral_by_phandle(p: u32) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|ph| p.phandle == Some(p)) + } -// 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) -} + #[doc = "find a peripheral by its NODES index"] + #[doc = "NODES indices are unique"] + pub fn peripheral_by_node(idx: usize) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| p.node == idx) + } -// 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) - }) -} + #[doc = "iterate all peripherals whose node name matches, with or without unit address"] + #[doc = "e.g. `channel` matches `channel@0`, `channel@1`, etc. across all parents"] + #[doc = "node names are only unique among siblings - use `peripheral_by_name_under` or"] + #[doc = "`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 - }) -} + #[doc = "find a peripheral by name scoped to a specific parent node index"] + #[doc = "safe form of name lookup since node names are only unique among siblings"] + 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; - } + #[doc = "find a peripheral by its full path from root, with or without unit addresses"] + #[doc = "e.g. `soc/i2c@40005400/lsm6dsl@6a` or `soc/i2c/lsm6dsl`"] + #[doc = "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; - peripheral_by_node(current) -} -"#, - ); + 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 { @@ -391,18 +421,17 @@ fn resolve_path(dt: &DeviceTree, path: &str) -> Option { // Alias module // ------------------------------------------------------------------------------------------------ -fn emit_aliases_module(out: &mut String, dt: &DeviceTree) { - let mut pairs: Vec<(String, usize)> = dt +fn emit_aliases_module(dt: &DeviceTree) -> TokenStream { + let pairs: Vec<(String, usize)> = dt .by_name .get("aliases") - .and_then(|indices| indices.first()) // there is usually only one /aliases node + .and_then(|indices| indices.first()) .map(|&idx| { dt.nodes[idx] .extra .iter() .filter_map(|(k, val)| { 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 @@ -412,52 +441,44 @@ fn emit_aliases_module(out: &mut String, dt: &DeviceTree) { }) .unwrap_or_default(); - // sort by alias name (e.g., serial0, serial1) for deterministic output + let mut pairs = pairs; 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))); + let entries = pairs.iter().map(|(name, idx)| { + quote! { (#name, #idx), } + }); - out.push_str("pub mod aliases {\n"); - out.push_str(" use super::*;\n\n"); + quote! { + pub mod aliases { + use super::*; - // 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)); - } + pub const ALIASES: &[(&str, usize)] = &[ + #(#entries)* + ]; - out.push_str(" ];\n\n"); - 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(|(_, idx)| peripheral_by_node(*idx)) + #[doc = "resolve an alias name to its Peripheral"] + pub fn resolve(alias: &str) -> Option<&'static Peripheral> { + ALIASES + .iter() + .find(|(k, _)| *k == alias) + .and_then(|(_, idx)| peripheral_by_node(*idx)) + } + } } -"#, - ); - - out.push_str("}\n"); } // ------------------------------------------------------------------------------------------------ // 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" +fn emit_memory_module(dt: &DeviceTree) -> TokenStream { let mut regions: Vec<(&str, u64, u64)> = dt .nodes .iter() .filter(|n| { n.extra .get("device_type") + // memory nodes attribute which must always exist like that .map(|v| matches!(v, crate::ir::PropValue::Str(s) if s == "memory")) .unwrap_or(false) }) @@ -466,47 +487,39 @@ fn emit_memory_module(out: &mut String, dt: &DeviceTree) { 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))); + let entries = regions.iter().map(|(name, base, size)| { + let base = *base as usize; + let size = *size as usize; + quote! { (#name, #base, #size), } + }); - out.push_str("pub mod memory {\n"); - out.push_str(" use super::*;\n\n"); - out.push_str(" pub const REGIONS: &[(&str, usize, usize)] = &[\n"); + quote! { + pub mod memory { + use super::*; - for (name, base, size) in ®ions { - out.push_str(&format!( - " ({:?}, {:#010x}, {:#010x}),\n", - name, base, size - )); - } + pub const REGIONS: &[(&str, usize, usize)] = &[ + #(#entries)* + ]; - 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)) - } + #[doc = "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() + #[doc = "total memory in bytes across all declared regions"] + pub fn total_bytes() -> usize { + REGIONS.iter().map(|(_, _, size)| size).sum() + } + } } -"#, - ); - out.push_str("}\n"); } -fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { +fn emit_chosen_module(dt: &DeviceTree) -> TokenStream { let chosen = dt .by_name .get("chosen") @@ -520,14 +533,9 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { } }); - // -------------------------------------------------------------------------------------------- - // Standard properties - // -------------------------------------------------------------------------------------------- - 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 { @@ -544,7 +552,6 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { } }) }?; - periph_indices.iter().position(|&idx| idx == node_idx) } else { None @@ -559,10 +566,6 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { } }); - // -------------------------------------------------------------------------------------------- - // Extra properties - // -------------------------------------------------------------------------------------------- - const KNOWN: &[&str] = &["stdout-path", "bootargs"]; let extras: Vec<(&str, &crate::ir::PropValue)> = chosen .map(|n| { @@ -577,91 +580,86 @@ fn emit_chosen_module(out: &mut String, dt: &DeviceTree) { }) .unwrap_or_default(); - // -------------------------------------------------------------------------------------------- - // Emit - // -------------------------------------------------------------------------------------------- - - out.push('\n'); - out.push_str(&format!("// {}\n", "-".repeat(96))); - out.push_str("// Chosen\n"); - out.push_str(&format!("// {}\n\n", "-".repeat(96))); - - // U32Array and Bytes backing statics must live outside the mod block - for (i, (_, val)) in extras.iter().enumerate() { - match val { + // backing statics for array types that can't be expressed inline + let backing_statics = extras + .iter() + .enumerate() + .filter_map(|(i, (_, val))| match val { crate::ir::PropValue::U32Array(arr) => { - out.push_str(&format!("static CHOSEN_EXTRA_{i}: &[u32] = &{arr:?};\n")); + let ident = quote::format_ident!("CHOSEN_EXTRA_{}", i); + Some(quote! { + static #ident: &[u32] = &[#(#arr),*]; + }) } crate::ir::PropValue::Bytes(arr) => { - out.push_str(&format!("static CHOSEN_EXTRA_{i}: &[u8] = &{arr:?};\n")); + let ident = quote::format_ident!("CHOSEN_EXTRA_{}", i); + Some(quote! { + static #ident: &[u8] = &[#(#arr),*]; + }) + } + _ => None, + }); + + let stdout_tokens = match stdout_periph_idx { + Some(idx) => quote! { + #[doc = "index into PERIPHERALS of the stdout peripheral, resolved from stdout-path"] + pub const STDOUT: Option = Some(#idx); + }, + None => quote! { + pub const STDOUT: Option = None; + }, + }; + + let bootargs_tokens = match bootargs { + Some(s) => quote! { + pub const BOOTARGS: Option<&str> = Some(#s); + }, + None => quote! { + pub const BOOTARGS: Option<&str> = None; + }, + }; + + let extra_entries = extras.iter().enumerate().map(|(i, (key, val))| { + let val_tokens = match val { + crate::ir::PropValue::Empty => quote! { PropValue::Empty }, + crate::ir::PropValue::U32(v) => { + let lit = syn::LitInt::new(&format!("{v:#010x}"), proc_macro2::Span::call_site()); + quote! { PropValue::U32(#lit) } } - _ => {} - } - } - - 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(" 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"), - } - - out.push('\n'); - out.push_str(" // non-standard properties of the chosen node\n"); - out.push_str(" pub const EXTRAS: &[(&str, PropValue)] = &[\n"); - for (i, (key, val)) in extras.iter().enumerate() { - let expr = match val { - crate::ir::PropValue::Empty => "PropValue::Empty".to_string(), - crate::ir::PropValue::U32(v) => format!("PropValue::U32({v:#010x})"), crate::ir::PropValue::U32Array(_) => { - format!("PropValue::U32Array(CHOSEN_EXTRA_{i}.to_vec())") + let ident = quote::format_ident!("CHOSEN_EXTRA_{}", i); + quote! { PropValue::U32Array(#ident.to_vec()) } + } + crate::ir::PropValue::Str(s) => quote! { PropValue::Str(#s) }, + crate::ir::PropValue::StringList(sl) => { + quote! { PropValue::StringList(vec![#(#sl),*]) } } - crate::ir::PropValue::Str(s) => format!("PropValue::Str({s:?})"), - crate::ir::PropValue::StringList(sl) => format!("PropValue::StringList(vec!{sl:?})"), crate::ir::PropValue::Bytes(_) => { - format!("PropValue::Bytes(CHOSEN_EXTRA_{i}.to_vec())") + let ident = quote::format_ident!("CHOSEN_EXTRA_{}", i); + quote! { PropValue::Bytes(#ident.to_vec()) } } }; - out.push_str(&format!(" ({key:?}, {expr}),\n")); - } + quote! { (#key, #val_tokens), } + }); - out.push_str(" ];\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]) - } -} + quote! { + #(#backing_statics)* -"#, - ); -} + pub mod chosen { + use super::*; -// ------------------------------------------------------------------------------------------------ -// Formatting helpers -// ------------------------------------------------------------------------------------------------ + #stdout_tokens + #bootargs_tokens -fn opt_u32(v: Option) -> String { - match v { - Some(n) => format!("Some({n})"), - None => "None".to_string(), - } -} + #[doc = "non-standard properties of the chosen node"] + pub const EXTRAS: &[(&str, PropValue)] = &[ + #(#extra_entries)* + ]; -fn opt_usize(v: Option) -> String { - match v { - Some(n) => format!("Some({n})"), - None => "None".to_string(), + #[doc = "resolve stdout to its Peripheral directly via PERIPHERALS index"] + pub fn stdout() -> Option<&'static Peripheral> { + STDOUT.map(|idx| &PERIPHERALS[idx]) + } + } } } From 2e114fd256aae52383287bf59bef08e1b855dc00 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Mon, 23 Mar 2026 20:07:44 +0100 Subject: [PATCH 07/21] [Fix] codegen * fixed parsing for memory nodes to correct properties * some dts providers might emit properties of memory nodes * more definite clues must be used such as naming pattern --- xtasks/crates/dtgen/src/codegen.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index 0feae7c..1e584cd 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -475,13 +475,7 @@ fn emit_memory_module(dt: &DeviceTree) -> TokenStream { let mut regions: Vec<(&str, u64, u64)> = dt .nodes .iter() - .filter(|n| { - n.extra - .get("device_type") - // memory nodes attribute which must always exist like that - .map(|v| matches!(v, crate::ir::PropValue::Str(s) if s == "memory")) - .unwrap_or(false) - }) + .filter(|n| n.name.starts_with("memory@") || n.name == "memory") .filter_map(|n| { let (base, size) = n.reg?; Some((n.name.as_str(), base, size)) @@ -492,6 +486,7 @@ fn emit_memory_module(dt: &DeviceTree) -> TokenStream { let entries = regions.iter().map(|(name, base, size)| { let base = *base as usize; let size = *size as usize; + quote! { (#name, #base, #size), } }); @@ -499,6 +494,7 @@ fn emit_memory_module(dt: &DeviceTree) -> TokenStream { pub mod memory { use super::*; + #[doc = "physical memory regions"] pub const REGIONS: &[(&str, usize, usize)] = &[ #(#entries)* ]; From eb75e0032fe173d8dff1aa2ea6a2fe9f96d66be7 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Mon, 23 Mar 2026 20:29:30 +0100 Subject: [PATCH 08/21] [Rewrite] * put code generation inside additional module for namespacing purposes --- xtasks/crates/dtgen/src/codegen.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index 1e584cd..dbb931f 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -15,15 +15,16 @@ pub fn generate_rust(dt: &DeviceTree) -> String { emit_chosen_module(dt), ]; - let mut out = emit_header(); - for tokens in segments { - let file: syn::File = syn::parse2(tokens.clone()).unwrap(); - - // concat each segment with empty line inbetween - out.push_str(&prettyplease::unparse(&file)); - out.push('\n'); - } + let combined = segments.iter().cloned().collect::(); + let wrapped = quote! { + pub mod device_tree { + #combined + } + }; + let file: syn::File = syn::parse2(wrapped).unwrap(); + let mut out = emit_header(); + out.push_str(&prettyplease::unparse(&file)); out } From 26de47d49eaa16660ed01e059cd94e64c25e2522 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Thu, 26 Mar 2026 18:35:37 +0100 Subject: [PATCH 09/21] [WIP] dtgen integration * rewritten memory allocator to use memory regions specified by the codegen file generated by dtgen --- src/lib.rs | 3 +- src/mem.rs | 94 ++++++++++++++++++++++-------------------------------- 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 844751b..cb40e60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub mod uspace; use hal::Machinelike; use interface::BootInfo; include!(concat!(env!("OUT_DIR"), "/syscalls_export.rs")); +include!(concat!(env!("OUT_DIR"), "/device_tree.rs")); /// The kernel initialization function. /// @@ -44,7 +45,7 @@ pub unsafe extern "C" fn kernel_init(boot_info: *const BootInfo) -> ! { print::print_header(); // Initialize the memory allocator. - if let Err(e) = mem::init_memory(boot_info) { + if let Err(e) = mem::init_memory(&device_tree::memory::REGIONS) { panic!("[Kernel] Error: failed to initialize memory allocator. Error: {e:?}"); } diff --git a/src/mem.rs b/src/mem.rs index e38b3a2..612649e 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -35,19 +35,16 @@ static GLOBAL_ALLOCATOR: SpinLocked = /// Initialize the memory allocator. /// -/// `boot_info` - The boot information. This contains the memory map. +/// `regions` - The memory node module of device tree codegen file. /// /// Returns an error if the memory allocator could not be initialized. -pub fn init_memory(boot_info: &BootInfo) -> Result<(), utils::KernelError> { +pub fn init_memory(regions: &[(&str, usize, usize)]) -> Result<(), utils::KernelError> { let mut allocator = GLOBAL_ALLOCATOR.lock(); - for entry in boot_info.mmap.iter().take(boot_info.mmap_len as usize) { - // We only add available memory to the allocator. - if entry.ty == MemoryTypes::Available as u32 { - let range = entry.addr as usize..(entry.addr + entry.length) as usize; - unsafe { - allocator.add_range(range)?; - } + for &(_, base, size) in regions { + let range = base..base + size; + unsafe { + allocator.add_range(range)?; } } @@ -92,13 +89,11 @@ pub fn align_up(size: usize) -> usize { (size + align - 1) & !(align - 1) } -// VERIFICATION ------------------------------------------------------------------------------------------------------- +// VERIFICATION ----------------------------------------------------------------------------------- #[cfg(kani)] mod verification { - use crate::mem::alloc::MAX_ADDR; - use super::*; - use interface::{Args, InitDescriptor, MemMapEntry}; + use crate::mem::alloc::MAX_ADDR; fn mock_ptr_write(dst: *mut T, src: T) { // Just a noop @@ -107,59 +102,46 @@ mod verification { #[kani::proof] #[kani::stub(core::ptr::write, mock_ptr_write)] fn proof_init_allocator_good() { - let mmap: [MemMapEntry; _] = kani::any(); - - kani::assume(mmap.len() > 0 && mmap.len() <= 8); - // Apply constraints to all - for entry in mmap.iter() { - // Ensure aligned. - kani::assume(entry.addr % align_of::() as u64 == 0); - // Ensure valid range. - kani::assume(entry.addr > 0); - kani::assume(entry.length > 0); - + const MAX_REGIONS: usize = 8; + let regions: [(&str, usize, usize); MAX_REGIONS] = + core::array::from_fn(|i| ("dummy", kani::any(), kani::any())); + + // contrain all regions + for &(_, base, size) in regions.iter() { + kani::assume(base % align_of::() as u64 == 0); + kani::assume(base > 0); + kani::assume(size > 0); kani::assume( - entry.length < alloc::MAX_ADDR as u64 - && entry.length > alloc::BestFitAllocator::MIN_RANGE_SIZE as u64, + size < alloc::MAX_ADDR as u64 + && size > alloc::BestFitAllocator::MIN_RANGE_SIZE as u64, ); - kani::assume(entry.addr < alloc::MAX_ADDR as u64 - entry.length && entry.addr > 0); - } - - for entry in mmap.iter() { - // Ensure non overlapping entries - for other in mmap.iter() { - if entry.addr != other.addr { - kani::assume( - entry.addr + entry.length <= other.addr - || other.addr + other.length <= entry.addr, - ); - } - } + kani::assume(base < alloc::MAX_ADDR as u64 - size); } - let mmap_len = mmap.len() as u64; - - let boot_info = BootInfo { - magic: interface::BOOT_INFO_MAGIC, - version: kani::any(), - mmap, - mmap_len, - args: Args { - init: InitDescriptor { - begin: kani::any(), - len: kani::any(), - entry_offset: kani::any(), - }, - }, - }; - - assert!(init_memory(&boot_info).is_ok()); + // for any i, j, i != j as indices into the memory regions the following should hold + let i: usize = kani::any(); + let j: usize = kani::any(); + kani::assume(i < MAX_REGIONS); + kani::assume(j < MAX_REGIONS); + kani::assume(i != j); + + /// non-overlapping regions + let (base_i, size_i) = regions[i]; + let (base_j, size_j) = regions[j]; + kani::assert( + base_i + size_i <= base_j || base_j + size_j <= base_i, + "memory regions should not overlap", + ); + + // verify memory init + assert!(init_memory(®ions).is_ok()); } #[kani::proof] fn check_align_up() { let size = kani::any(); kani::assume(size > 0); + let align = align_up(size); assert_ne!(align, 0); From 5ac9bdac0d39574347d4be45e199275a986da28a Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Thu, 26 Mar 2026 19:24:27 +0100 Subject: [PATCH 10/21] [Fix] typo --- xtasks/crates/dtgen/src/codegen.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index dbb931f..644a684 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -334,8 +334,8 @@ fn emit_query_api() -> TokenStream { #[doc = "find a peripheral by its phandle value"] #[doc = "ignores enabled status - phandle targets like clock providers may have no status prop"] #[doc = "phandle values are unique"] - pub fn peripheral_by_phandle(p: u32) -> Option<&'static Peripheral> { - PERIPHERALS.iter().find(|ph| p.phandle == Some(p)) + pub fn peripheral_by_phandle(ph: u32) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| p.phandle == Some(ph)) } #[doc = "find a peripheral by its NODES index"] From 0217888bb3774f0043c560ec5bd55b1761397578 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Thu, 26 Mar 2026 19:35:33 +0100 Subject: [PATCH 11/21] [Fix] kani --- src/mem.rs | 18 +++++------------- xtasks/crates/dtgen/src/codegen.rs | 1 + 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/mem.rs b/src/mem.rs index 612649e..693ebf9 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -95,12 +95,7 @@ mod verification { use super::*; use crate::mem::alloc::MAX_ADDR; - fn mock_ptr_write(dst: *mut T, src: T) { - // Just a noop - } - #[kani::proof] - #[kani::stub(core::ptr::write, mock_ptr_write)] fn proof_init_allocator_good() { const MAX_REGIONS: usize = 8; let regions: [(&str, usize, usize); MAX_REGIONS] = @@ -108,14 +103,11 @@ mod verification { // contrain all regions for &(_, base, size) in regions.iter() { - kani::assume(base % align_of::() as u64 == 0); + kani::assume(base % align_of::() == 0); kani::assume(base > 0); kani::assume(size > 0); - kani::assume( - size < alloc::MAX_ADDR as u64 - && size > alloc::BestFitAllocator::MIN_RANGE_SIZE as u64, - ); - kani::assume(base < alloc::MAX_ADDR as u64 - size); + kani::assume(size < alloc::MAX_ADDR && size > alloc::BestFitAllocator::MIN_RANGE_SIZE); + kani::assume(base < alloc::MAX_ADDR - size); } // for any i, j, i != j as indices into the memory regions the following should hold @@ -126,8 +118,8 @@ mod verification { kani::assume(i != j); /// non-overlapping regions - let (base_i, size_i) = regions[i]; - let (base_j, size_j) = regions[j]; + let (_, base_i, size_i) = regions[i]; + let (_, base_j, size_j) = regions[j]; kani::assert( base_i + size_i <= base_j || base_j + size_j <= base_i, "memory regions should not overlap", diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index 644a684..a4cc047 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -315,6 +315,7 @@ fn emit_peripherals(dt: &DeviceTree) -> TokenStream { ]; } } + // ------------------------------------------------------------------------------------------------ // Query API // ------------------------------------------------------------------------------------------------ From f5240fe8d4d6f7333247b33676c56a7aa8238429 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sat, 28 Mar 2026 19:50:52 +0100 Subject: [PATCH 12/21] Adding dts files device build support --- .cargo/config.toml | 6 + Cargo.lock | 1 + Cargo.toml | 1 + .../nucleo_l4r5zi/arduino_r3_connector.dtsi | 44 ++++ boards/nucleo_l4r5zi/nucleo_l4r5zi.dts | 227 ++++++++++++++++++ build.rs | 71 +++++- options.toml | 6 + 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 boards/nucleo_l4r5zi/arduino_r3_connector.dtsi create mode 100644 boards/nucleo_l4r5zi/nucleo_l4r5zi.dts diff --git a/.cargo/config.toml b/.cargo/config.toml index b73ed2c..c72f066 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,12 @@ xtask = "--config xtasks/.cargo/config.toml run -p xtask --release --" [env] +OSIRIS_TUNING_DEVICETREE = "boards/nucleo_l4r5zi/nucleo_l4r5zi.dts" +OSIRIS_TUNING_APPMEMSIZE = "8192" +OSIRIS_TUNING_ENABLEFPU = "false" +OSIRIS_DEBUG_RUNTIMESYMBOLS = "false" +OSIRIS_TUNING_APPSTACKSIZE = "2048" +OSIRIS_DEBUG_UART = "LPUART1" [build] target = "host-tuple" diff --git a/Cargo.lock b/Cargo.lock index f06e3f1..933791c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1062,6 +1062,7 @@ dependencies = [ "bindgen 0.69.5", "cbindgen", "cfg_aliases", + "dtgen", "envparse", "hal-select", "hal-testing", diff --git a/Cargo.toml b/Cargo.toml index 3305670..cb3fff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ syn = "2.0.100" quote = "1.0.40" rand = "0.8.5" cfg_aliases = "0.2.1" +dtgen = { path = "xtasks/crates/dtgen" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } diff --git a/boards/nucleo_l4r5zi/arduino_r3_connector.dtsi b/boards/nucleo_l4r5zi/arduino_r3_connector.dtsi new file mode 100644 index 0000000..a2be3c9 --- /dev/null +++ b/boards/nucleo_l4r5zi/arduino_r3_connector.dtsi @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 Linaro Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +/ { + arduino_header: connector { + compatible = "arduino-header-r3"; + #gpio-cells = <2>; + gpio-map-mask = <0xffffffff 0xffffffc0>; + gpio-map-pass-thru = <0 0x3f>; + gpio-map = , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + }; +}; + +arduino_i2c: &i2c1 {}; + +arduino_spi: &spi1 {}; + +arduino_serial: &usart3 {}; diff --git a/boards/nucleo_l4r5zi/nucleo_l4r5zi.dts b/boards/nucleo_l4r5zi/nucleo_l4r5zi.dts new file mode 100644 index 0000000..c01ae37 --- /dev/null +++ b/boards/nucleo_l4r5zi/nucleo_l4r5zi.dts @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2018 Pushpal Sidhu + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/dts-v1/; +#include +#include +#include "arduino_r3_connector.dtsi" +#include + +/ { + model = "STMicroelectronics STM32L4R5ZI-NUCLEO board"; + compatible = "st,stm32l4r5zi-nucleo"; + + chosen { + zephyr,console = &lpuart1; + zephyr,shell-uart = &lpuart1; + zephyr,sram = &sram0; + zephyr,flash = &flash0; + }; + + leds: leds { + compatible = "gpio-leds"; + + green_led_0: led_0 { + gpios = <&gpioc 7 GPIO_ACTIVE_HIGH>; + label = "User LD1"; + }; + + blue_led_0: led_1 { + gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>; + label = "User LD2"; + }; + + red_led_0: led_2 { + gpios = <&gpiob 14 GPIO_ACTIVE_HIGH>; + label = "User LD3"; + }; + }; + + gpio_keys { + compatible = "gpio-keys"; + + user_button: button { + label = "User"; + gpios = <&gpioc 13 GPIO_ACTIVE_HIGH>; + zephyr,code = ; + }; + }; + + pwmleds: pwmleds { + compatible = "pwm-leds"; + /* NOTE: disabled by default, PWM1 conflicts with SPI2 */ + status = "disabled"; + + red_pwm_led: red_pwm_led { + pwms = <&pwm1 2 PWM_MSEC(20) + (PWM_POLARITY_NORMAL | STM32_PWM_COMPLEMENTARY)>; + }; + }; + + aliases { + led0 = &green_led_0; + led1 = &blue_led_0; + led2 = &red_led_0; + sw0 = &user_button; + pwm-led0 = &red_pwm_led; + die-temp0 = &die_temp; + volt-sensor0 = &vref; + volt-sensor1 = &vbat; + }; +}; + +&clk_lsi { + status = "okay"; +}; + +&clk_hsi { + status = "okay"; +}; + +&clk_hsi48 { + status = "okay"; +}; + +&pll { + div-m = <4>; + mul-n = <40>; + div-p = <7>; + div-q = <2>; + div-r = <2>; + clocks = <&clk_hsi>; + status = "okay"; +}; + +&rcc { + clocks = <&pll>; + clock-frequency = ; + ahb-prescaler = <1>; + apb1-prescaler = <1>; + apb2-prescaler = <1>; +}; + +&usart1 { + pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&usart2 { + pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&usart3 { + pinctrl-0 = <&usart3_tx_pd8 &usart3_rx_pd9>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&lpuart1 { + pinctrl-0 = <&lpuart1_tx_pg7 &lpuart1_rx_pg8>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&i2c1 { + pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>; + pinctrl-names = "default"; + status = "okay"; +}; + +&spi1 { + pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>; + pinctrl-names = "default"; + cs-gpios = <&gpiod 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; + status = "okay"; +}; + +&spi2 { + pinctrl-0 = <&spi2_nss_pb12 &spi2_sck_pb13 + &spi2_miso_pb14 &spi2_mosi_pb15>; + pinctrl-names = "default"; + status = "okay"; +}; + +&spi3 { + /* SPI3 on the ST Morpho Connector CN7 pins 17, 1, 2, 3*/ + pinctrl-0 = <&spi3_nss_pa15 &spi3_sck_pc10 + &spi3_miso_pc11 &spi3_mosi_pc12>; + pinctrl-names = "default"; + status = "okay"; +}; + +zephyr_udc0: &usbotg_fs { + pinctrl-0 = <&usb_otg_fs_dm_pa11 &usb_otg_fs_dp_pa12 + &usb_otg_fs_id_pa10>; + pinctrl-names = "default"; + status = "okay"; +}; + +&timers1 { + status = "okay"; + + pwm1: pwm { + /* NOTE: disabled by default, PWM1 conflicts with SPI2 */ + pinctrl-0 = <&tim1_ch2n_pb14>; + pinctrl-names = "default"; + }; +}; + +&timers2 { + status = "okay"; + + pwm2: pwm { + status = "okay"; + pinctrl-0 = <&tim2_ch1_pa0>; + pinctrl-names = "default"; + }; +}; + +&rtc { + clocks = <&rcc STM32_CLOCK(APB1, 28)>, + <&rcc STM32_SRC_LSI RTC_SEL(2)>; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* Reserve last 16KiB for property storage */ + storage_partition: partition@1fb000 { + label = "storage"; + reg = <0x001fb000 DT_SIZE_K(16)>; + }; + }; +}; + +&adc1 { + pinctrl-0 = <&adc1_in1_pc0>; + pinctrl-names = "default"; + st,adc-clock-source = "SYNC"; + st,adc-prescaler = <4>; + status = "okay"; +}; + +&die_temp { + status = "okay"; +}; + +&vref { + status = "okay"; +}; + +&vbat { + status = "okay"; +}; diff --git a/build.rs b/build.rs index a571440..bbbe592 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ -use std::{collections::HashMap, fs::File, path::Path}; +use std::process::Command; +use std::{collections::HashMap, fs::File, path::Path, path::PathBuf}; extern crate rand; extern crate syn; @@ -19,6 +20,8 @@ fn main() { generate_syscall_map("src/syscalls").expect("Failed to generate syscall map."); generate_syscalls_export("src/syscalls").expect("Failed to generate syscall exports."); + generate_device_tree().expect("Failed to generate device tree."); + // Get linker script from environment variable if let Ok(linker_script) = std::env::var("DEP_HAL_LINKER_SCRIPT") { println!("cargo::rustc-link-arg=-T{linker_script}"); @@ -31,6 +34,72 @@ fn main() { } } +fn generate_device_tree() -> Result<(), Box> { + let dts_path = std::env::var("OSIRIS_TUNING_DEVICETREE").expect("OSIRIS_DTS not set"); + let dts = Path::new(&dts_path); + + println!("cargo::rerun-if-changed={dts_path}"); + + // dependencies SoC/HAL/pins + let zephyr = Path::new("/tmp/zephyr"); + let hal_stm32 = Path::new("/tmp/hal_stm32"); + sparse_clone( + "https://github.com/zephyrproject-rtos/zephyr", + zephyr, + &["include", "dts", "boards"], + )?; + sparse_clone( + "https://github.com/zephyrproject-rtos/hal_stm32", + hal_stm32, + &["dts"], + )?; + + let out = Path::new(&std::env::var("OUT_DIR").unwrap()).join("device_tree.rs"); + let include_paths = [ + zephyr.join("include"), + zephyr.join("dts/arm/st"), + zephyr.join("dts"), + zephyr.join("dts/arm"), + zephyr.join("dts/common"), + zephyr.join("boards/st"), + hal_stm32.join("dts"), + ]; + let include_refs: Vec<&Path> = include_paths.iter().map(PathBuf::as_path).collect(); + + dtgen::run(dts, &include_refs, &out)?; + Ok(()) +} + +fn sparse_clone(url: &str, dest: &Path, paths: &[&str]) -> Result<(), Box> { + if dest.exists() { + return Ok(()); + } + + Command::new("git") + .args(["clone", "--filter=blob:none", "--no-checkout", url]) + .arg(dest) + .status()?; + + Command::new("git") + .args(["sparse-checkout", "init", "--cone"]) + .current_dir(dest) + .status()?; + + Command::new("git") + .arg("sparse-checkout") + .arg("set") + .args(paths) + .current_dir(dest) + .status()?; + + Command::new("git") + .args(["checkout"]) + .current_dir(dest) + .status()?; + + Ok(()) +} + fn generate_syscalls_export>(root: P) -> Result<(), std::io::Error> { let syscalls = collect_syscalls_export(root); diff --git a/options.toml b/options.toml index 985e288..bbea74b 100644 --- a/options.toml +++ b/options.toml @@ -37,3 +37,9 @@ name = "Application Stack Size" description = "Sets the size of the stack for the init application. This must be less than the application memory size." type = { type = "Integer", min = 0 } default = 2048 + +[tuning.devicetree] +name = "Device Tree Source" +description = "Path to the board device tree source file." +type = "String" +default = "boards/nucleo_l4r5zi/nucleo_l4r5zi.dts" From a5f5c7f17382638105f0e298555eacc21b92b898 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sat, 28 Mar 2026 20:03:50 +0100 Subject: [PATCH 13/21] Adding dts files device build support --- build.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index bbbe592..056d904 100644 --- a/build.rs +++ b/build.rs @@ -35,7 +35,8 @@ fn main() { } fn generate_device_tree() -> Result<(), Box> { - let dts_path = std::env::var("OSIRIS_TUNING_DEVICETREE").expect("OSIRIS_DTS not set"); + let dts_path = + std::env::var("OSIRIS_TUNING_DEVICETREE").expect("OSIRIS_TUNING_DEVICETREE not set"); let dts = Path::new(&dts_path); println!("cargo::rerun-if-changed={dts_path}"); From 103164ed632a64fa92abf0f211f378a91e23c7aa Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sat, 28 Mar 2026 20:28:44 +0100 Subject: [PATCH 14/21] Better design choice for reusing zephyrs device tree structure --- .cargo/config.toml | 2 +- .../nucleo_l4r5zi/arduino_r3_connector.dtsi | 44 ---- boards/nucleo_l4r5zi/nucleo_l4r5zi.dts | 227 ------------------ build.rs | 40 ++- options.toml | 8 +- 5 files changed, 35 insertions(+), 286 deletions(-) delete mode 100644 boards/nucleo_l4r5zi/arduino_r3_connector.dtsi delete mode 100644 boards/nucleo_l4r5zi/nucleo_l4r5zi.dts diff --git a/.cargo/config.toml b/.cargo/config.toml index c72f066..438a857 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,7 +3,7 @@ xtask = "--config xtasks/.cargo/config.toml run -p xtask --release --" [env] -OSIRIS_TUNING_DEVICETREE = "boards/nucleo_l4r5zi/nucleo_l4r5zi.dts" +OSIRIS_TUNING_BOARD = "nucleo_l4r5zi" OSIRIS_TUNING_APPMEMSIZE = "8192" OSIRIS_TUNING_ENABLEFPU = "false" OSIRIS_DEBUG_RUNTIMESYMBOLS = "false" diff --git a/boards/nucleo_l4r5zi/arduino_r3_connector.dtsi b/boards/nucleo_l4r5zi/arduino_r3_connector.dtsi deleted file mode 100644 index a2be3c9..0000000 --- a/boards/nucleo_l4r5zi/arduino_r3_connector.dtsi +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2019 Linaro Limited - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include - -/ { - arduino_header: connector { - compatible = "arduino-header-r3"; - #gpio-cells = <2>; - gpio-map-mask = <0xffffffff 0xffffffc0>; - gpio-map-pass-thru = <0 0x3f>; - gpio-map = , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - , - ; - }; -}; - -arduino_i2c: &i2c1 {}; - -arduino_spi: &spi1 {}; - -arduino_serial: &usart3 {}; diff --git a/boards/nucleo_l4r5zi/nucleo_l4r5zi.dts b/boards/nucleo_l4r5zi/nucleo_l4r5zi.dts deleted file mode 100644 index c01ae37..0000000 --- a/boards/nucleo_l4r5zi/nucleo_l4r5zi.dts +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2018 Pushpal Sidhu - * - * SPDX-License-Identifier: Apache-2.0 - */ - -/dts-v1/; -#include -#include -#include "arduino_r3_connector.dtsi" -#include - -/ { - model = "STMicroelectronics STM32L4R5ZI-NUCLEO board"; - compatible = "st,stm32l4r5zi-nucleo"; - - chosen { - zephyr,console = &lpuart1; - zephyr,shell-uart = &lpuart1; - zephyr,sram = &sram0; - zephyr,flash = &flash0; - }; - - leds: leds { - compatible = "gpio-leds"; - - green_led_0: led_0 { - gpios = <&gpioc 7 GPIO_ACTIVE_HIGH>; - label = "User LD1"; - }; - - blue_led_0: led_1 { - gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>; - label = "User LD2"; - }; - - red_led_0: led_2 { - gpios = <&gpiob 14 GPIO_ACTIVE_HIGH>; - label = "User LD3"; - }; - }; - - gpio_keys { - compatible = "gpio-keys"; - - user_button: button { - label = "User"; - gpios = <&gpioc 13 GPIO_ACTIVE_HIGH>; - zephyr,code = ; - }; - }; - - pwmleds: pwmleds { - compatible = "pwm-leds"; - /* NOTE: disabled by default, PWM1 conflicts with SPI2 */ - status = "disabled"; - - red_pwm_led: red_pwm_led { - pwms = <&pwm1 2 PWM_MSEC(20) - (PWM_POLARITY_NORMAL | STM32_PWM_COMPLEMENTARY)>; - }; - }; - - aliases { - led0 = &green_led_0; - led1 = &blue_led_0; - led2 = &red_led_0; - sw0 = &user_button; - pwm-led0 = &red_pwm_led; - die-temp0 = &die_temp; - volt-sensor0 = &vref; - volt-sensor1 = &vbat; - }; -}; - -&clk_lsi { - status = "okay"; -}; - -&clk_hsi { - status = "okay"; -}; - -&clk_hsi48 { - status = "okay"; -}; - -&pll { - div-m = <4>; - mul-n = <40>; - div-p = <7>; - div-q = <2>; - div-r = <2>; - clocks = <&clk_hsi>; - status = "okay"; -}; - -&rcc { - clocks = <&pll>; - clock-frequency = ; - ahb-prescaler = <1>; - apb1-prescaler = <1>; - apb2-prescaler = <1>; -}; - -&usart1 { - pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; - pinctrl-names = "default"; - current-speed = <115200>; - status = "okay"; -}; - -&usart2 { - pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; - pinctrl-names = "default"; - current-speed = <115200>; - status = "okay"; -}; - -&usart3 { - pinctrl-0 = <&usart3_tx_pd8 &usart3_rx_pd9>; - pinctrl-names = "default"; - current-speed = <115200>; - status = "okay"; -}; - -&lpuart1 { - pinctrl-0 = <&lpuart1_tx_pg7 &lpuart1_rx_pg8>; - pinctrl-names = "default"; - current-speed = <115200>; - status = "okay"; -}; - -&i2c1 { - pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>; - pinctrl-names = "default"; - status = "okay"; -}; - -&spi1 { - pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>; - pinctrl-names = "default"; - cs-gpios = <&gpiod 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; - status = "okay"; -}; - -&spi2 { - pinctrl-0 = <&spi2_nss_pb12 &spi2_sck_pb13 - &spi2_miso_pb14 &spi2_mosi_pb15>; - pinctrl-names = "default"; - status = "okay"; -}; - -&spi3 { - /* SPI3 on the ST Morpho Connector CN7 pins 17, 1, 2, 3*/ - pinctrl-0 = <&spi3_nss_pa15 &spi3_sck_pc10 - &spi3_miso_pc11 &spi3_mosi_pc12>; - pinctrl-names = "default"; - status = "okay"; -}; - -zephyr_udc0: &usbotg_fs { - pinctrl-0 = <&usb_otg_fs_dm_pa11 &usb_otg_fs_dp_pa12 - &usb_otg_fs_id_pa10>; - pinctrl-names = "default"; - status = "okay"; -}; - -&timers1 { - status = "okay"; - - pwm1: pwm { - /* NOTE: disabled by default, PWM1 conflicts with SPI2 */ - pinctrl-0 = <&tim1_ch2n_pb14>; - pinctrl-names = "default"; - }; -}; - -&timers2 { - status = "okay"; - - pwm2: pwm { - status = "okay"; - pinctrl-0 = <&tim2_ch1_pa0>; - pinctrl-names = "default"; - }; -}; - -&rtc { - clocks = <&rcc STM32_CLOCK(APB1, 28)>, - <&rcc STM32_SRC_LSI RTC_SEL(2)>; - status = "okay"; -}; - -&flash0 { - partitions { - compatible = "fixed-partitions"; - #address-cells = <1>; - #size-cells = <1>; - - /* Reserve last 16KiB for property storage */ - storage_partition: partition@1fb000 { - label = "storage"; - reg = <0x001fb000 DT_SIZE_K(16)>; - }; - }; -}; - -&adc1 { - pinctrl-0 = <&adc1_in1_pc0>; - pinctrl-names = "default"; - st,adc-clock-source = "SYNC"; - st,adc-prescaler = <4>; - status = "okay"; -}; - -&die_temp { - status = "okay"; -}; - -&vref { - status = "okay"; -}; - -&vbat { - status = "okay"; -}; diff --git a/build.rs b/build.rs index 056d904..6944e71 100644 --- a/build.rs +++ b/build.rs @@ -35,15 +35,23 @@ fn main() { } fn generate_device_tree() -> Result<(), Box> { - let dts_path = - std::env::var("OSIRIS_TUNING_DEVICETREE").expect("OSIRIS_TUNING_DEVICETREE not set"); - let dts = Path::new(&dts_path); - - println!("cargo::rerun-if-changed={dts_path}"); + let board = + std::env::var("OSIRIS_TUNING_BOARD").unwrap_or_else(|_| "nucleo_l4r5zi".to_string()); + println!("cargo::rerun-if-changed={board}"); // dependencies SoC/HAL/pins let zephyr = Path::new("/tmp/zephyr"); let hal_stm32 = Path::new("/tmp/hal_stm32"); + + // clean state + if zephyr.exists() { + std::fs::remove_dir_all(zephyr)?; + } + + if hal_stm32.exists() { + std::fs::remove_dir_all(hal_stm32)?; + } + sparse_clone( "https://github.com/zephyrproject-rtos/zephyr", zephyr, @@ -55,6 +63,7 @@ fn generate_device_tree() -> Result<(), Box> { &["dts"], )?; + let dts = find_board_dts(zephyr, &board)?; let out = Path::new(&std::env::var("OUT_DIR").unwrap()).join("device_tree.rs"); let include_paths = [ zephyr.join("include"), @@ -67,15 +76,11 @@ fn generate_device_tree() -> Result<(), Box> { ]; let include_refs: Vec<&Path> = include_paths.iter().map(PathBuf::as_path).collect(); - dtgen::run(dts, &include_refs, &out)?; + dtgen::run(&dts, &include_refs, &out)?; Ok(()) } fn sparse_clone(url: &str, dest: &Path, paths: &[&str]) -> Result<(), Box> { - if dest.exists() { - return Ok(()); - } - Command::new("git") .args(["clone", "--filter=blob:none", "--no-checkout", url]) .arg(dest) @@ -101,6 +106,21 @@ fn sparse_clone(url: &str, dest: &Path, paths: &[&str]) -> Result<(), Box Result> { + let boards_dir = zephyr.join("boards"); + for entry in walkdir::WalkDir::new(&boards_dir) { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("dts") + && path.file_stem().and_then(|s| s.to_str()) == Some(board) + { + return Ok(path.to_path_buf()); + } + } + + Err(format!("board '{board}' not found in {}", boards_dir.display()).into()) +} + fn generate_syscalls_export>(root: P) -> Result<(), std::io::Error> { let syscalls = collect_syscalls_export(root); diff --git a/options.toml b/options.toml index bbea74b..7bb1959 100644 --- a/options.toml +++ b/options.toml @@ -38,8 +38,8 @@ description = "Sets the size of the stack for the init application. This must be type = { type = "Integer", min = 0 } default = 2048 -[tuning.devicetree] -name = "Device Tree Source" -description = "Path to the board device tree source file." +[tuning.board] +name = "Targeted Board" +description = "Board name the device tree sources are searched for." type = "String" -default = "boards/nucleo_l4r5zi/nucleo_l4r5zi.dts" +default = "nucleo_l4r5zi" From b9e3748df88f0a8c25bdab4099513dab8a2c80c6 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 16:25:38 +0200 Subject: [PATCH 15/21] CI test --- xtasks/crates/dtgen/src/parser.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xtasks/crates/dtgen/src/parser.rs b/xtasks/crates/dtgen/src/parser.rs index c602370..cf2bb5c 100644 --- a/xtasks/crates/dtgen/src/parser.rs +++ b/xtasks/crates/dtgen/src/parser.rs @@ -43,6 +43,16 @@ pub fn dts_to_dtb( return Err("cpp preprocessing failed".to_string()); } + // test + let output = std::process::Command::new("which") + .arg("dtc") + .output() + .expect("failed to run 'which'"); + + println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + println!("status: {}", output.status); + // stage 2 - dts compilation let mut dtc_cmd = std::process::Command::new("dtc"); dtc_cmd From f05ba4d86903c763243bb3315dc51d15fa429fe0 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 16:31:09 +0200 Subject: [PATCH 16/21] CI test --- xtasks/crates/dtgen/src/parser.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/xtasks/crates/dtgen/src/parser.rs b/xtasks/crates/dtgen/src/parser.rs index cf2bb5c..58338fd 100644 --- a/xtasks/crates/dtgen/src/parser.rs +++ b/xtasks/crates/dtgen/src/parser.rs @@ -44,15 +44,13 @@ pub fn dts_to_dtb( } // test - let output = std::process::Command::new("which") - .arg("dtc") - .output() - .expect("failed to run 'which'"); - - println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); - println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); - println!("status: {}", output.status); - + match std::process::Command::new("dtc").arg("--version").output() { + Ok(out) => println!( + "cargo:warning=dtc found, version: {}", + String::from_utf8_lossy(&out.stdout).trim() + ), + Err(e) => println!("cargo:warning=dtc not found: {}", e), + } // stage 2 - dts compilation let mut dtc_cmd = std::process::Command::new("dtc"); dtc_cmd From 5d2344c4fac12a87a9fe0b7147d63380e2dabdcc Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 17:34:22 +0200 Subject: [PATCH 17/21] boards toplevel dir should contain toplevel dts of targeted board; toml options, aswell as build.rs resembles this change --- .cargo/config.toml | 2 +- boards/nucleo_l4r5zi.dts | 235 ++++++++++++++++++++++++++++++ build.rs | 28 +--- options.toml | 8 +- xtasks/crates/dtgen/src/parser.rs | 8 - 5 files changed, 247 insertions(+), 34 deletions(-) create mode 100644 boards/nucleo_l4r5zi.dts diff --git a/.cargo/config.toml b/.cargo/config.toml index 438a857..61ec090 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,7 +3,7 @@ xtask = "--config xtasks/.cargo/config.toml run -p xtask --release --" [env] -OSIRIS_TUNING_BOARD = "nucleo_l4r5zi" +OSIRIS_TUNING_DTS = "nucleo_l4r5zi.dts" OSIRIS_TUNING_APPMEMSIZE = "8192" OSIRIS_TUNING_ENABLEFPU = "false" OSIRIS_DEBUG_RUNTIMESYMBOLS = "false" diff --git a/boards/nucleo_l4r5zi.dts b/boards/nucleo_l4r5zi.dts new file mode 100644 index 0000000..c90de1f --- /dev/null +++ b/boards/nucleo_l4r5zi.dts @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2018 Pushpal Sidhu + * + * SPDX-License-Identifier: Apache-2.0 + * + * Modified for OsirisRTOS: removed Zephyr-specific nodes and macros, + * stripped arduino_r3_connector include. + */ + +/dts-v1/; +#include +#include +#include + +/ { + model = "STMicroelectronics STM32L4R5ZI-NUCLEO board"; + compatible = "st,stm32l4r5zi-nucleo"; + + chosen { + /delete-property/ zephyr,crc; + /delete-property/ zephyr,entropy; + /delete-property/ zephyr,flash-controller; + + osiris,console = &lpuart1; + osiris,shell-uart = &lpuart1; + osiris,sram = &sram0; + osiris,flash = &flash0; + osiris,crc = &crc; + osiris,entropy = &rng; + }; + + leds: leds { + compatible = "gpio-leds"; + + green_led_0: led_0 { + gpios = <&gpioc 7 GPIO_ACTIVE_HIGH>; + label = "User LD1"; + }; + + blue_led_0: led_1 { + gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>; + label = "User LD2"; + }; + + red_led_0: led_2 { + gpios = <&gpiob 14 GPIO_ACTIVE_HIGH>; + label = "User LD3"; + }; + }; + + gpio_keys { + compatible = "gpio-keys"; + + user_button: button { + label = "User"; + gpios = <&gpioc 13 GPIO_ACTIVE_HIGH>; + osiris,code = ; + }; + }; + + pwmleds: pwmleds { + compatible = "pwm-leds"; + /* NOTE: disabled by default, PWM1 conflicts with SPI2 */ + status = "disabled"; + + red_pwm_led: red_pwm_led { + pwms = <&pwm1 2 PWM_MSEC(20) + (PWM_POLARITY_NORMAL | STM32_PWM_COMPLEMENTARY)>; + }; + }; + + aliases { + led0 = &green_led_0; + led1 = &blue_led_0; + led2 = &red_led_0; + sw0 = &user_button; + pwm-led0 = &red_pwm_led; + die-temp0 = &die_temp; + volt-sensor0 = &vref; + volt-sensor1 = &vbat; + }; +}; + +&clk_lsi { + status = "okay"; +}; + +&clk_hsi { + status = "okay"; +}; + +&clk_hsi48 { + status = "okay"; +}; + +&pll { + div-m = <4>; + mul-n = <40>; + div-p = <7>; + div-q = <2>; + div-r = <2>; + clocks = <&clk_hsi>; + status = "okay"; +}; + +&rcc { + clocks = <&pll>; + clock-frequency = ; + ahb-prescaler = <1>; + apb1-prescaler = <1>; + apb2-prescaler = <1>; +}; + +&usart1 { + pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&usart2 { + pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&usart3 { + pinctrl-0 = <&usart3_tx_pd8 &usart3_rx_pd9>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&lpuart1 { + pinctrl-0 = <&lpuart1_tx_pg7 &lpuart1_rx_pg8>; + pinctrl-names = "default"; + current-speed = <115200>; + status = "okay"; +}; + +&i2c1 { + pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>; + pinctrl-names = "default"; + status = "okay"; +}; + +&spi1 { + pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>; + pinctrl-names = "default"; + cs-gpios = <&gpiod 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; + status = "okay"; +}; + +&spi2 { + pinctrl-0 = <&spi2_nss_pb12 &spi2_sck_pb13 + &spi2_miso_pb14 &spi2_mosi_pb15>; + pinctrl-names = "default"; + status = "okay"; +}; + +&spi3 { + /* SPI3 on the ST Morpho Connector CN7 pins 17, 1, 2, 3*/ + pinctrl-0 = <&spi3_nss_pa15 &spi3_sck_pc10 + &spi3_miso_pc11 &spi3_mosi_pc12>; + pinctrl-names = "default"; + status = "okay"; +}; + +osiris_udc0: &usbotg_fs { + pinctrl-0 = <&usb_otg_fs_dm_pa11 &usb_otg_fs_dp_pa12 + &usb_otg_fs_id_pa10>; + pinctrl-names = "default"; + status = "okay"; +}; + +&timers1 { + status = "okay"; + + pwm1: pwm { + /* NOTE: disabled by default, PWM1 conflicts with SPI2 */ + pinctrl-0 = <&tim1_ch2n_pb14>; + pinctrl-names = "default"; + }; +}; + +&timers2 { + status = "okay"; + + pwm2: pwm { + status = "okay"; + pinctrl-0 = <&tim2_ch1_pa0>; + pinctrl-names = "default"; + }; +}; + +&rtc { + clocks = <&rcc STM32_CLOCK(APB1, 28)>, + <&rcc STM32_SRC_LSI RTC_SEL(2)>; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* Reserve last 16KiB for property storage */ + storage_partition: partition@1fb000 { + label = "storage"; + reg = <0x001fb000 DT_SIZE_K(16)>; + }; + }; +}; + +&adc1 { + pinctrl-0 = <&adc1_in1_pc0>; + pinctrl-names = "default"; + st,adc-clock-source = "SYNC"; + st,adc-prescaler = <4>; + status = "okay"; +}; + +&die_temp { + status = "okay"; +}; + +&vref { + status = "okay"; +}; + +&vbat { + status = "okay"; +}; diff --git a/build.rs b/build.rs index 6944e71..6399964 100644 --- a/build.rs +++ b/build.rs @@ -35,9 +35,11 @@ fn main() { } fn generate_device_tree() -> Result<(), Box> { - let board = - std::env::var("OSIRIS_TUNING_BOARD").unwrap_or_else(|_| "nucleo_l4r5zi".to_string()); - println!("cargo::rerun-if-changed={board}"); + let dts = + std::env::var("OSIRIS_TUNING_DTS").unwrap_or_else(|_| "nucleo_l4r5zi.dts".to_string()); + println!("cargo::rerun-if-changed={dts}"); + + let dts_path = std::path::Path::new("boards").join(dts); // dependencies SoC/HAL/pins let zephyr = Path::new("/tmp/zephyr"); @@ -55,7 +57,7 @@ fn generate_device_tree() -> Result<(), Box> { sparse_clone( "https://github.com/zephyrproject-rtos/zephyr", zephyr, - &["include", "dts", "boards"], + &["include", "dts"], )?; sparse_clone( "https://github.com/zephyrproject-rtos/hal_stm32", @@ -63,7 +65,6 @@ fn generate_device_tree() -> Result<(), Box> { &["dts"], )?; - let dts = find_board_dts(zephyr, &board)?; let out = Path::new(&std::env::var("OUT_DIR").unwrap()).join("device_tree.rs"); let include_paths = [ zephyr.join("include"), @@ -76,7 +77,7 @@ fn generate_device_tree() -> Result<(), Box> { ]; let include_refs: Vec<&Path> = include_paths.iter().map(PathBuf::as_path).collect(); - dtgen::run(&dts, &include_refs, &out)?; + dtgen::run(&dts_path, &include_refs, &out)?; Ok(()) } @@ -106,21 +107,6 @@ fn sparse_clone(url: &str, dest: &Path, paths: &[&str]) -> Result<(), Box Result> { - let boards_dir = zephyr.join("boards"); - for entry in walkdir::WalkDir::new(&boards_dir) { - let entry = entry?; - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) == Some("dts") - && path.file_stem().and_then(|s| s.to_str()) == Some(board) - { - return Ok(path.to_path_buf()); - } - } - - Err(format!("board '{board}' not found in {}", boards_dir.display()).into()) -} - fn generate_syscalls_export>(root: P) -> Result<(), std::io::Error> { let syscalls = collect_syscalls_export(root); diff --git a/options.toml b/options.toml index 7bb1959..63cae28 100644 --- a/options.toml +++ b/options.toml @@ -38,8 +38,8 @@ description = "Sets the size of the stack for the init application. This must be type = { type = "Integer", min = 0 } default = 2048 -[tuning.board] -name = "Targeted Board" -description = "Board name the device tree sources are searched for." +[tuning.dts] +name = "Device Tree Source" +description = "Board DTS file targeted to build OS for. Relative to the toplevel boards/ directory." type = "String" -default = "nucleo_l4r5zi" +default = "nucleo_l4r5zi.dts" diff --git a/xtasks/crates/dtgen/src/parser.rs b/xtasks/crates/dtgen/src/parser.rs index 58338fd..c602370 100644 --- a/xtasks/crates/dtgen/src/parser.rs +++ b/xtasks/crates/dtgen/src/parser.rs @@ -43,14 +43,6 @@ pub fn dts_to_dtb( return Err("cpp preprocessing failed".to_string()); } - // test - match std::process::Command::new("dtc").arg("--version").output() { - Ok(out) => println!( - "cargo:warning=dtc found, version: {}", - String::from_utf8_lossy(&out.stdout).trim() - ), - Err(e) => println!("cargo:warning=dtc not found: {}", e), - } // stage 2 - dts compilation let mut dtc_cmd = std::process::Command::new("dtc"); dtc_cmd From 0085369f4aaf55b849317b5cce005713d6cad9bc Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 18:54:58 +0200 Subject: [PATCH 18/21] [Fix] by accident a Vec got emitted --- xtasks/crates/dtgen/src/codegen.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xtasks/crates/dtgen/src/codegen.rs b/xtasks/crates/dtgen/src/codegen.rs index a4cc047..e290a7d 100644 --- a/xtasks/crates/dtgen/src/codegen.rs +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -375,13 +375,11 @@ fn emit_query_api() -> TokenStream { #[doc = "e.g. `soc/i2c@40005400/lsm6dsl@6a` or `soc/i2c/lsm6dsl`"] #[doc = "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 { + for segment in path.trim_start_matches('/').split('/') { let child = NODES[current].children.iter().find(|&&idx| { let n = NODES[idx].name; - n == *segment || n.split('@').next() == Some(*segment) + n == segment || n.split('@').next() == Some(segment) })?; current = *child; From f458d451909b2538d45d25b583c7a8226d72f996 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 19:38:52 +0200 Subject: [PATCH 19/21] [Fix] kani proof --- src/mem.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mem.rs b/src/mem.rs index 693ebf9..671cf8b 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -117,10 +117,10 @@ mod verification { kani::assume(j < MAX_REGIONS); kani::assume(i != j); - /// non-overlapping regions + // non-overlapping regions let (_, base_i, size_i) = regions[i]; let (_, base_j, size_j) = regions[j]; - kani::assert( + kani::assume( base_i + size_i <= base_j || base_j + size_j <= base_i, "memory regions should not overlap", ); From 9847a3430a63975826a5226d25a93c0415f9256f Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 19:45:23 +0200 Subject: [PATCH 20/21] [Fix] kani proof --- src/mem.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/mem.rs b/src/mem.rs index 671cf8b..17cf2c7 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -120,10 +120,7 @@ mod verification { // non-overlapping regions let (_, base_i, size_i) = regions[i]; let (_, base_j, size_j) = regions[j]; - kani::assume( - base_i + size_i <= base_j || base_j + size_j <= base_i, - "memory regions should not overlap", - ); + kani::assume(base_i + size_i <= base_j || base_j + size_j <= base_i); // verify memory init assert!(init_memory(®ions).is_ok()); From 8a4d9ee6082685b814966ac0e5a0af8eae7d03d3 Mon Sep 17 00:00:00 2001 From: Luis Ruisinger Date: Sun, 29 Mar 2026 20:09:22 +0200 Subject: [PATCH 21/21] [Fix] maybe I am genuinly too tired and stupid to write kani proofs --- src/mem.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mem.rs b/src/mem.rs index 17cf2c7..897e031 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -95,7 +95,12 @@ mod verification { use super::*; use crate::mem::alloc::MAX_ADDR; + fn mock_ptr_write(dst: *mut T, src: T) { + // noop + } + #[kani::proof] + #[kani::stub(core::ptr::write, mock_ptr_write)] fn proof_init_allocator_good() { const MAX_REGIONS: usize = 8; let regions: [(&str, usize, usize); MAX_REGIONS] =