C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
146: Writing C++ Code for Embedded Systems
I've sat in countless code reviews where a senior engineer—usually one who learned C in the 80s—looked at a C++ header and said, "We can't use this here; it's too heavy for the microcontroller." There is a persistent belief that C++ is just "C with a lot of bloated overhead" and that using it on a resource-constrained device will inevitably lead to a massive binary size or unpredictable runtime performance.
That is fundamentally wrong. In the embedded world, we care about "zero-cost abstractions." If you use the language correctly, the compiler often collapses your high-level C++ abstractions into the exact same machine code you would have written by hand in C. I'll show you why.
The Myth That C++ Always Adds Bloat
Consider how we typically handle a hardware register in C. You'd likely see a macro like this:
#define LED_REG (*((volatile uint32_t*)0x40021018))
LED_REG |= (1 << 5); // Turn on the LED
It's fast, but it's dangerous. There's no type safety, and it's easy to accidentally write to the wrong address. A novice C++ developer might try to wrap this in a heavy class with virtual functions and dynamic memory, which would indeed bloat the binary. But that's not how we do it in professional embedded systems.
Leveraging Zero-Cost Abstractions for Hardware
Instead, we use constexpr and templates. By telling the compiler that the address is a constant known at compile-time, we eliminate the overhead of variable storage. Look at this approach:
template <uintptr_t Address>
struct Register {
static void set_bit(int bit) {
*reinterpret_cast<volatile uint32_t*>(Address) |= (1u << bit);
}
};
using StatusLed = Register<0x40021018>;
// Usage:
StatusLed::set_bit(5);
If you look at the assembly generated for that set_bit call, it is identical to the C macro. The "class" doesn't actually exist in the final binary; it's just a way to organize the code. You get the type safety and organization of C++ without paying a single byte in RAM or a single clock cycle in execution time. That's the "zero-cost" promise.
Stopping the Heap from Killing Your System
Now, here is where I need you to be careful. While the language features are lean, the Standard Library can be a minefield. In a desktop app, std::vector or std::string are your best friends. In an embedded system with 32KB of RAM, they are your enemies.
Dynamic memory allocation (the heap) is non-deterministic. If malloc or new takes longer to find a block of memory because the heap is fragmented, you might miss a real-time deadline, which in an embedded context could mean a motor crashing or a sensor reading being skipped. I generally advise my teams to ban the heap entirely after the initialization phase. Use std::array instead of std::vector, and use fixed-size buffers. If you need a flexible list, allocate a static pool of objects at startup and manage them yourself.
Talking to Hardware Without the Compiler Optimizing You Into a Corner
One thing that trips up people moving from desktop C++ to embedded is the volatile keyword. Modern compilers are incredibly aggressive at optimizing. If you have a loop that waits for a hardware flag to change:
while (*status_reg == 0) {
// Wait for hardware to set the bit
}
The compiler looks at that and thinks, "I already read status_reg and it was 0. Nothing in this loop changes it, so I'll just assume it's always 0 and turn this into an infinite loop." It doesn't know that the hardware is changing that value behind its back. Always mark your memory-mapped I/O pointers as volatile to tell the compiler: "Don't optimize this; the value can change unexpectedly."
📋 Practical Task
Exercise: Implementing a Type-Safe PWM Controller Wrapper
You are working on a motor controller for a small drone. You have a PWM (Pulse Width Modulation) register at address 0x40012C00. Writing a value from 0 to 1023 to this register controls the motor speed.
Your task is to create a C++ wrapper that prevents the developer from writing a value outside the 0-1023 range, ensuring that this check happens at compile-time whenever possible, and uses volatile to ensure the hardware is actually updated.
Requirements:
- Create a template-based
PwmRegisterstructure that takes the memory address as a template parameter. - Implement a
set_duty_cycle(uint32_t value)method. - Inside this method, implement a check: if the value is greater than 1023, it should be clamped to 1023.
- Ensure the pointer to the register is marked
volatileto prevent compiler optimization. - Demonstrate the usage by creating a
Motor1alias for the address0x40012C00and calling the method.
Note: Since you aren't running this on actual hardware, you can simulate the register by declaring a global uint32_t fake_reg and using its address for the template parameter.
There are no comments for now.