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 = USART1and 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.
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 +
_ITor_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
_ITand_DMAare 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.
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.
⚠️
Sizeis a position, not an increment. The half-buffer callback will reportSize= 128, next comes IDLE withSize= 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
- A handle is settings plus state; it is global because it is
shared by
mainand the interrupt handler. One block — exactly one handle. - There are two states:
gState(block and transmission) andRxState(reception) — transmission and reception run simultaneously.READY= 0x20,BUSY_TX= 0x21,BUSY_RX= 0x22. - Three versions of every operation: blocking (the processor is busy for the whole transfer,
the timeout covers the whole message),
_IT(an interrupt for every byte),_DMA(the processor is free). The end of transmission in_ITand_DMAis announced by the TC interrupt — that is whereTxCpltCallbackcomes from. - A busy port answers
HAL_BUSY(2) — silently. The status code is always checked. Receive_ITreceives one portion and switches reception off; the restart goes insideRxCpltCallback(RxStateis alreadyREADYby then). Verified in QEMU on the real HAL from the pack.- ORE is a "blocking" error: HAL stops reception before
ErrorCallback; an emptyErrorCallback= a dead port. - Variable-length packets —
HAL_UARTEx_ReceiveToIdle_DMA+RxEventCallback;Sizethere is a position in the buffer, and the call also comes at half of the buffer and when it is full. HAL_GetTickis the clock of all timeouts: SysTick, 1 ms, priority 0x0F — the lowest;HAL_Delay(1)waits 1–2 ms; under FreeRTOS the HAL time base is moved to TIM6 (1 MHz, period 999).- Callbacks: weak functions are simple, but one for all and forever;
USE_HAL_*_REGISTER_CALLBACKS= 1 gives pointers in the handle, to be registered afterInit. - SPL in old code is "function + flag", without states; do not mix it with HAL in one project.
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.