Lecture 5: Memory Mapped I/O

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.

Glenn Chapman


  1. hi, why don't we assign the value we want, directly to the 32-bit i/o port A data register without using a bitwise operator ??

  2. I had take courses and read tons of books , after long experince no one explain as you did

  3. You have good quality slides but please use your own voice to explain instead of letting the computer reading a text

  4. Please use your voice. Don't care what kind of accent you have (you could even include subtitles if you feel it would help). Because a computer reading makes the content sound so dull…

  5. Very good video, informative, I really learned a lot. Just curious which tool you used to create such great video ?

Leave a Reply

Your email address will not be published. Required fields are marked *