This short video explains what is memory mapped I/O.
Usually, each on-chip peripheral device has a few registers, such as control registers,
status registers, data input registers, and data output registers.
In general, there are two approaches to exchange data between the processor core, and a peripheral
device, including port-mapped I/O, and memory-mapped I/O. Port-mapped I/O uses special CPU instructions,
which are designed specifically for I/O operations.
On the other hand, memory-mapped I/O does not need any special instructions.
Each register is assigned to a memory address in the memory address space of the microprocessor.
Memory-mapped I/O is performed by the native load and store instructions of the processor.
Therefore, memory-mapped I/O is a more convenient way to interface I/O devices.
Here is an example of memory mapped I/O. Suppose we want to set the output of a GPIO pin to
high, software can use the store instruction STR to set the corresponding bit in GPIO data
output register to 1.
When you write to this special memory location, the data you write is sent to the corresponding
I/O device.
ARM Cortex-M microprocessors use memory-mapped I/O.
The memory address of ARM Cortex-M has a total of 32 bits, supporting 4 gigabytes of memory
space.
The memory space is divided into six different pre-defined regions.
Each region is given for recommended usage.
The first region is code region.
It is primarily used to store program code.
It can also store data.
The code region is on-chip memory, typically on-chip flash.
The size of on-chip flash is limited to half a gigabyte.
The actual size of the on-chip flash varies based on different venders and different chips.
The second region is SRAM.
It is primarily used to store data, such as heaps and stacks.
We can also put code here.
It supports half a gigabyte.
The third region is peripheral.
These peripherals include Advanced High Performance Bus peripherals, such as GPIO and ADC, or
Advanced Peripheral Bus peripherals, such as timers and USART.
The next region is for external device, such as SD card.
The next is external RAM, which is executable region for data.
It is off-chip memory, primarily used to store large data blocks.
It has a total of 1 gigabyte.
The last region is system region, which includes the NVIC, system timer, system control block,
and vendor-specific memory.
Let's take a look at the peripheral region.
This region covers the memory addresses of all on-chip peripherals, such as GPIO, timers,
USART, SPI, and ADC.
Specific mapping addresses are dependent on vendors and chips.
We will use GPIO on STM32L4 as an example to illustrate the concept of memory-mapped
I/O. For instance, on STM32L4, the registers of GPIO Port A, are mapped to a small memory
region starting at 48000000 in hex.
Let's take a closer look at the memory map for GPIO Port A. Each port has 12 registers,
and each register has 4 bytes.
While a total of 1 kilobyte space is reserved for Port A, only 48 bytes are used.
Within this 48-byte memory region, the GPIO mode register MODER is mapped to the lowest
memory address, and the GPIO analog switch control register (ASCR) is mapped to the highest
memory address.
Suppose we want to set the output of pin 14 of Port A to high.
To achieve this, we need to set bit 14 of the output data register (ODR) of GPIO A to
1.
As discussed previously, each register has 32 bits, that is, 4 bytes.
The output data register (ODR) of Port A on STM32L4 are mapped to the memory addresses
from 48000014 to 48000017 in hex.
If little endian is used, the highest memory address holds the most significant 8 bits,
and the lowest memory address holds the least significant 8 bits.
We use this C statement to set, bit 14, of ODR to 1, by using bitwise OR.
This C statement also uses dereferencing to access the memory.
Dereferencing a pointer means, getting the value that is stored in the memory location
pointed by the pointer.
The operator asterisk is used to do this.
The asterisk is called the dereferencing operator.
A sequence of load, modify, and store operations are performed in this statement.
This statement casts the memory address to a memory pointer, which points to an 32-bit
unsigned integer.
First, the dereference operator retrieves the ODR register value, as a 32-bit unsigned
integer.
Then, a bit-wise operation is performed to modify this unsigned integer value.
Finally, the updated value is stored back to the ODR register, via the dereferencing
operation.
However, directly dereferencing a numeric memory address, is inconvenient to use in
practice.
The code uses numeric addresses directly.
Accordingly, it is not very readable.
The next slide will present a new approach, which uses register names instead of memory
addresses, thus greatly improving the code readability.
In memory-mapped I/O, all registers of a GPIO port are mapped to a contiguous block of physical
memory.
This memory block can be represented by using a struct, as defined in this slide.
A struct in C defines a physically grouped list of variables under one name.
Note that we put volatile qualifier on each register.
It informs the compiler that the variable might change spontaneously.
When a variable is declared as volatile, the compiler is informed that even though no statements
in the program appear to change it, the value might still change.
Typically, compilers minimize the number of memory accesses, by storing the memory value
in a register, and then repeatedly using it without accessing the memory.
The volatile qualifier on a variable prevents the compiler from making such optimization
on this variable.
We use a macro definition here to give a name to the struct pointer.
Let's name the macro GPIOA.
To set bit 14, we can use the membership operator to access the data output register ODR.
We can either use the arrow operator, or the dot operator, to access the output data register
ODR.
These two C statements are equivalent.
The asterisk GPIOA gets the structure that GPIOA points to.
If the program has to be written in assembly, we can use the EQN directive to define two
constants.
The first constant is the base memory address for Port A.
The second constant is the offset of the ODR register.
First, we load the base memory address into register r7.
Then we load the ODR value into register r1.
Next, we modify r1.
Finally, we store the modified value back to the ODR register.
Please visit the book website for more examples and tutorials.