Linux Memory Access /dev/mem
if you recall the discussion in the chapter on SYSFS Linux likes to have every I/O device look as much like a file as possible. This is a basic principle of Unix that Linux has taken to heart and in the main it sort of works. However sometimes you have to think that we are going the long way round to get a job done.
For example, how do you think Linux gives your user mode program access to raw memory?
Yes, that is correct. It represents the memory as a single binary file /dev/mem.
This is character device file that is an image of user mode memory. And when you read or write byte n this is the same as reading and writing the memory location at byte address n. You can move the pointer to any memory location using lseek and read and write blocks of bytes using fread and fwrite.
It is simple but it takes some time to get used to the idea.
For example to open the file to read and write it you might use:
int memfd = open("/dev/mem", O_RDWR | O_SYNC);
The O_RDWR opens the file for read and write and the O_SYNC flag makes the call blocking.
After this you can lseek to the memory location you want to work with. For example to go to the start of the GPIO registers you would use:
uint32_t p = lseek(memfd, (off_t) 0x3f200000, SEEK_SET);
for the Pi 2 or
uint32_t p = lseek(memfd, (off_t) 0x20200000, SEEK_SET);
for the Pi 1. Notice that these are the base address plus 0x20 000
Next you could read the 32 bits starting at that location i.e. the FSEl0 register:
int n = read(memfd, buffer, 4);
Unfortunately at the moment if you try this then you will find that it doesn't work and you get a bad address error. You can read and write other memory locations but the peripheral registers don't seem to work. If they did this would be a perfectly good way to read and write the registers.
As this doesn't work we need to move on to what does.
Memory Mapping Files
The Linux approach to I/O places the emphasis on files but there are times when reading and writing a file to an external device like a disk drive is too slow. To solve this problem Linux has a memory mapping function which will read any portion of a file into user memory so that you can work with is directly using pointers. This in principle is a very fast way to access any file - including /dev/mem.
This seems like a crazy round about route to get at memory. First implement memory access as a file you can read and then map that file into memory so that you can read it as if it was memory - which it is. However if you follow the story it is logical. What is more it solves a slightly different problem very elegantly. It allows the fixed physical addresses of the peripherals into the user space virtual addresses. In other words when you memory map the /dev/mem file into user memory it can be located anywhere and the address of the start of the register area will be within your programs allocated address space. This means that all of the addresses we have been listing will change. Of course as long as we work with offsets from the start of memory this is no problem - we update the staring value and use the same offsets.
Lets see how this works in practice.
The key function is mmap
void *mmap( void *addr, size_t length, int prot, int flags, int fd, off_t offset );
The function memory maps the file corresponding to the file descriptor fd into memory and returns its start address. The offset and length parameters control the portion of the file mapped i.e. the mapped portions starts at the byte given by offset and continues for length bytes.
There is a small complication in that for efficiency reasons the file is always mapped in units of the page size of the machine. So if you ask for a 1Kbyte file to be loaded into memory then, on the Pi with a 4Kbyte page size, 4Kbytes of memory will be allocated. The file will occupy the first 1Kbytes and the rest will be zeroed.
You can also specify the address that you would like the file loaded to in your programs address space but the system doesn't have to honor this request - it just uses it as a hint. Some programmers reserve and area of memory using malloc say and then ask the system to load the file into it - as this might not happen it seems simpler to let the system allocate the memory and pass NULL as the starting address.
Prot and flags specify various ways the file can be memory mapped and there are a lot of options - see the man page for details.
Notice that this is a completely general mechanism and you can use it to map any file into memory. For example if you have graphics file - image.gif - then you could load it into memory to make working with it faster. Many databases use this technique to speed up their processing.
Now all we have to do is map /dev/mem into memory.
First we need to open the /dev/mem device as usual:
uint32_t memfd = open("/dev/mem", O_RDWR | O_SYNC);
As long as this works we can map the file into memory.
We want to map the file starting at either 0x2020 0000 for the Pi 1 or starting at 0x3F20 0000 for the Pi 2. If we only want to work with the GPIO registers then we only need offsets of 0000 to 00B0 i.e. 176 bytes but as we get the a complete 4K page we might as well map 4KBytes worth of address space:
uint32_t * map = (uint32_t *)mmap( NULL, 4*1024, (PROT_READ | PROT_WRITE), MAP_SHARED, memfd, 0x3f200000);
If you try this remember to change the offset to be correct for the Pi you are using or better us bcm2835_peripherals_base to specify the address.
Notice also that we haven't set an address for the file to be loaded into - the system will take care of it and return the address in map. We also have asked for read/write permission and allowed other processes to share the map. This makes map a very important variable because now it gives the location of the start of the GPIO register area but in user space. The bcm2835 has a standard variable for this:
Now we can read and write a 3KByte block of addresses starting at the first GPIO register i.e. FSEL0.
For example to read FSEL0 we would use:
printf("fsel0 %X \n\r",*map);
To access the other registers we need to add their offset but there is one subtle detail. The pointer to the start of the memory has been caste to a uint32_t because we want to read and write 32 bit registers. However by the rules of pointer arithmetic when you add one to a pointer you actually add the size of the date type the pointer is pointing to. In this case when you add one to map you increment the location it is pointing at by four i.e. the size of a 32 bit unsigned integer.
The rule is that with this cast we are using word addresses which are byte addresses divided by 4.
Thus when we add the offsets we need to add the offset divided by 4.
With this all clear lets write a program that toggles GPIO 4 as fast as possible.