Rust Crates For Handling Device Tree
In this post I investigated the Rust crates that are used to handle device tree.
I searched with key words “fdt”, “device tree”, “dtb” and “dts” on crate.io and reviewed all the relevant crates I can find.
On the time of write, March 2023, I found 10 crates totally. I didn’t dive into each one. For each crate, I added its link, quoted their self-introduction and demo source code if there is. Finally I categorized the crates according to their functionalities, and tried to make a summary.
Crate List
fdt
A pure-Rust #![no_std]
crate for parsing Flattened Devicetrees, with the goal of having a very ergonomic and idiomatic API. (v0.1.5)
The crate fdt
can be used to parse DTB. The sample code of the crate is like:
static MY_FDT: &[u8] = include_bytes!("../dtb/test.dtb");
fn main() {
let fdt = fdt::Fdt::new(MY_FDT).unwrap();
println!("This is a devicetree representation of a {}", fdt.root().model());
println!("...which is compatible with at least: {}", fdt.root().compatible().first());
println!("...and has {} CPU(s)", fdt.cpus().count());
// More code ignored
}
vm-fdt
https://crates.io/crates/vm-fdt
The vm-fdt crate provides the ability to write Flattened Devicetree blobs as defined in the Devicetree specification. (v0.2.0)
vm-fdt
is a repo under rust-vmm
project. rust-vmm
is an open-source project that empowers the community to build custom Virtual Machine Monitors (VMMs) and hypervisors.
Crate vm-fdt
can be used to create DTB from scratch. It fits the use case of the virtual machine monitor (VMM).
In virtualization, especially on Arm architectures, a VMM may need to provide the hardware information to the guest machine in this way:
- The VMM generate a device tree for a virtual machine (VM). The content of the device tree is about the simulated hardware that the VMM has prepared for the VM, including: VCPU, memory, interrupts, peripheral devices, etc.
- The VMM creates the DTB binary, and saves the binary somewhere in the VM memory.
- The hypervisor or a boot loader set the address of the DTB binary to a specified register of the booting CPU. Linux kernel get the DTB location from the same register, read the DTB and parse it to get the hardware information.
The sample code of vm-fdt
is like:
use vm_fdt::{FdtWriter, Error};
fn create_fdt() -> Result<Vec<u8>, Error> {
let mut fdt = FdtWriter::new()?;
// Create the root node and set properties
let root_node = fdt.begin_node("root")?;
fdt.property_string("compatible", "linux,dummy-virt")?;
fdt.property_u32("#address-cells", 0x2)?;
fdt.property_u32("#size-cells", 0x2)?;
// Add a subnode `chosen` under the root node
let chosen_node = fdt.begin_node("chosen")?;
fdt.property_u32("linux,pci-probe-only", 1)?;
fdt.property_string("bootargs", "panic=-1 console=hvc0")?;
fdt.end_node(chosen_node)?;
// Add another node to describe memory
let memory_node = fdt.begin_node("memory")?;
fdt.property_string("device_type", "memory")?;
fdt.end_node(memory_node)?;
// Close the root node
fdt.end_node(root_node)?;
fdt.finish()
}
fdt-rs
https://crates.io/crates/fdt-rs
A Flattened Device Tree parser for embedded no-std environments (v0.4.3)
fdt-rs
is similar to fdt
, it is used to parse DTB.
Sample code:
extern crate fdt_rs;
use fdt_rs::prelude::*;
use fdt_rs::base::*;
// Place a device tree image into the rust binary and
// align it to a 32-byte boundary by using a wrapper struct.
#[repr(align(4))] struct _Wrapper<T>(T);
pub const FDT: &[u8] = &_Wrapper(*include_bytes!("../tests/riscv64-virt.dtb")).0;
fn main() {
// Initialize the devtree using an &[u8] array.
let devtree = unsafe {
// Get the actual size of the device tree after reading its header.
let size = DevTree::read_totalsize(FDT).unwrap();
let buf = &FDT[..size];
// Create the device tree handle
DevTree::new(buf).unwrap()
};
// Iterate through all "ns16550a" compatible nodes within the device tree.
// If found, print the name of each node (including unit address).
let mut node_iter = devtree.compatible_nodes("ns16550a");
while let Some(node) = node_iter.next().unwrap() {
println!("{}", node.name().unwrap());
}
}
devicetree
https://crates.io/crates/devicetree
The crate devicetree can be used for parsing Devicetree Blob (DTB), based on Devicetree Specification. (v0.1.1)
Similar to fdt
and fdt-rs
, devicetree
provides API for decoding DTB, as its document demonstrates:
use devicetree::DeviceTreeBlob;
fn main() {
let mut dtb: &[u8] = include_bytes!("<path-to-*.dtb>");
let tree = DeviceTree::from_bytes(&mut dtb).unwrap();
println!("{}", tree);
}
Additionally, devicetree
provides a struct DeviceTree
that can be used to build a new device tree. The DeviceTree
can also be built from DTB byte data. But I am not sure whether the DeviceTree
can generate DTB from itself, there is not such API published.
fdtdump
https://crates.io/crates/fdtdump
A rust version of fdtdump — a tool for printing flattened device trees. (v0.1.0)
fdtdump
is yet another tool that can be used to decode DTB. The difference from fdt
and fdt-rs
is that fdtdump
is a command line tool, it doesn’t provide API.
The CLI usage is like:
$ # Generate a DTB file
$ echo '/dts-v1/; / { prop = "hello"; };' | dtc > dtb-file
$
$ # Prase the DTB file, print the DTB header and DTS content
$ cargo run dtb-file
/dts-v1/;
// magic: 0xd00dfeed
// totalsize: 0x61 (97)
// off_dt_struct: 0x38
// off_dt_strings: 0x5c
// version: 0x11
// boot_cpuid_phys: 0x0
// last_comp_version: 16
// boot_cpuid_phys: 0x0
// size_dt_strings: 0x5
// size_dt_struct: 0x24
/ {
prop = "hello";
};
device_tree
https://crates.io/crates/device_tree
This library allows parsing the so-called flattened device trees, which are the compiled binary forms of these trees. (v1.1.0)
A DTB parsing crate. Example code:
fn main() {
// read file into memory
let mut input = fs::File::open("sample.dtb").unwrap();
let mut buf = Vec::new();
input.read_to_end(&mut buf).unwrap();
let dt = device_tree::DeviceTree::load(buf.as_slice ()).unwrap();
println!("{:?}", dt);
}
device_tree_source & dts_viewer
dts_tree_source
(https://crates.io/crates/device_tree_source) and dts_viewer
(https://crates.io/crates/dts_viewer) is a pair. The former one is a library, the latter one is a program consuming the library.
dts_tree_source
: A library to parse Device Tree Source (DTS) files. (v0.2.0)
According its own document, this crate includes functionality to parse Device Tree Source files into a tree structure and then manipulate that tree structure.
But the crate didn’t provide any sample code or usage guideline.
dts_viewer
: A program to allow easier viewing of device tree files. (v0.3.0)
Not any document is provided for dts_viewer
either.
dtb
This no_std crate contains types for reading and writing DTBs. (v0.2.0)
Sample code:
let mut buf = Vec::new();
// Read a DTB file
let mut file = File::open("example.dtb").unwrap();
file.read_to_end(&mut buf).unwrap();
let reader = Reader::read(buf.as_slice()).unwrap();
// Go through the memory reservation regions
for entry in reader.reserved_mem_entries() {
println!("reserved: {:?}, {:?}", entry.address, entry.size);
}
// Read the property of a node
let root = reader.struct_items();
let (prop, _) =
root.path_struct_items("/node/property").next().unwrap();
println!("property: {:?}, {:?}", prop.name(), prop.value_str());
The crate contains a Writer
struct for generating DTB, but I didn’t see the function that can be called to output DTB bytes.
The project builds a command line application as well. To parse a DTB file:
$ cargo run --example dump src/test_dtb/sample.dtb
dtb-walker
https://crates.io/crates/dtb-walker
A simple package for DTB depth-first walking. (v0.1.3)
I can only get a little information about this crate from its crate.io page or its github page.
Checking the code of the repo and its vague documents, I think dtb-walker
is a crate that can be used to parse DTB.
dtb_parser
https://crates.io/crates/dtb_parser
no std but alloc depended device tree blob parsing lib (v0.1.2)
Yet another DTB parser. It works in this way:
pub const BLOB: &[u8] = include_bytes!("device.dtb");
fn main() {
let tree = DeviceTree::from_bytes(BLOB).unwrap();
println!("{}", tree);
assert!(!matches!(tree.find_child("memory@0"), None));
}
Summary
Categories
All the crates that I have gone through can be divided into 3 categories: DTB parser, DTB builder and DTS parser.
DTB Parser:
- fdt
- fdt-rs
- devicetree
- fdtdump
- device_tree
- dtb
- dtb-walker
- dtb_parser
DTB Builder:
- vm-fdt
DTS Parser:
- device_tree_source & dts_viewer
Problems
In my personal opinion, there are still things to improve in some of these crates:
- All of the crates are in their early versions
- Some of them have been out of maintenance
- Each crate supports limited functionalities only
Expectation
Regarding the device tree handling software, usually there could be following requirements, depending on different use cases:
- DTB Parsing — Kernel, firmware or hypervisor may need to parse the DTB for obtaining the hardware information of the system.
- DTB Building — A VMM need this to build DTB according to the devices it simulated for the a guest VM.
- DTS Parsing — Firmware may need this, one of the use cases is to customize the device information for upper layer components (like boot loader or Kernel). As far as I know, ARM Trusted Firmware-A can work in this way.
- DTS Building — This is important for device tree developing and debugging.
libfdt
is the de facto standard software on Linux that can do all the 4 works (at least) mentioned above. It is written in C language. Although we can call libfdt
API in Rust with libc
crate. But that solution is not perfect, there will be a pain when you need to deploy your Rust software that have to invoke local libraries (see this post).
Rust world call for a device tree handling crate that can be used for all the scenarios, in the way of pure Rust. In the next post, I am going to introduce something I am doing for that purpose.