Перейти к содержанию

HAL, LL и регистры CMSIS в STM32: сравнение и примеры

STM32Cube даёт два слоя драйверов поверх регистров: переносимый HAL с дескрипторами, тайм-аутами и обратными вызовами и тонкий LL из встраиваемых функций. Здесь — чем они отличаются по руководству ST UM1850 и исходникам, что делает HAL «за кадром», можно ли смешивать слои и как одна и та же задача выглядит в HAL, LL и на регистрах.

Обновлено Редакция GAW

Программу для STM32 можно писать на трёх уровнях. Ниже всех — регистры периферии с именами из заголовка CMSIS устройства: GPIOA->BSRR, USART1->BRR. Над ними ST даёт в пакетах STM32Cube два слоя драйверов: LL (low-layer) — тонкие функции, каждая из которых меняет определённые биты регистров, и HAL (hardware abstraction layer) — переносимые функции высокого уровня с дескрипторами периферии, тайм-аутами и обратными вызовами (UM1850, введение). Слои можно совмещать, и в реальных проектах так обычно и делают.

Коротко

  • HAL — переносимый между семействами STM32, есть для всей периферии, три модели работы: опрос, прерывания, DMA; хранит состояние в дескрипторе.
  • LL — встраиваемые функции в заголовках, без состояния, по модели программирования из справочного руководства; есть не для всех блоков (нет, например, для USB).
  • Регистры CMSIS — полный контроль и никакой прослойки, но код привязан к семейству и требует знания справочного руководства.
  • Совмещение: HAL и LL можно использовать в одной программе, но не для одного экземпляра периферии.
  • HAL_Init включает SysTick с периодом 1 мс и самым низким приоритетом и выбирает группу приоритетов NVIC_PRIORITYGROUP_4.

Чем отличаются слои

HALLLРегистры CMSIS
Где кодфункции в stm32xxxx_hal_ppp.cвстраиваемые функции в stm32xxxx_ll_ppp.h, инициализация — в .cтолько заголовок устройства
Переносимостьмежду семействами, с расширениями _ex для особенностейв пределах того, что одинаково в регистрахнет
Состояниедескриптор PPP_HandleTypeDef: состояние, ошибки, блокировка, указатели на буферынетнет
Модели работыопрос с тайм-аутом, прерывания, DMAчто напишете самичто напишете сами
ПрерыванияHAL_PPP_IRQHandler и обратные вызовымакросы чтения и сброса флаговфлаги регистров
Настройка выводов и тактовобратный вызов HAL_PPP_MspInitфункции LL_GPIO, LL_xxx_GRPx_EnableClockрегистры RCC и GPIO
Проверка параметровassert_param при USE_FULL_ASSERTassert_param, файл stm32_assert.hнет
Охват перифериивсябез USB и других блоков со сложным стекомвся

Таблица составлена по UM1850 (разделы 3 и 4) и исходникам драйверов STM32CubeF1 и STM32CubeF4. ST сама описывает эти слои как взаимодополняющие: HAL скрывает сложность микроконтроллера и переносится между семействами, LL даёт лучшую оптимизацию ценой переносимости и требует знания периферии.

Одна задача тремя способами

Переключить вывод PA5 STM32F407 в режим выхода и мигнуть им.

HAL — структура настройки и функции, которые сами разберутся с регистрами:

__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef init = {0};
init.Pin = GPIO_PIN_5;
init.Mode = GPIO_MODE_OUTPUT_PP;
init.Pull = GPIO_NOPULL;
init.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &init);

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);

LL — по одной функции на поле регистра:

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);

LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_5, LL_GPIO_OUTPUT_PUSHPULL);

LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_GPIO_TogglePin(GPIOA, LL_GPIO_PIN_5);

Регистры — те же действия напрямую:

RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER5) | GPIO_MODER_MODER5_0;   /* output */

GPIOA->BSRR = GPIO_BSRR_BS5;                                               /* PA5 = 1 */
GPIOA->BSRR = (GPIOA->ODR & GPIO_ODR_OD5) ? GPIO_BSRR_BR5 : GPIO_BSRR_BS5; /* toggle */

