---
title: Programming the STM32 with the HAL
lang: en
type: tuto
phases:
  - preuve-de-concept
  - dossier-technique
tags:
  - eee
  - tuto
  - stm32
prerequis:
  - stm32-en
  - stm32-cubemx-en
aa:
  - RA-PROJET-C03-3/PROJ/5
draft: false
source_fr: embarque/mcu/stm32/stm32-hal.md
source_sha256: 8ead91bad69e129ebb45153ee1d21e6bd01f3700b9d7061f343c722ded75027d
---

The **HAL** (*Hardware Abstraction Layer*) is **STMicroelectronics' native API** for driving the peripherals of an STM32: GPIO, UART, timers, ADC, buses… It is the API that the code generated by [[stm32-cubemx-en|CubeMX]] uses by default. It is **portable** (the same `HAL_UART_Transmit` call works from an F0 to an H7), at the price of a little verbosity. Learning the HAL means learning to *program* the microcontroller once it is *configured*: the natural sequel to CubeMX. For the language itself, see [[cpp-en|C++]]; to structure the whole, [[firmware-en|firmware]].

## What is it for?

The HAL answers a problem: driving an STM32 peripheral "by hand" means writing the right bits into the right registers, in the right order. That is tedious and hardly portable. The HAL **wraps** those sequences into readable functions common to the whole range:

- **Readability.** `HAL_UART_Transmit(&huart2, ...)` says what it does. The equivalent register sequence does not.
- **Portability.** Code written with the HAL carries over from one STM32 chip to another with few changes, an asset when reusing a project baseline.
- **Consistency with CubeMX.** The *handles* the HAL works on are precisely what CubeMX generates: the two tools are made to go together.

The HAL is not the only layer: for raw performance, you drop down to LL or to the [[stm32-registres-en|register]]. But it is the **right default** for most of a project.

## The handles

Every peripheral is represented by a **handle structure**, generated by CubeMX: `UART_HandleTypeDef huart2`, `TIM_HandleTypeDef htim3`, `ADC_HandleTypeDef hadc1`… The handle holds the whole configuration of the peripheral (pins, baud rate, mode…). HAL functions take the **address** of the handle as their first argument:

```c
HAL_UART_Transmit(&huart2, (uint8_t *)"Bonjour\r\n", 9, HAL_MAX_DELAY);
```

You do not create these handles yourself: you **configure them in CubeMX** and **use** them in the code. Picking the right handle (`&huart2`, not `&huart1`) is a frequent source of mistakes.

## The three modes of a peripheral

One and the same peripheral is driven in **three ways** with the HAL, from the simplest to the most efficient. Understanding that choice is the heart of the HAL.

![Three chronograms compared for one and the same transfer: in polling the core is busy for its whole duration; in interrupt mode it starts the transfer, stays free and is only called back at the end; in DMA the controller moves the data in its place while it stays free.|640](/ressources/img/stm32-hal/trois-modes.svg)

**Polling (blocking).** The function does the work and **returns once it is finished** (or after a timeout). Simple, but it **freezes the program** for the duration of the operation.

```c
HAL_UART_Transmit(&huart2, data, len, 100);   // blocks until the end, or 100 ms
```

**Interrupt.** The `_IT` function **starts** the operation and returns at once. When the operation is finished, the HAL calls a **callback** that you write. The core stays free in the meantime. It requires the peripheral's interrupt to be **enabled in the NVIC** (a tick box in CubeMX).

```c
HAL_UART_Receive_IT(&huart2, &rx, 1);          // arms the reception of one byte

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
  // called automatically when a byte is received
  HAL_UART_Receive_IT(&huart2, &rx, 1);        // re-arm for the next one
}
```

**DMA (*Direct Memory Access*).** The `_DMA` function hands the transfer to a **dedicated controller** that moves the data **without the core**. A *callback* signals the end. Ideal for large volumes (audio, display, blocks of measurements). It requires a DMA channel configured in CubeMX.

```c
HAL_UART_Transmit_DMA(&huart2, buffer, taille);   // the DMA takes care of it
```

> [!tip]
> **Which mode should you pick?** Polling for the simple and the occasional (a debug message). Interrupt to react to an event without blocking (incoming bytes, end of an ADC conversion). DMA for large, regular transfers. You often start in polling and move to interrupt or DMA when the blocking becomes a nuisance, exactly the logic of the [[interruption-en|interrupt]] and of [[firmware-en|structuring the firmware]].

## HAL or LL

Alongside the HAL, ST ships the **LL** layer (*Low-Layer*): very thin functions, often *inline*, which are almost register writes in disguise (`LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5)`). Compared with the HAL:

- **HAL** — portable, readable, a little overhead and memory; the default.
- **LL** — close to the register, fast, compact, **less portable**; for performance-critical paths or chips with very little memory.

You **mix** the two without trouble, **peripheral by peripheral**: HAL everywhere, LL where it counts. The choice is made in CubeMX (*Project Manager → Advanced Settings → Driver Selector*) and it is **exclusive for one and the same peripheral**: a given USART is generated in HAL *or* in LL, never in both at once. The layer below that again, direct register access, is the subject of [[stm32-registres-en|going down to the register]].

## Example — Button, LED and serial port with the HAL

On a Nucleo configured in CubeMX (LD2 as an output, B1 as an input on `PC13`, USART2 at 115200), this program lights the LED for as long as the button is held and announces every change on the serial port, all in HAL, in polling:

