Embedded
- C++ course
- Expert
- Lesson 46/47
- 6 min
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(®, 64);, std::cout << std::endl << Engine::readRegister(®);. #include <cstdint> is required.
Starter code
#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
#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;
}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
std::uint32_t readRegister(volatile std::uint32_t* ____) { return *reg; }
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?
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.