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

    Стандартный подход к написанию программы для микроконтроллера сводится к использованию бесконечного цикла (суперлупа), внутри которого непрерывно опрашиваются флаги и запускаются те или иные функции. Этот подход вполне оправдан для написания небольших программ, но при превышении определенного порога сложности, такая программа становится громоздкой, непонятной и запутанной. Этой ситуации можно избежать, если на начальной стадии разработки оценить сложность программы и выбрать для нее более подходящую форму организации.
   Самой ближайшей альтернативой является событийная система. Она не такая тяжеловесная, как операционная, и в то же время позволяет организовать программу в довольно стройную и понятную конструкцию, которую легко наращивать и изменять. 

Принцип работы событийной системы

   Событийная система представляет собой один или несколько конечных автоматов (State Machine), обрабатывающих очередь событий. Событие можно определить как изменение в системе, требующее реакции (обработки) со стороны микроконтроллера. Источником события может выступать периферийный модуль, внешнее воздействие (например, нажатие кнопки), функция, ну и тому подобные вещи. Как правило, источников событий несколько и они асинхронны по отношению к друг другу. То есть, события могут происходить в произвольные моменты времени и в том числе одновременно. 
   В любой момент времени микроконтроллер может обрабатывать только одно событие и для того, чтобы другие события не потерялись, используется кольцевая очередь (кольцевой буфер). Это такая структура данных. С одного конца очереди (из головы)  событие извлекается и обрабатывается, на другой конец очереди (в хвост) поступают новые события. Событие, попавшее в очередь первым, будет первым и обработано. В качестве аналогии можно привести пример с очередью в магазине. 
   Для каждого события в системе определен код. Этот код и помещается в очередь источником события с помощью специальной функции – «положить событие». Другая функция – «взять событие» - извлекает из очереди код события и передает его диспетчеру для обработки. Один из вариантов его реализации основан на использовании массива указателей на функции, каждая из которой представляет собой конечный автомат, обрабатывающей одно событие. Этот подход  я описывал в одной из предыдущих статей, он имеет право на жизнь, имеет свои достоинства и недостатки. Есть другой вариант построения диспетчера событийной системы - с использованием таблицы переходов. В простой событийной системе на таблице диспетчер представляет собой один конечный автомат обрабатывающий все события.   

Событийная система на таблице

   В этой статье мы рассмотрим заготовку событийной системы на таблице, некий минимальный функционал,  на базе которого можно создавать проекты. В следующей статье будет рассмотрен практический пример – часы на микроконтроллере AVR
   
   Основу событийной системы на таблице составляет – кольцевая очередь (буфер), диспетчер и таблица переходов.

  
Реализация кольцевой очереди/буфера разбиралась в статье "Организация обмена по USART `у с использованием кольцевого буфера", здесь все то же самое. Есть массив cycleBuf для хранения кодов событий, переменные tailBuf, headBuf для указания хвоста и головы очереди соответственно и countBuf для учета количества событий в очереди.   
 
//кольцевая очередь/буфер
static volatile unsigned char cycleBuf[SIZE_BUF];
static volatile unsigned char tailBuf = 0;
static volatile unsigned char headBuf = 0;
static volatile unsigned char countBuf = 0;
 
Для работы с очередью используются три функции:
Функция инициализации -  void ES_Init(void) 
функция, извлекающая событие из очереди - unsigned char ES_GetEvent(void). 
функция, добавляющая событие в очередь - void ES_PlaceEvent(unsigned char event) 

//инициализация
void ES_Init(void)
{
  tailBuf = 0;
  headBuf = 0;
  countBuf = 0;
}
 
//взять событие
unsigned char ES_GetEvent(void)
{
  unsigned char event;
  if (countBuf > 0){                            //если приемный буфер не пустой  
    event = cycleBuf[headBuf];              //считать из него событие    
    countBuf--;                                     //уменьшить счетчик 
    headBuf++;                                    //инкрементировать индекс головы буфера  
    if (headBuf == SIZE_BUF) headBuf = 0;
    return event;                                    //вернуть событие
  }
  return 0;
}
 
//положить событие
void ES_PlaceEvent(unsigned char event) 
{
  if (countBuf < SIZE_BUF){                      //если в буфере еще есть место                     
      cycleBuf[tailBuf] = event;                    //кинуть событие в буфер
      tailBuf++;                                           //увеличить индекс хвоста буфера 
      if (tailBuf == SIZE_BUF) tailBuf = 0;  
      countBuf++;                                       //увеличить счетчик 
  }
}
 
Функция void ES_PlaceEvent(unsigned char event) вызывается источником события. Например, так:
 
//код события
#define EVENT_SYS_TIMER  0x04
...
 
//прерывание таймера Т1 
#pragma vector = TIMER1_COMP_vect
__interrupt void Timer1CompVect(void)
     ES_PlaceEvent(EVENT_SYS_TIMER);
}
 
   Функция unsigned char ES_GetEvent(void) вызывается из main`а в бесконечном цикле. Она возвращает код события и если он не равен нулю, то запускается диспетчер.
 
int main(void)
{
  unsigned char event = 0;
 
  ES_Init();
 
  while(1){
    event = ES_GetEvent();
    if (event){
      ES_Dispatch(event);
    }
  }
  
  return 0;
}
 
    Перейдем к таблице. В привычном для нас смысле, таблица - это структура данных, используемая для удобства представления информации. Она состоит из столбцов и строк, на пересечении которых расположены цифровые, текстовые или смешанные данные. Если программа микроконтроллера представляет собой конечный автомат, то с помощью таблицы удобно расписывать его логику. Например, кусок таблицы переходов для часов на микроконтроллере выглядит так.

часы на микроконтроллере - пример таблицы переходов

Согласитесь - это очень наглядно. Если часы находятся в состоянии «отображение времени» и пользователь нажимает кнопку Enter, то часы переходят в состояние «установка часов» и отображают на дисплее курсор. 
 
  Как закодировать такую таблицу на Си?
Первые три столбца таблицы представляют собой байтовые константы. Последний столбец – константный указатель на функцию. То есть у нас четыре константы разного типа, а значит, строку таблицы можно представить с помощью Си структуры – struct.
 
__flash struct ROW_TABLE
{
    unsigned char state;           //состояние
    unsigned char event;          //событие
    unsigned char nextState;    //следующее состояние
    void (*pStateFunc)(void);    //функция-обработчик события                 
};
 
__flash – это указание IARу, что структура хранится во флэш памяти микроконтроллера. 
 
Таблица это набор строк, то есть, массив структур.
Пустая таблица переходов будет выглядеть следующим образом.
 
struct ROW_TABLE table[] = {
    //  STATE                    EVENT                NEXT STATE              STATE_FUNC    
    {     0,                             0,                          0,                        EmptyFunc}
};
 
Приведенная строка таблицы обязательно должна быть в таблице! Она служит маркером ее конца. 
EmptyFunc – это указатель на пустую функцию. Она ничего не делает и используется просто как заглушка

void EmptyFunc(void) {}

Теперь рассмотрим функцию, которая будет работать с таблицей переходов. 
 
//глобальная переменная, в которой хранится текущее состояние
volatile unsigned char currentState = 0;
 
//состояния системы
#define STATE_NO_CHANGE 0
 
void ES_Dispatch(unsigned char currentEvent)
{
    unsigned char i;
        
    for (i=0; table[i].state; i++){
        if (table[i].state == currentState && table[i].event == currentEvent){
            if (table[i].nextState != STATE_NO_CHANGE){
              currentState = table[i].nextState;
            }
            table[i].pStateFunc();
            break;
        }
    }
}
 
   Алгоритм работы функции следующий. В цикле for считываются первые два столбца таблицы и сравниваются с кодом текущего состояния и кодом поступившего события. Код состояния хранится в глобальной переменной (currentState), а код события передается функции в качестве параметра. Когда нужная строка найдена, проверяется значение третьего столбца таблицы. Если оно не равно STATE_NO_CHANGE, текущее состояние системы меняется. Далее с помощью указателя из четвертого столбца запускается функция-обработчик события, а после ее выполнения цикл завершается. Вот и все.
 
Полный код заготовки событийной системы на таблице

Файлы

  

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