Skip to Content
Course content

208: Volatile and Memory-Mapped Registers Concept

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you start moving from general-purpose application programming into the world of embedded systems or driver development, you'll encounter a concept called Memory-Mapped I/O (MMIO). Essentially, the hardware designers decide that a specific address in the memory map isn't actually RAM, but a gateway to a piece of hardware—like a timer, a serial port, or a GPIO pin. Writing to that address toggles a physical pin; reading from it tells you the current state of a sensor.

Targeting a Specific Memory Address

Let's imagine we're working with a hypothetical microcontroller. There's a status register located at address 0x40001000. This register has a "Ready" bit at position 0. If that bit is 1, the hardware is ready to receive data. To interact with this in C, we can't just declare a variable; we have to point a pointer directly at that hardware address.

unsigned int *status_reg = (unsigned int *)0x40001000;

I've cast the integer address to an unsigned int *. This tells the compiler, "I know this looks like a random number, but trust me, there is an unsigned integer living at this exact spot in the memory map."

Waiting for the Hardware to Wake Up

Now, I want to write a function that blocks the program until the hardware is ready. The logic is simple: keep reading the register in a loop until that first bit becomes a 1. Here is how I first wrote it:

void wait_for_ready() {
    unsigned int *status_reg = (unsigned int *)0x40001000;
    
    while ((*status_reg & 0x1) == 0) {
        // Just spin here until the bit flips to 1
    }
}

On my first test run with optimizations turned off (-O0), it worked perfectly. But as soon as I turned on the compiler optimizations (-O2 or -O3), the program hung forever, even when the hardware was clearly ready. I spent an hour scratching my head before I realized I'd fallen into a classic trap.

The Optimizer's Trap

Here is what happened: the compiler looked at my while loop and saw that nothing inside the loop body was changing the value of *status_reg. From the compiler's perspective, if the value was 0 the first time it checked, it must be 0 forever. To "help" me, the optimizer decided to read the value once into a CPU register and then just check that register repeatedly, effectively transforming my code into this:

// What the compiler actually generated
unsigned int temp = *status_reg;
if ((temp & 0x1) == 0) {
    while (1) { } // Infinite loop!
}

The compiler has no idea that the hardware—something outside the scope of the C program—can change that memory location at any millisecond. It assumes it is the only entity manipulating memory.

Forcing a Fresh Read with Volatile

This is exactly why the volatile keyword exists. By marking the pointer as volatile, I'm telling the compiler: "The value at this address can change for reasons you cannot see. Do not optimize reads or writes to this location; fetch it from memory every single time."

Here is the corrected version:

void wait_for_ready() {
    // Note the 'volatile' keyword here
    volatile unsigned int *status_reg = (volatile unsigned int *)0x40001000;
    
    while ((*status_reg & 0x1) == 0) {
        // Now the compiler will actually re-read the memory address 
        // on every single iteration of the loop.
    }
}

A quick tip: notice where I put the volatile. I'm telling the compiler that the data being pointed to is volatile, not the pointer itself. If I wrote volatile unsigned int * volatile status_reg, I'd be saying both the address and the value could change unexpectedly. In 99% of MMIO cases, it's just the value that is volatile.




📋 Practical Task

Implementing a Hardware Timer Polling Loop

You are writing a driver for a hardware timer. The timer has a Control Register at 0x40002000 and a Value Register at 0x40002004.

  • The Control Register's bit 0 is the "Enable" bit (1 = On, 0 = Off).
  • The Value Register increments every clock cycle.

Write a function called timer_delay_cycles that takes an unsigned int cycles argument. The function should:

  1. Enable the timer by writing a 1 to the Control Register.
  2. Read the current value of the Value Register and store it as a starting point.
  3. Poll the Value Register in a loop until the difference between the current value and the starting value is greater than or equal to cycles.
  4. Disable the timer by writing a 0 to the Control Register.

Ensure that you use the volatile keyword correctly so that the compiler does not optimize away your polling loop.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.