Embedded HandbookApps
All sections › I. Basics
STM32F103, STM32F407 · level intermediate · 25 min read
Open in the interactive handbook

5а. HAL in practice: three transfer modes, states, callbacks

Section 5 showed what HAL turns into after the compiler. This one is about how to use it. Every transfer function has three versions, every block has a handle with a state machine, and almost all mysterious HAL failures come down to one thing: the code violated this state machine, and HAL silently returned a status code that nobody read. We will work through UART — from the source stm32f4xx_hal_uart.c in the pack; I2C, SPI and the ADC are built the same way.

Terms used in the documentation

Term Meaning
handle a structure with the settings and state of a block
polling / blocking mode the function waits for the end of the transfer
interrupt mode (_IT) the transfer is driven by interrupts
DMA mode (_DMA) the transfer is driven by DMA
callback a function the library calls back on an event
half-transfer callback a callback at the middle of the buffer
reception to IDLE reception until a pause on the line
tick, time base the HAL millisecond counter and its source
register callbacks callbacks through pointers in the handle
legacy weak callback the older way: weak functions

5а.1 The handle: settings and state in one structure

Every block in HAL is represented by a handle — a structure such as UART_HandleTypeDef. It holds everything HAL knows about the block:

Field What it holds
Instance a pointer to the registers: USART1, USART2… — the very address from CMSIS
Init configuration parameters: baud rate, frame format, mode
pTxBuffPtr, TxXferCount the buffer and the counter of the ongoing transmission
pRxBuffPtr, RxXferCount the same for reception
hdmatx, hdmarx links to the DMA channel handles, if they are attached
Lock the __HAL_LOCK latch (section 5.2)
gState the state of the block as a whole and of transmission
RxState the state of reception
ErrorCode the accumulated error flags HAL_UART_ERROR_*

The configuration looks like this (this is the experiment code that will later run in QEMU):

UART_HandleTypeDef huart1;

huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);

Why the handle is global. Not out of habit and not because that is how CubeMX generates it. During a transfer HAL keeps a pointer to the buffer and the counters in the handle, and the interrupt handler — HAL_UART_IRQHandler(&huart1) — must see the same structure as main. A local variable would die on leaving the function, and the interrupt would keep writing to an address where it no longer exists. The handle must live as long as the block lives — globally is the simplest.

There are two states, and this matters. gState is responsible for the block as a whole and for transmission, RxState only for reception. Thanks to this, transmission and reception run simultaneously and do not interfere with each other. The values come from HAL_UART_StateTypeDef:

Value Code Meaning
HAL_UART_STATE_RESET 0x00 HAL_UART_Init has not been called yet
HAL_UART_STATE_READY 0x20 configured and free
HAL_UART_STATE_BUSY 0x24 an internal operation (Init in progress)
HAL_UART_STATE_BUSY_TX 0x21 transmission in progress (only gState)
HAL_UART_STATE_BUSY_RX 0x22 reception in progress (only RxState)
HAL_UART_STATE_TIMEOUT 0xA0 a timeout occurred
HAL_UART_STATE_ERROR 0xE0 an error

Code 0x23 (BUSY_TX_RX) is also in the header, but it is not a separate state — it is the sum of the TX and RX bits for reporting outward; neither gState nor RxState ever takes it.

⚠️ One block — one handle. Create two structures with Instance = USART1 and you get two independent "states" of the same hardware: each will consider the port free, and the HAL checks will stop protecting you. HAL does not catch this.


5а.2 One transmission in three forms

Every transfer operation in HAL has three versions. They differ not in the result — the bytes on the line go out the same way in all cases — but in what the processor is busy with and when the function returns.

Function Returns Processor busy End of transfer
HAL_UART_Transmit when everything has gone out for the whole transmission return from the function
HAL_UART_Transmit_IT at once an interrupt for every byte TC interrupt → TxCpltCallback
HAL_UART_Transmit_DMA at once only the start DMA done → TC interrupt → TxCpltCallback

The blocking version is a polling loop. For every byte it waits for the TXE flag, and it measures time with HAL_GetTick (5а.6):

if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
{
  return HAL_TIMEOUT;
}

