Опрос кнопок в avr studio. Подключение кнопки к AVR

Итак мы добрались до неотъемлемой части большинства проектов на микроконтроллерах - до кнопок. Кнопка достаточно простое устройство, имеющее, как правило, всего два состояния, если говорить языком программирования это состояние логической 1 (контакты замкнуты) и логического 0 (контакты разомкнуты). Рассмотрим схему.

Имеем все туже схему с семисегментными индикаторами, но добавлены 4 кнопки. При помощи кнопок группы A будем увеличивать или уменьшать выводимое значение на первых трех индикаторах, а кнопками группы B – изменять значение на последних двух индикаторах.

Для начала кратко о кнопках. Кнопки применим с нормально разомкнутыми контактами без фиксации. Одним контактом подключим к земле, а другим к отдельным выводам микроконтроллера. Подтягивающий к плюсу резистор устанавливать не будем, так как таковой предусмотрен в самом микроконтроллере. Осталось только написать программу для опроса кнопок (состояния выводов микроконтроллера) и вывода результата на индикаторы. В связи с простотой схемы и затруднениями читателей в понимании программы на C, основной упор в этом разделе направим именно на разбор программы.

Итак программа.

#include //подключаем библиотеки #include #define SPI_SS PB2 //выход SS #define SPI_MOSI PB3 //выход MOSI #define SPI_MISO PB4 //выход MISO #define SPI_SCK PB5 //выход SCK #define BUTTON_AP PD4 //выход кнопки A+ #define BUTTON_AM PD5 //выход кнопки A- #define BUTTON_BP PD6 //выход кнопки B+ #define BUTTON_BM PD7 //выход кнопки B- char di; void spi(char cmd,char data) //Функция передачи двух пакетов по 8 бит по протоколу SPI { PORTB &= ~(1<999)a=999; //проверка достижения максимального значения переменной a if(a<0)a=0; //проверка достижения минимального значения переменной a if(b>99)b=99; //проверка достижения максимального значения переменной b if(b<0)b=0; //проверка достижения минимального значения переменной b } return 0; }

Приступим. Программу на C обычно начинают с подключения внешних библиотек. За это в программе отвечают две строки:

#include #include

avr/io.h это библиотека ввода/вывода которая объяснит компилятору какие порты ввода/вывода есть у микроконтроллера, как они обозначены и на что они способны. И самое интересное что эта библиотека сама выбирает из настроек проекта для какого микроконтроллера нужно применить описания, то позволяет использовать эту библиотеку для разных микроконтроллеров. Эту библиотеку нужно подключать в первую очередь.

Вторая часто используемая библиотека util/delay.h помогает создавать задержки в выполнении программы, что достаточно удобно.

#define BUTTON_AP PD4

указывает что на выводе PD4 ( регистр 4 порта B) мы будем подключать кнопку A+ ( смотрите схему ). Это нужно для того чтобы если мы вдруг надумаем кнопку переключить на другой вывод то нам не понадобится искать по всей программе, просто изменить в define название вывода PD4 на нужный, при этом в программе так и останется BUTTON_AP. Но в случае с выводами для SPI ничего изменить не получится потому что поддержка SPI аппаратная и жестко привязана к выводам микроконтроллера производителем.

Следующая интересная часть программы это описание портов.