```c
/* USER CODE BEGIN 3 */
GPIO_PinState etat = HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);

if (etat == GPIO_PIN_RESET) {              // B1 pressed = low level on a 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);                             // simple debounce
/* USER CODE END 3 */
```

To move the **serial reception to interrupt mode** (echoing the characters received, without blocking the loop), you arm the reception once and write the *callback*, inside the USER CODE zones:

```c
/* USER CODE BEGIN PV */
uint8_t rx;
/* USER CODE END PV */

/* USER CODE BEGIN 2 */         // once, after the 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);   // sends the received byte back
  HAL_UART_Receive_IT(&huart2, &rx, 1);     // re-arms the reception
}
/* USER CODE END 4 */
```

The main loop stays free while the characters are received and echoed "in the background" by the interrupt. **This is the move from blocking to reactive**, the gesture that tells a firmware that waits from a firmware that reacts.

The program says nothing of its own: it only returns what it receives. Typing `hello` into the monitor's input box, the board returns:

```
hello
```

Character by character, in fact: five trips through the *callback*. And all the while the LED keeps following the button: that is the proof that the loop never waited.

## Pitfalls

**Forgetting `HAL_Init()` or `SystemClock_Config()`.** CubeMX places them at the top of `main`. In hand-written code, leaving them out leaves the HAL or the clock uninitialised and nothing works. Check that they are there before anything else.

**Blocking on an infinite timeout.** `HAL_UART_Transmit(..., HAL_MAX_DELAY)` waits indefinitely. If the link is silent, the whole program freezes. Prefer a finite timeout when blocking is risky, or move to interrupt/DMA.

**Interrupt not enabled in the NVIC.** Using `HAL_..._IT` without ticking the peripheral's line in CubeMX's NVIC tab: the *callback* is never called, without any build error. The number one trap of interrupt mode.

**Forgetting to re-arm the `_IT` reception.** One `HAL_UART_Receive_IT` triggers **only one** *callback*. Without re-arming it inside, you only ever receive the first byte.

**Wrong handle.** Passing `&huart1` instead of `&huart2` (or one timer for another): the peripheral you address is not the one you think.

**`HAL_Delay` inside an interrupt.** `HAL_Delay` relies on the SysTick interrupt. Calling it from a *callback* of higher or equal priority can **block for good**. Do not wait inside an interrupt: do the strict minimum there.

## Exercises

> [!question] Exercise 1 — Blocking or not?
> In the example, the main loop calls `HAL_Delay(20)` on every pass. During those 20 ms, what happens if a character arrives on the serial port in interrupt mode? And in polling mode (blocking `HAL_UART_Receive`)?

> [!success]- Solution
> **In interrupt mode**, the character is received and handled **even during the `HAL_Delay`**: the interrupt briefly suspends the loop, runs the *callback*, then returns: the `HAL_Delay` does not get in the way of the reception. **In blocking polling**, the program would have to be *right at that instant* on the `HAL_UART_Receive` call to catch the byte. During the `HAL_Delay`, a character that arrived would be **missed**. That is the whole point of the interrupt: decoupling the event from the loop. See [[interruption-en|interrupt]].

> [!question] Exercise 2 — HAL or LL?
> You have to generate a square wave as fast as possible by toggling a pin in a tight loop. Is `HAL_GPIO_TogglePin` or `LL_GPIO_TogglePin` the better choice? Why? And which solution would be better still?

> [!success]- Solution
> `LL_GPIO_TogglePin` is **faster** than `HAL_GPIO_TogglePin`: the LL function is a thin wrapper (often *inline*) around the register write, without the checks and the handle indirection of the HAL. For a software square wave, LL toggles the pin in far fewer cycles. But the **best** solution is neither: a **hardware timer in PWM** generates the signal *without the core*, at a stable frequency, the role of [[timer-en|timers]]. General rule: for performance, drop down to LL or the register; for a periodic signal, hand the work to a dedicated peripheral.

## Where it fits in the project

- **Step 4 of the [[preuve-de-concept-en|proof-of-concept phase]].** The HAL is the tool for writing the logic of the PoC once the microcontroller is configured. Picking the right mode (polling / interrupt / DMA) as early as the PoC saves rewriting everything when blocking becomes a problem.
- **Readability and hand-over.** HAL code is read back and picked up more easily than register code, which counts in team projects and in the [[dossier-technique-en|technical file]].

Understanding the three modes (and that CubeMX configures the handles the HAL works on) gives the grammar of the native STM32: configure on one side, drive on the other, choosing the right level of reactivity every time.

## Going further

- [[stm32-registres-en|Going down to the register]] — the layer beneath the HAL and the LL, and why you would go there.
- [[stm32-cubemx-en|Configuring with CubeMX]] — where the handles and the NVIC enabling come from.
- [[interruption-en|Interrupts]] · [[timer-en|Timers]] — the cross-cutting concepts behind the `_IT` modes and PWM.
- [ST's HAL/LL documentation (UM1725 and equivalents)](https://www.st.com/en/embedded-software/stm32cubef4.html) — description of the functions family by family.

## See also

- [[stm32-en|STM32]] — hub of the STM32 tutorials
- [[stm32-cubemx-en|Configuring with CubeMX]] — the step before: configuring the peripherals
- [[stm32-registres-en|Going down to the register]] — beneath the HAL, direct access to the hardware
- [[stm32-arduino-core-en|Programming with the Arduino core]] — the door of continuity, laid on this same HAL
- [[firmware-en|Firmware]] — structuring the code, from the super-loop to an RTOS (cross-cutting)
