The Raspberry Pi has a set of General Purpose I/O (GPIO) pins that you can use to control homemade electronic projects. Most of the Raspberry Pi starter kits include a breadboard and a few electronic components to play with. In this chapter, we will look at programming GPIO pins from Assembly language.
We will experiment with a breadboard containing a number of LEDs and resistors, so we can write some real code. We will program the GPIO pins two ways, firstly by using the included Linux device driver and secondly by accessing the GPIO controller’s registers directly.
GPIO Overview
Pins 1 and 17: Provide +3.3V DC power
Pins 2 and 4: Provide +5V DC power
Pins 6, 9, 14, 20, and 25: Provide electrical ground
Pins 3, 5, 7–8, 10–13, 15, 16, 18, 19, 21–24, and 26: Are programmable general purpose
For the programmable pins, we can use them for output, where we control whether they output power or not (are binary 1 or 0). We can read them to see if power is provided, for instance, if it is connected to a switch.
However, this isn’t all there is to GPIO; besides the functions we’ve talked about so far, a number of the pins have alternate functions that you can select programmatically. For instance, pins 3 and 5 can support the I2C standard that allows two microchips to talk to each other.
There are pins that can support two serial ports which are handy for connecting to radios or printers. There are pins that support pulse width modulation (PWM) and pulse-position modulation (PPM) that convert digital to analog and are handy for controlling electric motors.
In Linux, Everything Is a File
The model for controlling devices in Linux is to map each device to a file. The file appears under either /dev or /sys and can be manipulated with the same Linux service calls that operate on regular files. The GPIO pins are no different. There is a Linux device driver for them that then controls the pin’s operations via application programs opening files then reading and writing data to them.
The files to controlling the GPIO pin all appear under the /sys/class/gpio folder. By writing short text strings to the files here, we control the operation of the pins.
/sys/class/gpio/gpio17/direction: Used to specify whether the pin is for input or output
/sys/class/gpio/gpio17/value: Used to set or read the value of the pin
/sys/class/gpio/gpio17/edge: Used to set an interrupt to detect value changes
/sys/class/gpio/gpio17/active_low: Used to invert the meaning of 0 and 1
The next thing we do is set the direction for the pin, either use it for input or for output. We either write “in” or “out” to the direction file to do this.
Now we can write to the value file for an output pin or read the value file for an input pin. To turn on a pin, we write “1” to value, and to turn it off, we write “0”. When activated, the GPIO pin provides +3.3V.
When we are done with a pin, we should write its pin number to /sys/class/gpio/unexport. However, this will be done automatically when our program terminates.
We can do all this with the macros we created in Chapter 7, “Linux Operating System Services,” in fileio.s. In fact, by providing this interface, you can control the GPIO pins via any programming language capable of reading and writing files, that is pretty much every single one. Raspbian includes some special libraries to control the GPIO pins for Python and Scratch to make it easier, but behind the scenes they are just making the file I/O calls we are describing.
Flashing LEDs
To demonstrate programming the GPIO, we will connect some LEDs to a breadboard and then make them flash in sequence.
We will connect each of three LEDs to a GPIO pin (in this case 17, 27, and 22), then to ground through a resistor. We need the resistor because the GPIO is specified to keep the current under 16mA, or you can damage the circuits. Most of the kits come with several 220 Ohm resistors. By Ohm’s law, I = V / R, these would cause the current to be 3.3V/220Ω = 15mA, so just right. You need to have a resistor in series with the LED since the LED’s resistance is quite low (typically around 13 Ohms and variable).
WARNING: LEDs have a positive and negative side. The positive side needs to connect to the GPIO pin; reversing it could damage the LED.
Macros to control the GPIO pins
Main program to flash the LEDs
This program is a straightforward application of the Linux system service calls we learned in Chapter 7, “Linux Operating System Services.”
Moving Closer to the Metal
For Assembly language programmers, the previous example is not satisfying. When we program in Assembly, we are usually directly manipulating devices for performance reasons or to perform operations that simply can’t be done in high-level programming languages. In this section, we will interact with the GPIO controller directly.
WARNING: Make sure you back up your work before running your program, since you may need to power off and power back on again. The GPIO controller controls 54 pins; the Raspberry Pi only exposes either 26 or 40 of them, depending on the Pi model, for external use; many of the others are used by the Raspberry Pi for other important tasks. In the previous section, the device driver provided a level of protection, so we couldn’t easily do any damage. Now that we are writing directly to the GPIO controller, we have no such protection; if we make a mistake and manipulate the wrong pins, we may interfere with the Raspberry Pi’s operation and cause it to crash or lock up.
Virtual Memory
We looked at how to access memory in Chapter 5, “Thanks for the Memories,” and we looked at the memory addresses our instructions are stored at in gdb. These memory addresses aren’t physical memory addresses; rather, they are virtual memory addresses. As a Linux process, our program is given a 4 GB virtual address space. 3 GB of this is for us and 1 GB is for system things. Within this address space, some of it is mapped to physical memory to store our Assembly instructions, our .data sections, and our 8 MB stack. Furthermore, Linux may swap some of this memory to secondary storage like the SD card as it needs more physical memory for other processes. There is a lot of complexity in the memory management process to allow dozens of processes to run independently of each other, each thinking it has the whole system to itself.
In the next section, we want access to specific physical memory addresses, but when we request that access, Linux returns a virtual memory pointer that is different than the physical address we asked for. This is okay, as we know that behind the scenes the memory management hardware in the Raspberry Pi will be doing the memory translations between virtual and physical memory for us.
About Raspberry Pi 4 RAM
You might wonder why the Raspberry Pi 4 comes with up to 4 GB of RAM, but our process can only access 3 GB of it? However, all this RAM will be used, since each process and the kernel can have up to 3 GB of RAM. In fact, the memory controller in the Raspberry Pi has 40 address pins, so it can address more than 4 GB of physical memory.
In the future, if there is a 16 GB version of the Pi, that memory can be used, even if Raspbian is still 32 bits. Every 32-bit process could map different sections of memory, so even though a 32-bit process can only access 3 GB of memory at a time, it can use more by swapping parts of its virtual address space to different physical regions.
In Devices, Everything Is Memory
The GPIO controller has 41 registers. We can’t read or write these like the ARM CPU’s registers. The ARM32 instruction set doesn’t know anything about the GPIO controller, and there are no special instructions to support it. The way we access these registers is by reading and writing to specific memory locations. There is circuitry in the Raspberry Pi’s system on a chip (SoC) that will see these memory reads and writes and redirect them to the GPIO’s registers. This is how most hardware communicates. This is the job of the Linux device drivers, to translate these memory register accesses into a standard set of file I/O calls.
The memory address for the GPIO registers on the Raspberry Pi 2, 3, and 4 is 0x3F200000 (for the Raspberry Pi 0 and 1, it is 0x20200000). Sounds easy—we know how to load addresses into registers, then reference the memory stored there. Not so fast, if we tried this, our program would just crash with a memory access error. This is because these memory addresses are outside those assigned to our program, and we are not allowed to use them. Our first job then is to get access.
- 1.
Open the file /dev/mem.
- 2.Then we ask /dev/mem to map the registers for GPIO into our memory space. We do this with the Linux mmap2 service. Mmap2 takes the following parameters:
R0: Hint for the virtual address we would like. We don’t really care and will use NULL, which gives Linux complete freedom to choose.
R1: Length of region. Should be a multiple of 4096, the memory page size.
R2: Memory protection required.
R3: File descriptor to open /dev/mem.
R4: Offset into physical memory in 4096-byte pages (we’ll use 0x3f200000/4096).
This call will return a virtual address in R0 that maps to the physical address we asked for. The original mmap took an offset in bytes for the physical address; this restricted the call to mapping the first 4 GB of memory. The newer mmap2 call takes the address in pages allowing a greater range of physical addresses without the need to go to full 64 bits. This function returns a small negative number if it fails.
Registers in Bits
We will cover just those registers we need to configure our pins for output, then to set the bits to flash the LEDs. If you are interested in the full functionality, then check the Broadcom datasheet for the GPIO controller.
Although we’ve mapped these registers to memory locations, they don’t always act like memory. Some of the registers are write-only, and if we read them, we won’t crash, but we’ll just read some random bits. Broadcom defines the protocol for interacting with the registers; it's a good idea to follow their documentation exactly. These aren’t like CPU registers or real memory. The circuitry is intercepting our memory reads and writes to these locations, but only acting on things that it understands. In the previous sections, the Linux device driver for the GPIO hid all these details from us.
GPIO Function Select Registers
GPIO Function Select Registers
No. | Address | Name | Pins |
---|---|---|---|
0 | 0x3f200000 | GPSEL0 | 0–9 |
1 | 0x3f200004 | GPSEL1 | 10–19 |
2 | 0x3f200008 | GPSEL2 | 20–29 |
3 | 0x3f20000c | GPSEL3 | 30–39 |
4 | 0x3f200010 | GPSEL4 | 40–49 |
5 | 0x3f200014 | GPSEL5 | 50–53 |
- 1.
Read the register.
- 2.
Set the bits for our register.
- 3.
Write the value back.
Note
We must be careful not to affect other bits in the register.
Pin number and corresponding bits for the GPSEL1 register
Pin no. | GPSEL1 bits |
---|---|
10 | 0–2 |
11 | 3–5 |
12 | 6–8 |
13 | 9–11 |
14 | 12–14 |
15 | 15–17 |
16 | 18–20 |
17 | 21–23 |
18 | 24–26 |
19 | 27–29 |
We store 000 in the 3 bits if we want to input from the pin, and we store 001 in the bits if we want to write to the pin.
GPIO Output Set and Clear Registers
The GP set and clear pin registers
No. | Address | Name | Pins |
---|---|---|---|
0 | 0x3f20001c | GPSET0 | 0–31 |
1 | 0x3f200020 | GPSET1 | 32–53 |
2 | 0x3f200028 | GPCLR0 | 0–31 |
3 | 0x3f20002c | GPCLR1 | 32–53 |
These registers are write-only. You should set the bit for the register you want (with all the other bits 0) and write that bit. Reading these registers is meaningless.
The Broadcom datasheet states this as a feature, in that they save you reading the register first, then it's easier to just set a single bit than edit a bit in a sequence of bits. However, it could also be that this saved them some circuitry and reduced the cost of the controller chip.
More Flashing LEDs
GPIO support macros using mapped memory
Main program for the memory mapped flashing lights
The main program is the same as the first example, except that it includes a different set of macros.
The first thing we need to do is call the mapMem macro. This opens /dev/mem and sets up and calls the mmap2 service as we described in the section “In Devices, Everything Is Memory.” We store the returned address into R8, so that it is easily accessible from the rest of the macros. There is error checking on the file open and mmap2 calls since these can fail.
Root Access
If you don’t, then the file open will fail. We didn’t have to do this with the last program, because the GPIO device driver keeps everything safe. Accessing /dev/mem is very powerful and gives you access to all memory and all hardware devices.
This is a restricted operation, so we need to be root. Programs that directly access memory are usually implemented as Linux device drivers or kernel loadable modules, but then installing these also requires root access. A virus or other malware would love to have access to all physical memory.
Table Driven
We won’t cover multiplication or division until Chapter 10, “Multiply, Divide, and Accumulate”; without these, it’s hard to compute the pin offsets inside these registers. Division is a slow operation, and Assembly language programmers tend to avoid it. The common workaround is to use a table of precomputed values, rather than calculating the values as we need them. A table lookup is very fast, and we examined all the features in the ARM instruction set to help us do this in Chapter 5, “Thanks for the Memories.”
- 1.
The offset to the select register (from the base memory address)
- 2.
The bit offset in select register for this pin
- 3.
The bit offset in set and clr register
With these in hand, accessing and manipulating the GPIO control registers is a snap.
Note
We only populate these tables for the three pins we use.
Setting Pin Direction
Setting and Clearing Pins
Setting and clearing pins is easier, since we don’t need to read the register first. We just need to construct the value to write to it and execute it.
Clearing the pin is the same, except that we use the clear register rather than the set register.
Summary
- 1.
Using the GPIO device driver by accessing the files under /sys/class/gpio
- 2.
Using direct memory access by asking the device driver for /dev/mem to give us a virtual block of memory corresponding to the GPIO’s control registers
Controlling devices are a key use case for Assembly language programming. Hopefully, this chapter gave you a flavor for what is involved.
In Chapter 9, “Interacting with C and Python,” we will learn how to interact with high-level programming languages like C and Python.