Access Raspberry Pi GPIO with ARM64 assembly
The Three Rs
Reading, writing, and arithmetic with the Raspberry Pi in ARM64 assembly language.
In this article, I explore the Raspberry Pi's general purpose input/output (GPIO) system and look at how to use it to perform some basic input and output tasks with four separate programs that run simultaneously and communicate with each other. Table 1 lists the various programs discussed in this article [1].
Table 1
Code Files
File Name | Function |
---|---|
|
Count up or down in different number systems |
|
C++ header file with declarations for exports in |
|
Optional C++ |
|
Read and write four-digit, common-anode, seven-segment displays |
|
C++ header file with declarations for exports in |
|
Optional C++ |
|
A shared library with GPIO functions used by |
arm64_include.asm |
Contains constants and macros for ARM64 assembly language and describes the Raspberry Pi GPIO register map |
|
An LKM that handles interrupts from user space |
Recon
The first program, gpiocount.asm
, counts up or down in various number systems: binary, octal, decimal, and hexadecimal – or really, any number system up to base 16. The count mode is changed with a switch that causes an interrupt to a loadable kernel module. That's the arithmetic portion of the project.
The gpiocount
program writes its values to 4 bytes of memory that is shared with gpiomux.asm
, which runs in the background, reads the 4 bytes written by gpiocount
, and writes the bytes into a four-digit, seven-segment display. All segment lines of the four displays are wired together inside the chip so that, to see the separate digits, you must turn on the four displays one at a time, faster than the eye can detect. That takes care of the reading, writing, and arithmetic.
Both gpiocount
and gpiomux
need to use the basic GPIO functions, so these functions have been placed into a shared library, libgpio.asm
, which is assembled and linked to libgpio.so
created by Makefile
.
Each separate GPIO operation – reading a GPIO pin, writing a pin, setting a pin to input mode or output mode – is a function in libgpio
. Mapping the GPIO's hardware registers into the memory spaces of gpiocount
and gpiomux
is another operation performed by libgpio
. The gpiocount
program creates a shared memory space and gpiomux
reads the address of this shared memory. Those two functions are also placed in libgpio
.
Each of the three GPIO programs is placed in a separate ARM64 assembly language file. Both gpiocount.asm
and gpiomux.asm
have main
functions, but they can be conditionally compiled to replace these functions with countmain.cpp
and muxmain.cpp
(the default in Makefile
). Seeing the same code in both assembly language and C++ should make the code easy to follow.
Both countmain.cpp
and muxmain.cpp
print a lot of output to the console; however, I didn't bother with that in the assembly language versions. It's very easy to add printf
calls to the assembly language mains, though: Simply place the string you want to print in the read-only data section (.section .rodata
),
initstr: .asciz "countmain: initializing\n"
and place the following lines in the code section (.section .text
):
main: push2 x29, x30 ... adr x0, initstr bl printf ... pop2 x29, x30 ret
The fourth program is a loadable kernel module (LKM) written in C, lkm_gpio.c
, that allows you to interrupt the kernel through a switch connected to a GPIO pin. It forces the current counter to end, whereupon the gpiocount
function main
starts a new task. The LKM module is a standalone program with a separate project directory and Makefile
.
Accessing the GPIO
The Raspberry Pi system-on-chip (SoC) contains 54, 32-bit hardware registers that provide access to the GPIO pins located in a 40-pin header. Although you could read and write to hardware registers, just as if they were memory locations, the register addresses are not within the range available to the programs.
Linux provides a way to map these addresses into virtual addresses that you can read and write. To do this, open the character device /dev/gpiomem
as a file and call mmap
to map the GPIO hardware register addresses. The code for this operation is located in the mapOpen
subroutine in the file libgpio.asm
.
Most of the code in this article is written in ARM64 Assembly Language. Assembly language gets you very close to the hardware and is often preferred over a high-level language when dealing with hardware. Figure 1 shows the breadboard connected to the Raspberry Pi through a 40-pin header and a breadboard adapter, and Figure 2 shows the wiring diagram for the device.
To save hardware, access to the GPIO registers is assigned to bits in a register, not the whole register. The gpiomux.asm
program (Listing 1) provides a lookup table starting at line 279 that allows you to find the register addresses needed. For example, GPIO pin 6, which is assigned to segment e of the seven-segment display (lines 292-294), is in function select register 0, which manages GPIO pins 0-9. Bits 0-2 are assigned to GPIO pin 0, bits 3-5 to GPIO pin 1, bits 18-20 to GPIO pin 6, and so on.
Listing 1
gpiomux.asm
001 //======================================================== 002 // gpiomux.asm - Read and write 4-digit common-anode 7-segment displays 003 // John Schwartzman, Forte Systems, Inc. 004 // 03/07/2021 005 // ARM64 006 //======================================================== 007 .include "arm64_include.asm" // contains constants & macros 008 009 //==================== CODE SECTION ====================== 010 .section .text 011 012 .ifdef OPT //========= use main from this file ========== 013 014 .global main 015 016 //======================================================== 017 main: 018 push2 x29, x30 // push fp & lr 019 020 mov w0, #SIGHUP // prepare handleHangup - sig number 021 adr x1, handleHangup // - function adr 022 bl signal // invoke glibc signal() function 023 024 bl initialize // set up gpio and shared memory 025 bl readWrite // continuously display shared memory 026 bl cleanUp // restore and unmap gpio 027 028 mov w0, wzr // w0 = EXIT_SUCCESS 029 bl exit // invoke glibc exit() function 030 031 pop2 x29, x30 // pop fp & lr 032 ret 033 034 //======================================================== 035 036 .else // OPT != __MAIN__ //== use main from muxmain.cpp == 037 038 .global initialize, cleanUp, readWrite, setExitFlag 039 040 .endif //================================================ 041 042 //======================================================== 043 // Handle the SIGHUP signal: call cleanUp 044 handleHangup: 045 push2 x29, x30 // push fp & lr 046 bl setExitFlag // tell readWrite to exit 047 pop2 x29, x30 // pop fp & lr 048 ret 049 050 //======================================================== 051 setExitFlag: // tell readWrite to exit 052 adr x0, exitFlag // write ONE to memory location exitFlag 053 mov w1, #ONE 054 strb w1, [x0] 055 ret 056 057 //======================================================== 058 // Map virtual memory to /dev/gpiomem and set GPIO pins for input or output. 059 initialize: 060 push2 x29, x30 // push fp & lr 061 062 adr x0, digits 063 bl readSharedMemory 064 cmp x0, #MINUS_ONE // success? 065 beq fin // branch if no 066 067 adr x0, memdev 068 adr x1, gpiobase 069 bl mapOpen // map the memory 070 cmp x0, xzr // success? 071 bmi fin // branch if no 072 073 adr x29, gpiobase // save gpiobase 074 str x0, [x29] 075 076 adr x29, gpiobase // This step is necessary for all 077 ldr x29, [x29] // GPIO activity. x29 => gpiobase 078 079 adr x0, switch_0 // GPIO pin 17 used for switch 080 bl gpioDirectionIn 081 082 adr x0, seg_a // GPIO pin 20 083 bl gpioDirectionOut 084 085 adr x0, seg_b // GPIO pin 21 086 bl gpioDirectionOut 087 088 adr x0, seg_c // GPIO pin 19 089 bl gpioDirectionOut 090 091 adr x0, seg_d // GPIO pin 13 092 bl gpioDirectionOut 093 094 adr x0, seg_e // GPIO pin 06 095 bl gpioDirectionOut 096 097 adr x0, seg_f // GPIO pin 16 098 bl gpioDirectionOut 099 100 adr x0, seg_g // GPIO pin 12 101 bl gpioDirectionOut 102 103 adr x0, seg_dp // GPIO pin 26 104 bl gpioDirectionOut 105 106 adr x0, digit_0 // GPIO pin 18 107 bl gpioDirectionOut 108 109 adr x0, digit_1 // GPIO pin 23 110 bl gpioDirectionOut 111 112 adr x0, digit_2 // GPIO pin 24 113 bl gpioDirectionOut 114 115 adr x0, digit_3 // GPIO pin 25 116 bl gpioDirectionOut 117 118 adr x0, pin_22 // GPIO pin 22 119 bl gpioDirectionOut 120 121 mov x0, xzr // clear error flag 122 123 fin: 124 pop2 x29, x30 // pop fp & lr 125 ret 126 127 //======================================================== 128 cleanUp: // cleanup has no parameters 129 push2 x29, x30 // push fp & lr 130 131 adr x29, gpiobase // x29 => gpiobase 132 ldr x29, [x29] 133 134 adr x0, seg_a // GPIO pin 20 135 bl gpioDirectionIn 136 137 adr x0, seg_b // GPIO pin 21 138 bl gpioDirectionIn 139 140 adr x0, seg_c // GPIO pin 19 141 bl gpioDirectionIn 142 143 adr x0, seg_d // GPIO pin 13 144 bl gpioDirectionIn 145 146 adr x0, seg_e // GPIO pin 06 147 bl gpioDirectionIn 148 149 adr x0, seg_f // GPIO pin 16 150 bl gpioDirectionIn 151 152 adr x0, seg_g // GPIO pin 12 153 bl gpioDirectionIn 154 155 adr x0, seg_dp // GPIO pin 26 156 bl gpioDirectionIn 157 158 adr x0, digit_0 // GPIO pin 18 159 bl gpioDirectionIn 160 161 adr x0, digit_1 // GPIO pin 23 162 bl gpioDirectionIn 163 164 adr x0, digit_2 // GPIO pin 24 165 bl gpioDirectionIn 166 167 adr x0, digit_3 // GPIO pin 25 168 bl gpioDirectionIn 169 170 adr x0, pin_22 // GPIO pin 22 171 bl gpioDirectionIn 172 173 mov x0, x29 174 bl mapClose // unmap gpio 175 176 pop2 x29, x30 // pop fp & lr 177 ret 178 179 //======================================================== 180 readWrite: // readWrite has no parameters 181 push2 x29, x30 // push fp & lr 182 push x22 183 184 adr x29, gpiobase 185 ldr x29, [x29] // x29 = gpiobase 186 187 adr x22, digits // get this from shared memory 188 ldr x22, [x22] 189 190 continue: 191 ldrb w0, [x22, #THREE] // get lsd -- digit_0 192 adr x1, hex_numbers // get lookup table hex_numbers patterns 193 ldr w1, [x1, x0, lsl #2] // point to correct num & get pins to clr 194 195 adr x0, digit_0 // write digit_0 196 bl writeDigit 197 198 ldrb w0, [x22, #TWO] // get digit_1 199 adr x1, hex_numbers // get lookup table hex_numbers patterns 200 ldr w1, [x1, x0, lsl #2] // point to correct num & get pins to clr 201 202 adr x0, digit_1 // write digit_1 203 bl writeDigit 204 205 ldrb w0, [x22, #ONE] // get digit_2 206 adr x1, hex_numbers // get lookup table hex_numbers patterns 207 ldr w1, [x1, x0, lsl #2] // point to correct num & get pins to clr 208 209 adr x0, digit_2 // write digit_2 210 bl writeDigit 211 212 ldrb w0, [x22, #ZERO] // get digit_3 213 adr x1, hex_numbers // get lookup table hex_numbers patterns 214 ldr w1, [x1, x0, lsl #2] // point to correct num & get pins to clr 215 216 adr x0, digit_3 // write digit_3 217 bl writeDigit 218 219 220 adr x0, exitFlag // read the exit flag byte 221 ldrb w0, [x0] 222 cmp w0, #ONE // do we need to exit? 223 bne continue // branch if no 224 225 pop x22 226 pop2 x29, x30 // pop fp & lr 227 ret 228 229 //======================================================== 230 writeDigit: // x29 => gpiobase, w0 => active digit, x1 = print pattern 231 push2 x29, x30 // push fp & lr 232 push x22 233 234 mov x22, x0 // x0 => active digit to write 235 236 adr x8, clrAllSeg 237 ldr w8, [x8] // w8 = bits we care about 238 str w8, [x29, #gpset0] // write 1st pattern to gpset0 239 str w1, [x29, #gpclr0] // write 2nd pattern to gpclr0 240 241 mov x0, x22 // turn on digit 242 mov x1, #ONE // one pulse to base of npn transistor 243 bl gpioSetState 244 245 bl sleep // display digit for 2.5ms 246 247 mov x0, x22 // turn off digit 248 mov x1, xzr // zero pulse to base of npn transistor 249 bl gpioSetState 250 251 pop x22 252 pop2 x29, x30 // pop fp & lr 253 ret 254 255 //======================================================== 256 sleep: 257 push2 x29, x30 258 ldr x0, =timespecsec // sleep for 2.5ms 259 ldr x1, =timespecsec 260 bl nanosleep 261 pop2 x29, x30 // pop fp & lr 262 ret 263 264 //===================== DATA SECTION ===================== 265 .section .data 266 267 exitFlag: .byte 0 // this will be 1 when we should exit 268 gpiobase: .dword 0 // memory mapped gpio address space 269 digits: .dword 0 // 4 digits memory 270 271 //============== READ-ONLY DATA SECTION ================== 272 .section .rodata 273 274 timespecsec: .dword 0 // 0 275 timespecnano: .dword 2500000 // 2.5ms 276 277 memdev: .asciz "/dev/gpiomem" 278 279 // GPIO pin lookup table 280 seg_a: .word 8 // pin20 - offset to select register 281 .word 0 // - bit offset in select reg 282 .word 20 // - bit offset in set & clr reg 283 seg_b: .word 8 // pin21 284 .word 3 285 .word 21 286 seg_c: .word 4 // pin19 287 .word 27 288 .word 19 289 seg_d: .word 4 // pin13 290 .word 9 291 .word 13 292 seg_e: .word 0 // pin06 293 .word 18 294 .word 6 295 seg_f: .word 4 // pin16 296 .word 18 297 .word 16 298 seg_g: .word 4 // pin12 299 .word 6 300 .word 12 301 seg_dp: .word 8 // pin26 302 .word 18 303 .word 26 304 switch_0: .word 4 // pin17 305 .word 21 306 .word 17 307 digit_0: .word 4 // pin18 - shared memory 308 .word 24 309 .word 18 310 digit_1: .word 8 // pin23 311 .word 9 312 .word 23 313 digit_2: .word 8 // pin24 314 .word 12 315 .word 24 316 digit_3: .word 8 // pin25 317 .word 15 318 .word 25 319 pin_22: .word 8 // pin22 320 .word 6 321 .word 22 322 323 hex_numbers: 324 .word 0x00392040 // 0 - write this to gpclr0 to display 0 325 .word 0x00280000 // 1 - write this to gpclr0 to display 1 326 .word 0x00303040 // 2 - write this to gpclr0 to display 2 327 .word 0x00383000 // 3 - write this to gpclr0 to display 3 328 .word 0x00291000 // 4 - write this to gpclr0 to display 4 329 .word 0x00193000 // 5 - write this to gpclr0 to display 5 330 .word 0x00193949 // 6 - write this to gpclr0 to display 6 331 .word 0x00380000 // 7 - write this to gpclr0 to display 7 332 .word 0x00393040 // 8 - write this to gpclr0 to display 8 333 .word 0x00391000 // 9 - write this to gpclr0 to display 9 334 .word 0x00391040 // A - write this to gpclr0 to display A 335 .word 0x00093040 // b - write this to gpclr0 to display b 336 .word 0x00112040 // C - write this to gpclr0 to display C 337 .word 0x00283040 // d - write this to gpclr0 to display d 338 .word 0x00113040 // E - write this to gpclr0 to display E 339 .word 0x00111040 // F - write this to gpclr0 to display F 340 341 clrAllSeg: 342 .word 0x04393040 // write this to gpset0 to make leds dark 343 344 //====================================================================
Why 3 bits? With 3 bits you can specify eight unique values. The value 000 means the pin is an input, 001 means the pin is an output, and the other six values 010-111 specify an alternative function for the GPIO pin. Here, I'll only deal with input and output. To write a 1
or a
to the GPIO output pin you have to write a bit into the set or clear register. The bit set is the same as the GPIO pin number, which is not the same as the 40-pin header number.
To set GPIO pin 6 to 0, write a 1
into bit 6 of the GPCLR0 register. To make everything a little more complicated, if you only want to change one pin, you have to read a register first, change the bit that needs to be changed, and then write back the modified register.
Look at the seg_e
entry in the lookup table and note the three values associated with that entry. The
means use gpfsel0 (GPIO function select register 0) and 18
means write into bits 18, 19, and 20 of gpfsel0 to set the register function. Finally, the 6
indicates the bit you have to set in gpset0 to put GPIO pin 6 in a 1 state and the bit you have to set in gpclr0 to put the pin in a 0 state. To set a GPIO pin high (1) or low (0), use gpioSetState()
(in the writeDigit()
function). Please see arm64_include.asm
for a description of the Raspberry Pi GPIO register map.
Counting
The gpiocount.asm
program has two methods that count in various number systems: countUp(base)
and countDown(base)
. Figure 3 shows a flowchart of the countUp
function. These routines write the count to four shared memory locations. The gpiocount
program doesn't need to know much about the hardware; it needs to know just enough to test whether it has been interrupted. When the LKM is interrupted by switch_0
pulling GPIO pin 17 low, it pulls GPIO pin 22 low, which countUp()
and countDown()
monitor to determine when they need to exit. They then restore GPIO pin 22 to a high state.
The gpiomux.asm
program takes the numbers from shared memory and writes them to the seven-segment display. Each of the eight LED segments is connected to a GPIO pin designated as output. The four-digit driver pins (digit_0
, digit_1
, digit_2
, and digit_3
) have been designated as outputs, as well.
In gpiomux.asm
, the initialize
function (line 59) gets the shared memory region, performs the virtual GPIO mapping, and sets the GPIO pin directions as desired.
The hex_numbers
lookup table (gpiomux.asm
, lines 323-339) describes what segments should be on (0) or off (1) to form the correct number on the seven-segment display. I know that sounds backward, but on a common-anode, seven-segment display, the anode of every LED in the device is connected through its driver pin and a general-purpose NPN transistor to 3.3V. Writing a 1 to a GPIO seg_n
pin means putting 3.3V on the pin. With 3.3V connected to the resistor and 3.3V on the LED, no current can flow, and the LED is unlit.
Only by writing a 0 to a GPIO seg_n
pin which puts 0V (ground) on the resistor, do you get a difference of potential across the LED, which makes it light up. The resistor is there to limit the current that the GPIO pin will sink to less than 16mA. Most devices can sink more current than they can source, so the common-anode design is a common approach. Nowhere in the circuit do you connect to 5V. (Note: The Pi's GPIO pins are not 5V tolerant. Stick to 3.3V everywhere.)
The readWrite
function (lines 180-227) is the workhorse of gpiomux.asm
, cycling through the four shared memory locations and writing each in turn to its associated GPIO digit. It turns on one digit at a time and sets the appropriate segments to be on or off for each of the four digits. It has to turn the digits on and off fast enough to fool the eye that it is seeing the display as four separate digits. An oscilloscope display of the four-digit outputs is shown in Figure 4.
The readWrite
function reads a digit in shared memory and determines from the hex_numbers
lookup table which segments should be lit. It passes the arguments address of digit_n
(digit_0
… digit_3
) and hex_numbers[n]
to writeDigit
, which writes the pattern to the GPIO hardware.
The writeDigit
subroutine (lines 230-253; Figure 5) gets the pattern stored at memory location clrAllSeg
and writes it into gpset0 (0x28) from the GPIO base address (gpiobase
). Next, it takes the pattern passed to it by readWrite()
in register w1 and writes it to clrregoffset
. That lights the appropriate segments. It then turns on the digit (writes a 1
to the base of the associated NPN transistor driver). The address of the digit lookup table is passed to writeDigit()
in register x0.
After writing to the GPIO hardware, writeDigit
goes to sleep for 2.5ms and then turns off the digit (writes a
to the base of the associated NPN transistor). The readWrite()
and writeDigit()
functions produce the waveform shown in Figure 4.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.
-
Juno Tab 3 Launches with Ubuntu 24.04
Anyone looking for a full-blown Linux tablet need look no further. Juno has released the Tab 3.
-
New KDE Slimbook Plasma Available for Preorder
Powered by an AMD Ryzen CPU, the latest KDE Slimbook laptop is powerful enough for local AI tasks.
-
Rhino Linux Announces Latest "Quick Update"
If you prefer your Linux distribution to be of the rolling type, Rhino Linux delivers a beautiful and reliable experience.