Эта статья взята с сайта www.chipenable.ru, но она произвела на меня
такое впечатление, что я решил выложить ее у себя. Теперь я понимаю, почему за некоторые коды платят миллионы. Несколько десятков строк, но гениальных. Спасибо автору.
Организация программ. Событийная система на таблице
Стандартный подход к написанию программы для микроконтроллера сводится к использованию бесконечного цикла (суперлупа), внутри которого непрерывно опрашиваются флаги и запускаются те или иные
функции. Этот подход вполне оправдан для написания небольших программ, но при превышении определенного порога сложности, такая программа становится громоздкой, непонятной и запутанной. Этой
ситуации можно избежать, если на начальной стадии разработки оценить сложность программы и выбрать для нее более подходящую форму организации.
Самой ближайшей альтернативой является событийная система. Она не такая тяжеловесная, как операционная, и в то же время позволяет организовать программу в довольно стройную и
понятную конструкцию, которую легко наращивать и изменять.
Принцип работы событийной системы
Событийная система представляет собой один или несколько конечных автоматов (State Machine), обрабатывающих очередь событий. Событие можно определить как изменение в системе,
требующее реакции (обработки) со стороны микроконтроллера. Источником события может выступать периферийный модуль, внешнее воздействие (например, нажатие кнопки), функция, ну и тому подобные
вещи. Как правило, источников событий несколько и они асинхронны по отношению к друг другу. То есть, события могут происходить в произвольные моменты времени и в том числе одновременно.
В любой момент времени микроконтроллер может обрабатывать только одно событие и для того, чтобы другие события не потерялись, используется кольцевая очередь (кольцевой буфер). Это
такая структура данных. С одного конца очереди (из головы) событие извлекается и обрабатывается, на другой конец очереди (в хвост) поступают новые события. Событие, попавшее в очередь
первым, будет первым и обработано. В качестве аналогии можно привести пример с очередью в магазине.
Для каждого события в системе определен код. Этот код и помещается в очередь источником события с помощью специальной функции – «положить событие». Другая функция – «взять событие» -
извлекает из очереди код события и передает его диспетчеру для обработки. Один из вариантов его реализации основан на использовании массива указателей на функции, каждая из которой представляет
собой конечный автомат, обрабатывающей одно событие. Этот подход я описывал в одной из предыдущих
статей, он имеет право на жизнь, имеет свои достоинства и недостатки. Есть другой вариант построения
диспетчера событийной системы - с использованием таблицы переходов. В простой событийной системе на таблице диспетчер представляет собой один конечный автомат обрабатывающий все события.
Событийная система на таблице
В этой статье мы рассмотрим заготовку событийной системы на таблице, некий минимальный функционал, на базе которого можно создавать проекты. В следующей статье будет рассмотрен
практический пример –
часы на микроконтроллере
AVR.
Основу событийной системы на таблице составляет – кольцевая очередь (буфер), диспетчер и таблица переходов.
//кольцевая очередь/буфер
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) {}
Теперь рассмотрим функцию, которая будет работать с таблицей переходов.
//состояния системы
#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 считываются первые два столбца таблицы и сравниваются с кодом текущего состояния и кодом поступившего события. Код состояния хранится в
глобальной переменной, а код события передается функции в качестве параметра. Когда нужная строка найдена, проверяется значение третьего столбца таблицы. Если оно не равно STATE_NO_CHANGE,
текущее состояние системы меняется. Далее с помощью указателя из четвертого столбца запускается функция-обработчик события, а после ее выполнения цикл завершается. Вот и все.