Организация программ. Событийная система (Event driven system)

24/01/2010 - 21:55
 Вопросы организации программ встраиваемых систем довольно скудно освещены в отечественной литературе. Поэтому у начинающих программистов микроконтроллеров рано или поздно возникают проблемы при написании  больших проектов.
   Стандартный подход к построению микроконтроллерных программ сводится к использованию бесконечного цикла, внутри которого происходит опрос флагов, и вызываются разные функции. Однако, такая программа не наглядна, и ее сложно модифицировать.
   Организация программы в виде событийной системы (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

Комментарии   

# alexandershahbazov 25.01.2010 14:09
"Стандартный подход к построению микроконтроллер ных программ сводится к использованию бесконечного цикла, внутри которого происходит опрос флагов, и вызываются разные функции."

Вышесказанное всегда сидело у меня в
голове как только я приступал к написанию
новой программы .

Этот while(1) я уже принял как необходимость .

Спасибо за статью .
Ответить | Ответить с цитатой | Цитировать
# foxit 25.01.2010 17:33
Ждем пример практической реализации.
Ответить | Ответить с цитатой | Цитировать
# Guest 01.02.2010 09:47
Интересная статья ) самостоятельно к таким решениям не сразу приходишь ) Подобный подход с указателями на функции удобно использовать для организации различных вложенных меню на LCD, которые потом можно гибко изменять и реагировать на различные события
Ответить | Ответить с цитатой | Цитировать
# Pashgan 01.02.2010 18:49
Для реализации меню очень удобно использовать событийную систему на таблицах. По-крайней мере на своих устройствах все менюшки я делаю именно так.
Ответить | Ответить с цитатой | Цитировать
# Guest 16.04.2010 19:52
Цитирую Pashgan:
Для реализации меню очень удобно использовать событийную систему на таблицах. По-крайней мере на своих устройствах все менюшки я делаю именно так.

Я нигде не могу найти пример реализации... Если есть время, напиши хотябы краткую статью про это, я думаю многим будет интересно
Ответить | Ответить с цитатой | Цитировать
# Pashgan 17.04.2010 06:07
Событийная система на таблице - http://chipenable.ru/index.php/programming-c/73-sobytijnaja-sistema-na-tablice.html
Ответить | Ответить с цитатой | Цитировать
# Guest 20.06.2010 17:37
Спасибо за статью! Не подскажете, в какой литературе можно почитать про такую организацию программ?
Ответить | Ответить с цитатой | Цитировать
# Pashgan 05.12.2010 16:14
Ничего не могу посоветовать, потому что не попадались такие книги.
Ответить | Ответить с цитатой | Цитировать
# Ecole 03.12.2010 15:04
"Стандартный подход к построению микроконтроллер ных программ сводится к использованию бесконечного цикла"

Собственно и здесь этот цикл присутствует. Просто здесь - более удобная (для программиста) организация программы.
Вероятно во многих случаях еще более удобным будет использование RTOS.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 05.12.2010 16:13
Зачем вырывать слова из предложения? Речь ведь не только о бесконечном цикле.
Цитата:
Стандартный подход к построению микроконтроллерных программ сводится к использованию бесконечного цикла, внутри которого происходит опрос флагов, и вызываются разные функции.
Ответить | Ответить с цитатой | Цитировать
# Інший Сергій 25.06.2011 22:21
Черга з організацією FIFO працює тільки за умови однорангових подій!. А що робити коли події мають різний приорітет? "Перетасовувати " чергу? А якщо подія настільки важлива, що потрібно перервати обробку поточної події?
Ответить | Ответить с цитатой | Цитировать
# wukrlvy 08.02.2012 19:14
Золотая серединка, обычно, лежит посредине.
1) в вариате с бесконечным циклом обычно анализируются флаги. Они очень быстро устанавливаются и быстро анализируются. Если надо очень быстро, флаг размещается в регистре. Поскольку флаги - это биты, их можно группировать в байты и одной командой проверки сразу проверять группу. Вот вы получили не линейную очередь, а дерево. Организуйте его так, как Вам надо.
2) описанный выше диспетчер может быть элементом этого дерева. Естественно, Вы не будете гонять диспетчер, если у Вас события происходят каждые 10 мкс. Ето всего 100 AVR-команд. Эти события Вы обработаете на предыдущем уровне.
3) система прерываний у AVR одноранговая. Т.е. она не допускает вложенных прерываний. Если Вы сами не позволите ей это делать. Для этого в конкретной программе обработки прерывания, в которой допустимо, вы разрешаете прерывание. А, чтобы прерывания не мешали друг - другу, Вы так продумываете работу в прерываниях, чтобы не мешать другим. Например, все, что можно сделать за 5 мкс, (50 команд AVR).
4) раз программа диспетчера достаточно проста, Вы можете себе позводить создать в системе несколько очередей с разным приоритетом. Кроме того, следя за счетчиком FIFO, Вы можете менять приоритеты очередей. Приоритеты меняет тот бесконечный цикл, который висит выше диспетчера.
5) в микроконтроллер е Вы работаете без операционной системы, и переключение между задачами Вы должны организовать сами, с нуля.
Ответить | Ответить с цитатой | Цитировать
# wukrlvy 21.02.2012 21:02
Все перечисленные выше варианты программировани я имеют определенные названия в зарубежной литературе. Ничего нового. Только все эти варианты надо еще раскопать.
Ответить | Ответить с цитатой | Цитировать
# Luckonov 29.03.2012 00:10
скажите Pashgan почему вы выбрали именно массив указателей на функции обработчики, вместо case. Какие приемущества? (кроме наверно скорости, но так ли суещственно)
Ответить | Ответить с цитатой | Цитировать
# Vlad 11.04.2013 08:06
Добрый день!

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

