Раніше ми розглянули як здійснювати найпростіші операції із таймерами, безпосередньо маніпулюючи їх регістрами. Звичайно, користуючись константами та функціями із CMSIS, але нічим більш високорівневим. Тепер подивимося, як це ж зробити за допомогою HAL. Важливо, що HAL -- лише бібліотека, якою користуватися можна по різному, сама по собі вона особливих переваг не надає. (IMHO, звичайно). Однак, STM32CubeMX вміє генерувати ініціалізацію периферії з візуальних налаштувань, користуючись цією бібліотекою. Це дуже спрощує розробку -- все ж, принаймні для мене, найбільша проблема роботи із такими контролерами -- сотні бітів конфігурації периферії, які слід вмикати і вимикати в правильній послідовності. Cube дозволяє, як мінімум, побачити правильну таку послідовність -- спрощуючи й "ручну" роботу з ними в майбутньому. Як максимум -- просто про це не турбуватися. Додаткова абстракція вносить свої труднощі, але серйозною проблемою це не є -- весь код доступний, навіть без додаткової документації можна розібратися, що-куди-як, а, як буде показано в подальшому, за ввімкненої оптимізації, при всій громіздкості (в порівнянні із ручною маніпуляцією регістрами) коду HAL/STM32CubeMX, зростання розмірів пам'яті обох видів -- цілком скромне.
Отож, повторимо зроблене в попередньому прикладі, користуючись HAL.
Зауваження -- цей же Cube-проект використовується і для наступного поста. (Лінь дублювати лише заради скріншотів). Все на них, що стосується TIM3 та піна PA7 поки просто ігноруємо, воно ніяк не зв'язано із рештою.
"Малюємо" проект
Отож, нам потрібно:
- Ініціалізувати на відлік таймери TIM1 і TIM6.
- Для цього їм слід забезпечити повноцінне тактування.
- Піни PA8 та PA9, до яких під'єднані світлодіоди, слід налаштувати працювати на вивід.
Почнемо із тактування та таймерів:
- Вмикаємо, в RCC, зовнішнє тактування -- із використанням HSE, котрий покладатиметься на кварцовий кристал (на платі він присутній).
- TIM6 зовсім простенький (нагадаємо, це базовий таймер), його достатньо просто ввімкнути.
- TIM1 складніший, але поки ніяких його просунутих можливостей не використовуватиметься, тому лише вмикаємо відлік від внутрішнього годинника.
Ще тут слід задати режим роботи пінів (гарні назви, як на скріншоті, ми дамо їм пізніше):
![]() |
Варіанти налаштування піна PC9 (PortC, пін номер 9). |
Тепер слід налаштувати тактування так, щоб контролер працював на максимальній для нього частоті, 24МГц. Зробити це можна на наступній вкладці, Clock Configuration:
![]() |
Можливість використовувати HSE з'явиться лише після того, як вона буде ввімкнена на попередній вкладці (RCC). |
Вкладка Configuration дозволяє більш тонко налаштувати обрані компоненти:
DMA ми поки не користуємося, тому навіть не заглядаємо. Кнопка NVIC дозволяє керувати пріоритетом переривань та дозволяти чи забороняти їх. Останнє можна також зробити, конфігуруючи відповідну периферію, а пріоритети для цього проекту не особливо важливі, тому тут теж нічого не робитимемо, але для ілюстрації можливості ними керувати, наведу:
Аналогічно, нічого не змінюватимемо і в конфігурації RCC -- там є калібрування HSI, керування глобальним перериванням від RCC, налаштування пінів кварцу і деякі інші спеціалізовані налаштування.
От кнопка GPIO та відповідний діалог знадобиться:
Як і решта подібних діалогів, воно дає доступ до різноманітного конфігурування всього, що стосується GPIO, наприклад, вкладка RCC дозволяє, як і аналогічна в діалозі "RCC", налаштувати піни кварцу. Але це нас не цікавить, (як і вкладка TIM3, котрий ми в цьому пості ігноруємо, див. вище).
На вкладці GPIO, заради якої нам цей діалог і потрібен, можна сконфігурувати будь-який пін, не прив'язаний до конкретної периферії. Обираємо для кожного із них (нижня частина рисунку):
- GPIO mode: Output push-pull
- Максимальну частоту: низька (в даному випадку це відповідатиме 2МГц, більш ніж достатньо)
- User Label, зручне для користувача ім'я. Давати такі імена -- корисно! Увага: вартує присвоювати їх згідно функціонального призначення, а не технічних подробиць -- номерів пінів і т.д. Однак тут, в навчальному прикладі, я частково порушую це правило, включаючи в ім'я мітки і порт-пін: GREEN_LED_PC8, BLUE_LED_PC9 -- щоб спростити співставлення логіки програм та описаних в цих текстах принципів роботи із таймерами. Це, власне ті імена, що видно на скріншоті першої вкладки, Pinout.
Нарешті, головне -- конфігурація таймерів. Почнемо з простішого, TIM6:
![]() |
Зауважте, якщо вибрати опцію, в нижньому рядку показує мінімальну підказку. Часто -- зовсім мінімальну, але буває корисно. |
Можна обрати подільник, PSC (24000-1, як і в попередньому пості), напрямок відліку -- вверх, частоту спрацювань -- 1000, раз в секунду, якщо контролер працює на 24 МГц. Таймер не слугуватиме тригером для іншого, тому дану опцію не чіпаємо.
Тепер потрібно дозволити переривання від таймера (інші вкладки зараз нічого цікавого не містять):
Тут достатньо поставити одну галочку, але є доступ і до пріоритету (про різницю між пріоритетом та суб-пріоритетом -- напишу колись окремо, а поки див. тут: "ARM. Учебный Курс. Прерывания и NVIC — приоритетный контроллер прерываний").
Конфігурація TIM1 містить більше опцій (див. його можливості у попередньому пості), але, на загал, така ж, як і для TIM6:
Ми хочемо, щоб цей таймер працював із періодом 0.3 с.
От з перериваннями цікавіше:
Їх у нього багато, дозволяємо лише переривання по оновленню.
Можна генерувати код. Хоча деякі нюанси із тим є, воно вже описувалося, тому тут не повторюватимемо. Як генерувати, як використати його разом із CoIDE, та його структуру було розглянуто в попередньому посиланні.
Код проекту
Спочатку розглянемо загальну структуру програми, потім подивимося, як працюють автоматично згенеровані функції та ключові для нашої програми елементи HAL.
main()
Спершу йде ініціалізація:
яка здійснюється автоматично згенерованими функціями, про них мова йтиме нижче. Тобто, весь код вище, разом із тілами функцій, згенеровано Cube.
Далі -- наш хід ("USER CODE BEGIN"), ініціалізуємо таймери:
Решту роботи буде зроблено в обробниках переривань. В принципі, їх можна писати так, як раніше, але це не цікаво. Як їх обробка організована в HAL, ми розглянемо пізніше -- вона складніша за структурою ніж ручна обробка з попереднього поста (звична плата за гнучкість). Поки достатньо сказати, що коли від будь-якого таймера прийде переривання оновлення, викликається функція зворотного виклику (callback):
Вона оголошена в нутрощах HAL як weak -- слабкий символ, тому ми можемо її замістити. Їй передається вказівник на структуру, що описує таймер, від якого прийшло переривання:
Для зміни стану світлодіода використовується функція HAL_GPIO_TogglePin().
Зауважте, на відміну від "великих" фреймворків типу Qt, HAL -- лише обгортка над засобами мікроконтролера, із мінімумом внутрішніх інваріантів, тому паралельно з нею можна користуватися засобами CMSIS, хоч й знижуючи переносимість коду. Тобто, для перемикання світлодіодів можна було скористатися й звичним:
/* MCU Configuration----------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_TIM1_Init(); MX_TIM6_Init(); /* USER CODE BEGIN 2 */
яка здійснюється автоматично згенерованими функціями, про них мова йтиме нижче. Тобто, весь код вище, разом із тілами функцій, згенеровано Cube.
Далі -- наш хід ("USER CODE BEGIN"), ініціалізуємо таймери:
HAL_TIM_Base_Start_IT(&htim1); HAL_TIM_Base_Start_IT(&htim6);
Решту роботи буде зроблено в обробниках переривань. В принципі, їх можна писати так, як раніше, але це не цікаво. Як їх обробка організована в HAL, ми розглянемо пізніше -- вона складніша за структурою ніж ручна обробка з попереднього поста (звична плата за гнучкість). Поки достатньо сказати, що коли від будь-якого таймера прийде переривання оновлення, викликається функція зворотного виклику (callback):
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
Вона оголошена в нутрощах HAL як weak -- слабкий символ, тому ми можемо її замістити. Їй передається вказівник на структуру, що описує таймер, від якого прийшло переривання:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if( htim->Instance == TIM6 ) { HAL_GPIO_TogglePin(BLUE_LED_PC9_GPIO_Port, BLUE_LED_PC9_Pin); } if( htim->Instance == TIM1 ) { HAL_GPIO_TogglePin(GREEN_LED_PC8_GPIO_Port, GREEN_LED_PC8_Pin); } }
Для зміни стану світлодіода використовується функція HAL_GPIO_TogglePin().
Зауважте, на відміну від "великих" фреймворків типу Qt, HAL -- лише обгортка над засобами мікроконтролера, із мінімумом внутрішніх інваріантів, тому паралельно з нею можна користуватися засобами CMSIS, хоч й знижуючи переносимість коду. Тобто, для перемикання світлодіодів можна було скористатися й звичним:
BLUE_LED_PC9_GPIO_Port->ODR^=(BLUE_LED_PC9_Pin); //Змінюємо стан світлодіодів
Як не дивно --- все. Це -- весь код. Два рядки ініціалізації та обробник зворотного виклику на п'ять рядків, не рахуючи дужок.
Скільки все це щастя потребує ресурсів, подивимося в одному із наступних постів, а зараз розберемося трішки детальніше, як воно організовано -- дійдемо від рівня абстракції HAL до CMSIS і окремих регістрів. Почнемо із згенерованих Cube функцій.
Вона ініціалізує RCC згідно обраної графічно конфігурації. Структура RCC_OscInitTypeDef описує налаштування високочастотних внутрішнього (HSI), зовнішнього (HSE), та аналогічних низькочастотних осциляторів (LSI, LSE), назви полів та обрані константи значень цілком промовисті:
Функція HAL_RCC_ClockConfig() конфігурує власне RCC, налаштовує частоту процесора та шин AHB, APB, беручи інформацію з RCC_ClkInitTypeDef, яка містить джерело тактового сигналу та значення різноманітних подільників. Детальніше див. ті ж .h та .c файли, що і для попередньої і вкладку "Clock Configuration".
Також, HAL, для своїх потреб, ініціалізує SysTick-таймер так, щоб він спрацьовував кожну мілісекунду:
HAL_RCC_GetHCLKFreq() повертає тактову частоту процесора, HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK) наказує не користуватися подільником, а HAL_NVIC_SetPriority() встановлює пріоритет для SysTick.
Подивимося на обробник переривання від цього таймера. Знаходиться він в файлі stm32f1xx_it.c, створеним Cube.
Перша із викликаних функцій лише збільшує внутрішній лічильник (Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal.c):
Лічильник цей -- статична змінна відповідного файлу, безпосередньо лізти до нього не варто, HAL містить ряд пов'язаних функцій доступу (Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal.h):
де:
Тобто, така складна конструкція, всього лиш, дозволяє викликати власний код у відповідь на кожне переривання цього таймера, визначивши свою HAL_SYSTICK_Callback(). Аналогічно, власний код можна дописувати і до SysTick_Handler() -- див. відповідні коментарі в її коді вище -- HAL не дуже вимогливий, строгих інваріантів має мало. Хоча, якщо немає поважних причин, відхилятися від рекомендованої ним практики не варто -- такі відхилення можуть ускладнити перехід на нові версії.
На загал, загальну ідею HAL видно вже на цьому прикладі -- схожі рівні опосередкованості постійно траплятимуться в подальшому . Звичайно, вище розглянуто далеко не весь пов'язаний код -- це було б занадто громіздко, див., наприклад, функції ініціалізації.
Важливо, що без оптимізації такий код видається надміру громіздкий для мікроконтролерів, однак, як буде показано в наступних постах, оптимізатор цілком справляється із його покращенням.
Скільки все це щастя потребує ресурсів, подивимося в одному із наступних постів, а зараз розберемося трішки детальніше, як воно організовано -- дійдемо від рівня абстракції HAL до CMSIS і окремих регістрів. Почнемо із згенерованих Cube функцій.
Ініціалізація
SystemClock_Config()
/** System Clock Configuration */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct; RCC_ClkInitTypeDef RCC_ClkInitStruct; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL3; HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0); HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); /* SysTick_IRQn interrupt configuration */ HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); }
Вона ініціалізує RCC згідно обраної графічно конфігурації. Структура RCC_OscInitTypeDef описує налаштування високочастотних внутрішнього (HSI), зовнішнього (HSE), та аналогічних низькочастотних осциляторів (LSI, LSE), назви полів та обрані константи значень цілком промовисті:
- HSEState = RCC_HSE_ON
- PLLState = RCC_PLL_ON
- PLLSource = RCC_PLLSOURCE_HSE
- і т.д.
Функція HAL_RCC_ClockConfig() конфігурує власне RCC, налаштовує частоту процесора та шин AHB, APB, беручи інформацію з RCC_ClkInitTypeDef, яка містить джерело тактового сигналу та значення різноманітних подільників. Детальніше див. ті ж .h та .c файли, що і для попередньої і вкладку "Clock Configuration".
Також, HAL, для своїх потреб, ініціалізує SysTick-таймер так, щоб він спрацьовував кожну мілісекунду:
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); /* SysTick_IRQn interrupt configuration */ HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
HAL_RCC_GetHCLKFreq() повертає тактову частоту процесора, HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK) наказує не користуватися подільником, а HAL_NVIC_SetPriority() встановлює пріоритет для SysTick.
Подивимося на обробник переривання від цього таймера. Знаходиться він в файлі stm32f1xx_it.c, створеним Cube.
Для кращої структурованості, генеруючи код, я обрав запис ініціалізації кожного виду периферії у окрему пару .c/.h файлів, тому обробники переривань опинилися в stm32f1xx_it.c, функції зворотного виклику ініціалізації в stm32f1xx_hal_msp.c, ініцілізація таймерів та GPIO -- в tim.c і gpio.c, відповідно. Крім "їх" хідерів генеруються також stm32f1xx_hal_conf.h, із всілякими налаштуваннями та mxconstants.h із іменами пінів та їх портами.Його будова відображає загальну філософію обробки переривань HAL.
/** * @brief This function handles System tick timer. */ void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); HAL_SYSTICK_IRQHandler(); /* USER CODE BEGIN SysTick_IRQn 1 */ /* USER CODE END SysTick_IRQn 1 */ }
Перша із викликаних функцій лише збільшує внутрішній лічильник (Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal.c):
__weak void HAL_IncTick(void) { uwTick++; }
Лічильник цей -- статична змінна відповідного файлу, безпосередньо лізти до нього не варто, HAL містить ряд пов'язаних функцій доступу (Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal.h):
- uint32_t HAL_GetTick(void) -- повертає кількість тіків (в поточній реалізації -- вміст цієї змінної),
- void HAL_Delay(__IO uint32_t Delay) -- створює вказану затримку. Для конфігурації по замовчуванню затримка задається в мілісекундах.
- void HAL_SuspendTick(void) -- призупиняє роботу таймера, а void HAL_ResumeTick(void) запускає його знову.
/** * @brief This function handles SYSTICK interrupt request. * @retval None */ void HAL_SYSTICK_IRQHandler(void) { HAL_SYSTICK_Callback(); }
де:
/** * @brief SYSTICK callback. * @retval None */ __weak void HAL_SYSTICK_Callback(void) { /* NOTE : This function Should not be modified, when the callback is needed, the HAL_SYSTICK_Callback could be implemented in the user file */ }
Тобто, така складна конструкція, всього лиш, дозволяє викликати власний код у відповідь на кожне переривання цього таймера, визначивши свою HAL_SYSTICK_Callback(). Аналогічно, власний код можна дописувати і до SysTick_Handler() -- див. відповідні коментарі в її коді вище -- HAL не дуже вимогливий, строгих інваріантів має мало. Хоча, якщо немає поважних причин, відхилятися від рекомендованої ним практики не варто -- такі відхилення можуть ускладнити перехід на нові версії.
На загал, загальну ідею HAL видно вже на цьому прикладі -- схожі рівні опосередкованості постійно траплятимуться в подальшому . Звичайно, вище розглянуто далеко не весь пов'язаний код -- це було б занадто громіздко, див., наприклад, функції ініціалізації.
Важливо, що без оптимізації такий код видається надміру громіздкий для мікроконтролерів, однак, як буде показано в наступних постах, оптимізатор цілком справляється із його покращенням.
Ініціалізація GPIO -- MX_GPIO_Init()
Знаходиться ця функція у файлі gpio.c:
Вже можна здогадатися, як воно працює. GPIO_InitTypeDef містить номера пінів, що конфігуруватимуться, їх бажаний режим, швидкодію та початковий логічний рівень на них (поле Pull, тут не ініціалізоване).
З __GPIOD_CLK_ENABLE і компанією -- трохи загадка, він оголошений в Drivers\STM32F1xx_HAL_Driver\Inc\Legacy\stm32_hal_legacy.h, як синонім до __HAL_RCC_GPIOD_CLK_ENABLE. Загадка в тому, чого Cube зразу цей макрос не підставив?
Сам макрос, оголошений в stm32f1xx_hal.h, виглядає так:
Тобто він лише встановлює біт IOPDEN в регістрі APB2ENR, як ми робили вручну раніше. Хіба що чекає, поки периферія таки ініціалізується.
HAL_GPIO_Init() налаштовує відповідні регістри. Її повний текст див. у Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c, він занадто громіздкий для наведення повністю, а тут покажу лише частину, яка працюватиме у нашому випадку:
Параметри перевіряються відповідними макросами, що контролюють їх припустимість, наскільки це можливо на етапі компіляції, наприклад (Drivers/CMSIS/STM32F1xx/Include/stm32f100xb.h):
Спроба використати інший порт гарантовано помилкова. Правда, такі перевірки часто скаржаться на код, згенерований Cube, хоча, за детальнішого аналізу -- дарма. Тобто, assert-и HAL місцями надміру параноїдальні, а Cube -- дещо неохайний по відношенню до них.
Далі, для кожного із 16 пінів порту, перевіряється чи встановлено його біт в GPIO_Init->Pin -- чи вимагають його ініціалізації. Якщо так, визначаються потрібні біти конфігурації (в фрагменті вище залишено лише код для Output-push/pull), визначається, чи це пін із нижніх восьми чи верхніх восьми, щоб вибрати регістр CRL чи CRH відповідно, записується отримане значення. Якщо пін має бути джерелом переривань, вони теж налаштовуються (для лаконічності цей код не наводиться).
Макрос MODIFY_REG робить, всього лиш, наступне (разом із використаними ним макросами -- як тільки людям не доводиться викручуватися без С++ і шаблонів):
Весь код простий, хоча, магію маніпуляції бітами слід згадати!
Переходимо до таймерів.
Структура TIM_HandleTypeDef -- головна при конфігуруванні таймерів:
TIM_TypeDef -- структура CMSIS, що відповідає регістрам таймера, Instance -- вказівник на початок області регістрів конкретного таймера.
TIM_Base_InitTypeDef описує загальні налаштування із очевидними назвами полів:
Перерахування HAL_LockTypeDef, котре може містити лише два значення, HAL_UNLOCKED або HAL_LOCKED, використовується HAL під час маніпуляцій таймером, для запобігання racing condition (стану гонитви).
HAL_TIM_ActiveChannel -- біти активних каналів, поки не актуальні для нас. Також неактуальними зараз є канали DMA.
HAL_TIM_StateTypeDef визначає поточний стан таймера:
Після заповнення структуру відповідними значенням, заданими під час графічної конфігурації, викликається функція ініціалізації, HAL_TIM_Base_Init(&htim6).
Структура TIM_MasterConfigTypeDef використовується для керування роботою таймера в ролі джерела тактування для іншого таймера. Цією можливістю ми поки не користуємося, тому забороняємо. Структура передається відповідній функції, HAL_TIMEx_MasterConfigSynchronization().
Розглянемо функцію ініціалізації таймера (Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c)
Після вже звичної перевірки параметрів, вона дивиться, чи даний таймер було хоча б раз ініціалізовано протягом цього запуску. Якщо ні -- викликає callback-функцію HAL_TIM_Base_MspInit(), перевизначення якої (все та ж магія weak-символів) знаходиться в "нашому" tim.c. Після цього помічається таймер як зайнятий, конфігурується за допомогою функції TIM_Base_SetConfig(), якій передається вказівник на структуру, що відображається на регістри таймера та структуру із даними для конфігурування та знову помічається як доступний.
HAL_TIM_Base_MspInit() єдина і для TIM6 і для TIM1, наведемо її тут:
Сподіваюся, на даному етапі детальніше її розбирати немає потреби -- дозволяється тактування, налаштовуються переривання. Як побачимо в майбутньому -- тут же знаходитиметься ініціалізація пінів, якими безпосередньо керуватимуть таймери. Макрос __TIM6_CLK_ENABLE теж знаходиться в stm32_hal_legacy.h. Він еквівалентний __HAL_RCC_TIM6_CLK_ENABLE, як і розглянутий вище аналогічний макрос для GPIO. Робить він, в основному, наступне:
Знайома конструкція. :-)
TIM_Base_SetConfig() із HAL, дотягується, нарешті, до рівня абстракції CMSIS (по простому -- заліза):
Вона, як не дивно, зовсім простенька. Якщо таймер підтримує різні напрямки відліку, подільник, повтори (TIM1 --- все перераховане підтримує, TIM6 -- все ні), вони налаштовуються, інакше, як і належить, відповідні біти не зачіпаються. Встановлюється, безпосередньо, вміст регістрів ARR та PSC. Майже все. (Див. також коментарі.)
Про обробку переривань -- трішки нижче, а поки --- ініціалізація TIM1.
Основна відмінність -- доналаштовуються можливості TIM1, відсутні в TIM6 -- кількість повторів рівна нулю, джерело тактування -- внутрішнє. Все. (Про HAL_TIM_ConfigClockSource() ще трішки згадаємо в одному із наступних постів.)
Як бачимо, переривання від обох таймерів (при чому, для TIM1 це одне із чотирьох можливих, але решта мали б бути забороненими) опрацьовуються однією і тією ж "мегафункцією" HAL_TIM_IRQHandler(), хоч можна також вписувати свій код. Доведеться розглядати і її.
Знаходиться ця функція в Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c, і доволі велика, її розмір аж дещо лякає, адже вона викликатиметься за кожного переривання. Розглядатимемо її по частинах. Більшість подій, що вона обробляє, не актуальні для цього поста, однак знадобляться в подальшому.
Частина перша -- оголошення і реакція на capture compare:
Однаковий код виконується для кожного із каналів, розглядаємо тільки перший. Спочатку перевіряється, чи є така подія, перевіряючи біт TIM_FLAG_CC1 із регістра статусу TIMx_SR. Макрос __HAL_TIM_GET_FLAG:
Якщо так, перевіряється чи відбулося відповідне переривання -- чи встановлено біт TIM_IT_CC1 із регістра TIMx_DIER:
Якщо так, приступається для обробки. Спочатку, за допомогою __HAL_TIM_CLEAR_IT, очищається прапорець переривання:
Активний канал вказується в структурі, що описує таймер, далі викликаються (слабкі, як завжди) функції зворотного виклику із доволі очевидною назвою:
Частина друга -- подія оновлення:
Аналогічно до першої частини, перевіряється в регістрі статусу, чи була подія (біт TIM_FLAG_UPDATE регістра статусу), потім -- чи було від неї переривання (біт TIM_IT_UPDATE з TIMx_DIER), якщо так -- переривання очищається та викликається функція зворотного виклику, якою ми вже користувалися вище:
Схема їх роботи та ж. Викликаються, відповідно, наступні функції зворотного виклику:
Тепер коротко подивимося і на неї:
Вона, вмикає перед-вибірку з flash, якщо контролер її підтримує (STM32F100 -- ні), налаштовує поділ пріоритетів переривань на групі і підгрупи, ініціалізує SysTick для потреб HAL, (спрацювання раз на мілісекунду, потім воно буде замінене на нову конфігурацію, викликом розглянутої вище SystemClock_Config()) та викликає функцію зворотного виклику ініціалізації периферії -- HAL_MspInit(). В проекті Cube вона заміщається (weak символ) у файлі stm32f1xx_hal_msp.c :
Яка, в принципі, повторює те ж. Основна відмінність -- цю конфігурацію можна змінювати в Cube, а HAL_Init() використовує типову, безпечну, конфігурацію, достатню для початкової ініціалізації контролера.
Перевіряється, а чи справді передано таймер (а не просто некоректний вказівник), після чого дозволяється, відповідними макросами, переривання по оновленню для цього таймера та сам таймер. Макроси, разом із своїми протилежностями, корисні й самі по собі:
Останній макрос трохи складніший, так як таймер можна вимикати/забороняти лише тоді, коли всі його канали заборонено:
Симетрична функція зупинки таймера теж проста:
Є ще два варіанти функцій запуску таймерів -- HAL_TIM_Base_Start()/HAL_TIM_Base_Stop(), які запускають/зупиняють відлік без вмикання переривань та HAL_TIM_Base_Start_DMA()/HAL_TIM_Base_Stop_DMA(), які вмикають таймер в режимі обміну з використанням DMA. Останні функції складніші, через необхідність маніпуляцій з DMA, не розглядатимемо їх поки.
Десь вже це було... :-)
HAL_GPIO_WritePin(), атомарно встановлює значення піну:
Встановлює в 1, користуючись нижньою частиною регістра BSRR, в нуль -- верхньою.
HAL_GPIO_ReadPin(), читає пін:
Як відомо, регістр GPIOx_IDR містить реальний стан піна, їх вміст оновлюється раз на такт шини APB2, до якої порти GPIO і під'єднані.
Скачати проект можна тут.
Поки все, в наступній парі постів (CMSIS/HAL) розглянемо генерацію ШІМ (PWM).
void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; /* GPIO Ports Clock Enable */ __GPIOD_CLK_ENABLE(); __GPIOA_CLK_ENABLE(); __GPIOC_CLK_ENABLE(); /*Configure GPIO pins : PCPin PCPin */ GPIO_InitStruct.Pin = GREEN_LED_PC8_Pin|BLUE_LED_PC9_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); }
Вже можна здогадатися, як воно працює. GPIO_InitTypeDef містить номера пінів, що конфігуруватимуться, їх бажаний режим, швидкодію та початковий логічний рівень на них (поле Pull, тут не ініціалізоване).
З __GPIOD_CLK_ENABLE і компанією -- трохи загадка, він оголошений в Drivers\STM32F1xx_HAL_Driver\Inc\Legacy\stm32_hal_legacy.h, як синонім до __HAL_RCC_GPIOD_CLK_ENABLE. Загадка в тому, чого Cube зразу цей макрос не підставив?
Сам макрос, оголошений в stm32f1xx_hal.h, виглядає так:
#define __HAL_RCC_GPIOD_CLK_ENABLE() do { \ __IO uint32_t tmpreg; \ SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPDEN);\ /* Delay after an RCC peripheral clock enabling */\ tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPDEN);\ UNUSED(tmpreg); \ } while(0)
Тобто він лише встановлює біт IOPDEN в регістрі APB2ENR, як ми робили вручну раніше. Хіба що чекає, поки периферія таки ініціалізується.
HAL_GPIO_Init() налаштовує відповідні регістри. Її повний текст див. у Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c, він занадто громіздкий для наведення повністю, а тут покажу лише частину, яка працюватиме у нашому випадку:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init) { uint32_t position; uint32_t ioposition = 0x00; uint32_t iocurrent = 0x00; uint32_t temp = 0x00; uint32_t config = 0x00; __IO uint32_t *configregister; /* Store the address of CRL or CRH register based on pin number */ uint32_t registeroffset = 0; /* offset used during computation of CNF and MODE bits placement inside CRL or CRH register */ /* Check the parameters */ assert_param(IS_GPIO_ALL_INSTANCE(GPIOx)); assert_param(IS_GPIO_PIN(GPIO_Init->Pin)); assert_param(IS_GPIO_MODE(GPIO_Init->Mode)); assert_param(IS_GPIO_PULL(GPIO_Init->Pull)); /* Configure the port pins */ for (position = 0; position < GPIO_NUMBER; position++) // Для всіх пінів, GPIO_NUMBER==16 { /* Get the IO position */ ioposition = ((uint32_t)0x01) << position; /* Get the current IO position */ iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition; if (iocurrent == ioposition) // Якщо біт даного піна встановлено в GPIO_Init->Pin { /* Check the Alternate function parameters */ assert_param(IS_GPIO_AF_INSTANCE(GPIOx)); /* Based on the required mode, filling config variable with MODEy[1:0] and CNFy[3:2] corresponding bits */ switch (GPIO_Init->Mode) { /* If we are configuring the pin in OUTPUT push-pull mode */ case GPIO_MODE_OUTPUT_PP: /* Check the GPIO speed parameter */ assert_param(IS_GPIO_SPEED(GPIO_Init->Speed)); config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP; // Комбінація бітів, що відповідає "General purpose output push-pull", 0 насправді break; //<.............SKIPPED .......................> //<.............SKIPPED .......................> //<.............SKIPPED .......................> } /* Check if the current bit belongs to first half or last half of the pin count number in order to address CRH or CRL register*/ configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH; registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2) : ((position - 8) << 2); /* Apply the new configuration of the pin to the register */ MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset ), (config << registeroffset)); /*--------------------- EXTI Mode Configuration ------------------------*/ /* Configure the External Interrupt or event for the current IO */ if((GPIO_Init->Mode & EXTI_MODE) == EXTI_MODE) { //<.............SKIPPED .......................> //<.............SKIPPED .......................> //<.............SKIPPED .......................> } } } }
Параметри перевіряються відповідними макросами, що контролюють їх припустимість, наскільки це можливо на етапі компіляції, наприклад (Drivers/CMSIS/STM32F1xx/Include/stm32f100xb.h):
#define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) || \ ((INSTANCE) == GPIOB) || \ ((INSTANCE) == GPIOC) || \ ((INSTANCE) == GPIOD) || \ ((INSTANCE) == GPIOE))
Спроба використати інший порт гарантовано помилкова. Правда, такі перевірки часто скаржаться на код, згенерований Cube, хоча, за детальнішого аналізу -- дарма. Тобто, assert-и HAL місцями надміру параноїдальні, а Cube -- дещо неохайний по відношенню до них.
Далі, для кожного із 16 пінів порту, перевіряється чи встановлено його біт в GPIO_Init->Pin -- чи вимагають його ініціалізації. Якщо так, визначаються потрібні біти конфігурації (в фрагменті вище залишено лише код для Output-push/pull), визначається, чи це пін із нижніх восьми чи верхніх восьми, щоб вибрати регістр CRL чи CRH відповідно, записується отримане значення. Якщо пін має бути джерелом переривань, вони теж налаштовуються (для лаконічності цей код не наводиться).
Макрос MODIFY_REG робить, всього лиш, наступне (разом із використаними ним макросами -- як тільки людям не доводиться викручуватися без С++ і шаблонів):
#define WRITE_REG(REG, VAL) ((REG) = (VAL)) #define READ_REG(REG) ((REG)) #define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
Весь код простий, хоча, магію маніпуляції бітами слід згадати!
Переходимо до таймерів.
Ініціалізуємо базовий TIM6 -- MX_TIM6_Init()
Функція ця знаходиться в автоматично згенерованому tim.c, відповідний обробник переривань -- в stm32f1xx_it.c.TIM_HandleTypeDef htim6; /* TIM6 init function */ void MX_TIM6_Init(void) { TIM_MasterConfigTypeDef sMasterConfig; htim6.Instance = TIM6; htim6.Init.Prescaler = 23999; htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = 1000; HAL_TIM_Base_Init(&htim6); sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig); }
Структура TIM_HandleTypeDef -- головна при конфігуруванні таймерів:
/** * @brief TIM Time Base Handle Structure definition */ typedef struct { TIM_TypeDef *Instance; /*!< Register base address */ TIM_Base_InitTypeDef Init; /*!< TIM Time Base required parameters */ HAL_TIM_ActiveChannel Channel; /*!< Active channel */ DMA_HandleTypeDef *hdma[7]; /*!< DMA Handlers array This array is accessed by a @ref TIM_DMA_Handle_index */ HAL_LockTypeDef Lock; /*!< Locking object */ __IO HAL_TIM_StateTypeDef State; /*!< TIM operation state */ }TIM_HandleTypeDef;
TIM_TypeDef -- структура CMSIS, що відповідає регістрам таймера, Instance -- вказівник на початок області регістрів конкретного таймера.
TIM_Base_InitTypeDef описує загальні налаштування із очевидними назвами полів:
/** * @brief TIM Time base Configuration Structure definition */ typedef struct { uint32_t Prescaler; /*!< Specifies the prescaler value used to divide the TIM clock. This parameter can be a number between Min_Data = 0x0000 and Max_Data = 0xFFFF */ uint32_t CounterMode; /*!< Specifies the counter mode. This parameter can be a value of @ref TIM_Counter_Mode */ uint32_t Period; /*!< Specifies the period value to be loaded into the active Auto-Reload Register at the next update event. This parameter can be a number between Min_Data = 0x0000 and Max_Data = 0xFFFF. */ uint32_t ClockDivision; /*!< Specifies the clock division. This parameter can be a value of @ref TIM_ClockDivision */ uint32_t RepetitionCounter; /*!< Specifies the repetition counter value. Each time the RCR downcounter reaches zero, an update event is generated and counting restarts from the RCR value (N). This means in PWM mode that (N+1) corresponds to: - the number of PWM periods in edge-aligned mode - the number of half PWM period in center-aligned mode This parameter must be a number between Min_Data = 0x00 and Max_Data = 0xFF. @note This parameter is valid only for TIM1 and TIM8. */ } TIM_Base_InitTypeDef;
Перерахування HAL_LockTypeDef, котре може містити лише два значення, HAL_UNLOCKED або HAL_LOCKED, використовується HAL під час маніпуляцій таймером, для запобігання racing condition (стану гонитви).
HAL_TIM_ActiveChannel -- біти активних каналів, поки не актуальні для нас. Також неактуальними зараз є канали DMA.
HAL_TIM_StateTypeDef визначає поточний стан таймера:
typedef enum { HAL_TIM_STATE_RESET = 0x00, /*!< Peripheral not yet initialized or disabled */ HAL_TIM_STATE_READY = 0x01, /*!< Peripheral Initialized and ready for use */ HAL_TIM_STATE_BUSY = 0x02, /*!< An internal process is ongoing */ HAL_TIM_STATE_TIMEOUT = 0x03, /*!< Timeout state */ HAL_TIM_STATE_ERROR = 0x04 /*!< Reception process is ongoing */ }HAL_TIM_StateTypeDef;
Після заповнення структуру відповідними значенням, заданими під час графічної конфігурації, викликається функція ініціалізації, HAL_TIM_Base_Init(&htim6).
Структура TIM_MasterConfigTypeDef використовується для керування роботою таймера в ролі джерела тактування для іншого таймера. Цією можливістю ми поки не користуємося, тому забороняємо. Структура передається відповідній функції, HAL_TIMEx_MasterConfigSynchronization().
Розглянемо функцію ініціалізації таймера (Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c)
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim) { /* Check the TIM handle allocation */ if(htim == NULL) { return HAL_ERROR; } /* Check the parameters */ assert_param(IS_TIM_INSTANCE(htim->Instance)); assert_param(IS_TIM_COUNTER_MODE(htim->Init.CounterMode)); assert_param(IS_TIM_CLOCKDIVISION_DIV(htim->Init.ClockDivision)); if(htim->State == HAL_TIM_STATE_RESET) { /* Allocate lock resource and initialize it */ htim->Lock = HAL_UNLOCKED; /* Init the low level hardware : GPIO, CLOCK, NVIC */ HAL_TIM_Base_MspInit(htim); } /* Set the TIM state */ htim->State= HAL_TIM_STATE_BUSY; /* Set the Time Base configuration */ TIM_Base_SetConfig(htim->Instance, &htim->Init); /* Initialize the TIM state*/ htim->State= HAL_TIM_STATE_READY; return HAL_OK; }
Після вже звичної перевірки параметрів, вона дивиться, чи даний таймер було хоча б раз ініціалізовано протягом цього запуску. Якщо ні -- викликає callback-функцію HAL_TIM_Base_MspInit(), перевизначення якої (все та ж магія weak-символів) знаходиться в "нашому" tim.c. Після цього помічається таймер як зайнятий, конфігурується за допомогою функції TIM_Base_SetConfig(), якій передається вказівник на структуру, що відображається на регістри таймера та структуру із даними для конфігурування та знову помічається як доступний.
HAL_TIM_Base_MspInit() єдина і для TIM6 і для TIM1, наведемо її тут:
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) { GPIO_InitTypeDef GPIO_InitStruct; if(htim_base->Instance==TIM1) { /* USER CODE BEGIN TIM1_MspInit 0 */ /* USER CODE END TIM1_MspInit 0 */ /* Peripheral clock enable */ __TIM1_CLK_ENABLE(); /* Peripheral interrupt init*/ HAL_NVIC_SetPriority(TIM1_UP_TIM16_IRQn, 0, 0); HAL_NVIC_EnableIRQ(TIM1_UP_TIM16_IRQn); /* USER CODE BEGIN TIM1_MspInit 1 */ /* USER CODE END TIM1_MspInit 1 */ } else if(htim_base->Instance==TIM6) { /* USER CODE BEGIN TIM6_MspInit 0 */ /* USER CODE END TIM6_MspInit 0 */ /* Peripheral clock enable */ __TIM6_CLK_ENABLE(); /* Peripheral interrupt init*/ HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 0, 0); HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); /* USER CODE BEGIN TIM6_MspInit 1 */ /* USER CODE END TIM6_MspInit 1 */ } }
Сподіваюся, на даному етапі детальніше її розбирати немає потреби -- дозволяється тактування, налаштовуються переривання. Як побачимо в майбутньому -- тут же знаходитиметься ініціалізація пінів, якими безпосередньо керуватимуть таймери. Макрос __TIM6_CLK_ENABLE теж знаходиться в stm32_hal_legacy.h. Він еквівалентний __HAL_RCC_TIM6_CLK_ENABLE, як і розглянутий вище аналогічний макрос для GPIO. Робить він, в основному, наступне:
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM6EN);
Знайома конструкція. :-)
TIM_Base_SetConfig() із HAL, дотягується, нарешті, до рівня абстракції CMSIS (по простому -- заліза):
/** * @brief Time Base configuration * @param TIMx : TIM periheral * @param Structure : TIM Base configuration structure * @retval None */ void TIM_Base_SetConfig(TIM_TypeDef *TIMx, TIM_Base_InitTypeDef *Structure) { uint32_t tmpcr1 = 0; tmpcr1 = TIMx->CR1; /* Set TIM Time Base Unit parameters ---------------------------------------*/ if (IS_TIM_COUNTER_MODE_SELECT_INSTANCE(TIMx)) /* Істина для TIM1/2/3/4 */ { /* Select the Counter Mode */ tmpcr1 &= ~(TIM_CR1_DIR | TIM_CR1_CMS); tmpcr1 |= Structure->CounterMode; } if(IS_TIM_CLOCK_DIVISION_INSTANCE(TIMx)) /* Істина для TIM1/2/3/4/15/16/17 */ { /* Set the clock division */ tmpcr1 &= ~TIM_CR1_CKD; tmpcr1 |= (uint32_t)Structure->ClockDivision; } TIMx->CR1 = tmpcr1; /* Set the Autoreload value */ TIMx->ARR = (uint32_t)Structure->Period ; /* Set the Prescaler value */ TIMx->PSC = (uint32_t)Structure->Prescaler; if (IS_TIM_REPETITION_COUNTER_INSTANCE(TIMx)) /* Істина для TIM1/15/16/17 */ { /* Set the Repetition Counter value */ TIMx->RCR = Structure->RepetitionCounter; } /* Generate an update event to reload the Prescaler and the repetition counter(only for TIM1 and TIM8) value immediatly */ TIMx->EGR = TIM_EGR_UG; }
Вона, як не дивно, зовсім простенька. Якщо таймер підтримує різні напрямки відліку, подільник, повтори (TIM1 --- все перераховане підтримує, TIM6 -- все ні), вони налаштовуються, інакше, як і належить, відповідні біти не зачіпаються. Встановлюється, безпосередньо, вміст регістрів ARR та PSC. Майже все. (Див. також коментарі.)
Навіщо отак багато тексту та прикладів вище? Щоб показати, як магія абстракцій HAL плавно перетікає в магію окремих бітів мікроконтролера. Розуміння цього дозволить максимально ефективно суміщати обидва підходи.
Про обробку переривань -- трішки нижче, а поки --- ініціалізація TIM1.
Ініціалізуємо просунутий TIM1 -- MX_TIM1_Init()
TIM_HandleTypeDef htim1; /* TIM1 init function */ void MX_TIM1_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig; TIM_MasterConfigTypeDef sMasterConfig; htim1.Instance = TIM1; htim1.Init.Prescaler = 23999; htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 300; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; HAL_TIM_Base_Init(&htim1); sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig); sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig); }
Основна відмінність -- доналаштовуються можливості TIM1, відсутні в TIM6 -- кількість повторів рівна нулю, джерело тактування -- внутрішнє. Все. (Про HAL_TIM_ConfigClockSource() ще трішки згадаємо в одному із наступних постів.)
Обробка переривань від таймерів
Починається вона у нашому stm32f1xx_it.c:/** * @brief This function handles TIM1 update interrupt and TIM16 global interrupt. */ void TIM1_UP_TIM16_IRQHandler(void) { /* USER CODE BEGIN TIM1_UP_TIM16_IRQn 0 */ /* USER CODE END TIM1_UP_TIM16_IRQn 0 */ HAL_TIM_IRQHandler(&htim1); /* USER CODE BEGIN TIM1_UP_TIM16_IRQn 1 */ /* USER CODE END TIM1_UP_TIM16_IRQn 1 */ } /** * @brief This function handles TIM6 global interrupt and DAC underrun error interrupts. */ void TIM6_DAC_IRQHandler(void) { /* USER CODE BEGIN TIM6_DAC_IRQn 0 */ /* USER CODE END TIM6_DAC_IRQn 0 */ HAL_TIM_IRQHandler(&htim6); /* USER CODE BEGIN TIM6_DAC_IRQn 1 */ /* USER CODE END TIM6_DAC_IRQn 1 */ }
Як бачимо, переривання від обох таймерів (при чому, для TIM1 це одне із чотирьох можливих, але решта мали б бути забороненими) опрацьовуються однією і тією ж "мегафункцією" HAL_TIM_IRQHandler(), хоч можна також вписувати свій код. Доведеться розглядати і її.
Знаходиться ця функція в Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_tim.c, і доволі велика, її розмір аж дещо лякає, адже вона викликатиметься за кожного переривання. Розглядатимемо її по частинах. Більшість подій, що вона обробляє, не актуальні для цього поста, однак знадобляться в подальшому.
Частина перша -- оголошення і реакція на capture compare:
/** * @brief This function handles TIM interrupts requests. * @param htim : TIM handle * @retval None */ void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim) { /* Capture compare 1 event */ if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC1) != RESET) { if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_CC1) !=RESET) { { __HAL_TIM_CLEAR_IT(htim, TIM_IT_CC1); htim->Channel = HAL_TIM_ACTIVE_CHANNEL_1; /* Input capture event */ if((htim->Instance->CCMR1 & TIM_CCMR1_CC1S) != 0x00) { HAL_TIM_IC_CaptureCallback(htim); } /* Output compare event */ else { HAL_TIM_OC_DelayElapsedCallback(htim); HAL_TIM_PWM_PulseFinishedCallback(htim); } htim->Channel = HAL_TIM_ACTIVE_CHANNEL_CLEARED; } } } /* Capture compare 2 event */ //<......................SKIPPED..........................> /* Capture compare 3 event */ //<......................SKIPPED..........................> /* Capture compare 4 event */ //<......................SKIPPED..........................>
Однаковий код виконується для кожного із каналів, розглядаємо тільки перший. Спочатку перевіряється, чи є така подія, перевіряючи біт TIM_FLAG_CC1 із регістра статусу TIMx_SR. Макрос __HAL_TIM_GET_FLAG:
#define __HAL_TIM_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR &(__FLAG__)) == (__FLAG__))
Якщо так, перевіряється чи відбулося відповідне переривання -- чи встановлено біт TIM_IT_CC1 із регістра TIMx_DIER:
#define __HAL_TIM_GET_IT_SOURCE(__HANDLE__, __INTERRUPT__) \
((((__HANDLE__)->Instance->DIER & (__INTERRUPT__)) == (__INTERRUPT__)) ? SET : RESET)
Якщо так, приступається для обробки. Спочатку, за допомогою __HAL_TIM_CLEAR_IT, очищається прапорець переривання:
#define __HAL_TIM_CLEAR_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->SR = ~(__INTERRUPT__))
Активний канал вказується в структурі, що описує таймер, далі викликаються (слабкі, як завжди) функції зворотного виклику із доволі очевидною назвою:
- HAL_TIM_IC_CaptureCallback(htim), якщо відбувається захоплення вводу,
- HAL_TIM_OC_DelayElapsedCallback(htim) та HAL_TIM_PWM_PulseFinishedCallback(htim), якщо порівняння і вивід.
Частина друга -- подія оновлення:
/* TIM Update event */ if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET) { if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) !=RESET) { __HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE); HAL_TIM_PeriodElapsedCallback(htim); } }
Аналогічно до першої частини, перевіряється в регістрі статусу, чи була подія (біт TIM_FLAG_UPDATE регістра статусу), потім -- чи було від неї переривання (біт TIM_IT_UPDATE з TIMx_DIER), якщо так -- переривання очищається та викликається функція зворотного виклику, якою ми вже користувалися вище:
- HAL_TIM_PeriodElapsedCallback(htim)
/* TIM Break input event */ if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_BREAK) != RESET) { if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_BREAK) !=RESET) { __HAL_TIM_CLEAR_IT(htim, TIM_IT_BREAK); HAL_TIMEx_BreakCallback(htim); } } /* TIM Trigger detection event */ if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_TRIGGER) != RESET) { if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_TRIGGER) !=RESET) { __HAL_TIM_CLEAR_IT(htim, TIM_IT_TRIGGER); HAL_TIM_TriggerCallback(htim); } } /* TIM commutation event */ if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_COM) != RESET) { if(__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_COM) !=RESET) { __HAL_TIM_CLEAR_IT(htim, TIM_FLAG_COM); HAL_TIMEx_CommutationCallback(htim); } } } /* кінець HAL_TIM_IRQHandler() */
Схема їх роботи та ж. Викликаються, відповідно, наступні функції зворотного виклику:
- HAL_TIMEx_BreakCallback(htim)
- HAL_TIM_TriggerCallback(htim)
- HAL_TIMEx_CommutationCallback(htim)
Початок ініціалізації --- HAL_Init()
Розглядаючи автоматично згенеровану ініціалізацію, ми пропустили, для простоти, функцію, яка викликається першою -- HAL_Init().Тепер коротко подивимося і на неї:
HAL_StatusTypeDef HAL_Init(void) { /* Configure Flash prefetch */ #if (PREFETCH_ENABLE != 0) #if defined(STM32F101x6) || defined(STM32F101xB) || defined(STM32F101xE) || defined(STM32F101xG) || \ defined(STM32F102x6) || defined(STM32F102xB) || \ defined(STM32F103x6) || defined(STM32F103xB) || defined(STM32F103xE) || defined(STM32F103xG) || \ defined(STM32F105xC) || defined(STM32F107xC) /* Prefetch buffer is not available on value line devices */ __HAL_FLASH_PREFETCH_BUFFER_ENABLE(); #endif #endif /* PREFETCH_ENABLE */ /* Set Interrupt Group Priority */ HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* Use systick as time base source and configure 1ms tick (default clock after Reset is MSI) */ HAL_InitTick(TICK_INT_PRIORITY); /* Init the low level hardware */ HAL_MspInit(); /* Return function status */ return HAL_OK; }
Вона, вмикає перед-вибірку з flash, якщо контролер її підтримує (STM32F100 -- ні), налаштовує поділ пріоритетів переривань на групі і підгрупи, ініціалізує SysTick для потреб HAL, (спрацювання раз на мілісекунду, потім воно буде замінене на нову конфігурацію, викликом розглянутої вище SystemClock_Config()) та викликає функцію зворотного виклику ініціалізації периферії -- HAL_MspInit(). В проекті Cube вона заміщається (weak символ) у файлі stm32f1xx_hal_msp.c :
void HAL_MspInit(void) { /* USER CODE BEGIN MspInit 0 */ /* USER CODE END MspInit 0 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* System interrupt init*/ /* SysTick_IRQn interrupt configuration */ HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); /* USER CODE BEGIN MspInit 1 */ /* USER CODE END MspInit 1 */ }
Яка, в принципі, повторює те ж. Основна відмінність -- цю конфігурацію можна змінювати в Cube, а HAL_Init() використовує типову, безпечну, конфігурацію, достатню для початкової ініціалізації контролера.
Деякі функції маніпуляції контролером
Раз вже HAL тут розглядається так детально, подивимося на використані в проекті функції маніпуляції його станом, периферією -- HAL_TIM_Base_Start_IT() , HAL_GPIO_TogglePin() та пов'язані із ними функції.HAL_TIM_Base_Start_IT()
Ця функція запускає таймер, дозволивши переривання:HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim) { /* Check the parameters */ assert_param(IS_TIM_INSTANCE(htim->Instance)); /* Enable the TIM Update interrupt */ __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE); /* Enable the Peripheral */ __HAL_TIM_ENABLE(htim); /* Return function status */ return HAL_OK; }
Перевіряється, а чи справді передано таймер (а не просто некоректний вказівник), після чого дозволяється, відповідними макросами, переривання по оновленню для цього таймера та сам таймер. Макроси, разом із своїми протилежностями, корисні й самі по собі:
#define __HAL_TIM_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->DIER |= (__INTERRUPT__)) #define __HAL_TIM_DISABLE_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->DIER &= ~(__INTERRUPT__)) #define __HAL_TIM_ENABLE(__HANDLE__) ((__HANDLE__)->Instance->CR1|=(TIM_CR1_CEN)) #define __HAL_TIM_DISABLE(__HANDLE__) \ do { \ if (((__HANDLE__)->Instance->CCER & TIM_CCER_CCxE_MASK) == 0) \ { \ if(((__HANDLE__)->Instance->CCER & TIM_CCER_CCxNE_MASK) == 0) \ { \ (__HANDLE__)->Instance->CR1 &= ~(TIM_CR1_CEN); \ } \ } \ } while(0)
Останній макрос трохи складніший, так як таймер можна вимикати/забороняти лише тоді, коли всі його канали заборонено:
#define TIM_CCER_CCxE_MASK ((uint32_t)(TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC3E | TIM_CCER_CC4E)) #define TIM_CCER_CCxNE_MASK ((uint32_t)(TIM_CCER_CC1NE | TIM_CCER_CC2NE | TIM_CCER_CC3NE))
Симетрична функція зупинки таймера теж проста:
HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim) { /* Check the parameters */ assert_param(IS_TIM_INSTANCE(htim->Instance)); /* Disable the TIM Update interrupt */ __HAL_TIM_DISABLE_IT(htim, TIM_IT_UPDATE); /* Disable the Peripheral */ __HAL_TIM_DISABLE(htim); /* Return function status */ return HAL_OK; }
Є ще два варіанти функцій запуску таймерів -- HAL_TIM_Base_Start()/HAL_TIM_Base_Stop(), які запускають/зупиняють відлік без вмикання переривань та HAL_TIM_Base_Start_DMA()/HAL_TIM_Base_Stop_DMA(), які вмикають таймер в режимі обміну з використанням DMA. Останні функції складніші, через необхідність маніпуляцій з DMA, не розглядатимемо їх поки.
Функції маніпуляції пінами
HAL_GPIO_TogglePin(), що використовується в коді вище, переключає значення піну на протилежне до початкового:void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); GPIOx->ODR ^= GPIO_Pin; }
Десь вже це було... :-)
HAL_GPIO_WritePin(), атомарно встановлює значення піну:
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) { /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); assert_param(IS_GPIO_PIN_ACTION(PinState)); if(PinState != GPIO_PIN_RESET) { GPIOx->BSRR = GPIO_Pin; } else { GPIOx->BSRR = (uint32_t)GPIO_Pin << 16; } }
Встановлює в 1, користуючись нижньою частиною регістра BSRR, в нуль -- верхньою.
HAL_GPIO_ReadPin(), читає пін:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { GPIO_PinState bitstatus; /* Check the parameters */ assert_param(IS_GPIO_PIN(GPIO_Pin)); if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET) { bitstatus = GPIO_PIN_SET; } else { bitstatus = GPIO_PIN_RESET; } return bitstatus; }
Як відомо, регістр GPIOx_IDR містить реальний стан піна, їх вміст оновлюється раз на такт шини APB2, до якої порти GPIO і під'єднані.
Завершальне слово
Повний main.c не наводжу -- весь код із нього описано, а виглядає він, через коментарі Cube, громіздко.Скачати проект можна тут.
Поки все, в наступній парі постів (CMSIS/HAL) розглянемо генерацію ШІМ (PWM).
Скажіть мені stm32f1 таймера регістрів с регістрами серії stm32f4
ВідповістиВидалитисхожі чи ні?