The timeout here covers the whole message: tickstart is taken once before the loop, so Timeout is the limit for all bytes together, not for each one. After the last byte the function also waits for the TC flag — "shift register empty" — and only then returns HAL_OK: the line really is free.

The interrupt version only lays out the work and leaves:

/* Enable the UART Transmit data register empty Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_TXE);

return HAL_OK;

From then on each TXE flag calls the interrupt, and the handler moves the next byte from the buffer to DR and exits. On the last byte the handler switches from TXE to TC:

if (--huart->TxXferCount == 0U)
{
  /* Disable the UART Transmit Data Register Empty Interrupt */
  __HAL_UART_DISABLE_IT(huart, UART_IT_TXE);

  /* Enable the UART Transmit Complete Interrupt */
  __HAL_UART_ENABLE_IT(huart, UART_IT_TC);
}

and it is the TC interrupt that sets gState = READY and calls HAL_UART_TxCpltCallback.

The DMA version configures the channel (section 9), sets the DMAT bit — and that is all: the DMA controller moves the bytes into DR. When DMA reports "buffer empty", HAL does a characteristic thing:

/* Disable the DMA transfer for transmit request by setting the DMAT bit
   in the UART CR3 register */
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);

/* Enable the UART Transmit Complete Interrupt */
ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);

DMA finished before the last byte left the shift register — which is why the end of transmission is announced not by DMA but by the USART: the TxCpltCallback here too comes from the TC interrupt.

One transmission with three HAL functions: the blocking one holds the processor for the whole frame, the interrupt version for every byte, DMA only for the start and one TC interrupt at the end

Two rules follow from how _IT and _DMA work. First: the callback runs in interrupt context — set a flag in it and leave, do not parse the protocol there (section 7). Second: the buffer must outlive the transmission.

⚠️ A buffer on the stack + _IT or _DMA = garbage on the line.

c void bad_send(void) { char msg[32]; /* buffer on the stack */ int n = mk_report(msg); HAL_UART_Transmit_IT(&huart1, (uint8_t *)msg, (uint16_t)n); } /* msg is dead, and the transmission has just begun */

The function returned at once, the array ceased to exist, and the interrupts keep reading bytes from it for a long time. Whatever has taken its place goes out. Buffers for _IT and _DMA are static or global.


5а.3 HAL_BUSY: refusal without an error

Almost every transfer function starts the same way:

/* Check that a Tx process is not already ongoing */
if (huart->gState == HAL_UART_STATE_READY)

and ends like this:

else
{
  return HAL_BUSY;
}

No message, no ErrorCallback, not a single flag — only the status code, one of four in HAL_StatusTypeDef: HAL_OK = 0, HAL_ERROR = 1, HAL_BUSY = 2, HAL_TIMEOUT = 3. You called HAL_UART_Transmit_IT while the previous transmission had not reached TC — the function simply did nothing. The string is "lost" without a trace, and this is the most frequent complaint about HAL: "I send two messages in a row — only one goes out".

The second cause of the same code is the Lock latch (__HAL_LOCK, covered in 5.2): it closes a short start-up window while the function rewrites the handle fields. The state, in contrast, is busy for the whole operation — from the start to the end callback.

🔧 Recipe. The HAL status code is always checked — it is not decoration:

if (HAL_UART_Transmit_IT(&huart1, buf, len) != HAL_OK) {
    /* порт занят: в очередь, счётчик потерь — но не делать вид, что ушло */
}

If messages are born faster than they go out on the line, the honest solution is a ring buffer with sending from TxCpltCallback, not waiting in a loop at a busy port.


5а.4 Interrupt reception: one portion per start

HAL_UART_Receive_IT(&huart1, buf, N) does not mean "enable reception forever", but "receive exactly N bytes". The function remembers the buffer, sets RxState = BUSY_RX, enables the RXNE interrupt and returns. The handler moves each arriving byte into the buffer, and on the N-th it does this:

/* Disable the UART Data Register not empty Interrupt */
__HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);

/* Disable the UART Parity Error Interrupt */
__HAL_UART_DISABLE_IT(huart, UART_IT_PE);

/* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
__HAL_UART_DISABLE_IT(huart, UART_IT_ERR);

/* Rx process is completed, restore huart->RxState to Ready */
huart->RxState = HAL_UART_STATE_READY;

— it disables the receive interrupts completely and only then calls HAL_UART_RxCpltCallback. Two consequences follow.

First: since RxState is already READY, restarting reception right inside the callback is legal — and mandatory if the bytes arrive as a stream:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        push_byte(rx_byte);              /* в очередь; разбор — в основном цикле */
        HAL_UART_Receive_IT(huart, (uint8_t *)&rx_byte, 1);   /* RxState уже READY */
    }
}

Second: if there is no restart, the port goes silent. The hardware will receive the next byte into DR, but the interrupt is disabled — there will be no more callbacks. One more byte — and that is already an overrun: the ORE flag (section 16.4).

An experiment in QEMU (machine netduinoplus2, STM32F405; the real HAL from the pack was built — files and script in _verify/hal5a/). The program starts reception of one byte, immediately tries to start it a second time, then the host sends two bytes — A and B:

r1=0x00000000 rxstate=0x00000022 r2=0x00000002
rx_count=0x00000001 byte=0x00000041
sr=0x000000e0 rxneie=0x00000000 rx_count=0x00000001
stuck=0x00000042 r3=0x00000000

The first start gives HAL_OK, state 0x22 (BUSY_RX), the repeated one right away gives HAL_BUSY (2), silently. Byte 0x41 (A) arrived, the callback fired once — and that is it: in SR = 0xE0 RXNE is lit (bit 5), but RXNEIE is cleared, and the counter froze at one. Byte 0x42 (B) just sits in DR — the application did not get it until the program took it by hand. After that RxState is READY again, and a new start of reception returned 0.

Interrupt reception: while a portion is in progress — HAL_BUSY; after the last byte the receiver is off, and without a restart in RxCpltCallback the next byte stays in DR

About ORE and ErrorCallback. HAL divides reception errors into two groups. Noise and framing error are "non-blocking": HAL_UART_ErrorCallback is called, but reception continues. Overrun is "blocking":

dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR);
if (((huart->ErrorCode & HAL_UART_ERROR_ORE) != RESET) || dmarequest)

in this branch HAL first stops reception — clears the interrupts and sets RxState = READY — and only then calls ErrorCallback. Moreover, the byte that managed to arrive together with the error is taken from DR by the handler before your callback — the data is not lost, the running reception is. Therefore an empty ErrorCallback is a port that goes silent forever after the very first overrun. The minimal correct variant:

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        HAL_UART_Receive_IT(huart, (uint8_t *)&rx_byte, 1);   /* после ORE приём поднимают заново */
    }
}

5а.5 🔧 Reception of any length: ReceiveToIdle

Receiving one byte at a time through Receive_IT means an interrupt for every byte and a restart in every callback; at 115200 this is still tolerable, at megabit speeds it is not. In 16.5 the same task is solved with registers: a pause on the line sets the IDLE flag, DMA stores the bytes without the processor. HAL has a ready wrapper for this combination:

uint8_t rx_dma[256];

void rx_start(void)
{
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_dma, sizeof(rx_dma));
    __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);   /* вызов на половине буфера не нужен */
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1) {
        parse(rx_dma, Size);             /* Size — сколько байт лежит от начала буфера */
        rx_start();                      /* и снова: запуск — порция — запуск */
    }
}

HAL_UARTEx_ReceiveToIdle_DMA starts an ordinary DMA reception and additionally enables the IDLE interrupt. It has its own callback — HAL_UARTEx_RxEventCallback, and it arrives in three cases:

Event When Size
a pause on the line (IDLE) the packet ended before the buffer how much has been received so far
half of the buffer DMA passed the middle half of the buffer
buffer full DMA reached the end the whole buffer

The DMA channel must be configured and attached to hdmarx beforehand — CubeMX does this in MX_DMA_Init. There are relatives too: HAL_UARTEx_ReceiveToIdle (blocking, with a timeout) and HAL_UARTEx_ReceiveToIdle_IT.

