Skip to Content
Course content

146: Writing C++ Code for Embedded Systems

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

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 PwmRegister structure 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 volatile to prevent compiler optimization.
  • Demonstrate the usage by creating a Motor1 alias for the address 0x40012C00 and 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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.