Базовый интерфейс
Так, сейчас у нас есть многие «кирпичики» по отдельности: есть машина логических кнопок, машина энкодера и библиотеки для работы с семисегментным дисплеем и с АЦП, а также библиотека и виртуальная машина кухонного таймера. Попробуем сделать виртуальную машину, которая находится на уровень выше всех этих «кирпичиков» - основную машину, включающую работу с пользователем (пользовательский интерфейс) и работу непосредственно с печкой (нагрев и поддержание заданной температуры, «кухонный» таймер). На первом этапе нам достаточно реализовать простую модель:
Рисунок 1. Модель работы контроллера печи с точки зрения пользователя
В состояниях 3 и 5 (состояниях настройки) дисплей должен мигать; исходное состояние – естественно, выключенное.
Как видно, из виртуальных машин постоянно (даже в выключенном состоянии) работать должны как минимум машины «физических» и «логических» кнопок (речь идёт о виртуальных машинах в основном цикле; все прерывания при этом работают в любом случае).
Попробуем изобразить виртуальную машину для пользовательского интерфейса.
Но сперва обсудим некоторые важные моменты:
- виртуальная машина не привязана ко времени и вызывается часто – иногда слишком часто: например, если мы будем в состоянии «Отображение текущей температуры» каждый раз вызывать функцию получения текущего значения температуры, а потом его отрисовывать, то дисплей просто будет отображать последние цифры как «8» - произойдет наложение нескольких значений друг на друга; поэтому отображать текущее значение температуры нужно, например, раз в полсекунды. А это значит – ещё одна виртуальная машина и одно прерывание для часового таймера.
- в каждом активном состоянии (2-5) мы должны проверять все доступные сигналы от пользователя: длительное нажатие на кнопку 1 («кнопку для пользователя»), короткое нажатие на кнопку 1 и переход счётчика энкодера в активное или спящее состояние; также в этих состояниях нужно проверять, не сработал ли таймер. При этом действия при получении посылки о длительном нажатии на кнопку и получении посылки о срабатывании таймера из всех активных состояний одинаковы – поэтому проверка на эти посылки может быть вынесена отдельно.
- Переход в каждое состояние требует предварительной подготовки – например, включения-отключения мигания, очищения каких-либо посылок или выведения определённого текста на дисплей (например, надписи «OFF» при переходе в выключенное состояние)
- Машина достаточно проста по сути – в каждом состоянии мы перебираем приход возможных посылок и указываем, в какие состояния нужно после этого перейти; при этом очень схожи между собой состояния «Отображение текущей температуры» и «Отображение таймера», а также «Настройка желаемой температуры» и «Настройка таймера».
Общий вид схемы представлен ниже. Выглядит устрашающе, но это впечатление складывается исключительно из-за того, что в каждом «базовом» состоянии происходит несколько проверок на входящие посылки.
Рисунок 2.Схема виртуальной машины базового интерфейса.
Попробуем разобраться в схеме машины по частям. Начнём с перехода от выключенного состояния к активному и обратно.
В самом выключенном состоянии нам ничего не нужно делать, кроме как проверять, пришла ли посылка на долгое нажатие кнопки. Если пришла – то придется шевелиться нужно включать устройство: запускать АЦП, таймер печки, энкодер, счётчик энкодера и нагревательный элемент. Эти действия можно сделать и прямо из состояния «Выключенный режим»; дальше же мы переходим к подготовке к отображению текущей температуры. Эта подготовка вынесена в отдельное состояние, поскольку мы будем проводить эти действия регулярно: и сразу после включения, и после перехода от настройки температуры, и после перехода от отображения текущего значения таймера – но об этом подробнее позже.
Рисунок 3. Схема перехода из выключенного состояния в активные и обратно.
Из любого активного состояния мы будем проверять, не пришла ли посылка о длительном нажатии кнопки или посылка о срабатывании кухонного таймера. Если хоть одна из этих посылок пришла, то нужно подготовиться к переходу в выключенный режим: отключить АЦП, энкодер и счётчик энкодера, а также выключить нагревательный элемент. Выключение энкодера и счётчика энкодера выделено цветом – хотя по логике, мы должны их выключать, на самом деле мы этого не делаем - мы просто не запускаем их машины в выключенном состоянии. А при включении, соотвественно, заново инициализируем их.
Попробуем теперь разобраться с отображением и настройкой температуры.
Само отображение текущей температуры требует отдельной машины и функции для прерывания; логика же работы практически повторяет логику работы виртуальной машины отображения значения таймера печки. Создаём некоторый счётчик, который в прерывании будет отсчитывать нам секунду; по достижению этого времени он должен отправить некую посылку – назовём её, например, OnNeedChange. В основном же цикле машина просто проверяет, не пришла ли посылка; если пришла – посылка очищается, а температура отображается. Всё! При инициализации посылка и счётчик очищаются, и на дисплей выводится текущая температура.
Создаем файл "BasicInterface.h":
#ifndef BASIC_INTERFACE_H #define BASIC_INTERFACE_H #include "SegmentDisplay.h" #include "ADCTemperature.h" #include "Timings.h" void TemperatureShowMachineInterrupt(); #endif
В "BasicInterface.c" пишем:
#include "BasicInterface.h" //машина отображения текущей температуры typedef struct { volatile uint8_t tsOnNeedChange; //посылка об необходимости изменения значения текущей температуры uint16_t tsChangeCounter; //счётчик для изменения значения текущей температуры uint8_t tsIsOn; //флаг включения машины } TVMTemperatureShowMachine; //виртуальная машина отображения текущей температуры TVMTemperatureShowMachine VMTemperatureShowMachine; //включение виртуальной машины отображения текущей температуры void TemperatureShowMachineOn() { __disable_interrupt(); VMTemperatureShowMachine.tsChangeCounter = 0; __enable_interrupt(); VMTemperatureShowMachine.tsOnNeedChange = 0; VMTemperatureShowMachine.tsIsOn = 1; if (ADCGetNewTemperatureValue()) DisplayShowTemperatureValue(ADCTemperature.atCurrTemperature); } //выключение виртуальной машины отображения текущей температуры #pragma inline=forced void TemperatureShowMachineOff() { VMTemperatureShowMachine.tsIsOn = 0; } //прерывание для виртуальной машины отображения текущей температуры void TemperatureShowMachineInterrupt() { if (VMTemperatureShowMachine.tsIsOn) //если машина включена { //ждём, пока не настанет время отображать температуру if (VMTemperatureShowMachine.tsChangeCounter < CLOCK_TIMER_INTERRUPTS_PER_SECOND) VMTemperatureShowMachine.tsChangeCounter++; else { VMTemperatureShowMachine.tsChangeCounter = 0; VMTemperatureShowMachine.tsOnNeedChange = 1; } } } //виртуальная машина отображения текущей температуры void TemperatureShowMachine() { //если пришла посылка о необходимости отображения температуры - пытаемся получить новое значение и отобразить его на дисплее if (VMTemperatureShowMachine.tsOnNeedChange) { if (ADCGetNewTemperatureValue()) { DisplayShowTemperatureValue(ADCTemperature.atCurrTemperature); VMTemperatureShowMachine.tsOnNeedChange = 0; } } }
С машиной отображения текущей температуры разобрались, вернёмся к общей машине пользовательского интерфейса.
Мы остановились на том, что определили, каким образом контроллер будет выходить из выключенного состояния, и каким образом снова входить в него.
После включения же происходит переход к отображению текущей температуры - соответственно, мы должны подготовиться к этому: отключить мигание и инициализировать машину отображения.
В базовом состоянии «Отображение текущей температуры» мы вызываем машину отображения текущей температуры и занимаемся анализом входящих посылок: про посылку о длинном нажатии на кнопку и посылку о срабатывании таймера печки уже было сказано выше; также может прийти посылка от счётчика энкодера – что он перешел из спящего режима в активный; в этом случае нужно очистить посылку и перейти к подготовке настройки температуры: включить мигание, отобразить заданную температуру (не путать с текущей! Заданная температура – та, к поддержанию которой мы стремимся!) и соответствующе настроить счётчик энкодера. Также из состояния отображения текущей температуры можно перейти к подготовке к отображению значения таймера – по получению посылки о коротком нажатии (не забываем также очищать посылку!).
Рассмотрим дальше работу с настройкой температуры. В этом состоянии также в первую очередь проверяем на необходимость перехода в выключенный режим (приход посылки о длительном нажатии на кнопку или посылки о срабатывании таймера печки); дальше проверяем, не изменилось ли значение счётчика энкодера – если да, то очищаем посылку об изменении и отображаем на дисплее новое значение заданной температуры. Если же и этого не произошло, то проверяем входящие посылки на переход обратно в состояние отображения текущей температуры – это может случится либо по короткому нажатию на кнопку, либо по «засыпанию» счётчика энкодера. В обоих случаях очищаем все посылки, принудительно отправляем счётчик энкодера в спящий режим, сохраняем новое значение заданной температуры и возвращаемся к подготовке к отображению текущей температуры.
По поводу принудительного отправления в спящий режим: дело в том, что если мы вернулись к отображению текущей температуры через нажатие на кнопку, то энкодер все равно рано или поздно уйдет в спящий режим. Но посылку-то об этом мы не очистим! И тогда в следующий раз, когда мы соберемся что-либо настраивать, то мы просто очень быстро проскочим всю настройку и снова вернемся к пассивному отображению. Непорядок! Поэтому либо нам нужно будет постоянно в режиме отображения очищать посылку о переходе в спящий режим, либо просто принудительно отправлять энкодер в спящий режим самим.
Рисунок 4. Виртуальная подмашина отображения и настройки температуры.
Отображение и настройка таймера происходят практически аналогично – только при настройке таймера печки мы сначала отключаем таймер; таким образом, при настройке таймера контроллер не сможет выключиться по посылке «Таймер сработал» - прерывание кухонного таймера, а значит, и уменьшение его текущего значения, и отсылка его посылок просто не будут работать. По завершению настройки (отключению счётчика энкодера или краткому нажатию на кнопку) посылки будут очищены, новое значение таймера печки будет сохранено, а сам таймер – включён. При этом в функции включения таймера стоит «защита»: если его текущее значение равно 0 (то есть пользователь не хочет использовать таймер), то таймер не включится (на дисплее при этом будет надпись «On»).
Рисунок 5. Виртуальная подмашина отображения и настройки кухонного таймера.
Попробуем теперь все это реализовать – напишем отдельную библиотеку, а также модифицируем и дополним уже имеющиеся библиотеки.
В библиотеку логических кнопок добавим функцию очищения посылок:
//очищение посылок void LogicalButtonVMErasePackages() { uint8_t i; for (i = 0; i < LOGICAL_BUTTONS_COUNT; i++) { VMLogicalButtons[i].lbOnLongPress = 0; VMLogicalButtons[i].lbOnShortPress = 0; } }
А в библиотеку энкодера - функцию принудительного усыпления счётчика энкодера:
//принудительное усыпление счётчика энкодера void EncoderCounterSleep() { VMEncoderCounter.ecntMachineState = ecnsSleep; }
Файл "BasicInterface.h":
#ifndef BASIC_INTERFACE_H #define BASIC_INTERFACE_H #include "SegmentDisplay.h" #include "LogicalButton.h" #include "Encoder.h" #include "Timings.h" #include "ADCTemperature.h" #include "FurnaceTimer.h" #include "HeatElement.h" //прерывание для виртуальной машины отображения текущей температуры void TemperatureShowMachineInterrupt(); //состояния виртуальной машины пользовательского интерфейса typedef enum {bisOff, bisOffInit, bisTemperatureShowInit, bisTemperatureShow, bisTemperatureSetupInit, bisTemperatureSetup, bisTimerShowInit, bisTimerShow, bisTimerSetupInit, bisTimerSetup} TVMBasicInterfaceState; //текущее состояние виртуальной машины базового интерфейса extern TVMBasicInterfaceState VMBIState; void BIMachineInit(); void BIMachine(); #endif
Файл "BasicInterface.c":
#include "BasicInterface.h" //машина отображения текущей температуры typedef struct { volatile uint8_t tsOnNeedChange; //посылка об необходимости изменения значения текущей температуры uint16_t tsChangeCounter; //счётчик для изменения значения текущей температуры uint8_t tsIsOn; //флаг включения машины } TVMTemperatureShowMachine; //виртуальная машина отображения текущей температуры TVMTemperatureShowMachine VMTemperatureShowMachine; //включение виртуальной машины отображения текущей температуры void TemperatureShowMachineOn() { __disable_interrupt(); VMTemperatureShowMachine.tsChangeCounter = 0; __enable_interrupt(); VMTemperatureShowMachine.tsOnNeedChange = 0; VMTemperatureShowMachine.tsIsOn = 1; if (ADCGetNewTemperatureValue()) DisplayShowTemperatureValue(ADCTemperature.atCurrTemperature); } //выключение виртуальной машины отображения текущей температуры #pragma inline=forced void TemperatureShowMachineOff() { VMTemperatureShowMachine.tsIsOn = 0; } //прерывание для виртуальной машины отображения текущей температуры void TemperatureShowMachineInterrupt() { if (VMTemperatureShowMachine.tsIsOn) //если машина включена { //ждём, пока не настанет время отображать температуру if (VMTemperatureShowMachine.tsChangeCounter < CLOCK_TIMER_INTERRUPTS_PER_SECOND) VMTemperatureShowMachine.tsChangeCounter++; else { VMTemperatureShowMachine.tsChangeCounter = 0; VMTemperatureShowMachine.tsOnNeedChange = 1; } } } //виртуальная машина отображения текущей температуры void TemperatureShowMachine() { //если пришла посылка о необходимости отображения температуры - пытаемся получить новое значение и отобразить его на дисплее if (VMTemperatureShowMachine.tsOnNeedChange) { if (ADCGetNewTemperatureValue()) { DisplayShowTemperatureValue(ADCTemperature.atCurrTemperature); VMTemperatureShowMachine.tsOnNeedChange = 0; } } } //текущее состояние виртуальной машины базового интерфейса TVMBasicInterfaceState VMBIState; void BIMachineInit() { VMBIState = bisOffInit; } //проверка на выключения контроллера из активных состояний #pragma inline=forced void CheckToOff() { //если было длительное нажатие на пользовательскую кнопку или сработал кухонный таймер if ((VMLogicalButtons[0].lbOnLongPress) || (VMFurnaceTimer.ftOnExpired)) { VMLogicalButtons[0].lbOnLongPress = 0; VMFurnaceTimer.ftOnExpired = 0; VMBIState = bisOffInit; } } void BIMachine() { //постоянно включенные машины: PhysicalButtonMachine(); LogicalButtonMachine(); //машины и проверки, работающие в активном состоянии) if (VMBIState != bisOff) { EncoderMachine(); EncoderCounterMachine(); HEMachine(); CheckToOff(); } switch (VMBIState) { case bisOff: //выключенное состояние - проверяем и при необходимости включаем периферию if (VMLogicalButtons[0].lbOnLongPress) { //если нужно включить контроллер: очищаем все посылки, включаем АЦП, таймер, нагревательный элемент, счётчик энкодера LogicalButtonVMErasePackages(); ADCOn(); FurnaceTimerOn(); HEMachineOn(); EncoderMachineInit(); EncoderCounterMachineInit(); VMBIState = bisTemperatureShowInit; } break; case bisOffInit: //переход в выключенное состояние - выключаем периферию, отображаем "OFF" на дисплее ADCOff(); FurnaceTimerOff(); HEMachineOff(); DisplayBlinkOff(); DisplayShowStr("OFF"); VMBIState = bisOff; break; case bisTemperatureShowInit: //подготовка к отображению температуры - выключаем мигание, инициализируем ВМ DisplayBlinkOff(); TemperatureShowMachineOn(); VMBIState = bisTemperatureShow; break; case bisTemperatureShow: //отображение температуры + проверка на переход к настройке или к отображению кухонного таймера TemperatureShowMachine(); if (VMEncoderCounter.ecntOnActiveMode) { VMEncoderCounter.ecntOnActiveMode = 0; VMBIState = bisTemperatureSetupInit; } else if (VMLogicalButtons[0].lbOnShortPress) { VMLogicalButtons[0].lbOnShortPress = 0; TemperatureShowMachineOff(); VMBIState = bisTimerShowInit; } break; case bisTemperatureSetupInit: //подготовка к настройке температуры - включаем мигание, настраиваем энкодер отображаем заданную температуру DisplayBlinkOn(); //исправить мин-мах-шаг EncoderCounterSetValue(ADCTemperature.atSetTemperature, 300, 20, 0.5); DisplayShowTemperatureValue(VMEncoderCounter.ecntValue); VMBIState = bisTemperatureSetup; break; case bisTemperatureSetup: //настройка температуры - при повороте энкодера отображаем новую температуру, при выходе из режима настройки сохраняем значение //если энкодер был повернут - отображаем новое значение if (VMEncoderCounter.ecntOnValueChanged) { VMEncoderCounter.ecntOnValueChanged = 0; DisplayShowTemperatureValue(VMEncoderCounter.ecntValue); } else if ((VMLogicalButtons[0].lbOnShortPress) || (VMEncoderCounter.ecntOnSleepMode)) { //сохраняем новое значение ADCTemperature.atSetTemperature = VMEncoderCounter.ecntValue; //очищаем посылки VMLogicalButtons[0].lbOnShortPress = 0; VMEncoderCounter.ecntOnSleepMode = 0; EncoderCounterSleep(); VMBIState = bisTemperatureShowInit; } break; case bisTimerShowInit: //подготовка к отображению кухонного таймера - выключаем мигание, инициализируем ВМ DisplayBlinkOff(); FurnaceTimerShowMachineInit(); VMBIState = bisTimerShow; break; case bisTimerShow: //отображение кухонного таймера + проверка на переход к настройке или к отображению температуры FurnaceTimerShowMachine(); if (VMEncoderCounter.ecntOnActiveMode) { VMEncoderCounter.ecntOnActiveMode = 0; VMBIState = bisTimerSetupInit; } else if (VMLogicalButtons[0].lbOnShortPress) { VMLogicalButtons[0].lbOnShortPress = 0; VMBIState = bisTemperatureShowInit; } break; case bisTimerSetupInit: //подготовка к настройке таймера - включаем мигание, настраиваем энкодер, выключаем кухонный таймер FurnaceTimerOff(); DisplayBlinkOn(); EncoderCounterSetValue(VMFurnaceTimer.ftValue, 240, 0, 1); VMBIState = bisTimerSetup; break; case bisTimerSetup: //настройка таймера - при повороте энкодера отображаем новое значение, при выходе из режима настройки сохраняем значение и вкл. таймер //если энкодер был повернут - отображаем новое значение if (VMEncoderCounter.ecntOnValueChanged) { VMEncoderCounter.ecntOnValueChanged = 0; DisplayShowTimerValue((uint16_t)VMEncoderCounter.ecntValue); } else if ((VMLogicalButtons[0].lbOnShortPress) || (VMEncoderCounter.ecntOnSleepMode)) { //очищаем посылки VMLogicalButtons[0].lbOnShortPress = 0; VMEncoderCounter.ecntOnSleepMode = 0; //принудительно усыпляем счётчик энкодера EncoderCounterSleep(); VMFurnaceTimer.ftValue = (uint16_t)VMEncoderCounter.ecntValue; //сохраняем новое значение кухонного таймера FurnaceTimerOn(); //запускаем при необходимости кухонный таймер VMBIState = bisTimerShowInit; } break; } }
Автор - Moriam =ˆˆ=