В итоге все три варианта пишут в одни и те же регистры. HAL_GPIO_WritePin и LL_GPIO_SetOutputPin записывают маску в BSRR, а переключение и в HAL, и в LL — это чтение ODR и одна запись в BSRR: сброс выводов, которые были в единице, и установка остальных. Разница — в том, что вокруг: HAL_GPIO_Init обходит все выводы маски в цикле и настраивает ещё и прерывания EXTI, если режим их требует, а LL_GPIO_SetPinMode — одна операция MODIFY_REG над MODER, где номер вывода вычисляется из маски макросом POSITION_VAL через команды RBIT и CLZ. Подробности о регистрах портов — в статье о GPIO STM32.

Что HAL делает за кадром

Знать устройство HAL полезно даже тем, кто его не любит: большая часть «странного» поведения программ на HAL объясняется несколькими механизмами.

  • HAL_Init выбирает группу приоритетов NVIC_PRIORITYGROUP_4 (16 уровней вытеснения без подприоритетов) и запускает SysTick с периодом 1 мс и приоритетом TICK_INT_PRIORITY, который в шаблоне конфигурации равен 0x0F — самому низкому. Поэтому HAL_Delay и функции с тайм-аутом не работают в обработчиках с более высоким приоритетом — подробнее в статье о прерываниях STM32.
  • HAL_Delay прибавляет к задержке один тик, чтобы гарантировать минимальное ожидание: HAL_Delay(1) длится от 1 до 2 мс.
  • SystemInit в system_stm32f1xx.c не настраивает тактирование — это делает пользовательский код через HAL (UM1850, таблица 3). Пока программа не вызвала настройку RCC, STM32F103 работает от HSI на 8 МГц.
  • Обратные вызовы MSP: HAL_PPP_Init сам вызывает HAL_PPP_MspInit, где пользователь включает тактирование, выводы, DMA и прерывания. Функции обратного вызова объявлены в HAL «слабыми» и переопределяются в программе (UM1850, таблица 8).
  • Обработчики прерываний: при работе через прерывания или DMA обработчик PPP_IRQHandler должен вызывать HAL_PPP_IRQHandler, а тот уже разбирает флаги и вызывает HAL_PPP_TxCpltCallback, HAL_PPP_ErrorCallback и другие.
  • Блокировка: макрос __HAL_LOCK проверяет и ставит поле Lock дескриптора обычными командами, без атомарных операций. Если та же функция HAL для того же дескриптора вызывается из прерывания, пока она выполняется в основном цикле, она вернёт HAL_BUSY.
  • Статус: функции HAL возвращают HAL_OK, HAL_ERROR, HAL_BUSY или HAL_TIMEOUT; тайм-аут задаётся в миллисекундах, HAL_MAX_DELAY = 0xFFFF FFFF — ждать бесконечно (UM1850, раздел 3.12.4).
  • Проверка параметров assert_param включается определением USE_FULL_ASSERT в stm32xxxx_hal_conf.h; в шаблоне оно закомментировано.

LL: особенности

Функции LL, работающие с регистрами, объявлены как __STATIC_INLINE в заголовках, и после оптимизации от них остаются те же команды, что и при прямом доступе. Функции инициализации LL_PPP_Init, LL_PPP_StructInit и LL_PPP_DeInit лежат в .c-файлах и компилируются, только если определён USE_FULL_LL_DRIVER (UM1850, раздел 4.2.1). Своего конфигурационного файла у LL нет; без HAL нужен файл stm32_assert.h из шаблона stm32_assert_template.h. Включение тактирования вынесено в stm32xxxx_ll_bus.h: LL_APB2_GRP1_EnableClock, LL_AHB1_GRP1_EnableClock и так далее.

LL не обёртка «для удобства», а точное отражение аппаратуры: функции вызывают в том порядке, который требует модель программирования справочного руководства, и LL не проверяет, что последовательность правильная. Зато имена функций и констант читаются лучше голых масок и подсказывают допустимые значения.

Как совмещать HAL и LL

Правила из UM1850 (раздел 5):

  • один и тот же экземпляр периферии не ведут одновременно через HAL и LL: LL может изменить регистры, состояние которых HAL хранит в дескрипторе. Если всё же приходится, поля дескриптора обновляют вручную;
  • функции LL, которые не меняют ничего из отражённого в дескрипторе, — например, чтение флагов, — можно вызывать и для периферии под HAL;
  • с модулями HAL без дескрипторов — RCC, GPIO, FLASH и общим HAL — LL совмещается без ограничений;
  • если инициализация сделана через LL, HAL_PPP_MspInit не вызывается; если передача сделана через LL, обратные вызовы HAL не придут, а обработчик прерывания надо писать самому.

