«Логические» кнопки
С дребезгом мы разобрались, теперь переходим на уровень выше и учимся отличать долгое нажатие от короткого – создаем виртуальную машину для «логических» кнопок.
На вход мы получаем сообщения от машины защиты от дребезга: посылки (сообщения) «OnRisingEdge» и «OnFallingEdge»; на выход, в соответствии с задачей, должны выдавать посылки «OnShortPress» или «OnLongPress».
Определять длительность нажатия на кнопку просто – достаточно засечь время от прихода посылки «OnRisingEdge» до прихода «OnFallingEdge»; но вот досада – имеющееся прерывание вызывается слишком часто, а нам нужно считать секунды (для человека длительное нажатие на кнопку – это минимум секунда - полторы; не слишком долго, но и ощутимо отличается от кратковременного быстрого нажатия).
Поэтому будем использовать ещё один таймер – так как в дальнейшем нам все равно придётся отсчитывать минуты (для таймера уже самой печки), то попробуем подключить часовой кварц (на самом деле, это излишество и стоит воспользоваться все-таки обычным таймером - так мы сэкономим целых два вывода; но поскольку проект учебный, то не будем себя ограничивать). С часовым кварцем работает таймер/счётчик 2 в асинхронном режиме. Кварц подключается напрямую к выводам TOSC1-TOSC2, а дальше следуем последовательности действий из даташита:
- Отключаем прерывания таймера/счётчика2: очищаем флаги, разрешаюшие прерывания таймера/счётчика2 в регистре TIMSK2.
- Включаем асинхронный режим: возводим бит AS2 регистра ASSR.
- Дальше можно настраивать работу таймера с помощью регистров TCCR2A и TCCR2B: устанавливаем его в нормальном режиме с предделителем 8 – тогда прерывание будет вызываться каждую 1/16 с.
- Очищаем текущее значение счётчика.
- Дальше нужно дождаться, пока наши настроенные регистры перезапишутся – в асинхронном режиме запись в регистры синхронизируется с тактовым сигналом таймера/счётчика; за готовность к дальнейшей работе (флаги Осторожно, идёт запись!» отвечают биты TCN2UB, TCR2AUB, TCR2BUB регистра ASSR – как только все они будут сняты, можно продолжать.
- Сбрасываем флаги прерываний в регистре TIFR2.
- Разрешаем нужное нам прерывание – прерывание по переполнению таймера 2.
Код выглядит так:
//инициализация таймера 2 на часовом кварце (при этом необходимо "включить" фьюз CKOPT, если он есть) //часовой кварц (32768 Гц) с предделителем 8; прерывание вызывается раз в (256 * 8 / 32768 = 1/16 с) void ClockTimerInit() { TIMSK2 &= ˜((1 << TOIE2) | (1 << OCIE2A) | (1 << OCIE2B)); //запрещаем прерывания от таймера 2 ASSR |= (1 << AS2); //включаем асинхронный режим TCCR2A = 0; //таймер 2 будет включен в нормальном режиме TCCR2B = (1 << CS21); //с предделителем 8 TCNT2 = 0; while (ASSR & (1 << TCN2UB | 1 << TCR2AUB | 1 << TCR2BUB)); //ждём, пока все запишется в свои регистры TIFR2 &= ˜(1 << OCF2A | 1 << OCF2B | 1 << TOV2); //очищаем флаги прерываний TIMSK2 |= (1 << TOIE2); //разрешаем прерывание по переполнению таймера 2 }
На микросхеме ATmega168 этого достаточно, чтобы включить асинхронный режим. Однако если у вас другая микросхема – например, ATmega8 - то нужно включить фьюз CKOPT.
В прерывании, соответственно, мы будем только проверять, нужно ли нам увеличивать счётчик – и при необходимости делать это. Так как прерывание вызывается не очень часто, то мы можем позволить себе использовать условный оператор с двумя условиями: нужно, чтобы флаг для работы счётчика был взведен и счётчик не превышал максимального значения (теоретически, хоть и маловероятно, может произойти переполнение – счётчик увеличивается каждые 1/16 секунды, значит, чтобы переполнился байт, нужно 255 * 1/16 = ˜16 с – и тогда мы не отследим длительное нажатие).
Ну а дальше продумаем виртуальную машину.
Вариант 1.
Рисунок 1. Вариант простой виртуальной машины логических кнопок.
Вариант достаточно простой, но главный его недостаток в том, что пользователь узнает о том, какое было нажатие, только после того, как отпустит кнопку. Таким образом, он никогда не может быть уверен, что держал кнопку достаточно долго – не слишком-то интуитивный интерфейс получается; намного лучше, когда контроллер реагирует на длительное нажатие на кнопку сразу, как только счётчик длительности достиг максимума – тогда это будет наглядно.
Поэтому виртуальная машина усложняется: вариант 2.
Рисунок 2. Вариант усовершенствованной виртуальной машины логических кнопок.
Вот, так лучше: как только счётчик длительности нажатия достиг максимума, можно отсылать посылку «Длительное нажатие», и потом просто ждать заднего фронта.
В данном случае мы используем комбинированную виртуальную машину (тут ссылка): например, в состоянии «Короткое нажатие», если счётчик длительности нажатия не достиг ещё максимума, мы делаем проверку на отпуск кнопки в том же состоянии – проверка не занимает много времени, поэтому нет смысла выносить её в отдельное состояние.
Если счётчик длительности нажатия включён и не достиг ещё максимума, то он увеличивается на единицу каждое прерывание часового таймера; нам останется определить значение этого самого максимума, и вуаля – логика виртуальной машины продумана!
Нужно также отметить, что и сейчас, и в дальнейшем нам потребуются данные о длительности секунды, минуты, половины минуты и так далее (так, например, длительность длительного нажатия мы определили как 2 секунды) - поэтому лучше вынести константы времени в отдельный файл.
Попробуем все это реализовать.
Файл с константами времени "Timings.h":
#ifndef TIMINGS_H #define TIMINGS_H #define CLOCK_TIMER_INTERRUPTS_PER_SECOND 16 #define CLOCK_TIMER_INTERRUPTS_PER_MINUTE 960 #define CLOCK_TIMER_INTERRUPTS_PER_HALFMINUTE 480 #endif
Заголовочный файл:
#ifndef LOGICAL_BUTTON_H #define LOGICAL_BUTTON_H #include "stdint.h" #include "PhysicalButton.h" typedef enum {lbsWaitPressing, lbsRisingEdge, lbsShortPress, lbsSwitchToLongPress, lbsLongPress, lbsFallingEdge} TVMLogicalButtonState; typedef struct { TVMPhysicalButton *lbVMPhysicalButton; //указатель на "физическую" кнопку, откуда мы будем получать сообщения о фронтах volatile uint8_t lbPressDurationCounter; //счётчик длительности нажатия TVMLogicalButtonState lbMachineState; volatile uint8_t lbPressDurationCounterOn: 1; //флаг включения счётчика длительности нажатия //исходящие сообщения uint8_t lbOnShortPress: 1; uint8_t lbOnLongPress: 1; } TVMLogicalButton; #define LOGICAL_BUTTONS_COUNT 2 extern TVMLogicalButton VMLogicalButtons[LOGICAL_BUTTONS_COUNT]; #define LB_LONG_PRESS_DURATION_COUNTER (CLOCK_TIMER_INTERRUPTS_PER_SECOND * 2) //инициализация логических кнопок void LogicalButtonMachineInit(); //прерывание для работы с логическими кнопками void LogicalButtonMachineInterrupt(); //виртуальная машина для работы с логическими кнопками void LogicalButtonMachine(); #endif
Здесь важное замечание: как мы помним, операции с битовыми полями неатомарны; поэтому ииспользовать битовое поле и в прерывании, и в основной программе нужно осторожно. В нашем случае, если смотреть по схеме, ничего страшного произойти не может: в прерывании мы только считываем битовое поле, расположенное в однобайтовой переменной, - то есть вызов прерывания точно не "разорвёт" изменение этой переменной; а так как в прерывании мы значение поля не изменяем, то и в основной программе нам бояться нечего.
Файл .c:
#include "LogicalButton.h" TVMLogicalButton VMLogicalButtons[LOGICAL_BUTTONS_COUNT]; //инициализация логических кнопок void LogicalButtonMachineInit() { uint8_t i; //все посылки и флаги очищены; считаем, что кнопка по умолчанию отпущена for (i = 0; i < LOGICAL_BUTTONS_COUNT; i++) { VMLogicalButtons[i].lbOnLongPress = 0; VMLogicalButtons[i].lbOnShortPress = 0; VMLogicalButtons[i].lbPressDurationCounter = 0; VMLogicalButtons[i].lbPressDurationCounterOn = 0; VMLogicalButtons[i].lbMachineState = lbsWaitPressing; } } //прерывание для работы с логическими кнопками void LogicalButtonMachineInterrupt() { uint8_t i; TVMLogicalButton *currVMLogicalButton; currVMLogicalButton = &VMLogicalButtons[0]; for (i = 0; i < LOGICAL_BUTTONS_COUNT; i++) { //если для выбранной кнопки активен счётчик длительности нажатия, и он ещё не достиг максимального значения, то увеличиваем его на единицу if ((currVMLogicalButton->lbPressDurationCounterOn) && (currVMLogicalButton->lbPressDurationCounter < LB_LONG_PRESS_DURATION_COUNTER)) currVMLogicalButton->lbPressDurationCounter++; currVMLogicalButton++; } } //виртуальная машина для работы с логическими кнопками void LogicalButtonMachine() { uint8_t i, currPressDurationCounter; TVMLogicalButton *currVMLogicalButton; currVMLogicalButton = &VMLogicalButtons[0]; for (i = 0; i < LOGICAL_BUTTONS_COUNT; i++) { switch (currVMLogicalButton->lbMachineState) { case lbsWaitPressing: if (currVMLogicalButton->lbVMPhysicalButton->pbOnRisingEdge == 1) //если пришла посылка о переднем фронте - переходим в следующее состояние currVMLogicalButton->lbMachineState = lbsRisingEdge; break; case lbsRisingEdge: //очищаем посылку, включаем счётчик, переходим в следующее состояние currVMLogicalButton->lbVMPhysicalButton->pbOnRisingEdge = 0; currVMLogicalButton->lbPressDurationCounter = 0; currVMLogicalButton->lbPressDurationCounterOn = 1; currVMLogicalButton->lbMachineState = lbsShortPress; break; case lbsShortPress: //считаем, что пока короткое нажатие; проверяем на длинное нажатие и на задний фронт __disable_interrupt(); currPressDurationCounter = currVMLogicalButton->lbPressDurationCounter; __enable_interrupt(); //проверяем счётчик на длинное нажатие if (currPressDurationCounter >= LB_LONG_PRESS_DURATION_COUNTER) currVMLogicalButton->lbMachineState = lbsSwitchToLongPress; else //если нажатие пока короткое - проверяем на отпуск кнопки { if (currVMLogicalButton->lbVMPhysicalButton->pbOnFallingEdge == 1) currVMLogicalButton->lbMachineState = lbsFallingEdge; } break; case lbsSwitchToLongPress: //определили, что нажатие длительное - останавливаем счётчик, отправляем посылку о длительном нажатии currVMLogicalButton->lbPressDurationCounterOn = 0; currVMLogicalButton->lbOnLongPress = 1; currVMLogicalButton->lbMachineState = lbsLongPress; break; case lbsLongPress: //определено, что нажатие длительное - осталось дождаться, когда кнопку отпустят if (currVMLogicalButton->lbVMPhysicalButton->pbOnFallingEdge == 1) currVMLogicalButton->lbMachineState = lbsFallingEdge; break; case lbsFallingEdge: //задний фронт - если нажатие было кратким, нужно отправить соответствующую посылку //очищаем посылку currVMLogicalButton->lbVMPhysicalButton->pbOnFallingEdge = 0; //отключаем счётчик currVMLogicalButton->lbPressDurationCounterOn = 0; //если нажатие было кратким - нужно отправить посылку //не запрещаем прерывания, так как счётчик отключен if (currVMLogicalButton->lbPressDurationCounter < LB_LONG_PRESS_DURATION_COUNTER) currVMLogicalButton->lbOnShortPress = 1; currVMLogicalButton->lbMachineState = lbsWaitPressing; break; } currVMLogicalButton++; } }
Ну и основной файл:
/* Использование выводов: D0 - D7 - семисегментный дисплей, шина данных B0 - B5 - семисегментный дисплей, шина адресации C0 - АЦП, получение текущей температуры C1 - управление нагревательным элементом C2 - кнопка для глобальных настроек и калибровки С3 - "основная" кнопка C4 - C5 - энкодер B6 - B7 - часовой кварц */ //выводим на дисплей нужные нам строки #include "SegmentDisplay.h" #include "PhysicalButton.h" #include "LogicalButton.h" #include "ioavr.h" #include "inavr.h" #include "stdint.h" //инициализация таймера Т0 void FastTimerInit() { __disable_interrupt(); TCCR0A = 0; TCCR0B = (1 << CS01); //таймер 0 включён с предделителем 8 в нормальном режиме; TIMSK0 |= (1 << TOIE0); //разрешили прерывание по переполнению Т0 __enable_interrupt(); } //прерывание таймера Т0 #pragma vector = TIMER0_OVF_vect __interrupt void FastTimerInterrupt(void) { RedrawDispInterrupt(); //перерисовка дисплея PhysicalButtonMachineInterrupt(); //машина "физических" кнопок - защита от дребезга } //инициализация таймера 2 на часовом кварце (при этом необходимо "включить" фьюз CKOPT, если он есть) //часовой кварц (32768 Гц) с предделителем 8; прерывание вызывается раз в (256 * 8 / 32768 = 1/16 с) void ClockTimerInit() { TIMSK2 &= ˜((1 << TOIE2) | (1 << OCIE2A) | (1 << OCIE2B)); //запрещаем прерывания от таймера 2 ASSR |= (1 << AS2); //включаем асинхронный режим TCCR2A = 0; //таймер 2 будет включен в нормальном режиме TCCR2B = (1 << CS21); //с предделителем 8 TCNT2 = 0; while (ASSR & (1 << TCN2UB | 1 << TCR2AUB | 1 << TCR2BUB)); //ждём, пока все запишется в свои регистры TIFR2 &= ˜(1 << OCF2A | 1 << OCF2B | 1 << TOV2); //очищаем флаги прерываний TIMSK2 |= (1 << TOIE2); //разрешаем прерывание по переполнению таймера 2 } //прерывание таймера 2 на часовом кварце #pragma vector = TIMER2_OVF_vect __interrupt void ClockTimerInterrupt(void) { LogicalButtonMachineInterrupt(); //работа с счётчиком длительности нажатия для логических кнопок } //инициализация кнопок и энкодера void InitButtonsAndEncoder() { uint8_t i; //определяем местоположение физических кнопок (4 штуки) for (i = 0; i < PHYSICAL_BUTTONS_COUNT; i++) { //записываем физические данные в структуру VMPhysicalButtons[i].pbPin = (uint8_t*)&PINC; VMPhysicalButtons[i].pbDdr = (uint8_t*)&DDRC; VMPhysicalButtons[i].pbPort = (uint8_t*)&PORTC; VMPhysicalButtons[i].pbPinNumber = i + 2; } PhysicalButtonMachineInit(); //инициализируем машину "физических" кнопок - защиты от дребезга VMLogicalButtons[0].lbVMPhysicalButton = &VMPhysicalButtons[1]; VMLogicalButtons[1].lbVMPhysicalButton = &VMPhysicalButtons[0]; LogicalButtonMachineInit(); //инициализируем структуры логических кнопок } void main( void ) { DDRB = 0; PORTB = 0; DDRD = 0; PORTD = 0; DDRC = 0; PORTC = 0; DisplayInit(); InitButtonsAndEncoder(); FastTimerInit(); ClockTimerInit(); while(1) { PhysicalButtonMachine(); LogicalButtonMachine(); if (VMLogicalButtons[0].lbOnShortPress) //если короткое нажатие на кнопку - принимаем посылку и пишем сообщение { DisplayShowStr("SHORT"); VMLogicalButtons[0].lbOnShortPress = 0; } if (VMLogicalButtons[0].lbOnLongPress) //если длительное нажатие на кнопку - принимаем посылку и пишем сообщение { DisplayShowStr("LONG"); VMLogicalButtons[0].lbOnLongPress = 0; } } }
Не забываем инициализировать все, что мы добавили: в этот раз часовой таймер и логические кнопки (нужно также прописать им ссылки на соответствующие «физические» кнопки!)
В результате при долгом нажатии на кнопку будет высвечиваться надпись «Lon» (букву ‘G’ на семисегментном дисплее сложно отобразить), а при коротком – надпись «SHort».