Несколько действий на одной кнопке
В некоторых устройствах приходится ставить вместо двух кнопок одну. Причины могут быть разными: экономическими, эргономическими, или банально не хватает места на печатной плате. Настроить прерывание и написать обработчик для простого нажатия кнопки не вызывает проблем, но что делать, если действий должно быть несколько в зависимости от того, как кнопка была нажата?
Возьмем в качестве примера устройство, рассмотренное ранее, часы с датчиком температуры. Условие перехода в режим настройки был довольно странным и неинтуитивным: нужно выставить минимальную яркость на экране и нажать на кнопку. Под открытым солнцем пользователь явно не будет рад такому решению. Рассмотрим два сценария: удержание кнопки и двойное нажатие.
Такое решение было выбрано осознанно, чтобы можно было обособленно описать другое решение и показать, какие проблемы могут возникнуть.
Сценарий №1
При кратковременном нажатии на кнопку устройство переходит от отображения времени к отображению температуры. Если удерживать кнопку более, допустим, 3 секунд, то устройство переходит в режим настройки.
Возьмите свой телефон и найдите кнопку включения/выключения экрана. В большинстве случаев на эту же кнопку будет повешено еще одно действие: зажали кнопку на три секунды — выключили телефон. Действие по простому нажатию реализуется довольно просто: добавляем сглаживающую RC-цепочку, настраиваем прерывание и пишем его обработчик. Как реализовать обработку долгого удержания?
Решений может быть несколько. Самый банальный способ — это внутри прерывания замерять время зажатия кнопки.
// delay for 3 seconds
#define LONG_PRESS_DELAY 3000
// ...
void EXTI0_1_IRQHandler(void) {
timer_start(); // reset counter and start timer
while (GPIO_ReadBit() != RESET);
timer_stop(); // stop timer
if (timer_get_value() < LONG_PRESS_DELAY)
// short-press action here
else
// long-press action here
// clean pending but
}
Если вы, читая книгу, дошли до этого момента и не видите здесь проблемы, то… лучше перечитайте раздел про прерывания. Здесь сразу же нарушается правило работы с ними: код должен выполняться как можно быстрее. Пока нажата кнопка, всё устройство будет парализовано. Если другой таймер отвечает за обновление показаний на дисплее (выставляя нужное значение битов в выходном регистре), то обработчик прерывания не будет запущен, пока вы не отпустите кнопку. Такое поведение вполне безобидно в случае часов, но в других устройствах это может привести к катастрофе: например, пока микроконтроллер в устройстве управления шлагбаумом завис в прерывании, он не успел отреагировать на сигнал от датчика движения и при закрытии врезал по лобовому стеклу проезжающего автомобиля.
Прерывание по входному сигналу (модуль EXTI) может происходить либо по переднему (момент нажатия), либо по заднему (момент отпускания), либо по обоим фронтам сразу. Типовое решение для кратковременного нажатия работает только с одним из фронтов (чаще передним, как в примере выше). Для реализации удержания кнопки придется использовать оба фронта. К сожалению, напрямую проверить, по какому из них произошло прерывание, нельзя: нет специального регистра, который бы сигнализировал об этом. Но можно использовать знание того, как работает схема.

RC-цепочка надежно справляется с дребезгом контактов. Это значит, что при нажатии на линии гарантировано высокий уровень, т.е. считав входной регистр и обнаружив там 1
, мы с уверенностью можем сказать, что прерывание произошло по переднему фронту. Соответственно, обнаружив там 0
, мы поймем, что это был задний фронт.
Реализуем это программно.
// delay for 3 seconds
#define LONG_PRESS_DELAY 3000
// ...
void EXTI0_1_IRQHandler(void) {
if (front_edge()) { // Read IDR register
timer_start();
} else {
timer_stop();
if (timer_get_value() < LONG_PRESS_DELAY)
// short-press action here
else
// long-press action here
}
// clean pending but
}
Предделитель таймера рассчитан так, чтобы увеличение счетчика происходило раз в 1 мс. Функция timer_start()
обнуляет состояние счетчика, а затем запускает его. Функция timer_stop()
останавливает таймер. Эти две функции полезно сделать в виде макроса, либо присвоить им модификатор inline
.
Если нужно отреагировать до того, как кнопка будет отпущена, необходимо настроить прерывание по достижении определенного значения и написать обработчик.
Сценарий №2
Поведение при кратковременном нажатии повторяет поведение из сценария №1. Переход в режим настройки осуществляется последовательным кратковременным нажатием кнопки (скажем, в течение 1 секунды).
Здесь придется добавить еще одну функцию, is_timer_on()
, которая позволит контролировать, включен ли таймер. При первом нажатии на кнопку запускается таймер. Если пользователь не нажмет еще раз на него в течение, скажем, 0,5 секунд, то сработает прерывание от таймера TIM14_IRQHandler()
, в котором нужно: а) отключить таймер; б) совершить действие для одиночного кратковременного нажатия. Если же таймер уже был запущен (значение счетчика будет меньше 500), то это двойное нажатие.
#define MAX_DELAY 500
// ...
void EXTI0_1_IRQHandler(void) {
if (is_timer_on()) {
timer_stop();
// double-click action here
} else {
start_timer();
}
}
// ...
void TIM14_IRQHandler(void) {
timer_stop();
// single-click action
// clean pending bit
}
Такая реализация довольно проста, но имеет недостаток: присутствует задержка в реакции на MAX_DELAY
.