четвер, 29 вересня 2016 р.

GPIO мікроконтролерів STM32F303 з використанням HAL

STM32F3Discovery
Виникла чисто практична потреба написати коротке введення в GPIO контролера STM32F303 плати STM32F3Discovery. Подумав, що є сенс зробити його в блозі. 
Серія про таймери, далекомір і продовження -- написана ще весною, але, на жаль, катастрофічно бракує часу довести її до ладу і опублікувати. Вірю, що скоро мені нарешті вдасться це зробити. Done. Див. відповідні теги: таймери, далекомір.
Працюватимемо із використанням візуального генератора конфігурації STM32CubeMX, бібліотеки роботи із периферією HAL та середовища System Workbench for STM32 (SW4STM32). 

Update 20-11-2016: виправлено прикру помилку -- додано заборону переривань при звертанні до змінних, що можуть бути модифіковані в їх обробниках. Прикру -- бо в таких простих прикладах вона себе проявляє рідко, але є дуже "фундаментальною", якщо можна так сказати про помилку.

Update 17-09-2017:  додано вирішення кількох типових проблем, що виникають при роботі із SW4STM32, оновлено пару посилань, дрібні стилістичні зміни.


Наведу декілька тематичних посилань. На жаль, із декомпозицією постів у мене не дуже добре -- часто охоплюються декілька різних питань, тому безпосередньо розглянутих тем може стосуватися лише (невелика) частина відповідних текстів:
  • Інсталяція STM32CubeMX описана тут: Далекомір HC-SR04 -- використовуючи GPIO/HAL/STM32CubeMX. На жаль, там використано інше, більш примітивне, хоч і теж на базі Eclipse, середовище -- CoIDE.
  • Там же є приклад конфігурування GPIO з використанням CubeMX та робота із ним з використанням HAL (розділ "GPIO" -- в проміжку між інсталяцією та цим розділом розглядається вміст згенерованого конфігуратором проекту).
  • System Workbench for STM32 -- офіційний сайт. На жаль, навіть для доступу до документації вимагає реєстрації.
  • Огляд STM32 (ARM Cortex-M від STMicroelectronics)  -- огляд архітектури мікроконтролерів сімейства STM32, щоправда на прикладі більш простого STM32F1. Зокрема, короткий огляд архітектури GPIO (розділ "GPIO -- порти вводу-виводу") та структури тактування, (розділ "Шини і тактові генератори"). Деталі для STM32F3 відрізняються, місцями -- суттєво, але загальні принципи ті ж
  • Низькорівнева робота із GPIO -- із безпосереднім використанням регістрів периферії, описана, зокрема, тут: "Далекомір HC-SR04 -- використовуючи GPIO/CMSIS". Однак, див. нижче -- для STM32F3 є суттєві відмінності.

 Завдання

  • Керування вбудованими світлодіодами плати. Наприклад, запрограмувати подання сигналу SOS одним із них, або рух вогників по колу.
  • Підключення зовнішніх світлодіодів. Розрахунок необхідних параметрів схеми.
  • Реакція на вбудовану кнопку. 
  • Підключення зовнішньої кнопки.
  • Використання переривань для реакції на натискання кнопки.

Теорія -- GPIO мікроконтролерів STM32F3

Детально описувати тут не буду -- можливо, колись напишу окремо, але якщо потрібно -- див. документацію. 

 Документація

Нагадаю, що документація STM32 трішки дивно організована. Якщо знати -- доволі зручно, якщо ні -- часто неможливо знайти необхідні документи. 
Головним документом нашого контролера є "RM0316 Reference manual: STM32F303xB/C/D/E, STM32F303x6/8, STM32F328x8, STM32F358xC, STM32F398xE advanced ARM®-based MCUs"

Конкретні особливості мікроконтролерів цієї групи описані в "STM32F303xB STM32F303xC: ARM®-based Cortex®-M4 32b MCU+FPU, up to 256KB Flash+ 48KB SRAM, 4 ADCs, 2 DAC ch., 7 comp, 4 PGA, timers, 2.0-3.6 V". Зазвичай, шукаючи, натрапляють саме на цей документ, але важливої інформації - будови підсистем, призначення регістрів та способів їх використання, у ньому якраз немає.

