Стандартный подход к написанию программы для микроконтроллера сводится к использованию бесконечного цикла (суперлупа), внутри которого непрерывно опрашиваются флаги и запускаются те или иные функции. Этот подход вполне оправдан для написания небольших программ, но при превышении определенного порога сложности, такая программа становится громоздкой, непонятной и запутанной. Этой ситуации можно избежать, если на начальной стадии разработки оценить сложность программы и выбрать для нее более подходящую форму организации.
Самой ближайшей альтернативой является событийная система. Она не такая тяжеловесная, как операционная, и в то же время позволяет организовать программу в довольно стройную и понятную конструкцию, которую легко наращивать и изменять.
Принцип работы событийной системы
Событийная система представляет собой один или несколько конечных автоматов (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).
функция, извлекающая событие из очереди - 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) {}
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, текущее состояние системы меняется. Далее с помощью указателя из четвертого столбца запускается функция-обработчик события, а после ее выполнения цикл завершается. Вот и все.
Полный код заготовки событийной системы на таблице
Файлы
Событийная система. Заготовка для IAR AVR
Событийная система. Заготовка для WINAVR
Событийная система. Заготовка для CodeVision
Событийная система. Заготовка для WINAVR
Событийная система. Заготовка для CodeVision