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.
Чем отличаются слои
| HAL | LL | Регистры 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_ASSERT | assert_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.
Источники
- 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
- 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
- STMicroelectronics. RM0090 Rev 22. Reference manual: STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 — 8.4 — регистры GPIO для примера на регистрах
Нашли ошибку?
Смотрите также
- GPIO STM32Режимы выводов, регистры F1 и F4, BSRR, AF, скорость, токи, 5 В, код
- UART STM32BRR и OVER8, допуск по частоте, флаги, прерывания, DMA с IDLE, полудуплекс, RS-485
- Прерывания STM32Векторы и IRQn, приоритеты и HAL, EXTI и общие векторы, флаги, события, примеры
- Таймеры STM32Тактирование, PSC/ARR, ШИМ, мёртвое время, захват, энкодер, OPM, синхронизация
- Конструктор регистров AVRБиты регистра → hex, строка на C и ассемблере
- STM32F1Cortex-M3: F100, F101, F102, F103, F105/107 — линейки и распиновки
- STM32F4Cortex-M4 с FPU: от F401 и F411 до F429 и F469 — линейки и распиновки