Крім того, є ще загальний документ про те, як в STM32 реалізували опціональні можливості ядра ARM Cortex M4F: "PM0214 Programming manual: STM32F3 and STM32F4 Series Cortex®-M4 programming manual".

Увага! Посилання постійно змінюється -- я вже здався і не виправляю в старих постах. Але за назвою завжди можна знайти.

Тактування

Підсистема, що відповідає за тактування, називається RCC -- Reset and clock control. Для генерації системної тактової частоти SYSCLK може використовуватися три джерела тактової частоти:
  • HSI -- "High-speed Internal", внутрішній RC-генератор на 8МГц. Доволі неточний -- частота може помітно відрізнятися від 8МГц та змінюватися в залежності від зовнішніх умов. 
  • HSE -- "High-Speed External", зовнішнє джерело. Найпростіший варіант -- кварц. На платі STM32F3Discovery є місце під зовнішній кварц, але він не встановлений. 
  • PLL ("Phase-locked loop") -- внутрішній блок, який множить тактову частоту від того чи іншого джерела (HSI/HSE) на заданий коефіцієнт  та передає далі.
  • Крім того є додаткові джерела тактування для "вартових" таймерів (watchdog) та RTC.
Дерево тактування  STM32F303xB/C та STM32F358xC.
Червоним кольором відмічено блоки, що мають безпосереднє відношення до теми цього поста. Порти GPIO під'єднані до шини AHB.
Синім вказано шлях тактування, який тут використовуватиметься. Світло-синій -- альтернативні шляхи.
Зеленим -- ті, що знадобляться в майбутньому.
(c) STMicroelectronics

Огляд GPIO -- General-purpose I/Os


Піни GPIO зібрані у порти, по 16 на порт. Порти іменуються  A, B, C, D, E, F, G, H. Зазвичай їх менше, ніж 8 -- залежить від реальної кількості пінів конкретного мікроконтролера.

Кожен із пінів може бути окремо сконфігурований:
  • На вивід, у режимі push-pull -- напруга на піні встановлюється "примусово". 
  • На вивід, з відкритим колектором. Можна із підтяжкою вверх чи вниз.
    Ілюстрація, що таке пін із відкритим колектором. Реальна схема багато складніша -- див. документацію.
    (c) Wiki
  • На ввід -- floating, коли піну дозволяється вільно приймати значення, а мікроконтролер просто його читає. Якщо пін ні до чого не під'єднаний (принаймні в деяких режимах роботи пристрою) -- на вхід поступатиме сміття, наловлене металевим піном в ролі антени з астралу ефіру.
  • На ввід, із підтяжкою вверх (до 1) чи вниз (до 0).
  • Вхід також може бути аналоговим -- працювати як АЦП.
  • Крім того, до піна може бути під'єднана та чи інша альтернативна функція -- контролер шини (скажімо, І2С), канал таймера і т.д.
Вибір режиму кожного піна здійснюється на STM32F303 за допомогою відповідних бітів цілого сімейства регістрів. Детально їх тут не розглядатиму, CubeMX про них потурбується, але на табличку режимів варто подивитися:
Регістри конфігурації пінів порту STM32F3. Для використання альтернативних функцій пінів доведеться працювати із багатьма іншими регістрами.
(с) STMicroelectronics

Якщо порт сконфігурований на вивід, кожен із перших 16 біт регістра GPIOx_ODR, де x -- назва порту, виводиться на відповідний пін. 

В коді звертання до цього регістра виглядає так: GPIOA->ODR для порту А, GPIOB->ODR для B і т.д. Відповідні змінні та імена їх полів надає CMSIS.

Крім того, регістр GPIOx_ BSRR дозволяє встановлювати чи очищати стан піна записом одинички у відповідний пін його нижньої 16-бітної чи верхньої 16-бітної половинки.

Кожен такт шини AHB, регістр GPIOx_IDR фіксує логічні значення на відповідних пінах.

Функції HAL інкапсулюють роботу із цими регістрами, тому тут в подробиці їх безпосереднього використання не вдаватимемося. Детальний аналіз роботи цих функцій можна знайти у постах серій про таймери і далекомір.

Деякі функції HAL


Вимір часу та затримки: HAL надає ряд простих функцій, що базуються на таймері SysTick:

  • void HAL_Delay(__IO uint32_t Delay) -- затримка в мілісекундах.("Busy loop" ).
  • uint32_t HAL_GetTick(void) -- отримати показники лічильника, що збільшується раз на мілісекунду.

