Comment organiser un projet firmware qui scale — séparation HAL / drivers / application, règles de dépendance, et conventions de nommage industrielles.
Un projet embarqué qui démarre dans un seul main.c de 2000 lignes, c'est un projet difficile à maintenir, à tester, et à faire évoluer. Dès qu'un deuxième développeur touche au code — ou que vous revenez dessus 6 mois plus tard — c'est la confusion.
La solution : séparer les responsabilités en couches logiques claires, avec des règles de dépendance strictes entre elles.
Une couche ne connaît que la couche directement en dessous. L'application ne touche jamais les registres. Les drivers ne connaissent pas la logique métier.
Le header est le contrat public d'un module. Il expose uniquement ce dont les autres modules ont besoin — rien de plus.
// ── INCLUDE GUARD ── obligatoire pour éviter les inclusions multiples #ifndef GPIO_DRIVER_H #define GPIO_DRIVER_H #include "stm32f4xx.h" #include <stdint.h> // ── CONSTANTES PUBLIQUES ── #define GPIO_MODE_INPUT 0x00U #define GPIO_MODE_OUTPUT 0x01U #define GPIO_MODE_AF 0x02U #define GPIO_PULL_NONE 0x00U #define GPIO_PULL_UP 0x01U // ── TYPES PUBLICS ── typedef struct { GPIO_TypeDef *port; uint8_t pin; uint8_t mode; uint8_t pull; } GPIO_Config_t; // ── API PUBLIQUE ── (les seules fonctions que les autres modules voient) void GPIO_Init (const GPIO_Config_t *cfg); void GPIO_Write (const GPIO_Config_t *cfg, uint8_t state); uint8_t GPIO_Read(const GPIO_Config_t *cfg); void GPIO_Toggle(const GPIO_Config_t *cfg); #endif /* GPIO_DRIVER_H */
#include "gpio_driver.h" // son propre header EN PREMIER // ── VARIABLES PRIVÉES ── (static = invisible hors de ce .c) static uint32_t s_init_count = 0; // ── FONCTIONS PRIVÉES ── (static = non exportées) static void enable_gpio_clock(GPIO_TypeDef *port) { if (port == GPIOA) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; else if (port == GPIOB) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; else if (port == GPIOC) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; /* ... */ } // ── API PUBLIQUE ── void GPIO_Init(const GPIO_Config_t *cfg) { enable_gpio_clock(cfg->port); cfg->port->MODER &= ~(0x3U << (cfg->pin * 2)); cfg->port->MODER |= (cfg->mode << (cfg->pin * 2)); cfg->port->PUPDR &= ~(0x3U << (cfg->pin * 2)); cfg->port->PUPDR |= (cfg->pull << (cfg->pin * 2)); s_init_count++; } void GPIO_Toggle(const GPIO_Config_t *cfg) { cfg->port->ODR ^= (1U << cfg->pin); }
Toute variable et toute fonction qui ne fait pas partie de l'API publique doit être déclarée static. Ça évite les collisions de noms et ça documente l'intention.
main.c ne doit contenir aucune logique matérielle directe. Il initialise les modules et lance la boucle principale — point.
#include "gpio_driver.h" #include "uart_driver.h" #include "scheduler.h" #include "app.h" int main(void) { // 1. Init système SystemClock_Config(); // 2. Init drivers GPIO_Init(&led_config); UART_Init(&uart_config); // 3. Init application App_Init(); // 4. Boucle principale while (1) { Scheduler_Run(); } }
gpio_driver, uart_driver, scheduler — snake_case, nom explicite du rôleGPIO_Init(), UART_Send() — préfixe MODULE_ + PascalCase de l'actionstatic void enable_clock() — snake_case, pas de préfixe, toujours staticstatic uint32_t s_tick_count — préfixe s_ pour static, g_ si vraiment global (à éviter)Avec cette architecture en place, on peut ajouter un ring buffer UART comme module autonome — c'est exactement l'exercice de la session 7.