Организация программ. Событийная система (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

Comments   

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

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

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

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

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

Собственно и здесь этот цикл присутствует. Просто здесь - более удобная (для программиста) организация программы.
Вероятно во многих случаях еще более удобным будет использование RTOS.
# Pashgan 2010-12-05 16:13
Зачем вырывать слова из предложения? Речь ведь не только о бесконечном цикле.
Quote:
Стандартный подход к построению микроконтроллерных программ сводится к использованию бесконечного цикла, внутри которого происходит опрос флагов, и вызываются разные функции.
# Інший Сергій 2011-06-25 22:21
Черга з організацією FIFO працює тільки за умови однорангових подій!. А що робити коли події мають різний приорітет? "Перетасовувати " чергу? А якщо подія настільки важлива, що потрібно перервати обробку поточної події?
# wukrlvy 2012-02-08 19:14
Золотая серединка, обычно, лежит посредине.
1) в вариате с бесконечным циклом обычно анализируются флаги. Они очень быстро устанавливаются и быстро анализируются. Если надо очень быстро, флаг размещается в регистре. Поскольку флаги - это биты, их можно группировать в байты и одной командой проверки сразу проверять группу. Вот вы получили не линейную очередь, а дерево. Организуйте его так, как Вам надо.
2) описанный выше диспетчер может быть элементом этого дерева. Естественно, Вы не будете гонять диспетчер, если у Вас события происходят каждые 10 мкс. Ето всего 100 AVR-команд. Эти события Вы обработаете на предыдущем уровне.
3) система прерываний у AVR одноранговая. Т.е. она не допускает вложенных прерываний. Если Вы сами не позволите ей это делать. Для этого в конкретной программе обработки прерывания, в которой допустимо, вы разрешаете прерывание. А, чтобы прерывания не мешали друг - другу, Вы так продумываете работу в прерываниях, чтобы не мешать другим. Например, все, что можно сделать за 5 мкс, (50 команд AVR).
4) раз программа диспетчера достаточно проста, Вы можете себе позводить создать в системе несколько очередей с разным приоритетом. Кроме того, следя за счетчиком FIFO, Вы можете менять приоритеты очередей. Приоритеты меняет тот бесконечный цикл, который висит выше диспетчера.
5) в микроконтроллер е Вы работаете без операционной системы, и переключение между задачами Вы должны организовать сами, с нуля.
# wukrlvy 2012-02-21 21:02
Все перечисленные выше варианты программировани я имеют определенные названия в зарубежной литературе. Ничего нового. Только все эти варианты надо еще раскопать.
# Luckonov 2012-03-29 00:10
скажите Pashgan почему вы выбрали именно массив указателей на функции обработчики, вместо case. Какие приемущества? (кроме наверно скорости, но так ли суещственно)
# Vlad 2013-04-11 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 2013-04-11 09:51
А как работает, если ты обращаешься к элементу двумерного массива, а поле структуры не указываешь?
# Vlad 2013-04-11 10:46
Извиняюсь -- пропустил при наборе:

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

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

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

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

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

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

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

Code: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++; //увеличить счетчик
}
}



Диспетчер теперь такой:
Code: 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;
}

}


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


Теперь вопрос, будет ли сиё работать?
# Vlad 2015-08-03 10:30
Теперь вопрос, будет ли сиё работать?

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

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

не поделитесь исходной проблемой?
# hes 2015-08-05 18:24
Да я тут попутал с приведениями, поскольку ими не пользовался. Тут разобрался наконец. По новому будет так:
Положить событие:
Code: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++; //увеличить счетчик
}
}


Диспетчер:
Code: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 2015-08-06 05:18
глобальный (кольцевой) буфер, событие иницирует передачу/вывод, дальше прерывания/дма.

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

У вас недостаточно прав для комментирования.