Кухонный таймер (таймер печки)

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

  1. постоянный обратный отсчёт в течение всего времени активности (когда печка находится в режиме отображения текущей температуры/настройки температуры или в режиме отображения текущего значения таймера)
  2. отображение значения таймера (в режиме отображения текущего значения таймера)

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

//структура для виртуальной машины таймера печки
typedef struct 
{
  //посылки не являются битовыми полями, поскольку происходит запись в прерывании!
  volatile uint8_t ftIsOn;                              //флаг включения печки
  volatile uint8_t ftOnExpired;                         //посылка об срабатывании таймера и необходимости выключения
  volatile uint8_t ftOnValueChange;                     //посылка об изменении значения таймера печки
  uint16_t ftValue;                                     //текущее значение
  uint16_t ftChangeCounter;                             //счётчик для изменения значения таймера   
} TVMFurnaceTimer;

//прерывание для работы с таймером (изменение значения, остановка таймера)
void FurnaceTimerInterrupt()
{
  if (VMFurnaceTimer.ftIsOn)                                                    //если таймер включён
  {
    if (VMFurnaceTimer.ftChangeCounter < CLOCK_TIMER_INTERRUPTS_PER_MINUTE)     //если минута ещё не прошла - просто увеличиваем счётчик
        VMFurnaceTimer.ftChangeCounter++;
    else                                                                        //если нужно уменьшить значение таймера
    {
      VMFurnaceTimer.ftChangeCounter = 0;                                       //обнуляем счётчик
      VMFurnaceTimer.ftValue--;                                                 //уменьшаем значение
      VMFurnaceTimer.ftOnValueChange = 1;                                       //отсылаем посылку об изменении значения
      if (VMFurnaceTimer.ftValue == 0)                                          //если таймер отработал
      {
        VMFurnaceTimer.ftIsOn = 0;                                              //выключаем
        VMFurnaceTimer.ftOnExpired = 1;                                         //посылаем сообщение о том, что таймер сработал
      }
    }
  }
}

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

Также стоит обратить внимание, что переменные, хранящие значение кухонного таймера и значение его счётчика - двухбайтовые; поэтому в основной программе нам однозначно придётся запрещать прерывания при работе с ними.

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

#ifndef FURNACE_TIMER_H
#define FURNACE_TIMER_H
#include "stdint.h"
#include "SegmentDisplay.h"
#include "Timings.h"

//структура для виртуальной машины таймера печки
typedef struct 
{
  //посылки не являются битовыми полями, поскольку происходит запись в прерывании!
  volatile uint8_t ftIsOn;                              //флаг включения печки
  volatile uint8_t ftOnExpired;                         //посылка об срабатывании таймера и необходимости выключения
  volatile uint8_t ftOnValueChange;                     //посылка об изменении значения таймера печки
  uint16_t ftValue;                                     //текущее значение
  uint16_t ftChangeCounter;                             //счётчик для изменения значения таймера   
} TVMFurnaceTimer;

extern TVMFurnaceTimer VMFurnaceTimer;

//прерывание для работы с таймером (изменение значения, остановка таймера)
void FurnaceTimerInterrupt();
//виртуальная машина для отображения текущего значения таймера печки
void FurnaceTimerShowMachine();
//инициализация машины для отображения текущего значения таймера печки
void FurnaceTimerShowMachineInit();
//инициализация всего таймера печки
void FurnaceTimerInit();
//запуск таймера
void FurnaceTimerOn();
//остановка таймера
void FurnaceTimerOff();
//устанавливает текущее значение таймера
#pragma inline=forced
void FurnaceTimerSetValue(uint16_t NewValue);
//возвращает текущее значение таймера
#pragma inline=forced
uint16_t FurnaceTimerGetValue();

#endif

Файл "FurnaceTimer.c":

#include "FurnaceTimer.h"

TVMFurnaceTimer VMFurnaceTimer;

//прерывание для работы с таймером (изменение значения, остановка таймера)
void FurnaceTimerInterrupt()
{
  if (VMFurnaceTimer.ftIsOn)                                                    //если таймер включён
  {
    if (VMFurnaceTimer.ftChangeCounter < CLOCK_TIMER_INTERRUPTS_PER_MINUTE)     //если минута ещё не прошла - просто увеличиваем счётчик
        VMFurnaceTimer.ftChangeCounter++;
    else                                                                        //если нужно уменьшить значение таймера
    {
      VMFurnaceTimer.ftChangeCounter = 0;                                       //обнуляем счётчик
      VMFurnaceTimer.ftValue--;                                                 //уменьшаем значение
      VMFurnaceTimer.ftOnValueChange = 1;                                       //отсылаем посылку об изменении значения
      if (VMFurnaceTimer.ftValue == 0)                                          //если таймер отработал
      {
        VMFurnaceTimer.ftIsOn = 0;                                              //выключаем
        VMFurnaceTimer.ftOnExpired = 1;                                         //посылаем сообщение о том, что таймер сработал
      }
    }
  }
}