Робота з пінами:
  • GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) -- прочитати значення піна.
  • void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) -- встановити значення піна.
  • void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) -- змінити значення піна на протилежний.
Де:
  • GPIOx -- власне, GPIOA, GPIOB, і т.д. 
  • GPIO_Pin -- номер піна, GPIO_PIN_x, x з 0..15. 
  • GPIO_PinState -- перерахування (enum) із двома значеннями, GPIO_PIN_RESET = 0 і  GPIO_PIN_SET = 1

Переривання


Переривання заслуговують значно довшої розмови, ніж може поміститися в цьому пості.  Детальніші розповіді про них вже майже дописані -- скоро рано чи пізно опублікую. Тому тут -- зовсім коротко та примітивно, без подробиць.
Приклади обробників переривань та трішки суміжної теорії є в постах про далекомір: "Далекомір HC-SR04 -- зовнішні переривання EXTI/HAL", "Далекомір HC-SR04 -- зовнішні переривання EXTI/CMSIS", та про таймери: "Таймери STM32 -- відлік часу/CMSIS", "Таймери STM32 -- одноімпульсний режим/HAL", "Таймери STM32 -- внутрішні тригери/HAL" і т.д. У них шукайте слова "handler" та "callback".
  • Переривання -- спосіб зупинити виконання основної програми у відповідь на виникнення якоїсь зовнішньої події та викликати код реакції на неї -- обробник цього переривання.
  • Ядро ARM Cortex спроектовано так, що обробником переривання може бути звичайна функція на С.
  • Піни GPIO вміють генерувати переривання у відповідь на зростання та/або спад сигналу. Це так-звані EXTIx, де x --- між 0 і 15, є номером піну відповідного переривання. (Для x між 8 і 15 є нюанс!).
Функція зворотного виклику void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) викликається кодом, згенерованим Cube, за кожного EXTIx переривання, їй передається номер піна, від якого переривання прийшло.
Технічно вона визначена в коді HAL як слабкий (weak) символ. Тобто, якщо в користувацькому коді такої функції не визначено, береться варіант із HAL, який нічого не робить, інакше -- використовується користувацька.

Конфігурування мікроконтролера та імпорт проекту STM32CubeMX в SW4STM32


Запустивши STM32CubeMX, обираємо File->New Project. В діалозі, що з'явився, вибираємо:
  • Series: STM32F3
  • Lines:  STM32F303
  • Із списку внизу обираємо STM32F303VCTx
Звичайно, можна скористатися і Board Selector. Якщо використовувати багато можливостей плати, це зручніше -- простіше використати готові налаштування для конкретної плати. Однак, для таких простих прикладів воно більше  заплутуватиме, ніж спрощуватиме.
Клікабельно -- відкривши у повний розмір, можна нормально роздивитися.

Коли проект згенеровано, першим кроком є налаштувати тактування. Обрані по замовчуванню значення можуть бути незадовільними.

Плата STM32F3Discovery не обладнана кварцом для головного мікроконтролера (програматор має свій кварцовий кристал), хоч і має місце для нього:
Місце для кварцового кристалу. Використання вимагає певних модифікацій плати. Див. главу "OSC clock supply" документації: "UM1570 User manual: Discovery kit with STM32F303VC MCU".

Тому доводиться користуватися HSI. Конфігуруємо тактування на вкладці "Clock Configuration" (для зовнішнього кварцу треба ще ввімкнути його використання для підсистеми RCC на вкладці Pinout):

Конфігурація тактування. Клікабельно!
 Або, детальніше:

Конфігурація тактування - 2. Клікабельно!
 Ключові елементи:
  • Джерело HSI -- єдине доступне зразу.
  • Множник PLL -- 16x, при тому досягається частота 64МГц, достатня для наших потреб, але менша за максимально можливу -- 72МГц.
  • Частота на APB1 при тому стане завеликою, тому для неї слід встановити подільник 1/2.
По великому рахунку -- все.

