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).
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.
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.
« Previous 1 2 3 Next »
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
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
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.