La HAL (Hardware Abstraction Layer, couche d’abstraction matérielle) est l’API native de STMicroelectronics pour piloter les périphériques d’un STM32 : GPIO, UART, timers, ADC, bus… C’est l’API que le code généré par CubeMX emploie par défaut. Elle est portable (le même appel HAL_UART_Transmit fonctionne d’une F0 à une H7), au prix d’un peu de verbosité. Apprendre la HAL, c’est apprendre à programmer le microcontrôleur une fois qu’il est configuré : la suite naturelle de CubeMX. Pour le détail du langage, voir C++ ; pour structurer l’ensemble, firmware.
À quoi ça sert ?
La HAL répond à un problème : piloter un périphérique STM32 « à la main » demande d’écrire les bons bits dans les bons registres, dans le bon ordre. C’est fastidieux et peu portable. La HAL encapsule ces séquences dans des fonctions lisibles et communes à toute la gamme :
- Lisibilité.
HAL_UART_Transmit(&huart2, ...)dit ce qu’il fait. La séquence de registres équivalente, non. - Portabilité. Le code écrit en HAL se transpose d’une puce STM32 à une autre avec peu de changements, un atout pour réutiliser une base de projet.
- Cohérence avec CubeMX. Les handles manipulés par la HAL sont précisément ce que CubeMX génère : les deux outils sont faits pour aller ensemble.
La HAL n’est pas la seule couche : pour la performance brute, on descend en LL ou au registre. Mais c’est le bon défaut pour la majorité d’un projet.
Les handles
Chaque périphérique est représenté par une structure handle, générée par CubeMX : UART_HandleTypeDef huart2, TIM_HandleTypeDef htim3, ADC_HandleTypeDef hadc1… Le handle contient toute la configuration du périphérique (broches, débit, mode…). Les fonctions HAL prennent l’adresse du handle en premier argument :
HAL_UART_Transmit(&huart2, (uint8_t *)"Bonjour\r\n", 9, HAL_MAX_DELAY);On ne crée pas ces handles soi-même : on les configure dans CubeMX et on les utilise dans le code. Choisir le bon handle (&huart2, pas &huart1) est une cause d’erreur fréquente.
Les trois modes d’un périphérique
Un même périphérique se pilote de trois façons en HAL, du plus simple au plus efficace. Comprendre ce choix est le cœur de la HAL.
Scrutation (polling, bloquant). La fonction fait le travail et rend la main une fois terminé (ou au bout d’un délai d’attente). Simple, mais elle immobilise le programme pendant l’opération.
HAL_UART_Transmit(&huart2, data, len, 100); // bloque jusqu'à la fin, ou 100 msInterruption. La fonction _IT lance l’opération et rend la main aussitôt. Quand l’opération est finie, la HAL appelle une fonction de rappel (callback) que vous écrivez. Le cœur reste libre entre-temps. Nécessite que l’interruption du périphérique soit activée dans le NVIC (case à cocher dans CubeMX).
HAL_UART_Receive_IT(&huart2, &rx, 1); // arme la réception d'un octet
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// appelée automatiquement quand un octet est reçu
HAL_UART_Receive_IT(&huart2, &rx, 1); // ré-armer pour le suivant
}DMA (Direct Memory Access). La fonction _DMA confie le transfert à un contrôleur dédié qui déplace les données sans le cœur. Un callback signale la fin. Idéal pour de gros volumes (audio, écran, blocs de mesures). Nécessite un canal DMA configuré dans CubeMX.
HAL_UART_Transmit_DMA(&huart2, buffer, taille); // le DMA s'en chargeTip
Quel mode choisir ? En scrutation pour le simple et l’occasionnel (un message de debug). En interruption pour réagir à un événement sans bloquer (réception d’octets, fin de conversion ADC). En DMA pour les gros transferts réguliers. On commence souvent en scrutation, on passe en interruption ou DMA quand le blocage devient gênant, exactement la logique de l’interruption et de la structuration du firmware.
HAL ou LL
À côté de la HAL, ST fournit la couche LL (Low-Layer) : des fonctions très fines, souvent inline, qui sont presque des écritures de registre déguisées (LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5)). Comparées à la HAL :
- HAL — portable, lisible, un peu de surcoût et de mémoire ; le défaut.
- LL — proche du registre, rapide, compacte, moins portable ; pour les chemins critiques en performance ou les puces à très peu de mémoire.
On mélange les deux sans problème, périphérique par périphérique : HAL partout, LL là où ça compte. Le choix se fait dans CubeMX (Project Manager → Advanced Settings → Driver Selector) et il est exclusif pour un même périphérique : un USART donné est généré en HAL ou en LL, jamais dans les deux à la fois. La couche encore en dessous, l’accès direct aux registres, fait l’objet de descendre au registre.
Exemple — Bouton, LED et port série en HAL
Sur une Nucleo configurée dans CubeMX (LD2 en sortie, B1 en entrée sur PC13, USART2 à 115200), ce programme allume la LED tant que le bouton est appuyé et annonce chaque changement sur le port série, tout en HAL, en scrutation :
/* USER CODE BEGIN 3 */
GPIO_PinState etat = HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);
if (etat == GPIO_PIN_RESET) { // B1 appuyé = niveau bas sur Nucleo
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
}
HAL_Delay(20); // anti-rebond simple
/* USER CODE END 3 */Pour passer la réception série en interruption (écho des caractères reçus, sans bloquer la boucle), on arme la réception une fois et on écrit le callback, dans les zones USER CODE :
/* USER CODE BEGIN PV */
uint8_t rx;
/* USER CODE END PV */
/* USER CODE BEGIN 2 */ // une fois, après l'init
HAL_UART_Receive_IT(&huart2, &rx, 1);
/* USER CODE END 2 */
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
HAL_UART_Transmit(&huart2, &rx, 1, 10); // renvoie l'octet reçu
HAL_UART_Receive_IT(&huart2, &rx, 1); // ré-arme la réception
}
/* USER CODE END 4 */La boucle principale reste libre pendant que les caractères sont reçus et renvoyés « en tâche de fond » par l’interruption. C’est le passage du bloquant au réactif, le geste qui distingue un firmware qui attend d’un firmware qui réagit.
Le programme ne dit rien de lui-même : il ne renvoie que ce qu’il reçoit. En tapant bonjour dans la zone de saisie du moniteur, la carte retourne :
bonjour
Caractère par caractère, en réalité : sept passages dans le callback. Et pendant ce temps la LED continue de suivre le bouton : c’est la preuve que la boucle n’a jamais attendu.
Pièges
Oublier HAL_Init() ou SystemClock_Config(). CubeMX les place au début de main. En code écrit à la main, les omettre laisse la HAL ou l’horloge non initialisées et rien ne marche. Vérifier leur présence avant toute chose.
Bloquer sur un délai infini. HAL_UART_Transmit(..., HAL_MAX_DELAY) attend indéfiniment. Si la liaison est muette, tout le programme se fige. Préférer un délai d’attente fini quand le blocage est risqué, ou passer en interruption/DMA.
Interruption non activée dans le NVIC. Utiliser HAL_..._IT sans cocher la ligne du périphérique dans l’onglet NVIC de CubeMX : le callback n’est jamais appelé, sans aucune erreur de compilation. Le piège n°1 du mode interruption.
Oublier de ré-armer la réception _IT. Un HAL_UART_Receive_IT ne déclenche qu’un callback. Sans le re-armer à l’intérieur, on ne reçoit que le premier octet.
Mauvais handle. Passer &huart1 à la place de &huart2 (ou un timer pour un autre) : le périphérique visé n’est pas celui qu’on croit.
HAL_Delay dans une interruption. HAL_Delay repose sur l’interruption SysTick. L’appeler depuis un callback de priorité supérieure ou égale peut bloquer définitivement. Ne pas temporiser dans une interruption : y faire le strict minimum.
Exercices
Exercice 1 — Bloquant ou non ?
Dans l’exemple, la boucle principale appelle
HAL_Delay(20)à chaque tour. Pendant ces 20 ms, que se passe-t-il si un caractère arrive sur le port série en mode interruption ? Et en mode scrutation (HAL_UART_Receivebloquant) ?
Corrigé
En interruption, le caractère est reçu et traité même pendant le
HAL_Delay: l’interruption suspend brièvement la boucle, exécute le callback, puis rend la main : leHAL_Delayne gêne pas la réception. En scrutation bloquante, il faudrait que le programme soit justement à l’instant sur l’appelHAL_UART_Receivepour attraper l’octet. Pendant leHAL_Delay, un caractère arrivé serait manqué. C’est tout l’intérêt de l’interruption : découpler l’événement de la boucle. Voir interruption.
Exercice 2 — HAL ou LL ?
Vous devez générer un signal carré le plus rapide possible en basculant une broche dans une boucle serrée. Vaut-il mieux
HAL_GPIO_TogglePinouLL_GPIO_TogglePin? Pourquoi ? Et quelle solution serait encore meilleure ?
Corrigé
LL_GPIO_TogglePinest plus rapide queHAL_GPIO_TogglePin: la fonction LL est une fine enveloppe (souvent inline) autour de l’écriture de registre, sans les vérifications et l’indirection par handle de la HAL. Pour un signal carré logiciel, LL bascule la broche en bien moins de cycles. Mais la meilleure solution n’est ni l’une ni l’autre : un timer matériel en PWM génère le signal sans le cœur, à fréquence stable, le rôle des timers. Règle générale : pour la performance, descendre en LL ou registre ; pour un signal périodique, confier le travail à un périphérique dédié.
Raccrochage projet
- Étape 4 de la phase de preuve de concept. La HAL est l’outil d’écriture de la logique de la PoC une fois le microcontrôleur configuré. Choisir le bon mode (scrutation / interruption / DMA) dès la PoC évite de devoir tout réécrire quand le blocage devient un problème.
- Lisibilité et reprise. Un code HAL se relit et se reprend plus facilement qu’un code registre, ce qui compte en projet d’équipe et dans le dossier technique.
Comprendre les trois modes (et que CubeMX configure les handles que la HAL utilise) donne la grammaire du STM32 natif : configurer d’un côté, piloter de l’autre, en choisissant à chaque fois le bon niveau de réactivité.
Aller plus loin
- Descendre au registre — la couche sous la HAL et la LL, et pourquoi y aller.
- Configurer avec CubeMX — d’où viennent les handles et l’activation NVIC.
- Interruptions · Timers — les concepts transverses derrière les modes
_ITet la PWM. - Documentation HAL/LL de ST (UM1725 et équivalents) — description des fonctions par famille.
Voir aussi
- STM32 — hub des tutoriels STM32
- Configurer avec CubeMX — l’étape qui précède : configurer les périphériques
- Descendre au registre — sous la HAL, l’accès direct au matériel
- Programmer avec l’Arduino-core — la porte de continuité, posée sur cette même HAL
- Firmware — structurer le code, du super-loop au RTOS (transverse)