Linux and Hardware: A Top-Down Perspective

In this series of posts, we will explore Linux's hardware-software interface using a BeagleBoard.

Categories: Beginner

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
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)

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:

The PocketBeagle's User (USR) LEDs

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:

Using the Adafruit BBIO Python library to blink USR3

(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:

Exploring the contents of sysfs

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:

Looking at the leds directory of sysfs

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:

Examining USR3's configuration files

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:

Checking USR3's maximum brightness value

From this, we see that USR3’s brightness can take on a value between 0 and 255 [1]. 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:

Toggling USR3 by writing to sysfs files

And, with that, we have successfully toggled our LED on and off using only sysfs [2]. You may have noticed the close correspondence between our Adafruit BBIO code and sysfs commands:

Comparing Adafruit BBIO code with 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.).

You can find the PocketBeagle’s device tree
here

.
Notice the nested structure of the device tree. Essentially, the tree is broken up into nodes with the following format:

node-name {

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:

The leds node in the PocketBeagle's device tree

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
capes

with a BeagleBoard, in which case you’ve probably worked with device tree overlays (DTOs). For instance, if you are working with the
TechLab

and 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:

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
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:

An example of a function in leds-gpio.c

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 leds node in the PocketBeagle's device tree (revisited)

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:

Setting leds-gpio.c's compatible property

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:

leds-gpio.c's probe function

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.

Useful resources:

Conclusion

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

[1] How do we know this? You can find the LED sysfs documentation
here.
You can find similar sysfs documentation for other peripherals in the
kernel’s main documentation.

[2] 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.

Comments are not currently available for this post.