четверг, 22 октября 2009 г.

Менеджер очереди для микроконтроллеров

Возникло желание упростить себе написание программ для микроконтроллеров, в частности atmega8. Прежде чем заняться изобретением собственного велосипеда, я нашел и осмотрел несколько уже представленных в интернет реализаций менеджера очереди. Как обычно, все оказалось довольно печально - авторы усиленно втискивают менеджер очереди в прерывания. "И втискивал, и всовывал, и плотно утрамбовывал." (с). Самые продвинутые даже понимают, какие проблемы они себе этим создают и начинают с ними бороться путем создания мьютексов и прочих костылей. Ну, ладно, пусть их себе борются, мне-то что. Понятно, что в итоге было решено потратить часок времени на свою собственную реализацию, которая не будет отличаться неестественным интеллектом.

Рассмотрим свои потребности.

Динамическое выделение памяти менеджером очереди совершенно не оправдано, поскольку существующие процедуры известны на момент компиляции. Вполне хватит инициализированного при объявлении массива. Очередность запуска процедур пусть определяется их порядком в массиве или временем запуска, если оно указано.

При запуске контроллера нужно разово и в определенном порядке выполнить набор процедур, желательно с фиксированными задержками между ними. Отработала одна процедура, сколько требуется подождали, выполнили следующую. Все просто.

Во время работы задачи периодически повторяются. Их можно разделить на два класса - повторяемых с точно заданным интервалом и повторяемые в нужной последовательности. Как пример, отображение данных на дисплее требует точного соблюдения интервалов между запусками, что вполне себе разумно делать в таймере. Опрос клавиатуры особенной точности не требует, можно в таймере, а можно и без него. Если есть лишний таймер, пусть потрудится. А вот остальное, как-то опрос сенсоров, управление исполнительными устройствами и проч. особой точности зачастую не требуют, зато требуют фиксированного порядка запуска. Скажем, получение температуры с далласовского датчика DS18B20 это миллисекунда-другая на отправку команды датчику, ожидание 750 мс (для точности 12 бит) и снова примерно миллисекунда на получение результата. После этого надо проверить состояние исполнительного устройства. Но торопиться сильно некуда, достаточно сделать это за те же пару миллисекунд.

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

Таким образом, нам достаточно вот такой реализации c менеджером очереди в главном цикле:
typedef unsigned char (*fptr)();
typedef struct {
 fptr func;
 unsigned char state;
 unsigned int delay;
} queue_type;

// очередь задач
queue_type g_queue[]={
 // функции инициализации, выполняются единожды при запуске
 {init_usart,0,0},
 {init_timers,0,0},
 {init_ports,0,0},
 {init_interrupts,0,0},
 {selftest,0,500},
 {beeper,0,0},
 // рабочие функции, выполняются циклически
 {sensor_conv,1,0},
 {sensor_get,1,750},
 {out,1,0}
};

main()
{
 unsigned char i,queue_size;
 int queue_delay=0, iqueue;

 // размер всей очереди
 queue_size = sizeof(g_queue)/sizeof(queue_type);

 // разовые задания, выполняются единожды при запуске с указанной задержкой после каждого задания
 for (i=0;i<queue_size;i++) {
  if (0 == g_queue[i].state) {
   g_queue[i].func();
   delay_ms(g_queue[i].delay);
  }
 }

 // время выполнения всей очереди, исключая разовые и периодические задания
 for (i=0;i<queue_size;i++) {
  if (1 == g_queue[i].state) queue_delay+=g_queue[i].delay;
 }
 // регулярные задания, выполняются в указанный момент времени или периодически кратно заданному интервалу
 while(1) {
  for(iqueue=0;iqueue <= queue_delay; iqueue++) {
  for (i=0;i<queue_size;i++) {
   if ((1==g_queue[i].state && g_queue[i].delay == iqueue) || 
    (2==g_queue[i].state && iqueue % g_queue[i].delay == 0)) {
    g_queue[i].func();
   }
  }
  delay_ms(1);
  }
 }
}


Вот, в принципе, и все. Теперь мы видим все действия и их порядок, за исключением спрятанных в таймерах, которых немного и короткие они, поскольку в таймерах критично время выполнения кода. state=0 указывает разовые процедуры инициализации, state=1 - выполняемые один раз в каждом рабочем цикле, state=2 - выполняемые периодически в каждом рабочем цикле. Можно предусмотреть временную блокировку задач, используя разные значения state. Но у меня есть сомнения, что это удобнее, чем использование глобальных флагов. А уж по эффективности так и точно хуже.

Представленный выше код можно упростить или усложнить на ваш вкус. Поставленные задачи он решает, что от него, собственно, и требовалось.

2 комментария:

Анонимный комментирует...

кое-где в циклах for потерян знак меньше из-за неправильной разметки.

Печников Алексей комментирует...

Спасибо, поправил.


(C) Alexey Pechnikov aka MBG, mobigroup.ru