Машины для общения с пользователем
Следующее, что мы должны реализовать после подключения дисплея – это средства управления, общения с пользователем: у нас есть кнопки (одна основная, а вторая будет «для администрирования»: вызова глобальных настроек и калибровки), также у нас есть энкодер. При этом механический энкодер, по сути своей, - две кнопки.
Получается два уровня работы с кнопками: «физический» - тут мы будем избегать дребезга – и «логический», куда будет относиться работа с длиной нажатия кнопкок (при моделировании мы решили, что будем различать короткое и длительное нажатие на кнопку) и работа с энкодером.
В итоге получаем целых три виртуальных машины: машина «физических» кнопок (машина защиты от дребезга; знает о физическом расположении кнопок; возвращает информацию о том, в каком состоянии кнопка: отпущена или нажата, также сигнализирует, когда произошло нажатие или отпуск кнопки), машина «логических» кнопок (получает информацию от машины защиты от дребезга (при этом в этой машине уже не важно физическое расположение выводов кнопки), возвращает информацию о том, нажата ли кнопка и каким образом: кратким или долгим нажатием). Машина же энкодера тоже получает информацию от машины защиты от дребезга, а возвращать должна информацию о повороте энкодера в ту или иную сторону.
Машина «физических» кнопок (защиты от дребезга)
Начнём с машины защиты от дребезга.Дребезг контактов — явление, происходящее в электромеханических устройствах (кнопках, реле, переключателях и др.), длящееся некоторое время после замыкания электрических контактов. После замыкания происходят многократные неконтролируемые замыкания и размыкания контактов за счет упругости материалов и деталей контактной системы — некоторое время контакты «подпрыгивают» при соударениях, размыкая и замыкая электрическую цепь.
Рисунок 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 =ˆˆ=