Access Raspberry Pi GPIO with ARM64 assembly

Change Mode

So far, you have a device that counts over and over again, forever. To make it do something else – that is, to change the mode of the device – you need to interrupt the kernel and make it tell the current subroutine that it's finished and should stop. That involves the GPIO pin 17 single pole, single throw, normally open switch and lkm_gpio.c to watch GPIO pin 17. Note that pin 17 is normally at 3.3V. When the switch is pressed, pin 17 goes low and then high again when the switch is released. Mechanical switches don't produce a clean on-off state. They are actually quite noisy and can produce many spikes that can confuse the kernel (Figure 6).

Figure 6: Switch noise – a single press of the switch produces 300ms of noise before settling into the 0 state.

A noisy switch can be dealt with in one of two ways: Ask the kernel to take care of it in software, or take care of it yourself in hardware. The software method (software debouncing) involves telling the kernel module that it should check again after a given number of microseconds and make sure that was really a falling edge pulse transition that it saw. Still, you will see a few extraneous triggers with software debouncing. Although the software solution is easy, it's kind of hard on the kernel, because it's already pretty busy and you are asking it to set timers and retest. The hardware method (hardware debouncing) involves adding hysteresis and a slight delay to the switch. The program lkm_gpio.c can be compiled both ways, so just uncomment line 16,

// #define __HW__DEBOUNCE__ //

if you want add the extra components necessary for hardware debouncing to the switch (Figure 7). The device works fine with either software or hardware debouncing, so you don't need to deal with adding the components shown in Figure 7 unless you want. Note that gpio_set_debounce() does not seem to be implemented on the Raspberry Pi.

Figure 7: Schematic diagram of a circuit that performs hardware switch debouncing.

Basic ARM64 Assembly Tasks

Next, I look at some basic GPIO tasks and see how they work in ARM64 assembly language. Remember that when you pass arguments to a function, they are placed in x0, x1, …, x7. If you need more than eight arguments, the remaining arguments must be passed on the stack. That's the convention for calling the kernel or a glibc function. Because you're calling your own assembly language functions, you could pass the arguments any way you like, but for consistency, I'll stick with the C calling convention.

In libgpio.asm, the gpioDirectionIn function (Listing 2, line 143) sets a GPIO pin as an input, for which you need the 3 bits that control its function. To make it easy, forgo multiplication and division and use a lookup table to find everything needed for any GPIO pin that will be used.

Listing 2

libgpio.asm (excerpt)

