Article STM32 Sessions 1–2 · 8 min de lecture

GPIO Bare-Metal
sur STM32

Du registre AHB1ENR à la construction d'un driver générique avec structs et pointeurs de fonction — sans HAL, sans abstraction magique.

Pourquoi bare-metal ?

Utiliser le HAL STM32 (la bibliothèque officielle ST) c'est pratique, mais ça masque ce qui se passe réellement dans le microcontrôleur. En bare-metal, on écrit directement dans les registres matériels — on comprend exactement ce qu'on contrôle, et on gagne en performance et en maîtrise.

// principe fondamental

Sur un STM32, tout est registre. Une broche GPIO, une horloge, un timer — chaque fonctionnalité est contrôlée par des bits précis dans des registres mappés en mémoire.

La carte mémoire STM32

Le cœur Cortex-M voit tout comme de la mémoire. Les registres des périphériques sont à des adresses fixes — c'est le memory-mapped I/O.

// memory_map.h — adresses clés STM32F4
0xFFFFFFFF
Cortex-M core (NVIC, SysTick...)
0x40023800
RCC — Reset & Clock Control
0x40021000
GPIOF, GPIOE, GPIOD...
0x40020000
GPIOA — base address
0x20000000
SRAM — variables, stack
0x08000000
Flash — code, const

Activer l'horloge : RCC_AHB1ENR

Avant de toucher une GPIO, il faut activer son horloge dans le registre RCC_AHB1ENR. Sans ça, écrire dans les registres GPIO ne fait rien — le périphérique est éteint.

// RCC->AHB1ENR (32 bits) — chaque bit = un port GPIO
AHB1ENR
...
7
6
5
4
3
GPIOD
2
GPIOC
1
GPIOB
0
GPIOA

bit 0 = 1 → GPIOA activé  |  bit 1 = 1 → GPIOB activé  |  etc.

C · rcc_init.c
// Activer l'horloge de GPIOA
// RCC est un pointeur vers la struct des registres RCC
RCC->AHB1ENR |= (1U << 0);   // bit 0 = GPIOA
RCC->AHB1ENR |= (1U << 1);   // bit 1 = GPIOB

// Ou avec les macros du CMSIS :
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
⚠ erreur fréquente

Ne jamais utiliser = mais toujours |= pour ne pas effacer les bits des autres ports déjà activés.

Configurer le mode : MODER

Chaque broche GPIO a 2 bits dans MODER pour définir son mode : entrée, sortie, alternatif (UART, SPI...) ou analogique.

// GPIOA->MODER — 2 bits par pin (32 bits total = 16 pins)
MODER
...[31:10]
MODER4[5:4]
PA4
MODER3[7:6]
MODER2[5:4]
MODER1[3:2]
MODER0[1:0]
PA0=OUT
00 → Entrée
01 → Sortie
10 → Alternatif
11 → Analogique
C · gpio_raw.c
// Configurer PA5 en sortie (LED sur Nucleo)
// Étape 1 : effacer les 2 bits de PA5 (bits 11:10)
GPIOA->MODER &= ~(0x3U << (5 * 2));

// Étape 2 : écrire 01 = sortie
GPIOA->MODER |=  (0x1U << (5 * 2));

// Allumer la LED : mettre PA5 à 1
GPIOA->ODR |= (1U << 5);

// Éteindre : mettre PA5 à 0
GPIOA->ODR &= ~(1U << 5);

// Toggle propre (atomique) via BSRR
GPIOA->BSRR = (1U << (5 + 16));  // reset PA5
GPIOA->BSRR = (1U << 5);          // set PA5

Le driver générique : structs + pointeurs

Copier-coller du code brut pour chaque broche est vite ingérable. La solution : encapsuler la configuration dans une struct et passer un pointeur — le firmware devient modulaire et réutilisable.

// Architecture du driver générique
typedef struct GPIO_Config_t
GPIO_TypeDef*
port
GPIOA, GPIOB, GPIOC...
uint8_t
pin
0 → 15
uint8_t
mode
GPIO_MODE_OUTPUT / INPUT / AF
uint8_t
pull
GPIO_PULL_NONE / UP / DOWN
C · gpio_driver.c
// Initialisation via pointeur sur config
void GPIO_Init(const GPIO_Config_t *cfg) {
    // 1. Activer l'horloge du port
    RCC_EnableGPIOClock(cfg->port);

    // 2. Configurer MODER
    cfg->port->MODER &= ~(0x3U << (cfg->pin * 2));
    cfg->port->MODER |=  (cfg->mode << (cfg->pin * 2));

    // 3. Configurer PUPDR (pull-up/down)
    cfg->port->PUPDR &= ~(0x3U << (cfg->pin * 2));
    cfg->port->PUPDR |=  (cfg->pull << (cfg->pin * 2));
}

// Utilisation — propre et lisible
GPIO_Config_t led = {
    .port = GPIOA,
    .pin  = 5,
    .mode = GPIO_MODE_OUTPUT,
    .pull = GPIO_PULL_NONE
};
GPIO_Init(&led);
GPIO_Write(&led, 1);  // allumer

Points clés à retenir

Ordre obligatoire
Toujours activer l'horloge RCC avant de configurer les registres GPIO — sinon les écritures sont ignorées silencieusement.
Masque avant écriture
Toujours &= ~(mask) pour effacer les bits cibles avant |= valeur. Ne jamais faire = valeur directement.
BSRR vs ODR
Préférer BSRR à ODR pour set/reset : opération atomique, pas de risque de corruption par interruption.
Généricité via struct
Passer const GPIO_Config_t* aux fonctions driver plutôt que des paramètres séparés — le code est plus lisible, testable et réutilisable.

Séquence d'initialisation

1. RCC→AHB1ENR |= (1 << port)
2. MODER : effacer 2 bits → écrire mode
3. OTYPER : push-pull (0) ou open-drain (1)
4. OSPEEDR : vitesse de commutation
5. PUPDR : pull-up / pull-down / none
6. ODR / BSRR : contrôle de l'état logique
// session suivante

La prochaine étape naturelle : ajouter des pointeurs de fonction (callbacks) au driver pour rendre les transitions d'état configurables à l'exécution.