Машины для общения с пользователем

Следующее, что мы должны реализовать после подключения дисплея – это средства управления, общения с пользователем: у нас есть кнопки (одна основная, а вторая будет «для администрирования»: вызова глобальных настроек и калибровки), также у нас есть энкодер. При этом механический энкодер, по сути своей, - две кнопки.
Получается два уровня работы с кнопками: «физический» - тут мы будем избегать дребезга – и «логический», куда будет относиться работа с длиной нажатия кнопкок (при моделировании мы решили, что будем различать короткое и длительное нажатие на кнопку) и работа с энкодером.
В итоге получаем целых три виртуальных машины: машина «физических» кнопок (машина защиты от дребезга; знает о физическом расположении кнопок; возвращает информацию о том, в каком состоянии кнопка: отпущена или нажата, также сигнализирует, когда произошло нажатие или отпуск кнопки), машина «логических» кнопок (получает информацию от машины защиты от дребезга (при этом в этой машине уже не важно физическое расположение выводов кнопки), возвращает информацию о том, нажата ли кнопка и каким образом: кратким или долгим нажатием). Машина же энкодера тоже получает информацию от машины защиты от дребезга, а возвращать должна информацию о повороте энкодера в ту или иную сторону.

Машина «физических» кнопок (защиты от дребезга)

Начнём с машины защиты от дребезга.

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


Рисунок 1. Пример дребезга контактов.

Чтобы обойти дребезг, достаточно несколько раз за короткое время проверить состояние кнопки – если каждый раз получилось одно и то же значение – значит, оно «устойчиво»; на сам же дребезг, когда состояние кнопки меняется несколько раз за короткое время, просто не будем обращать внимания.
Определим вход и выход машины: на вход получаем расположение физических выводов кнопок; на выход же мы должны возвращать текущее очищенное от дребезга состояние кнопки (флаг), а также нам потребуются флажки, а точнее, посылки (тут вставить сноску на определение посылок и флагов, разницы между ними), которые будут извещать о нажатии на кнопку (передний фронт, очищенный от дребезга, переход от состояния «Кнопка отпущена» в состояние «Кнопка нажата») и о отпускании кнопки (задний фронт, переход от «Кнопка нажата» в «Кнопка отпущена»).
Так как в задаче фигурирует фраза «короткое время» - значит, будем использовать прерывание – то же самое, что и для работы с дисплеем. Нам нужно определять текущее состояние кнопки, а потом увеличивать или уменьшать некоторый счётчик; потом уже в основной программе мы будем сравнивать счётчик с максимумом – тогда считаем, что кнопка нажата – и с минимумом – тогда считаем, что кнопка отпущена.
Попробуем представить алгоритм, работающий в прерывании:


Рисунок 2. Алгоритм прерывания для защиты от дребезга с использованием счётчика.

Как видно – очень, очень много действий, при этом этот алгоритм выполняется для каждой «физической» кнопки – в нашем случае, 4 раза каждый раз, когда вызывается прерывание, а вызывается оно часто – этак ещё пара прерываний, и у нас не останется времени на основную программу!
Нужно сделать так, чтобы вся работа по расчёту состояния кнопки была в основном цикле, а в прерывании мы запоминали текущее состояние – и только!
Также нужно учитывать, что для анализа состояния кнопки нам нужно хранить несколько «текущих» состояний кнопки, и эти состояния могут быть только или 0, или 1. Поэтому мы можем просто взять один байт, и считать его «хранилищем состояний», очередью, где каждый бит – это текущее состояние кнопки.
У нас кнопки и энкодер подключены так, что когда на выводе «1» - кнопка отпущена, когда «0» - кнопка нажата. Получаем примерно такой вариант возможной очереди:

Исходное состояние: 11111111
Получаем новое состояние: <- 11111110
Потом получаем дребезг: <- 11111101
<- 11111010
<- 11110101
Дальше стабильное нажатие: <- 11101010
<- 11010100
<- 10101000
<- 01010000
...
<- 00000000

По умолчанию будем считать, что все кнопки отпущены:

currStateFIFO  = 0xFF

Тогда для того, чтобы запомнить текущее состояние, достаточно будет пары действий:

currStateFIFO  = currStateFIFO << 1;
if (ButtonPIN  & ButtonMask)
	currStateFIFO  |= 1;

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

