CodeBlanks Logo Icon CodeBlanks

Embedded

Write optimized code with embedded constraints.

volatile and memory-mapped I/O

Memory-mapped I/O (MMIO) makes hardware registers appear at ordinary memory addresses. Reading a register means reading the value at that address. The volatile keyword tells the compiler "always read from memory on every access, do not cache".

Example: volatile std::uint32_t* reg points at a hardware register. *reg = 64; writes; *reg reads. The compiler cannot skip these accesses. In this lesson we use a volatile std::uint32_t variable as a fake register to learn the MMIO pattern.

Compiler optimization and volatile

The compiler optimizer may reuse a previous result when you read a variable multiple times. For an ordinary variable this is safe; the value stays the same unless the program changes it. For a hardware register the value may change outside the program; a cached read would be wrong.

volatile turns this optimization off. Every *reg read goes to memory. In this lesson's example we write 64 to the fake register, then read it back. The result is 64; thanks to volatile, the read is not skipped.

Practice Add the readRegister and writeRegister methods

Take the previous lesson's solution (with parallelSum in the Engine class). Under Engine's public: section add two static methods: static std::uint32_t readRegister(volatile std::uint32_t* reg) with body return *reg;, static void writeRegister(volatile std::uint32_t* reg, std::uint32_t value) with body *reg = value;. In main after the parallelSum call add: volatile std::uint32_t reg = 0;, Engine::writeRegister(&reg, 64);, std::cout << std::endl << Engine::readRegister(&reg);. #include <cstdint> is required.

Starter code

main.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <cstdint>

class Engine {
public:
    static const int Version = 1;
    static int parallelSum() {
        std::atomic<int> total{0};
        std::thread t1([&]() { for (int i = 1; i <= 4; ++i) total += i; });
        std::thread t2([&]() { for (int i = 5; i <= 8; ++i) total += i; });
        t1.join();
        t2.join();
        return total.load();
    }
    // Add readRegister and writeRegister here
};

template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static const int value = 1;
};

int main() {
    std::cout << "Ready" << std::endl;
    std::cout << Factorial<5>::value << std::endl;
    std::cout << Engine::parallelSum() << std::endl;
    // Add the register access here
    return 0;
}

Sample solution

main.cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <cstdint>

class Engine {
public:
    static const int Version = 1;
    static int parallelSum() {
        std::atomic<int> total{0};
        std::thread t1([&]() { for (int i = 1; i <= 4; ++i) total += i; });
        std::thread t2([&]() { for (int i = 5; i <= 8; ++i) total += i; });
        t1.join();
        t2.join();
        return total.load();
    }
    static std::uint32_t readRegister(volatile std::uint32_t* reg) {
        return *reg;
    }
    static void writeRegister(volatile std::uint32_t* reg, std::uint32_t value) {
        *reg = value;
    }
};

template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static const int value = 1;
};

int main() {
    std::cout << "Ready" << std::endl;
    std::cout << Factorial<5>::value << std::endl;
    std::cout << Engine::parallelSum() << std::endl;
    volatile std::uint32_t reg = 0;
    Engine::writeRegister(&reg, 64);
    std::cout << Engine::readRegister(&reg);
    return 0;
}

Expected output

Ready
120
36
64

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 Complete the register pointer

Question
std::uint32_t readRegister(volatile std::uint32_t* ____) { return *reg; }
Answer
std::uint32_t readRegister(volatile std::uint32_t* reg) { return *reg; }

Explanation: The pointer name reg is used.

2 Match each piece to its role

Term Meaning
volatile Disables optimization
std::uint32_t* Register pointer
*reg = 64 Write to register
*reg Read from register

3 What does the volatile keyword do?

main.cpp
volatile std::uint32_t* reg;
  • It makes the variable constant
  • It tells the compiler not to skip reads Correct answer
  • It automatically resets the variable
  • It makes the variable thread-local

Explanation: volatile forces every read to go to memory; it does not cache.

All lessons in Expert