Descendre au registre, c’est piloter le STM32 en écrivant directement dans ses registres matériels (GPIOA->ODR, RCC->AHB1ENR…), sous la HAL et la LL. C’est le niveau le plus bas accessible en C, celui que la HAL masque : on y gagne en performance, en déterminisme et en compréhension, on y perd en confort et en portabilité. C’est aussi le terrain d’application direct de la manipulation de bits : masques, décalages, |=, &= ~, ^= deviennent ici concrets. On y va par besoin, pas par principe.

À quoi ça sert ?

La HAL suffit pour l’essentiel d’un projet. Quatre raisons justifient de descendre plus bas :

  • Performance. Basculer une broche par registre prend quelques cycles, là où la HAL en prend beaucoup plus (vérifications, indirection par handle). Pour une boucle serrée ou un signal rapide, l’écart compte.
  • Déterminisme. Un accès registre a une durée connue et stable, précieux pour du temps réel fin.
  • Empreinte mémoire. Sans le code de la HAL, le binaire est plus petit, utile sur les puces à très peu de Flash.
  • Comprendre et débloquer. Lire ce que fait réellement la HAL aide à déboguer, et quand un réglage matériel n’est pas exposé par la HAL, le registre reste le seul accès.

Le revers : le code registre est spécifique à la puce (il ne se transpose pas tel quel d’une F4 à une G0) et moins lisible. C’est un outil ciblé, pas un mode de vie.

Les registres via CMSIS

On n’écrit pas des adresses mémoire en dur. CMSIS (la couche standard ARM, fournie par ST) définit des en-têtes (stm32f4xx.h et consorts) qui décrivent chaque périphérique comme une structure pointant sur ses registres. Écrire GPIOA->ODR revient à accéder au registre de sortie du port A, à la bonne adresse, sans la connaître. Ces structures sont déclarées volatile : le compilateur ne doit ni supprimer ni réordonner ces accès (un registre peut changer « tout seul », côté matériel).

CMSIS fournit aussi des macros de bits nommées — RCC_AHB1ENR_GPIOAEN, GPIO_MODER_MODE5_0, GPIO_BSRR_BS5… — qu’on utilise à la place des nombres magiques. C’est exactement la discipline de la manipulation de bits : un masque nommé vaut mieux qu’un 0x00000400 illisible.

Info

Les noms exacts des macros dépendent de la famille et de la version de l’en-tête CMSIS (GPIO_MODER_MODE5_0 sur les versions récentes, parfois GPIO_MODER_MODER5_0 sur d’anciennes). En cas de doute, ouvrir l’en-tête de sa puce (Ctrl+clic sur le nom dans CubeIDE) pour vérifier l’orthographe et le champ visé.

Faire clignoter LD2 (broche PA5) sans la HAL, registre par registre, est le « Hello World » du bas niveau. Trois gestes :

/* USER CODE BEGIN 3 */
// 1. Alimenter l'horloge du port A  (sinon, tout écrit dans GPIOA est ignoré !)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
 
// 2. Configurer PA5 en sortie : champ MODE5 = 01
GPIOA->MODER &= ~GPIO_MODER_MODE5;     // effacer les 2 bits du champ
GPIOA->MODER |=  GPIO_MODER_MODE5_0;   // poser le bit de poids faible -> 01 = sortie
 
// 3. Faire clignoter via BSRR (atomique)
while (1) {
  GPIOA->BSRR = GPIO_BSRR_BS5;         // mettre PA5 à 1
  for (volatile int i = 0; i < 800000; i++);   // délai grossier
  GPIOA->BSRR = GPIO_BSRR_BR5;         // remettre PA5 à 0
  for (volatile int i = 0; i < 800000; i++);
}
/* USER CODE END 3 */

On reconnaît la grammaire de la manipulation de bits : |= pour poser un bit, &= ~ pour en effacer, des masques nommés partout. Comparé à l’unique HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin) de la HAL, c’est plus long, mais on voit exactement ce que le matériel fait. La boucle for à compteur volatile est un délai rudimentaire (à proscrire en vrai code, où l’on utilise un timer). Ici elle sert juste à rendre le clignotement visible.

Le registre MODER d'un port GPIO STM32 : 16 broches × 2 bits ; les 2 bits de la broche 5 à 01 configurent PA5 en sortie (00 entrée, 01 sortie, 10 fonction alternée, 11 analogique)|640

