Базовый интерфейс

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


Рисунок 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  =ˆˆ=

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