Организация программ. Событийная система на таблице

06/12/2010 - 18:14
    Стандартный подход к написанию программы для микроконтроллера сводится к использованию бесконечного цикла (суперлупа), внутри которого непрерывно опрашиваются флаги и запускаются те или иные функции. Этот подход вполне оправдан для написания небольших программ, но при превышении определенного порога сложности, такая программа становится громоздкой, непонятной и запутанной. Этой ситуации можно избежать, если на начальной стадии разработки оценить сложность программы и выбрать для нее более подходящую форму организации.
   Самой ближайшей альтернативой является событийная система. Она не такая тяжеловесная, как операционная, и в то же время позволяет организовать программу в довольно стройную и понятную конструкцию, которую легко наращивать и изменять. 

Принцип работы событийной системы

   Событийная система представляет собой один или несколько конечных автоматов (State Machine), обрабатывающих очередь событий. Событие можно определить как изменение в системе, требующее реакции (обработки) со стороны микроконтроллера. Источником события может выступать периферийный модуль, внешнее воздействие (например, нажатие кнопки), функция, ну и тому подобные вещи. Как правило, источников событий несколько и они асинхронны по отношению к друг другу. То есть, события могут происходить в произвольные моменты времени и в том числе одновременно. 
   В любой момент времени микроконтроллер может обрабатывать только одно событие и для того, чтобы другие события не потерялись, используется кольцевая очередь (кольцевой буфер). Это такая структура данных. С одного конца очереди (из головы)  событие извлекается и обрабатывается, на другой конец очереди (в хвост) поступают новые события. Событие, попавшее в очередь первым, будет первым и обработано. В качестве аналогии можно привести пример с очередью в магазине. 
   Для каждого события в системе определен код. Этот код и помещается в очередь источником события с помощью специальной функции – «положить событие». Другая функция – «взять событие» - извлекает из очереди код события и передает его диспетчеру для обработки. Один из вариантов его реализации основан на использовании массива указателей на функции, каждая из которой представляет собой конечный автомат, обрабатывающей одно событие. Этот подход  я описывал в одной из предыдущих статей, он имеет право на жизнь, имеет свои достоинства и недостатки. Есть другой вариант построения диспетчера событийной системы - с использованием таблицы переходов. В простой событийной системе на таблице диспетчер представляет собой один конечный автомат обрабатывающий все события.   

Событийная система на таблице

   В этой статье мы рассмотрим заготовку событийной системы на таблице, некий минимальный функционал,  на базе которого можно создавать проекты. В следующей статье будет рассмотрен практический пример – часы на микроконтроллере AVR
   
   Основу событийной системы на таблице составляет – кольцевая очередь (буфер), диспетчер и таблица переходов.

  
Реализация кольцевой очереди/буфера разбиралась в статье "Организация обмена по USART `у с использованием кольцевого буфера", здесь все то же самое. Есть массив cycleBuf для хранения кодов событий, переменные tailBuf, headBuf для указания хвоста и головы очереди соответственно и countBuf для учета количества событий в очереди.   
 
//кольцевая очередь/буфер
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) {}

Теперь рассмотрим функцию, которая будет работать с таблицей переходов. 
 
//глобальная переменная, в которой хранится текущее состояние
volatile unsigned char currentState = 0;
 
//состояния системы
#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 считываются первые два столбца таблицы и сравниваются с кодом текущего состояния и кодом поступившего события. Код состояния хранится в глобальной переменной (currentState), а код события передается функции в качестве параметра. Когда нужная строка найдена, проверяется значение третьего столбца таблицы. Если оно не равно STATE_NO_CHANGE, текущее состояние системы меняется. Далее с помощью указателя из четвертого столбца запускается функция-обработчик события, а после ее выполнения цикл завершается. Вот и все.
 
Полный код заготовки событийной системы на таблице

Файлы

  

Comments   

# Alca 2010-12-07 07:03
О новый урок!!!
Спасибо.
# Антон 2010-12-07 10:44
А как передать параметры в обработчик события или использовать только глобальные переменные?
# Pashgan 2010-12-07 16:27
Использовать глобальные переменные.
# BSVi 2010-12-07 22:26
>но при превышении определенного порога сложности