⚠️ Size is a position, not an increment. The half-buffer callback will report Size = 128, next comes IDLE with Size = 150 — of these only 22 bytes are new. In Normal mode, as in the recipe above, it is simplest to restart reception after each packet and always parse the buffer from zero. The half-buffer call only gets in the way in this scheme — which is why it is disabled by the line __HAL_DMA_DISABLE_IT. In circular DMA mode you will have to keep your own "parsed up to here" pointer.


5а.6 HAL_GetTick: the clock of the whole library

Everything in HAL called Timeout is measured by one counter:

__weak uint32_t HAL_GetTick(void)
{
  return uwTick;
}

uwTick grows by one in an interrupt: at start-up HAL_Init configures SysTick for 1 ms, and the generated SysTick_Handler in stm32f4xx_it.c calls HAL_IncTick. The tick priority is set by TICK_INT_PRIORITY in stm32f4xx_hal_conf.h, and by default it is 0x0F — the lowest of sixteen (section 7).

Hence the classic trap. HAL_Delay is a wait loop on the same counter:

__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
    wait += (uint32_t)(uwTickFreq);
  }

  while((HAL_GetTick() - tickstart) < wait)
  {
  }
}

Call it — or any blocking HAL function with a timeout — from an interrupt handler with a priority higher than or equal to SysTick, and uwTick will stop growing: the tick interrupt cannot preempt yours. The loop will become endless. There are two options: do not call HAL from handlers (the right one) or raise the tick priority (tolerable: TICK_INT_PRIORITY in the configuration).

The listing also shows: wait += uwTickFreq — one tick is always added to the delay, otherwise HAL_Delay(1), called a microsecond before the tick interrupt, would return almost immediately. So HAL_Delay(1) means "from 1 to 2 ms", not "exactly 1 ms".

Time base on TIM6. SysTick is the rightful property of the OS: FreeRTOS takes it for itself (sections 23–24). So that HAL keeps its own clock, CubeMX with the OS enabled offers to move the time base to a timer, usually TIM6. The pack contains a ready sample stm32f4xx_hal_timebase_tim_template.c: it overrides the weak HAL_InitTick, runs TIM6 counting at 1 MHz with a period of 999 — an interrupt once a millisecond, — and HAL_IncTick moves into TIM6_DAC_IRQHandler. For the code there is no difference: HAL_GetTick returns milliseconds as before.


5а.7 🔧 Templates for every day

Four fragments that cover most everyday tasks. All of them were built by the compiler (file _verify/hal5a/templates5a.c); the details of each block are in its own section.

I2C: reading from an EEPROM with one function (section 17). Mem_Read itself performs the whole transaction: start, chip address, cell address, repeated start, reading with NAK at the end.

#define EEPROM_ADDR (0x50 << 1)          /* HAL ждёт 7-битный адрес уже сдвинутым */

uint8_t page[32];

void eeprom_read(void)
{
    if (HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR, 0x0000, I2C_MEMADD_SIZE_16BIT,
                         page, sizeof(page), 100) != HAL_OK)
        error();
}

The shift << 1 is not decoration: HAL puts the parameter on the line as is, only substituting the direction into the least significant bit. Pass an unshifted 0x50 — and a foreign address 0x28 appears on the line, and the device will not answer.

SPI: an exchange with a manual CS (section 18). SPI is always full duplex, so the main function is TransmitReceive; HAL does not control the CS pin:

uint8_t tx[4] = {0x9F, 0, 0, 0}, rx[4];
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi1, tx, rx, sizeof(tx), 100);
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);

ADC in a circle through DMA (section 14):

uint16_t adc_buf[16];

void adc_start(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 16);   /* длина — в преобразованиях */
}

The cast (uint32_t *) is deceptive: the data is not 32-bit, the function simply has one signature for all resolutions, and the transfer width is set by the DMA channel configuration. The length is in conversions, not in bytes.

PWM: the channel is silent until it is started (section 13):

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);

MX_TIM3_Init only configures: it is HAL_TIM_PWM_Start that enables the output and the counting. The duty cycle is changed on the fly with the macro __HAL_TIM_SET_COMPARE — a direct write to CCR1, without re-initialisation.

