Rust Crates For Handling Device Tree

Michael Zhao
6 min readMar 3, 2023

--

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

https://crates.io/crates/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

https://crates.io/crates/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.

--

--