...
142 // Set pin to input
143 gpioDirectionIn:          // x29 => gpiobase, x0 => seg lookup tbl
144   ldr   w2, [x0]          // w2 = offset to gpfseln - 1st val in tbl
145   ldr   w1, [x29, x2]     // w1 = contents of gpfseln
146   ldr   w3, [x0, #FOUR]   // w3 = offect in gpfseln - 2nd val in tbl
147   mov   w0, #CLEAR_MASK   // w0 = mask to clear 3 bits (111)
148   lsl   w0, w0, w3        // shift CLEAR_MASK into position
149   bic   w1, w1, w0        // clr the 3 bits of gpfseln - 000 = input
150   str   w1, [x29, x2]     // write it to gpfseln
151   ret
...

For example, say you want to make GPIO pin 22 an input (as in the initialize() subroutine in gpiocount). For all of the libgpio exported functions, set x29 to point to the base register of GPIO. Any routine that deals with GPIO will need the GPIO base register address, so x29 is a sort of global variable.

The first thing to do inside gpioDirectionIn is to load the contents of the first element (8) of pin_22 in the GPIO lookup table (gpiocount.asm, line 300, on FTP site [1]) into x2:

// GPIO pin lookup table
pin_22:   .word  8    // pin22 - LKM will notify us of interrupts by bringing this pin low
          .word  6
          .word 22

The first element of the lookup table is the offset of the function select register that controls GPIO pin 22. The function select registers start at the GPIO base register address, so x29 plus x2 is the address of gpfseln. The value in the lookup table is 8 bytes, so line 145 points to gpfsel2, which is 8 bytes away from the GPIO base register. The next value in the lookup table is the bit offset in gpfsel2, so 6 will be the value returned in w3 by the statement in line 146.

Next, create a mask of 3 bits (0b111) in w0 and perform a logical shift left of that mask by w3 (which from the lookup table is 6). The bic instruction with w1 as the destination register then clears the 3-bit mask, because 000 means input. The value read from gpfsel2 is modified by writing three   bits into the register at bits 6, 7, and 8 before the modified data is written back into gpfsel2.

The gpioDirectionOut section starts the same way as gpioDirectionIn but then writes 001 (output) into gpfsel2.

The hex_numbers table (Listing 1, line 323) contains the values you want to write into gpclr0 for each of the 16 number symbols. Remember, you write a 1 to gpclr0 to set the output pin to 0, which makes the LED segment light. I recommend that you take a piece of graph paper and lay out the 32 bits of the gpclr0 register from left to right, with 31 as the first column on the left and 0 as the last column on the right. Now mark the columns with the segment letters for the GPIO pins: An a would go in column 20, a b in column 21, a c in column 19, and so on.

As an example, take the symbol 1, which has only segments b and c lit. Place a 1 in column 20, a 1 in column 19, and a 0 in every other column. Next, map each 4 bits into a number from 0000 (0) to 1111 (F), and you will have written the number symbol in hexadecimal. The value shown in hex_numbers[1] is 0x00280000. The symbol 7 lights segments a, b, and c. Go through the same process as before, and you should get 0x00380000. That is the value in hex_numbers[7]. Every one of the 16 possible values is an element of hex_numbers.

Running

See the boxout titled "Installing the Source Code."

Installing the Source Code

To begin, open a terminal on the Raspberry Pi, make sure your operating system is up to date, and install the tools you'll need:

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install make
$ sudo apt install gcc
$ cd

Now, create a ~/Development folder and install gpiosource.zip in the new directory:

$ mkdir Development
$ cd Development
$ mkdir lib
$ unzip gpiosource.zip

Next, open the Bash initialization file,

$ cd
$ nano .bashrc

and add the following line at the end to set the shared library search path:

export LD_LIBRARY_PATH=~/Development/lib

Now, save the file and close nano, close and reopen the terminal, and check your work:

$ echo $LD_LIBRARY_path
/home/<username>/Development/lib

Finally, make and run the code:

$ cd ~/Development/lkm_gpio
$ make
$ cd ~/Development/gpiomux
$ make
$ ./gpiocount

On the Ubuntu 20.10 operating system, /dev/gpiomem is owned by root:root and marked as read/write for root only, so you should create a group named gpio and assign your user to that group. Also, change the ownership of /dev/gpiomem to root:gpio and assign group read/write permissions. This change should occur on every bootup. As a quick alternative, you can use

sudo ./gpiocount

to run the application with root privileges. (See the "Setting Permissions at Bootup" box to see how to set the permissions automatically every time you boot up the Raspberry Pi).

Setting Permissions at Bootup

1. Create a new group and add yourself to it:

sudo groupadd gpio
sudo usermod -a -G gpio <your_user_name>

2. Create file /usr/sbin/gpiopermission.sh:

sudo nano /usr/sbin/gpiopermission.sh
 **
#/bin/bash
/usr/bin/chown root.gpio /dev/gpiomem && /usr/bin/chmod g+rw /dev/gpiomem
exit 0

3. Make gpiopermission.sh executable:

sudo chmod +x /usr/sbin/gpiopermission.sh

4. Create file /etc/systemd/system/gpiopermission.service:

[Unit]
Description=Grants rw permission to /dev/gpiomem
After=network.target
[Service]
Type=simple
ExecStart=/bin/bash /usr/sbin/gpiopermission.sh
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target

5. Reload services, generate dependencies, and enable the new service:

systemctl daemon-reload
systemctl enable gpiopermission.service

6. Confirm that all went well:

systemctl -all | grep gpiopermission.service

7. Reboot:

sudo reboot

8. Verify that dev/gpiomem belongs to root:gpio and that group gpio has read/write permissions:

ls -l dev/gpiomem
crw-rw---- 1 root gpio 240, 0 Jan  6 15:40 /dev/gpiomem

Now look at the operation and installation of the loadable kernel module lkm_gpio.c (Listing 3), which is pretty much self-explanatory. An init() function (line 104) configures the interrupt mechanism with irq_config() (line 44), and a cleanup() function (line 112) releases the interrupt mechanism.

Listing 3

lkm_gpio.c (excerpt)

01 //*******************************************************
02 // @file  lkm_gpio.c
03 // @author  John Schwartzman
04 // @date  04/11/2021
05 // @version 1.0
06 // @brief   A loadable kernel module (LKM) that handles interrupts
07 //          from user space.
08 //*******************************************************
09
10 #include <linux/init.h>
11 #include <linux/module.h>
12 #include <linux/kernel.h>
13 #include <linux/interrupt.h>
14 #include <linux/gpio.h>
15
16 // #define __HW__DEBOUNCE__ // uncomment this line for __HW__DEBOUNCE__
17
18 static unsigned int irq_number;
19 static unsigned int gpio_button = 17;     // switch connects to Pin 17
20 static unsigned int pin_to_control = 22;  // Pin 22 - tell user about int.
21
22 //*******************************************************
23 // irq handler - fired on interrupt
24 static irqreturn_t irq_handler(int irq, void* dev_id, struct pt_regs* regs)
25 {
26   unsigned long flags;
27
28   // disable this hardware interrupt
29   local_irq_save(flags);
30
31   // Turn on GPIO pin 22).
32   // That's how the user space program determines that it
33   // has been interrupted. It reads pin 22
34   // It reads pin 22 and if it gets a 0 then it knows it should finish.
35   gpio_set_value(pin_to_control, 0);
36
37   // restore this hardware interrupt
38   local_irq_restore(flags);
39   return IRQ_HANDLED;
40 }
41
42 //*******************************************************
43 // configure interrupts
44 static void irq_config(void)
45 {
46 #ifndef __HW__DEBOUNCE__
47   int retval;
48 #endif
49
50   gpio_request(gpio_button, "gpio_button");
51   gpio_direction_input(gpio_button);
52   gpio_direction_output(pin_to_control, 1);
53
54   // If you don't debounce the switch with hardware,
55   // uncomment gpio_set_debounce(), and the kernel
56   // module will do it for you in software.
57   irq_number = gpio_to_irq(gpio_button);
58   printk(KERN_INFO "lkm_gpio: gpio_button is currently %d\n", gpio_get_value(gpio_button));
59   printk(KERN_INFO "lkm_gpio: Mapped irq number to %d\n", irq_number);
60
61   #ifndef __HW__DEBOUNCE__
62     // Use IRQF_TRIGGER_FALLING if the switch is
63     // connected directly to gpio_button.
64     // If the switch is debounced using an Schmitt
65     // trigger inverter chip then use IRQF_TRIGGER_RISING.
66     retval = gpio_set_debounce(gpio_button, 600); // recheck after 600µs
67     if (retval != 0)
68     {
69       printk(KERN_WARNING "lkm_gpio: gpio_set_debounce() returned %d\n", retval);
70     }
71     if (request_irq(irq_number,
72         (irq_handler_t)irq_handler,
73         IRQF_TRIGGER_FALLING, // falling if software debouncing
74         "RPI_gpio_handler",   // identify owner in /proc/interrupts
75         NULL))
76     {
77       printk(KERN_INFO "lkm_gpio: Irq request failure");
78     }
79   #else // we're doing our debouncing in hardware
80     // Use IRQF_TRIGGER_FALLING if the switch is
81     // connected directly to gpio_button.
82     // If the switch is debounced using an Schmitt trigger
83     // inverter chip then use IRQF_TRIGGER_RISING.
84     if (request_irq(irq_number,
85         (irq_handler_t)irq_handler,
86         IRQF_TRIGGER_RISING,  // rising if debounced switch
87         "RPI_gpio_handler",   // identify owner in /proc/interrupts
88         NULL))
89     {
90       printk(KERN_INFO "lkm_gpio: Irq request failure");
91     }
92   #endif
93 }
94 ...

The interrupt mechanism behaves differently depending on whether __HW__DEBOUNCE__ is defined at the top of this program. The default behavior is to use software debugging, but you can change that by uncommenting line 16, if you decide you want to add the components necessary for hardware debugging. Note that the irq_handler function, which is called when you depress switch_0, sets pin 22 to 0.

In gpiocount.asm, before sleeping for 1s, countUp() and countDown() invoke gpioReadPin() on GPIO pin 22. Then, countUp() and countDown() check the Z (zero) flag and exit if Z == 1, which returns the action to main() (in countmain.cpp) or main (in gpiocount.asm), launching its next subroutine and changing the program's mode.

When gpiocount finishes its initialization, it starts gpiomux in background mode with:

system("./gpiomux &")

When gpiocount finishes its assigned work or if it detects a Ctrl+C, it finishes counting, cleans up, and stops gpiomux with the hang-up signal:

signal("killall -HUP gpiomux")

To build lkm_gpio, type make at the command prompt in the lkm_gpio directory. The makefile auto-loads lkm_gpio. At the command line, type

lsmod | grep lkm_gpio

to see whether lkm_gpio has been installed. You might have to reboot Linux to get it running the first time.

Next, go to the gpiomux directory and type make at the command prompt to build gpiolib, gpiocount, and gpiomux. The shared object file gpiolib is used by both gpiocount and gpiomux. You can type make to build the standard version, or

make OPT=__MAIN__

to build without countmain.cpp and muxmain.cpp. You can also type

make debug

(with or without OPT=__MAIN__), so you can execute the programs in the GNU project debugger, gdb.

To start the action, launch the gpiocount executable (with ./gpiocount at the command line), which will start up the gpiomux executable and stop it when gpiocount finishes.

Clicking switch_0 changes the mode from countUp(2) to countUp(7) to countUp(10) to countUp(16) to countDown(16) to countDown(10) to countDown(7) to countDown(2). On the last click of switch_1, gpiocount cleans up and terminates because it has no more work to do and signals gpiomux with a SIGHUP to tell it to clean up and terminate. Because gpiocount must clean up and tell gpiomux to clean up, it also traps and handles a Ctrl+C from the keyboard.

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