Using an External LED on a PocketBeagle as an Example
This article introduces the basics of a device tree in an embedded Linux system. As an example project, I add an LED to the device tree of a PocketBeagle board and discuss the LED subsystem features in embedded Linux.
Due to the high availability of sophisticated System-on-Chips (SoCs) and System-in-Packages (SiPs) in recent years, embedded Linux is becoming an increasingly suitable choice as an operating system in a great variety of devices, from wearable devices to autonomous vehicles.
The Linux kernel (and, consequently, embedded Linux as well) is a project under continuous development, with constant and significant improvements and evolution, thanks to the contributions of thousands of developers around the world. This increases its popularity, innovation, and applicability.
One of the greatest features of embedded Linux running on an SoC or an SiP with ARM architecture is the device tree. A device tree is a feature that allows developers to describe all the pieces of hardware wired to the board, informing the Linux kernel which peripheral drivers to use and their parameters (addresses, bus, GPIO pin, and so on).
This article covers the basics of a device tree in an embedded Linux system. I’ll show you, from beginning to end, how to properly add an LED to the device tree of a PocketBeagle board. Then I’ll discuss how to control the LED with the features of the LED subsystem in embedded Linux.
— ADVERTISMENT—
—Advertise Here—
POCKETBEAGLE: A QUICK OVERVIEW
For this project I used the PocketBeagle board, a tiny, open-source, Linux-compatible single-board computer (SBC) from the BeagleBoard.org Foundation. It has a very small footprint (just 56mm x 35mm x 5mm), and hardware powerful enough to support most IoT, robotics, and general embedded systems applications. One highlight is its SiP, the Octavo Systems OSD3358-SM, which contains a CPU (that will run our embedded Linux) as well as two additional and fully independent programmable real-time units (PRUs). Hence, PocketBeagle can run both real-time and non-real-time pieces of software. This opens the door for developing asymmetric software solutions, as Linux can take care of the non-real-time features while the PRUs do the real-time work. Figure 1 shows the PocketBeagle board.
PocketBeagle board
(Source: BeagleBoard.org)
WHAT IS A DEVICE TREE?
General purpose operating systems like embedded Linux are designed to communicate to a large variety of hardware, peripherals and buses, such as power management integrated circuits (PMICs), buttons, LEDs, processors, shift-registers, buses (SPI, I²C, and so on), serial communication (USART), RAM memory, and much more. Indeed, you can wire up just about whatever you wish to your Linux-running board. But Linux needs a way to know what’s wired where.
In systems with an SoC or SiP based on ARM architecture, the solution is to describe to the embedded Linux and to bootloader which peripherals are wired to the main board, along with information on the pins and GPIOs they use and the parameters for their correct operation. This “full description” is called a device tree.
More specifically, a device tree is a data-structured file (or a set of these files) for these descriptions. It was created as part of Open Firmware, a boot firmware standard developed by Sun. (In this context it is sometimes written as “Devicetree” or “DT”.) They’re used in ARM architecture systems to standardize peripheral, bus, and external hardware descriptions, and how these items should be used in embedded Linux. Without this standardization afforded by a device tree, ARM-based Linux systems’ hardware initialization would be a huge mess—the simple tasks of adding, configuring, and removing peripherals and so forth would be a nightmare.
In fact, it was a nightmare for some time. Linux-supported ARM architecture systems at one point lacked standardization. This made Linux kernel development messy and confusing, with a ton of specific ARM board file initialization codes thrown into the Linux kernel project. Every main board with an ARM-based SoC or SiP had its own ARM board file in Linux. It got to the point that Linus Torvalds himself refused to accept new ARM board files, forcing manufacturers of ARM-based boards to adopt the device tree standard. This, of course, led to all the well-known benefits of standardization, including code reusability (device tree files can be imported and reused in other device tree files) and easy-to-understand hardware descriptions on embedded Linux.
HOW IS A DEVICE TREE USED BY LINUX?
When an embedded Linux system on an ARM-based SoC or SiP boots, the first piece of software that runs is the bootloader. Among its other tasks, the bootloader passes a device tree file to the Linux kernel. The kernel can then initialize the hardware described by the device tree in runtime, loading the correct device drivers for each peripheral, bus or external hardware wired to the main board.
A device tree is formatted in named nodes, where each node refers to a certain hardware item and contains information on its properties, such as its type (CPU, memory, bus, shift-register, and so on), memory sizes and addresses, and GPIO/pins the peripheral is wired to.
Listing 1 shows the device tree node description for an I2C bus. The node name “&i2c3” refers to the third I²C bus of the SoC/SiP. The “clock-frequency” property refers to the I²C bus’s clock frequency, as you might expect, which here is 100kHz. This node contains a sub-node that refers to a peripheral that makes use of or is attached to this I²C bus. It describes an EEPROM memory (name: “eeprom”) whose I²C address is set to 0x50.
Listing 1
Example of a device tree node description for an I2C bus
&i2c3 {
clock-frequency = <100000>;
eeprom@50 {
compatible = “ti,eeprom”;
reg = ;
};
};
The sub-node property “compatible” informs the Linux kernel which device driver to use for this device. During kernal execution, if a driver declares itself compatible with “ti,eeprom,” Linux kernel automatically associates this EEPROM described in the device tree to that driver (using EEPROM addresses and other properties described in its device tree). From this point on, the device described in the device tree will be supported by the Linux kernel and thus usable in the Linux system.
— ADVERTISMENT—
—Advertise Here—
Device tree files come in two forms in a Linux system: device tree sources files (.dts) and device tree blob files (.dtb). A .dts file is the source code of a device tree. It contains all the node descriptions in a human-readable form. A .dts file can import other .dts files, just like source codes in any programming language.
A .dtb file is the device tree binary file, the result of a compiled device tree. It can be generated from one or more .dts files. This is the file format handled by the bootloader and passed to the Linux kernel during board initialization. In the case of a PocketBeagle using version 4.9 of the Linux kernel, all device tree blobs are available in the /boot/dtbs/4.9.147-ti-r117 folder.
COMPILING AND DECOMPILING A DEVICE-TREE BLOB
In embedded Linux, you can quickly compile (.dts to .dtb) and decompile (.dtb to .dts) device trees in runtime via the appropriately titled Device Tree Compiler (dtc) software. This is a major advantage, as you don’t need to fully recompile the Linux kernel to change a device tree. Of course, you’ll need to reboot embedded Linux to use the newly compiled device tree blobs, but that’s still significantly faster than a full recompilation of the Linux kernel. As you might imagine, having this capability is especially convenient during the development phase of a project. Here’s the procedure to do so:
- Identify the device tree blob used for the main board.
- Decompile this .dtb to a .dts file using dtc (I’ll explain this further).
- Use any text editor to change what you need in the .dts file.
- Use dtc to recompile the .dts file to a .dtb file, replace the old .dtb file with this newly recompiled version, and reboot the board to check the results.
So, let’s get deeper into how to decompile and compile device tree blobs on the PocketBeagle board. The DT blob of the PocketBeagle is the file named am335x-PocketBeagle.dtb. To decompile it, you need to access the board via a terminal session (over SSH or UART). Adafruit has a great tutorial on how to do this over SSH [1]. (Note that the SSH access process for PocketBeagle is the same as that for the BeagleBone Black.)
Once you’ve accessed the board via a terminal session, go to the device tree blobs folder with the command:
cd /boot/dtbs/4.9.147-ti-r117
Now, to decompile am335x-PocketBeagle.dtb via dtc, run the command:
sudo dtc -I dtb -O dts -o am335x-PocketBeagle.dts am335x-PocketBeagle.dtb
The “-I” parameter specifies the input file type (dtb, in this case), while the “-O” parameter specifies the output file type (dts, here). As I mentioned, dtc is used to both compile and decompile device tree blobs, so you can switch those parameter specifications as needed.
After running that last command, the device tree source will be available in the am335x-PocketBeagle.dts file, and you can read and modify it with a text editor.
Once you’ve made your desired changes and are ready to compile am335x-PocketBeagle.dts, run the command:
sudo dtc -I dts -O dtb -o am335x-PocketBeagle.dts am335x-PocketBeagle.dtb
As you’ll observe, the parameter specifications have swapped places, just like I mentioned.
The device tree blob will now be available in the file am335x-PocketBeagle.dtb. As this process was executed in the board’s device tree blobs folder, the compile process will automatically replace the previous .dtb file with the newly recompiled one.
ADDING AN LED
Now for the LED project I described earlier. To reproduce this project, you’ll need the following:
— ADVERTISMENT—
—Advertise Here—
- PocketBeagle board with pin headers soldered to the board
- A microSD card flashed with the PocketBeagle board image (See the BeagleBoard link on Circuit Cellar’s article materials page for more details on how to do this [2].)
- An LED (any color you like)
- A 470Ω/0.25W resistor
Figure 2 is the wiring diagram for the circuit in this project. The LED is controlled by GPIO 57. We’re going to add this external LED to the board’s device tree source, then compile the source to a new device tree blob. Our goal is to control the LED with the Linux kernel LED subsystem/device driver. In other words, the LED will behave (and will be controlled) as any other “officially supported” built-in LED.
Diagram for wiring an external LED to PocketBeagle Board
To better demonstrate this, the LED’s default trigger (which describes how the LED behaves) will be the CPU’s “heartbeat.” In other words, its blink frequency will reflect the CPU load—a busier CPU means the LED blinks faster. I’ve included a link on Circuit Cellar’s article materials page that shows how the heartbeat trigger works under the hood, for those interested [3].
PocketBeagle built-in user LEDs (from USR0 to USR3)
First, identify where in the device tree source file the PocketBeagle’s built-in LEDs are described. To do this, take a closer look at the PocketBeable’s built-in LEDs names on the board itself. You’ll notice the PocketBeagle board has four built-in LEDs, identified from USR0 to USR3, as seen in Figure 3. Now, if you search for “leds” in the am335x-PocketBeagle.dts file, you’ll find the LEDs’ nodes and their sub-nodes (each one referring to a user LED, from beaglebone:green:usr0 to beaglebone:green:usr3), as seen on Listing 2. Note that each LED has these properties:
- Label—the name it will display in embedded Linux
- GPIOs—some SiP-specific codes that refer to the GPIO to which the LED is wired
- linux,default-trigger—the default LED trigger behavior
- default-state—the default state of the referred LED
Listing 2
LEDs node and its sub-nodes
leds {
pinctrl-names = “default”, “sleep”;
pinctrl-0 = <0x197>;
pinctrl-1 = <0x198>;
compatible = “gpio-leds”;
led@2 {
label = “beaglebone:green:usr0”;
gpios = <0x196 0x15 0x0>;
linux,default-trigger = “heartbeat”;
default-state = “off”;
};
led@3 {
label = “beaglebone:green:usr1”;
gpios = <0x196 0x16 0x0>;
linux,default-trigger = “mmc0”;
default-state = “off”;
};
led@4 {
label = “beaglebone:green:usr2”;
gpios = <0x196 0x17 0x0>;
linux,default-trigger = “cpu0”;
default-state = “off”;
};
led@5 {
label = “beaglebone:green:usr3”;
gpios = <0x196 0x18 0x0>;
linux,default-trigger = “mmc1”;
default-state = “off”;
};
};
Here’s the good news: you found where to add the external LED in the device tree source. But to do so, you need to know what to enter for these properties I just listed. The label’s easiest. To be in accordance with the other LED names, the label beaglebone:green:extLed is a logical choice for the external LED. In this case, the linux,default-trigger property value is also straightforward: “heartbeat.” The default-state can be set as off, because it will be overwritten by the LED trigger. So, the only value missing is that for the GPIOs property.
To understand the SiP-specific codes mentioned in the GPIOs bullet point, you’d need to read the massive SiP datasheet and user manuals. If you do want to get a full understanding of how the PocketBeagle SiP works, see the Circuit Cellar article materials page for a link to its documentation [4]. Luckily, there’s a shortcut for getting the required GPIOs property value. As the wiring diagram in Figure 2 shows, the external LED will be wired to the P2_06 pin. According to the PocketBeagle board pinout, P2_06 corresponds to GPIO 57, as circled in red in Figure 4.
PocketBoard pinout
Each GPIO/pin of PocketBeagle’s SiP is described in am335x-PocketBeagle.dts. Listing 3 shows the node in the dts for P2_06. Note the codes listed in the GPIO field: <0x196 0x19 0x0>. This is exactly what you enter for the GPIO property of the external LED. Listing 4 shows the sub-node with all the necessary information for the external LED, and Listing 5 shows the full node for all of the LEDs.
Listing 3
P2_06 (or GPIO 57) node
P2_06 {
gpio-name = “P2_06”;
gpio = <0x196 0x19 0x0>;
input;
dir-changeable;
};
Listing 4
External LED sub-node
led@6 {
label = “beaglebone:green:extLed”;
gpios = <0x196 0x19 0x00>;
linux,default-trigger = “heartbeat”;
default-state = “off”;
};
Listing 5
Full LEDs node, containing the external LED
leds {
pinctrl-names = “default”, “sleep”;
pinctrl-0 = <0x197>;
pinctrl-1 = <0x198>;
compatible = “gpio-leds”;
led@2 {
label = “beaglebone:green:usr0”;
gpios = <0x196 0x15 0x0>;
linux,default-trigger = “heartbeat”;
default-state = “off”;
};
led@3 {
label = “beaglebone:green:usr1”;
gpios = <0x196 0x16 0x0>;
linux,default-trigger = “mmc0”;
default-state = “off”;
};
led@4 {
label = “beaglebone:green:usr2”;
gpios = <0x196 0x17 0x0>;
linux,default-trigger = “cpu0”;
default-state = “off”;
};
led@5 {
label = “beaglebone:green:usr3”;
gpios = <0x196 0x18 0x0>;
linux,default-trigger = “mmc1”;
default-state = “off”;
};
led@6 {
label = “beaglebone:green:extLed”;
gpios = <0x196 0x19 0x00>;
linux,default-trigger = “heartbeat”;
default-state = “off”;
};
}
Having added the external LED sub-node to the LEDs node, save the am335x-PocketBeagle.dts file and compile the device tree blob by running the following command:
sudo dtc -I dts -O dtb -o am335x-PocketBeagle.dts am335x-PocketBeagle.dtb
Make sure to move the newly compiled am335x-PocketBeagle.dtb to the /boot/dtbs/4.9.147-ti-r117 folder, and you’re all set. After rebooting the board, the external LED will be handled by the Linux kernel LED sub-system and behave as the other build-in LEDs. You can see the external LED in the /sys/class/leds/ folder, as shown in Figure 5.
External LED in sysfs, handled as the other built-in board LEDs
CONCLUSION
This article covers the basics of device trees in embedded Linux. I hope this helped you understand not only how to read device tree files, but how to change them and how to add a new device into an embedded Linux system—and not just LEDs, of course, but any peripheral that is supported by device trees. Hence, you can build more complex—and more exciting—embedded Linux projects that interact with multiple peripherals. Please feel free to reach out to Circuit Cellar if you have any questions about details in this piece.
REFERENCES
[1] Tutorial on SSH to BeagleBone Black over USB: https://learn.adafruit.com/ssh-to-beaglebone-black-over-usb[2] Getting started with Beagleboard: https://beagleboard.org/getting-started#update[3] Heartbeat trigger code: https://elixir.bootlin.com/linux/v4.8/source/drivers/leds/trigger/ledtrig-heartbeat.c[4] PocketBeagle SiP documentation: http://octavosystems.com/octavo_products/osd335x-sm/
SOURCES
Book: Derek Molloy. “Exploring Beaglebone: Tools and Techniques for Building with Embedded Linux (second edition)”, Wiley, 2019
PocketBeagle official website: https://beagleboard.org/pocket
Article: “Linux e o suporte à device tree – Parte 2”, by Sergio Prado: https://sergioprado.org/linux-e-o-suporte-a-device-tree-parte-2/
RESOURCES
Octavo Systems | octavosystems.com
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MAY 2023 #394 – Get a PDF of the issue
Sponsor this Article