Du registre AHB1ENR à la construction d'un driver générique avec structs et pointeurs de fonction — sans HAL, sans abstraction magique.
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.
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.
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.
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.
bit 0 = 1 → GPIOA activé | bit 1 = 1 → GPIOB activé | etc.
// 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;
Ne jamais utiliser = mais toujours |= pour ne pas effacer les bits des autres ports déjà activés.
Chaque broche GPIO a 2 bits dans MODER pour définir son mode : entrée, sortie, alternatif (UART, SPI...) ou analogique.
// 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
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.
// 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
&= ~(mask) pour effacer les bits cibles avant |= valeur. Ne jamais faire = valeur directement.BSRR à ODR pour set/reset : opération atomique, pas de risque de corruption par interruption.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.La prochaine étape naturelle : ajouter des pointeurs de fonction (callbacks) au driver pour rendre les transitions d'état configurables à l'exécution.