HAL stm32
HAL (Hardware Abstraction Layer) — это библиотека для создания приложений на stm32, разработанная компанией
Заранее скажу, что эта статья никакой не мануал, это просто попытка описать внутреннее устройство HAL, ну или можно сказать, что это метод изучения сабжа.
Итак, HAL позволяет абстрагироваться от работы с регистрами и прочей сложной магии. Грубо говоря, HAL это обёртка над низкоуровневыми операциями. Конечно же это не отменяет необходимости понимания устройства микроконтроллеров, но значительно снижает уровень вхождения.
Например, чтоб запустить таймер, достаточно перед бесконечным циклом прописать вот такую функцию…
HAL_TIM_Base_Start_IT(&htim1);
То есть нам не нужно знать какие регистры отвечают за это, и что в них записывать. Более того, эта функция будет работать на любых микроконтроллерах серии stm32.
Сама функция выглядит так:
Вначале происходит проверка параметров на ошибки (assert_param), и после этого активируется прерывание и запускается таймер.
Строчки начинающиеся с
Однако я немного забежал вперёд. Прежде чем изучать HAL, нужно познакомиться с программой CubeMX (в просторечии «Куб») так как HAL является неотъемлемой частью «Куба», и именно в нём генерится весь начальный код будущего приложения включая описанные выше функции. Подробно про CubeMX читайте здесь...
Познакомились — тогда продолжим…
Воспользуемся примером, который сделан по ссылке выше, и рассмотрим подробно всю программу.
Итак мы сгенерировали проект, в котором есть таймер вызывающий прерывание при переполнении, и GPIO. Открываем этот проект в среде разработки (у меня TrueStudio) и в левой панели клацаем файл main.c…
Куб создал все необходимые функции инициализации…
… и избавил нас от возни с настройками, и от возможных ошибок.
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
// ШЕФ ВСЁ ПРОПАЛО
/* USER CODE END Error_Handler_Debug */
}
Ниже есть ещё одна функция проверок на ошибки —
Функция работает при условии, что задефайнен
посмотреть
В конце файла обрисован механизм передачи assert_param() в void assert_failed()…
/* ########################## Assert Selection ############################## */
/**
* @brief Uncomment the line below to expanse the "assert_param" macro in the
* HAL drivers code
*/
/* #define USE_FULL_ASSERT 1U */
В конце файла обрисован механизм передачи assert_param() в void assert_failed()…
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function
* which reports the name of the source file and the source
* line number of the call that failed.
* If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
Если хотите чтоб он раскомментировался, то надо в Кубе сделать так…
Enable Full Assert. Эти ассерты занимают определённое количество памяти, поэтому их лучше использовать только для отладки, а в релизе отключать.
В общем с проверками на ошибки у HAL'а всё очень удобно и информативно.
Теперь давайте рассмотрим процесс инициализации на примере таймера.
В функции
Теперь клацните функцию
Здесь происходит следующее:
Проверяется не пустой ли указатель структуры (htim == NULL) и заполнены ли все элементы структуры (assert_param).
Проверяется статус таймера (htim->State == HAL_TIM_STATE_RESET). В данном случае статус
посмотреть
Заголовочный файлstm32f1xx_hal_tim.h .
Заголовочный файл
Если статус удовлетворяет, то снимается блокировка (htim->Lock = HAL_UNLOCKED) и вызывается функция
посмотреть
Здесь проверяется какой именно таймер настраивается (htim_base->Instance==TIM1) и вызываются функции которые включают тактирование таймера, активирует прерывание и настраивают приоритет.
Здесь проверяется какой именно таймер настраивается (htim_base->Instance==TIM1) и вызываются функции которые включают тактирование таймера, активирует прерывание и настраивают приоритет.
Далее устанавливается статус «занято» (htim->State= HAL_TIM_STATE_BUSY) — если по каким-то причинам, параллельно будет вызвана ещё одна функция инициализации таймера, то она не сможет ничего испортить.
После этого вызывается функция
посмотреть
Файлstm32f1xx_hal_tim.c
Файл
Ну и наконец устанавливается статус «готов к труду и обороне» (htim->State= HAL_TIM_STATE_READY) и возвращается —
Функции связанные с таймером находятся либо в том же файле (stm32f1xx_hal_tim.c), либо в
Все функции имеют характерные названия определяющие их назначение…
Окончание
Например запуск таймера без прерываний выглядит так:
HAL_TIM_Base_Start(&htim1);
Как вы уже наверно поняли, одна из особенностей библиотеки HAL это большое количество проверок защищающих разработчиков от массы неприятностей. Кто-то расценит это как добродетель, а кто-то решит что такое количество проверок избыточно. Однако не стоит занимать какую-либо позицию на этот счёт так как в процессе изучения вы поймёте где нужно оставлять эти проверки, а где их можно удалить. То же самое касается и всего остального, со временем вы будете отказываться от каких-то функций HAL и заменять их работой с регистрами.
При работе с любой другой периферией, все необходимые функции вы найдёте в соответствующих файлах…
Названия файлов говорят сами за себя.
Функция запуска таймера…
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
/* USER CODE END 2 */
… сама по себе не особо интересна.
посмотреть
Функция устанавливает бит разрешающий прерывания по переполнению — __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE) и бит активации таймера — __HAL_TIM_ENABLE(htim).
Функция устанавливает бит разрешающий прерывания по переполнению — __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE) и бит активации таймера — __HAL_TIM_ENABLE(htim).
А вот механизм вызова прерывания поможет понять устройство библиотеки HAL. Разберём его…
Когда мы в Кубе активируем прерывание от какой-либо периферии, то в файле
Сюда программа переходит как только сработает прерывание от любого из событий таймера №1.
Этот обработчик (условно назовём его низкоуровневым) вызывает HAL-обработчик
события
Программа войдя в функцию
Нас интересует блок TIM Update event…
про макросы
Библиотека HAL под завязку напичкана различными макросами. Как уже говорилось в начале статьи, они начинаются с __двойного подчёркивания и имеют характерные имена определяющие их назначение. Эти макросы очёнь клёвая штука, они позволяют оперировать различными битами в различных регистрах без необходимости копаться в даташитах.
_GET_ — читать биты, _SET_ — устанавливать биты, _CLEAR_ — очищать биты, и т.д. Посмотреть макросы можно в хедерах соответствующей периферии, например, всё что связано с таймерами находится в файле stm32f1xx_hal_tim.h .
Внутри макроса__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE) содержится вот такая конструкция…
Этот макрос сбрасывает бит (указанный вторым аргументом) в регистре состояния (Status Register).
В первый аргумент подставляется указатель на структуру таймера, а вторым аргументом идёт дефаин флага который взводится при возникновении прерывания…
Написав программу на HAL вы можете проследить где-какие макросы/функции вызываются, и работать с регистрами напрямую. То есть HAL можно с лёгкостью использовать как пособие для изучения низкоуровневых операций.
Внутри макроса
#define __HAL_TIM_CLEAR_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->SR = ~(__INTERRUPT__))
Этот макрос сбрасывает бит (указанный вторым аргументом) в регистре состояния (Status Register).
В первый аргумент подставляется указатель на структуру таймера, а вторым аргументом идёт дефаин флага который взводится при возникновении прерывания…
#define TIM_IT_UPDATE (TIM_DIER_UIE)
Написав программу на HAL вы можете проследить где-какие макросы/функции вызываются, и работать с регистрами напрямую. То есть HAL можно с лёгкостью использовать как пособие для изучения низкоуровневых операций.
Если установлен флаг переполнения (TIM_FLAG_UPDATE) и источником является прерывание по переполнению (TIM_IT_UPDATE), тогда флаг сбрасывается и вызывается колбек —
заметка
В принципе нам ничто не мешает мигать лампочкой прямо в обработчике, да ещё и оперировать регистрами напрямую (немного хардкора)…
В этом примере делается то же самое, что делает HAL — сбрасывается флаг прерывания и вместо вызова колбека сразу же выполняется действие (мигание светиком).
Таким образом количество кода сократилось бы в разы, но не было бы никаких проверок, и в любом случае нужно было бы самостоятельно писать проверку — какое именно событие случилось (если бы их было несколько). Тем не менее, в дальнейшем, когда хорошенько прокачаетесь, будете потихонечку переходить на работу с регистрами.
В этом примере делается то же самое, что делает HAL — сбрасывается флаг прерывания и вместо вызова колбека сразу же выполняется действие (мигание светиком).
Таким образом количество кода сократилось бы в разы, но не было бы никаких проверок, и в любом случае нужно было бы самостоятельно писать проверку — какое именно событие случилось (если бы их было несколько). Тем не менее, в дальнейшем, когда хорошенько прокачаетесь, будете потихонечку переходить на работу с регистрами.
Если вы внимательно посмотрите, то увидите что у каждого события есть свой колбек. Например у захвата/сравнения их несколько…
посмотреть
HAL_TIM_IC_CaptureCallback, HAL_TIM_OC_DelayElapsedCallback и HAL_TIM_PWM_PulseFinishedCallback.
Все эти колбеки прописаны в том же файле, с атрибутом
weak
Атрибут __weak говорит компилятору, что эта функция может быть переопределена. То есть если такую же функцию, но без weak, прописать ещё где-то, то функция с weak будет игнорироваться.
Находим нужный нам колбек…
… и переопределяем его в файл
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1) //check if the interrupt comes from TIM1
{
HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin); //Toggle the state of pin
}
}
Проверяем что прерывание пришло от таймера №1 и мигаем светиком.
Проверять от какого таймера пришло прерывание нужно в том случае, если используется несколько таймеров. Тут дело вот в чём: если мы настроим ещё один таймер, например №2, и он тоже будет вызывать прерывания, тогда в файле
Не смотря на то, что обработчиков два, функция
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM1)
{
HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin);
}
if(htim->Instance == TIM2)
{
HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin);
}
}
Это касается не только таймеров, но и прочей периферии — USART, SPI, I2C и т.д.
Программирование всего остального выглядит примерно так же как и таймера. Открываем соответствующий файл, например
Рассмотрим работу USART'а с DMA, там механизм несколько сложнее чем с таймером. В Кубе настройте USART с использованием DMA на приём…
Инициализация USART'а точно такая же как и у таймера…
Параметры загружаются в структуру и передаются в функцию.
Команда запуска опять же схожа с таймером (передаётся структура + доп. аргументы)…
HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buff, 10);
Первый аргумент это адрес структуры, второй — это адрес массива в который будут складываться полученные данные, третий — это количество байт которые нужно принять.
Здесь у нас много чего интересненького.
В первую очередь происходит проверка — занят USART или нет (HAL_UART_STATE_READY).
Если до этого функция уже запускалась и данные ещё не получены, то эта проверка не пройдёт и функция вернёт статус «занято» (return HAL_BUSY). Если же необходимо перезапустить функцию, то предварительно надо вызвать — HAL_UART_AbortReceive(&huart1). Как видите названия функций говорят сами за себя.
Далее проверяется не пустой ли указатель на приёмный буфер, и чтоб размер данных был не нулевой. Устанавливается блокировка (__HAL_LOCK) и начинается заполнение структуры
посмотретьhuart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;
Здесь помимо проверки и нового вида макроса (CLEAR_BIT) мы наконец-то видим колбек —HAL_UART_RxCpltCallback(huart) , который и нужно прописывать в main.c . Этот колбек вызывается когда буфер будет заполнен полностью.
Здесь помимо проверки и нового вида макроса (CLEAR_BIT) мы наконец-то видим колбек —
Прерывание может вызываться при заполнении половины буфера. За это отвечает
посмотреть
Для ошибки тоже есть функция с колбеком —
посмотреть
Следом идёт запуск DMA —
посмотреть
В функцию передаётся: указатель на структуру, источник данных (в нашем случае это регистр данных (DR) USART'а), получатель данных (адрес буфера), и ожидаемое кол-во байт.
Потом всё это хозяйство передаётся в функцию конфигурирования —DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength) , после чего происходит это…
Если элемент структурыhdma->XferHalfCpltCallback не пустой, то разрешаются прерывания по заполнению буфера полностью (DMA_IT_TC), по заполнению буфера наполовину (DMA_IT_HT), и при ошибке (DMA_IT_TE). Если нам не нужно отслеживать заполнение половины буфера, то надо в huart->hdmarx->XferHalfCpltCallback записать NULL.
Потом всё это хозяйство передаётся в функцию конфигурирования —
Если элемент структуры
Далее сбрасывается флаг ошибки переполнения (__HAL_UART_CLEAR_OREFLAG), снимается блокировка (__HAL_UNLOCK), с помощью макроса
На этом функция
Низкоуровневый обработчик прерываний от DMA выглядит так же как и в случае с таймером…
… вызывает HAL-обработчик
И опять же как и у таймера, функция состоит из нескольких блоков. Первый блок срабатывает при заполнении половинки буфера, второй — целиком, а третий при ошибке. Для примера рассмотрим блок полного буфера…
Проверяются флаги полного буфера (DMA_FLAG_TC1) и разрешённого прерывания (DMA_IT_TC).
Если отключён циклический режим DMA —
Заметьте, разработчики HAL снабдили всё функции, макросы и флаги связанные с прерываниями буквами
Далее устанавливается статус готовности к очередному приёму —
блокировка
Блокировка организована очень просто…
Если сделать__HAL_LOCK(huart) , то при обращении к структуре huart будет возвращаться статус «занято» — return HAL_BUSY;
Файлstm32f1xx_hal_def.h .
Если сделать
Файл
Последнее условие связано с тем, что было сделано в функции запуска. Если мы там сделали так —
Такая вот хитроумная конструкция
Если приём ведётся без DMA…
HAL_UART_Receive_IT(&huart1, (uint8_t*)rx_buff, 10);
Тогда после включения глобального прерывания USART'а появится его обработчик…
Перейдём к функции
Тут появился ещё один макрос —
Далее проверятся что произошло:
В зависимости от длины принимаемого слова (8 или 9 бит) выбирается первая или вторая конструкция, и данные из регистра DR (Data Register) записываются в приёмный буфер —
Если длина слова 9 бит, то для его сохранения используется два байта — huart->pRxBuffPtr += 2U;
Следом проверяется счётчик принятых байт —
Вы наверно обратили внимание, что при принятии одного байта происходит очень много операций
В завершение хочется рассказать про копирование через DMA. Для этого режима у DMA есть механизм создания колбеков.
Настроим Куб для копирование массива из одной области памяти в другую при помощи DMA…
Длина слова указана Word (32 бита), то есть копироваться будет по четыре байта за один такт.
программа
#include "main.h"
#define BUFFSIZE 20
DMA_HandleTypeDef hdma_memtomem_dma1_channel1;
uint8_t src_buff[BUFFSIZE] = {0,};
uint8_t dst_buff[BUFFSIZE] = {0,};
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
void DMA_m2m_Callback(DMA_HandleTypeDef *hdma_memtomem_dma1_channel1) // колбек по окончанию копирования через DMA
{
// копирование завершено
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// регистрация колбека по окончанию копирования через DMA
if(HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, DMA_m2m_Callback) != HAL_OK)
{
Error_Handler();
}
while (1)
{
// запускаем копирование через DMA
HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE / 4);
HAL_Delay(1000);
}
}
Функция регистрации колбека…
В функцию передаются три аргумента:
1. Указатель на структуру.
2. Ключ, по которому определяется какое событие должно вызвать колбек — скопирован весь буфер, скопирована половина буфера и т.д.
В нашем случае указан полный буфер — HAL_DMA_XFER_CPLT_CB_ID.
3. Название колбека. Придумайте сами.
Таким образом мы зарегистрировали колбек —
Функция запуска копирования…
HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE / 4);
Аргументы: указатель на структуру, массив из которого копируется, массив в который копируется, количество байт (ячейки массива 8-ми битные, а DMA будет копировать по 32 бита за раз).
Содержимое этой функции поизучайте самостоятельно, вы уже всё знаете
По окончанию копирования произойдёт прерывание и будет вызван обработчик…
В функции
Элемент структуры
На этом наверно всё.
Всем спасибо
User Manual — UM1850
Телеграм-чат istarik
Телеграм-чат STM32
- 0
- stD
87573
Поддержать автора
Комментарии (0)