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