Skip to main content

Section 19.1 GPIO I/O Memory

The peripheral address space, \(\hex{0x3e000000}\)–\(\hex{0x3effffff}\) on the Raspberry Pi 3, cannot be accessed directly from an application program. The Linux kernel includes a driver to a special character file, /dev/mem, which is a mirror of main memory. Similar to any file, we can open it, read bytes from or write bytes to it, and close it. The “position” of each byte in /dev/mem is the byte address in physical memory. In principle, we could program the GPIO device by opening /dev/mem, moving a pointer to the desired location of a GPIO port, and writing the appropriate value in that byte location.

As you can probably guess, this would create a serious security flaw for ordinary user programs because the program could access any byte in memory. To provide some security protection, only programs with root access can open the /dev/mem file. In Raspbian this is done by preceding the program's name with sudo when executing it.

Raspbian provides even more security by including a driver to the /dev/gpiomem file. This file only mirrors the memory associated with the GPIO device. User programs that access the /dev/gpiomem do not need to be executed with the sudo modifier.

Linux (and most other modern operating systems) use a memory management scheme that is based on virtual memory addresses. When you create a program, the loader assigns virtual addresses to all the memory locations. All memory references in the program are based on the virtual memory address space. Program code is divided into Pages of equal size. As an application executes, the pages containing the currently needed code and data are loaded into physical memory. The Linux kernel automatically translates the virtual memory addresses into the corresponding physical memory addresses, which depend on where the page was loaded.

We need to do the inverse when referencing I/O addresses. I/O devices are assigned physical memory addresses, which the Linux kernel prevents user programs from accessing. We need to map this physical memory into the program's virtual addressing scheme so that the program can access it. Listing 19.1.1 shows how this is done.

@ IOmemory.s
@ Opens the /dev/gpiomem device and maps GPIO memory
@ into program virtual address space.
@ 2017-09-29: Bob Plantz 

@ Define my Raspberry Pi
        .cpu    cortex-a53
        .fpu    neon-fp-armv8
        .syntax unified         @ modern syntax

@ Constants for assembler
@ The following are defined in /usr/include/asm-generic/fcntl.h:
@ Note that the values are specified in octal.
        .equ    O_RDWR,00000002   @ open for read/write
        .equ    O_DSYNC,00010000  @ synchronize virtual memory
        .equ    __O_SYNC,04000000 @      programming changes with
        .equ    O_SYNC,__O_SYNC|O_DSYNC @ I/O memory
@ The following are defined in /usr/include/asm-generic/mman-common.h:
        .equ    PROT_READ,0x1   @ page can be read
        .equ    PROT_WRITE,0x2  @ page can be written
        .equ    MAP_SHARED,0x01 @ share changes
@ The following are defined by me:
        .equ    PERIPH,0x3f000000   @ RPi 2 & 3 peripherals
@        .equ    PERIPH,0x20000000   @ RPi zero & 1 peripherals
        .equ    GPIO_OFFSET,0x200000  @ start of GPIO device
        .equ    O_FLAGS,O_RDWR|O_SYNC @ open file flags
        .equ    PROT_RDWR,PROT_READ|PROT_WRITE
        .equ    NO_PREF,0
        .equ    PAGE_SIZE,4096  @ Raspbian memory page
        .equ    FILE_DESCRP_ARG,0   @ file descriptor
        .equ    DEVICE_ARG,4        @ device address
        .equ    STACK_ARGS,8    @ sp already 8-byte aligned

@ Constant program data
        .section .rodata
        .align  2
device:
        .asciz  "/dev/gpiomem"
fdMsg:
        .asciz  "File descriptor = %i\n"
memMsg:
        .asciz  "Using memory at %p\n"

@ The program
        .text
        .align  2
        .global main
        .type   main, %function
main:
        sub     sp, sp, 16      @ space for saving regs
        str     r4, [sp, 0]     @ save r4
        str     r5, [sp, 4]     @      r5
        str     fp, [sp, 8]     @      fp
        str     lr, [sp, 12]    @      lr
        add     fp, sp, 12      @ set our frame pointer
        sub     sp, sp, STACK_ARGS @ sp on 8-byte boundary

@ Open /dev/gpiomem for read/write and syncing        
        ldr     r0, deviceAddr  @ address of /dev/gpiomem
        ldr     r1, openMode    @ flags for accessing device
        bl      open
        mov     r4, r0          @ use r4 for file descriptor

@ Display file descriptor
        ldr     r0, fdMsgAddr   @ format for printf
        mov     r1, r4          @ file descriptor
        bl      printf

@ Map the GPIO registers to a virtual memory location so we can access them        
        str     r4, [sp, FILE_DESCRP_ARG] @ /dev/gpiomem file descriptor
        ldr     r0, gpio        @ address of GPIO
        str     r0, [sp, DEVICE_ARG]      @ location of GPIO
        mov     r0, NO_PREF     @ let kernel pick memory
        mov     r1, PAGE_SIZE   @ get 1 page of memory
        mov     r2, PROT_RDWR   @ read/write this memory
        mov     r3, MAP_SHARED  @ share with other processes
        bl      mmap
        mov     r5, r0          @ save virtual memory address
        
