Notes techniques sur le C++ appliqué au firmware — les quatre piliers OOP avec exemples drivers, complexité algorithmique des structures embarquées courantes, concepts OS essentiels et pièges classiques du C bas niveau.
// Masquer l'implémentation — exposer uniquement l'interface class UartDriver { public: UartDriver(uint32_t baudrate); bool send(const char *msg, size_t len); bool recv(char *buf, size_t &len); private: uint32_t _baudrate; RingBuffer _rx_buf; // implémentation cachée bool _init_done; void _configRegisters(); // privé = non exposé };
// Classe de base abstraite — interface commune class Sensor { public: virtual float read() = 0; // pure virtual virtual bool isReady() = 0; virtual ~Sensor() = default; // destructeur virtuel OBLIGATOIRE }; // Implémentations concrètes class BME280 : public Sensor { float read() override { return i2c_read_temp(); } bool isReady() override { return check_status_register(); } }; class SimSensor : public Sensor { float read() override { return 25.0f; } // mock pour tests bool isReady() override { return true; } }; // Le code applicatif utilise Sensor* sans savoir quel driver concret est dessous void acquire(Sensor *s) { if (s->isReady()) log(s->read()); // dispatch dynamique via vtable } // Instanciation — choix à la compilation ou au runtime Sensor *prod = new BME280(); Sensor *test = new SimSensor(); acquire(prod); // lit le vrai capteur acquire(test); // lit la simulation
"Pourquoi le destructeur de la classe de base doit être virtuel ?" — Si le destructeur n'est pas virtual et qu'on delete via un pointeur de base, seul le destructeur de base est appelé. Le destructeur de la classe dérivée n'est pas appelé → fuite de ressources. Avec virtual → dispatch dynamique, les deux destructeurs sont appelés dans le bon ordre.
// Ring buffer générique — fonctionne pour uint8_t, Command_t, float... template<typename T, size_t N> class RingBuffer { public: bool push(const T &item) { size_t next = (head + 1) % N; if (next == tail) return false; // plein buf[head] = item; head = next; return true; } bool pop(T &item) { if (head == tail) return false; // vide item = buf[tail]; tail = (tail + 1) % N; return true; } bool empty() const { return head == tail; } private: T buf[N]; size_t head = 0, tail = 0; }; // Utilisation — tout à la compilation, zéro overhead runtime RingBuffer<uint8_t, 64> uart_rx; RingBuffer<Command_t, 8> cmd_queue;
| Algo | Best | Average | Worst | Embarqué ? |
|---|---|---|---|---|
| Accès tableau | O(1) | O(1) | O(1) | ✓ Oui |
| Recherche linéaire | O(1) | O(n) | O(n) | Si n petit |
| Recherche binaire | O(1) | O(log n) | O(log n) | ✓ Oui |
| Tri à bulles | O(n) | O(n²) | O(n²) | Rarement |
| Quicksort | O(n log n) | O(n log n) | O(n²) | Avec précaution |
| Push/Pop ring buffer | O(1) | O(1) | O(1) | ✓ Toujours |
// Complexité : O(log n) — tableau trié obligatoire int binary_search(const int *arr, int n, int target) { int left = 0, right = n - 1; while (left <= right) { int mid = left + (right - left) / 2; // évite overflow vs (l+r)/2 if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; // non trouvé } // Cas classique : "Trouve le premier doublon dans un tableau" int first_duplicate(const int *arr, int n) { // Approche O(n²) naïve — voir si on peut faire mieux for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) if (arr[i] == arr[j]) return arr[i]; return -1; // Amélioration O(n) avec hash set — mais pas de malloc en embarqué critique }
// volatile — interdit les optimisations du compilateur // TOUJOURS sur les variables modifiées par une ISR ou hardware volatile uint32_t tick_count; // ISR l'incrémente volatile uint32_t *reg = (volatile uint32_t*)0x40020000; // registre GPIO // const — valeur ne change pas → optimisation et sécurité void GPIO_Init(const GPIO_Config_t *cfg); // cfg ne sera pas modifié // Piège : que fait ce code ? int x = 5; /* Sans volatile, le compilateur peut "cacher" x dans un registre et ne jamais relire la mémoire → boucle infinie si ISR modifie x */ while (x != 0); // ← comportement indéfini sans volatile
// Mettre un bit à 1 (set) reg |= (1U << n); // Mettre un bit à 0 (clear) reg &= ~(1U << n); // Toggler un bit reg ^= (1U << n); // Lire un bit bool bit_val = (reg >> n) & 1U; // Modifier un champ de bits (ex: bits 5:4 = 0b10) reg &= ~(0x3U << 4); // effacer bits 5:4 reg |= (0x2U << 4); // écrire 0b10 // Cas classique : "Est-ce que n est une puissance de 2 ?" bool is_power_of_2(uint32_t n) { return (n > 0) && ((n & (n - 1)) == 0); // Astuce : 8 = 0b1000, 7 = 0b0111 → 8 & 7 = 0 } // Compter les bits à 1 (Hamming weight) int count_bits(uint32_t n) { int count = 0; while (n) { count += n & 1; n >>= 1; } return count; }
count++ n'est pas atomique sur ARM (3 instructions).// Watchdog MATÉRIEL (IWDG/WWDG STM32) // Timer hardware indépendant — reset si non rafraîchi dans le délai // Persiste même si le firmware est bloqué dans une ISR IWDG->KR = 0xAAAA; // "kick" le watchdog (reload) // Si on n'exécute pas cette ligne → reset automatique MCU // Watchdog LOGICIEL (notre pattern séance 12) // Tâche FreeRTOS surveille d'autres tâches via sémaphore // Plus flexible mais ne résiste pas à un blocage kernel-level // Les deux sont complémentaires en production
Complexité ring buffer push/pop : O(1) — index arithmétique
Pourquoi éviter malloc en critique : Non déterministe, fragmentation
Mutex vs sémaphore binaire : Mutex = propriété + priority inheritance. Sémaphore = signal sans propriétaire
Effet de volatile : Interdit au compilateur de cacher la variable en registre — re-lit toujours depuis la mémoire
ISR et vTaskDelay : Jamais combinables — utiliser xSemaphoreGiveFromISR puis portYIELD_FROM_ISR