Each of these functions has _IT and _DMA siblings with their own callbacks: HAL_I2C_MemRxCpltCallback, HAL_SPI_TxRxCpltCallback, HAL_ADC_ConvCpltCallback, HAL_TIM_PeriodElapsedCallback — the scheme from 5а.2 repeats word for word in every block.


5а.8 Your own callbacks: weak functions and pointers

By default callbacks are weak functions (the mechanics and the main trap, a typo in the name, are covered in 5.7). In stm32f4xx_hal_uart.c there are eleven of them: MspInit, MspDeInit, a "complete"/"half" pair for transmission and reception, ErrorCallback, three variants of Abort completion and RxEventCallback. A weak function is one for all instances of the block — which is why inside it is customary to work out who has arrived by huart->Instance.

This way has a limit: the handler is chosen at link time, one and forever. The second mechanism — pointers in the handle — is enabled in stm32f4xx_hal_conf.h:

#define  USE_HAL_UART_REGISTER_CALLBACKS        1U /* UART register callback disabled      */

(the comment in the template describes the default value — 0). Then UART_HandleTypeDef gets the fields TxCpltCallback, RxCpltCallback and the rest, and the binding is done on the fly:

HAL_UART_Init(&huart1);              /* Init ставит во все указатели слабые заглушки */
HAL_UART_RegisterCallback(&huart1, HAL_UART_RX_COMPLETE_CB_ID, on_rx_done);

The order matters: registration is allowed in the READY state, that is after HAL_UART_Init (before it — only MspInit/MspDeInit, while gState equals RESET). The gain is different handlers for different ports and replacement on the fly; the price is an extra level of indirection and pointers that you must not forget to register. In single projects weak functions are enough; pointers are needed by libraries that cannot take the global name HAL_UART_RxCpltCallback away from the user.

StdPeriph in old code. Examples from before ≈2014 are written with SPL (section 5.1) — it is recognisable at once by the "function + flag" pair and structures with the block prefix:

USART_InitTypeDef u;
u.USART_BaudRate = 115200;
/* ... */
USART_Init(USART1, &u);
USART_Cmd(USART1, ENABLE);

USART_SendData(USART1, 'A');
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) { }

SPL has no handles, states or callbacks — these are thin wrappers over registers, closer in spirit to today's LL. Such code can be read literally: USART_SendData is a write to DR, USART_GetFlagStatus is a read of SR. It is worth porting it to a new project either onto registers (section 16) or onto HAL entirely; SPL and HAL must not be mixed in one project — they manage the same registers differently.


5а.9 Pitfalls

The status code is not checked — messages "get lost". A second call on a busy port silently returns HAL_BUSY. Check the return value; queue from TxCpltCallback (5а.3).

A buffer for _IT/_DMA on the stack. The function returned at once, the array died, and the interrupts send garbage. Buffers are static (5а.2).

Reception took one portion and went silent. Receive_IT is one portion per start: on the last byte RXNEIE is switched off. Restart it right in RxCpltCallback (5а.4, the QEMU experiment).

An empty ErrorCallback — the port dies after the first ORE. Overrun is a "blocking" error: HAL stops reception and waits for you to bring it back up (5а.4).

HAL_Delay in an interrupt handler. SysTick with priority 0x0F will not preempt your handler — the counter freezes, the loop becomes endless (5а.6).

Size in RxEventCallback is a position, not an increment. And do not forget about the half-buffer call if you do not need it (5а.5).


5а.10 Summary

Full sources

templates5a.c — the working fragments of the section in one file — transmission, reception, ReceiveToIdle, templates.

/* Раздел 5а: рабочие фрагменты из текста — файл собирается armclang целиком
 * с HAL из пака STM32F4xx_DFP 2.17.1 (проверка hal5a_check.py). Листинги
 * подразделов 5а.2, 5а.4, 5а.5 и 5а.7 — вырезки отсюда. */
#include "stm32f4xx_hal.h"

I2C_HandleTypeDef hi2c1;
SPI_HandleTypeDef hspi1;
ADC_HandleTypeDef hadc1;
TIM_HandleTypeDef htim3;
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;

#define CS_GPIO_Port GPIOA
#define CS_Pin GPIO_PIN_4

