Вопросы организации программ встраиваемых систем довольно скудно освещены в отечественной литературе. Поэтому у начинающих программистов микроконтроллеров рано или поздно возникают проблемы при написании больших проектов.
Стандартный подход к построению микроконтроллерных программ сводится к использованию бесконечного цикла, внутри которого происходит опрос флагов, и вызываются разные функции. Однако, такая программа не наглядна, и ее сложно модифицировать.
Организация программы в виде событийной системы (event driven system) лишена этих недостатков, позволяет реализовывать сложную логику, и не так “тяжеловесна” как операционные системы (RTOS). В этой статья мы разберемся с принципом ее работы и рассмотрим простой вариант ее реализации.
Теория
Событийная система представляет собой набор конечных автоматов, вызываемых диспетчером для обработки очереди событий. Звучит несколько заумно, но на деле все довольно просто.
Событие - это нажатие кнопки, наступление прерывания, окончание вычисления формулы и т.п. вещи. Термин этот вроде бы настолько очевиден, что сложно дать ему строгое определение. В одной книге я встречал определение события как местоположения в пространстве-времени имеющее значение для системы. В этом что-то есть.
Каждое событие в системе имеет свой код, который помещается в кольцевую очередь в случае его наступления. Очередь – это структура данных, основанная на принципе “первый вошел, первый вышел” (first in, first out - FIFO) или “кто первый встал, того и тапочки”. То есть событие, попавшее в очередь первым, будет первым и обработано. С этой структурой мы уже имели дело в статье про USART, только там я называл ее кольцевой буфер.
Диспетчер извлекает событие из очереди и вызывает для него соответствующий обработчик. Когда обработчик заканчивает работу, управление снова передаются диспетчеру. Так повторяется до тех пор, пока очередь не опустеет.
Обработчик события это функция, которая чаще всего построена в виде конечного автомата (Finite State Machine или FSM). Автомат можно определить как виртуальное устройство, умеющее выполнять определенную работу. Он характеризуется таким понятием, как состояние. В зависимости от состояния , автомат по разному реагирует на входные воздействия и выполняет разную работу. Число возможных состояний автомата может быть велико, но оно всегда конечно (поэтому автомат так и называют). Текущее состояние автомата – результат воздействий на него в прошлом, а набор правил, по которым происходит смена состояний автомата называется функцией переходов. Существует много вариантов реализации автоматов, в простейшем случае он строится на операторе switch. Тема автоматов довольно интересна, но выходит за рамки этой статьи. В будущем обязательно напишу что-нибудь по этому вопросу.
Программная реализация
Практический пример будем рассматривать в следующей статье, а в этом разберем только заготовку для простой событийной системы.
Из приведенного выше рисунка понятно, что костяк системы составляют – кольцевой буфер/очередь, диспетчер и обработчики. Для заготовки нужен только буфер и диспетчер. Обработчики вещь индивидуальная и от проекта к проекту меняются.
Кольцевой буфер и как он используется
Не буду приводить код кольцевого буфера, он уже подробно разбирался.Для работы с кольцевым буфером у нас есть две основные функции:
void ES_PlaceEvent(unsigned char event) – положить событие в буфер
unsigned char ES_GetnEvent(void) – взять событие из буфера
Функция ES_PlaceEvent вызывается источником события. Чаще всего это происходит в прерываниях. Например так:
//коды событий
...
#define EVENT_TIMER0 0x01
…
#pragma vector = TIMER0_COMP_vect
__interrupt void Timer0CompVect(void)
{
ES_PlaceEvent(EVENT_TIMER0);
}
Функция ES_GetEvent вызывается из main`а в бесконечном цикле while. Она возвращает код события и если он не равен нулю, то запускается диспетчер.
Диспетчер
Диспетчер должен вызывать для каждого события свой обработчик. Его можно построить на операторе switch, но мне больше нравиться диспетчер с массивом функций. Он простой, как три копейки, но не очень безопасный.
void ES_Dispatch(unsigned char event)
{
void (*pFunc)(void); //переменная – указатель на функцию
pFunc = FuncAr[event-1]; //присваиваем ей значение из массива
pFunc(); //вызываем функцию
}
FuncAr[] – это массив указателей на функции-обработчики событий. Код события используется в качестве индекса, а значит функции-обработчики событий должны размещаться в массиве FuncAr[] в определенном порядке. Диспетчер не безопасный, потому что если передать ему событие, код которого больше размерности массива, то случится какой-нибудь коллапс.
Массив указателей на функции должен быть объявлен до диспетчера. //пустая функция, ничего не делает
void EmptyFunc(void)
{
}
//массив указателей на функции-обработчики
__flash void (*FuncAr[])(void) =
{
EmptyFunc
//потом добавим сюда указатели на обработчики
};
Пока что массив содержит указатель на пустую функцию EmptyFunc() – это просто для наглядности.
Вот собственно и все содержимое заготовки.
event-system.c
Функция main()
Функция main() будет выглядеть так:
//подключение заголовочных файлов
//макроопределения
unsigned char event = 0;
int main(void)
{
//инициализация периферии
//разрешение прерываний
while(1){
event = ES_GetEvent();
if (event)
ES_Dispatch(event)
}
return 0;
}
Файлы
Заготовка для IAR`aЗаготовка для CodeVisionПродолжение - Событийная система на таблице