Планировщик
02/11/2011 - 14:05
Pavel Bobkov
Стандартный путь построения программ для микроконтроллеров основывается на применении так называемого суперлупа (superloop). Он представляет собой бесконечный цикл, в теле которого запускаются различные функции. Функции могут запускаться постоянно или в случае выполнения каких-то условий, например установки флагов.
Программы, построенные на таком принципе, обычно используются для простых приложений с небольшим количеством задач, и в которых нет требований к таймингам.
Другой способ организации микроконтроллерных программ основан на применении планировщиков. Такие программы лучше структурированы, их проще модифицировать и они позволяют задавать время запуска задач.
В этой статье мы рассмотрим один из вариантов реализации простого планировщика.
Планировщики – это одни из основных компонентов операционных систем, так как они распределяет процессорное время между задачами (процессами), создавая иллюзию параллельной работы. Планировщики подразделяются на кооперативные и вытесняющие.
В случае с кооперативными планировщиками, одиночная задача выполняется от начала и до конца. Поэтому важно, чтобы даже самая длинная задача укладывалась во временной интервал системного таймера. Такие планировщики простые и понятные.
В случае с вытесняющими планировщиками, задачам отводится определенный квант времени. По завершению выделенного времени планировщик прерывает выполнение задачи, сохраняет ее контекст и запускает на выполнение другую задачу. Спустя какое-то время, первоначальная задача будет снова запущена на выполнение с того места, на котором она была остановлена. Реализация такого планировщика более трудоемка.
Идея с кооперативным планировщиком заключается в следующем. У нас есть постоянно работающий таймер, который при переполнении перезагружается. Таймер формирует отрезки времени, используемые для синхронизации задач.
Для каждой задачи мы определяем две основные вещи – задержку перед самым первым выполнением задачи и период ее выполнения. Во время каждого прерывания таймера, запускается планировщик, который уменьшает значения счетчиков задач и помечает те из них, которые готовы к выполнению.
В главном цикле программы функция, именуемая диспетчером, выполняет помеченные задачи.
Задачи
Давайте начнем с рассмотрения задачи. Посмотрите на следующее описание.
#include <ioavr.h>
#include <intrinsics.h>
// Типы данных //
typedef unsigned char u8;
typedef unsigned int u16;
typedef struct task
{
// указатель на функцию
void (*pfunc) (void);
// задержка перед первым запуском задачи
u16 delay;
// период запуска задачи
u16 period;
// флаг готовности задачи к запуску
u8 run;
}task;
/// Определения ///////////
// Константа для таймера Т0
// 25 мс при тактовой частоте
// 8 МГц и предделителе 1024
#define StartFrom 0x3D
// максимальное количество задач
#define MAXnTASKS 8
/// Массив задач ///////////
volatile task TaskArray[MAXnTASKS];
Мы определяем новый тип данных, именуемый task. Это структура, которая содержит в себе основные параметры задачи: указатель на функцию, временные параметры и флаг ее готовности.
Каждая задача выполняет свою функцию и чтобы это обобщить, мы используем такую вещь, как указатель - void (*pfunc) (void). Как видно из описания, это указатель на функцию, которая не принимает аргументы и ничего не возвращает. Следует оставить функции задач именно в таком виде. Если у вас есть потребность в коммуникации задач между собой, нужно обеспечить ее с помощью глобальных переменных.
Задержка до первоначального выполнения задачи и интервал между последующими запусками (переменные dalay и period)задаются в виде количества «тиков» системного таймера, в роли которого у нас выступает 8-ми разрядный таймер Т0.
Далее по коду мы задаем константу, определяющую период переполнения системного таймера, максимальное количество задач и объявляем массив для их хранения.
Частота работы системного таймера должна быть подобрана таким образом, чтобы даже самая продолжительная задача успевала завершиться в пределах одного цикла. В данном случае я выбрал 25-ти миллисекундный интервал, но он может быть и меньше.
Перейдем к функции инициализации планировщика.
void InitScheduler (void)
{
u8 i;
// устанавливаем прескалер - 1024
TCCR0 |= (1<<CS02)|(1<<CS00);
// очищаем флаг прерывания таймера Т0
TIFR = 1<<TOV0;
// разрешаем прерывание по переполнению
TIMSK |= 1<<TOIE0;
// загружаем начальное зн. в счетный регистр
TCNT0 = StartFrom;
// очищаем массив задач
for (i=0; i<MAXnTASKS; i++) DeleteTask(i);
}
void DeleteTask (u8 j)
{
TaskArray[j].pfunc = 0x0000;
TaskArray[j].delay = 0;
TaskArray[j].period = 0;
TaskArray[j].run = 0;
}
Здесь мы просто конфигурируем таймер Т0 и очищаем массив задач с помощью функции DeleteTask().
Для добавления задач используется функция AddTask(). Она определяет доступную позицию в массиве задач и помещает в нее новую задачу.
void AddTask (void (*taskfunc)(void), u16 taskdelay, u16 taskperiod)
{
u8 n=0;
// поиск следующей доступной позиции в массиве задач
while ((TaskArray[n].pfunc != 0) && (n < MAXnTASKS)) n++;
// размещение задачи
if (n < MAXnTASKS)
{
TaskArray[n].pfunc = taskfunc;
TaskArray[n].delay = taskdelay;
TaskArray[n].period = taskperiod;
TaskArray[n].run = 0;
}
}
Например, если вы добавите следующие задачи в массив задач (AddTask (TestA, 0, 3), AddTask (TestB, 1, 4), AddTask (TestC, 4, 0)), то получите изображенный на рисунке режим работы.
Прерывание системного таймера и диспетчер
Самое время обсудить обработчик прерывания таймера и что в нем должно происходить.
#pragma vector=TIMER0_OVF_vect
__interrupt void Timer0ovf(void)
{
u8 m;
//перезагрузка таймера Т0
TCNT0 = StartFrom;
for (m=0; m<MAXnTASKS; m++)
{
if (TaskArray[m].pfunc)
{
//если подошло время запуска задачи
if (TaskArray[m].delay == 0)
{
//устанавливаем флаг и перезаписываем счетчик
TaskArray[m].run = 1;
TaskArray[m].delay = TaskArray[m].period;
}
else TaskArray[m].delay--;
}
}
}
Что ж, это элементарно. Мы просто уменьшаем задержку для каждой задачи и помечаем задачи, готовые к запуску. Заметьте, что обработчик прерывания не вызывает никакие задачи, потому что должен работать быстро. Задачи запускаются из главного цикла программы с помощью специальной функции, называемой диспетчером.
Как видно из кода, переменная, в которой хранилась задержка первоначального запуска задачи, используется в качестве счетчика времени. После ее обнуления, она инициализируется значением переменной period.
Последняя функция, завершающая функционал нашего планировщика, это диспетчер. Она вызывается из основного цикла программы
void DispatchTask (void)
{
u8 k;
for (k=0; k<MAXnTASKS; k++)
{
if (TaskArray[k].run == 1)
{
// запуск задачи
(*TaskArray[k].pfunc)();
// очистка флага задачи
TaskArray[k].run = 0;
}
}
}
Тестовый проект
Всесторонние испытания и доработку планировщика я оставляю на ваше усмотрение.
В качестве примера использования описанного планировщика, я предлагаю тестовый проект с тремя задачами. Задачи ничего полезного не делают, только выводят в последовательный порт определенные цифры.
#include <ioavr.h>
#include <intrinsics.h>
#define sei() __enable_interrupt()
/// Типы данных //
typedef unsigned char u8;
typedef unsigned int u16;
typedef struct task
{
// указатель на функцию
void (*pfunc) (void);
// задержка перед первым запуском задачи
u16 delay;
// период запуска задачи
u16 period;
// флаг готовности задачи к запуску
u8 run;
}task;
/// Определения ///////////
// Константа для таймера Т0
// 25 мс при тактовой частоте
// 8 МГц и предделителе 1024
#define StartFrom 0x3D
// максимальное количество задач
#define MAXnTASKS 8
//Константа для UART`a
//скорость обмена 9600 при частоте 8 МГц
#define UBRRvalue 0x0033
/// Массив задач ///////////
volatile task TaskArray[MAXnTASKS];
/// Прототипы функций ////////
void InitUART (u16 baud);
void TransmitByte (u8 data);
void InitScheduler (void);
void UpdateScheduler(void);
void DeleteTask (u8 index);
void AddTask (void (*taskfunc)(void), u16 taskdelay, u16 taskperiod);
void DispatchTask (void);
void TestA (void);
void TestB (void);
void TestC (void);
/// Main //////////////
int main(void)
{
// Инициализация
InitUART (UBRRvalue);
InitScheduler();
// Добавление задач
AddTask (TestA, 0, 3);
AddTask (TestB, 1, 4);
AddTask (TestC, 4, 0);
sei();
while (1)
{
DispatchTask();
}
}
void InitUART (u16 baud)
{
UBRRH = (u8)(baud>>8);
UBRRL = (u8)baud;
UCSRB = (1<<RXEN)|(1<<TXEN);
UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);
}
void TransmitByte (u8 data)
{
while ( !( UCSRA & (1<<UDRE)) );
UDR = data;
}
void InitScheduler (void)
{
u8 i;
// устанавливаем прескалер - 1024
TCCR0 |= (1<<CS02)|(1<<CS00);
// очищаем флаг прерывания таймера Т0
TIFR = 1<<TOV0;
// разрешаем прерывание по переполнению
TIMSK |= 1<<TOIE0;
// загружаем начальное зн. в счетный регистр
TCNT0 = StartFrom;
// очищаем массив задач
for (i=0; i<MAXnTASKS; i++) DeleteTask(i);
}
void DeleteTask (u8 j)
{
TaskArray[j].pfunc = 0x0000;
TaskArray[j].delay = 0;
TaskArray[j].period = 0;
TaskArray[j].run = 0;
}
void AddTask (void (*taskfunc)(void), u16 taskdelay, u16 taskperiod)
{
u8 n=0;
// поиск следующей доступной позиции в массиве задач
while ((TaskArray[n].pfunc != 0) && (n < MAXnTASKS)) n++;
// размещение задачи
if (n < MAXnTASKS)
{
TaskArray[n].pfunc = taskfunc;
TaskArray[n].delay = taskdelay;
TaskArray[n].period = taskperiod;
TaskArray[n].run = 0;
}
}
#pragma vector=TIMER0_OVF_vect
__interrupt void Timer0ovf(void)
{
u8 m;
TransmitByte('-');
TCNT0 = StartFrom;
for (m=0; m<MAXnTASKS; m++)
{
if (TaskArray[m].pfunc)
{
if (TaskArray[m].delay == 0)
{
TaskArray[m].run = 1;
TaskArray[m].delay = TaskArray[m].period;
}
else TaskArray[m].delay--;
}
}
}
void DispatchTask (void)
{
u8 k;
for (k=0; k<MAXnTASKS; k++)
{
if (TaskArray[k].run == 1)
{
// запуск задачи
(*TaskArray[k].pfunc)();
// очистка флага задачи
TaskArray[k].run = 0;
}
}
}
void TestA (void)
{
TransmitByte('1');
}
void TestB (void)
{
TransmitByte('2');
}
void TestC (void)
{
TransmitByte('3');
}
Если вы подключите вашу любимую терминальную программу и получите следующий ряд… -1 -2 - - -13 -3 -23 -3 -13 -3 -3 -23 -13 -3 -3 -3 -123 -3 -3 -3 -13 -23 -3 -3 -13 -3 -23 -3 -13 -3 -3 -23 -13 -3 -3 -3 -123 ... это значит, что планировщик работает.
Напоследок скажу, не желательно использовать с этой схемой прерывания. Единственное прерывание, которое должно здесь быть – это прерывание переполнения таймера. Если вы задействуете другие прерывания, задачи не смогут запускаться в точно заданное время!
Файлы
Планировщик. Проект для IARAVRПланировщик. Проект для WINAVR
Планировщик. Проект для CodeVisionAVR
По материалам сайта avrtutor.com, который, судя по всему, прекратил функционирование. Код полностью взят оттуда. Текст частично мой, частично переводной. Pashgan.
Comments
Частота работы системного таймера должна быть подобрана таким образом, чтобы даже самая продолжительная задача успевала завершиться в пределах одного цикла.
А на самом деле вижу что:
Частота работы системного таймера должна быть подобрана таким образом, чтобы ОДИН ПРОХОД ВСЕХ(!) задач успевал завершиться в пределах одного цикла.
Так ли это?
Это может происходить, когда прерывание произойдет во время цикла в DispatchTask, когда индекс k уже не равен 0. Прерывание выставит run=1 для всех(как в последнем звене Вашей цепочки -123...) - и диспетчер запустит k-тую задачу.
Скоро будет проект по работе где возможно использую данный принцип или пойду по протоптанному пути - событийной системы, которая тоже очень нравится как работает.Недавн о делал макетик с проигрыванием Wav+SD как раз по принципу событийной системы читал/воспроизв одил аудио данные из кольцевого буфера, обработку кнопок, индикацию. все очень аккуратно пишется и работает стабильно. Кстате было бы очень интересно увидеть от Вас статью про SD карточки + FAT. Думаю ребята меня поддержут, а мы Вас ;)
Как я смог понять "жёсткого" реалтайма на такой системе не получить.
нашёл по теме:
"Простой анализ показывает, что кооперативные многозадачные системы пригодны только для учебных проектов или тех ситуаций, когда программисту на скорую руку необходимо сотворить многозадачное ядро. Вторая ситуация кажется несколько странной — зачем для серьезной работы может потребоваться быстро сделанное ядро, если существует много готовых систем реального времени"
Вопрос: чем вообще обеспечивается условие реалтайма "успел/неуспел" , кроме вычислительной мощности.
Где можно почитать про вытесняющую многозадачность и построение программы с помощью вытесняющего планировщика?
Вот только включать и выключать задачи в прерывании - это действительно слегка игрушечный подход. Возникает конфликт когда задачу запускает/остан авливает другая задача и прерывание. Лучше в прерывании только установить флаг события который в диспетчере уже обработать(к примеру перенести for из прерывания в диспетчер перед стоящем там for). Тогда все задачи и их переключение происходит на одном уровне не пересекаясь на служебных переменных.
RSS feed for comments to this post