void error(void);
int mk_report(char *dst);
void push_byte(uint8_t b);
void parse(const uint8_t *data, uint16_t len);

/* ── 5а.2: буфер обязан пережить передачу ──────────────────────────────── */
void bad_send(void)
{
    char msg[32];                        /* буфер на стеке */
    int n = mk_report(msg);
    HAL_UART_Transmit_IT(&huart1, (uint8_t *)msg, (uint16_t)n);
}                                        /* msg умер, а передача только началась */

/* ── 5а.3: код возврата проверяется всегда ─────────────────────────────── */
void tx_try(uint8_t *buf, uint16_t len)
{
    if (HAL_UART_Transmit_IT(&huart1, buf, len) != HAL_OK) {
        /* порт занят: в очередь, счётчик потерь — но не делать вид, что ушло */
    }
}

/* ── 5а.4: перезапуск приёма в обратных вызовах ────────────────────────── */
volatile uint8_t rx_byte;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        push_byte(rx_byte);              /* в очередь; разбор — в основном цикле */
        HAL_UART_Receive_IT(huart, (uint8_t *)&rx_byte, 1);   /* RxState уже READY */
    }
}

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        HAL_UART_Receive_IT(huart, (uint8_t *)&rx_byte, 1);   /* после ORE приём поднимают заново */
    }
}

/* ── 5а.5: приём пакетов любой длины ───────────────────────────────────── */
uint8_t rx_dma[256];

void rx_start(void)
{
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_dma, sizeof(rx_dma));
    __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);   /* вызов на половине буфера не нужен */
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1) {
        parse(rx_dma, Size);             /* Size — сколько байт лежит от начала буфера */
        rx_start();                      /* и снова: запуск — порция — запуск */
    }
}

/* ── 5а.7: I2C — чтение из EEPROM одной функцией ───────────────────────── */
#define EEPROM_ADDR (0x50 << 1)          /* HAL ждёт 7-битный адрес уже сдвинутым */

uint8_t page[32];

void eeprom_read(void)
{
    if (HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR, 0x0000, I2C_MEMADD_SIZE_16BIT,
                         page, sizeof(page), 100) != HAL_OK)
        error();
}

/* ── 5а.7: SPI — обмен с ручным CS ─────────────────────────────────────── */
uint8_t jedec_id[3];

void flash_read_id(void)
{
    uint8_t tx[4] = {0x9F, 0, 0, 0}, rx[4];
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
    HAL_SPI_TransmitReceive(&hspi1, tx, rx, sizeof(tx), 100);
    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
    jedec_id[0] = rx[1];                 /* rx содержит и байты, пришедшие под команду */
    jedec_id[1] = rx[2];
    jedec_id[2] = rx[3];
}

/* ── 5а.7: АЦП по кругу через DMA ──────────────────────────────────────── */
uint16_t adc_buf[16];

void adc_start(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 16);   /* длина — в преобразованиях */
}

/* ── 5а.7: ШИМ — канал молчит, пока его не запустили ───────────────────── */
void pwm_start(void)
{
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);
}

templates5a_cb.c — callbacks through pointers in the handle (5а.8).

/* Раздел 5а.8: обратные вызовы через указатели в дескрипторе. Файл собирается
 * armclang с USE_HAL_UART_REGISTER_CALLBACKS = 1U в stm32f4xx_hal_conf.h —
 * проверка hal5a_check.py подкладывает правленый conf. */
#include "stm32f4xx_hal.h"

UART_HandleTypeDef huart1;

static void on_rx_done(UART_HandleTypeDef *huart)
{
    (void)huart;
}

void uart_setup(void)
{
    HAL_UART_Init(&huart1);              /* Init ставит во все указатели слабые заглушки */
    HAL_UART_RegisterCallback(&huart1, HAL_UART_RX_COMPLETE_CB_ID, on_rx_done);
}

uart5a.c — the 5а.3 and 5а.4 experiment on the real HAL in QEMU: HAL_BUSY and interrupt reception.