У кнопки в данной машине может быть 4 состояния: «Ждём нажатия», «Передний фронт», «Ждём отпускания», «Задний фронт» (заметим, что понятия переднего и заднего фронта определяются именно логически, то есть переходами из отпущенного состояния к нажатому, и наоборот; при этом помним, что реально кнопка нажата, когда на выводе «логический 0»).


Рисунок 3. Алгоритм работы виртуальной машины «физических» кнопок (защиты от дребезга).

Немного пояснений: допустим, нам нужно проверить 5 раз состояние кнопки, чтобы убедиться, что дребезга нет.
Тогда выделяем маску для проверки последний 5 состояний в очереди: ButtonFIFOCheckMask будет равно 0x1F.
Дальше нужно определить, чему должны равняться эти 5 значений, чтобы считать кнопку нажатой или отпущенной: если кнопка нажата, то в очереди должно быть xxx00000, где xxx – старое состояние, которое нам не важно; если кнопка нажата, то в очереди должно быть xxx11111.
С виртуальной машиной разобрались, можно переходить ближе к коду.
Сначала определим переменные – для каждой «физической» кнопки создадим структуру, которая будет хранить входящие, выходящие данные и также внутренние переменные:

//состояния виртуальной машины физической кнопки (защита от дребезга)
typedef enum {pbsWaitPressing, pbsRisingEdge, pbsWaitReleasing, pbsFallingEdge} TVMPhysicalButtonState;

//сама структура виртуальной машины для физической кнопки (защиты от дребезга)
typedef struct 
{
	uint8_t* pbPin;								//указатели на выводы порта, к которому относится элемент
	uint8_t* pbDdr;
  	uint8_t* pbPort;
	uint8_t pbPinMask;							//маска вывода кнопки
	uint8_t pbCurrStateFIFO;					//очередь из "текущих состояний"
  	TVMPhysicalButtonState pbMachineState;
	//выходные данные - посылки и флаг
  	uint8_t pbOnRisingEdge: 1;
	uint8_t pbOnFallingEdge: 1;
	uint8_t pbIsOn: 1;
    
  	uint8_t pbPinNumber: 3;					    //номер вывода порта: 0-7, достаточно 3-х бит
} TVMPhysicalButton;

На что тут стоит обратить внимание: на описание выходных данных – они представляют собой битовые поля. Язык Си позволяет использовать в структурах битовые поля, имеющие вид:
тип имя: длина
Тип – это int, unsigned или signed
Имя – это, собственно, способ обращения к данному элементу структуры.
Длина – это количество бит, выделенных для данного поля.
Например:

struct TBitfieldExample
{
  int a: 9;
  int b: 4;
  uint8_t c: 3;
  uint8_t d: 4;
}

Так будет представлена эта структура в памяти микроконтроллера AVR:


Рисунок 4. Представление битовых полей в памяти

Отметим, что несмотря на то, что битовое поле c влезло бы в оставшуюся память к типу int, компилятор выделяет его отдельно, так как «тип текущего битового поля не совпадает с типом предыдущего битового поля».
Таким образом, для сохранения 4 различных переменных нам понадобится всего 3 байта, тогда как без использования битовых полей потребовалось бы 6 байт (вообще 5, если сделать b не int, а unsigned char).
Битовые поля, состоящие только из 1 бита, не могут относится к знаковому типу; так, для объявления однобитных полей можно использовать тип unsigned char, выделяя тем самым 8 бит. Мы используем неклассическую запись uint8_t, которая объявлена в библиотеке stdint.h - так сразу понятно, знаковая или беззнаковая переменная, а также её размер.
Для битовых полей есть ряд ограничений: например, нельзя получить адрес битового поля, а также нельзя создавать массивы битовых полей (но можно создавать массивы структур, содержащих битовые поля).
Также нужно учитывать, что операции с битовыми полями неатомарны, даже если битовые поля "однобайтный" тип (unsigned char или uint8_t) - значит, при использовании таких полей и в прерываниях, и в теле основной программы придётся запрещать прерывания (что всегда не очень желательно).


Начнём по порядку – в самом начале нам нужно проинициализировать данную структуру, причём как в рамках библиотеки: переменные, флаги и посылки - так и в основном цикле: здесь мы будем инициализировать сами порты ввода-вывода (настраивать ddr и port), а также укажем в расположение кнопок в структуре (pbDdr, pbPort, pbPin, pbPinMask).


