Стандартный подход к написанию программы для микроконтроллера сводится к использованию бесконечного цикла (суперлупа), внутри которого непрерывно опрашиваются флаги и запускаются те или иные функции. Этот подход вполне оправдан для написания небольших программ, но при превышении определенного порога сложности, такая программа становится громоздкой, непонятной и запутанной. Этой ситуации можно избежать, если на начальной стадии разработки оценить сложность программы и выбрать для нее более подходящую форму организации.
Самой ближайшей альтернативой является событийная система. Она не такая тяжеловесная, как операционная, и в то же время позволяет организовать программу в довольно стройную и понятную конструкцию, которую легко наращивать и изменять.
Принцип работы событийной системы
Событийная система представляет собой один или несколько конечных автоматов (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).
функция, извлекающая событие из очереди - 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) {}
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, текущее состояние системы меняется. Далее с помощью указателя из четвертого столбца запускается функция-обработчик события, а после ее выполнения цикл завершается. Вот и все.
Полный код заготовки событийной системы на таблице
Файлы
Событийная система. Заготовка для IAR AVR
Событийная система. Заготовка для WINAVR
Событийная система. Заготовка для CodeVision
Событийная система. Заготовка для WINAVR
Событийная система. Заготовка для CodeVision
Comments
Спасибо.
Фигня, если превышается уровень сложности - стоит юзать полноценную ОС, плюс ко всему получаешь кучу плюшек.
Если ОС - жирно, и хочется как-то покрасивше и побыстрее, можно заюзать протопотоки. А городить такой велосипед я бы не стал.
В винде события сделаны из-за того, что они хорошо ложатся на логику работы форм, а не из-за быстродействия.
1. Кратко на русском про протопотоки - http://bsvi.ru/protopotoki-protothreads/
2. Оригинал на англ. и сама библиотека - http://www.sics.se/~adam/pt/
Ну и до кучи тогда. Можно в обоих функциях избавиться от условий вида:
Code:
if (XXX == SIZE_BUF) XXX = 0;
Заменив на:
Code:
XXX &= (SIZE_BUF-1)
Единственное только число SIZE_BUF должно быть степенью двойки.
Code:
__disable_interrupt();
ES_PlaceEvent(..);
__enable_interrupt();
ну или объявить ее с ключевым словом __monitor
Code:
__monitor void ES_PlaceEvent(..)
Grigorij, если прервется ES_GetEvent() ничего не случиться. До тех пор пока в буфере есть события голова и хвост указывают на разные ячейки.
А по поводу сокращения условия - да. можно избавиться. Я написал так код для наглядности
По поводу флагов. Флаг можно выставить однократно. А если событие успело прийти несколько раз? Как это отразить?
Источник запроса, например, обработчик быстрого 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 далее работает с буфером точно как у Вас. Все!
if (countBuf > 0)
....
+1
Можно, конечно, сразу на ногу перепады выложить...
Спасибо Pashgan
А можно подробней про обмен между мк
Протоколов обмена между контролерами существует не очень много. Я делаю обычно по аналогу модбас с небольшой доработкой. В зависимости от сложности задачи, с контрольной или без контрольной сумы, с слейвом или без. Функции пишу свои.
Code:
__flash struct ROW_TABLE table[]
Перед структурой CV это слово игнорирует, а потому засовывает всю таблицу в ОЗУ.
Для подменю не нужно создавать свою структуру. Просто забивай подменю в таблицу и все.
Когда выложишь?
Не объявленна переменная table
Error[Pe020]: identifier "table" is undefined
и не объявлен currentState:
Error[Pe020]: identifier "currentState" is undefined
Как я понимаю диспетчер должен тупо крутиться по кругу.
хотя в статье нет ни слова про то что currentState нужно обьявить как глобальную переменную...
Quote: Сейчас что-нибудь добавлю, чтобы было более очевидно.
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;
}
}
}
Ну, вас не поймёшь: то мгновенная передача, то завершение текущих циклов.
Если хотите мгновенную, то можно в прервании перейти куда-нибудь в точку до вызова main и запустить его снова или вообще несколько main-ов сделать и запускать нужный.
Или RTOS попробуйте - будет переключаться за 400-500 тактов.
Впервые слышу, что в исполнении main'а можно выходить за его же пределы, в данном случае, используя Си-код. И вообще, разве переход куда-либо в прерывании без возврата не забивает стек?
Насчет RTOS, думаю, что ее использование будет слишком жирненько для Atmega32.
Если хочется, то можно извратится: с помощью манипуляций с SP можно что-нибудь придумать. Правда надо ли такое?
Quoting Spectr:
Стек можно откорректировать.
Quoting Spectr:
На 3-5 задач 1кБ ОЗУ хватит. Если оставшегося килобайта для программы достаточно, то можно и RTOS применить.
На асме что ли?
Можно пример?
Не обязательно на асме: у atmega32 все нужные регистры доступны. Но стеком можно и не заморачиваться - проще так:
Code:
void isr_handler()
{
if (NeedRestart())
asm("JMP 0x0000");
}
Но вряд ли нужно так программу делать...
RSS feed for comments to this post