@ Display virtual address
        mov     r1, r5
        ldr     r0, memMsgAddr
        bl      printf
                
        mov     r0, r5          @ memory to unmap
        mov     r1, PAGE_SIZE   @ amount we mapped
        bl      munmap          @ unmap it

        mov     r0, r4          @ /dev/gpiomem file descriptor
        bl      close           @ close the file
        
        mov     r0, 0           @ return 0;
        add     sp, sp, STACK_ARGS  @ fix sp
        ldr     r4, [sp, 0]     @ restore r4
        ldr     r5, [sp, 4]     @      r5
        ldr     fp, [sp, 8]     @         fp
        ldr     lr, [sp, 12]    @         lr
        add     sp, sp, 16      @ restore sp
        bx      lr              @ return
        
        .align  2
@ addresses of messages
fdMsgAddr:
        .word   fdMsg
deviceAddr:
        .word   device
openMode:
        .word   O_FLAGS
memMsgAddr:
        .word   memMsg
gpio:
        .word   PERIPH+GPIO_OFFSET
Listing 19.1.1. Map GPIO physical memory into virtual memory space for programming. (prog asm)

When I ran this program I got (your numerical output may differ):

pi@rpi3:~/bookProgs/chap19 $sudo ./IOmemory
File descriptor = 3
Using memory at 0x76f77000

The open function requires two arguments:

  1. A pointer to the name of the device/file to be opened.

  2. A mode specifying how the opened device/file can be used by the program.

The name of the special file we are opening is “/dev/gpiomem”. The flags specifying the open mode, O_FLAGS, were chosen such that our program can read values from this I/O memory and write to it. The synchronization flags ensure that any values written to this memory will be completed before the program continues execution. It returns a file descriptor number.

The mmap function takes six arguments:

  1. The address where the device should be mapped into, which needs to be in our programming virtual memory space. Specifying \(0\) to mmap allows the operating system to pick an address for us.

  2. The amount of memory for the mapping. One page is sufficient.

  3. A mode specifying the protection given to the mapped memory. This cannot conflict with the protection specified in the open operation. We want to read from and write to this memory.

  4. A mode specifying whether this mapped memory should or should not be shared with other processes. We will allow sharing. You may wish to have more than one program accessing the GPIO at the same time.

  5. The file descriptor to the device being mapped. This came from our call to open.

  6. The physical memory location of the I/O device.

There are two steps for finding the physical memory location of the GPIO device:

  1. Determine the address where peripherals begin. It is \(\hex{0x3e000000}\) on my Raspberry Pi 3. The program in Exercise 18.6.1 will do this for your Raspberry Pi.

  2. Obtain the relative address of the GPIO device from the beginning of the peripheral address.

The relative address of the GPIO device is found in the BCM2835 datasheet [6]. You will be working with three addresses when reading this datasheet:

  • VideoCore bus addresses are the ones given in the BCM2835 datasheet.

  • Physical device addresses are where the GPIO registers are actually hardwired.

  • Virtual memory addresses are the ones that your program will use to access the GPIO registers.

The VideoCore bus addresses all begin at \(\hex{0x7e000000}\text{,}\) so you need to subtract this number from the addresses of devices listed in the datasheet in order to obtain the relative address of the specific device you wish to program. This relative address will be combined with your program's virtual memory address to access the device.

The GPIO has 41 registers. Table 6-1 in the BCM2835 datasheet shows that they start at \(\hex{0x7e200000}\text{,}\) so the GPIO is located at \(\hex{0x200000}\) relative to the beginning of I/O memory. This is shown in Listing 19.1.1 with the statement:

.equ    GPIO_OFFSET,0x200000  @ start of GPIO device

And we then go on to specify the physical address of the GPIO device with:

gpio:
        .word   PERIPH+GPIO_OFFSET

where PERIPH was previously determined as explained above and defined:

.equ    PERIPH,0x3f000000   @ RPi 2 & 3 peripherals

The mmap function returns the virtual address that the GPIO device has been mapped to. We can now program the device.

Note that before ending this function I unmap the GPIO memory and close the /dev/gpiomem device:

mov     r0, r5          @ memory to unmap
mov     r1, PAGE_SIZE   @ amount we mapped
bl      munmap          @ unmap it

mov     r0, r4          @ /dev/gpiomem file descriptor
bl      close           @ close the file

Both these actions would occur automatically when ending the program, so they may seem unnecessary since the /dev/gpiomem device is opened and the GPIO memory is mapped in the main function in this program. But a common programming error is to open a device or allocate memory in a subfunction and not close the device or deallocate the memory. If the subfunction is called multiple times in the program, multiple instances of the open device or allocated memory are the result. Good rules of thumb are:

  • Close devices in the same function where you opened them.

  • Deallocate memory in the same function where you allocated it.

Now that we know how to access GPIO memory, we will see how to use specific pins in Section 19.2.