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-overlay.dts my_overlay.dtbo

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 overlays
  • dtoverlay -l to list the currently applied overlays
  • dtoverlay -h <overlay_name> to get information about any overlay, its parameters and possible values
  • dtoverlay <overlay_name> <param_name>=<param_value> to add an overlay with some parameters
  • dtoverlay -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 pins
  • raspi-gpio get lists how every pin is currently configured

Debugging

  • Add dtdebug=1 to /boot/config.txt to get a detailed output from vcdbg 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.

More Resources