C++
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.
Exercise 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(®, 64);, std::cout << std::endl << Engine::readRegister(®);. #include <cstdint> is required.
Show example solution
#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(®, 64);
std::cout << Engine::readRegister(®);
return 0;
}
Test yourself
-
Complete the register pointer
std::uint32_t readRegister(volatile std::uint32_t* ____) { return *reg; }
Show answer
std::uint32_t readRegister(volatile std::uint32_t* reg) { return *reg; }
-
Match each piece to its role
- volatile
- Disables optimization
- std::uint32_t*
- Register pointer
- *reg = 64
- Write to register
- *reg
- Read from register
-
What does the volatile keyword do?
volatile std::uint32_t* reg;
- It makes the variable constant
- It tells the compiler not to skip reads
- It automatically resets the variable
- It makes the variable thread-local
Show answer
It tells the compiler not to skip reads — volatile forces every read to go to memory; it does not cache.
Next lesson: Putting it together