//инициализация всего таймера печки
void FurnaceTimerInit()
{
  VMFurnaceTimer.ftIsOn = 0;
  VMFurnaceTimer.ftOnExpired = 0;
  VMFurnaceTimer.ftOnValueChange = 0;
  __disable_interrupt();
  VMFurnaceTimer.ftValue = 0;
  VMFurnaceTimer.ftChangeCounter = 0;
  __enable_interrupt();
}

//запуск кухонного таймера - если значение > 0
void FurnaceTimerOn()
{
  //очищаем посылки таймера печки
  VMFurnaceTimer.ftOnExpired = 0;
  VMFurnaceTimer.ftOnValueChange = 0;
  __disable_interrupt();
  VMFurnaceTimer.ftChangeCounter = 0;
  VMFurnaceTimer.ftIsOn = (VMFurnaceTimer.ftValue != 0);                        //если значение таймера не равно нулю (т.е. таймер не должен быть выключен), то включаем его
  __enable_interrupt();
}

//остановка таймера
void FurnaceTimerOff()
{
  VMFurnaceTimer.ftIsOn = 0;
}

//устанавливает текущее значение таймера
#pragma inline=forced
void FurnaceTimerSetValue(uint16_t NewValue)
{
  __disable_interrupt();
  VMFurnaceTimer.ftValue = NewValue;
  VMFurnaceTimer.ftChangeCounter = 0;
  __enable_interrupt();
}

//возвращает текущее значение таймера
#pragma inline=forced
uint16_t FurnaceTimerGetValue()
{
  uint16_t currTimerValue;
  __disable_interrupt();
  currTimerValue = VMFurnaceTimer.ftValue;
  __enable_interrupt();
  return currTimerValue;
}

//виртуальная машина для отображения текущего значения таймера печки
void FurnaceTimerShowMachine()
{
  //если пришла посылка об изменении значения таймера - отображаем значение и очищаем посылку
  if (VMFurnaceTimer.ftOnValueChange)
  {
    DisplayShowTimerValue(FurnaceTimerGetValue());                              //отображаем текущее значение таймера на дисплее
    VMFurnaceTimer.ftOnValueChange = 0;                                         //очищаем посылку об изменении значения таймера (текущее значение уже отобразили)
  }
}

//инициализация машины для отображения текущего значения таймера печки
void FurnaceTimerShowMachineInit()
{
  DisplayShowTimerValue(FurnaceTimerGetValue());                                //отображаем текущее значение таймера на дисплее
  VMFurnaceTimer.ftOnValueChange = 0;                                           //очищаем посылку об изменении значения таймера (текущее значение уже отобразили)
}

Обратите внимание, перед вспомогательными функциями FurnaceTimerSetValue и FurnaceTimerGetValue, которые соответственно задают и возвращают текущее значение таймера добавлена директива

#pragma inline=forced

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

Также мы добавили функцию для отображения значения таймера печки в библиотеку семисегментного дисплея:

//отображение значения таймера на дисплее
void DisplayShowTimerValue(uint16_t Value)
{
	char buff[7];
  int8_t currBuffIndex;
  buff[6] = 0;
  buff[5] = 'T';
  buff[4] = ' ';
  if (Value == 0)                                         //если таймер выключен - пишем "On"
  {
    buff[2] = 'O';
    buff[3] = 'N';
    currBuffIndex = 1;
  }
  else
  {
    //заполняем строку справа налево от младшего разряда к старшему
    //если таймер включён - отображаем три возможных разряда
    currBuffIndex = 3;
    while (Value > 0)
    {
      buff[currBuffIndex] = Value % 10 + '0';
      Value = Value / 10;
      currBuffIndex--;
    }   
  }
  //оставшееся начало строки заполняем пробелами
  for(; currBuffIndex >= 0; currBuffIndex--)
    buff[currBuffIndex] = ' ';
	DisplayShowStr(buff);
}

Ну и в основном файле мы не забываем добавить функцию в прерывание и пишем простенькую программу для проверки работы библиотеки:

//прерывание таймера 2 на часовом кварце
#pragma vector = TIMER2_OVF_vect
__interrupt void ClockTimerInterrupt(void)
{
	LogicalButtonMachineInterrupt();								//работа с счётчиком длительности нажатия для логических кнопок
  EncoderCounterMachineInterrupt();
  FurnaceTimerInterrupt();
}
...
void main( void )
{
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  DDRC = 0;
  PORTC = 0;
  
  DisplayInit();
  FurnaceTimerInit();
  VMFurnaceTimer.ftValue = 3;
  
  FurnaceTimerOn();
  FastTimerInit();
  ClockTimerInit();
  
  FurnaceTimerShowMachineInit();
  while(1)
  {
    FurnaceTimerShowMachine();
    //если таймер сработал - выводим на дисплей сообщение о выключении
    if (VMFurnaceTimer.ftOnExpired)
    {
      VMFurnaceTimer.ftOnExpired = 0;
      DisplayShowStr("OFF");
      while (1);
    }
  }
}

Вот проект и прошивка.

Наверх

Автор - Moriam  =ˆˆ=

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