Фигня, если превышается уровень сложности - стоит юзать полноценную ОС, плюс ко всему получаешь кучу плюшек.

Если ОС - жирно, и хочется как-то покрасивше и побыстрее, можно заюзать протопотоки. А городить такой велосипед я бы не стал.

В винде события сделаны из-за того, что они хорошо ложатся на логику работы форм, а не из-за быстродействия.
# Pashgan 2010-12-07 22:40
А мне нравится этот велосипед. На нем, например, удобно меню делать.
# Grigorij 2010-12-09 10:10
Внесу маленькое дополнение:
1. Кратко на русском про протопотоки - http://bsvi.ru/protopotoki-protothreads/
2. Оригинал на англ. и сама библиотека - http://www.sics.se/~adam/pt/
# Pashgan 2010-12-09 23:19
Спасибо, надо будет с ними разобраться.
# Indigo 2010-12-08 08:57
А источник событий может быть только из прерывания? А чтобы и прерывание и опрос генерили события можно сделать?
# Pashgan 2010-12-08 19:41
"Источником события может выступать периферийный модуль, внешнее воздействие (например, нажатие кнопки), функция,..."
# Indigo 2010-12-09 15:58
Тоесть и "фоновая" функция кнопку опросит сгенерит свое событие, и независимо от нее таймер в обработчике прерывания тоже может сгенерить свое событие? Тогда уж перепишите функцию ES_PlaceEvent чтобы запрещала прерывания при манипуляциях с переменной tailBuf.
# Grigorij 2010-12-09 17:08
Тогда, наверное, надо переписать еще и ES_GetEvent. Ведь теоретически возможна ситуация, когда при вызове ES_PlaceEvent (в прерывании) будет перезаписано то значение cycleBuf[headBu f], которые мы ожидали получить вызвав ES_GetEvent.

Ну и до кучи тогда. Можно в обоих функциях избавиться от условий вида:
Code:if (XXX == SIZE_BUF) XXX = 0;
Заменив на:
Code:XXX &= (SIZE_BUF-1)
Единственное только число SIZE_BUF должно быть степенью двойки.
# Pashgan 2010-12-09 22:53
Indigo, необязательно переписывать функцию, можно вызывать ее так
Code:
__disable_interrupt();
ES_PlaceEvent(..);
__enable_interrupt();


ну или объявить ее с ключевым словом __monitor
Code:
__monitor void ES_PlaceEvent(..)


Grigorij, если прервется ES_GetEvent() ничего не случиться. До тех пор пока в буфере есть события голова и хвост указывают на разные ячейки.
А по поводу сокращения условия - да. можно избавиться. Я написал так код для наглядности
# Indigo 2010-12-10 10:55
Запрет прерывания? Не хватало еще МК "оглохнуть" на 40 тактов :) Подход в принципе не тот. Поясняю: ко вводу в очередь нового события подпускать нельзя ни фоновую программу, ни прерывательскую . Они все ставят только флаг и код события. А функция (пучть в Вашем случае ES_GetEvent), прежде чем анализировать что там есть в буфере событий, в цикле спокойно "сканирует источники" на предмет установленных флагов и тогда уж и кладет результаты их жизнедеятельнос ти в буфер, каждый раз на коротенькое время запрещая прерывания на момент опроса флага конкретного источника. Ну буквально на 4 такта. Кстати, этим одновременно придается источникам некий "приоритет", ставя важные опросы пораньше и прерывая дальнейший опрос при нахождении флага.
# Indigo 2010-12-10 11:06
Конечно, у каждого источника можно не один код события поставлять "наверх", а поставить свой индивидуальный например 2-х уровневый буферок. Так, на всякий случай, чтоб не быть особо уязвимым при частых событиях. Get_Event будет анализировать char-флаг а там 0(пусто), 1(одно событие от опрашиваемого устройства), или 2(ого, накопилось! :)). Причем все это без особенных затрат.
# Pashgan 2010-12-10 14:46
Quote:
Запрет прерывания? Не хватало еще МК "оглохнуть" на 40 тактов :) Подход в принципе не тот.
Это стандартный подход, используемый повсеместно. А вообще все зависит от приложения. Для многих приложений эти 40 тактов, что мертвому припарка, ничего не дают. Ни в плане улучшения, ни в плане ухудшения характеристик.
По поводу флагов. Флаг можно выставить однократно. А если событие успело прийти несколько раз? Как это отразить?
# Indigo 2010-12-10 16:25
Флаг - это не обязательно бит. Это, как написал выше, может быть char-величина количества пришедших но необработанных запросов от конкретного источника в своем небольшом буферке. 0-нет запросов, 1,2,3,пусть даже допустили чудовищный случай 4 события без обработки.
Источник запроса, например, обработчик быстрого SPI-slave, принял байт, положил новый пришедший байт в свой маленький буферок-массивч ик[4], и инкрементировал "флаг"(который не флаг а char). Прерывания запрещаются всего на несколько тактов.
-------------------------------------
Code:
char smallbufSPI[4], flag_counterSPI=0;
-------
-- Событие: пришел очередной байт SPI--
SAVE_INTERRUPT;
CLI;
smallbuf[flag_counterSPI]=SPDR;
if(flag_counterSPI<3) flag_counterSPI++;
RESTORE_INTERRUPT;