Prendre capture d’écran de la perspective Debug de CubeIDE, vue Registers ouverte sur GPIOA, où le bit 5 du registre ODR bascule à chaque pas d’exécution, sous forme de GIF, le mouvement étant le message.

BSRR contre ODR — l’atomicité

Deux façons de changer l’état d’une broche, qui ne se valent pas :

  • Par ODR : GPIOA->ODR |= GPIO_ODR_OD5;. C’est un lire-modifier-écrire (le |= lit le registre, modifie un bit, réécrit le tout). Si une interruption modifie une autre broche du même port entre la lecture et l’écriture, sa modification est perdue. Non atomique.
  • Par BSRR : GPIOA->BSRR = GPIO_BSRR_BS5;, une simple écriture, atomique. Les 16 bits de poids faible mettent à 1, les 16 bits de poids fort remettent à 0. Les bits non visés ne sont pas touchés. Aucune course possible.

La règle : préférer BSRR pour poser ou effacer une broche, surtout si des interruptions touchent le même port. C’est un cas d’école d’atomicité, prolongeant directement la manipulation de bits.

Le Reference Manual, pas la datasheet

La source de vérité du bas niveau est le Reference Manual (document RM0xxx de ST) : il décrit chaque registre, chaque champ de bits, leur adresse et leur effet. À ne pas confondre avec la datasheet de la puce, qui donne le brochage, les caractéristiques électriques et les limites (voir lire une datasheet). Pour coder un registre, c’est le Reference Manual qu’on ouvre, souvent en parallèle de l’en-tête CMSIS, qui en est la traduction en macros.

Pièges

Oublier d’alimenter l’horloge du périphérique. Le piège n°1 du bas niveau : écrire dans GPIOA (ou tout autre périphérique) sans avoir activé son horloge dans RCC au préalable. Les écritures sont silencieusement ignorées, sans aucune erreur. Toujours commencer par le RCC->...ENR |= ..._EN.

Lire-modifier-écrire non atomique. Utiliser ODR |= / ^= là où une interruption touche le même port : modification perdue. Préférer BSRR.

Nombres magiques au lieu de macros. Écrire 0x400 plutôt que GPIO_MODER_MODE5_0 : illisible, fragile, faux dès qu’on change de broche. Utiliser les macros CMSIS : c’est la leçon de la manipulation de bits.

Oublier volatile. Si l’on fabrique son propre pointeur de registre (au lieu de passer par CMSIS), ne pas le déclarer volatile laisse le compilateur optimiser les accès, et le code « marche en debug, pas en release ». Les définitions CMSIS sont déjà volatile.

Croire le code portable. Un blink registre écrit pour une F4 ne compile pas tel quel sur une G0 : les noms de bus (AHB1ENR vs IOPENR), parfois les champs, diffèrent. C’est le coût assumé de quitter la HAL.

Exercices

Exercice 1 — Le blink muet

Un étudiant copie les étapes 2 et 3 du blink bare-metal (MODER puis BSRR) mais oublie l’étape 1. Le code compile, se flashe, mais la LED ne s’allume jamais. Pourquoi, et que vérifier en premier ?

Exercice 2 — Pourquoi BSRR ?

Une LED est sur PA5, et une interruption rapide bascule PA6 du même port via GPIOA->ODR ^= GPIO_ODR_OD6. Dans la boucle principale, vous écrivez GPIOA->ODR |= GPIO_ODR_OD5 pour allumer la LED. Quel bug peut survenir, et comment l’éviter ?

Raccrochage projet

  • Étape 4 de la phase de preuve de concept. Descendre au registre est une optimisation ciblée, pas le point de départ : on prototype en HAL, on ne « registre » que le chemin critique identifié (un signal trop lent, une empreinte trop grosse). Une PoC qui démarre directement en bas niveau perd du temps sans gain.
  • Mélange assumé. La pratique courante consiste à configurer avec CubeMX et la HAL, puis à remplacer par des accès registre seulement les quelques lignes où la performance l’exige. Le reste reste lisible et portable.

Comprendre les registres, même sans les utiliser au quotidien, démystifie la HAL : on sait désormais ce qu’elle fait « en dessous », et l’on choisit en connaissance de cause à quel niveau écrire chaque partie du firmware.

Aller plus loin

  • Manipulation de bits — la notion transverse que cette fiche met en œuvre : masques, décalages, atomicité.
  • Programmer avec la HAL · Configurer avec CubeMX — les couches au-dessus, et d’où vient l’init des horloges.
  • Reference Manual de votre puce (RM0xxx, sur st.com) — la description exhaustive des registres.

Voir aussi