Тепер можна генерувати код:  Project -> Generate Code:
Слід обрати ім'я проекту, шлях до нього, який буде, за сумісництвом, Workspace для SW4STM32 та обрати генерацію коду для SW4STM32. Тут же можна керувати резервуванням пам'яті для купи та стеку (сині стрілки).
УВАГА! В імені проекту чи шляхах до нього не повинно бути пробілів, кирилиці чи інших національних або спецсимволів! (Симптом -- загадкові помилки при компіляції).
Як на мене, деякі зміни вартує зробити і на вкладці Code Generator цього діалогу:
1. Червона стрілка -- резервні копії варто мати завжди! Генератор зараз вже ніби акуратний, але мені не раз траплялося, коли він робив щось не те і мій код пропадав чи пошкоджувався.
2. Верхня синя стрілка: видається зручнішим мати кілька дрібних спеціалізованих файлів, ніж один великий.
3. Full Assert хоча б частково рятує від дурних помилок.

Тиснемо ОК, генеруємо код.

Тепер слід імпортувати отриманий проект в SW4STM32. Подробиці цього процесу описані за посиланням: "Importing an STM32CubeMX generated project under System Workbench for STM32". (На жаль, доступ вимагає реєстрації!).

Якщо не брати до уваги екзотичні комбінації версій, це доволі простий процес. Так як за посиланням опис детальний та з малюнками, не повторюватиму.

УВАГА! Якщо проект створювати для плати, він вже міститиме інформацію для зневаджувача (debugger). Однак, ми зупинилися на "голому" мікроконтролері. Тому, після імпорту (та першої компіляції) слід додати інформацію про способи зневадження, згідно описаного в кінці попереднього посилання.

В проектах нижче вважатимемо, що тактування налаштовано, проект успішно імпортовано в SM4STM32.

Керування світлодіодами

 

Вбудовані світлодіоди

Поміж інших, службових, плата STM32F3Discovery обладнана вісьмома світлодіодами, доступними програмно:


Їх підключення (PE -- скорочення від PortE):
  • PE8 -- LD4, синій,
  • PE9 -- LD3,  червоний,
  • PE10 -- LD5, оранжевий,  
  • PE11 -- LD7,  зелений,
  • PE12 -- LD9,  синій,
  • PE13 -- LD10, червоний,
  • PE14 -- LD8, оранжевий,  
  • PE15 -- LD6,   зелений.
Якщо почати із LD4 і рухатися за годинниковою стрілкою, порядок слідування LDx буде якраз таким, як у списку.

Конфігуруємо в Cube:

PE8 -- на вихід вивід.

Можна також, на вкладці Configure, задати ім'я цьому піну:
Клікабельно!

Для решти параметрів значення по замовчуванню зараз нас влаштовують. (Порівняйте їх вибір із таблицею режимів GPIO вище.)

Можна генерувати код.

Після цього, запустивши SW4STM32, вказуємо в ролі Workspace шлях до згенерованого проекту -- він буде спільним для середовища та Cube.
Cube не дуже любив, коли до проекту додавали нові файли... Не знаю, чи вони вже повноцінно це пофіксили.
Подальша процедура описана в "Importing an STM32CubeMX generated project under System Workbench for STM32", коротко розглянемо її. Після запуску йдемо до File->Import, обираємо General->Existing Project into Workspace, вказуємо шлях до проекту, імпортуємо.

Галочки біля "Copy projects into workspace" не повинно бути! Зніміть її, якщо є.

Якщо проект в директорії є, але середовище не хоче його імпортувати, перевірте, чи код генерувався саме для цього середовища, а не для EWARM чи ще якогось.
Після першої компіляції проекту можна заливати його на мікроконтролер та спробувати запустити:

Увага! Пам'ятайте, що програма виконуватиметься на пристрої, відмінному від того, на якому вона компілювалася!

Конфігурація для зневадження:

Відповідна перспектива:

Клікабельно!
На загал, особливості роботи з SW4STM32 заслуговують окремого поста. Наведу лише короткі рекомендації, що робити, якщо раптом відладка (зневадження) чи прошивання перестали працювати (те що в паніці називають "нічого не працює" ;-):
  1. Перш за все, закрийте всі сесії зневадження (їх може назбиратися багато), часто цього досить:

  1. Якщо це не допомогло, варто від'єднати і знову приєднати плату. Можливо -- із іншим кабелем.
  2. В більшості інших випадків допомагає оновлення компонент SW4STM32 (нагадаю, це Eclipse із плагінами та сторонніми програмами): Help -> Check for updates. Часто займає доволі багато часу!  
  3. На деяких машинах із Linux, (можливо, лише Ubuntu?), SW4STM32 зависає при спробі доступитися до опцій компіляції. Якщо ви зіткнулися із цією проблемою, спробуйте запропоноване тут: http://www.openstm32.org/forumthread2852.
