Мы тут обсуждали вроде бы простейший вопрос, как обрабатывать нажатия кнопок наиболее универсально и удобно. В результате мне прислали библиотеку для этого, которой я и поделюсь в статье с разрешения автора (я правда изменил оформление библиотеки под привычный мне стиль). Можно это даже приурочить к моему «курсу» по датчикам, так как один из модулей этого набора состоит из тактовой кнопки. Фото я прикрепил сверху, это модуль KY-004, но это не важно, речь пойдет о любых тактовых кнопках на примере использования их с STM32.
Тактовая кнопка выглядит так:
Не путаем с переключателем:
Переключатель фиксирован либо в одном, либо в другом положении. Тактовая кнопка фиксирована в не нажатом положении, чтобы ее удерживать в нажатом положении надо прилагать усилия. Но это все и так знают )
От такой кнопки хочется чуть более расширенного функционала, который мне бывает нужен почти в каждом проекте:
- отслеживание короткого нажатия кнопки
- длительного
- еще более длительного
Длительности могут быть разными для разных случаев. Этим целям и служит библиотека Button
, которую я опишу. Только начну я с применения готовой библиотеки, а потом опишу, как она работает. Кому просто нужно готовое проверенное решение, вторую часть можно даже и не читать.
Схема подключения кнопок к STM32.
У меня есть:
- Отладочная плата с STM32F401CCU6.
- Кнопка «Up», то есть вверх, допустим для перехода по пунктам меню. Подключена к выводу PA2.
- Кнопка «Down», то есть вниз. Подключена к PA3.
Обработка кнопок заключается в том, что нужно выполнять разные действия в зависимости от того, какое нажатие:
- Короткое нажатие – меньше 500 мс.
- Длинное нажатие – от 500 мс – до 3 секунд.
- Продолжительное нажатие – удержание кнопки более 3-х секунд.
Создание проекта для обработки кнопок.
Я использую STM32CubeIDE, поэтому в STM32CubeMx настраиваю два вывода STM32 на вход и любой из таймеров (нужен для библиотеки Button
). И еще один выход (PC13) для тестового светодиода. У меня кнопки подтянуты электрически к питанию, поэтому внутренняя подтяжка GPIO мне не нужна:
Таймер настроен на генерацию прерывания каждую миллисекунду:
Тактирование у меня настроено так:
То есть на таймер приходит частота 84 МГц. При предделителе 8399 получаю частоту:
F = 84 МГц / (8399 + 1) = 10 КГц
То есть один отсчет таймера – 100 мкс. Ставлю период (Counter Period), равный 10, получаю прерывание каждые - 100 мкс * 10 = 1 мс. Инициализация на этом закончена. Генерирую проект и перехожу к настройке библиотеки для обработки нажатий кнопок.
Конфигурация библиотеки Button для обработки кнопок.
Настройка очень проста, я ее разделил на несколько этапов, чтобы запутаться было невозможно.
- Добавляем файлы библиотеки
Button
в проект, эту операцию подробно думаю не надо описывать (если что спрашивайте в комментариях, я всем помогу):
- В файле button.h задается перечисление
ButtonID
с «названиями» кнопок. У меня это кнопка «Up» и «Down»:
typedef enum { BUTTON_UP, BUTTON_DOWN, BUTTONS_NUM, } ButtonID;
Если бы было три кнопки, то мог бы быть вот такой вариант:
typedef enum { BUTTON_MY_1, BUTTON_MY_2, BUTTON_MY_3, BUTTONS_NUM, } ButtonID;
Думаю, суть вы поняли ) В файле button.h также задаем числовые значения для длительностей нажатия кнопок. У меня длинное нажатие начинается с 500 мс, продолжительное нажатие – с 3000 мс:
#define BUTTONS_LONG_PRESS_MS 500 #define BUTTONS_VERY_LONG_PRESS_MS 3000
Все настраивается максимально просто и понятно. Кнопка может быть подключена к STM32 по-разному – при нажатии замыкать на землю, либо на +3.3 В. У меня замыкает на землю. Это значит, что в не нажатом состоянии с кнопки приходит высокий уровень, поэтому я делаю так:
#define GPIO_BUTTON_NOT_PRESSED (GPIO_PIN_SET)
Если бы замыкала на питание, то есть в не нажатом состоянии – низкий уровень, то было бы:
#define GPIO_BUTTON_NOT_PRESSED (GPIO_PIN_RESET)
Тоже ничего сложного.
- Последний шаг. В файле button.c задаем, куда подключены кнопки:
static McuPin buttons[BUTTONS_NUM] = {{GPIOA, GPIO_PIN_2}, {GPIOA, GPIO_PIN_3}};
Размер массива равен BUTTONS_NUM
. То есть я добавил две кнопки:
typedef enum { BUTTON_UP, BUTTON_DOWN, BUTTONS_NUM, } ButtonID;
И задал два порта ввода-вывода. На этом конфигурация закончена. Подытожим, полная конфигурация заключается в настройке:
- button.h,
GPIO_BUTTON_NOT_PRESSED
- button.h,
enum ButtonID
- button.h,
BUTTONS_LONG_PRESS_MS
,BUTTONS_VERY_LONG_PRESS_MS
- button.c,
static McuPin buttons[BUTTONS_NUM]
Использование библиотеки Button на STM32.
Обработку нажатий кнопок тоже разбиваю на понятные шаги. Все в main.c:
- Каждую миллисекунду будем вызывать функцию
BUTTON_TimerProcess()
из библиотекиButton
:
/* USER CODE BEGIN 4 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == htim10.Instance) { BUTTON_TimerProcess(); } } /* USER CODE END 4 */
Можно настроить прерывания и на больший период, тогда значения:
#define BUTTONS_LONG_PRESS_MS 500 #define BUTTONS_VERY_LONG_PRESS_MS 3000
Нужно будет задать другими. Если, например, период таймера – 100 мс, то все эти значения надо разделить на 100. У меня будет 1 мс, и значения такие, как я поставил (тоже в миллисекундах).
- Запускаем таймер в функции
main()
, почему-то постоянно об этом забываю…
/* USER CODE BEGIN 2 */ HAL_TIM_Base_Start_IT(&htim10); /* USER CODE END 2 */
- Работу библиотеки и обработку кнопок обеспечивают три функции:
BUTTON_Process()
– здесь крутятся внутренние процессы.BUTTON_GetAction()
– эта функция возвращает состояние кнопки.BUTTON_ResetActions()
– эта функция сбрасывает состояния кнопок после их анализа.
В итоге получается такой код:
while (1) { BUTTON_Process(); // Work with buttons // Button "Up" if (BUTTON_GetAction(BUTTON_UP) == BUTTON_SHORT_PRESS) { // LED on HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); } // Button "Down" if (BUTTON_GetAction(BUTTON_DOWN) == BUTTON_SHORT_PRESS) { // LED off HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); } BUTTON_ResetActions(); }
Здесь у меня по короткому нажатию кнопки Up зажигается светодиод на PC13, по короткому нажатию кнопки Down – светодиод гаснет. Все очень просто, минимум действий – и полноценная работа кнопок в кармане. Если нужно отловить длительные нажатия кнопки Up, то:
if (BUTTON_GetAction(BUTTON_UP) == BUTTON_LONG_PRESS) { // Do something } if (BUTTON_GetAction(BUTTON_UP) == BUTTON_VERY_LONG_PRESS) { // Do something }
Все, больше никаких действий не требуется. Антидребезг, расчет длительности нажатия, переход между состояниями – все внутри библиотеки Button
. Я теперь ее пихаю абсолютно во все проекты, чаще всего для работы с меню на каком-нибудь дисплее. Подключаю 4 кнопки, настраиваю так:
typedef enum { BUTTON_UP, BUTTON_DOWN, BUTTON_LEFT, BUTTON_RIGHT, BUTTONS_NUM, } ButtonID;
И в main()
элементарно отлавливаю любые действия пользователя.
Готовый проект – KY004_Button.
Разбор кода библиотеки.
А теперь рассмотрим, что в библиотеке под капотом, для начала переменные:
debounceCounter
– счетчик для исключения дребезга.waitButtonRelease
- переменная-флаг, показывающая, что кнопка нажата и мы ждем ее отпускания.- В то время, пока кнопка нажата, счетчик
buttonPressCounter
отсчитывает время нажатия. buttonActions
– состояния каждой из кнопок, которые анализируем потом в нашем коде.buttonState
– а это низкоуровневые состояния кнопок.
Низкоуровневые состояния могут быть такие:
typedef enum { BUTTON_STARTING = 0, BUTTON_NOT_PRESSED = 1, BUTTON_WAIT_DEBOUNCE = 2, BUTTON_PRESSED = 3, } ButtonState;
Первое из них – состояние по умолчанию, оно позволяет отслеживать ошибки, если кнопка подключена неправильно, к примеру. BUTTON_NOT_PRESSED
– кнопка не нажата, BUTTON_WAIT_DEBOUNCE
– обработка антидребезга, BUTTON_PRESSED
– кнопка нажата.
Высокоуровневые состояния:
typedef enum { BUTTON_NONE = 0, BUTTON_SHORT_PRESS = 1, BUTTON_LONG_PRESS = 2, BUTTON_VERY_LONG_PRESS = 3, } ButtonAction;
BUTTON_SHORT_PRESS
– короткое нажатие, BUTTON_LONG_PRESS
– длинное нажатие, BUTTON_VERY_LONG_PRESS
– продолжительное (еще более долгое) нажатие. При желании можно добавить и еще таких состояний, для других временных интервалов, но это не так часто требуется.
Обработка кнопок заключается в вызове функции BUTTON_Process()
, внутри которой операции подразделяются на низкоуровневые и высокоуровневые: BUTTON_LowLevelManager()
и BUTTON_HighLevelManager():
void BUTTON_Process() { BUTTON_LowLevelManager(); BUTTON_HighLevelManager(); }
Посмотрим код первой из функций и проанализируем ее построчно:
void BUTTON_LowLevelManager() { uint8_t currentStates[BUTTONS_NUM]; for (uint8_t i = 0; i < BUTTONS_NUM; i++) { currentStates[i] = HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin); switch (buttonState[i]) { case BUTTON_STARTING: if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED) { buttonState[i] = BUTTON_NOT_PRESSED; } break; case BUTTON_NOT_PRESSED: if (currentStates[i] == GPIO_BUTTON_PRESSED) { buttonState[i] = BUTTON_WAIT_DEBOUNCE; debounceCounter[i] = 0; } break; case BUTTON_WAIT_DEBOUNCE: if (debounceCounter[i] == DEBOUNCE_TIME_MS) { if (currentStates[i] == GPIO_BUTTON_PRESSED) { buttonState[i] = BUTTON_PRESSED; } else { buttonState[i] = BUTTON_NOT_PRESSED; } } break; case BUTTON_PRESSED: if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED) { buttonState[i] = BUTTON_WAIT_DEBOUNCE; debounceCounter[i] = 0; } break; default: break; } } }
Процессы идут для каждой из кнопок в цикле for (uint8_t i = 0; i < BUTTONS_NUM; i++)
. Сначала считывается состояние:
currentStates[i] = HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin);
Далее в switch()
проверяем текущее низкоуровневое состояние. Если - BUTTON_STARTING
, то проверяем, что кнопка не нажата и переходим в другое состояние. Если кнопка подключена неправильно, то есть замыкает на +3.3 В вместо земли (например), то кнопка зависнет в этом состоянии.
Когда кнопка не нажата, проверяем ее новое состояние (if (currentStates[i] == GPIO_BUTTON_PRESSED)
). Если она стала нажатой, то переходим в состояние антидребезговой обработки:
if (currentStates[i] == GPIO_BUTTON_PRESSED) { buttonState[i] = BUTTON_WAIT_DEBOUNCE; debounceCounter[i] = 0; }
Антидребезг по стандартному алгоритму – ожидаем DEBOUNCE_TIME_MS
и, если кнопка осталась в нажатом состоянии, то переводим ее в BUTTON_PRESSED
. Иначе переходим в BUTTON_NOT_PRESSED
.
Если кнопка сейчас нажата (BUTTON_PRESSED
), то проверяем ее новое состояние ((currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
). И если оно изменилось и стало GPIO_BUTTON_NOT_PRESSED
, то точно так же переходим к фильтрации дребезга контактов. Надеюсь, что я нормально описываю… Если что, пишите в комментарии, подправлю.
Осталась функция BUTTON_HighLevelManager()
, то есть высокоуровневые процессы:
void BUTTON_HighLevelManager() { for (uint8_t i = 0; i < BUTTONS_NUM; i++) { if (buttonActions[i] == BUTTON_NONE) { if (waitButtonRelease[i] == 0) { if (buttonState[i] == BUTTON_PRESSED) { waitButtonRelease[i] = 1; } } else { if (buttonState[i] == BUTTON_NOT_PRESSED) { waitButtonRelease[i] = 0; if (buttonPressCounter[i] >= BUTTONS_VERY_LONG_PRESS_MS) { buttonActions[i] = BUTTON_VERY_LONG_PRESS; } else { if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS) { buttonActions[i] = BUTTON_LONG_PRESS; } else { buttonActions[i] = BUTTON_SHORT_PRESS; } } } } } } }
Здесь проще, если кнопка не нажата:
if (buttonActions[i] == BUTTON_NONE)
Но при этом ее низкоуровневое состояние соответствует нажатой кнопке (if (buttonState[i] == BUTTON_PRESSED)
), то устанавливаем флаг waitButtonRelease[i]
в единицу. Здесь кстати так же вся работа в цикле по всем кнопкам. При следующем заходе в эту функцию, видим, что флаг в единице и ждем отпускания кнопки:
if (buttonState[i] == BUTTON_NOT_PRESSED) { waitButtonRelease[i] = 0; if (buttonPressCounter[i] >= BUTTONS_VERY_LONG_PRESS_MS) { buttonActions[i] = BUTTON_VERY_LONG_PRESS; } else { if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS) { buttonActions[i] = BUTTON_LONG_PRESS; } else { buttonActions[i] = BUTTON_SHORT_PRESS; } } }
При этом идет инкрементирование счетчика buttonPressCounter[i]
(об этом чуть ниже). Когда кнопка будет отпущена (if (buttonState[i] == BUTTON_NOT_PRESSED)
), по этому значению сделаем вывод о текущем нажатии.
Следующая функция библиотеки – BUTTON_TimerProcess()
– та, вызов которой мы добавили в callback по переполнению таймера:
void BUTTON_TimerProcess() { for (uint8_t i = 0; i < BUTTONS_NUM; i++) { if (debounceCounter[i] < DEBOUNCE_TIME_MS) { debounceCounter[i]++; } if (waitButtonRelease[i] == 1) { buttonPressCounter[i]++; } else { buttonPressCounter[i] = 0; } } }
Здесь просто инкрементируются использованные счетчики: счетчик для антидребезга и счетчик для нажатия. На этом я заканчиваю статью, мне эта библиотека очень подошла для обработки кнопок, может кому-то и не подойдет, как говорится «на вкус и цвет» )
Отдельная ссылка на библиотеку: Button.
Update 1.
Спасибо Kir за дельные замечания, обновил код и ссылки, а также добавил функцию инициализации void BUTTON_Init()
, которая задает начальные значения всех переменных. Как полностью справдливо замечено, слепо доверять компилятору мы не имеем никакого морального права! Вызываем эту функцию в main()
:
/* Initialize all configured peripherals */ MX_GPIO_Init(); MX_TIM10_Init(); /* USER CODE BEGIN 2 */ BUTTON_Init(); HAL_TIM_Base_Start_IT(&htim10); /* USER CODE END 2 */
Еще раз спасибо Kir, здравая критика и дельные замечания!