Print this page

Планировщик

02/11/2011 - 14:05
   
Стандартный путь построения программ для микроконтроллеров основывается на применении так называемого суперлупа (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.

Related items