Получаем вот такую библиотеку:
Заголовочный файл:

//работа с физическими кнопками (защита от дребезга)
#ifndef PHYSICAL_BUTTON_H
#define PHYSICAL_BUTTON_H

#include "ioavr.h"
#include "inavr.h"
#include "stdint.h"

//состояния виртуальной машины физической кнопки (защита от дребезга)
typedef enum {pbsWaitPressing, pbsRisingEdge, pbsWaitReleasing, pbsFallingEdge} TVMPhysicalButtonState;

//сама структура виртуальной машины для физической кнопки (защиты от дребезга)
typedef struct 
{
	uint8_t* pbPin;								//указатели на выводы порта, к которому относится элемент
	uint8_t* pbDdr;
	uint8_t* pbPort;
	uint8_t pbPinMask;							//маска вывода кнопки
	uint8_t pbCurrStateFIFO;					//очередь из "текущих состояний"
  TVMPhysicalButtonState pbMachineState;
	//выходные данные - посылки и флаг
	uint8_t pbOnRisingEdge: 1;
	uint8_t pbOnFallingEdge: 1;
	uint8_t pbIsOn: 1;
	
    uint8_t pbPinNumber: 3;					//номер вывода порта: 0-7, достаточно 3-х бит
} TVMPhysicalButton;

#define PHYSICAL_BUTTONS_COUNT 4
extern TVMPhysicalButton VMPhysicalButtons[PHYSICAL_BUTTONS_COUNT];

//прерывание для считывания текущего состояния кнопки
void PhysicalButtonMachineInterrupt();

//виртуальная машина для "физических кнопок" (защиты от дребезга)
void PhysicalButtonMachine();

//инициализация "физических кнопок"
void PhysicalButtonMachineInit();

#endif

Отметим, что в заголовочном файле указан массив для физических кнопок, но объявлен как «extern» - это значит, что переменная на самом деле объявляется в другом файле (файле .c или в основном файле проекта, например).
Теперь файл .c:

#include "PhysicalButton.h"

#define PHYSICAL_BUTTON_FIFO_CHECK_MASK 0x1F
#define PHYSICAL_BUTTON_FIFO_PRESSED 0
#define PHYSICAL_BUTTON_FIFO_RELEASED 0x1F

TVMPhysicalButton VMPhysicalButtons[PHYSICAL_BUTTONS_COUNT];

//прерывание для считывания текущего состояния кнопки
void PhysicalButtonMachineInterrupt()
{
  uint8_t i;
  TVMPhysicalButton* currVMPhysicalButton;
  currVMPhysicalButton = &VMPhysicalButtons[0];
  for (i = 0; i < PHYSICAL_BUTTONS_COUNT; i++)
  {
    currVMPhysicalButton->pbCurrStateFIFO <<= 1;                                //добавляем текущее состояние кнопки в очередь
    if (*(currVMPhysicalButton->pbPin) & currVMPhysicalButton->pbPinMask)
      currVMPhysicalButton->pbCurrStateFIFO |= 1;
    /*
    //или
    currVMPhysicalButton->pbCurrStateFIFO = (currVMPhysicalButton->pbCurrStateFIFO << 1) |
                                          ((*(currVMPhysicalButton->pbPin) >> currVMPhysicalButton->pbPinNumber) & 1); 
    */
    currVMPhysicalButton++;
  }
}

