5а. HAL в работе: три режима обмена, состояния, обратные вызовы
Раздел 5 показал, во что HAL превращается компилятором. Этот — про то, как
им пользоваться. У каждой функции обмена три версии, у каждого блока —
дескриптор с автоматом состояний, и почти все загадочные отказы HAL сводятся
к одному: код нарушил этот автомат, а HAL молча вернул код возврата, который
никто не прочитал. Разбираться будем на UART — по исходнику
stm32f4xx_hal_uart.c из пака; у I2C, SPI и АЦП устройство то же самое.
Как это называется в документации
Оригинал Перевод handle дескриптор: структура с настройками и состоянием блока polling / blocking mode блокирующий режим: функция ждёт конца обмена interrupt mode ( _IT)режим по прерыванию DMA mode ( _DMA)режим с DMA callback обратный вызов half-transfer callback обратный вызов на половине буфера reception to IDLE приём до паузы в линии tick, time base счётчик миллисекунд HAL и его источник register callbacks обратные вызовы через указатели в дескрипторе legacy weak callback прежний способ: слабые функции
5а.1 Дескриптор: настройки и состояние в одной структуре
Каждый блок в HAL представлен дескриптором — структурой вида
UART_HandleTypeDef. В ней живёт всё, что HAL знает о блоке:
| Поле | Что хранит |
|---|---|
Instance |
указатель на регистры: USART1, USART2… — тот самый адрес из CMSIS |
Init |
параметры настройки: скорость, формат кадра, режим |
pTxBuffPtr, TxXferCount |
буфер и счётчик идущей передачи |
pRxBuffPtr, RxXferCount |
то же для приёма |
hdmatx, hdmarx |
ссылки на дескрипторы каналов DMA, если они привязаны |
Lock |
защёлка __HAL_LOCK (раздел 5.2) |
gState |
состояние блока в целом и передачи |
RxState |
состояние приёма |
ErrorCode |
накопленные флаги ошибок HAL_UART_ERROR_* |
Настройка выглядит так (это код опыта, который дальше поедет в 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);
Почему дескриптор глобальный. Не по привычке и не потому, что так
генерирует CubeMX. Во время обмена HAL хранит в дескрипторе указатель на
буфер и счётчики, а обработчик прерывания — HAL_UART_IRQHandler(&huart1) —
должен видеть ту же структуру, что и main. Локальная переменная умерла бы
на выходе из функции, и прерывание продолжило бы писать по адресу, где её
уже нет. Дескриптор обязан жить, пока живёт блок, — проще всего глобально.
Состояний два, и это важно. gState отвечает за блок в целом и за
передачу, RxState — только за приём. Благодаря этому передача и приём
идут одновременно и не мешают друг другу. Значения — из
HAL_UART_StateTypeDef:
| Значение | Код | Смысл |
|---|---|---|
HAL_UART_STATE_RESET |
0x00 | HAL_UART_Init ещё не вызывалась |
HAL_UART_STATE_READY |
0x20 | настроен и свободен |
HAL_UART_STATE_BUSY |
0x24 | внутренняя операция (идёт Init) |
HAL_UART_STATE_BUSY_TX |
0x21 | идёт передача (только gState) |
HAL_UART_STATE_BUSY_RX |
0x22 | идёт приём (только RxState) |
HAL_UART_STATE_TIMEOUT |
0xA0 | вышел таймаут |
HAL_UART_STATE_ERROR |
0xE0 | ошибка |
Код 0x23 (BUSY_TX_RX) в заголовке тоже есть, но это не отдельное
состояние, а сумма битов TX и RX для отчёта наружу — ни gState,
ни RxState его не принимают.
⚠️ Один блок — один дескриптор. Заведёте две структуры с
Instance = USART1— получите два независимых «состояния» одного железа: каждая будет считать порт свободным, и проверки HAL перестанут защищать. HAL этого не ловит.
5а.2 Одна передача в трёх видах
У каждой операции обмена в HAL три версии. Отличаются они не результатом — байты в линии во всех случаях идут одинаково, — а тем, чем занят процессор и когда возвращается функция.
| Функция | Возвращается | Процессор занят | Конец обмена |
|---|---|---|---|
HAL_UART_Transmit |
когда всё ушло | всю передачу | возврат из функции |
HAL_UART_Transmit_IT |
сразу | прерывание на каждый байт | прерывание TC → TxCpltCallback |
HAL_UART_Transmit_DMA |
сразу | только запуск | DMA кончил → прерывание TC → TxCpltCallback |
Блокирующая версия — цикл опроса. На каждый байт она ждёт флага TXE,
и время меряет по HAL_GetTick (5а.6):
if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
{
return HAL_TIMEOUT;
}
Таймаут здесь общий на всю посылку: tickstart берётся один раз до цикла,
так что Timeout — это предел для всех байтов вместе, а не для каждого.
После последнего байта функция ждёт ещё и флага TC — «сдвиговый регистр
пуст» — и только тогда возвращает HAL_OK: линия действительно свободна.
Версия по прерыванию только раскладывает работу и уходит:
/* Enable the UART Transmit data register empty Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_TXE);
return HAL_OK;
Дальше каждый флаг TXE зовёт прерывание, обработчик перекладывает очередной
байт из буфера в DR и выходит. На последнем байте обработчик пересаживается
с TXE на 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);
}
и уже прерывание TC ставит gState = READY и зовёт HAL_UART_TxCpltCallback.
Версия с DMA настраивает канал (раздел 9), ставит бит DMAT — и всё:
байты в DR перекладывает контроллер DMA. Когда DMA сообщает «буфер
пуст», HAL делает характерную вещь:
/* 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 закончил раньше, чем последний байт ушёл из сдвигового регистра, — потому
конец передачи объявляет не DMA, а USART: обратный вызов TxCpltCallback
и здесь приходит из прерывания TC.
Из устройства _IT и _DMA следуют два правила. Первое: обратный вызов
исполняется в контексте прерывания — в нём флаг поставить и выйти, а не
разбирать протокол (раздел 7). Второе: буфер должен пережить передачу.
⚠️ Буфер на стеке +
_ITили_DMA= мусор в линии.
c void bad_send(void) { char msg[32]; /* буфер на стеке */ int n = mk_report(msg); HAL_UART_Transmit_IT(&huart1, (uint8_t *)msg, (uint16_t)n); } /* msg умер, а передача только началась */Функция вернулась сразу, массив перестал существовать, а прерывания ещё долго читают из него байты. Уйдёт то, что успело лечь на его место. Буферы для
_ITи_DMA— статические или глобальные.
5а.3 HAL_BUSY: отказ без ошибки
Почти каждая функция обмена начинается одинаково:
/* Check that a Tx process is not already ongoing */
if (huart->gState == HAL_UART_STATE_READY)
а кончается так:
else
{
return HAL_BUSY;
}
Никакого сообщения, никакого ErrorCallback, ни одного флага — только код
возврата, один из четырёх в HAL_StatusTypeDef: HAL_OK = 0,
HAL_ERROR = 1, HAL_BUSY = 2, HAL_TIMEOUT = 3. Вызвали
HAL_UART_Transmit_IT, пока прошлая передача не дошла до TC, — функция
просто ничего не сделала. Строка «теряется» без следа, и это самая частая
жалоба на HAL: «отправляю два сообщения подряд — уходит одно».
Вторая причина того же кода — защёлка Lock (__HAL_LOCK, разобрана
в 5.2): она закрывает короткое окно запуска, пока функция переписывает поля
дескриптора. Состояние же занято всю операцию — от запуска до обратного
вызова конца.
🔧 Рецепт. Код возврата HAL проверяется всегда — это не украшение:
if (HAL_UART_Transmit_IT(&huart1, buf, len) != HAL_OK) {
/* порт занят: в очередь, счётчик потерь — но не делать вид, что ушло */
}
Если сообщения рождаются быстрее, чем уходят в линию, честное решение —
кольцевой буфер с отправкой из TxCpltCallback, а не ожидание в цикле
у занятого порта.
5а.4 Приём по прерыванию: одна порция на запуск
HAL_UART_Receive_IT(&huart1, buf, N) — это не «включить приём навсегда»,
а «принять ровно N байт». Функция запоминает буфер, ставит
RxState = BUSY_RX, включает прерывание RXNE и возвращается. Каждый
пришедший байт обработчик перекладывает в буфер, а на N-м делает вот что:
/* 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;
— выключает приёмные прерывания совсем и только потом зовёт
HAL_UART_RxCpltCallback. Отсюда два следствия.
Первое: раз RxState уже READY, перезапуск приёма прямо внутри
обратного вызова законен — и обязателен, если байты идут потоком:
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 */
}
}
Второе: если перезапуска нет, порт глохнет молча. Следующий байт железо
примет в DR, но прерывание выключено — обратных вызовов больше не будет.
Ещё один байт — и это уже переполнение: флаг ORE (раздел 16.4).
Опыт в QEMU (машина netduinoplus2, STM32F405; собран настоящий HAL из
пака — файлы и скрипт в _verify/hal5a/). Программа запускает приём одного
байта, тут же пробует запустить второй раз, потом хост шлёт два байта —
A и B:
r1=0x00000000 rxstate=0x00000022 r2=0x00000002
rx_count=0x00000001 byte=0x00000041
sr=0x000000e0 rxneie=0x00000000 rx_count=0x00000001
stuck=0x00000042 r3=0x00000000
Первый запуск — HAL_OK, состояние 0x22 (BUSY_RX), повторный тут же —
HAL_BUSY (2), молча. Пришёл байт 0x41 (A), обратный вызов сработал один
раз — и всё: в SR = 0xE0 горит RXNE (бит 5), но RXNEIE снят, счётчик
замер на единице. Байт 0x42 (B) так и лежит в DR — приложение его не
получило, пока программа не забрала его руками. После этого RxState
снова READY, и новый запуск приёма вернул 0.
Про ORE и ErrorCallback. Ошибки приёма HAL делит на две группы.
Помеха, ошибка кадра — «неблокирующие»: HAL_UART_ErrorCallback вызывается,
но приём продолжается. Переполнение — «блокирующее»:
dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR);
if (((huart->ErrorCode & HAL_UART_ERROR_ORE) != RESET) || dmarequest)
в этой ветке HAL сначала останавливает приём — снимает прерывания и ставит
RxState = READY — и лишь потом зовёт ErrorCallback. Причём байт, который
успел прийти вместе с ошибкой, обработчик забирает из DR до вашего
обратного вызова — данные не пропадают, пропадает запущенный приём. Поэтому
пустой ErrorCallback — это порт, который после первого же переполнения
замолкает навсегда. Минимальный правильный вариант:
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
HAL_UART_Receive_IT(huart, (uint8_t *)&rx_byte, 1); /* после ORE приём поднимают заново */
}
}
5а.5 🔧 Приём любой длины: ReceiveToIdle
Принимать по одному байту через Receive_IT — это прерывание на каждый
байт и перезапуск в каждом обратном вызове; на 115200 это ещё терпимо, на
мегабитных скоростях — уже нет. В 16.5 та же задача решена регистрами:
пауза в линии взводит флаг IDLE, DMA складывает байты без процессора. У HAL
для этой связки есть готовая обёртка:
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 запускает обычный приём в DMA и дополнительно
включает прерывание IDLE. Обратный вызов у него свой —
HAL_UARTEx_RxEventCallback, и приходит он в трёх случаях:
| Событие | Когда | Size |
|---|---|---|
| пауза в линии (IDLE) | пакет кончился раньше буфера | сколько уже принято |
| половина буфера | DMA прошёл середину | половина буфера |
| буфер полон | DMA дошёл до конца | весь буфер |
Канал DMA должен быть настроен и привязан к hdmarx заранее — CubeMX
делает это в MX_DMA_Init. Есть и родственники: HAL_UARTEx_ReceiveToIdle
(блокирующий, с таймаутом) и HAL_UARTEx_ReceiveToIdle_IT.
⚠️
Size— это позиция, а не приращение. Обратный вызов на половине буфера сообщитSize= 128, следом придёт IDLE сSize= 150 — из них новых байт только 22. В режиме Normal, как в рецепте выше, проще всего перезапускать приём после каждого пакета и всегда разбирать буфер с нуля. Вызов на половине буфера в этой схеме только мешает — потому он и выключен строкой__HAL_DMA_DISABLE_IT. В кольцевом режиме DMA придётся вести свой указатель «докуда разобрано».
5а.6 HAL_GetTick: часы всей библиотеки
Всё, что в HAL называется Timeout, меряется одним счётчиком:
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}
uwTick растёт на единицу в прерывании: HAL_Init при старте настраивает
SysTick на 1 мс, а сгенерированный SysTick_Handler из stm32f4xx_it.c
зовёт HAL_IncTick. Приоритет тика задаёт TICK_INT_PRIORITY в
stm32f4xx_hal_conf.h, и по умолчанию это 0x0F — самый низкий из
шестнадцати (раздел 7).
Отсюда классическая ловушка. HAL_Delay — это цикл ожидания по тому же
счётчику:
__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)
{
}
}
Позовите его — или любую блокирующую функцию HAL с таймаутом — из
обработчика прерывания с приоритетом выше или равным SysTick, и uwTick
перестанет расти: прерывание тика не может вытеснить ваше. Цикл станет
вечным. Варианта два: не звать HAL из обработчиков (правильный) или поднять
приоритет тика (терпимый: TICK_INT_PRIORITY в конфигурации).
Заодно из листинга видно: wait += uwTickFreq — к задержке всегда
прибавляется один тик, иначе HAL_Delay(1), вызванный за микросекунду до
прерывания тика, вернулся бы почти сразу. Так что HAL_Delay(1) — это
«от 1 до 2 мс», а не «ровно 1 мс».
Тайм-база на TIM6. SysTick — законная собственность ОС: FreeRTOS
забирает его себе (разделы 23–24). Чтобы у HAL остались свои часы, CubeMX
при включённой ОС предлагает перенести тайм-базу на таймер, обычно TIM6.
В паке лежит готовый образец stm32f4xx_hal_timebase_tim_template.c: он
переопределяет слабую HAL_InitTick, разгоняет TIM6 на счёт 1 МГц
с периодом 999 — прерывание раз в миллисекунду, — и HAL_IncTick
переезжает в TIM6_DAC_IRQHandler. Для кода разницы нет: HAL_GetTick
как возвращал миллисекунды, так и возвращает.
5а.7 🔧 Шаблоны на каждый день
Четыре фрагмента, закрывающие большинство бытовых задач. Все они собраны
компилятором (файл _verify/hal5a/templates5a.c); подробности каждого
блока — в его разделе.
I2C: чтение из EEPROM одной функцией (раздел 17). Mem_Read сам
делает всю транзакцию: старт, адрес микросхемы, адрес ячейки, повторный
старт, чтение с NAK в конце.
#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();
}
Сдвиг << 1 — не украшение: HAL кладёт параметр в линию как есть, только
подставляя в младший бит направление. Передадите несдвинутый 0x50 —
на линии окажется чужой адрес 0x28, и устройство не ответит.
SPI: обмен с ручным CS (раздел 18). SPI всегда дуплекс, поэтому
основная функция — TransmitReceive; выводом CS HAL не управляет:
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);
АЦП по кругу через DMA (раздел 14):
uint16_t adc_buf[16];
void adc_start(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 16); /* длина — в преобразованиях */
}
Приведение (uint32_t *) обманчиво: данные не 32-битные, просто сигнатура
у функции одна на все разрядности, а ширину переноса задаёт настройка
канала DMA. Длина — в преобразованиях, не в байтах.
ШИМ: канал молчит, пока его не запустили (раздел 13):
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);
MX_TIM3_Init только настраивает: включает выход и счёт именно
HAL_TIM_PWM_Start. Скважность на ходу меняют макросом
__HAL_TIM_SET_COMPARE — это прямая запись в CCR1, без переинициализации.
У каждой из этих функций есть братья _IT и _DMA со своими обратными
вызовами: HAL_I2C_MemRxCpltCallback, HAL_SPI_TxRxCpltCallback,
HAL_ADC_ConvCpltCallback, HAL_TIM_PeriodElapsedCallback — схема
из 5а.2 повторяется в каждом блоке дословно.
5а.8 Свои обратные вызовы: слабые функции и указатели
По умолчанию обратные вызовы — слабые функции (механика и главная ловушка,
опечатка в имени, разобраны в 5.7). В stm32f4xx_hal_uart.c их одиннадцать:
MspInit, MspDeInit, по паре «готово»/«половина» на передачу и приём,
ErrorCallback, три варианта завершения Abort и RxEventCallback.
Слабая функция одна на все экземпляры блока — потому внутри принято
разбирать, кто пришёл, по huart->Instance.
У этого способа есть предел: обработчик выбирается на этапе компоновки,
один и навсегда. Второй механизм — указатели в дескрипторе — включается
в stm32f4xx_hal_conf.h:
#define USE_HAL_UART_REGISTER_CALLBACKS 1U /* UART register callback disabled */
(комментарий в шаблоне описывает значение по умолчанию — 0). Тогда в
UART_HandleTypeDef появляются поля TxCpltCallback, RxCpltCallback
и остальные, а привязка делается на ходу:
HAL_UART_Init(&huart1); /* Init ставит во все указатели слабые заглушки */
HAL_UART_RegisterCallback(&huart1, HAL_UART_RX_COMPLETE_CB_ID, on_rx_done);
Порядок важен: регистрация разрешена в состоянии READY, то есть после
HAL_UART_Init (до неё — только MspInit/MspDeInit, пока gState равен
RESET). Выгода — разные обработчики на разные порты и замена на лету;
цена — лишний уровень косвенности и указатели, которые надо не забыть
зарегистрировать. В одиночных проектах слабых функций хватает; указатели
нужны библиотекам, которые не могут занять глобальное имя
HAL_UART_RxCpltCallback у пользователя.
StdPeriph в старом коде. Примеры до ≈2014 года написаны на SPL (раздел 5.1) — её видно сразу по паре «функция + флаг» и структурам с приставкой блока:
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 нет — это тонкие обёртки
над регистрами, по духу ближе к сегодняшнему LL. Читать такой код можно
буквально: USART_SendData — запись в DR, USART_GetFlagStatus — чтение
SR. Переносить его в новый проект стоит либо на регистры (раздел 16),
либо на HAL целиком; смешивать SPL и HAL в одном проекте нельзя — они
по-разному распоряжаются одними и теми же регистрами.
5а.9 Грабли
Код возврата не проверен — сообщения «теряются». Второй вызов по
занятому порту молча возвращает HAL_BUSY. Проверять возврат, очередь —
из TxCpltCallback (5а.3).
Буфер для _IT/_DMA на стеке. Функция вернулась сразу, массив умер,
прерывания досылают мусор. Буферы — статические (5а.2).
Приём принял порцию и замолк. Receive_IT — одна порция на запуск:
на последнем байте RXNEIE выключается. Перезапуск — прямо в
RxCpltCallback (5а.4, опыт в QEMU).
Пустой ErrorCallback — порт умирает после первого ORE. Переполнение —
«блокирующая» ошибка: HAL останавливает приём и ждёт, что вы его поднимете
(5а.4).
HAL_Delay в обработчике прерывания. SysTick с приоритетом 0x0F не
вытеснит ваш обработчик — счётчик замрёт, цикл станет вечным (5а.6).
Size в RxEventCallback — позиция, а не приращение. И не забудьте
про вызов на половине буфера, если он вам не нужен (5а.5).
5а.10 Итоги
- Дескриптор — настройки плюс состояние; он глобальный, потому что его
делят
mainи обработчик прерывания. Один блок — ровно один дескриптор. - Состояний два:
gState(блок и передача) иRxState(приём) — передача и приём идут одновременно.READY= 0x20,BUSY_TX= 0x21,BUSY_RX= 0x22. - Три версии каждой операции: блокирующая (процессор занят весь обмен,
таймаут общий на посылку),
_IT(прерывание на каждый байт),_DMA(процессор свободен). Конец передачи в_ITи_DMAобъявляет прерывание TC — оттуда и приходитTxCpltCallback. - Занятый порт отвечает
HAL_BUSY(2) — молча. Код возврата проверяется всегда. Receive_ITпринимает одну порцию и выключает приём; перезапуск — внутриRxCpltCallback(RxStateк этому моменту ужеREADY). Проверено в QEMU на настоящем HAL из пака.- ORE — «блокирующая» ошибка: HAL останавливает приём до
ErrorCallback; пустойErrorCallback= мёртвый порт. - Пакеты переменной длины —
HAL_UARTEx_ReceiveToIdle_DMA+RxEventCallback;Sizeтам — позиция в буфере, и вызов приходит ещё на половине буфера и при его заполнении. HAL_GetTick— часы всех таймаутов: SysTick, 1 мс, приоритет 0x0F — самый низкий;HAL_Delay(1)ждёт 1–2 мс; под FreeRTOS тайм-базу HAL переносят на TIM6 (1 МГц, период 999).- Обратные вызовы: слабые функции — просто, но одна на всех и навсегда;
USE_HAL_*_REGISTER_CALLBACKS= 1 даёт указатели в дескрипторе, регистрировать послеInit. - SPL в старом коде — «функция + флаг», без состояний; с HAL в одном проекте не смешивать.
Полные исходники
templates5a.c — рабочие фрагменты раздела одним файлом — передача, приём, ReceiveToIdle, шаблоны.
/* Раздел 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 — обратные вызовы через указатели в дескрипторе (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 — опыт 5а.3 и 5а.4 на настоящем HAL в QEMU: HAL_BUSY и приём по прерыванию.
/* Раздел 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 (;;) {
}
}
Дальше: раздел 6 «Тактирование (RCC)» — дерево тактов, PLL, настройка на
72 МГц вручную и почему неверный HSE_VALUE ломает все задержки.