Можна приступати до написання коду.

Найпростіша задача -- мигання світлодіодом.

Структура main.c детально аналізується в попередніх постах: "Далекомір HC-SR04 -- використовуючи GPIO/HAL/STM32CubeMX", розділ "Загальний вигляд коду". Тому тут не зупинятимуся. Зверніть лише увагу на коментарі -- маркери користувацького коду, по яких орієнтується Cube, перегенеровуючи проект.

Один із варантів реалізації мигання:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_SET);
   HAL_Delay(100);
   HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_RESET);
   HAL_Delay(100);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }

Більш елегантний, але і більш спеціалізований варіант:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_8);
   HAL_Delay(100);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}


Зовнішні світлодіоди

Програмно використання зовнішніх світлодіодів нічим не відрізняється від використання вбудованих в плату. Однак, електричне підключення до піна повинно враховувати той факт, що максимальний струм, який здатен віддати один пін мікроконтролера без його виходу із ладу -- 20мА. Тому підключення світлодіодів вимагає використання резисторів для обмеження струму:


Величину резистора можна знайти із закону Ома, враховуючи вольт-амперну характеристику світлодіода:
Взято, із змінами, тут.
Як видно, спад напруги на світлодіоді складатиме 1-3В, при тому, на відміну від спаду на резисторах, його можна вважати майже фіксованим.

Нехай він складає, скажімо, 2В. Візьмемо напругу живлення: Vcc=3.3В, як на нашій платі. Тоді на резисторі спад напруги складатиме U = 3.3-2 = 1.3В. Відповідно, мінімальну безпечну величину резистора можна знайти із закону Ома: I=U/R, знаючи, який максимальний струм може протікати через пін.

Розрахунок цього значення залишимо читачу. Злобно вважаючи, що читач, не здатний, після всіх пояснень вище, виконати його самостійно, заслуговує на покарання у вигляді спаленого мікроконтролера.

Увага! Підключаючи зовнішні пристрої до STM32F3Discovery, не варто використовувати наступні піни: PA13, PA14, PA15, PB3, PB4. Вони використовуються для прошивки та зневадження мікроконтролера, зміна їх конфігурації ускладнить роботу з платою.

Реакція на кнопку

Реакція на вбудовану кнопку

Як згадувалося вище, мікроконтролер здатен прочитати, є на піні логічний нуль чи логічна одиниця. Тому кнопку слід підключати так, щоб вона цей стане змінювала. Вбудована користувацька кнопка плати STM32F3Discovery під'єднана до піна PA0.

Схема під'єднання наступна:
Зверніть увагу на дещо незвичне для нас позначення резисторів -- такою собі пружинкою.
(с) STMicroelectronics
Питання -- а як воно працює? Для кращого розуміння спростимо схему, позбувшись непринципових елементів:

Так має бути зрозуміліше.
  • Коли кнопка розімкнена, пін, через резистор R4, заземлено -- як іноді кажуть, притягнуто до землі. На ньому логічний нуль. 
  • Коли кнопка натиснута, пін (через відносно маленький резистор R38,  про який трішки пізніше) під'єднано до напруги живлення -- на ньому логічна одиничка. 
  • Резистор R38, опором 330Ом виконує роль "захисту від дурня". Якщо помилково перевести пін PA0 в режим виводу, подати на нього нуль і натиснути кнопку -- через контролер потече струм, який обмежуватиметься лише опором між живленням і піном -- R38. Із закону Ома, він складатиме лише 10мА. Якщо б цей резистор був відсутнім -- така ситуація вивела б з ладу мікроконтролер. Так як R38 на три порядки менший за R4, спад напруги на ньому не заважає спрацьовувати піну при натисканні кнопки.
 Розібравшись із фізикою під'єднання, конфігуруємо та генеруємо проект.