/* Раздел 5а: настоящий HAL (пак Keil STM32F4xx_DFP 2.17.1) в QEMU netduinoplus2 (STM32F405).
 * Опыт из 5а.3 и 5а.4:
 *  1) пока приём по прерыванию идёт, повторный HAL_UART_Receive_IT возвращает HAL_BUSY (2),
 *     а RxState равен HAL_UART_STATE_BUSY_RX (0x22);
 *  2) после RxCpltCallback приём выключен: второй байт остаётся в DR, RXNE горит,
 *     RXNEIE снят, счётчик принятых не растёт — байт до приложения не доходит.
 * Печать — прямой записью в USART1 (как в rmw5), приём — целиком через HAL. */
#include "stm32f4xx_hal.h"

/* Три определения из system_stm32f4xx.c — весь файл здесь не нужен */
uint32_t SystemCoreClock = 16000000; /* HSI после сброса */
const uint8_t AHBPrescTable[16] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 6, 7, 8, 9};
const uint8_t APBPrescTable[8]  = {0, 0, 0, 0, 1, 2, 3, 4};

UART_HandleTypeDef huart1;
volatile uint32_t rx_count;
volatile uint8_t rx_buf[4];
volatile uint8_t rx_last;

void SysTick_Handler(void)
{
    HAL_IncTick();
}

void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);
}

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    (void)huart;
    __HAL_RCC_USART1_CLK_ENABLE();      /* в QEMU область RCC — заглушка, но порядок вызовов настоящий */
    HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    (void)huart;
    rx_last = rx_buf[0];
    rx_count++;                          /* перезапуска приёма нарочно НЕТ — в этом и опыт */
}

static void uputs(const char *s)
{
    for (; *s; s++) {
        while (!(USART1->SR & USART_SR_TXE)) {
        }
        USART1->DR = (uint32_t)*s;
    }
}

static void uhex(const char *k, uint32_t v)
{
    static const char h[] = "0123456789abcdef";
    char b[12] = "=0x";
    uputs(k);
    for (int i = 0; i < 8; i++)
        b[3 + i] = h[(v >> (28 - 4 * i)) & 0xF];
    b[11] = 0;
    uputs(b);
    uputs(" ");
}

static void spin(volatile uint32_t n)
{
    while (n--) {
    }
}

int main(void)
{
    HAL_Init();

    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart1);

    /* 1. Первый запуск приёма одного байта — HAL_OK, состояние BUSY_RX */
    uint32_t r1 = HAL_UART_Receive_IT(&huart1, (uint8_t *)rx_buf, 1);
    uint32_t st = huart1.RxState;
    /* 2. Повторный запуск, пока приём идёт, — молча HAL_BUSY */
    uint32_t r2 = HAL_UART_Receive_IT(&huart1, (uint8_t *)rx_buf, 1);
    uhex("r1", r1);
    uhex("rxstate", st);
    uhex("r2", r2);
    uputs("\n");

    /* 3. Хост шлёт два байта: 'A' и 'B'. Первый принимается по прерыванию. */
    uint32_t guard = 0;
    while (rx_count == 0 && ++guard < 2000000000u) {
    }
    uhex("rx_count", rx_count);
    uhex("byte", rx_last);
    uputs("\n");

    /* 4. Приём никто не перезапустил. Второй байт уже пришёл в DR — но RXNEIE снят. */
    spin(2000000);
    uhex("sr", USART1->SR);              /* бит 5 (RXNE) горит: байт ждёт в DR */
    uhex("rxneie", (USART1->CR1 >> USART_CR1_RXNEIE_Pos) & 1u);
    uhex("rx_count", rx_count);          /* так и остался 1 — обратный вызов больше не приходил */
    uputs("\n");

    /* 5. Достаём застрявший байт напрямую и убеждаемся, что это 'B' */
    uint32_t stuck = USART1->DR;
    uhex("stuck", stuck);
    /* 6. Теперь приём свободен (RxState = READY) — запуск снова разрешён */
    uint32_t r3 = HAL_UART_Receive_IT(&huart1, (uint8_t *)rx_buf, 1);
    uhex("r3", r3);
    uputs("\ndone\n");
    for (;;) {
    }
}

Next: section 6 "Clocking (RCC)" — the clock tree, the PLL, manual setup for 72 MHz and why a wrong HSE_VALUE breaks all delays.

Open in the interactive handbook