Access Raspberry Pi GPIO with ARM64 assembly

The Three Rs

© Lead Image © drizzd, 123RF.com

© Lead Image © drizzd, 123RF.com

Article from Issue 247/2021
Author(s):

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

gpiocount.asm

Count up or down in different number systems

gpiocount.h

C++ header file with declarations for exports in gpiocount.asm

countmain.cpp

Optional C++ main() for gpiocount.asm

gpiomux.asm

Read and write four-digit, common-anode, seven-segment displays

gpiomux.h

C++ header file with declarations for exports in gpiomux.asm

muxmain.cpp

Optional C++ main() for gpiomux.asm

libgpio.asm

A shared library with GPIO functions used by gpiocount.asm and gpiomux.asm

arm64_include.asm

Contains constants and macros for ARM64 assembly language and describes the Raspberry Pi GPIO register map

lkm_gpio.c

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.

Figure 1: The breadboard and 40-pin header adapter with the Pi counting up in hexadecimal.
Figure 2: The breadboard wiring diagram.

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.

Figure 3: Flowchart for the countUp(base) subroutine.

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.

Figure 4: The oscilloscope trace indicates the waveforms produced by gpiomux: yellow = GPIO pin 18, purple = pin 23, blue = pin 24, and green = pin 25.

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_0digit_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.

Figure 5: Flowchart for the writeDigit(digit, pattern) subroutine.

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

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Kitchen Timer

    A simple kitchen helper with two timers assists budding chefs in coping with dishes that are unlikely to be ready at the same time.

  • GPIO on Linux Devices

    The general purpose input/output interface is not just for small-board computers anymore: You can use GPIO on your Linux desktop or laptop, too, through the USB port.

  • Automated Irrigation

    An automated watering system comprising a Raspberry Pi Zero W, an analog-to-digital converter, and an inexpensive irrigation kit can help keep your potted plants from dying of thirst.

  • Rasp Pi Fox Trap

    As a countermeasure to predators of rare ground-breeding birds, live traps are monitored by a microcontroller and a Raspberry Pi.

  • Go on the Rasp Pi

    We show you how to create a Go web app that controls Raspberry Pi I/O.

comments powered by Disqus
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.

Learn More

News