Этот скетч и библиотека показывают, как использовать один вывод порта микроконтроллера AVR, чтобы детектировать типичные события на кнопке, такие как одиночное нажатие (single click), двойное нажатие (double click) и длительное нажатие (long press). Этот функционал позволяет использовать одну и ту же кнопку для нескольких функций с небольшими затратами на аппаратуру - особенно удобно в случае применения поворотного энкодера (rotary encoder, jog-shuttle) с одной кнопкой.
Примечание: здесь приведен перевод статьи [1], исходный код библиотеки можно скачать по ссылке [2] с использованием систем GIT или SVN, либо просто в виде архива OneButton-master.zip, если нажать на кнопку "Download ZIP".
[Введение]
Начинающим программировать в системе Arduino полезно рассмотреть простой обучающий пример опроса кнопки (Button tutorial), чтобы понять, как нужно читать состояние кнопки, и пример Debounce, который показывает, как нужно очистить сигнал кнопки, устраняя ложные срабатывания от импульсов дребезга контактов. Описанная ниже библиотека является как бы результатом объединения этих двух примеров в одну маленькую библиотеку OneButton, которую Вы можете очень просто использовать в своих скетчах для опроса различных событий на кнопке. Здесь будет также объяснено, как использовать библиотеку, как она устроена, что даст Вам понимание принципа работы библиотеки.
[Чтение кнопки без блокировки программы]
Один из недостатков простых примеров опроса кнопки (имеются в виду простые примеры скетчей типа Button и Debounce, поставляемые вместе с системой программирования Arduino) в том, что они используют функцию loop(), что делает сложным повторное использование кода, который опрашивает кнопки.
Скорее всего, Ваш скетч, где нужно опрашивать кнопку, должен быть сложнее, чем вышеупомянутые примеры, поскольку обычно кроме опроса кнопок в программе нужно периодически выполнять и другие функции. Другими словами, в главном цикле loop() должны быть вызовы других функций, выполняющих нужную для Вас работу. По этой причине общее требование для библиотек и повторно используемого кода - избегать использования функции loop(). Обычно это реализуют либо с помощью обработчика прерывания таймера, либо код библиотеки должен быть устроен так, чтобы они не блокировать прокрутку цикла loop() на длительное время.
В библиотеке OneButton в цикле loop() для опроса кнопки здесь использовалась неблокирующая функция tick() библиотеки, которая должна вызываться циклически, с интервалом порядка 10 мс. Внутри этой функции Вы найдете код для детектирования всех трех опрашиваемых событий, которые происходят на кнопках (single click, double click, long press).
Неблокирующая функция получила свое название из-за того, что она не блокирует выполнение кода на циклы ожидания какого-либо события (или событий). Вместо этого она быстро выполняет внутри себя некую обработку и сразу делает возврат, позволяя другим функциям программы (которые вставлены в тело цикла loop) успешно выполниться. Все нужные результаты вызова запоминаются во внутренних статических переменных неблокирующей функции.
В нашем случае библиотека OneButton использует неблокирующую функцию tick, которая запоминает в статических переменных счетчики времени и флаги, применяемые для алгоритма детектирования событий в программе.
Неблокирующие функции нужны для реализации псевдо многозадачности, когда время процессора распределено для "одновременного" выполнения различных, иногда влияющих друг на друга задач.
Для каждого из 3 событий Вы можете зарегистрировать функцию обработки, и таким способом подключить Ваш код к библиотеке OneButton. Ваши функции будут вызываться в тот момент, когда будет детектирована соответствующая ситуация (single click, double click, long press).
[Пример использования библиотеки OneButton]
В этом простом примере показано использование библиотеки OneButton. Код скетча меняет состояния светодиода по умолчанию (на платах Arduino светодиод LED подключен к цифровому порту микроконтроллера 13), когда детектирован двойной клик на кнопке, подключенной к порту микроконтроллера A1.
/* S01_SimpleOneButton
Используемая схема:
* Подключите простую кнопку типа pushbutton к выводу A1 (ButtonPin)
* и результат работы программы смотрите на выводе 13 (StatusPin).
* Пример создан 03.03.2011, автор Matthias Hertel
*/
#include "OneButton.h"
// Настройка нового объекта OneButton, связанного с портом A1.
OneButton button(A1);
// Код настройки, вызываемый 1 раз:
voidsetup()
{
// Разрешить работу стандартного светодиода, подключенного// на платах Arduino к цифровому порту 13:
pinMode(13, OUTPUT); // настройка порта для работы как выход// Привязка функции doubleclick, которая должна быть вызвана// при событии двойного нажатия на кнопке.
button.attachDoubleClick(doubleclick);
} // setup
// Это основной код, который прокручивается постоянно:
voidloop() {
// Вызов функции, обрабатывающей состояния кнопки:
button.tick();
// В этом месте могут быть другие вызовы неблокирующих// функций и процедур, выполняющих какие-либо действия в// программе.
delay(10);
} // loop
// Эта функция будет вызвана в тот момент, когда кнопка
// была нажата дважды с коротким интервалом между нажатиями
// (так называемый даблклик, doubleclick).
voiddoubleclick() {
staticint m = LOW;
// Поменять состояние светодиода LED:
m =!m;
digitalWrite(13, m);
} // doubleclick
[Подробности реализации библиотеки OneButton]
Внутри функции tick() библиотеки OneButton Вы найдете код, опрашивающий вход кнопки, который детектирует события single click, double click или long press. Код использует принцип конечного автомата [5] - так называемая машина конечных состояний, finite state machine (FSM), алгоритм которой показан на следующей диаграмме состояний:
Пояснения к диаграмме:
Start начало алгоритма. wait ожидание. Press timeout время таймаута, после истечения которого считается, что было нажатие. Button Up кнопка отпущена. Button down кнопка нажата. jitter дребезг контактов, подавляемый библиотекой. Click timeout таймаут клика на кнопке (т. е. событие, когда кнопка нажата и отпущена). click(), doubleclick() обработчики событий одиночного и двойного нажатия. 0, 1, 2 .. состояния машины FSM.
Каждый раз, когда вызывается функция tick() в какой-то текущей ситуации, делается оценка логического уровня на входе кнопки в контексте истории предыдущих вызовов и оценок этого входа. При изменении состояний входа происходит переключение по узлам машины FSM, чтобы правильно детектировать события одиночного, двойного и долгого нажатия на кнопке. Обратите внимание, что внутри библиотеки OneButton никогда не вызываются функции задержки наподобие delay(). Таким образом, все действия библиотеки быстро завершают свою работу, оставляя процессорное время микроконтроллера AVR для выполнения других функций в программе.
Ниже приведено краткое описание алгоритма работы программной машины состояний функции tick библиотеки.
Входные значения. Для оценки состояния кнопки происходит чтение логического уровня (лог. 0 или лог. 1) вывода цифрового порта, к которому подключена кнопка. Уровень на этом выводе подтягивается к лог. 1 внутри микроконтроллера AVR (так называемый pull-up, управляемый настройкой регистров DDRxn и PORTxn, подробнее см. [4]). Опрашиваемая кнопка одним выводом подключена ко входу порта и вторым выводом к земле (GND), поэтому при нажатии кнопки на входе появляется уровень лог. 0.
Примечание: на рисунке показан пример схемы подключения кнопки ко входу микроконтроллера, когда при нажатии на кнопку происходит замыкание порта на землю GND (подача на входной порт сигнала лог. 0). Это наиболее часто использующееся подключение кнопки к микроконтроллеру, однако библиотека OneButton поддерживает также и другой способ подключения, когда замыкание кнопки вызывает подачу сигнала лог. 1. Схема подключения гибко может быть настроена вызовом конструктора, в котором во втором параметре activeLow передается активный уровень для нажатия (см. исходный код библиотеки ниже).
Реализация опроса источника событий (определение лог. уровня на входе и запоминание его в переменной buttonLevel) происходит в начале выполнения кода машины состояний:
int buttonLevel = digitalRead(_pin); // текущее значение сигнала от кнопки.
Для работы алгоритма нужны также и другие данные - значение текущего времени. Эти данные вычитываются с помощью вызова функции millis(), которая показывает, сколько прошло времени относительно предыдущего вызова кода машины состояний. Текущее значение времени копируется в переменную now:
unsignedlong now = millis(); // текущее (относительное) время в миллисекундах.
State (состояние FSM). Информация, которая была запомнена между вызовами FSM (т. е. что происходило в предыдущем вызове tick() относительно текущего вызова) называется состоянием машины FSM (state). Для этого специально используется одна переменная с именем state, в которой удерживается информация, описывающая текущую ситуацию. Разновидности ситуаций кодируются простыми числами (0, 1, 2, ...), показанными на диаграмме выше, и это первая составляющая информации состояния. Другая составляющая - переменная, в которой сохранено время, когда кнопка была нажата в первый раз.
Эти 2 переменные определяют функционирования машины состояний, и они должны быть помечены ключевым словом static (так называемые статические переменные языка C/C++), или должны быть сделаны глобальными - это гарантирует сохранность состояния этих переменных между отдельными вызовами функции tick().
staticint _state =0; // Начальное состояние FSM будет 0:// в нем ожидается нажатие на кнопку.
staticunsignedlong _startTime; // Эта переменная будет установлена в state 1.
Исходная ситуация (state 0). Машина стартует с ситуации (state 0), когда происходит ожидание нажатия на кнопку. Если кнопка не нажата (на входе порта кнопки лог. 1), то функция tick() просто делает возврат, потому что она будет вызвана снова после истечения короткого промежутка времени (около 10 мс), чтобы снова проверить состояние кнопки.
Однако если кнопка была нажата (на входе порта кнопки лог. 0), это запоминается в переходе к следующему состоянию FSM (state 1). Все последующие оценки интервалов времени отсчитываются от этого момента, поэтому текущее относительное время запоминается в переменной _startTime.
if (_state ==0)
{
// Ожидание нажатия на кнопку:if (buttonLevel == ButtonDown)
{
_state =1; // переход к 1
_startTime = now; // запоминание начального времени
}
...
Кнопка нажата первый раз (state 1). В следующий раз функция будет вызвана снова (когда пройдет несколько миллисекунд, обычно 10 мс) уже в новой ситуации (state 1), в этой ситуации кнопка уже нажата. Теперь, в state 1, происходит ожидание отпускания кнопки, тогда будет переход в следующее состояние (state 2). Кнопка останется в нажатом состоянии еще несколько вызовов в состоянии state 1, даже если было короткое нажатие, поскольку пальцы пользователя не настолько быстро могут нажимать на кнопку, чтобы не успело по времени пройти еще несколько вызовов tick().
Когда ситуация state 1 происходит достаточно долго (в реализации выбран интервал порядка 1 секунды), возникнет другое состояние - state 6. Время измеряется сравнением текущего времени и того времени, которое было запомнено в переменной _startTime при нажатии кнопки в состоянии state 0. В состоянии state 6 будет сделан вызов внешней функции press().
...
}
elseif (_state ==1)
{
// Ожидание отпускания кнопки:if (buttonLevel == ButtonUp)
{
_state =2; // переход в состояние state 2
}
elseif ((buttonLevel == ButtonDown) && (now > _startTime + _pressTicks))
{
if (_pressFunc)
_pressFunc();
_state =6; // переход в состояние state 6
}
...
Кнопка была отпущена после первого нажатия (state 2). В этой ситуации (state 2), когда кнопка была быстро отпущена (или относительно быстро отпущена, прошло времени менее 1 секунды), могли иметь место 2 варианта событий:
Если прошло некоторое время после отпускания, и не было повторного нажатия, то будет вызвана внешняя функция click(), и машина состояний вернется в 0.
Если кнопка после отпускания была сразу нажата второй раз, то происходит переход к следующей ситуации (state 3):
...
elseif (_state ==2)
{
// Ожидание вторичного нажатия на кнопку или истечения таймаута.if (now > _startTime + _clickTicks)
{
// Это было простое короткое нажатиеif (_clickFunc)
_clickFunc();
_state =0; // возврат в исходное состояние.
}
elseif (buttonLevel ==false)
{
_state =3; // переход в состояние state 3
}
}
...
Ожидание отпускания кнопки после двойного нажатия (state 3). В заключительной ситуации (state 3), после того как был определен второй клик в заданном интервале времени, снова происходит ожидание отпускания кнопки. Когда это произойдет, будет вызвана внешняя функция doubleClic(), и машина состояний вернется в стартовое состояние (state 0), и все начнется сначала.
Ожидание отпускания кнопки после долгого нажатия (state 6). Обработка этой ситуации (state 6) очень проста, и похожа на обработку ситуации двойного клика, где также было ожидание отпускания кнопки. Отличие только в том, что происходит простой возврат в начальное состояние машины, без вызова внешней функции.
...
}
elseif (_state ==6)
{
// Ожидание отпускания кнопки после длительного нажатия:if (buttonLevel == ButtonUp)
{
_state =0; // возврат в исходное состояние
}
}
Реализация машины конечных состояний (finite state machine, FSM) [5] во многих ситуациях упрощает программирование, и помогает в написании скетчей Arduino, имитирующих многозадачную систему.
[Инсталляция библиотеки OneButton]
Весь функциональный код библиотеки OneButton сосредоточен в двух файлах, и написан в соответствии с официальным руководством Arduino [6] (там же описана процедура сборки библиотеки).
Инсталляция кода библиотеки OneButton заключается в простом создани нового подкаталога (с именем OneButton) в папке libraries, и копировании туда файлов (OneButton.h, OneButton.cpp) из архива [2]. После этого Вам нужно перезапустить среду Arduino IDE, потому что процедура опроса наличия библиотек осуществляется при старте среды разработки Arduino IDE.
Для того, чтобы использовать библиотеку OneButton в своих программах, добавьте в начала скетча программы оператор #include со ссылкой на заголовочный файл библиотеки, указанный в угловых скобках:
#include < OneButton.h >
Можно использовать альтернативный метод подключения библиотеки, если оставить те же 2 файла в корневой папке скетча. Тогда эти файлы станут доступны для среды разработки Arduino, когда Вы заново откроете этот скетч. Это может пригодиться, если Вы хотите доработать код библиотеки с целью её улучшения или добавления функционала. В таком случае нужно подключить заголовок библиотеки с помощью оператора #include, с указанием имени заголовочного файла в двойных кавычках:
#include "OneButton.h"
Рабочие примеры использования библиотеки OneButton можно найти в архиве [2], см. каталог OneButton-master\examples.
public:// ----- Конструктор -----
OneButton(int pin, int active);
// ----- Установка параметров времени выполнения -----// Установка количества миллисекунд, после которых// считается, что был одиночный клик.voidsetClickTicks(int ticks);
// Установка количества миллисекунд, после которых// считается, что было длинное нажатие.voidsetPressTicks(int ticks);
// Подключение функций, которые будут вызваны, когда// кнопка будет нажиматься определенным образом.voidattachClick(callbackFunction newFunction);
voidattachDoubleClick(callbackFunction newFunction);
voidattachPress(callbackFunction newFunction); // эта функция устарела, поскольку// была заменена на longPressStart,// longPressStop и duringLongPress.voidattachLongPressStart(callbackFunction newFunction);
voidattachLongPressStop(callbackFunction newFunction);
voidattachDuringLongPress(callbackFunction newFunction);
// ----- Функции машины состояний -----// Функция tick должна вызываться периодически, с интервалом времени// порядка 10 мс - чтобы библиотека могла обработать события кнопки.voidtick(void);
boolisLongPressed();
private:int _pin; // Номер входного порта кнопки.int _clickTicks; // Количество тиков, которое должно пройти, чтобы// было засчитано короткое нажатие на кнопку.int _pressTicks; // Количество тиков, которое должно пройти, чтобы// было засчитано длинное нажатие на кнопку.constint _debounceTicks =50; // количество тиков для подавления дребезга.//Примечание: под "тиками" подразумевается количество вызовов// функции tick().int _buttonReleased;
int _buttonPressed;
bool _isLongPressed;
// Эти переменные хранят указатели на функции обратного вызова,// которые определит пользователь для обработки событий кнопки.
callbackFunction _clickFunc;
callbackFunction _doubleClickFunc;
callbackFunction _pressFunc;
callbackFunction _longPressStartFunc;
callbackFunction _longPressStopFunc;
callbackFunction _duringLongPressFunc;
// Переменные ниже хранят информацию машины состояний, сохраняющуюся// между отдельными вызовами tick(). Они инициализируются один раз// при старте программы, и обновляются каждый раз при вызове функции// tick().int _state;
unsignedlong _startTime; // Эта переменная будет установлена в state 1.
};
#endif
Модуль основного кода OneButton.cpp:
// -----
// OneButton.cpp - библиотека для определения нажатий на кнопку,
// двойных нажатий и долгих нажатий.
// Этот класс реализован для использования в среде разработки
// Arduino.// Copyright (c) by Matthias Hertel, http://www.mathertel.de
// Код защищен лицензией стиля BSD, см.:
// http://www.mathertel.de/License.aspx
// Дополнительная информация:
// http://www.mathertel.de/Arduino
// Историю изменения библиотеки см. в файле OneButton.h.
// -----
#include "OneButton.h"
// ----- Инициализация и значения по умолчанию -----
OneButton::OneButton(int pin, int activeLow)
{
pinMode(pin, INPUT); // настройка порта кнопки в качестве входа
_pin = pin;
_clickTicks =600; // Количество тиков, которое должно пройти, чтобы// было засчитано короткое нажатие на кнопку.
_pressTicks =1000; // Количество тиков, которое должно пройти, чтобы// было засчитано длинное нажатие на кнопку.
_state =0; // Начальное состояние state 0: в нем ожидается первое нажатие// на кнопку.
_isLongPressed =false; // Флаг, отслеживающий долгое нажатие.
if (activeLow)
{
// При нажатии на кнопку порт получает лог. 0 (уровень GND).
_buttonReleased = HIGH; // состояние, когда кнопка не нажата
_buttonPressed = LOW; // состояние, когда кнопка нажата
digitalWrite(pin, HIGH); // включить верхний нагрузочный резистор (pullUp)
}
else
{
// При нажатии на кнопку порт получает лог. 1 (уровень VCC).
_buttonReleased = LOW;
_buttonPressed = HIGH;
}
_doubleClickFunc =NULL;
_pressFunc =NULL;
_longPressStartFunc =NULL;
_longPressStopFunc =NULL;
_duringLongPressFunc =NULL;
} // Это был конструктор OneButton
// Установка количества тиков (или миллисекунд, если вызовы tick происходят
// раз в миллисекунду), которое должно пройти, чтобы было детектировано
Отличие порта от оригинального класса OneButton в том, что отсутствует конструктор, настраивающий ножку порта и его активный уровень он (отсутствуют переменные _buttonReleased и _buttonPressed). Предполагается, что активный уровень кнопки низкий (замыкание кнопки осуществляется на землю, GND), и должны присутствовать внешние макроопределения для имен регистров и ножки порта (DDR_Enc, PORT_Enc, Btn_Enc). Также должен быть миллисекундный счетчик относительного реального времени timestamp, обновляемый с помощью обработчика прерывания таймера или другим способом. Машина конечных состояний обрабатывается процедурой OBtick, которая должна вызываться с интервалами примерно 10 миллисекунд.
Заголовочный файл OneButton.h:
// -----
// OneButton.h - библиотека для определения нажатий на кнопку,
// двойных нажатий и долгих нажатий. Это порт для проектов на
// языке C (WinAVR, AVR Studio).
// Copyright (c) by Matthias Hertel, http://www.mathertel.de
// Код защищен лицензией стиля BSD, см.:
// http://www.mathertel.de/License.aspx
// Дополнительная информация:
// http://www.mathertel.de/Arduino
// -----
// 02.10.2010 начало создания
// 21.04.2011 оформление кода в библиотеку
// 01.12.2011 изменен заголовочный файл, чтобы он был совместим
// со средой разработки Arduino 1.0.
// 23.03.2014 улучшена обработка долгих нажатий путем добавления
// функций обратного вызова (callback) longPressStart
// Функция tick должна вызываться периодически, с интервалом времени
// порядка 10 мс - чтобы библиотека могла обработать события кнопки.
voidOBtick(void);boolOBIsLongPressed();
// Эти переменные хранят указатели на функции обратного вызова,
// которые определит пользователь для обработки событий кнопки.
callbackFunction _clickFunc;
callbackFunction _doubleClickFunc;
callbackFunction _pressFunc;
callbackFunction _longPressStartFunc;
callbackFunction _longPressStopFunc;
callbackFunction _duringLongPressFunc;
#endif
Модуль основного кода OneButton.c:
// -----
// OneButton.c - библиотека для определения нажатий на кнопку,
// двойных нажатий и долгих нажатий. Это порт для проектов на
// языке C (WinAVR, AVR Studio).
// Код защищен лицензией стиля BSD, см.:
// http://www.mathertel.de/License.aspx
// Дополнительная информация:
// http://www.mathertel.de/Arduino
// -----
// Историю изменения библиотеки см. в файле OneButton.h.
// -----
#include < stddef.h >
#include "OneButton.h"
#include "timer.h"
#include "encoder.h"
int _clickTicks; // Количество тиков, которое должно пройти, чтобы// было засчитано короткое нажатие на кнопку.
int _pressTicks; // Количество тиков, которое должно пройти, чтобы// было засчитано длинное нажатие на кнопку.
constint _debounceTicks =50; // количество тиков для подавления дребезга.bool _isLongPressed;
// Переменные ниже хранят информацию машины состояний, сохраняющуюся
// между отдельными вызовами tick(). Они инициализируются один раз
// при старте программы, и обновляются каждый раз при вызове функции
// tick().
int _state;
unsignedlong _startTime; // Эта переменная будет установлена в state 1.
voidOBInit (void)
{
ClearBit(DDR_Enc, Btn_Enc); //настройка ножки кнопки на вход
SetBit(PORT_Enc, Btn_Enc); //вкл. подтягивающий резистор
_clickTicks =600; // Количество тиков, которое должно пройти, чтобы// было засчитано короткое нажатие на кнопку.
_pressTicks =1000; // Количество тиков, которое должно пройти, чтобы// было засчитано длинное нажатие на кнопку.
_state =0; // Начальное состояние state 0: в нем ожидается первое нажатие// на кнопку.
_isLongPressed =false; // Флаг, отслеживающий долгое нажатие.
_doubleClickFunc =NULL;
_pressFunc =NULL;
_longPressStartFunc =NULL;
_longPressStopFunc =NULL;
_duringLongPressFunc =NULL;
}
// Установка количества тиков (или миллисекунд, если вызовы tick происходят
// раз в миллисекунду), которое должно пройти, чтобы было детектировано