Типичная схема: тактирование и выводы настраивает код CubeMX на HAL, сложные блоки вроде USB и файловой системы работают через HAL и промежуточное ПО, а быстрые и простые — GPIO, таймеры, отдельные USART с DMA — через LL или регистры.

Как выбрать

  • HAL — когда важны скорость разработки и переносимость, когда нужен USB, Ethernet, файловая система или другой стек ST, и когда частые вызовы и обработка прерываний не упираются во время.
  • LL — для узких мест: обработчиков с высокой частотой, циклов опроса, точной работы с таймерами и DMA, где лишний вызов функции и проверки заметны, но хочется читаемых имён.
  • Регистры — для минимального кода и полного понимания: загрузчики, демонстрации, обучение, места, где нужна одна запись в регистр. Поля всех регистров STM32F1 и STM32F4 с кодом на CMSIS разобраны на страницах регистров STM32F1 и STM32F4, а значение регистра можно собрать в конструкторе регистров.

Все примеры в статьях цикла — о GPIO, таймерах, UART, DMA, АЦП, прерываниях и Flash — написаны на регистрах CMSIS: так видно, что именно происходит, а перевести их на LL или HAL по именам битов несложно.

Частые вопросы

Чем LL отличается от HAL?

HAL — функции высокого уровня с дескриптором периферии, состоянием, блокировкой, тайм-аутами и обратными вызовами; они переносимы между семействами STM32 и есть для всех блоков. LL — встраиваемые функции, каждая из которых меняет конкретные биты регистров по модели программирования из справочного руководства; состояния они не хранят и есть не для всех блоков — например, для USB их нет (UM1850, разделы 3 и 4).

Можно ли использовать HAL и LL в одной программе?

Да. UM1850 разрешает смешивать слои, но не для одного экземпляра периферии: если USART1 работает через HAL, его регистры, отражённые в дескрипторе, менять через LL нельзя, а USART2 можно целиком вести через LL. Без ограничений LL совмещается с модулями HAL, у которых нет дескрипторов, — RCC, GPIO, FLASH и общим HAL (UM1850, раздел 5).

Почему HAL_Delay(1) ждёт 2 мс?

HAL_Delay прибавляет к заданной задержке один период тика, чтобы гарантировать минимальное ожидание: текущий тик мог почти закончиться к моменту вызова. При частоте тика 1 кГц HAL_Delay(n) ждёт от n до n + 1 мс (stm32f4xx_hal.c).

Что быстрее: HAL_GPIO_WritePin или запись в BSRR?

Обе пишут в регистр BSRR одной командой сохранения, но HAL_GPIO_WritePin — обычная функция из .c-файла: вызов, проверка параметров при USE_FULL_ASSERT, возврат. LL_GPIO_SetOutputPin объявлена как __STATIC_INLINE и после оптимизации превращается в ту же запись, что и прямой доступ к GPIOx->BSRR.

Источники

  1. STMicroelectronics. UM1850 Rev 3. Description of STM32F1 HAL and low-layer drivers, 2020 — введение — назначение HAL и LL; 3 — возможности HAL, файлы (таблицы 2 и 3), обратные вызовы (таблица 8), группы функций (таблица 9), HAL_StatusTypeDef, блокировка, тайм-ауты (таблица 15); 4 — LL: файлы (таблица 16), USE_FULL_LL_DRIVER; 5 — совместная работа HAL и LL
  2. STMicroelectronics. STM32CubeF1 и STM32CubeF4: драйверы HAL и LL, заголовки CMSIS — stm32f4xx_hal.c — HAL_Init, HAL_Delay с добавкой uwTickFreq, HAL_IncTick; stm32f4xx_hal_def.h — __HAL_LOCK; stm32f4xx_hal_conf_template.h — USE_FULL_ASSERT, TICK_INT_PRIORITY; stm32f4xx_ll_gpio.h, stm32f4xx_ll_bus.h — LL_GPIO_SetPinMode, LL_GPIO_TogglePin, LL_AHB1_GRP1_EnableClock; stm32f1xx.h — SET_BIT, MODIFY_REG, POSITION_VAL
  3. STMicroelectronics. RM0090 Rev 22. Reference manual: STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 — 8.4 — регистры GPIO для примера на регистрах

Нашли ошибку?