Print this page

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

30/09/2012 - 19:44

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

Один обработчик для нескольких событий

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


MENU_DEVICE menu[] = {
{STATE1, EVENT_KEY1, STATE1, Handler1},
{STATE1, EVENT_KEY2, STATE1, Handler1},
{STATE1, EVENT_KEY3, STATE1, Handler1}


{0,0,0,0}
};

   Таблица переходов в таком случае может разрастись до «бесконечности». Это повлечет за собой увеличение времени реакции событийной системы на событие, а также увеличение размера прошивки, что менее болезненно, но может быть критично в отдельных ситуациях.
   Можно объединить несколько таких событий в одно и сделать вложенный автомат, но в этом случае потеряется наглядность программы, а я стараюсь этого избегать.
   Лучшее решение состоит в том, чтобы задавать в таблице переходов не событие, а их диапазон. Для этого достаточно добавить в таблицу дополнительный столбец и подправить функцию диспетчер. Таблица в этом случае будет выглядеть так:


#define EVENT_KEY1 34
#define EVENT_KEY2 35
#define EVENT_KEY3 36
#define EVENT_KEY4 37

MENU_DEVICE menu[] = {
{STATE1, EVENT_KEY1, EVENT_KEY3, STATE1, Handler1},
{STATE2, EVENT_KEY4, EVENT_KEY4, STATE2, Handler2},

{0,0,0,0}
};

Если события во втором столбце и третьем не совпадают, то подразумевается диапазон событий. Естественно при этом коды событий должны идти друг за другом, как показано в примере.
Если в этих столбцах одно и то же событие, то будет обрабатываться только оно.
Таблица переходов сократилась, наглядность и быстродействие остались практически на том же уровне.
Функцию диспетчер подправим в конце статьи.

Один обработчик события для нескольких состояний

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



MENU_DEVICE menu[] = {
{STATE2, EVENT_CANCEL, STATE1, Handler1},
{STATE3, EVENT_CANCEL, STATE1, Handler1},
{STATE4, EVENT_CANCEL, STATE1, Handler1}

{0,0,0,0}
};

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


#define STATE2 55
#define STATE3 56
#define STATE4 57

MENU_DEVICE menu[] = {
{STATE2, STATE4, EVENT_CANCEL, STATE1, Handler1},

{0,0,0,0}
};


Изменение состояния событийной системы в обработчиках

   В таблице переходов «жестко забиты» все возможные изменения состояния событийной системы. Это очень наглядно – ведь, по сути, ты видишь весь алгоритм работы программы на одном листе.
  Также такая структура позволяет быстро и безболезненно модифицировать систему - добавлять в нее новый функционал или наоборот убирать. Причем для этого совсем необязательно удалять строки, достаточно их закомментировать.
Иногда требуется изменять состояние системы внутри какого-нибудь обработчика событий, что делать в этом случае?

 Я вижу два решения этой проблемы.
 Первое. Определить некую константу, указывающую событийной системе и вам, что изменение состояние будет происходить в обработчике. Например, так:

#define STATE_CH_HANDLER 55

MENU_DEVICE menu[] = {
{STATE1, EVENT_KEY1, STATE1, Handler1},
{STATE2, EVENT_TIMER, STATE_CH_HANDLER, Handler2},
{STATE4, EVENT_UART, STATE4, Handler3}

{0,0,0,0}
};

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

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

MENU_DEVICE menu[] = {
{STATE1, EVENT_KEY1, STATE1, Handler1},
{STATE1, EVENT_CH_STATE2, STATE2, Empty},
{STATE1, EVENT_CH_STATE3, STATE3, Empty},

{0,0,0,0}
};

void Handler1(void)
{

if (data == 1) {event = EVENT_CH_STATE2;}
else {event = EVENT_CH_STATE3;}

ES_PlaceEvent(event);
}

где Empty – указатель на пустую функцию void Empty(void){}
Handler1() - функция, внутри которой требуется изменить состояние системы.

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

Финальный код

#include "event-system.h"

//кольцевой буфер
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;

//взять событие
unsigned char ES_GetEvent(void)
{
   unsigned char event;
   if (countBuf > 0){
      event = cycleBuf[headBuf];
      countBuf--;
      headBuf = (headBuf + 1) & (SIZE_BUF - 1);
      return event;
   }
  return 0;
}

//положить событие
void ES_PlaceEvent(unsigned char event)
{
   if (countBuf < SIZE_BUF){
      cycleBuf[tailBuf] = event;
      tailBuf = (tailBuf + 1) & (SIZE_BUF - 1);
      countBuf++;
   }
}

//*****************************************************

volatile unsigned char state = 0;

__flash typedef struct
{
   unsigned char startState;
   unsigned char endState;
   unsigned char startEvent;
   unsigned char endEvent;
   unsigned char nextstate;
   void (*pStateFunc1)(void);
   void (*pStateFunc2)(void);
} table_state_t;

//****************************************************
void ES_Init(void)
{
   tailBuf = 0;
   headBuf = 0;
   countBuf = 0;
}

void EmptyFunc(void)
{
}


//таблица состояний
table_state_t table[] = {

//обязательная пустая строка таблицы
{0, 0, 0, 0, 0, NULL, NULL}
};

//_______диспетчер событийной системы________
void ES_Dispatch(unsigned char event)
{
   void (*pStateFunc1)(void);
   void (*pStateFunc2)(void);
   unsigned char i;

   pStateFunc1 = NULL;
   pStateFunc2 = NULL;

   //определяем следующее состояние
   for (i=0; table[i].startState; i++)
   {
      //если текущее состояние попадает в диапазон
      if ((state >= table[i].startState)&&(state <= table[i].endState)){
         //если поступившее событие попадает в диапазон
         if((event >= table[i].startEvent) && (event <= table[i].endEvent)){
            //меняем состояние если требуется
            if (table[i].nextstate != STATE_NO_CHANGE)
               state = table[i].nextstate;
            pStateFunc1 = table[i].pStateFunc1;
            pStateFunc2 = table[i].pStateFunc2;

            break;
           }
       }
   }
   if (pStateFunc1) pStateFunc1();
   if (pStateFunc2) pStateFunc2();
}

Файлы

Событийная система. Заготовка для IAR`а

Related items