DDRB |= (1<

Так были переключены на вывод перечисленные разряды порта B (по умолчанию все разряды всех портов настроены на ввод) . Э ту строку можно записать следующим образом без использования макроопределения в define

DDRB |= (1<

Эти записи равноценны и предназначены для подстановки 1 в регистр DDRB в соответствующий разряд BR3 ( разряд 3 ), BR5 ( разряд 5 ) и BR2 ( разряд 2 ). Остальные разряды регистра DDRB остаются без изменения. Также можно записать эту строку вот так

DDRB |= (1<<3)|(1<<5)|(1<<2);

что конечно более запутанно.

Запись 1 <<3 означает что двоичную единицу нужно сдвинуть влево на 3 позиции, позиции справа заполняются нулями.

1 <<3 будет означать 1000 (один ноль ноль ноль) в двоичной системе. И при выполнении операции (1<<3)|(1<<5) мы получим 101000 в двоичной системе.

В следующей строке программы подключаем подтягивающие резисторы для кнопок. Запишем единицы в соответствующие разряды регистра PORTD

PORTD |= (1<

Эти резисторы встроены в микроконтроллер. Их можно подключить на разряды порта с условием что эти разряды определены как ввод. Резисторы подтягивают нужный вывод микроконтроллера к логической 1.

Остальные настройки производятся аналогично с использованием соответствующих регистров микроконтроллера.

Теперь рассмотрим основную часть программы которую микроконтроллер после основных настроек выполняет бесконечно. Эта часть программы заключена в бесконечный цыкл.

While(1) { };

Все заключенное между фигурными скобками строки программы будут выполнятся по кругу. Внутри этого бесконечного цикла происходит поразрядное разбитие десятичного числа для вывода на отдельные разряды семисегментного индикатора. После разбиения отправляем поразрядно данные по SPI для каждого отдельного индикатора.

Delay_ms(100);

для предотвращения ложного срабатывания от «дребезга» контактов кнопки. И второй раз опрашиваем кнопки о их нажатии и сверяем выставленным ранее флагам о нажатии. Если условия соблюдены изменяем значения выводимые на индикаторы. Затем сбрасываем флаги нажатия кнопок в 0. После чего цикл повторяется.

В следующей - заключительной части будет рассмотрен АЦП (аналого-цифровой преобразователь) и применение всего ранее изученного на практике.

Мы провели эмуляцию схемы в программе Proteus, помигали светодиодом и научились прошивать наш виртуальный микроконтроллер. Наверняка многим из читателей пришла в голову мысль: “А можно ли помигать светодиодом, использую кнопку, подключенную к МК?

Да, разумеется, это возможно. Реализуется довольно легко. Причем можно сэмулировать кнопку как с фиксацией так и без фиксации. Причем в программе Proteus применить оба типа кнопок можно с помощью одного и того же одинакового макроса кнопки. В каких случаях это может быть полезно? Например, нам требуется осуществить выбор режимов работы устройства. Давайте разберем подробнее, как это реализовать с помощью микроконтроллера, и проведем эмуляцию в программе Proteus.


Для того, чтобы иметь наглядное представление, что у нас действительно выбор из двух режимов, мы соберем простенькую схемку на 4 светодиодах с управлением одной кнопкой. При первом варианте у нас поочередно загораются с первого по четвертый светодиоды. При втором варианте то же самое, но в обратной последовательнос ти, то есть с четвертого по первый. Единственное, что хочу уточнить, кнопка у нас опрашивается на нажатие или отжатие только перед началом эффекта. До тех пор, пока эффект не закончит свою работу, программа не реагирует на нажатие или отжатие кнопки.

Итак к делу. Так выглядит у нас наша схема в программе Proteus (кликните для увеличения):

В этой схеме мы уже видим отличия от той, которую собирали еще в прошлой статье. В левой части схемы мы видим обозначения кнопки и источника питания +5 вольт.

Как мы уже разобрали, питание и землю мы берем во вкладке “Терминал”. Обозначаются они у нас соответственно Power и Ground.

Обозначается у нас питание схемы треугольником с чертой, делящей его по высоте. Рядом, на рисунке, изображено обозначение кнопки. Справа от кнопки мы видим закрашенный красный круг с двухнаправленной стрелочкой. Если во время эмуляции нажать на него, то кнопка у нас зафиксируется и будет постоянно нажата. После повторного нажатия на него фиксация снимается.


Перед использованием нам нужно выбрать кнопку в библиотеке аналогично остальным деталям. Для этого нужно набрать в поле “Маска” слово “but”. Затем в поле “Результаты” слово “BUTTON”:


После этого кнопка появиться у нас в списке, вместе с выбранными деталями, применяемыми в проекте.

Какие порты у нас используются в проекте. Ниже на рисунке мы видим отходящие линии от портов РA0, РВ0, РВ1, РВ2 и РВ3. К порту В у нас подключены светодиоды, а к порту А – кнопка.

Итак, при нажатии, мы замыкаем цепь соединяющую +5 вольт с портом РА0 и верхним выводом резистора. Для чего у нас здесь вообще установлен резистор? Дело в том, что цепь кнопки должна быть замкнутой. После того как мы установили резистор, ток у нас течет от плюса питания через кнопку, резистор и дальше на землю.

Номинал резистора достаточно взять равным 200 Ом. Итак, когда мы нажимаем кнопку, мы соединяем порт РА0 с +5 вольт питания, и если мы опросим ножку РА0 на наличие напряжения или его отсутствие, мы сможем влиять на выполнение нашей программы.

Скрины с текстом нашей программы я привел ниже:


Итак отличия от прошлого проекта заключаются в том, что все 8 выводов порта РА мы конфигурируем на вход, выводы порта РВ0 – РВ3 мы конфигурируем на выход, а РВ4 – РВ7 на вход.


Затем мы используем в нашей программе проверку условия “ if”


Итак, мы видим в строчке после “if”, в скобках, условие выполнения. Код ниже выполняется, если на порту PA0 у нас присутствует логический ноль, или ноль вольт. Этот текст в скобках – сдвиг бита порта. Мы разберем в одной из следующих статей, а пока достаточно принять на веру, что этим мы опрашиваем кнопку на отжатие . Затем в фигурных скобках идет текст программы, который выполняется, если условие верно. Если условие не верно, программа продолжает выполняться дальше, пропустив текст в фигурных скобках.

Аналогично, с помощью условия “if” мы опрашиваем кнопку на нажатие . Обратите внимание, текст у нас в скобках изменился. Это означает что если на ножке РА0 у нас логическая единица, мы выполняем условие, то есть текст в фигурных скобках. То есть другими словами, у нас при отжатой кнопке, поочередно загораются и тухнут светодиоды с первого по четвертый, а при нажатии и удерживании, загораются и тухнут с четвертого по первый. Таким образом, мы можем влиять на выполнение программы, с помощью нажатия кнопки, опрашивая наличие на ней логического нуля, или логической единицы

Также , в котором находятся файл “сишник”, HEX и файл Протеуса.

А вот и видео

Почти ни одно изделие с микроконтроллером не обходится без кнопок. Тема эта уже избитая и во многом известная. Написанием этой статьи я не пытаюсь изобрести велосипед. Просто решил собрать всю инфу по схемотехнике воедино. Думаю, что материал будет полезен начинающим.Чтобы не сбивать вас с толку, на приведенных ниже рисунках не показаны схемы питания, сброса и тактирования микроконтроллеров.

Способ первый - традиционный

рис1а рис1б

Если кнопок немного и дефицита выводов мк не наблюдается, используем традиционный способ подключения.

Когда кнопка отпущена – вывод мк через резистор соединен с “плюсом” питания (рис. 1а). Когда кнопка нажата – вывод мк соединен с землей. Подтягивающий резистор R1 ограничивает силу тока в цепи переключателя. Если бы его не было, то при нажатии кнопки мы бы просто закоротили наш источник питания.

В большинстве современных микроконтроллеров есть встроенные подтягивающие резисторы, поэтому внешние можно не ставить (рис1б). В программе микроконтроллера нужно будет настроить используемый вывод на вход и включить внутренний подтягивающий резистор.

Что произойдет, если вывод микроконтроллера окажется в режиме выхода? Это будет зависеть от состояния этого вывода. Если на выводе “логический ноль” – ничего страшного не случиться, потому что - в первом случае (рис1а) величина втекающего тока ограничена резистором R1, а во втором случае (рис1б) никакой ток вообще не потечет. При нажатии кнопки тоже ничего не случиться, поскольку разность потенциалов между выводом и “землей” в этом случае будет равна нулю.

Если же на выводе будет ”логическая единица” и кнопка окажется нажатой, то через вывод микроконтроллера на землю потечет ток величиной в несколько десятков миллиампер и вывод порта может “погореть”. Предельно допустимый ток для вывода микроконтролера AVR согласно документации равен 40 мА. Поэтому иногда нелишним бывает поставить между выводом мк и кнопкой резистор номиналом в несколько сотен ом, например 330 (рис 1с). Так, например, подключены кнопки на отладочной плате STK500. Это сделано для подстраховки, чтобы пользователь нечаянно не спалил микроконтроллер в ходе своих эксперементов.

Для своих макетов впрочем можно обойтись и без этого резистора.

Второй способ - с использованием диодов

Используется когда кнопок больше двух, а выводы мк хочется сэкономить. Каждой кнопке в данном случае соответствует свой цифровой код, а количество кнопок, которые можно таким способом повесить на N выводов мк = 2 N - 1. То есть на три вывода можно повесить 7 кнопок, на четыре – 15 и так далее... но я бы больше 7-ми вешать не стал. Увеличивается количество дополнительных внешних компонентов, усложняется схема и программа мк. Кроме того, для большого количества кнопок есть и другие схемы включения. Подтягивающие резисторы на схеме не показаны, подразумевается, что используются внутренние.

Кстати, через диоды еще можно завести сигналы от кнопок на вывод внешнего прерывания контроллера (рис. 3). При нажатии любой кнопки вывод внешнего прерывания через диод будет замыкаться на землю и вызывать прерывание (естественно при условии, что оно настроено и разрешено). Таким образом контроллеру не нужно будет постоянно опрашивать кнопки, эта процедура будет запускаться только по событию внешнего прерывания.

Данная схема актуальна не для всех микроконтроллеров AVR, потому что в некоторых моделях микроконтроллеров внешнее прерывание может возникать по любому изменению на любом выводе. (например в ATmega164P)

Третий способ – для матричной клавиатуры

Такой вариант подключения обычно используется для блоков из нескольких кнопок, которые объединены конструктивно и соединены электрически по матричной схеме. Но никто не запрещает использовать эту схему и для включения обычных кнопок, однако реальную экономию она дает при количестве кнопок? 9.

Выводы PС0, PС1, PС2, PC3 – это строки матрицы, выводы PB0, PB1, PB2 – это столбцы матрицы. Кнопки можно опрашивать либо по строкам, либо по столбцам. Допустим, мы опрашиваем их по столбцам. Процедура опроса будет выглядеть следующим образом. Начальное состояние всех выводов – вход с включенным подтягивающим резистором. Устанавливаем вывод PB0 в режим выхода и выставляем ноль. Теперь нажатие кнопок S1, S2, S3, S4 будет замыкать выводы PС0, PС1, PС2, PC3 на 0 питания. Опрашиваем эти выводы и определям нажата ли какая-нибудь кнопка в данный момент. Устанавливаем вывод PB0 в режим выхода и включаем подтягивающий резистор. Устанавливаем вывод PB1 в режим выхода и выставляем ноль. Снова опрашиваем выводы PС0, PС1, PС2, PC3. Теперь нажатие кнопок S5, S6, S7, S8 будет замыкать выводы PС0, PС1, PС2, PC3. Последний столбец кнопок опрашиваем аналогично.

Строки матрицы можно завести через диоды на вывод внещнего прерывания. Тогда логику программы можно было бы построить так. Если клавиатура не используется в течении нескольких минут, микроконтроллер переходит в режим пониженного энергопотребления. При этом выводы PB0, PB1, PB2 – конфигурируются как выходы с нулевым логическим уровнем. Когда одна из кнопок нажимается, вывод прерывания через диод замыкается на ноль. Это вызывает внешнее прерывание, микроконтроллер просыпается и запускает таймер по сигналам которого происходит сканирование клавиатуры. Параллельно запускается счетчик времени, который сбрасывается при нажатии любой из кнопок. Как только он переполняется, микроконтроллер опять переходит в режим пониженного энергопотребления.

Задача: Разработаем программу управления одним светодиодом. При нажатии на кнопку светодиод горит, при отпускании гаснет.

Для начала разработаем принципиальную схему устройства. Для подключения к микроконтроллеру любых внешних устройств используются порты ввода-вывода. Каждый из портов способен работать как на вход так и на выход. Подключим светодиод к одному из портов, а кнопку к другому. Для этого опыта мы будем использовать контроллер Atmega8 . Эта микросхема содержит 3 порта ввода-вывода, имеет 2 восьмиразрядных и 1 шестнадцатиразрядный таймер/счетчик. Также на борту имеется 3-х канальный ШИМ, 6-ти канальный 10-ти битный аналого-цифровой преобразователь и многое другое. По моему мнению микроконтроллер прекрасно подходит для изучения основ программирования.

Для подключения светодиода мы будем использовать линию PB0, а для считывания информации с кнопки воспользуемся линией PD0. Схема приведена на рис.1.

Рис. 1

Через резистор R2 на вход PD0 подается плюс напряжения питания, что соответствует сигналу логической единице. При замыкании кнопки напряжение падает до нуля, что соответствует логическому нулю. В дальнейшем R2 можно исключить из схемы, заменяя его на внутренний нагрузочный резистор, введя необходимые настройки в программе. Светодиод подключен к выходу порта PB0 через токоограничивающий резистор R3. Для того чтобы зажечь светодиод надо подать в линию PB0 сигнал логической единицы. Задающий тактовый генератор будем использовать внутренний на 4MHz, так как в устройстве нет высоких требований к стабильности частоты.

Теперь пишем программу. Для написания программ я использую программную среду AVR Studio и WinAvr. Открываем AVR Studio, всплывает окошко приветствия, нажимаем кнопку "Создать новый проект" (New project), далее выбираем тип проекта - AVR GCC, пишем имя проекта например "cod1", ставим обе галочки "Создать папку проекта" и "Создать файл инициализации", нажимаем кнопку "Далее", в левом окошке выбираем "AVR Simulator", а в правом тип микроконтроллера "Atmega8", нажимаем кнопку "Финиш", открывается редактор и дерево категорий проекта - начальные установки закончены.

Для начала добавим стандартный текст описаний для Atmega8 с помощью оператора присоединения внешних файлов: #include

синтаксис директивы #include

#include <имя_файла.h>
#include “имя_файла.h”

Угловые скобки < и > указывают компилятору, что подключаемые файлы нужно сначала искать в стандартной папке WinAvr с именем include. Двойные кавычки “ и “ указывают компилятору начинать поиск с директории, в которой хранится проект.

Для каждого типа микроконтроллера есть свой заголовочный файл. Для ATMega8 этот файл называется iom8.h, для ATtiny2313 - iotn2313.h. В начале каждой программы мы должны подключать заголовочный файл того микроконтроллера, который мы используем. Но есть и общий заголовочный файл io.h. Препроцессор обрабатывает этот файл и в зависимости от настроек проекта включает в нашу программу нужный заголовочный файл.

Для нас первая строчка программы будет выглядеть вот так:

#include

Любая программа на языке Си должна обязательно содержать одну главную функцию. Она имеет имя main. Выполнение программы всегда начинается с выполнения функции main. У функции есть заголовок - int main(void) и тело - оно ограниченно фигурными скобками {}.

int main(void)
{
тело функции
}

В тело функции мы и будем добавлять наш код. Перед именем функции указывается тип возвращаемого значения. Если функция не возвращает значение - используется ключевое void .

int - это целое 2-х байтное число, диапазон значений от - 32768 до 32767

После имени функции в скобках () указываются параметры, которые передаются функции при ее вызове. Если функция без параметров - используется ключевое слово void . Функция main содержит в себе набор команд, настройки системы и главный цикл программы.

Далее настраиваем порт D на вход. Режим работы порта определяется содержимым регистра DDRD (регистр направления передачи информации). Записываем в этот регистр число "0x00" (0b0000000 - в двоичном виде), кроме кнопки к этому порту ничего не подключено, поэтому настраиваем весь порт D на вход. Настроить порт поразрядно можно записав в каждый бит регистра числа 0 или 1 (0-вход, 1-выход), например DDRD = 0x81 (0b10000001) - первая и последняя линия порта D работают на выход, остальные на вход. Необходимо также подключить внутренний нагрузочный резистор. Включением и отключением внутренних резисторов управляет регистр PORTx, если порт находится в режиме ввода. Запишем туда единицы.

Настраиваем порт B на выход. Режим работы порта определяется содержимым регистра DDRB . Ничего кроме светодиода к порту B не подключено, поэтому можно весь порт настроить на выход. Это делается записью в регистр DDRB числа "0xFF". Для того чтобы при первом включении светодиод не загорелся запишем в порт B логические нули. Это делается записью PORTB = 0x00;

Для присвоения значений используется символ "=" и называется оператором присваивания, нельзя путать со знаком "равно"

Настройка портов будет выглядеть так:

DDRD = 0x00;
PORTD = 0xFF;
DDRB = 0xFF;
PORTB = 0x00;

Пишем основной цикл программы. while ("пока" с англ.) - эта команда организует цикл, многократно повторяя тело цикла до тех пор пока выполняется условие, т. е пока выражение в скобках является истинным. В языке Си принято считать, что выражение истинно, если оно не равно нулю, и ложно, если равно.

Команда выглядит следующим образом:

while (условие)
{
тело цикла
}

В нашем случае основной цикл будет состоять лишь из одной команды. Эта команда присваивает регистру PORTB инвертируемое значение регистра PORTD .

PORTB = ~PIND; //взять значение из порта D, проинвертировать его и присвоить PORTB (записать в PORTB)

// выражения на языке Си читаются справа налево

PIND регистр ввода информации. Для того, чтобы прочитать информацию с внешнего вывода контроллера, нужно сначала перевести нужный разряд порта в режим ввода. То есть записать в соответствующий бит регистра DDRx ноль. Только после этого на данный вывод можно подавать цифровой сигнал с внешнего устройства. Далее микроконтроллер прочитает байт из регистра PINx . Содержимое соответствующего бита соответствует сигналу на внешнем выводе порта. Наша программа готова и выглядит так:

#include int main (void) { DDRD = 0x00; //порт D - вход PORTD = 0xFF; //подключаем нагрузочный резистор DDRB = 0xFF; //порт B - выход PORTB = 0x00; //устанавливаем 0 на выходе while(1) { PORTB = ~PIND; //~ знак поразрядного инвертирования } }

В языке Си широко используются комментарии. Есть два способа написания.

/*Комментарий*/
//Комментарий

При этом компилятор не будет обращать внимание на то что написано в комментарии.

Если используя эту же программу и подключить к микроконтроллеру 8 кнопок и 8 светодиодов, как показано на рисунке 2, то будет понятно что каждый бит порта D соответствует своему биту порта B . Нажимая кнопку SB1 - загорается HL1, нажимая кнопку SB2 - загорается HL2 и т.д.

Рисунок 2

В статье были использованы материалы из книги Белова А.В. "Самоучитель разработчика устройств на AVR"

Иногда приходится делать очень маленькое устройство, например, велокомпьютер. Или конструктив не позволяет разместить много кнопок. В общем есть у нас одна кнопка на ввод и ничего более.

Спартанские условия, но и тут можно развернуть мощный функционал, многоуровневые меню и прочие прелести жизни. сейчас я покажу одну из реализаций такого управления.

Итак, что у умеет наша кнопка?

  • Ее можно нажимать кратко
  • Можно жать длинно
  • Можно делать разные комбинации нажатий
  • Ее можно отпускать в нужный момент


Не густо, но вполне ничего. Для одной кнопки то. Главный затык при написании этого не просрать массу системных ресурсов (время процессора, таймеры и тыды) на обработку этой несчастной кнопки. Ведь нам придется отслеживать факт нажатия, факт отжатия, время нажатия, число нажатий с разным временем. Я видел такие адовые реализации этого интерфейса, что просто диву давался как можно нагородить столько тупняков и тормозов в этой одной сосне, да еще потратить все таймеры:)

Так что ТЗ выставим следующее:

  • Никаких аппаратных таймеров, кроме таймера диспетчера.
  • Никаких временных задержек, только вызов себя же по таймеру.
  • Никаких ожиданий нажатия-отжатия в цикле. Зашли, проверили — отдали управление.
  • Введем временной интервал mode_time, в течении которого будем отслеживать комбинацию нажатий. Скажем 2с
  • На выходе будем иметь число коротких и длинных нажатий за данный интервал

Алгоритм
Сделаем все на конечном автомате. У него будут три состояния:

  • Up — кнопка не нажата
  • Dn — кнопка нажата
  • Al — кнопка отпущена после длительного нажатия

А также будет одна служебная процедура, которая спустя mode_time (2c) после первого экшна с кнопкой сгребет все результаты и что-нибудь с ними сделает. Что — это уже не важно. От программы зависит.
И вся эта дребедень будет крутиться в цикле, вызывая сама себя через диспетчер (или каким еще образом) раз в 20мс.

Up
Входим.
Смотрим не нажата ли кнопка? Если нет — выходим. Если нажата, то переводим автомат в положение Dn
Проверяем первый ли раз за интервал мы тут? Если первый, то поставим нашу служебную процедуру на отложенный запуск (через 2с), взведем флаг, что процесс пошел.
Выходим.

Dn
Входим.
Еще нажата? Если нет, значит кнопка уже отпущена, скидываемся в состояние в Up и засчитываем одно короткое нажатие, увеличивая счетчик коротких нажатий cnt_s . Если еще нажата, то щелкаем счетчиком времени замера длительности нажатия Timе. Замер длительности у нас идет в итерациях автомата. Одна итерация 20мс. В лимит длинного нажатия я заложил 20 итераций, что дает около 400мс. Все что больше 0.4с считаем длинным нажатием. Как натикает больше 20 итераций, то засчитываем одно длинное нажатие и перекидываем автомат в состояние Al. Выходим.

Al
Входим.
Еще не отпустили? Если нет, то выходим. Если кнопка отпущена, то перебрасываемся в Up, скинув переменную Time.


За время mode_time, за те две секунды, сколько успеем натыкать — все наше. Запустится процедура анализа собранных данных и разгребет натыканное. Там уже все просто. Банальным case’ом делаем нужный нам экшн. Я вот, например, флажки выставляю которые перехватывает другая задача. Главное не блокировать эту задачу ничем тяжелым, чтобы не прозевать следующую комбинацию.

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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; // Переменные - флажки нажатых кнопок. // Эффективней их сделать битовыми полями // Но мне было лень. Оптимизируйте сами:) u16 bt_mode_time = 2000 ; // Длительность последовательности анализа // Сделано переменной, а не константой // чтобы можно было менять на лету. Снижая // там, где не надо ждать длительных комбинаций // что сильно повышает отзывчивость интерфейса u08 bt_cnt; // Флаг сигнализирующий, что идет анализ последовательности u08 bt_cnt_s; // Счетчик коротких нажатий u08 bt_cnt_l; // Счетчик длинных нажатий void bt_scan(void ) // Эта функция сканер. Она должна вызываться раз в 20мс { #define up 0 // Дефайны состояний автомата. 0 - по дефолту. #define dn 1 #define al 3 static u08 bt_state= 0 ; // Переменная состояния автомата static u08 bt_time = 0 ; // Переменная времени работы автомата switch (bt_state) // Собственно автомат { case up: { if (Close) // Если нажата кнопка { bt_state = dn; // Стадию в Down bt_time = 0 ; // Обнуляем счетчик времени if (bt_cnt== 0 ) // Если первый раз в комбинации { SetTimerTask(bt_ok, bt_mode_time) ; // Запускаем процедуру анализа bt_cnt = 1 ; // Подсчет пошел! } } break ; // Выход } case dn: { if (Close) // Все еще нажато? { if (20 > bt_time) // Нажато меньше чем 20*20мс? { // Да bt_time++; // Увеличиваем счетчик итераций } else { bt_state = al; // Нет, уже больше! Да у нас длинное нажатие! Переходим в АЛ bt_time = 0 ; // Сбрасываем время замера нажатия bt_cnt_l++; // Засчитываем одно длинное нажатие } } else { bt_state = up; // Кнопка отпущена? bt_time = 0 ; // Время замера в ноль bt_cnt_s++; // Счетчик коротких нажатий } break ; // Выход } case al: // А тут мы если было длинное нажатие { if (Open) // Отпустили? { bt_state = up; // Да! Стадию в Up bt_time = 0 ; // Время в 0 bt_al = 1 ; // Зафиксировали событие "Отпускание после длинного" } break ; } } SetTimerTask(bt_scan, 20 ) ; // Зациклились через диспетчер. } // А это функция которая через 2 секунды сработает и подберет все результаты подсчета void bt_ok(void ) // Ловим дешифровку событий тут { switch (bt_cnt_s) // Смотрим сколько нажатий коротких { case 1 : bt1 = 1 ; break ; // Такой флажок и ставим case 2 : bt2 = 1 ; break ; case 3 : bt3 = 1 ; break ; case 4 : bt4 = 1 ; break ; case 5 : bt5 = 1 ; break ; default : break ; } switch (bt_cnt_l) // Смотрим сколько нажатий длинных { case 1 : bt_l = 1 ; break ; // Такой флажок и ставим case 2 : bt_l2 = 1 ; break ; default : break ; } bt_cnt = 0 ; // Сбрасываем счетчики bt_cnt_s = 0 ; bt_cnt_l = 0 ; }

#include u08 bt1,bt2,bt3,bt4,bt5,bt_l,bt_l2,bt_al; // Переменные - флажки нажатых кнопок. // Эффективней их сделать битовыми полями // Но мне было лень. Оптимизируйте сами:) u16 bt_mode_time = 2000; // Длительность последовательности анализа // Сделано переменной, а не константой // чтобы можно было менять на лету. Снижая // там, где не надо ждать длительных комбинаций // что сильно повышает отзывчивость интерфейса u08 bt_cnt; // Флаг сигнализирующий, что идет анализ последовательности u08 bt_cnt_s; // Счетчик коротких нажатий u08 bt_cnt_l; // Счетчик длинных нажатий void bt_scan(void) // Эта функция сканер. Она должна вызываться раз в 20мс { #define up 0 // Дефайны состояний автомата. 0 - по дефолту. #define dn 1 #define al 3 static u08 bt_state=0; // Переменная состояния автомата static u08 bt_time =0; // Переменная времени работы автомата switch(bt_state) // Собственно автомат { case up: { if(Close) // Если нажата кнопка { bt_state = dn; // Стадию в Down bt_time = 0; // Обнуляем счетчик времени if(bt_cnt==0) // Если первый раз в комбинации { SetTimerTask(bt_ok,bt_mode_time); // Запускаем процедуру анализа bt_cnt =1; // Подсчет пошел! } } break; // Выход } case dn: { if(Close) // Все еще нажато? { if (20 > bt_time) // Нажато меньше чем 20*20мс? { // Да bt_time++; // Увеличиваем счетчик итераций } else { bt_state = al; // Нет, уже больше! Да у нас длинное нажатие! Переходим в АЛ bt_time = 0; // Сбрасываем время замера нажатия bt_cnt_l++; // Засчитываем одно длинное нажатие } } else { bt_state = up; // Кнопка отпущена? bt_time = 0; // Время замера в ноль bt_cnt_s++; // Счетчик коротких нажатий } break; // Выход } case al: // А тут мы если было длинное нажатие { if(Open) // Отпустили? { bt_state = up; // Да! Стадию в Up bt_time = 0; // Время в 0 bt_al = 1; // Зафиксировали событие "Отпускание после длинного" } break; } } SetTimerTask(bt_scan,20); // Зациклились через диспетчер. } // А это функция которая через 2 секунды сработает и подберет все результаты подсчета void bt_ok(void) // Ловим дешифровку событий тут { switch(bt_cnt_s) // Смотрим сколько нажатий коротких { case 1: bt1 = 1; break; // Такой флажок и ставим case 2: bt2 = 1; break; case 3: bt3 = 1; break; case 4: bt4 = 1; break; case 5: bt5 = 1; break; default: break; } switch(bt_cnt_l) // Смотрим сколько нажатий длинных { case 1: bt_l = 1; break; // Такой флажок и ставим case 2: bt_l2 = 1; break; default: break; } bt_cnt = 0; // Сбрасываем счетчики bt_cnt_s = 0; bt_cnt_l = 0; }

Код написан так, что на AVR там завязана буквально пара строчек. По крайней мере в коде обработчика нажатий кнопки. Все привязки на железо идут в хидере, да и их там всего ничего:

1 2 3 4 5 6 7 8 9 10 11 #include #include #define Open (BTN_PIN & 1< #define Close (!Open) extern void bt_scan(void ) ; void bt_ok(void ) ; extern u08 bt1, bt2, bt3, bt4, bt5, bt_l, bt_l2, bt_al; extern u16 bt_mode_time;

#include #include #define Open (BTN_PIN & 1<

Так что портировать это на другую архитектуру дело смены двух строчек. Ну и, возможно, вам потребуется изменить механизм автозапуска и запуска функции обработчика. Если вы будете использовать какой то свой диспетчер, ОС или еще какую систему организации прошивки. Но это две строчки в коде поправить.

Все описанное в статье мясо лежит в двух файлах button.c и button.h

Видео работы

Дребезг
Боротся с дребезгом тут уже не обязательно. Т.к. частота сканирования небольшая, так что даже голимая и наглухо окисленная кнопка модели ТМ2 не давала дребезга — он кончался раньше, чем наступал следующий скан. А вот что тут можно докурить, так это защиту от ложных сработок в результате наводок. Ведь стоит помехе продавить линию в момент считывания и засчитается сработка однократного нажатия. Это можно избежать сделав проверочные состояния автомата. Скажем добавив в Up счетчик итераций, чтобы в течении, скажем, двух-трех итераций подтвердить, что кнопка таки нажата и только тогда переходить в Dn.

Варианты
Правда в своем проекте я несколько изменил обработку. Т.к. мне не нужны были множественные длинные нажатия, то я сделал выставление флага «Длинное нажатие» сразу же в обработчике AL и, заодно, убрал подсчет числа длинных нажатий. Что позволило повысить отзывчивость работы интерфейса прибора, где длительным нажатием осуществлялся вход в пункт меню, а комбинаций с двумя длинными нажатиями не использовались вообще.

Ну и, разумеется, этот же конечный автомат можно поставить на разбор чего угодно. Например на отслеживание какого-либо другого флажка, от матричной клавиатуры или какого сигнального устройства.