Print this page

Планировщик для микроконтроллера

14/06/2014 - 10:00 Владимир Шибанов

Введение

Вопреки расхожему мнению, применение планировщиков/диспетчеров позволяет значительно ускорить разработку приложений, затратив при этом совсем немного памяти. А, как известно, время разработчика дороже последней. Средний диспетчер занимает около 1 кБ flash. Это совсем немного, учитывая те возможности, которые он предоставляет.

Сделать свои программы более организованными мне, простому любителю, хотелось достаточно давно. Я опробовал на реальных устройствах событийную систему на таблице, диспетчер ДиХальта и планировщик. Последний меньше всего мне понравился. При использовании его "как есть" он, на мой взгляд, мало пригоден для создания приложений посложнее мигания светодиодами. Но у него есть один плюс, который меня покорил. Он очень, очень простой. А значит быстрый и надежный. Поэтому его я и взял за основу.

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

1) Возможность однократного запуска задачи;
2) Возможность циклического запуска задачи с заданным периодом;
3) Возможность однократного или циклического отложенного запуска с заданной паузой;
4) Удаление задачи.

Перечисленного вполне достаточно для реализации самых разнообразных алгоритмов. При этом, как видим, ничего лишнего нет.

Доработка планировщика

Старый планировщик умеет запускать циклические задачи отложенно (или сразу). Это уже немало. Но чтобы реализовать весь желаемый функционал, пришлось изменить практически все его функции. Теперь по порядку.

Структура задачи и очередь задач остались неизменными:


typedef struct task
{
   void (*pFunc) (void); // указатель на функцию
   u16 delay; // задержка перед первым запуском задачи
   u16 period; // период запуска задачи
   u08 run; // флаг готовности задачи к запуску
}task;

volatile static task TaskArray[MAX_TASKS]; // очередь задач


Начиная с инициализации начинаются отличия. В исходном варианте очередь очищается путем обнуления параметров задач во всей очереди. Я же ввел дополнительную переменную «хвоста» очереди и просто обнуляю ее.


inline void RTOS_Init()
{
   TCCR0 |= (1<<CS01)|(1<<CS00); // прескалер - 64
   TIFR = (1<<TOV0); // очищаем флаг прерывания таймера Т0
   TIMSK |= (1<<TOIE0); // разрешаем прерывание по переполнению
   TIMER_COUNTER = 130; // загружаем начальное зн. в счетный регистр

   arrayTail = 0; // "хвост" в 0
}


Постановщик в очередь. В старом планировщике это функция AddTask. При наличии свободного места в очереди она добавляет задачу, даже если такая уже есть в списке. Мне показалось это неудобным, поэтому задача добавляется только в том случае, если ее в списке нет. В противном случае обновляются ее параметры: пауза до выполнения и период. Поэтому функция теперь называется SetTask. Это логичней.


u08 i;

if(!taskFunc) return;

for(i = 0; i < arrayTail; i++) // поиск задачи в текущем списке
{
   if(TaskArray[i].pFunc == taskFunc) // если нашли, то обновляем переменные
   {
      DISABLE_INTERRUPT;

      TaskArray[i].delay = taskDelay;
      TaskArray[i].period = taskPeriod;
      TaskArray[i].run = 0;

      RESTORE_INTERRUPT;
      return; // обновив, выходим
   }
}

if (arrayTail < MAX_TASKS) // если такой задачи в списке нет
{ // и есть место,то добавляем
   DISABLE_INTERRUPT;

   TaskArray[arrayTail].pFunc = taskFunc;
   TaskArray[arrayTail].delay = taskDelay;
   TaskArray[arrayTail].period = taskPeriod;
   TaskArray[arrayTail].run = 0;

   arrayTail++; // увеличиваем "хвост"
   RESTORE_INTERRUPT;
}
}


