diff --git a/Cargo.lock b/Cargo.lock index bdc12ce..933791c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,6 +523,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dtgen" +version = "0.1.0" +dependencies = [ + "clap", + "fdt", + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dtor" version = "0.1.1" @@ -613,6 +625,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" @@ -1044,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/README.md b/README.md index 427c5e2..928d496 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ An RTOS designed and verified to enable reliable software updates and operation |-----------|-------------| | [src/](src/) | This is the actual kernel code of osiris. It is a hardware independent layer providing scheduling, memory management, etc. | | [machine/](machine/) | This contains all the HALs and hardware specific code in general. It exports a hardware independent interface to the kernel. | +| [boards/](boards/) | This contains Device Tree Source (.dts) files for targeted boards. | ## Build @@ -20,6 +21,10 @@ An RTOS designed and verified to enable reliable software updates and operation * **Clang**: Used as the C/C++ compiler. * **Kani**: A recent version of the Kani Rust Verifier. +Furthermore the following repositories are automatically fetched to invoke a subproject of the build process: +* **[Zephyr](https://github.com/zephyrproject-rtos/zephyr)**: Pinned to release v4.3.0 +* **[HAL_STM32](https://github.com/zephyrproject-rtos/hal_stm32)**: The utilized commit is bound to the Zephyr release and can be infered through the Zephyr manifest [west.yaml](https://github.com/zephyrproject-rtos/zephyr/blob/main/west.yml) + ### Development & Debugging Tools These tools are used for flashing, debugging, and other development tasks. * [stlink (Fork)](https://github.com/CreaxxOG/stlink): For flashing and debugging on STM32 hardware. diff --git a/boards/nucleo_l4r5zi.dts b/boards/nucleo_l4r5zi.dts new file mode 100644 index 0000000..d99a7d1 --- /dev/null +++ b/boards/nucleo_l4r5zi.dts @@ -0,0 +1,233 @@ +/* + * 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,entropy; + /delete-property/ zephyr,flash-controller; + + osiris,console = &lpuart1; + osiris,shell-uart = &lpuart1; + osiris,sram = &sram0; + osiris,flash = &flash0; + 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 a571440..eb314c7 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, 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,137 @@ fn main() { } } +// Device Tree Codegen ---------------------------------------------------------------------------- + +fn generate_device_tree() -> Result<(), Box> { + 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(&std::env::var("OUT_DIR").unwrap()).join("zephyr"); + let hal_stm32 = Path::new(&std::env::var("OUT_DIR").unwrap()).join("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, + // the west.yaml file is a manifest to manage/pin subprojects used for a specific zephyr + // release + &["include", "dts", "boards", "west.yaml"], + Some("v4.3.0"), + )?; + + // retrieve from manifest + let hal_rev = get_hal_revision(&zephyr)?; + println!("cargo:warning=Detected hal_stm32 revision: {hal_rev}"); + + sparse_clone( + "https://github.com/zephyrproject-rtos/hal_stm32", + &hal_stm32, + &["dts"], + Some(&hal_rev), + )?; + + 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/arm/st/l4"), + zephyr.join("dts"), + zephyr.join("dts/arm"), + zephyr.join("dts/common"), + zephyr.join("boards/st"), + hal_stm32.join("dts"), + hal_stm32.join("dts/st"), + ]; + let include_refs: Vec<&Path> = include_paths.iter().map(PathBuf::as_path).collect(); + + for path in &include_paths { + if !path.exists() { + println!("cargo:warning=MISSING INCLUDE PATH: {:?}", path); + } + } + + dtgen::run(&dts_path, &include_refs, &out)?; + Ok(()) +} + +fn get_hal_revision(zephyr_path: &Path) -> Result> { + let west_yml = fs::read_to_string(zephyr_path.join("west.yml"))?; + let mut in_hal_stm32_block = false; + + for line in west_yml.lines() { + let trimmed = line.trim(); + + // Check if we've entered the hal_stm32 section + if trimmed == "- name: hal_stm32" || trimmed == "name: hal_stm32" { + in_hal_stm32_block = true; + continue; + } + + // If we are in the block, look for the revision + if in_hal_stm32_block { + if trimmed.starts_with("revision:") { + return Ok(trimmed.replace("revision:", "").trim().to_string()); + } + + // If we hit a new project name before finding a revision, something is wrong + if trimmed.starts_with("- name:") || trimmed.starts_with("name:") { + in_hal_stm32_block = false; + } + } + } + + Err("Could not find hal_stm32 revision in west.yml".into()) +} + +fn sparse_clone( + url: &str, + dest: &Path, + paths: &[&str], + revision: Option<&str>, +) -> Result<(), Box> { + 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()?; + + let mut checkout = Command::new("git"); + checkout.current_dir(dest).arg("checkout"); + + if let Some(rev) = revision { + checkout.arg(rev); + } + + checkout.status()?; + Ok(()) +} + +// Syscalls --------------------------------------------------------------------------------------- + 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 58f3392..d7bf1ce 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.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.dts" 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..897e031 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,74 +89,53 @@ 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 + // noop } #[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); - - kani::assume( - entry.length < alloc::MAX_ADDR as u64 - && entry.length > alloc::BestFitAllocator::MIN_RANGE_SIZE as u64, - ); - kani::assume(entry.addr < alloc::MAX_ADDR as u64 - entry.length && entry.addr > 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::() == 0); + kani::assume(base > 0); + kani::assume(size > 0); + kani::assume(size < alloc::MAX_ADDR && size > alloc::BestFitAllocator::MIN_RANGE_SIZE); + kani::assume(base < alloc::MAX_ADDR - size); } - 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, - ); - } - } - } + // 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::assume(base_i + size_i <= base_j || base_j + size_j <= base_i); - 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()); + // 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); 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..2cedf04 --- /dev/null +++ b/xtasks/crates/dtgen/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dtgen" +version = "0.1.0" +edition = "2024" + +[lib] +name = "dtgen" +path = "src/lib.rs" + +[[bin]] +name = "dtgen" +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/README.md b/xtasks/crates/dtgen/README.md new file mode 100644 index 0000000..e5128b3 --- /dev/null +++ b/xtasks/crates/dtgen/README.md @@ -0,0 +1,301 @@ +# 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 +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 + +// scoped to a specific parent - safe when names are not globally unique +peripheral_by_name_under("channel", adc1.node) // Option<&'static Peripheral> + +// 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 +// 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() + .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 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 flags = cells[2]; +} +``` + +### Walk children of a node +```rust +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 + } + } +} +``` + +### 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 +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"); + 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..e290a7d --- /dev/null +++ b/xtasks/crates/dtgen/src/codegen.rs @@ -0,0 +1,661 @@ +use crate::ir::{DeviceTree, PropValue}; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn generate_rust(dt: &DeviceTree) -> String { + 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 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 +} + +// ------------------------------------------------------------------------------------------------ +// File header +// ------------------------------------------------------------------------------------------------ + +fn emit_header() -> String { + r#"// ------------------------------------------------------------------------------------------------ +// AUTOGENERATED BY dtgen - DO NOT EDIT FILE +// ------------------------------------------------------------------------------------------------ + +"# + .to_string() +} + +// ------------------------------------------------------------------------------------------------ +// Type defitions +// ------------------------------------------------------------------------------------------------ + +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() -> 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 { + #[doc = "returns the parent TreeNode, if any"] + pub fn parent_node(&self) -> Option<&'static TreeNode> { + self.parent.map(|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() -> 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 { + #[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) + } + + #[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)) + } + + #[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) + } + + #[doc = "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 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 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 = "returns the base address from reg, if present"] + pub fn reg_base(&self) -> Option { + self.reg.map(|(base, _)| base) + } + + #[doc = "returns the size from reg, if present"] + pub fn reg_size(&self) -> Option { + self.reg.map(|(_, size)| size) + } + + #[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) + } + + #[doc = "returns the TreeNode for this peripheral"] + pub fn tree_node(&self) -> &'static TreeNode { + &NODES[self.node] + } + + #[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(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)* + ]; + } +} + +// ------------------------------------------------------------------------------------------------ +// Peripherals +// ------------------------------------------------------------------------------------------------ + +fn emit_peripherals(dt: &DeviceTree) -> TokenStream { + let mut indices: Vec = Vec::new(); + dt.walk(|idx, node| { + if !node.compatible.is_empty() { + indices.push(idx); + } + }); + + 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 ident = quote::format_ident!("PERIPH_{}_PROP_{}", i, j); + backing_statics.extend(quote! { + const #ident: &[u32] = &[#(#arr),*]; + }); + } + PropValue::StringList(list) => { + let ident = quote::format_ident!("PERIPH_{}_PROP_{}_STRS", i, j); + backing_statics.extend(quote! { + const #ident: &[&str] = &[#(#list),*]; + }); + } + PropValue::Bytes(b) => { + let ident = quote::format_ident!("PERIPH_{}_PROP_{}_BYTES", i, j); + backing_statics.extend(quote! { + const #ident: &[u8] = &[#(#b),*]; + }); + } + _ => {} + } + } + + // props array + 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(_) => { + 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) } + } + }; + + 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 }, + }; + + // automatic fallback to &[] on empty + let irqs = &node.interrupts; + let phandle_tokens = match node.phandle { + Some(v) => quote! { Some(#v) }, + None => quote! { None }, + }; + + peripheral_entries.push(quote! { + Peripheral { + node: #idx, + compatible: &[#(#compat_strs),*], + reg: #reg_tokens, + interrupts: &[#(#irqs),*], + phandle: #phandle_tokens, + props: #props_ident, + }, + }); + } + + quote! { + #backing_statics + + pub const PERIPHERALS: &[Peripheral] = &[ + #(#peripheral_entries)* + ]; + } +} + +// ------------------------------------------------------------------------------------------------ +// Query API +// ------------------------------------------------------------------------------------------------ + +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()) + } + + #[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()) + } + + #[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(ph: u32) -> Option<&'static Peripheral> { + PERIPHERALS.iter().find(|p| p.phandle == Some(ph)) + } + + #[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) + } + + #[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) + }) + } + + #[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 + }) + } + + #[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 mut current = 0usize; + 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) + })?; + + 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); + + 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(dt: &DeviceTree) -> TokenStream { + let pairs: Vec<(String, usize)> = dt + .by_name + .get("aliases") + .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_path(dt, path).map(|target_idx| (k.clone(), target_idx)) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default(); + + let mut pairs = pairs; + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + let entries = pairs.iter().map(|(name, idx)| { + quote! { (#name, #idx), } + }); + + quote! { + pub mod aliases { + use super::*; + + pub const ALIASES: &[(&str, usize)] = &[ + #(#entries)* + ]; + + #[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)) + } + } + } +} + +// ------------------------------------------------------------------------------------------------ +// Memory module +// ------------------------------------------------------------------------------------------------ + +fn emit_memory_module(dt: &DeviceTree) -> TokenStream { + let mut regions: Vec<(&str, u64, u64)> = dt + .nodes + .iter() + .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)) + }) + .collect(); + regions.sort_by_key(|&(_, base, _)| base); + + let entries = regions.iter().map(|(name, base, size)| { + let base = *base as usize; + let size = *size as usize; + + quote! { (#name, #base, #size), } + }); + + quote! { + pub mod memory { + use super::*; + + #[doc = "physical memory regions"] + pub const REGIONS: &[(&str, usize, usize)] = &[ + #(#entries)* + ]; + + #[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)) + } + + #[doc = "total memory in bytes across all declared regions"] + pub fn total_bytes() -> usize { + REGIONS.iter().map(|(_, _, size)| size).sum() + } + } + } +} + +fn emit_chosen_module(dt: &DeviceTree) -> TokenStream { + let chosen = dt + .by_name + .get("chosen") + .and_then(|indices| indices.first()) + .map(|&idx| &dt.nodes[idx]); + + 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 + } + }) + }?; + 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 + } + }); + + 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(); + + // 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) => { + let ident = quote::format_ident!("CHOSEN_EXTRA_{}", i); + Some(quote! { + static #ident: &[u32] = &[#(#arr),*]; + }) + } + crate::ir::PropValue::Bytes(arr) => { + 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) } + } + crate::ir::PropValue::U32Array(_) => { + 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::Bytes(_) => { + let ident = quote::format_ident!("CHOSEN_EXTRA_{}", i); + quote! { PropValue::Bytes(#ident.to_vec()) } + } + }; + quote! { (#key, #val_tokens), } + }); + + quote! { + #(#backing_statics)* + + pub mod chosen { + use super::*; + + #stdout_tokens + #bootargs_tokens + + #[doc = "non-standard properties of the chosen node"] + pub const EXTRAS: &[(&str, PropValue)] = &[ + #(#extra_entries)* + ]; + + #[doc = "resolve stdout to its Peripheral directly via PERIPHERALS index"] + pub fn stdout() -> Option<&'static Peripheral> { + STDOUT.map(|idx| &PERIPHERALS[idx]) + } + } + } +} diff --git a/xtasks/crates/dtgen/src/ir.rs b/xtasks/crates/dtgen/src/ir.rs new file mode 100644 index 0000000..7de1d5c --- /dev/null +++ b/xtasks/crates/dtgen/src/ir.rs @@ -0,0 +1,133 @@ +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()) + } +} 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..c602370 --- /dev/null +++ b/xtasks/crates/dtgen/src/parser.rs @@ -0,0 +1,248 @@ +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 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 + // -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 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") + })?; + + if !dtc_status.success() { + return Err("dtc failed".to_string()); + } + + 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}"))?; + 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_exact(4) + .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + .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); + } + + // 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); + 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 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 && 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()) + } 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()) +}