Introduction: Why C++ in Embedded is Not Masochism, But a Superpower
\n\nWhen 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++.
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\nIn 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\n1. The Basics: How Embedded C++ Differs from Regular C++
\n\nThe 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\nWhat 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
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
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\n2. Practice: Working with Registers in C++ Style
\n\nIn 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\nThis works, but looks archaic. In C++, we can make a convenient wrapper without overhead.
\n\nExample: 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