Introduction : Pourquoi le C++ en embarqué n'est pas du masochisme, mais un super-pouvoir
\n\nQuand on entend « développement embarqué », on imagine généralement un ingénieur austère avec un fer à souder, qui écrit en C pur et prie sur la datasheet. Ou, à l'inverse, un passionné qui charge du digitalWrite() sur Arduino et se réjouit de faire clignoter une LED. Mais entre ces deux mondes, il existe un juste milieu — le développement embarqué en C++.
Oui, oui, ce même C++ qui fait peur avec ses templates et ses fonctions virtuelles. En réalité, les microcontrôleurs modernes (ARM Cortex-M, RISC-V) sont tout à fait capables de digérer du code C++. De plus, une utilisation judicieuse de la POO, de l'encapsulation et même des templates peut rendre votre code embarqué plus propre, plus sûr et plus rapide.
\n\nDans cet article, nous allons voir comment écrire du code C++ efficace pour microcontrôleurs, contourner les pièges des tables virtuelles et apprendre à travailler avec le matériel au niveau des registres, mais dans le style C++. C'est parti ?
\n\n1. Les bases : En quoi le C++ embarqué diffère du C++ classique
\n\nLa règle d'or du développement embarqué : les ressources sont limitées. Vous n'avez pas des gigaoctets de RAM ni un processeur à plusieurs gigahertz. Vous avez 32 Ko de flash et 8 Ko de RAM. Chaque octet compte. Par conséquent, le C++ en embarqué, ce n'est pas « je vais écrire une classe avec 10 niveaux d'héritage », mais le contrôle des abstractions.
\n\nCe que vous pouvez utiliser sans crainte :
\n- \n
- Les classes et structures — pour regrouper données et fonctions. C'est gratuit (en mémoire) s'il n'y a pas de fonctions virtuelles. \n
- Les espaces de noms — pour éviter de multiplier les préfixes comme
hal_gpio_init(). \n - constexpr et les templates — calculs à la compilation, économie de cycles. \n
- Les références et le passage par référence constante — plus sûrs que les pointeurs. \n
Ce qu'il vaut mieux éviter sauf nécessité absolue :
\n- \n
- La mémoire dynamique (new/delete) — le tas se fragmente, l'allocateur peut faillir. Dans les projets bare-metal, c'est tabou. \n
- Les exceptions (try/catch) — nécessitent un support runtime, prennent beaucoup de place. \n
- Les fonctions virtuelles — chaque fonction virtuelle crée une table (vtable) en RAM/Flash, et l'appel passe par une double indirection. Mais elles sont parfois justifiées (pilotes). \n
- Le RTTI (typeid) — totalement inutile. \n
Le principe est simple : le compilateur C++ génère un code aussi efficace que le C, si vous n'utilisez pas de fonctionnalités « lourdes ». Vérifiez le désassembleur — et vous serez heureux.
\n\n2. Pratique : Travailler avec les registres dans le style C++
\n\nEn C embarqué classique, on écrit quelque chose comme :
\n#define GPIOA_BASE 0x40020000\n#define GPIOA_MODER (*(volatile uint32_t*)(GPIOA_BASE + 0x00))\nGPIOA_MODER |= (1 << 10); // On met le bit 10 à 1 (sortie)\n\nCela fonctionne, mais semble archaïque. En C++, nous pouvons créer une enveloppe pratique sans surcoût.
\n\nExemple : Classe pour travailler avec une broche
\n#include <cstdint>\n\nclass GpioPin {\npublic:\n // Interdire la copie\n GpioPin(const GpioPin&) = delete;\n GpioPin& operator=(const GpioPin&) = delete;\n\n // Énumération pour les modes\n enum class Mode : uint32_t {\n Input = 0b00,\n Output = 0b01,\n AltFunc = 0b10,\n Analog = 0b11\n };\n\n // Constructeur : prend l'adresse du port et le numéro de broche (0-15)\n GpioPin(volatile uint32_t* port_base, uint8_t pin)\n : port_(port_base), pin_(pin) {\n // On peut configurer la broche immédiatement\n }\n\n // Définir le mode de fonctionnement\n void setMode(Mode mode) {\n volatile uint32_t* moder = port_ + 0x00; // Décalage du registre MODER\n *moder &= ~(0b11 << (pin_ * 2));\n *moder |= (static_cast<uint32_t>(mode) << (pin_ * 2));\n }\n\n // Mettre à l'état haut\n void high() {\n volatile uint32_t* bsrr