Может подскажете как это сделать?

строка таблицы имеет такой тип:
typedef struct {
byte NState; // Next State
void (*func)(void); // State-Event Handler Function
} t_TT_Row;

таблицу объявляю так:
const t_TT_Row TransitionTable [2][2] = {
{{ns1,func1},{ns2,func2}},
{{ns3,func3},{ns4,func4}}
};

далее, в main, делаю так:
curr_event = get_event();
TransitionTable[curr_evetn][curr_state](); // вызов функции
next_state = TransitionTable [curr_evetn][cu rr_state]; // смена состояния

так всё работает. но если обхявляю талицу как PROGMEM (gcc), то новое состояние получаю через pgm_read_byte.
А как вызывать функцию в этом случае -- понять не могу.

Надеюсь на Вашу помощь.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 11.04.2013 09:51
А как работает, если ты обращаешься к элементу двумерного массива, а поле структуры не указываешь?
Ответить | Ответить с цитатой | Цитировать
# Vlad 11.04.2013 10:46
Извиняюсь -- пропустил при наборе:

TransitionTable[curr_event][curr_state].func(); // вызов функции
next_state = TransitionTable [curr_event][cu rr_state].NStat e; // смена состояния
Ответить | Ответить с цитатой | Цитировать
# Vlad 12.04.2013 07:36
Разобрался.
В моём случае заработало так:

void (*pFunc)(void);
pFunc = ((void*)pgm_read_word(&TransitionTable[curr_event][curr_state].func));
pFunc();
Ответить | Ответить с цитатой | Цитировать
# Pashgan 12.04.2013 11:59
Хорошо. Не успел тебе ответить. В статье по событийной системе на таблице в проекте для WinAvr так и сделано.
Ответить | Ответить с цитатой | Цитировать
# FreshMan 08.06.2014 04:45
Цитирую wukrlvy:
Все перечисленные выше варианты программирования имеют определенные названия в зарубежной литературе. Ничего нового. Только все эти варианты надо еще раскопать.

дайте пожалуйста ссылки на первоисточники :-)
Ответить | Ответить с цитатой | Цитировать
# hes 01.08.2015 15:05
А как бы передать еще и параметры функциям-обрабо тчикам?
Ответить | Ответить с цитатой | Цитировать
# Влади 01.08.2015 17:41
Цитирую hes:
А как бы передать еще и параметры функциям-обработчикам?

Насколько я это всё понимаю -- нужно все функции делать с одинаковыми параметрами.
Гораздо проще, ИМХО, сделать подобие почтовой системы. "почтовый ящик" -- какой-либо буфер, в который складывать "сообщения" в формате "id получателя; код сообщения". а оброботчики уже будут разгребать этот бефер.
Ответить | Ответить с цитатой | Цитировать
# hes 03.08.2015 08:12
Добавление еще одного буфера по такой схеме будет сложней чем сама эта событийная система. Например его сортировка и изъятие "сообщения" с середины.
Гораздо интересней будет переделать функцию ES_PlaceEvent() добавив вторым параметром сообщение обработчику.Ну и научить диспетчер передавать параметр. Очередь вырастит только вширь. Кто шарит попробуйте сделать так.
Ответить | Ответить с цитатой | Цитировать
# hes 03.08.2015 10:20
Немного переделал но пока не тестировал.

Массив теперь unsigned int
Код: static volatile unsigned int cycleBuf[SIZE_BUF];// старшие 8 бит код события, мл-параметр обработчику

Так же изменились прототипы функций
Код:
unsigned int ES_GetEvent(void);//взять код собыитя
void ES_PlaceEvent(unsigned char event,unsigned char message);//разместить событие
void ES_Dispatch(unsigned int event); //вызов диспетчера

К функции положить событие добавился параметр для обработчика.

Код:void ES_PlaceEvent(unsigned char event,unsigned char message)
{
if (countBuf < SIZE_BUF){ //если в буфере еще есть место


cycleBuf[tailBuf] = ((unsigned int)event<<8)|(message); //кинуть событие в буфер
tailBuf++; //увеличить индекс хвоста буфера
if (tailBuf == SIZE_BUF) tailBuf = 0;
countBuf++; //увеличить счетчик
}
}



