Як вже говорилося, CMSIS -- занадто низькорівнева, SPL від неї не далеко втекла. Після років роздумів та роботи, інженери STMicroelecronics випустили чергову спробу --- пакет STM32Cube. Він складається із двох частин:
- Коду для мікроконтролерів: бібліотеки, призначеної замінити SPL -- HAL, (Hardware Abstraction Layer) та всіляких middleware ("проміжних") компонент -- бібліотеку підтримки FAT, бібліотеку для роботи із USB, TCP/IP стек, RTOS, тощо.
- STM32CubeMX -- програми для комп'ютера, яка дозволяє графічно конфігурувати периферію, генеруючи код мовою С, який реалізує "намальоване".
Після кількох років дозрівання проект став дуже потужним. Особисті враження: як мало треба для щастя -- лише не виставляти ті сотні бітиків вручну, медитуючи над даташітами і аппнотами (appnotes).
Подивимося, як із ним працювати, на прикладі нашого простенького коду роботи із далекоміром.
Інсталяція
Скачати STM32CubeMX можна тут. Інсталяцію детально не розглядатиму, вона менш більш типова. Зауважу тільки, що проект модульний -- складається із самої програми та бібліотек для конкретних сімейств мікроконтролерів: STM32F1, STM32F3, STM32F4, STM32L1, STM32F0, і т.д. (повний список див. за посиланням):
Пункт меню із керуванням оновленнями та вікно із списком бібліотек STM32CubeMX. |
Відповідно, заінсталювавши сам "Cube", слід встановити бібліотеки для тих сімейств, із якими планується працювати. Як видно із рисунка вище, встановлення бібліотек, а також оновлень, є достатньо тривіальним. Доступна навіть можливість встановлення старіших версій бібліотек.
Всі зображення клікабельні! На жаль, вони або замалі, щоб щось можна було прочитати, або не вміщаються в ширину колонки.
Всі зображення клікабельні! На жаль, вони або замалі, щоб щось можна було прочитати, або не вміщаються в ширину колонки.
Створення проекту
Початок стандартний -- "New Project". При тому програма дозволяє вибрати мікроконтролер або плату, із списку підтримуваних. Пошук спрощується великою колекцією фільтрів:
Отримаємо таку картинку:
Є чотири вкладки. Перша, та що відкрита на рисунку вище, дозволяє вибрати налаштування периферії, яку слід ініціалізувати.
Для цього проекту STM32CubeMX -- як з гармати по горобцях, але для демонстрації зійде. Отож, нам потрібні:
Друга вкладка -- Clock Configuration. Тут варто налаштувати тактування, щоб добитися максимальної частоти, 24МГц:
Зауважте, яка велика кількість всіляких подільників та множників частот. :-)
Наступний етап -- конфігурування обраних компонент, вкладка Configuration:
В принципі, через простоту проекту, тут все вже готове, але глянути може бути цікаво.
Налаштування NVIC та GPIO:
Остання вкладка займається розрахунком витрат електроенергії, на ній поки не зупинятимемося.
Для більшості вкладок, в головному меню з'являється відповідний пункт із додатковими можливостями. Поміж них варто відзначити можливість генерації звіту по використанню пінів, в форматі CSV.
Коли графічне налаштування завершено, можна згенерувати проект, за допомогою пункту Project->Generate Code. Вікно має дві вкладки. Перша -- Project:
На ній можна вибрати назву проекту (це буде ім'я директорії, тому без пробілів, кирилиці чи спецсимволів!, туди ж збереже сам файл проекту "Cube" -- .ioc.), шлях до нього та середовище, для якого генерувати проект. Донедавна тут були лише комерційні оболонки. Зараз з'явилася безкоштовна, на базі Eclipse -- System Workbench for STM32 (SW4STM32). Його і обираємо -- хоча б з тієї точки зору, що схрестити з CoIDE простіше буде. Хоч це і збочення, anyway. :-)
Друга вкладка, Code Generator:
В порівнянні із варіантом по замовчуванню, вибрано "Enable Full Assert" -- це може бути корисно, особливо на початках, та "Backup previously generated files when re-generating" -- не зважаючи на обіцянки, іноді має звичку прибити користувацький код.
Дерево проекту виглядає так:
Нам знадобиться:
Крім того, Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc містить startup-файли для різних контролерів. Нам підійшов би startup_stm32f100xb.s. Однак, порівнявши його із своїм, про який мова йшла тут, вирішив залишити свій. Але різниця між ними невелика. Файл із бібліотеки CUBE викликає додаткову ініціалізацію:
Що конкретно являє собою ця ініціалізація, яку частку із необхідної, здійснює, поговоримо окремо.
В моєму, старішому за походженням, проекті, адреса початкового стеку називається _eram, в новішому _estack, але це ім'я задається скриптом лінкера (який у нас також свій). Кілька імен обробників переривань відрізняються: було RTCAlarm_IRQHandler, стало RTC_Alarm_IRQHandler, і двічі щось, що видається мені помилкою. В старішому:
.weak I2C1_ER_IRQHandler
.thumb_set I2C1_ER_IRQHandler,Default_Handler
.weak I2C2_EV_IRQHandler
.thumb_set I2C2_EV_IRQHandler,Default_Handler
.weak I2C2_ER_IRQHandler
.thumb_set I2C2_ER_IRQHandler,Default_Handler
в новішому:
.weak I2C1_ER_IRQHandler
.thumb_set I2C1_ER_IRQHandler,Default_Handler
.weak I2C2_EV_IRQHandler
.thumb_set I2C1_EV_IRQHandler,Default_Handler
.weak I2C2_ER_IRQHandler
.thumb_set I2C1_ER_IRQHandler,Default_Handler
аналогічно для SPI2.
Отож, за мотивами попереднього абзацу, із проекту, описаного в попередніх постах про С чи С++, беремо startup-файл (startup_stm32f10x_md_vl.s), за потреби -- скрипт лінкера (), syscalls.c та runtime_config.h для підтримки стандартної бібліотеки С. Крім того, зручно залишити компоненту SemiHosting -- реалізація від CoIDE, при всій своїй простоті, достатньо зручна.
Також, слід оголосити (Configuration->Compile->Defined symbols) макрос STM32F100xB -- він використовується новішими версіями CMSIS для ідентифікації нашого мікроконтролера (на відміну від макросу STM32F100RBT6B, що використано в CMSIS, який йде із CoIDE).
Ще одним корисним макросом є USE_FULL_ASSERT -- він вкаже компілятору включати в прошивку код, який, за проблем із переданими аргументами чи некоректним використанням функцій HAL, виводитиме (користуючись перенаправлябельним -- retagretable, printf) відповідні повідомлення. Щоправда, він доволі часто спрацьовує і для коректних, максиму -- дещо нетипових, параметрів... Сподіваюся, діагностику покращать в наступних версіях.
Згенерований main.c виглядає так (частину коду викинуто, для зменшення об'єму посту):
Видно купу секцій USER CODE BEGIN / USER CODE END, відмічених коментарями. Думаю, слід дослухатися до їх рекомендацій -- CUBE, оновлюючи проект, їх поважає, залишаючи код у цих секціях без змін (резервні копії краще мати, звичайно).
Послідовно викликаються функції HAL_Init(), SystemClock_Config(), MX_GPIO_Init(), які ініціалізують периферію, налаштовану графічним інструментом. Про них поговоримо трішки пізніше. Далі йде while(1) -- класичний нескінчений цикл, (хитро розбитий секціями USER CODE на дві частини). В кінці -- реалізація функції assert_failed(), в якій можна організувати вивід інформації про проблеми зручним для програміста способом (скажімо, за допомогою UART чи SemiHosting).
Функція HAL_Init() знаходиться у файлі stm32f1xx_hal.c, її призначення: налаштувати вибірку із випередженням з флеш-пам'яті (Flash prefetch), якщо така є, конфігурування системного таймера та задати початкові налаштування NVIC. Зокрема, HAL використовує SysTick для відліку часу та затримок, тому, по замовчуванню, включається генерація його переривань кожну мілісекунду.
SystemClock_Config() знаходиться в main() і здійснює специфічне для проекту налаштування:
В нашому випадку -- встановлює режим тактування, який описано вище, "підтверджує" налаштування SysTick, встановлює пріоритети переривань (зараз -- тільки для SysTick).
MX_GPIO_Init() налаштовує піни згідно обраної конфігурації:
Код достатньо очевидний. GPIO_InitStruct знайома ще по SPL, код функції HAL_GPIO_Init знаходиться в Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c, він завеликий, щоб я його тут наводив, але дуже рекомендую подивитися!
В цій реалізації роботи із HC-SR04, по великому рахунку, нам потрібні три речі -- можливість змінити значення піна, можливість прочитати логічний рівень на ньому та можливість відліку часу.
Для читання використовується функція HAL_GPIO_ReadPin(). Для даного мікроконтролера (і у цій версії HAL) вона виглядає так:
GPIO_PinState -- перерахування, яке має два можливих значення, GPIO_PIN_SET та GPIO_PIN_RESET. Як видно, ніякої особливої магії, в порівнянні із SPL, немає -- все те ж звертання до регістру IDR відповідного порту із використанням переданої маски. Вартує уваги хіба перевірка, за допомогою assert_param(), чи справді передані номери можуть кодують піни (перевірка зовсім проста, але, все ж, частину помилок здатна відловити).
Користуватися цією функцією можна так:
Так як GPIO_PIN_RESET==0, GPIO_PIN_SET==1, можна скористатися лінивим (хоч і не рекомендованим STM!) способом:
Аналогічно, для зміни стану піна можна використати HAL_GPIO_WritePin():
Ось з відліком часу складніше. HAL налаштовує таймер для генерації тіків кожної мілісекунди, надаючи доступ до поточного значення лічильника функцією HAL_GetTick(), (а також, функцію HAL_Delay(), яка створює затримку на передану їй кількість мілісекунд, вважаючи, що SysTick сконфігурований на спрацювання раз в мілісекунду).
Однак, для нас це завеликий інтервал -- потрібен доступ до замірів інтервалів часу, співмірних із мікросекундами. Зробити це можна різними способами, найбільш акуратний, напевне, скористатися іншим таймером. Однак, в цьому проекті, для простоти, я зробив дещо негарно -- просто змінив частоту спрацювання SysTick() -- в функції SystemClock_Config(), замість HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000) вставив вже звичне по іншим варіантам цього проекту:
Пам'ятайте, робити так -- потенційно небезпечно! Код HAL покладається на те, що таймер спрацьовує раз в мілісекунду. В даному, простому, випадку, можна легко перевірити, що для нашого простого проекту нічого не псується, але будьте обережні.
Як би там не було, в порівнянні із іншими проектами, особливо, якщо компілювати без оптимізації, розмір коду зріс і контролер не встигає тепер опрацьовувати переривання кожних дві мікросекунди. Хіба -- кожних три-чотири, тому MICROSECONDS_GRANULARITY встановлено 4. На кінцевий результат це не дуже сильно впливає.
Тепер наш код виглядає так:
Скачати відповідний демонстраційний проект можна тут (містить купу зайвого з HAL, зокрема -- шаблони).
Приклад вибору контролера. Для демонстрації, ввімкнено всі додаткові фільтри. Клікабельно! |
Головне вікно проекту. Клікабельно! |
Для цього проекту STM32CubeMX -- як з гармати по горобцях, але для демонстрації зійде. Отож, нам потрібні:
- SysTick
- PB1 -- Trig, вивід
- PB2 -- Echo, ввід,
- PC8 -- вивід, синій світлодіод
- PC9 -- вивід, зелений світлодіод
- Крім того, варто сконфігурувати тактування від зовнішнього кварцу (підсистема RCC).
Приклад конфлікту по пінах, разом із описом. Він не є проблемою -- ми тут ADC не користуємося. |
Тактування мікроконтролера. Все зроблено тривіально -- зовнішнє високошвидкісне джерело (HSI), від кварцу 8МГц, через помножувач (PLL -- Phase-locked loop, або система фазового автопідстроювання частоти), вибравши в ролі системного тактового сигналу -- PLL, до SYSCLK і далі, до високошвидкісної шини AHB. За потреби можна налаштовувати все, що налаштовується. Клікабельно! |
Зауважте, яка велика кількість всіляких подільників та множників частот. :-)
Наступний етап -- конфігурування обраних компонент, вкладка Configuration:
Налаштування периферії. Клікабельно! |
Налаштування NVIC та GPIO:
Переривання від SysTick дозволені. Видно можливість налаштування пріоритетів та груп пріоритетів переривань. Клікабельно! |
Налаштування режиму окремих пінів GPIO, (вкладка RCC зараз нецікава -- там налаштування пінів, до яких під'єднано кварц). Можна також давати пінам назви. Клікабельно! |
Для більшості вкладок, в головному меню з'являється відповідний пункт із додатковими можливостями. Поміж них варто відзначити можливість генерації звіту по використанню пінів, в форматі CSV.
Коли графічне налаштування завершено, можна згенерувати проект, за допомогою пункту Project->Generate Code. Вікно має дві вкладки. Перша -- Project:
Вкладка Project діалогу створення проекту. |
Друга вкладка, Code Generator:
Налаштування генерації коду. |
CoIDE
Звичайно, розумнішим було б скористатися згаданим System Workbench for STM32 для подальшої роботи. Це й перегенерацію проекту спростить -- якщо треба буде щось в "Cube" виправити. Але про нього напишу окремо, а зараз, для одноманітності викладу, скористаємося CoIDE.Дерево проекту виглядає так:
Нам знадобиться:
- Вмісти директорії Drivers/CMSIS/Device/ST/STM32F1xx/Include
- Вмісти директорії Drivers/CMSIS/Inlcude
- Файл Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c
- Вміст Drivers/STM32F1xx_HAL_Driver/Inc, крім вмісту Legacy (просто не потрібне),
- Вміст Drivers/STM32F1xx_HAL_Driver/Src, крім stm32f1xx_hal_msp_template.c. Цей файл -- просто шаблон.
- Вміст Inc та Src -- власне, коду, згенерованого STM32CubeMX для нашого проекту (все вище -- CMSIS та HAL). Зокрема, тут знаходиться stm32f1xx_hal_msp.c, згенерований із шаблону, згаданого вище -- якщо шаблон, stm32f1xx_hal_msp_template.c, залишити -- будуть проблеми лінкування.
Крім того, Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc містить startup-файли для різних контролерів. Нам підійшов би startup_stm32f100xb.s. Однак, порівнявши його із своїм, про який мова йшла тут, вирішив залишити свій. Але різниця між ними невелика. Файл із бібліотеки CUBE викликає додаткову ініціалізацію:
/* Call static constructors */ bl __libc_init_array
Що конкретно являє собою ця ініціалізація, яку частку із необхідної, здійснює, поговоримо окремо.
В моєму, старішому за походженням, проекті, адреса початкового стеку називається _eram, в новішому _estack, але це ім'я задається скриптом лінкера (який у нас також свій). Кілька імен обробників переривань відрізняються: було RTCAlarm_IRQHandler, стало RTC_Alarm_IRQHandler, і двічі щось, що видається мені помилкою. В старішому:
.weak I2C1_ER_IRQHandler
.thumb_set I2C1_ER_IRQHandler,Default_Handler
.weak I2C2_EV_IRQHandler
.thumb_set I2C2_EV_IRQHandler,Default_Handler
.weak I2C2_ER_IRQHandler
.thumb_set I2C2_ER_IRQHandler,Default_Handler
в новішому:
.weak I2C1_ER_IRQHandler
.thumb_set I2C1_ER_IRQHandler,Default_Handler
.weak I2C2_EV_IRQHandler
.thumb_set I2C1_EV_IRQHandler,Default_Handler
.weak I2C2_ER_IRQHandler
.thumb_set I2C1_ER_IRQHandler,Default_Handler
аналогічно для SPI2.
Отож, за мотивами попереднього абзацу, із проекту, описаного в попередніх постах про С чи С++, беремо startup-файл (startup_stm32f10x_md_vl.s), за потреби -- скрипт лінкера (), syscalls.c та runtime_config.h для підтримки стандартної бібліотеки С. Крім того, зручно залишити компоненту SemiHosting -- реалізація від CoIDE, при всій своїй простоті, достатньо зручна.
Також, слід оголосити (Configuration->Compile->Defined symbols) макрос STM32F100xB -- він використовується новішими версіями CMSIS для ідентифікації нашого мікроконтролера (на відміну від макросу STM32F100RBT6B, що використано в CMSIS, який йде із CoIDE).
Ще одним корисним макросом є USE_FULL_ASSERT -- він вкаже компілятору включати в прошивку код, який, за проблем із переданими аргументами чи некоректним використанням функцій HAL, виводитиме (користуючись перенаправлябельним -- retagretable, printf) відповідні повідомлення. Щоправда, він доволі часто спрацьовує і для коректних, максиму -- дещо нетипових, параметрів... Сподіваюся, діагностику покращать в наступних версіях.
Загальний вигляд коду
Згенерований main.c виглядає так (частину коду викинуто, для зменшення об'єму посту):
/* Includes ------------------------------------------------------------------*/ #include "stm32f1xx_hal.h" /* USER CODE BEGIN Includes */ /* USER CODE END Includes */ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* Private variables ---------------------------------------------------------*/ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); /* USER CODE BEGIN PFP */ /* Private function prototypes -----------------------------------------------*/ /* USER CODE END PFP */ /* USER CODE BEGIN 0 */ /* USER CODE END 0 */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* 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(); /* USER CODE BEGIN 2 */ /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** System Clock Configuration */ void SystemClock_Config(void) { // Тут купа коду, викинув заради лаконічності, див. нижче та файли проекту. } /** Configure pins as * Analog * Input * Output * EVENT_OUT * EXTI */ void MX_GPIO_Init(void) { // Тут купа коду, викинув заради лаконічності, див. нижче та файли проекту. } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ #ifdef USE_FULL_ASSERT /** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */ void assert_failed(uint8_t* file, uint32_t line) { /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ } #endif /** * @} */ /** * @} */ /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
Видно купу секцій USER CODE BEGIN / USER CODE END, відмічених коментарями. Думаю, слід дослухатися до їх рекомендацій -- CUBE, оновлюючи проект, їх поважає, залишаючи код у цих секціях без змін (резервні копії краще мати, звичайно).
Послідовно викликаються функції HAL_Init(), SystemClock_Config(), MX_GPIO_Init(), які ініціалізують периферію, налаштовану графічним інструментом. Про них поговоримо трішки пізніше. Далі йде while(1) -- класичний нескінчений цикл, (хитро розбитий секціями USER CODE на дві частини). В кінці -- реалізація функції assert_failed(), в якій можна організувати вивід інформації про проблеми зручним для програміста способом (скажімо, за допомогою UART чи SemiHosting).
Функція HAL_Init() знаходиться у файлі stm32f1xx_hal.c, її призначення: налаштувати вибірку із випередженням з флеш-пам'яті (Flash prefetch), якщо така є, конфігурування системного таймера та задати початкові налаштування NVIC. Зокрема, HAL використовує SysTick для відліку часу та затримок, тому, по замовчуванню, включається генерація його переривань кожну мілісекунду.
SystemClock_Config() знаходиться в main() і здійснює специфічне для проекту налаштування:
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); }
В нашому випадку -- встановлює режим тактування, який описано вище, "підтверджує" налаштування SysTick, встановлює пріоритети переривань (зараз -- тільки для SysTick).
MX_GPIO_Init() налаштовує піни згідно обраної конфігурації:
void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; /* GPIO Ports Clock Enable */ __GPIOD_CLK_ENABLE(); __GPIOB_CLK_ENABLE(); __GPIOC_CLK_ENABLE(); /*Configure GPIO pin : HC_SR04_Trig_Pin */ GPIO_InitStruct.Pin = HC_SR04_Trig_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_LOW; HAL_GPIO_Init(HC_SR04_Trig_GPIO_Port, &GPIO_InitStruct); /*Configure GPIO pin : HC_SR04_Echo_Pin */ GPIO_InitStruct.Pin = HC_SR04_Echo_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(HC_SR04_Echo_GPIO_Port, &GPIO_InitStruct); /*Configure GPIO pins : Blue_LED_Pin Red_LED_Pin */ GPIO_InitStruct.Pin = Blue_LED_Pin|Red_LED_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); }
Код достатньо очевидний. GPIO_InitStruct знайома ще по SPL, код функції HAL_GPIO_Init знаходиться в Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c, він завеликий, щоб я його тут наводив, але дуже рекомендую подивитися!
GPIO
В цій реалізації роботи із HC-SR04, по великому рахунку, нам потрібні три речі -- можливість змінити значення піна, можливість прочитати логічний рівень на ньому та можливість відліку часу.
Для читання використовується функція HAL_GPIO_ReadPin(). Для даного мікроконтролера (і у цій версії HAL) вона виглядає так:
/** * @brief Reads the specified input port pin. * @param GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral * @param GPIO_Pin: specifies the port bit to read. * This parameter can be GPIO_PIN_x where x can be (0..15). * @retval The input port pin value. */ 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; }
GPIO_PinState -- перерахування, яке має два можливих значення, GPIO_PIN_SET та GPIO_PIN_RESET. Як видно, ніякої особливої магії, в порівнянні із SPL, немає -- все те ж звертання до регістру IDR відповідного порту із використанням переданої маски. Вартує уваги хіба перевірка, за допомогою assert_param(), чи справді передані номери можуть кодують піни (перевірка зовсім проста, але, все ж, частину помилок здатна відловити).
Користуватися цією функцією можна так:
if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_SET)
Так як GPIO_PIN_RESET==0, GPIO_PIN_SET==1, можна скористатися лінивим (хоч і не рекомендованим STM!) способом:
if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) )
Аналогічно, для зміни стану піна можна використати HAL_GPIO_WritePin():
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
Ось з відліком часу складніше. HAL налаштовує таймер для генерації тіків кожної мілісекунди, надаючи доступ до поточного значення лічильника функцією HAL_GetTick(), (а також, функцію HAL_Delay(), яка створює затримку на передану їй кількість мілісекунд, вважаючи, що SysTick сконфігурований на спрацювання раз в мілісекунду).
Однак, для нас це завеликий інтервал -- потрібен доступ до замірів інтервалів часу, співмірних із мікросекундами. Зробити це можна різними способами, найбільш акуратний, напевне, скористатися іншим таймером. Однак, в цьому проекті, для простоти, я зробив дещо негарно -- просто змінив частоту спрацювання SysTick() -- в функції SystemClock_Config(), замість HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000) вставив вже звичне по іншим варіантам цього проекту:
HAL_SYSTICK_Config( TICKS );
Пам'ятайте, робити так -- потенційно небезпечно! Код HAL покладається на те, що таймер спрацьовує раз в мілісекунду. В даному, простому, випадку, можна легко перевірити, що для нашого простого проекту нічого не псується, але будьте обережні.
Як би там не було, в порівнянні із іншими проектами, особливо, якщо компілювати без оптимізації, розмір коду зріс і контролер не встигає тепер опрацьовувати переривання кожних дві мікросекунди. Хіба -- кожних три-чотири, тому MICROSECONDS_GRANULARITY встановлено 4. На кінцевий результат це не дуже сильно впливає.
Тепер наш код виглядає так:
int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* 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(); /* USER CODE BEGIN 2 */ // Перевіримо, чи лінія Echo в нулі поки ми ще не подали імпульс if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) ) { // Помилка -- імпульсу не було, а на Echo вже одиниця HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET); // Синім позначатимемо помилку printf("Error -- Echo line is high, though no impuls was given\n"); while(1); // Зависаємо } uint32_t starting_ticks; uint32_t timeout_ticks; uint32_t distance_mm; bool are_echoing=false; // For C needs <stdbool.h>! Some violation of strict C standard here. /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ //! Якщо не подавали сигнал - вперед, подаємо if(!are_echoing) { // Пауза між вимірами, двічі по 1/4 секунди delay_some_us(500000/2); // Гасимо світлодіоди HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_RESET); delay_some_us(500000/2); are_echoing=true; // Посилаємо імпульс HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); delay_some_us(12); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // Починаємо заміряти час timeout_ticks=HAL_GetTick(); }else{ // Якщо ж подали -- чекаємо на імпульс і заміряємо його тривалість bool measuring=false; uint32_t measured_time; // Скільки чекати, поки не вирішити, що імпульсу вже не буде uint32_t usoniq_timeout = 100000; while( (HAL_GetTick() - timeout_ticks) < usoniq_timeout ) { // Перевіряємо логічний рівень на Echo //printf("B->IDR %X\n",GPIOB->IDR); if( HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) ) { // Якщо раніше сигналу не було, починаємо заміряти його тривалість if( !measuring ) { starting_ticks=HAL_GetTick(); measuring=true; } }else{ // Якщо сигнал на Echo був і зник -- заміряємо його тривалість if(measuring) { // Результат буде в міліметрах measured_time = (HAL_GetTick() - starting_ticks)*10*MICROSECONDS_GRANULARITY; distance_mm = measured_time/58; // Повідомляємо про успішний вимір зеленим світлодіодом HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_SET); break; } } } if(!measuring) { // Не отримали сигналу, повідомляємо про помилку і пробуємо ще HAL_GPIO_WritePin(GPIOC, GPIO_PIN_8, GPIO_PIN_SET); printf("Echo signal not arrived in time\n"); }else{ printf("Distance: %u mm, measured time: %lu us\n",distance_mm, measured_time/10); } are_echoing=false; } } /* USER CODE END 3 */ }
Скачати відповідний демонстраційний проект можна тут (містить купу зайвого з HAL, зокрема -- шаблони).
Hello, I do not understand MICROSECONDS_GRANULARITY. Can you please explain. You have done a good job here. I am working with my STM32F4 Discovery board.
ВідповістиВидалитиHello!
ВидалитиReason for existance of this constant is the following. MCU(*) works at 24MHz, so it just cannot handle interrupts once a microsecond (with frequency 1MHz) --- just entering and exiting ISR takes 24 tacts. So I let it count every MICROSECONDS_GRANULARITY microseconds -- 2, 4 or more, lowering time resolution but leaving time for other work.
I must admit, that described here method of measuring time intervals, is inferior anyway. Using TIMx timers (subject of several other my posts) is much better. It's only advantage is simplicity. Most simple relevant post: http://indrekis2.blogspot.com/2016/10/stm32_20.html (using TIM6). More advanced: http://indrekis2.blogspot.com/2016/03/stm32-hal.html and http://indrekis2.blogspot.com/2016/03/stm32-cmsis.html. (I do understand language problem, but cannot do anything about it right now :-) But do not hesitate to ask.)
(*) Board STM32F1Discovery with STM32F100RB MCU was used for this posts.
Thank you for your feedback and praise.
I found it in a different place.
ВідповістиВидалити