Вопросы организации программ встраиваемых систем довольно скудно освещены в отечественной литературе. Поэтому у начинающих программистов микроконтроллеров рано или поздно возникают проблемы при написании больших проектов.
Стандартный подход к построению микроконтроллерных программ сводится к использованию бесконечного цикла, внутри которого происходит опрос флагов, и вызываются разные функции. Однако, такая программа не наглядна, и ее сложно модифицировать.
Организация программы в виде событийной системы (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
Вышесказанное всегда сидело у меня в
голове как только я приступал к написанию
новой программы .
Этот while(1) я уже принял как необходимость .
Спасибо за статью .
Я нигде не могу найти пример реализации... Если есть время, напиши хотябы краткую статью про это, я думаю многим будет интересно
Собственно и здесь этот цикл присутствует. Просто здесь - более удобная (для программиста) организация программы.
Вероятно во многих случаях еще более удобным будет использование RTOS.
Quote:
1) в вариате с бесконечным циклом обычно анализируются флаги. Они очень быстро устанавливаются и быстро анализируются. Если надо очень быстро, флаг размещается в регистре. Поскольку флаги - это биты, их можно группировать в байты и одной командой проверки сразу проверять группу. Вот вы получили не линейную очередь, а дерево. Организуйте его так, как Вам надо.
2) описанный выше диспетчер может быть элементом этого дерева. Естественно, Вы не будете гонять диспетчер, если у Вас события происходят каждые 10 мкс. Ето всего 100 AVR-команд. Эти события Вы обработаете на предыдущем уровне.
3) система прерываний у AVR одноранговая. Т.е. она не допускает вложенных прерываний. Если Вы сами не позволите ей это делать. Для этого в конкретной программе обработки прерывания, в которой допустимо, вы разрешаете прерывание. А, чтобы прерывания не мешали друг - другу, Вы так продумываете работу в прерываниях, чтобы не мешать другим. Например, все, что можно сделать за 5 мкс, (50 команд AVR).
4) раз программа диспетчера достаточно проста, Вы можете себе позводить создать в системе несколько очередей с разным приоритетом. Кроме того, следя за счетчиком FIFO, Вы можете менять приоритеты очередей. Приоритеты меняет тот бесконечный цикл, который висит выше диспетчера.
5) в микроконтроллер е Вы работаете без операционной системы, и переключение между задачами Вы должны организовать сами, с нуля.
сделал обработчик событий на таблице.
хочу таблицу хранить в 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.
А как вызывать функцию в этом случае -- понять не могу.
Надеюсь на Вашу помощь.
TransitionTable[curr_event][curr_state].func(); // вызов функции
next_state = TransitionTable [curr_event][cu rr_state].NStat e; // смена состояния
В моём случае заработало так:
void (*pFunc)(void);
pFunc = ((void*)pgm_read_word(&TransitionTable[curr_event][curr_state].func));
pFunc();
дайте пожалуйста ссылки на первоисточники :-)
Насколько я это всё понимаю -- нужно все функции делать с одинаковыми параметрами.
Гораздо проще, ИМХО, сделать подобие почтовой системы. "почтовый ящик" -- какой-либо буфер, в который складывать "сообщения" в формате "id получателя; код сообщения". а оброботчики уже будут разгребать этот бефер.
Гораздо интересней будет переделать функцию ES_PlaceEvent() добавив вторым параметром сообщение обработчику.Ну и научить диспетчер передавать параметр. Очередь вырастит только вширь. Кто шарит попробуйте сделать так.
Массив теперь 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);
}
Теперь вопрос, будет ли сиё работать?
О... нечто подобное я и имел в виду.
у меня встречный вопрос -- а зачем обработчикам передавать какие-либо сообщения?
не поделитесь исходной проблемой?
Положить событие:
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-порт так же отправить символ. В конце концов никто не заставляет обработчикам отправлять сообщения.
А я делаю открыватель двери по сканеру отпечатка пальца, есть экран, несколько кнопок и замок. Вот думаю.
у себя делат. так, чтобы на каждую пару состояние+событ ие была одна функция которая делает одно конкретное действие. так гораздо проще сопровождать программу и не заблудиться в логике.
Да и к месту говоря функцию "Взять событие" , вернее часть её кишок закинул в диспетчер, пускай при нуле в буфере на пустышку идет, там тоже может быть добро.
/дма - да будь такое, rtos и все дела, там кстати так же кольцевые буфера всюду для передачи.
Шикарная система прерываний с нивелирована к тупому polling - запросов от оборудования. Почему предлагаемый способ плох в контроллера с ограниченным размером памяти ?
Буфер в системе маловат и возможна потеря событий и не только событий. Да и нет большого смысла складывать эти события в кольцевой буфер. Эти запросы всё одно торчат в регистрах оборудования чипа в виде запросов на обслуживание. Ну так и опрашивай в цикле эти запросы и не морочь себе голову с кольцевым буфером.
RSS feed for comments to this post