Организация программ. Событийная система - практический пример

   Вступление

    Выбирая пример для этой статьи, я колебался между несколькими вариантами. С одной стороны хотелось написать простую программу, чтобы было понятно начинающим, с другой стороны программа должна оправдывать применение событийной системы.
   Думал я, думал и в итоге остановился на термостате. Программа носит учебный характер и не претендует на оптимальность. Некоторые функции в ней не реализованы.

   Схема термостата

   Основу схемы составляет микроконтроллер ATmega8535. Он работает на частоте 16МГц с внешним кварцевым резонатором.
   Для измерения температуры используется датчик TMP37 фирмы Analog Devices. На выходе он выдает аналоговое напряжение пропорциональное его температуре – 20 мв на градус.    
   Ключ для управления нагревателем – полевой n канальный транзистор серии IRL. Транзисторы этой серии управляются логическими уровнями, что позволяет подключать их к микроконтроллеру напрямую.
   Для реализации пользовательского интерфейса используются 3 кнопки и ЖК дисплей на основе контроллера ks0066
   Питание схемы организовано на линейном 5-ти вольтовом стабилизаторе напряжения L7805.
схема термостата на AVR

Требования к устройству

Перед написанием программы я сформулировал к термостату ряд требований:
- термостат должен измерять и отображать на lcd текущее значение температуры
- значение поддерживаемой температуры должно задаваться двумя значениями – нижним и верхним
- термостат должен иметь простой пользовательский интерфейс для установки крайних значений поддерживаемой температуры. 

Распределение ресурсов микроконтроллера

   Опрос кнопок выполняется в прерывании таймера Т0. У меня был готовый драйвер для 4-ех кнопочного джойстика, который я слегка переделал. В старом варианте коды кнопок записывались в однобайтовый буфер, а в текущем варианте коды кнопок записываются в кольцевой.
   Обновление значения температуры и управление нагревателем осуществляется по сигналу таймера Т1. Каждые 250 мс в прерывании таймера Т1 в буфер кидается код события. Температура вещь инерционная, поэтому нет нужды обновлять ее часто.
   Для измерения температуры используется АЦП в режиме однократного преобразования. Опорный источник – напряжение питания (5 В), выравнивание -  влево (используются только старшие 8 разрядов). Значение температуры рассчитывается путем усреднения 8-ми измерений.
   Как вычисляется значение температуры? Очень просто. АЦП микроконтроллера AVR 10-ти разрядный. У каждого разряда есть так называемый вес. У младшего (нулевого) разряда АЦП вес равен Uref/1024 = 5/1024 примерно 5 мВ. У первого разряда вес в два раза больше - 10 мВ, у второго 20 мВ и т.д. TMP37 выдает напряжение пропорциональное температуре - 20 мв на градус, а это как раз вес второго разряда. Берем от результата преобразования АЦП разряды с 9 по 2-ой и получаем температуру датчика в целых числах. Для учебного примера большая точность не нужна.

Подход к написанию программы

Первое с чем нужно определиться, это в каких состояниях может находиться устройство. У нашего устройства будет три состояния:

Основное
Установка нижнего порога температуры
Установка верхнего порога температуры


Далее нужно определить количество событий. В нашем случае их четыре:

Нажатие кнопки Enter
Нажатие кнопки Up
Нажатие кнопки Down
Прерывание таймера Т1


И состояниям и событиям присваиваются коды. В программе я вынес их в отдельный файл – list-event.h.

//коды событий
#define KEY_NULL           0
#define KEY_UP              1
#define KEY_DOWN         2
#define KEY_ENTER         3
#define EVENT_TIMER     4

//коды состояний
#define GENERAL                          0
#define SET_TEMPERATURE_LOW  1
#define SET_TEMPERATURE_HI      2

   Когда известно количество событий – можно набросать обработчики. Обработчики вызываются из массива указателей, поэтому порядок их расположения должен соотноситься с кодами событий!

//обработчик события - таймер
void HandlerEventTimer(void) {}

//обработчик кнопки Enter
void HandlerEventButEnter(void) {}

//обработчик кнопки Up
void HandlerEventButUp(void) {}

//обработчик кнопки Down
void HandlerEventButDown(void) {}

//массив указателей на функции-обработчики
__flash void (*FuncAr[])(void) =
{
  HandlerEventButUp,  
  HandlerEventButDown,
  HandlerEventButEnter,
  HandlerEventTimer
};

Код события  “нажатие кнопки UP” равен 1, поэтому его обработчик в массиве первый!

Теперь неплохо было бы описать источники событий – функции в которых коды событий помещаются в очередь. В нашем случае -  это обработчики прерываний таймеров Т0 и Т1.

#pragma vector = TIMER1_COMPA_vect
__interrupt void Timer1CompA(void)
{
  ES_PlaceEvent(EVENT_TIMER);  
}

#pragma vector = TIMER0_COMP_vect
__interrupt void Timer0Comp(void)
{
  BUT_Debrief();  
}

.....