//виртуальная машина для "физических кнопок" (защиты от дребезга)
void PhysicalButtonMachine()
{
  uint8_t i;
  TVMPhysicalButton* currVMPhysicalButton;
  uint8_t currFIFOState;
  currVMPhysicalButton = &VMPhysicalButtons[0];
  for (i = 0; i < PHYSICAL_BUTTONS_COUNT; i++)
  {
    switch (currVMPhysicalButton->pbMachineState)
    {
    case pbsWaitPressing:                                                       //ждём нажатия - проверяем очередь
      __disable_interrupt();
      currFIFOState = currVMPhysicalButton->pbCurrStateFIFO;
      __enable_interrupt();
      if ((currFIFOState & PHYSICAL_BUTTON_FIFO_CHECK_MASK) == PHYSICAL_BUTTON_FIFO_PRESSED)
        currVMPhysicalButton->pbMachineState = pbsRisingEdge;
      break;
    case pbsRisingEdge:                                                         //передний фронт - отправляем посылку и возводим флаг
      currVMPhysicalButton->pbIsOn = 1;
      currVMPhysicalButton->pbOnRisingEdge = 1;
      currVMPhysicalButton->pbMachineState = pbsWaitReleasing;
      break;
    case pbsWaitReleasing:                                                      //ждём, когда кнопка будет отпущена - проверяем очередь
      __disable_interrupt();
      currFIFOState = currVMPhysicalButton->pbCurrStateFIFO;
      __enable_interrupt();
      if ((currFIFOState & PHYSICAL_BUTTON_FIFO_CHECK_MASK) == PHYSICAL_BUTTON_FIFO_RELEASED)
        currVMPhysicalButton->pbMachineState = pbsFallingEdge;
      break;
    case pbsFallingEdge:                                                        //задний фронт - отправляем посылку и опускаем флаг
      currVMPhysicalButton->pbIsOn = 0;
      currVMPhysicalButton->pbOnFallingEdge = 1;
      currVMPhysicalButton->pbMachineState = pbsWaitPressing;
      break;
    }
    currVMPhysicalButton++;
  }
}

//инициализация "физических кнопок"
void PhysicalButtonMachineInit()
{
  uint8_t i;
  for (i = 0; i < PHYSICAL_BUTTONS_COUNT; i++)
  {
    //определяем маску для вывода кнопки на основе номера вывода
    VMPhysicalButtons[i].pbPinMask = 1 << VMPhysicalButtons[i].pbPinNumber;
    //инициализируем кнопку на вход с подтяжкой
    *VMPhysicalButtons[i].pbPort |= VMPhysicalButtons[i].pbPinMask;
    *VMPhysicalButtons[i].pbDdr &= ˜VMPhysicalButtons[i].pbPinMask;
    //считаем по умолчанию, что кнопка отпущена
    VMPhysicalButtons[i].pbCurrStateFIFO = PHYSICAL_BUTTON_FIFO_RELEASED;
    //очищаем флаг и посылки, изначально считаем, что кнопка не нажата
    VMPhysicalButtons[i].pbIsOn = 0;
    VMPhysicalButtons[i].pbOnFallingEdge = 0;
    VMPhysicalButtons[i].pbOnRisingEdge = 0;
    VMPhysicalButtons[i].pbMachineState = pbsWaitPressing;
  }
}

Здесь все согласно схеме виртуальной машины.

Ну и основные изменения в основном файле:

/*
Использование выводов:
D0 - D7 - семисегментный дисплей, шина данных
B0 - B5 - семисегментный дисплей, шина адресации
C0 - АЦП, получение текущей температуры
C1 - управление нагревательным элементом
C2 - кнопка для глобальных настроек и калибровки
С3 - "основная" кнопка
C4 - C5 - энкодер
B6 - B7 - часовой кварц
*/

//выводим на дисплей нужные нам строки
#include "SegmentDisplay.h"
#include "PhysicalButton.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();									//машина "физических" кнопок - защита от дребезга
}

//инициализация кнопок и энкодера
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();											//инициализируем машину "физических" кнопок - защитыуот дребезга
}

void main( void )
{
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  DDRC = 0;
  PORTC = 0;
  
  DisplayInit();
  InitButtonsAndEncoder();
  FastTimerInit();
  while(1)
  {
    PhysicalButtonMachine();
    if (VMPhysicalButtons[1].pbOnRisingEdge)							//если передний фронт - принимаем посылку и пишем сообщение
    {
      DisplayShowStr("PRESS");
      VMPhysicalButtons[1].pbOnRisingEdge = 0;
    }  
    if (VMPhysicalButtons[1].pbOnFallingEdge)							//если задний фронт - принимаем посылку и пишем сообщение
    {
      DisplayShowStr("RELEASED");
      VMPhysicalButtons[1].pbOnFallingEdge = 0;
    }
  }
}

В результате при включении контроллера дисплей не горит; при нажатии на кнопку выводится сообщение «PrESS», при отпускании оно меняется на «rELEAS».

Вот ссылка на проект с выделенной библиотекой "физических" кнопок (защиты от дребезга).
А вот отдельно прошивка.


Наверх

Автор - Moriam  =ˆˆ=

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