Linux is one of the largest open source projects in existence. Whether it’s being used on a personal computer or a server, Linux plays a crucial role in our society. Being so versatile, it should come as no surprise that the Linux kernel provides a powerful interface between hardware (LEDs, motors, etc.) and software (shell scripts, C files, etc.). In this post, we will explore this interface using a BeagleBoard.
Users who are new to BeagleBoard -- or any microcontroller running Linux -- will find it easy to perform simple tasks, like blinking an LED. For instance, BeagleBoard provides high-level tools like bonescript and the Adafruit BBIO Python library for interacting with GPIO and other peripherals. These libraries, while simple to use, provide an opaque layer of abstraction. In other words, while these tools are great starting points for learning embedded Linux, they make it difficult to appreciate all of Linux’s background machinery when it comes to working with hardware.
For instance, consider the situation where you’ve found a new piece of hardware that is not supported by bonescript or Adafruit BBIO. Without a deeper knowledge of the kernel, you’ll be stuck waiting for someone to add support for this hardware. On the other hand, if you understand the software-to-hardware pipeline of Linux, it will be much easier for you to take action and add the support yourself. Even if you never find yourself in the aforementioned situation, learning about this pipeline will allow you to appreciate the flexibility of Linux, i.e. how it can be used on so many different hardware platforms.
In this article, we will take a top-down approach to Linux’s hardware-software interface. In particular, we will start off by looking at the Adafruit BBIO Python library. From there, we will dig deeper and deeper into the kernel, removing one layer of abstraction at a time. By the end of the article, you should have a more complete understanding of Linux as a whole, and you’ll also have some great jumping-off points for learning more. Let’s get started!Prerequisites
This series of posts have been designed to be accessible to beginners. In order to get the most out of these posts, it may be helpful to have:
- A basic understanding of hardware/microcontrollers (LEDs, GPIOs, etc.)
- A little bit of experience with BeagleBoards -- we’ll be using a PocketBeagle, but any BeagleBoard should do
- Experience accessing hardware through SSH, using a tool like PuTTY
- Knowledge of basic Linux commands (cd, ls, cat, echo, redirection with ‘>’)
- Experience programming with a high-level language (e.g. Python)
- Experience with basic C syntax (this will only be helpful for the last section)
We begin with (arguably) the highest level of abstraction: Adafruit BBIO. This Python library allows you to easily interact with a BeagleBoard’s hardware. As an example, we will blink USR3, the on-board LED closest to the PocketBeagle’s microUSB connector:
An important thing to note about USR3 is that it is connected to GPIO1_24. This information can be found in the PocketBeagle’s System Reference Manual, Section 6.5. This means we can blink USR3 by simply turning GPIO1_24 on and off.
To accomplish this with Adafruit BBIO, we start a Python REPL instance in the shell by typing python3. We then type the following commands:
(note that I’m assuming you have installed the Adafruit_BBIO library; if you haven’t, follow the “Installation on Debian” instructions here)
It should not be too hard to understand what this code does. With the GPIO.setup function, we set up USR3 as an output (we are trying to write out data -- send data to an LED to turn on -- not read in data). Notice that the Adafruit BBIO library allows us to refer to the USR3 LED with the string “USR3”, rather than something more complicated like “GPIO1_24”. This is a nice convenience, and it isn’t hard to imagine that, under the hood, Adafruit BBIO is just decoding “USR3” to “GPIO1_24”. We then use GPIO.output to turn on USR3, which will turn on our LED. Finally, after waiting for a second, we turn off USR3. Pretty simple!
Of course, Adafruit BBIO’s appeal is in its simplicity. Writing the code above only required knowledge of Python -- we really didn’t have to know anything about the hardware, aside from the fact that USR3 exists. So, overall, Adafruit BBIO provides a nice, clean abstraction from the BeagleBoard’s hardware. This may be especially appealing to someone who is mostly interested in software.
However, if you are interested in hardware, you may have a lot of questions. What is going on “behind the scenes” here? How does this library know that USR3 exists (try typing in a non-existent LED, like USR5, and the library will complain)? How is Python able to reach out to the processor on the PocketBeagle (i.e. the TI AM3358) and tell it to supply a voltage to GPIO1_24? There seems to be a mysterious disconnect between the hardware and software with the Adafruit BBIO code, and this could be frustrating to some people.
As it turns out, Adafruit BBIO does not turn on GPIO1_24 by itself. Adafruit BBIO has some help from sysfs, which is our next topic.Layer 2: sysfs
One of Linux’s mantras is the following
Everything is a file
This mantra extends to Linux’s hardware-software interface: We can use Linux’s file system to interact with our PocketBeagle’s hardware. The standard way of doing this is through sysfs, a (pseudo) file system designed for hardware interaction. In this section, we will blink USR3 using sysfs.
The sysfs file system can be found in /sys (in the root directory). If you navigate to /sys and display its contents, you’ll get the following:
As you can see, /sys contains a few directories. From the directory names alone (devices, firmware, power, etc.), you can already see that sysfs is closely tied to the hardware. We want to interact with USR3, and there are a few ways of doing this. One way is through the /sys/class/gpio directory, where we can directly interact with GPIO1_24 (if we perform some additional configurations). We’ll opt for an alternative method: the /sys/class/leds directory. This directory provides control over the on-board LEDs. Displaying its contents, we find the following:
We have four directories (or symbolic links that point to directories). As you may have guessed, these correspond to the four USR LEDs on the PocketBeagle. Like we did in our previous example, we want to blink USR3. So, we head to beaglebone:green:usr3, and looks at its contents:
These files essentially allow us to configure USR3. The two files we are interested in are max_brightness and brightness. The max_brightness file tells us the range of numbers we can set USR3’s brightness to. We can read this file like we read any other file in Linux:
From this, we see that USR3’s brightness can take on a value between 0 and 255 . As it turns out, USR3 does not support dimming, so a value of 0 will correspond to off, and any value between 1 and 255 will correspond to on. To set the brightness (i.e. turn USR3 on and off), we just write our desired value to the brightness file. We can easily do this with standard Linux redirection. So, to recreate our Adafruit BBIO program, we do the following:
And, with that, we have successfully toggled our LED on and off using only sysfs . You may have noticed the close correspondence between our Adafruit BBIO code and sysfs commands:
This should give you some insight into how Adafruit BBIO is working under the hood!
It definitely feels like we have dug deeper into the kernel with sysfs: We are now interacting directly with the file system. However, you may not be satisfied yet -- there are a lot of unanswered questions. If you spend some time exploring the /sys directory, you’ll find folders and configuration files for a ton of peripherals: GPIO, SPI, I2C, etc. Surprisingly, these files are specific to our PocketBeagle; in other words, we won’t find any files in /sys designed for hardware that our PocketBeagle does not support. It seems like /sys was tailor-made for our PocketBeagle.
How is this possible? How does Linux know exactly what kind of hardware we have on our board? Moreover, when we write to USR3’s brightness file, how does Linux know that the contents of this file should determine the state of GPIO1_24? To answer these questions, we’ll have to remove another layer of abstraction and look at device trees. In doing this, we’ll discover how Linux is flexible enough to run on BeagleBoards, Raspberry Pis, cell phones, servers, etc.Layer 3: Device Trees / Device Tree Overlays
We have successfully toggled USR3 by writing to files in the /sys directory (i.e. using sysfs). Now, we want to know: How does Linux determine what hardware to include in the /sys directory? In this section, we’ll answer that question by taking a closer look at device trees.
A device tree is essentially a file, with a particular syntax, that describes hardware. When the Linux kernel is started, it knows where to look for the device tree file. Once the kernel has this file, it proceeds to read and parse it, extracting information about all of the hardware on the board you’re using (whether that be a PocketBeagle, an Android cell phone, etc.).
property1-name = “property1-value”;
property2-name = “property2-value”;
where nodes can be nested in one another. We won’t go over all of the details of the device tree format, but there are plenty of resources online explaining its syntax. Some of these resources are included at the end of this section.
How do device trees tie into our LED example? As you scroll through the PocketBeagle’s device tree, you’ll likely find some peripheral “buzz words” jumping out at you, e.g. spi0_pins. In particular, on line 26 of the device tree, you’ll see the following leds node:
We’ll take a high-level look at this node. Notice that leds contains a property called compatible, which is set to gpio-leds. This will be important to us in the next section. Also, notice that the leds node contains 4 subnodes (usr0, usr1, usr2, usr3). It’s not hard to guess that these four subnodes correspond to the 4 USR LEDs on the PocketBeagle.
We have been dealing with USR3, so let’s focus on the usr3 subnode. Many of usr3’s properties should look familiar. For starters, its label is set to beaglebone:green:usr3. Believe it or not, we’ve seen this string already. If you go back to Layer 2 (sysfs), you’ll see that we found USR3’s configuration files in /sys/class/leds/beaglebone:green:usr3. So, this label is what determines USR3’s directory name in sysfs! If you wanted to, you could modify the PocketBeagle’s device tree and change usr3’s label to my_favorite_led. Then, after recompiling the device tree and properly configuring your PocketBeagle, you’d find USR3’s configuration files in /sys/class/leds/my_favorite_led.
Let’s take a look at another property, gpios, whose value is set to <&gpio1 24 GPIO_ACTIVE_LOW>. This syntax is a little strange, but notice that this value contains gpio1 and 24. Which GPIO is USR3 connected to? GPIO1_24! This is how Linux knows to correspond the configuration files in /sys/class/leds/beaglebone:green:usr3 to GPIO1_24.
This is all great, and hopefully you are starting to see how Linux can interface with a variety of different hardware peripherals. When we were dealing with sysfs, we mentioned that the /sys directory only contains folders for hardware we actually have on the PocketBeagle. This is possible because we are using a device tree custom-made for the PocketBeagle -- that device tree tells Linux exactly what hardware we want to work with.
You may have experience using capeswith a BeagleBoard, in which case you’ve probably worked with device tree overlays (DTOs). For instance, if you are working with the TechLaband want to use its accelerometer, you have to load a DTO called PB-I2C2-ACCEL-TECHLAB-CAPE.dts.
You’ll notice that this DTO’s formatting is very similar to a device tree’s formatting. There’s a good reason for this: a DTO is used to modify a device tree, without actually having to modify the original device tree file itself. Essentially, a DTO either adds nodes to a device tree, or overwrites existing nodes. In the case of the TechLab accelerometer’s DTO, a few new properties are added. With these new properties, our PocketBeagle can tell Linux: “Hey! I know I don’t usually have an accelerometer, but, now that I have this new cape, I do have one. Please generate the necessary sysfs files so I can interact with this accelerometer.” Without the DTO, Linux has no way of knowing your PocketBeagle has access to an accelerometer.
DTOs add a level of modularity to device trees. This modularity, in turn, only increases Linux’s flexibility. You can find out more about DTOs in the resources at the end of this section.
With device trees and DTOs, you now know how to tell Linux what hardware you have at your disposal. If you were to build a new Linux device or BeagleBoard cape, you would want to develop a device tree or DTO to specify what hardware your device/cape has. You may still have some questions, though. For example, how do you know what properties to specify within a device tree node? Where did those property names/values come from? If you were to make a brand new hardware device, how can you make sure Linux interacts with it properly?
The problem is that device trees tell Linux what hardware you have, but not what to do with that hardware. So, to answer the above questions, we’ll have to dig a little deeper.
Some useful resources on device trees and device tree overlays:
We have made it to the fourth and final layer: device drivers. While device drivers are not necessarily the “deepest” layer of the kernel, there is no doubt that we have come a long way from the Adafruit BBIO library. After learning about device drivers, you should have a fairly comprehensive understanding of the hardware-software interface in Linux.
A device driver is just a program, usually written in C, that interacts with hardware. To understand Linux drivers, it helps to look at an example. We’ll take a look at a driver called leds-gpio.c, which we’ll soon see is very relevant to the work we’ve been doing thus far.
You’ll see that this driver is written in C, and contains a lot of different functions. We won’t go through this code in detail, but, just by skimming through it, you should see a lot of useful functions. For example, there is a function called gpio_led_set which, as you may imagine, can be used to turn an LED on or off:
There are a lot of “outside” helper functions called in this driver, but this driver is essentially where the pure hardware interaction occurs. In many drivers, you’ll see some bare-metal code (e.g. writing directly to a processor’s memory addresses, performing bitwise operations, etc.).
Although we’ve only taken a high-level look at this driver, you can likely see that it has the functionality we need to control USR3. Believe it or not, we’ve already been using this driver to control USR3. To elaborate, when we were writing the value “1” to USR3’s brightness file in sysfs, that was actually calling gpio_led_set under the hood, allowing us to turn on USR3. As it turns out, that brightness configuration file is not a standard text file -- when we wrote a 1 to it, the kernel didn’t just store the value 1 in a document. Instead, the kernel called gpio_led_set, passing the value 1 in as an argument.Essentially, the sysfs configuration files are just a facade -- the kernel disguises leds-gpio.c’s function calls as files in /sys/class/leds.
So, how does Linux know to associate (or “bind”) this driver, rather than some other driver, with USR3? Let’s take a look back at the PocketBeagle’s device tree, specifically the leds node we discussed in the previous section:
The key here is the compatible property, which is set to “gpio-leds” on line 30 of the device tree. Looking back at the leds-gpio.c driver, on line 197 we see the following:
We have a struct that contains a compatible field, which is also set to “gpio-leds”. Essentially, when MODULE_DEVICE_TABLE is called with that struct (line 202), the kernel is making a “mental note” of sorts. Basically, the kernel is saying “If I ever find a device tree node whose compatible field is set to ‘gpio-leds, ’ I should use the leds-gpio.c driver for that node.”
To solidify this point, let’s take a high-level (if not somewhat artificial) look at how Linux parses our PocketBeagle’s device tree. Linux reads through all of the nodes, eventually reaching the leds node. It then takes all of the properties in this node and stores them in a data structure. It then looks for the node’s compatible property and sees that it is set to “gpio-leds”. From there, Linux asks, “Which driver can I use with this device?” Remembering that leds-gpio.c also has a compatible property set to “gpio-leds”, Linux associates (or “binds”) the leds device with leds-gpio.c.
But how is the code in leds-gpio.c executed? Better yet, when is the code in leds-gpio.c executed? You may have noticed that leds-gpio.c has no main function, so which function gets called first? As it turns out, no functions are called until Linux binds the leds device with the leds-gpio.c driver. As soon as this happens, though, Linux calls leds-gpio.c’s “probe” function. The probe function is a standard across all Linux drivers, and you can just think of it as the function that is called when a binding occurs. The probe function for leds-gpio.c is found on line 250, a snippet of which is shown below:
Linux passes all of the device tree properties into this probe function. The probe function is responsible for handling all of the device set up from there. In leds-gpio.c, a lot of the probe function is abstracted away with library calls. Nonetheless, you can imagine that this probe function sets up the brightness file we found with sysfs and establishes a callback between writes to that file and the gpio_led_set function we explored previously.
This also allows us to answer a question we posed previously: What determines the properties of a device tree node? In other words, how do we know which properties are necessary to get our hardware working (e.g. why does the usr3 subnode contain a gpios property)? The answer: It depends on what driver that hardware is going to bind to. If you have a new hardware device with an existing driver that you want to use, look at that driver’s probe function and see what properties it makes use of! While there are some special Linux properties, the properties used in the probe function are essentially the only ones you need to include.
You can find many of Linux’s drivers in the kernel source tree. With that being said, looking through the drivers for each hardware device you want to use can be a hassle. Luckily, many driver developers document what device tree properties are needed to work with their drivers. This layer of abstraction allows you to use drivers without worrying about their underlying details. These documentation files are called the “Device Tree Bindings, ” and they can be found here. Note that some drivers do not have device tree bindings, meaning that it will be on you to find the driver’s source code and determine which device tree properties are necessary for your system.
And with that, we have wrapped up our discussion on Linux drivers. We have covered a lot of content in this post, and I do not expect anyone to walk away from these posts as an expert -- I certainly am not an expert yet! Nonetheless, I hope that these posts serve as a nice starting point for learning more about the Linux kernel.Notes
 You may be wondering: Does the max_brightness value matter? From my own experimentation, it does not. If I write echo 300 > brightness, USR3 turns on just fine. That being said, many sysfs subsystems will strictly follow their maximum settings and throw errors if you violate them. To play it safe, it is best to obey these maximums. You’ll be in a better position to understand how these maximums are enforced after we discuss device drivers.