«Логические» кнопки

С дребезгом мы разобрались, теперь переходим на уровень выше и учимся отличать долгое нажатие от короткого – создаем виртуальную машину для «логических» кнопок.
На вход мы получаем сообщения от машины защиты от дребезга: посылки (сообщения) «OnRisingEdge» и «OnFallingEdge»; на выход, в соответствии с задачей, должны выдавать посылки «OnShortPress» или «OnLongPress».
Определять длительность нажатия на кнопку просто – достаточно засечь время от прихода посылки  «OnRisingEdge» до прихода «OnFallingEdge»; но вот досада – имеющееся прерывание вызывается слишком часто, а нам нужно считать секунды (для человека длительное нажатие на кнопку – это минимум секунда - полторы; не слишком долго, но и ощутимо отличается от кратковременного быстрого нажатия).
Поэтому будем использовать ещё один таймер – так как в дальнейшем нам все равно придётся отсчитывать минуты (для таймера уже самой печки), то попробуем подключить часовой кварц (на самом деле, это излишество и стоит воспользоваться все-таки обычным таймером - так мы сэкономим целых два вывода; но поскольку проект учебный, то не будем себя ограничивать). С часовым кварцем работает таймер/счётчик 2 в асинхронном режиме. Кварц подключается напрямую к выводам TOSC1-TOSC2, а дальше следуем последовательности действий из даташита:

  1. Отключаем прерывания таймера/счётчика2: очищаем флаги, разрешаюшие прерывания таймера/счётчика2 в регистре TIMSK2.
  2. Включаем асинхронный режим: возводим бит AS2 регистра ASSR.
  3. Дальше можно настраивать работу таймера с помощью регистров TCCR2A и TCCR2B: устанавливаем его в нормальном режиме с предделителем 8 – тогда прерывание будет вызываться каждую 1/16 с.
  4. Очищаем текущее значение счётчика.
  5. Дальше нужно дождаться, пока наши настроенные регистры перезапишутся – в асинхронном режиме запись в регистры синхронизируется с тактовым сигналом таймера/счётчика; за готовность к дальнейшей работе (флаги Осторожно, идёт запись!» отвечают биты TCN2UB, TCR2AUB, TCR2BUB регистра ASSR – как только все они будут сняты, можно продолжать.
  6. Сбрасываем флаги прерываний в регистре TIFR2.
  7. Разрешаем нужное нам прерывание – прерывание по переполнению таймера 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».

Вот программа и прошивка

Наверх

Автор - Moriam  =ˆˆ=

Обсудить на форуме