Диспетчер теперь такой:
Код: void ES_Dispatch(unsigned int event)
{
unsigned int message=(event<<8);
message=event>>8;
(unsigned char) message;
switch(event>>8){

case 1: EmptyFunc(message);break;
case 2: EmptyFunc(message);break;
case 3: EmptyFunc(message);break;
}

}


Ну и майн:
Код: while(1){
event = ES_GetEvent();
if (event>>8)
ES_Dispatch(event);
}


Теперь вопрос, будет ли сиё работать?
Ответить | Ответить с цитатой | Цитировать
# Vlad 03.08.2015 10:30
Теперь вопрос, будет ли сиё работать?

О... нечто подобное я и имел в виду.

у меня встречный вопрос -- а зачем обработчикам передавать какие-либо сообщения?

не поделитесь исходной проблемой?
Ответить | Ответить с цитатой | Цитировать
# hes 05.08.2015 18:24
Да я тут попутал с приведениями, поскольку ими не пользовался. Тут разобрался наконец. По новому будет так:
Положить событие:
Код:void ES_PlaceEvent(unsigned char event,unsigned char message)
{ // Первый параметр код событие, второй - параметр для функции обработчика

if (countBuf < SIZE_BUF){ //если в буфере еще есть место

cycleBuf[tailBuf] = (event<<8)|message; //кинуть событие и параметр в буфер
tailBuf++; //увеличить индекс хвоста буфера
if (tailBuf == SIZE_BUF) tailBuf = 0;
countBuf++; //увеличить счетчик
}
}


Диспетчер:
Код:void ES_Dispatch(unsigned int event)
{
unsigned char message=((event<<8)>>8);

switch((unsigned char)(event>>8))
{
case 1: open(message);break;
case 2: send_uart(message);break;
case 3: EmptyFunc(message);break;
}
}

Всё это дело работает в железе,второй параметр функции "Положить событие" проваливается до исполнителя.
Влад, как зачем? ну например хочу печатать на экран значение. Куды проще отправить его вместе с событием чем через внешние переменные, или в COM-порт так же отправить символ. В конце концов никто не заставляет обработчикам отправлять сообщения.
А я делаю открыватель двери по сканеру отпечатка пальца, есть экран, несколько кнопок и замок. Вот думаю.
Ответить | Ответить с цитатой | Цитировать
# Vlad 06.08.2015 05:18
глобальный (кольцевой) буфер, событие иницирует передачу/вывод, дальше прерывания/дма.

у себя делат. так, чтобы на каждую пару состояние+событ ие была одна функция которая делает одно конкретное действие. так гораздо проще сопровождать программу и не заблудиться в логике.
Ответить | Ответить с цитатой | Цитировать
# hes 06.08.2015 18:46
Ну отослать 2 разных символа это что две разные функции? Понятно что нет но они символ возьмут либо из переменной либо из очереди/буфера. На счёт глобального буфера эта система и есть оно самое, зачем еще что-то аналогичное.
Да и к месту говоря функцию "Взять событие" , вернее часть её кишок закинул в диспетчер, пускай при нуле в буфере на пустышку идет, там тоже может быть добро.
/дма - да будь такое, rtos и все дела, там кстати так же кольцевые буфера всюду для передачи.
Ответить | Ответить с цитатой | Цитировать
# pooh 19.08.2015 03:48
ну ни кокая это не Event driven system.
Шикарная система прерываний с нивелирована к тупому polling - запросов от оборудования. Почему предлагаемый способ плох в контроллера с ограниченным размером памяти ?
Буфер в системе маловат и возможна потеря событий и не только событий. Да и нет большого смысла складывать эти события в кольцевой буфер. Эти запросы всё одно торчат в регистрах оборудования чипа в виде запросов на обслуживание. Ну так и опрашивай в цикле эти запросы и не морочь себе голову с кольцевым буфером.
Ответить | Ответить с цитатой | Цитировать
# hhhh 03.11.2016 12:44
Цитата:
Эти запросы всё одно торчат в регистрах оборудования чипа в виде запросов на обслуживание. Ну так и опрашивай в цикле эти запросы и не морочь себе голову с кольцевым буфером.
Полностью поддерживаю сам написал штуки 3 таких программ пока до меня дошло что оно мне и не надо вовсе. Теперь у меня крутятся в цикле автоматы вызываемые последовательно и все прекрасно успевают.
Ответить | Ответить с цитатой | Цитировать
# Keroronsk 01.11.2017 05:45
Попробовал открыть страницу с событийной таблицей (73-sobytijnaja -sistema-na-tab lice.html), а там вирь. Можно что-то сделать с этим?
Ответить | Ответить с цитатой | Цитировать

Добавить комментарий

Защитный код
Обновить