C++ Algorithmes Embarqué Notes techniques · 15 min

C++, Algos
& Embedded Tricks

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.

C++ — Les 4 piliers OOP pour l'embarqué

1 · Encapsulation

C++ · encapsulation
// 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é
};

2 · Héritage & Polymorphisme — hiérarchie de drivers

C++ · héritage pour drivers embarqués
// 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
⚠ Piège classique du destructeur

"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.

3 · Templates (généricité sans héritage)

C++ · template ring buffer générique
// 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;

Algorithmes & Complexité

// Complexités usuelles en embarqué
AlgoBestAverageWorstEmbarqué ?
Accès tableauO(1)O(1)O(1)✓ Oui
Recherche linéaireO(1)O(n)O(n)Si n petit
Recherche binaireO(1)O(log n)O(log n)✓ Oui
Tri à bullesO(n)O(n²)O(n²)Rarement
QuicksortO(n log n)O(n log n)O(n²)Avec précaution
Push/Pop ring bufferO(1)O(1)O(1)✓ Toujours
C · recherche binaire — exercice whiteboard classique
// 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
}

Embedded Tricks & Questions Pièges

volatile, const, restrict

C · qualificateurs critiques
// 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

Bitfields & Masques

C · opérations bit à bit — whiteboard classique
// 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;
}

OS Concepts — Questions fréquentes

ISR vs Task FreeRTOS
ISR : contexte hardware, non bloquant, pas de vTaskDelay. Task : contexte software, peut attendre, a sa propre stack. Les deux partagent la même mémoire → protéger avec xSemaphoreGiveFromISR.
Stack vs Heap
Stack : allocation automatique, déterministe, taille fixe. Heap : allocation dynamique (malloc), non déterministe, fragmentation possible. En embarqué critique → préférer stack et buffers statiques.
Race Condition
Deux tâches lisent-modifient-écrivent la même variable sans protection. Solution : mutex, section critique, ou variable atomique. Exemple : count++ n'est pas atomique sur ARM (3 instructions).
Malloc en embarqué critique
Non déterministe (temps d'exécution variable), fragmentation heap, peut retourner NULL. En critique (DO-178C, IEC 61508) : allocation dynamique interdite après init. Utiliser des pools statiques.

Watchdog — Hardware vs Software

C · différence watchdog matériel vs logiciel
// 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
// règles courtes à mémoriser

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