-------------------------------------
Ваша функция ES_GetEvent() при обходе всех источников событий смотрит на flag_counterSPI и "высасывает" этот буферок до опустошения.
-------------------------------------
Code:
ES_GetEvent:
SAVE_INTERRUPT;
CLI;
tmp_flag=flag_counterSPI;
flag_counterSPI=0;
tmp0=smallbufSPI[0];
tmp1=smallbufSPI[1];
tmp2=smallbufSPI[2];
tmp3=smallbufSPI[3];
RESTORE_INTERRUPT;
if(tmp_flag!=0)
{ спокойно без запрета прерываний
кладет tmpi в Ваш НАСТОЯЩИЙ буфер
событий.
}

Аналогично и для других источников запросов.
Все! Буфер сформирован.
И ES_GetEvent далее работает с буфером точно как у Вас. Все!
# Pashgan 2010-12-13 21:59
Quote:
Ваша функция ES_GetEvent() при обходе всех источников событий смотрит на flag_counterSPI и "высасывает" этот буферок до опустошения.
Это опять возврат к суперлупу и полингу большого количества флагов.
# Indigo 2010-12-19 14:48
Ничуть. Просто более быстрый и безопасный вариант все того же Вашего опроса
if (countBuf > 0)
....
# Антон 2010-12-08 10:22
Хорошо было бы сделать статью по организации программы по-switch технологии (конечный автомат). Мне понравилась событийна система. Я даже сделал на её основе регулятор давления пара и всё "сносно" работает.
# Pashgan 2010-12-09 23:18
Даешь материал по регулятору давления пара!!!
# Антон 2010-12-10 14:10
Я делал на ПИК24. В регуляторе нет ничего "космического". П-закон регулирования, динамическая индикация, + АЦП, - выход ШИМ :-)
# Pashgan 2010-12-10 14:49
Quote:
В регуляторе нет ничего "космического"
Это скорее достоинство, чем недостаток. Все статьи сайта я пытаюсь делать простыми и понятными.
# foxit 2010-12-10 13:26
Quoting Антон:
Хорошо было бы сделать статью по организации программы по-switch технологии (конечный автомат).


+1
# Pashgan 2010-12-13 22:02
Ученые будут биться над этим. Но не обещаю в ближайшее время по этой теме статьи. У меня есть почти законченный перевод про реализацию конечного автомата. Довольно интересный. Вот может его выложу.
# DVF 2010-12-09 17:02
Самое интересное тут - это как реализовать точность хода "часов". Гуманоиду, конечно, на мили и микро наплевать, а если надо синхронизироват ь от них внешние действия? А событие в очереди, блин, застряло.
Можно, конечно, сразу на ногу перепады выложить...
# Pashgan 2010-12-09 23:16
Да, сразу на ногу перепад давать. Такая событийная система не обеспечивает заданное время реакции на событие. Это же не RTOS.
# Zliva 2010-12-09 22:04
Спер. Респект. Жду часть 2 для наглядности. А то уже начал путаться в структуре switch-case. Эта система более наглядная и понятная. Сейчас думаю, как организовать обмен между двумя контролерами по УСАПП чтобы каждый видел статус, а такой подход вообще шикарно подходит.
Спасибо Pashgan
# Pashgan 2010-12-09 23:17
Да пожалуйста.
# foxit 2010-12-10 13:25
Quoting Zliva:
Спер. Респект. Жду часть 2 для наглядности. А то уже начал путаться в структуре switch-case. Эта система более наглядная и понятная. Сейчас думаю, как организовать обмен между двумя контролерами по УСАПП чтобы каждый видел статус, а такой подход вообще шикарно подходит.
Спасибо Pashgan