Конфігурація піна PA0. (На вкладці Configuration йому присвоєно вже ім'я UButton)

Спробуємо, користуючись згаданими вище функціями HAL_GPIO_ReadPin() та  HAL_GPIO_WritePin() і реалізувати програму, яка засвічує LED4 лише тоді, коли натиснута кнопка.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   // Свідомо написано двома рядками -- для кращого розуміння
   GPIO_PinState btnState = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
   HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, btnState);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

Все працюватиме. Окрилені цим, ускладнимо задачу -- рахуватимемо кількості натискань, та відображатимемо їх двійковим числом, за допомогою світлодіодів: LD4, LD3, LD5, LD7 (PE8, PE9, PE10, PE11).

Переконфігуровуємо:
Піни LEDBITx -- на вивід, UButton -- на ввід.

Цього разу в коді скористаємося таки заданими іменами пінів (відповідні константи оголошено в mxconstants.h).

Почнемо із найбільш прямолінійного та відверто неелегантного підходу:
  • Кількість натискань зберігаємо у змінній   pressed.
  • Якщо кнопку натиснули, збільшуємо лічильник на одиницю та чекаємо, поки її не відпустять, перш ніж продовжити.
  • Далі робимо дуже примітивно --- аналізуємо кожен біт змінної pressed та вмикаємо чи вимикаємо світлодіод, що за нього відповідає. Більш елегантний (але і більш спеціалізований!) підхід -- нижче.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  /* USER CODE BEGIN WHILE */
  int pressed = 0;
  while (1)
  {
   // Справжній С-ник написали б так:
   // if ( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) )
   if ( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) == GPIO_PIN_SET )
   {
    ++pressed;
    while( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) == GPIO_PIN_SET )
    {}
   }

   //! УВАГА! Такий код відверто вульгарний, зате максимально прямолінійний
   if( pressed & 1 )
    HAL_GPIO_WritePin(LEDBIT0_GPIO_Port, LEDBIT0_Pin, GPIO_PIN_SET);
   else
    HAL_GPIO_WritePin(LEDBIT0_GPIO_Port, LEDBIT0_Pin, GPIO_PIN_RESET);

   if( pressed & 1<<1 )
    HAL_GPIO_WritePin(LEDBIT1_GPIO_Port, LEDBIT1_Pin, GPIO_PIN_SET);
   else
    HAL_GPIO_WritePin(LEDBIT1_GPIO_Port, LEDBIT1_Pin, GPIO_PIN_RESET);

   if( pressed & 1<<2 )
    HAL_GPIO_WritePin(LEDBIT2_GPIO_Port, LEDBIT2_Pin, GPIO_PIN_SET);
   else
    HAL_GPIO_WritePin(LEDBIT2_GPIO_Port, LEDBIT2_Pin, GPIO_PIN_RESET);

   if( pressed & 1<<3 )
    HAL_GPIO_WritePin(LEDBIT3_GPIO_Port, LEDBIT3_Pin, GPIO_PIN_SET);
   else
    HAL_GPIO_WritePin(LEDBIT3_GPIO_Port, LEDBIT3_Pin, GPIO_PIN_RESET);

  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

Випробовуємо. І тут нас може чекати сюрприз. Іноді одне натискання кнопки збільшує відлік на 2-3, а то й більше! В чому причина? Це -- приклад втручання реального світу, із його різноманітними фізичними явищами, у наш ідеальний комп'ютерний світ. Іншими словами -- дрижання контактів (вікіпедія користується перекладом "Брязкіт контактів").
Із вбудованою кнопкою цей ефект траплятиметься відносно рідко, завдяки отому викинутому при розгляді конденсатору і загальному проектуванню схеми та плати. Однак, для зовнішніх кнопок він буде дуже потужним. Див., наприклад, розділ "Експерименти з фільтрами" в "Таймери STM32 -- зовнішнє тактування/CMSIS": "Не претендуючи на якусь точність -- це вимагало б значно довшого експерименту, можна оцінити частоту таких випадків як 10%." та "Зауважу, кнопка під'єднана акуратно, дрижання проявляє себе значно слабше, ніж для простої зовнішньої кнопки.".
Цитуючи вікіпедію:
"При натисненні на кнопку або спрацюванні перемикача напруга не відразу встановлюється на заданому рівні, а «скаче» протягом деякого часу, поки коло надійно не замкнеться. Після того, як кнопка буде відпущена чи контакти розімкнені, напруга також «скаче», поки не встановиться на відповідному рівні, що відповідає цьому стану кнопки чи іншого пристрою перемикання.

