Linux and Hardware: A Top-Down Perspective
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
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!
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
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)
Layer 1: Adafruit BBIO
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
. 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
It should not be too hard to understand what this code does. With the
function, we set up USR3 as an output (we are trying to write
data — send data to an LED to turn on — not read
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
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
(in the root directory). If you navigate to
and display its contents, you’ll get the following:
As you can see,
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
directory, where we can directly interact with GPIO1_24 (if we perform some additional configurations). We’ll opt for an alternative method: the
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
, and looks at its contents:
These files essentially allow us to configure USR3. The two files we are interested in are
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
, and any value between 1 and 255 will correspond to
. To set the brightness (i.e. turn USR3 on and off), we just write our desired value to the
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
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
designed for hardware that our PocketBeagle does not support. It seems like
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
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
directory (i.e. using sysfs). Now, we want to know: How does Linux determine what hardware to include in the
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.
. In particular, on line 26 of the device tree, you’ll see the following
We’ll take a high-level look at this node. Notice that
contains a property called
, which is set to
. This will be important to us in the next section. Also, notice that the
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
subnode. Many of
’s properties should look familiar. For starters, its
is set to
. Believe it or not, we’ve seen this string already. If you go back to
(sysfs), you’ll see that we found USR3’s configuration files in
. So, this
is what determines USR3’s directory name in sysfs! If you wanted to, you could modify the PocketBeagle’s device tree and change
’s label to
. Then, after recompiling the device tree and properly configuring your PocketBeagle, you’d find USR3’s configuration files in
Let’s take a look at another property,
, whose value is set to
<&gpio1 24 GPIO_ACTIVE_LOW>
. This syntax is a little strange, but notice that this value contains
. Which GPIO is USR3 connected to? GPIO1_24! This is how Linux knows to correspond the configuration files in
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
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
with a BeagleBoard, in which case you’ve probably worked with device tree overlays (DTOs). For instance, if you are working with the
and want to use its accelerometer, you have to load a DTO called
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
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:
Layer 4: Device Drivers
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
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
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
file in sysfs, that was actually calling
under the hood, allowing us to turn on USR3. As it turns out, that
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
, passing the value 1 in as an argument.
Essentially, the sysfs configuration files are just a facade — the kernel disguises
’s function calls as files in
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
node we discussed in the previous section:
The key here is the
property, which is set to “gpio-leds” on line 30 of the device tree. Looking back at the
driver, on line 197 we see the following:
We have a struct that contains a
field, which is also set to “gpio-leds”. Essentially, when
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
field is set to ‘gpio-leds, ’ I should use the
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
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
property and sees that it is set to “gpio-leds”. From there, Linux asks, “Which driver can I use with this device?” Remembering that
also has a
property set to “gpio-leds”, Linux associates (or “binds”) the
But how is the code in
executed? Better yet, when is the code in
executed? You may have noticed that
function, so which function gets called first? As it turns out, no functions are called until Linux binds the
device with the
driver. As soon as this happens, though, Linux calls
’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
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
, a lot of the probe function is abstracted away with library calls. Nonetheless, you can imagine that this probe function sets up the
file we found with sysfs and establishes a callback between writes to that file and the
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
subnode contain a
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
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.
 You may be wondering: Does the
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.