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
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:
A pointer to the name of the device/file to be opened.
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:
The address where the device should be mapped into, which needs to be in our programming virtual memory space. Specifying \(0\) to
mmapallows the operating system to pick an address for us.The amount of memory for the mapping. One page is sufficient.
A mode specifying the protection given to the mapped memory. This cannot conflict with the protection specified in the
openoperation. We want to read from and write to this memory.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.
The file descriptor to the device being mapped. This came from our call to
open.The physical memory location of the I/O device.
There are two steps for finding the physical memory location of the GPIO device:
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.
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.