Завади, викликані брязкотом контактів, можуть інтерпретуватися як декілька розмикань і замикань ключа. Якщо клавіша використовується для керування пристроєм, то цей пристрій сприйме завади як багаторазове натискання клавіші, що викличе значні ускладнення при його використанні.
"
Тривалість таких перехідних процесів -- десятки мілісекунд. На екрані осцилографа виглядають вони якось так:
Зображення взято тут: "Дребезг контактов и способы подавления дребезга".


Для мікроконтролера, котрий працює на частоті 72МГц --- величезний проміжок часу.

Як із цим боротися? Два основних підходи:
  • Програмний -- після реєстрації першого переключення, зачекати якийсь час, перш ніж реагувати на наступні.
  • Апаратний -- використати тригер. 
Зараз скористаємося програмним підходом:


1
2
3
4
5
6
7
8
   if ( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) == GPIO_PIN_SET )
   {
    ++pressed;
    HAL_Delay(50); //<========
    while( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) == GPIO_PIN_SET )
    {}
    HAL_Delay(50); //<========
   }

Тепер зайві спрацюванні повинні практично зникнути.

Облагородимо код переключення світлодіодів. Якщо згадати, що GPIO_PIN_SET == 1 а GPIO_PIN_RESET == 0, його можна переписати дещо краще. Замість:


1
2
3
4
   if( pressed & 1 )
    HAL_GPIO_WritePin(LEDBIT0_GPIO_Port, LEDBIT0_Pin, GPIO_PIN_SET);
   else
    HAL_GPIO_WritePin(LEDBIT0_GPIO_Port, LEDBIT0_Pin, GPIO_PIN_RESET);

напишемо:


1
             HAL_GPIO_WritePin(LEDBIT0_GPIO_Port, LEDBIT0_Pin, pressed & 1);


Тоді весь цикл виглядатиме помітно симпатичніше!


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  /* USER CODE BEGIN WHILE */
  int pressed = 0;
  while (1)
  {
   if ( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) )
   {
    ++pressed;
    HAL_Delay(50); //<========
    while( HAL_GPIO_ReadPin(UButton_GPIO_Port, UButton_Pin) )
    {}
    HAL_Delay(50); //<========
   }

   //! УВАГА! Такий код відверто вульгарний, зате максимально прямолінійний
   HAL_GPIO_WritePin(LEDBIT0_GPIO_Port, LEDBIT0_Pin, pressed & 1);
   HAL_GPIO_WritePin(LEDBIT1_GPIO_Port, LEDBIT1_Pin, pressed & 1 << 1);
   HAL_GPIO_WritePin(LEDBIT2_GPIO_Port, LEDBIT2_Pin, pressed & 1 << 2);
   HAL_GPIO_WritePin(LEDBIT3_GPIO_Port, LEDBIT3_Pin, pressed & 1 << 3);

  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */


Однак, при тому порушується абстракція, надана бібліотекою HAL. Правда, об'єктивно, порушення -- зовсім незначне.

Давайте тепер згадаємо, що обрані нами піни є пінами із послідовними номерами (8-11) одного і того ж порту (PE). Тобто, насправді, достатньо лише вміст перших чотирьох бітів pressed перемістити в GPIOE->ODR. Тому, замість рядків 15-18 попереднього прикладу, можна написати:


1
2
3
4
   // Замість бавитися в копіювання бітів, спочатку всі очищаємо.
   GPIOE->ODR &= ~(0xF << 8);
   // А потім встановлюємо лише потрібні.
   GPIOE->ODR |= (pressed & 0xF) << 8;

Правда, ціною втрати загальності та доволі сильного порушення абстракцій, наданих HAL.

Підказка: 0xF = 0b00....01111 (в двійковій системі).
Зауваження: шаблони С++ дозволяють записати код вище елегантно, при тому не порушувати абстракції та без втрати загальності.

 

 Реакція на зовнішню кнопку

Як і для зовнішніх світлодіодів, програмно використання зовнішньої кнопки нічим не відрізняється від   використання встановленої на платі.

Схемотехнічно ж є декілька рішень:
  • Реалізувати схему підключення, описану вище.
  • Скористатися вбудованою можливістю підтягування пінів вверх чи вниз. 
