Событийная система на таблице очень удобная вещь для организации микроконтроллерных программ. На ее основе можно построить всю программу микроконтроллера или какую-то ее отдельную часть, например, меню пользователя. В этой статье мы рассмотрим несколько модификаций старого варианта событийной системы, которые расширяют ее функциональность.
Один обработчик для нескольких событий
Довольно часто возникает ситуация, когда нужно обрабатывать несколько событий одним и тем же обработчиком. Например, это могут быть нажатия кнопок клавиатуры. Если использовать старый вариант событийной системы, то придется для каждой кнопки клавиатуры описывать отдельную строку в таблице переходов. Например, так:
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();
}