Embedded C++: How to Write Code for Microcontrollers Without Losing Your Mind

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why C++ in Embedded is Not Masochism, But a Superpower

\n\n

When you hear "embedded development," the typical image that comes to mind is a stern engineer with a soldering iron, writing in pure C and praying to the datasheet. Or, on the contrary, an enthusiast uploading digitalWrite() on Arduino and being happy that an LED is blinking. But between these two worlds, there is a golden mean — embedded development in C++.

\n\n

Yes, that same C++ that scares people with templates and virtual functions. In reality, modern microcontrollers (ARM Cortex-M, RISC-V) are perfectly capable of digesting C++ code. Moreover, the judicious use of OOP, encapsulation, and even templates can make your embedded code cleaner, safer, and faster.

\n\n

In this article, we will break down how to write efficient C++ code for microcontrollers, avoid the pitfalls of virtual tables, and learn to work with hardware at the register level, but in a C++ style. Let's go?

\n\n

1. The Basics: How Embedded C++ Differs from Regular C++

\n\n

The main rule of embedded development: resources are limited. You don't have gigabytes of RAM and a gigahertz processor. You have 32 KB of flash and 8 KB of RAM. Every byte counts. Therefore, C++ in embedded is not about "I'll write a class with 10 levels of inheritance," but about control over abstractions.

\n\n

What you can use without fear:

\n
    \n
  • Classes and structs — for grouping data and functions. This is free (in terms of memory) if there are no virtual functions.
  • \n
  • Namespaces — to avoid proliferating prefixes like hal_gpio_init().
  • \n
  • constexpr and templates — compile-time calculations, saving clock cycles.
  • \n
  • References and passing by const reference — safer than pointers.
  • \n
\n\n

What it's better not to touch without a dire need:

\n
    \n
  • Dynamic memory (new/delete) — the heap fragments, the allocator can fail. In bare-metal projects, it's taboo.
  • \n
  • Exceptions (try/catch) — require runtime support, take up a lot of space.
  • \n
  • Virtual functions — each virtual function creates a table (vtable) in RAM/Flash, and the call goes through double indirection. But sometimes they are justified (drivers).
  • \n
  • RTTI (typeid) — not needed at all.
  • \n
\n\n

The essence is simple: the C++ compiler generates code as efficient as C, if you don't use "heavy" features. Check the disassembler — and you'll be happy.

\n\n

2. Practice: Working with Registers in C++ Style

\n\n

In classic embedded C, we write something like:

\n
#define GPIOA_BASE 0x40020000\n#define GPIOA_MODER (*(volatile uint32_t*)(GPIOA_BASE + 0x00))\nGPIOA_MODER |= (1 << 10); // Set bit 10 (output)
\n\n

This works, but looks archaic. In C++, we can make a convenient wrapper without overhead.

\n\n

Example: A class for working with a pin

\n
#include <cstdint>\n\nclass GpioPin {\npublic:\n    // Disable copying\n    GpioPin(const GpioPin&) = delete;\n    GpioPin& operator=(const GpioPin&) = delete;\n\n    // Enumeration for modes\n    enum class Mode : uint32_t {\n        Input = 0b00,\n        Output = 0b01,\n        AltFunc = 0b10,\n        Analog = 0b11\n    };\n\n    // Constructor: takes port address and pin number (0-15)\n    GpioPin(volatile uint32_t* port_base, uint8_t pin)\n        : port_(port_base), pin_(pin) {\n        // Can configure the pin immediately\n    }\n\n    // Set the operating mode\n    void setMode(Mode mode) {\n        volatile uint32_t* moder = port_ + 0x00; // MODER register offset\n        *moder &= ~(0b11 << (pin_ * 2));\n        *moder |= (static_cast<uint32_t>(mode) << (pin_ * 2));\n    }\n\n    // Set high level\n    void high() {\n        volatile uint32_t* bsrr                

Blogs

Book Recommendations