void BUT_Debrief(void)
{
unsigned char key = 0;

  //последовательный опрос выводов мк
  if (BitIsClear(PIN_BUTTON, UP))     
    key = KEY_UP;
  else if (BitIsClear(PIN_BUTTON, DOWN))    
    key = KEY_DOWN;
  else if (BitIsClear(PIN_BUTTON, ENTER))        
    key = KEY_ENTER;    
  else {
    key = KEY_NULL;
  }

  //если во временной переменной что-то есть

  if (key) {
 
    //и если кнопка удерживается долго
    //записать ее номер в буфер

    if (comp == THRESHOLD) {
      comp = THRESHOLD+10;
      ES_PlaceEvent(key);
      return;
    }
    else if (comp < (THRESHOLD+5)) comp++;
    
  }
  else comp=0;
}

 Чтобы вы яснее представили структуру ”получающейся” программы, привожу дополнительно ее блок-схему.
блок схема событийной системы (event-driven system)
 Дошли до обработчиков событий.  
Обработчики обычно построены в виде конечных автоматов. Самый простой способ реализации автоматов основан на использовании оператора SWITCH.
   Для написания обработчика кнопки Enter нужно определиться, какие функции будут вызываться в различных состояниях и как состояние устройства будет меняться. Для наглядности можно составить таблицу (или нарисовать граф переходов).
//обработчик кнопки Enter

void HandlerEventButEnter(void)
{
  switch (state){
    case GENERAL:
      GUI_SelectTempLow();
      state = SET_TEMPERATURE_LOW;
      break;
      
    case SET_TEMPERATURE_LOW:
      GUI_SelectTempHi();
      state = SET_TEMPERATURE_HI;
      break;
      
    case SET_TEMPERATURE_HI:
      GUI_General();
      state = GENERAL;
      break;
    
    default:
      break;

  }
}

state – это глобальная переменная, в которой хранится код текущего состояния системы. GUI_SelectTempLow(), GUI_SelectTempHi(), GUI_General() - низкоуровневые функции пользовательского интерфейса, они вынесены в отдельный программный модуль - interface.c. На начальном этапе их можно сделать пустыми. 
 
Обработчик нажатия кнопки UP 
Если следовать таблице, то обработчик кнопки UP будет выглядеть следующим образом:

//обработчик кнопки Up

void HandlerEventButUp(void)
{
  switch (state){
    case GENERAL:
      state = GENERAL;
      break;

    case SET_TEMPERATURE_LOW:
      GUI_IncTempLow();
      state = SET_TEMPERATURE_LOW;
      break;
      
    case SET_TEMPERATURE_HI:
      GUI_IncTempHi();
      state = SET_TEMPERATURE_HI;
      break;
    
    default:
      break;

  }
}

Но поскольку в некоторых случаях выполняется пустая работа, сокращаем обработчик до:

//обработчик кнопки Up

void HandlerEventButUp(void)
{
  switch (state){
    case SET_TEMPERATURE_LOW:
      GUI_IncTempLow();
      break;
      
    case SET_TEMPERATURE_HI:
      GUI_IncTempHi();
      break;
    
    default:
      break;

  }
}

Обработчик нажатия кнопки DOWN выглядит аналогично, поэтому его не привожу.

И последний обработчик.

void
HandlerEventTimer(void)
{
   GUI_Control();
}

   Функция GUI_Control() выводит на lcd значение температуры и управляет нагревателем. Эта функция запускается во всех состояния устройства. Она построена в виде конечного автомата с двумя состояниями - TEMP_LOW  и TEMP_HI. (Первоначально я думал, что понадобится еще и третье состояние - TEMP_NOM, но обошелся без него. В программе оно осталось, я просто забыл его убрать).   
   В состоянии TEMP_LOW автомат находится до тех пор, пока значение температуры меньше или равно верхнему температурному порогу. В этом состоянии нагреватель включен и на LCD отображается символ ‘+’. Когда значение температуры превышает верхний температурный порог, автомат переходит в состояние TEMP_HI. Если значение температуры выше нижнего порога, нагреватель выключается, а на LCD отображается символ ‘-’. Когда значение температуры станет меньше нижнего порога, автомат вернется в состоянии TEMP_LOW и снова выключит нагреватель.

#define TEMP_LOW    0
#define TEMP_HI        2
unsigned char stateTerm = TEMP_LOW;

void GUI_Control(void)
{
  //считываем результат и отображаем на lcd
  unsigned char curTemp = ADC_GetBuf();
  LCD_Goto(7,0);
  BCD_3Lcd(curTemp);
 
  //управление нагревателем
  switch(stateTerm){
    case TEMP_LOW:
      if (curTemp <= tempHi){
        LCD_Goto(12, 0);
        LCD_WriteData('+');
        PORTD |= (1<<TR);
      }
      else stateTerm = TEMP_HI;
      break;
    
    case TEMP_HI:
      if (curTemp >= tempLow) {
         LCD_Goto(12, 0);
         LCD_WriteData('-');
         CONTR_PORT &= ~(1<<TR);
      }
      else stateTerm = TEMP_LOW;
      break;
      
    default:
      break;

  }
 
  //запускаем АЦП
  ADC_StartConv();
}

На этом все. Остальная часть программы не должна вызвать затруднений.
Полный текст модуля event-system.c

event-system.c

Заключение

  В программе термостата не реализовано сохранение температурных порогов в EEPROM и защита от установки неправильных значений. То есть пользователь вполне может установить верхний температурный порог меньше нижнего. Устранение недочетов предлагаю в качестве домашнего задания тем, кому будет интересно доработать проект. 

Продолжение следует...

Файлы

Проект для IAR`a
Проект для CodeVision
Проект для Proteus`a

У вас недостаточно прав для комментирования.