Raspberry Pi 4 - Device Tree
This article consists of three parts:
- What is the Device Tree in Linux?
- Useful Commands
- Writing your own Device Tree Overlays
- Example: Editing MCP2515 overlay to work with new SPI6
What is the Device Tree?
Roughly speaking the Device Tree describes all the hardware of a device in a tree format. It is especially used for anything that can't be discovered automatically like serial interfaces, internal memory, oscillators, ... (in contrast to e.g. USB or PCI devices). As the Raspberry Pi has a lot of GPIO headers exposed, additional devices can be added that the current device tree doesn't know about. That's where overlays come into play.
The default base device tree source file for the RPI4 B can be found in the kernel sources in the raspberrypi/linux
under linux/arch/arm/boot/dts/bcm2711-rpi-4-b.dts
. This is the source file for the board, which is based itself on the source file for the SoC linux/arch/arm/boot/dts/bcm2711.dtsi
.
What is an overlay?
Overlays are applied to the base device tree at a later point to change it, usually to add a device, or to configure/enable one that is already present in the base device tree.
For example: the overlay spi5-1cs-overlay.dts
configures the spi5
node already present in the device tree to enable it with one CS pin and a spidev device.
All available overlays can be found in the /boot/overlays
directory. Albeit already compiled.
Useful Commands
dtc
dtc is the device tree compiler. It is used to losslessly compile .dts
files to .dtb
files. As a naming convention all overlays source files end in -overlay.dts
and are compiled to *.dtbo
.
pi@raspberrypi:~ $ dtc -@ -I dts -O dtb -o my_overlay.dtbo my_overlay-overlay.dts
The -@
is needed for overlays as it prevents the compiler from looking up the references from the source file, which it - of course - wouldn't find.
dtoverlay
dtoverlay is used to dynamically load device trees.
dtoverlay -a
to list all available overlaysdtoverlay -l
to list the currently applied overlaysdtoverlay -h <overlay_name>
to get information about any overlay, its parameters and possible valuesdtoverlay <overlay_name> <param_name>=<param_value>
to add an overlay with some parametersdtoverlay -r <overlay_name>
to remove a previously loaded overlay
Note: dtoverlay -l
only lists dynamically loaded modules and only those can be removed with dtoverlay -r
. Anything that was loaded via /boot/config.txt
is considered part of the base device tree. Some overlays have to be added this way and can't be loaded by dtoverlay
.
raspi-gpio
- Install it if not yet present:
apt-get install raspi-gpio
raspi-gpio funcs
lists all available functions for all pinsraspi-gpio get
lists how every pin is currently configured
Debugging
- Add
dtdebug=1
to/boot/config.txt
to get a detailed output fromvcdbg log msg
, when an overlay fails to be applied at startup it will just be skipped. - Remember
dmesg | grep -i <device_name>
can be useful to see if the module corresponding to the overlay has been loaded successfully. dtc -I fs /proc/device-tree
prints the current complete device tree
Writing your own Device Tree Overlays
For an explanation of the syntax of *.dts
files take a look at the official documentation. It's really good and you'll need it as the syntax is weird. Especially for overlays.
Miscellaneous
The number behind the @ in the name of a node (e.g. can@0
) is the address inside the parent the node can be found at. It is by convention equal to the value of the reg property, in this case reg = <0>;
.
#address-cells = <1>;
defines how many addresses the children contain (here 1).
#size-cells = <0>;
sets the size of the address space to 0. That means that the children will have a single address, instead of an address space (for e.g. memory).
Combined one can conclude that the reg property will only contain one value for the address and an empty value for the length (like above).
This also means, that this overlay can only be enabled once on a given SPI bus, because the addresses would clash. To enable it a second time the reg property and address have to be changed. This is independent of the CS used.
Example: Editing MCP2515 overlay to work with new SPI5
The overlay mcp2515-can0-overlay.dts
references <&spi0>
in multiple places. In principle the only thing you have to do to make it work with another spi interface is to change all of them to e.g. <&spi6>
. While you're at it also change all occurrences of can0
to can6
and can0_osc
to can6_osc
so they don't conflict when loading both.
/*
* Device tree overlay for mcp251x/can0 on spi0.0
*/
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835";
/* disable spi-dev for spi0.0 */
fragment@0 {
target = <&spi0>;
__overlay__ {
status = "okay";
};
};
fragment@1 {
target = <&spidev0>;
__overlay__ {
status = "disabled";
};
};
/* the interrupt pin of the can-controller */
fragment@2 {
target = <&gpio>;
__overlay__ {
can0_pins: can0_pins {
brcm,pins = <25>;
brcm,function = <0>; /* input */
};
};
};
/* the clock/oscillator of the can-controller */
fragment@3 {
target-path = "/clocks";
__overlay__ {
/* external oscillator of mcp2515 on SPI0.0 */
can0_osc: can0_osc {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <16000000>;
};
};
};
/* the spi config of the can-controller itself binding everything together */
fragment@4 {
target = <&spi0>;
__overlay__ {
/* needed to avoid dtc warning */
#address-cells = <1>;
#size-cells = <0>;
can0: mcp2515@0 {
reg = <0>;
compatible = "microchip,mcp2515";
pinctrl-names = "default";
pinctrl-0 = <&can0_pins>;
spi-max-frequency = <10000000>;
interrupt-parent = <&gpio>;
interrupts = <25 8>; /* IRQ_TYPE_LEVEL_LOW */
clocks = <&can0_osc>;
};
};
};
__overrides__ {
oscillator = <&can0_osc>,"clock-frequency:0";
spimaxfrequency = <&can0>,"spi-max-frequency:0";
interrupt = <&can0_pins>,"brcm,pins:0",<&can0>,"interrupts:0";
};
};
I additionally edited the target=<&spi6>
section and added a parameter so that the CS pin can be freely chosen as well:
/*
* Device tree overlay for mcp251x/can6 on spi6.0
*/
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835";
/* spi6 cs pin config */
fragment@0 {
target = <&spi6_cs_pins>;
frag0: __overlay__ {
brcm,pins = <26>; /* gpio26, hardware pin 37 */
brcm,function = <1>; /* output */
};
};
/* spi6 config */
fragment@1 {
target = <&spi6>;
frag1: __overlay__ {
/* needed to avoid dtc warning */
#adress-cells = <1>;
#size-cells = <0>;
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi6_pins &spi6_cs_pins>;
cs-gpios = <&gpio 26 1>;
};
};
/* the interrupt pin of the can-controller */
fragment@2 {
target = <&gpio>;
__overlay__ {
can6_pins: can6_pins {
brcm,pins = <16>; /* gpio16, hardware pin 36 */
brcm,function = <0>; /* input */
};
};
};
/* the clock/oscillator of the can-controller */
fragment@3 {
target-path = "/clocks";
__overlay__ {
/* external oscillator of mcp2515 on SPI6.0 */
can6_osc: can6_osc {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <16000000>;
};
};
};
/* the spi config of the can-controller itself binding everything together */
fragment@4 {
target = <&spi6>;
__overlay__ {
/* needed to avoid dtc warning */
#address-cells = <1>;
#size-cells = <0>;
can6: mcp2515@0 {
reg = <0>;
compatible = "microchip,mcp2515";
pinctrl-names = "default";
pinctrl-0 = <&can6_pins>;
spi-max-frequency = <10000000>;
interrupt-parent = <&gpio>;
interrupts = <16 8>; /* IRQ_TYPE_LEVEL_LOW */
clocks = <&can6_osc>;
};
};
};
__overrides__ {
cs_pin = <&frag0>,"brcm,pins:0",
<&frag1>,"cs-gpios:4";
oscillator = <&can6_osc>,"clock-frequency:0";
spimaxfrequency = <&can6>,"spi-max-frequency:0";
interrupt_pin = <&can6_pins>,"brcm,pins:0",
<&can6>,"interrupts:0";
};
};
This just needs to be compiled and copied to /boot/overlays/
:
dtc -@ -I dts -O dtb -o mcp2515-can6.dtbo mcp2515-can6-overlay.dts
cp mcp2515-can6.dtbo /boot/overlays/
After that you can load it dynamically:
overlay mcp2515-can6 cs_pin=16 interrupt_pin=26 oscillator=16000000
Or statically adding to /boot/config.txt
. Don't add anything else and make sure that nothing uses the same pins!
dtoverlay=mcp2515-can6,cs_pin=16,interrupt_pin=26,oscillator=16000000
Now that the device is known to Linux it behaves the same as usual. Just follow any of the myriad of guides on connecting the MCP2515. Pay special attention the the mentioned common mistakes (like having only one node). Put the can interface up and start sending data:
ip link set can0 up type can bitrate 50000
cansend can0 111#FF
It could also be adapted to work for any SPI interface like the overlay for the MCP3008 works for SPI0, SPI1 and SPI2.
I hope this was somewhat useful.