Следующий шаг - удаление задачи. В старом планировщике такая функция есть, но по индексу в очереди. На практике неудобно, поэтому она была изменена. Теперь задачи удаляются по имени, причем удаление происходит не смещением всех последующих задач на шаг назад, а переносом последней задачи на место удаляемой. Это гораздо быстрее.


void RTOS_DeleteTask (void (*taskFunc)(void))
{
   u08 i,j;

   for (i=0; i<arrayTail; i++) // проходим по списку задач
   {
      if(TaskArray[i].pFunc == taskFunc) // если задача в списке найдена
      {

         DISABLE_INTERRUPT;
         if(i != (arrayTail - 1)) // переносим последнюю задачу
         { // на место удаляемой
            TaskArray[i] = TaskArray[arrayTail - 1];
         }
         arrayTail--; // уменьшаем указатель "хвоста"
         RESTORE_INTERRUPT;
         return;
      }
   }
}


Для реализации однократного запуска была изменена функция извлечения задачи из очереди. Если подошло время выполнения (флаг Run установлен), проверяется значение периода, если он равен 0, задача просто удаляется из очереди.


void RTOS_DispatchTask()
{
   u08 i;
   void (*function) (void);
   
   for (i=0; i<arrayTail; i++) // проходим по списку задач
   {
      if (TaskArray[i].run == 1) // если флаг на выполнение взведен,
      { // запоминаем задачу, т.к. во
         function = TaskArray[i].pFunc; // время выполнения может
         // измениться индекс
        if(TaskArray[i].period == 0)
        { // если период равен 0
           RTOS_DeleteTask(TaskArray[i].pFunc); // удаляем задачу из списка,
        }
        else
        {
           TaskArray[i].run = 0; // иначе снимаем флаг запуска
           if(!TaskArray[i].delay) // если задача не изменила задержку
           { // задаем ее
               TaskArray[i].delay = TaskArray[i].period-1;
           } // задача для себя может сделать паузу
        }
       
        (*function)(); // выполняем задачу
   }
}


Таймерная служба осталась почти без изменений, разве что очередь теперь сканируется не полностью, а только до "хвоста".


ISR(RTOS_ISR)
{
   u08 i;

   TIMER_COUNTER = 130; // задаем начальное значение таймера

   for (i=0; i<arrayTail; i++) // проходим по списку задач
   {
      if (TaskArray[i].delay == 0) // если время до выполнения истекло
      TaskArray[i].run = 1; // взводим флаг запуска,
      else TaskArray[i].delay--; // иначе уменьшаем время
   }
}


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

Примеры использования планировщика

1) Запуск сканирования клавиатуры 100 раз в секунду


RTOS_SetTask(KeyScan, 0, 10); // период 10 мс, неотложенный запуск


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


// где-то в программе

// вывод на экран текущей температуры каждые 300 мс
RTOS_SetTask(FEN_ViewCurrentTemp, 0, 300); 
...
...

FEN_TempUp()
{
...
// пауза до вывода текущей температуры 1000 мс
RTOS_SetTask(FEN_ViewCurrentTemp, 1000, 300);

// отображение заданной температуры (1 раз) 
RTOS_SetTask(FEN_ViewTargetTemp, 0, 0); 
...
}


3) Зажигаем светодиод на 5 секунд:


RTOS_SetTask(LED_ON, 0, 0); // зажигаем
RTOS_SetTask(LED_OFF, 5000, 0); // гасим через 5 секунд


4) Смена цвета моргания светодиода при смене режима работы:


// где-то в программе
mode = GREEN; // установили режим
RTOS_SetTask(Blink_green, 0, 500); // задали цвет моргания

// изменение режима
mode = RED;
RTOS_DeleteTask(Blink_green); // выключили зеленый цвет моргания
RTOS_SetTask(Blink_red, 0, 500); // задали красный цвет моргания

Файлы

scheduler.rar 


Автор статьи: Владимир Шибанов. 

Related items