← How-Tos
raspberry-pi 1 hr ago ◯ 6 min read

Writing Custom Device Tree Overlays on Raspberry Pi: Adding Sensors Without a HAT

raspberry pidevice treedtoverlaykernel drivercustom carrier boarddtscompute module

Most Raspberry Pi GPIO tutorials work entirely at the application level — wiringPi, RPi.GPIO, gpiozero, libgpiod — and never touch the layer underneath that actually tells the Linux kernel what hardware is connected to which pins. That layer is the device tree, and understanding it opens up a category of projects that plain application-level GPIO libraries can't reach: adding a sensor that needs a genuine kernel driver (not just bit-banged reads), enabling hardware peripherals (a second SPI or I2C bus, a hardware PWM channel) that aren't turned on by default, or building a custom carrier board and telling the Pi exactly what's wired to it. This guide covers what device tree overlays actually are, how to write and apply your own, and where they fit relative to a HAT.

What the Device Tree Actually Does

On boot, the Raspberry Pi's firmware loads a device tree blob (.dtb) that describes the hardware present on the board — what's connected to which GPIO pins, which peripherals (I2C, SPI, UART, PWM) are enabled, and what kernel driver should bind to each device. This is fundamentally different from an application reading and writing GPIO pins in userspace: a device tree entry tells the kernel "there is a DS18B20 temperature sensor on GPIO4 using the 1-Wire protocol," and the kernel loads the appropriate driver and exposes it through the standard Linux hardware interfaces (like /sys/bus/w1/devices/ or a Linux IIO device), rather than the application having to implement the sensor's protocol itself from scratch.

A device tree overlay is a small, separately-loadable fragment that modifies the base device tree at boot time — adding a node for a specific sensor or peripheral without needing to rebuild the entire device tree from scratch. This is exactly what a commercial HAT's EEPROM does automatically when you plug it in: it tells the Pi which overlay to load for that specific board.

When You Need One (and When You Don't)

SituationApproach Reading a simple sensor over I2C/SPI in PythonApplication-level library (smbus2, spidev) — no overlay needed, the I2C/SPI bus is already enabled by default Bit-banging a protocol not natively supportedApplication-level library or userspace bit-banging — no overlay needed A sensor that needs a genuine kernel driver (1-Wire devices, some ADCs/DACs, hardware RTCs)Device tree overlay to bind the kernel driver to the right pins Enabling a second SPI or I2C bus, or hardware PWM on non-default pinsDevice tree overlay (often one of the many overlays already shipped with Raspberry Pi OS) A custom carrier board or non-standard wiring that needs the kernel to know pin assignments at bootCustom device tree overlay you write yourself

Using a Stock Overlay First

Raspberry Pi OS ships with dozens of overlays already compiled and ready to use in /boot/firmware/overlays/ (or /boot/overlays/ on older OS versions) — covering common needs like a second SPI bus (spi1-3cs), 1-Wire on a custom pin (w1-gpio), hardware PWM on additional pins (pwm-2chan), and RTC chips (i2c-rtc). Before writing a custom overlay, check whether one of these already covers your need — enabling one is as simple as adding a line to /boot/firmware/config.txt:

dtoverlay=w1-gpio,gpiopin=4 dtoverlay=i2c-rtc,ds3231 dtoverlay=spi1-3cs

Reboot after editing config.txt, then confirm the overlay loaded correctly with dtoverlay -l (lists currently applied overlays) or by checking that the expected device node appeared, such as /sys/bus/w1/devices/ for a 1-Wire overlay or a new /dev/i2c-* node for an additional I2C bus.

Writing a Custom Overlay

When no stock overlay fits — a custom carrier board with non-standard pin assignments, or a sensor whose kernel driver exists but has no ready-made overlay for your exact wiring — you write one in device tree source (.dts) format and compile it into a .dtbo binary the firmware can load.

A minimal overlay adding an I2C device at a specific address on a specific bus looks roughly like this:

/dts-v1/; /plugin/; / { compatible = "brcm,bcm2835"; fragment@0 { target = &i2c1; __overlay__ { #address-cells = <1>; #size-cells = <0>; status = "okay"; my_sensor: my_sensor@76 { compatible = "bosch,bme280"; reg = <0x76>; status = "okay"; }; }; }; };

The key parts: fragment@0 targets an existing node in the base device tree (here, the i2c1 bus) and merges new child nodes into it; compatible tells the kernel which driver to bind, matching a string the driver itself registers; reg sets the device's bus address. Compile it with the device tree compiler, which ships with Raspberry Pi OS:

dtc -@ -I dts -O dtb -o my-sensor.dtbo my-sensor-overlay.dts sudo cp my-sensor.dtbo /boot/firmware/overlays/

Then reference it in config.txt with dtoverlay=my-sensor and reboot.

Debugging a Non-Loading Overlay

Overlays vs. a HAT's Auto-Configuration

A commercial HAT with an ID EEPROM handles this automatically: the Pi reads the EEPROM at boot, finds the overlay name and any needed parameters stored on it, and applies the correct overlay without any manual config.txt editing. Writing your own overlay is effectively doing by hand what a HAT's EEPROM does for you — which is exactly why it matters for custom carrier boards, breadboard prototypes, or any sensor wiring that doesn't come from an off-the-shelf HAT. If you're designing your own carrier board for a Compute Module and want it to behave like a real HAT (auto-configuring without manual setup), programming an ID EEPROM with your overlay reference is the way to get there.

A Practical Example: Adding a Sensor with No Existing Overlay

For a sensor with a mainline Linux kernel driver but no pre-built overlay (common for newer or less mainstream I2C/SPI sensors), the workflow is: confirm the driver exists in the kernel (check for it under /lib/modules/$(uname -r)/kernel/drivers/, or search the kernel source for the device's compatible string), write a minimal overlay fragment targeting the correct bus with the correct compatible string and address, compile and install it, then verify with dmesg that the driver bound successfully and the expected sysfs or IIO device node appeared. From there, the sensor behaves like any other kernel-backed device — readable through standard Linux interfaces rather than a Python library reimplementing its protocol from scratch.

Device tree overlays are one of the more "systems" corners of Raspberry Pi work compared to GPIO scripting, but they're the difference between a sensor your application code has to manage entirely itself and one the kernel understands natively — and they're the exact mechanism a custom carrier board needs to behave like proper, plug-and-play hardware instead of a pile of wires an application script has to know about by convention.