Конструюючи власну схему, не лінуйтеся встановити резистор для обмеження струму на випадок помилок! (Див. R38 на схемі вище). Опір резистора, що підтягуватиме пін до 0 чи 1, повинен бути достатньо великим, щоб струм через нього не був надмірним -- наприклад, не вичерпував намарно батарею. (Власне, традиційне підтягування до землі у не натиснутому стані використовується і для економії електроенергії). Але завеликий опір приводитиме до недостатньо "потужного" підтягування. Розумним значенням є десятки або перші сотні кілоОм.

Зовнішній резистор корисний тоді, коли внутрішні (величезного опору) якраз виявляються заслабкими для "перетягування" рівня на піні до землі (0) чи живлення (1) і завади в схемі приводять до спонтанних переключень рівнів або просто роблять це занадто повільно.

Використання переривань для реакції на натискання кнопки

Сконфігуруємо наш проект так, щоб пін кнопки був джерелом переривання. Так як його номер 0,  йому відповідатиме лінія переривання EXTI0:
На вкладці Configure слід дозволити це джерело переривань:

Все, можна генерувати код.

Для збереження поточного стану кнопки та кількості її натискань, нам знадобляться дві глобальних змінних, а функція HAL_GPIO_EXTI_Callback() буде викликатися, коли від кнопки приходитиме переривання. (Яке ми сконфігурували так, щоб воно виникало при зміні логічного рівня на ній).


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* USER CODE BEGIN PFP */
/* Private function prototypes -------------------------------------------*/

volatile int pressed = 0; // Ініціалізується нулем по замовчуванню, але так гарніше
volatile int button_is_pressed = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
 if( GPIO_Pin == UButton_Pin)
 {
  static uint32_t last_change_tick;
  if( HAL_GetTick() - last_change_tick < 50 )
  {
   return;
  }
  last_change_tick = HAL_GetTick();
  if(button_is_pressed)
  {
   button_is_pressed = 0;
   ++pressed;
  }else
  {
   button_is_pressed = 1;
  }
 }
}
/* USER CODE END PFP */

Статична змінна  last_change_tick використовується для ігнорування кількох переривань підряд, в рамках боротьби із дрижанням контактів.

Головний цикл тоді може займатися своїми справами (наприклад -- спати), лиш час від часу кидаючи оком на вміст змінної pressed:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   // Замість бавитися в копіювання бітів, спочатку всі очищаємо.
   GPIOE->ODR &= ~(0xF << 8);
   // А потім встановлюємо лише потрібні.
   __disable_irq();  // Перш, ніж звертатися до змінної, що може бути
           // зміненою в обробнику переривань,
           // забороняємо їх! 
   GPIOE->ODR |= (pressed & 0xF) << 8;
   __enable_irq(); // Завершивши звертання -- знову дозволяємо.
   
   HAL_Delay(50);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

УВАГА! Звертатися до змінної, яка модифікується в обробнику переривань небезпечно! Атомарність звертання не гарантується, тобто в його процесі може статися переривання, яке її модифікує. (Звичайно, для читання одного int це не дуже серйозна проблема, але вона таки є!)

Тому, перш ніж її читати, слід заборонити переривання. В коді вище це робиться у рядку 8. Використано вбудовану функцію (intrinsic) компілятора __disable_irq(), яка для ядер ARM Cortex M очищає регістр PRIMASK. Дозволяються переривання __enable_irq.

Менш все-загальним рішенням буде скористатися специфічною для STM32 функцією  HAL_NVIC_DisableIRQ(), котра може заборонити конкретне джерело переривань, EXTI0 у нашому випадку:


1
2
3
   HAL_NVIC_DisableIRQ(EXTI0_IRQn);
   GPIOE->ODR |= (pressed & 0xF) << 8;
   HAL_NVIC_EnableIRQ(EXTI0_IRQn);

Ось і все.

Післямова

Ми коротко розглянули способи роботи із пінами вводу-виводу (GPIO) з використанням HAL та конфігурації, згенерованої STM32CubeMX. Виклад відносно поверхневий, орієнтований на практичне використання. Дещо нетипово для моїх постів. :-)

На жаль, із не-технічних міркувань готові проекти не викладатиму. Однак, створити їх самостійно -- не проблема.

А на разі --
Дякую за увагу!

6 коментарів: