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

09/02/2010 - 03:40

   Вступление

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

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

   Основу схемы составляет микроконтроллер 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

Комментарии   

# alexandershahbazov 10.02.2010 18:27
Да , здесь уже программирование
становится более привычным !
Ответить | Ответить с цитатой | Цитировать
# Pyku_He_oTTyda 12.02.2010 07:43
Спасибо! Интересная статья!
Ответить | Ответить с цитатой | Цитировать
# Виктор 15.02.2010 09:39
Спасибо!
Статья заслуживает уважительного изучения.Очень и очень интересно.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 15.02.2010 19:29
Это только один из подходов. Есть и более изощренные способы организации программ. Надеюсь и до них доберемся.
Ответить | Ответить с цитатой | Цитировать
# Guest 22.02.2010 23:12
Спасибо, интересная статья.

Небольшой багреперорт к проекту для WinAVR:
1. adc.c, строка 31: условие должно быть (count == 7)
2. interface.c, строка 108: должно быть не PORTD а CONTR_PORT
Ответить | Ответить с цитатой | Цитировать
# Pashgan 23.02.2010 07:47
1. Нет, условие должно быть ( count == 8 )
if (count == 8){
...adcValueBuf = adcValue>>3;
...adcValue = 0;
...count = 0;
}else{
...count++;//ин кр. счетчик
...ADC_StartCon v();//запускаем преобр.
}
2. Да, не заметил.
Ответить | Ответить с цитатой | Цитировать
# Guest 23.02.2010 21:22
Буду отстаивать свою точку зрения:

До кода, который вы привели есть строчки
static unsigned int adcValue = 0;
static unsigned char count = 0;
adcValue += ADCH;
Значит, при значениях count 0-8 к acdvalue буду добавлятся значения с АЦП. Т.е. будет сумма 9ти значений, а не 8ми.

Другими словами, первое преобразование запускается из GUI_Control(), и еще 8 из прерывания.

Если все же не верите, прогоните в дебагере этот кусок кода.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 24.02.2010 06:12
Согласен. Прогнал в симуляторе. Условие должно быть ( count == 7 )
Ответить | Ответить с цитатой | Цитировать
# Guest 25.06.2010 12:06
А как к событийной системе правильно прикрутить меню?

например Микроменю (http://easyelectronics.ru/organizaciya-drevovidnogo-menyu.html)

Я это пока прикручиваю, но может быть есть какой-нибудь элегантный способ?
Ответить | Ответить с цитатой | Цитировать
# Pashgan 19.12.2010 19:42
Можно сделать меню вот так
http://chipenable.ru/index.php/programming-c/73-sobytijnaja-sistema-na-tablice.html
Ответить | Ответить с цитатой | Цитировать
# Pavlya 15.09.2010 07:35
При компиляции проекта для WinAVR выдает предупреждение "assignment makes pointer from integer without a cast" для строки pFunc = pgm_read_word_n ear(&(FuncAr[ev ent-1]));
Компилятор WinAVR-20100110 Кто-то еще пробовал собрать проект под WinAVR?
Ответить | Ответить с цитатой | Цитировать
# Pashgan 19.12.2010 19:53
если сделать так, то предупреждение исчезнет.
Код:pFunc = (void*)pgm_read_word_n ear(&(FuncAr[event-1]));
Ответить | Ответить с цитатой | Цитировать
# Lukialex 17.12.2010 10:54
Можно обойтись и одним таймером, прерывание каждую мс:
Код:
interrupt [TIM0_OVF] void timer0_ovf_isr(void)
{
static unsigned int count = 0;
TCNT0=0x7D;
switch(count){
case 9:
BUT_Debrief();
break;
case 249:
ES_PlaceEvent(EVENT_TIMER);
count = 0; // обнуляем в последнем case
break;
default:
break;
}
count++;
}

Для тех кто говорит что switch в прерывании огромная трата тактов... Пример для наглядности, у меня в виде ассемблерной вставки.
З.Ы. Применил в своем проекте, остался доволен, спасибо!
Ответить | Ответить с цитатой | Цитировать
# wukrlvy 21.02.2012 21:22
Все обработчики в данном примере - это функции, которые не принимают параметров и не возвращают значения.

Вопрос - А можно ли так расширить обработчики, чтобы они, например, возвращали значение?
Код:
unsigned int Handle (void)

Таблица __flash void (*FuncAr[])(voi d)- это в принципе всего лишь список указателей. Почему не позволить добавлять, например, один параметр?

Я понимаю, что интерфейсная часть всех функций должна быть одинакова. Диспетчер ведь не имеет информации о том, какая функция запускается.

При этом появляется новая функциональност ь, которая может быть использована, например, при построении иерархического дерева взаимодействия диспетчеров на различных уровнях работы программы.
Ответить | Ответить с цитатой | Цитировать
# lv 10.04.2013 10:05
Помогите с задачей!!! Можно ли сделать из этой схемы полноценный регулятор???. Т.е. Добавить еще один выходной канал на куллер, что бы охлаждал при изменении верхнего предела температуры в низшую сторону.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 10.04.2013 19:10
Ну да, можно. Допиливать нужно.
Ответить | Ответить с цитатой | Цитировать
# lv 10.04.2013 10:06
И как перестроить на датчик от 0 до 20 мА???
Ответить | Ответить с цитатой | Цитировать
# Pashgan 10.04.2013 19:11
Что за датчик? Здесь используется температурный датчик, который на выходе дает аналоговое напряжение, пропорционально е температуре.
Ответить | Ответить с цитатой | Цитировать
# ralex 10.06.2013 18:28
День добрый. Почему невозможно скомпилировать в avr toolchaine? Постоянно ругается что ссылка на массив функций должна быть константой:
void (*FuncAr[])(voi d) PROGMEM=
{
HandlerEventButUp,
HandlerEventButDown,
HandlerEventButEnter,
HandlerEventTimer
};
error: variable must be const in order to be put into read-only section by means of '__attribute__( (progmem))'
Ответить | Ответить с цитатой | Цитировать
# Роман Киртаев 17.06.2014 08:28
а где тут инициализация настроек микроконтроллер а?
Ответить | Ответить с цитатой | Цитировать

Добавить комментарий

Защитный код
Обновить