А можно подробней про обмен между мк
# Zliva 2010-12-10 06:13
У меня вопрос: Можно ли организовать двух и трех поточную событийную систему? У кого какие будут идеи?
# Pashgan 2010-12-10 14:54
Да можно, конечно. Делаешь два буфера и два обработчика. Навернуть тут можно много, главное не увлечься. Ведь все эту нужно в конце концов для решения конкретных задач.
# demiurg 2010-12-10 12:43
В этой системе точно такая же ситуация, как в подобии RTOS на easyelectronics .ru. События-задачи болтаются в буфере-очереди задач. И все это как дамоклов меч. Представим ситуацию, когда событие вдруг стало не актуально, но оно болтается в буфере-очереди. И когда очередь дойдет до него оно и произойдет. Хотя, вполне может быть, что эта ситуация как-то у вас решается...
# Pashgan 2010-12-10 14:56
Это не RTOS. Такая система не обеспечивает заданное время реакции на события. Может случиться вообще такая ситуация, что программа заткнется в обработчике.
# Zliva 2010-12-12 12:00
Quoting foxit:
Quoting Zliva:
Спер. Респект. Жду часть 2 для наглядности. А то уже начал путаться в структуре switch-case. Эта система более наглядная и понятная. Сейчас думаю, как организовать обмен между двумя контролерами по УСАПП чтобы каждый видел статус, а такой подход вообще шикарно подходит.
Спасибо Pashgan

А можно подробней про обмен между мк

Протоколов обмена между контролерами существует не очень много. Я делаю обычно по аналогу модбас с небольшой доработкой. В зависимости от сложности задачи, с контрольной или без контрольной сумы, с слейвом или без. Функции пишу свои.
# teiggery 2011-02-01 22:44
а вот по поводу массивов структур очень познавательно. :lol:
# teiggery 2011-02-01 22:45
думал что CVAVR не поймет. а понял. отлично!
# Pashgan 2011-02-03 13:32
Только там один косячок всплыл. Нужно добавить слово __flash для CV перед обявлением массива. Вот так.
Code:
__flash struct ROW_TABLE table[]

Перед структурой CV это слово игнорирует, а потому засовывает всю таблицу в ОЗУ.
# teiggery 2011-02-03 08:50
Pashgan, а для организации меню на массиве структур как задать структуре следующий элемент и предыдущий? и еще: для каждого подменю, как я понимаю нужно создавать свою структуру?
# Pashgan 2011-02-03 13:34
А зачем это делать? Это уже будут связанные списки. Несколько иная структура данных.
Для подменю не нужно создавать свою структуру. Просто забивай подменю в таблицу и все.
# foxit 2011-03-04 17:28
Quoting Pashgan:
У меня есть почти законченный перевод про реализацию конечного автомата. Довольно интересный. Вот может его выложу.


