
How to Sensor with Embedded Linux' Devicetree (on Raspberry Pi)
Tuesday, 20th October, 2026 6min #raspberry_pi #hardware #linux #dlr
In the beginning there was the Arduino.
The way you read out a sensor on an Arduino is by using libraries.
You search for your sensor's Arduino library and use its API.
This library code runs as part of the application's code directly on bare-metal — without an operating system.
On desktop Linux your code runs as a process and doesn't directly interact with the hardware.
Instead, the Linux kernel provides abstractions over the hardware; like a filesystem for storage or ALSA for audio.
Firstly, the motherboard's firmware (i.e., UEFI/BIOS) tells Linux what hardware is needed to boot.
Secondly, Linux automatically enumerates certain devices, e.g., those over USB or PCIe.
Therefore, the user and application developer doesn't need to worry about telling Linux what hardware there is.
But then there's embedded Linux:
We have a Raspberry Pi 5 Compute Module with
- a DS18B20 temperature sensor connected via One-Wire,
- an MCP3008 Analogue-to-Digital Converter (ADC) connected via SPI0 and chip-select GPIO18 and
- an AISIH accelerometer connected via SPI1 and chip-select GPIO16.

How do you drive these sensors? For one, the drivers need to know what SPI interface with what chip-select pin interfaces the hardware. There's no enumeration procedure that could do this.
Devicetree
And this brings us to the Devicetree.
If your Linux uses Devicetree you can run dtc -I fs /sys/firmware/devicetree/base to print out the currently loaded device tree (on x86 this is quite rare but on ARM, like on a Raspberry Pi, Devicetree is quite ubiquitous).
TODO: use full, real example
// --snip--
spi@54000 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
// --snip--
mcp3008@0 {
compatible = "microchip,mcp3008";
reg = <0>;
spi-max-frequency = <1600000>;
};
};
// --snip--
In this example there are two nodes describing hardware: an spi@54000 node with a single child, an mcp3008@0 sensor.
Notice how the spi@54000 node specifies what chip-select pins are under this interface's control, only GPIO18.
This allows the child, mcp3008@0 to set the reg attribute to <0>, selecting the first and only chip-select pin.
If you had other or more chip-select pins, you would need to change the Devicetree accordingly.
Let's slowly get to how to adjust the Devictree:
The extract above is part of a Devicetree source (.dts) file.
I won't go into the details of the Devicetree specification and invite you to read the well-written documentation, instead.
But as you might have guessed, dtc, the Devicetree Compiler, converts the loaded devicetree from the virtual filesystem into the Devicetree source representation.
Besides the source and filesystem representation there also is the Devicetree blob representation(.dtb or .dtbo files).
Typically, these are the type of file the firmware loads at boot as a basis to create the final Devicetree.
Because in the Raspberry Pi's case (like in a lot of other embedded devices) it's the bootloader's job to provide Linux with a Devicetree.
The Raspberry Pi relies quite heavily on the Devicetree, actually.
A very important concept for this article are Devictree overlays:
In our case we want to add more hardware to the Devicetree; hardware that the Raspberry Pi bootloader isn't aware of.
The way one does this is by instructing the bootloader to load a Devicetree overlay, in the form of a .dtbo file.
- overlay: including both formats
- raspberry pi config for devicetree, including how to load custom overlays
- config for adc
- custom overlay for adc
- custom overlay for stm acc
- building the kernel for stm modules
- helpful commands to debug:
vclog --msgdmesg- compile dts to dtbo
- decompile currently loaded log, combined with
dtdiff - (raspberry pi) linux sources
DS18B20: Enable One-Wire
dtoverlay=w1-gpioinconfig.txtcat /sys/bus/w1/devices/28-0000112202bc/temperature(28-... differs)/sys/bus/w1/devices/28-0000112202bc/temperature
# METABEGIN
# X-Env-Layer-Name: ds18b20
# X-Env-Layer-Version: 1.0.0
# X-Env-Layer-Category: example
# X-Env-Layer-Desc: Enable temperature sensor.
# X-Env-Layer-Requires: debian-trixie-arm64-multi,
# rpi-debian-trixie,
# rpi-misc-utils,
# rpi-essential-base,
# rpi-misc-skel
# METAEND
# TODO: the requirements are not necessarily absolutely required.
---
mmdebstrap:
customize-hooks:
- |
cat <<'EOF' >> "$1/boot/firmware/config.txt"
[all]
dtoverlay=w1-gpio
EOF
MCP3008: Configure SPI
- raspberry pi mcp3008
- enable SPI on Raspberry Pi with raspi-config
dtparam=spi=oninconfig.txtthis isn't actually needed import gpiozerothis isn't actually neededgpiozero.MCP3008(5, select_pin=18).valuethis isn't actually neededdtoverlay=mcp3008inconfig.txt/boot/firmware/overlays/mcp3008.dtbosudo vclog --msgfor firmware logsudo dtc -@ -I dts -O dtb -o /boot/firmware/overlays/mcp3008.dtbo mcp3008.dtscat /sys/bus/iio/devices/iio\:device0/in_voltage{0..7}_rawcat /sys/bus/iio/devices/iio\:device0/of_node/namemust behk_adc- better:
cat /sys/bus/spi/devices/spi0.2/iio\:*/in_voltage{0..7}_raw - this also loads the module autmatically:
lsmod | grep mcp320x - Apparently, the Raspberry Pi people created a lot of overlays for all sorts of hardware. I don't quite like that closed source firmware loading code does all this but it does appear to have good support.
arch/arm/boot/dts/overlays/mcp3008-overlay.dtsThe relevant documentation is inscripts/dtc/include-prefixes/arm64/overlays/README /sys/bus/iio/devices/iio:device0/in_voltage0_raw/sys/bus/iio/devices/iio:device0/in_voltage_scale
# METABEGIN
# X-Env-Layer-Name: mcp3008
# X-Env-Layer-Version: 1.0.0
# X-Env-Layer-Category: example
# X-Env-Layer-Desc: Enable house keeping ADC.
# X-Env-Layer-Requires: debian-trixie-arm64-multi,
# rpi-debian-trixie,
# rpi-misc-utils,
# rpi-essential-base,
# rpi-misc-skel
# METAEND
# TODO: the requirements are not necessarily absolutely required.
---
mmdebstrap:
customize-hooks:
- |
dtc -@ -I dts -O dtb -o "$1/boot/firmware/overlays/mcp3008_with_vref.dtbo" <<'EOF'
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2712";
fragment@0 {
target-path = "spi0/spidev@0";
__overlay__ {
status = "disabled";
};
};
fragment@1 {
// I thought we had to use spi4 but spi4 only supports the periphery mode of spi, not controller.
// Confusingly, the spi0 pins are labeled SPI0_SIO[0] and SPI0_SIO[1].
target-path = "spi0";
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
hk_adc@0 {
compatible = "microchip,mcp3008";
reg = <0>;
vref-supply = <&vdd_3v3_reg>;
spi-max-frequency = <1000000>;
};
};
};
};
EOF
- |
cat <<'EOF' >> "$1/boot/firmware/config.txt"
[all]
dtoverlay=spi0-1cs,cs0_pin=18
# We could also not use the custom overlay and instead use the Raspberry Pi's mcp3008 overlay.
# However, that overlay doesn't expose the vref-supply attribute.
dtoverlay=mcp3008_with_vref
EOF
AIS2IH: The Annoying One
- has linux drivers that must be installed in custom kernel
- https://github.com/STMicroelectronics/st-mems-android-linux-drivers-iio
- SPI1 and cs GPIO16
sudo dtc -@ -I dts -O dtb -o /boot/firmware/overlays/ais2ih.dtbo ais2ih.dtsdtoverlay=ais2ihinconfig.txtdrivers/iio/stm/accel/st_lis2dw12/st_lis2dw12_spi.cCONFIG_IIO_ST_LIS2DW12_SPI=m- described in
scripts/dtc/include-prefixes/arm64/overlays/READMEin the raspberry pi linux kernel sources - module to load:
st_lis2dw12_spi - raspberry pi convention:
__dormant__is an inactive version of__overlay__, which actually does something
https://www.raspberrypi.com/documentation/computers/configuration.html#part2.2.8 cat /sys/bus/iio/devices/iio\:device1/in_accel_{x,y,z}_*/sys/bus/iio/devices/iio:device1/in_accel_x_raw
# METABEGIN
# X-Env-Layer-Name: ais2ih
# X-Env-Layer-Version: 1.0.0
# X-Env-Layer-Category: example
# X-Env-Layer-Desc: Enable accelerometer.
# X-Env-Layer-Requires: debian-trixie-arm64-multi,
# rpi-debian-trixie,
# rpi-misc-utils,
# rpi-essential-base,
# rpi-misc-skel
# METAEND
# TODO: the requirements are not necessarily absolutely required.
---
mmdebstrap:
customize-hooks:
- |
dtc -@ -I dts -O dtb -o "$1/boot/firmware/overlays/ais2ih.dtbo" <<'EOF'
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2712";
fragment@0 {
target-path = "spi1/spidev@0";
__overlay__ {
status = "disabled";
};
};
fragment@1 {
target-path = "spi1";
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
hk_acc@0 {
compatible = "st,ais2ih";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
};
};
EOF
- |
cat <<'EOF' >> "$1/boot/firmware/config.txt"
[all]
dtoverlay=spi1-1cs,cs0_pin=16
dtoverlay=ais2ih
EOF
Building the Image
building the kernel on an x86 build machine (see https://chris-besch.com/articles/embedded_systemd):
sudo apt update && sudo dpkg --add-architecture arm64 && sudo apt install -y git bc bison flex libssl-dev make libssl-dev:arm64git clone https://github.com/raspberrypi/linux && cd linux && git checkout 5d39807272da852e10f3fc4467c33b57f59ecc61oldstable_20260611 somehow doesn't compilegit checkout -b custom_kernelgit remote add stmems_iio_github https://github.com/STMicroelectronics/st-mems-android-linux-drivers-iio.gitgit fetch stmems_iio_githubgit checkout 0455f406e00484260474473e8fed29bddd15bfa2git checkout -b stemems_iio_branchgit checkout custom_kernelgit merge --allow-unrelated-histories custom_kernel stemems_iio_branchYes, do fix the merge conflict inREADME.mdmanuallygit am stm_iio_patches/6.12.y/*-stm-*.patchKERNEL=kernel_2712make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2712_defconfigmake ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfigand
- mark as module Device Drivers/Industrial I/O support/STM MEMS Device drivers
- mark as module Device Drivers/Industrial I/O support/STM MEMS Device drivers/Accelerometer/STMicroelectronics LIS2DW12/IIS2DLPC/AIS2IH Accelerometer Driver
Similar Articles

Real-Time Linux on RISC-V
27th June, 2025 8min
Setting up Linux v6.12 on a StarFive VisionFive 2 with PREEMPT_RT. Also: How to setup your Linux laptop as a simple NAT router.

Slay the Daemons on Embedded
4th October, 2026 15min
My name is Chris and I'm afraid of daemons. Well, not literally but I am engineering the firmware for an embedded, real-time device. And I chose to use Debian Linux with systemd. What I'm afraid of are background processes, daemons, performing actions I don't anticipate. This article explains how I create a linux image for my device with only a handful purposeful daemons.

Automatic Firmware Generation for Spaceflight Hardware based on Schematics
19th August, 2026 15min
The interaction between hardware design and firmware implementation requires strict consistency in embedded systems. Firmware must exactly match the schematic-level component configuration and connectivity. In current workflows, this consistency is largely ensured through manual inspection and extensive testing. In this work, we introduce a schematics-aware approach to automate firmware generation. We propose the Group Netlist, a design- and PCB tool-agnostic abstraction of hardware. Using a reference implementation, we demonstrate our proposal for the open-source PCB design suite KiCad. As added benefit our approach serves for hardware verification purposes that we demonstrate through a fault-injection analysis. Considering relevant cases, our tool detects 75% of otherwise undetected injected faults and requires negligible overhead (less than 0.2% codebase addition and on average a 4.8% runtime increase). Lastly, we verify our tooling by successfully using it in the development workflow of an embedded satellite module.

Dabbling with Zephyr Driver Development: STM32 direct LCD Driver
31st May, 2026 5min
I want to use an LCD glass on an STM32WB55RG, which has an LCD direct driver in hardware. You directly connect all LCD segment and common pins to the SoC. Because I want to use Zephyr but Zephyr doesn't have an LCD driver already, I implemented one myself.
