Guide Architecture Session 6 · 12 min de lecture

Architecture Multi-Fichiers
Embarquée

Comment organiser un projet firmware qui scale — séparation HAL / drivers / application, règles de dépendance, et conventions de nommage industrielles.

Le problème du fichier unique

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.

Les 3 couches fondamentales

// architecture.layers — du matériel à l'applicatif
Application
Logique métier, machine à états, orchestration des drivers
app.c / app.h
main.c
↕ appelle uniquement vers le bas (jamais vers le haut)
Drivers
GPIO, UART, SPI — logique de périphérique, indépendante du HAL
gpio_driver.c/.h
uart_driver.c/.h
scheduler.c/.h
HAL / BSP
Accès registres, CMSIS, abstraction matérielle minimale
stm32f4xx.h
system_stm32f4xx.c
Hardware
STM32, périphériques physiques, signaux électriques
MCU · PCB
// règle d'or

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.

Structure de fichiers type

projet_stm32/
├── Core/
├── Inc/ // headers uniquement
├── gpio_driver.h
├── uart_driver.h
├── scheduler.h
└── app.h
└── Src/ // implémentations
├── gpio_driver.c
├── uart_driver.c
├── scheduler.c
├── app.c
└── main.c
├── Drivers/ // CMSIS + HAL ST (ne pas modifier)
├── CMSIS/
└── STM32F4xx_HAL_Driver/
└── Makefile / .cproject

Anatomie d'un fichier .h correct

Le header est le contrat public d'un module. Il expose uniquement ce dont les autres modules ont besoin — rien de plus.

C · gpio_driver.h
// ── 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 */

Anatomie d'un fichier .c correct

C · gpio_driver.c
#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);
}
⚠ règle static

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 : chef d'orchestre, pas artisan

main.c ne doit contenir aucune logique matérielle directe. Il initialise les modules et lance la boucle principale — point.

C · main.c — structure idéale
#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();
    }
}

Conventions de nommage

Modules (fichiers)
gpio_driver, uart_driver, scheduler — snake_case, nom explicite du rôle
Fonctions publiques
GPIO_Init(), UART_Send() — préfixe MODULE_ + PascalCase de l'action
Fonctions privées
static void enable_clock() — snake_case, pas de préfixe, toujours static
Variables globales
static uint32_t s_tick_count — préfixe s_ pour static, g_ si vraiment global (à éviter)
// session suivante

Avec cette architecture en place, on peut ajouter un ring buffer UART comme module autonome — c'est exactement l'exercice de la session 7.