Когда выложишь?
# Pashgan 2011-03-05 06:37
Еще не отредактировал. Не знаю.
# Iren 2011-06-06 13:40
А что будет, если автомат находится в состоянии "Установка времени", а пришло прерывание от секундного таймера? Даже для такой несложной задачи таблица становится либо многомерной либо двумерной, но с повторениями. Может автомат лучше прицепить именно к часам, а не к диспетчеру. Диспетчер пусть занимается обработкой событий, то есть вызовом функций, а уж что эти функции делают - их проблемы.
# sayya78 2013-10-02 09:54
Пробую перенести данную заготовку на IAR под АРМы 6.5 и тут же получаю затыки:
Не объявленна переменная table
Error[Pe020]: identifier "table" is undefined
и не объявлен currentState:
Error[Pe020]: identifier "currentState" is undefined
Как я понимаю диспетчер должен тупо крутиться по кругу.
# sayya78 2013-10-02 10:04
решено
хотя в статье нет ни слова про то что currentState нужно обьявить как глобальную переменную...
# Pashgan 2013-10-02 10:44
Есть.
Quote:
Код состояния хранится в глобальной переменной, а код события передается функции в качестве параметра.
Сейчас что-нибудь добавлю, чтобы было более очевидно.
# Spectr 2014-05-18 11:18
Подскажите, как можно было бы организовать постоянную передачу управления различным функциям при возникновении соотв. события? Например, есть бегущая строка, которая работает в 3 основных режимах: скроллинг текста, редактор текста и командная консоль. Каждый из этих режимов - это функция с бесконечным циклом внутри. В любой момент вся система работает под управлением только ОДНОЙ функции. Задача: при возникновении события (в прерывании) производить передачу управления конкретному режиму (другой ф-ии с бесконечным циклом), при этом завершая выполнение текущей ф-ии. Т.е. как скакать по бесконечным циклам, в соотв. с тем или иным событием?
# _Артём_ 2014-05-19 16:01
Code:
volatile unsigned char SheduledTask;
unsigned char CurrentTask;
void interrupt_handler()
{
SheduledTask=DefineActualTask();
}

void Task0()
{
while (CurrentTask==SheduledTask) {
// çàäà÷à 0
}
}

void Task1()
{
while (CurrentTask==SheduledTask) {
// çàäà÷à 1
}
}

void Task2()
{
while (CurrentTask==SheduledTask) {
// çàäà÷à 2
}
}

int main()
{
while (1) {
CurrentTask=SheduledTask;
switch (SheduledTask) {
case 0:
Task0();
break;
case 1:
Task1();
break;
case 2:
Task2();
break;
}
}
}
# Spectr 2014-05-20 07:08
Спасибо! Пару дней назад тоже пришел к похожему коду. Только тут есть своя особенность. В функции Task1(), например, в условии главного цикла проверяется флаг CurrentTask. Такая организация становится неприемлемой, если в таком цикле есть множество других вложенных циклов, которые тормозят мгновенную передачу управления другой ф-ии, при возникновении события. Вывод: нужно ставить проверку флага в другое - более "оживленное" место цикла.
# _Артём_ 2014-05-20 12:22
Quoting Spectr:
тормозят мгновенную передачу управления другой ф-ии, при возникновении события.

Ну, вас не поймёшь: то мгновенная передача, то завершение текущих циклов.
Если хотите мгновенную, то можно в прервании перейти куда-нибудь в точку до вызова main и запустить его снова или вообще несколько main-ов сделать и запускать нужный.
Или RTOS попробуйте - будет переключаться за 400-500 тактов.
# Spectr 2014-05-21 11:00
Quoting _Артём_:
можно в прервании перейти куда-нибудь в точку до вызова main и запустить его снова...

Впервые слышу, что в исполнении main'а можно выходить за его же пределы, в данном случае, используя Си-код. И вообще, разве переход куда-либо в прерывании без возврата не забивает стек?
Насчет RTOS, думаю, что ее использование будет слишком жирненько для Atmega32.
# _Артём_ 2014-05-21 12:14
Quoting Spectr:

Впервые слышу, что в исполнении main'а можно выходить за его же пределы, в данном случае, используя Си-код.

Если хочется, то можно извратится: с помощью манипуляций с SP можно что-нибудь придумать. Правда надо ли такое?

Quoting Spectr:
И вообще, разве переход куда-либо в прерывании без возврата не забивает стек?

Стек можно откорректировать.

Quoting Spectr:

Насчет RTOS, думаю, что ее использование будет слишком жирненько для Atmega32.

На 3-5 задач 1кБ ОЗУ хватит. Если оставшегося килобайта для программы достаточно, то можно и RTOS применить.
# Spectr 2014-05-21 13:29
Quoting _Артём_:
Стек можно откорректировать.

На асме что ли?
Можно пример?
# _Артём_ 2014-05-21 14:04
Quoting Spectr:

На асме что ли?

Не обязательно на асме: у atmega32 все нужные регистры доступны. Но стеком можно и не заморачиваться - проще так:
Code:
void isr_handler()
{
if (NeedRestart())
asm("JMP 0x0000");
}


Но вряд ли нужно так программу делать...

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