Диспетчер, снова диспетчер

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

В интернете есть масса информации по этой тематике, я же особо тщательно рассматривал следующие варианты:
easyelectronics.ru/avr-uchebnyj-kurs-arxitektura-programm-chast-2.html
chipenable.ru/index.php/programming-avr/item/110-planirovschik.html
chipenable.ru/index.php/programming-avr/item/73-organizatsiya-programm-sobytiynaya-sistema-na-tablitse.html

Третий вариант быстро отсеялся из-за невозможности формирования временных задержек. Хотя я на нем сделал когда-то логический анализатор на Меге и экране 128х64. Параметрами не блещет, зато логика программы кристально ясна.

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

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

Начнем, как водится, со структуры задачи. Она осталась без изменений, хотя для экономии памяти можно было избавиться от флага Run.


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


Уже по этой структуре можно заметить первый недостаток планировщика — он ориентирован только на циклические задачи. Нет возможности поставить в очередь задачу для разового выполнения.
Сама очередь выглядит так:

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

Тут должно быть все понятно. Размер очереди необходимо делать такой, чтобы поместились все предполагаемые задачи.
Далее инициализация диспетчера. Настраивается аппаратный таймер (у меня таймер0) на прерывание через 1 мс. Можно больше, главное, чтобы самая долгая подпрограмма успела выполнится за этот промежуток.
И тут начинаются отличия. В исходном варианте очередь очищается путем обнуления параметров задач во всей очереди. Я же ввел дополнительную переменную «хвоста» очереди и просто обнуляю ее.

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

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


Дальше интересней. Постановщик задачи в очередь, с заданными периодом и паузой до первого выполнения.В исходном варианте идет простой перебор очереди и, если есть свободное место, добавляется задача. То есть возможно дублирование задачи вместо обновления ее параметров. А это крайне неудобно. К тому же перебор очереди занимает лишнее время. В моем варианте сначала идет поиск добавляемой функции в очереди (только до «хвоста» естесственно). Если такая функция уже есть — обновляются ее параметры: пауза до первого запуска и период выполнения. Если таковой нет в списке и есть место — задача добавляется в «хвост» очереди. При этом нет никаких дополнительных переборов.

void RTOS_SetTask (void (*taskFunc)(void), u16 taskDelay, u16 taskPeriod)
{
   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;
   }
}

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

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. Если выставлен — выполняем задачу. А дальше пошли отличия от оригинала, в котором параметр задачи Delay (задержка перед запуском) приравнивается к значению Period, снимается флаг запуска, и на этом все. Мне же хотелось реализовать однократный вызов, поэтому если заданный период равен 0, то задачу я удаляю из списка. Если период не нулевой, обнуляю флаг запуска. И тут важный момент: я проверяю значение поля Delay. При нуле я приравниваю его к периоду, но если там не 0, значит задача сама изменила свои параметры, вызвав SetTask на саму себя. Для чего это было нужно, я покажу чуть дальше.

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)();                               // выполняем задачу
      }
   }
}

Таймерная служба. Обработчик прерывания аппаратного таймера. Очень простой, идет перебор всей очереди (до «хвоста») и у каждой задачи уменьшается параметр Delay. Если он уже в нуле, выставляется флаг запуска задачи. Все просто.

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. Вы отображаете на дисплее текущую температуру. В какой то момент решили повысить заданную. При этом хотите, чтобы значение последней отображалось 1 секунду, после чего снова началось отображение текущей температуры. У меня это реализовано двумя строчками.

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

FEN_TempUp()
{
...
RTOS_SetTask(FEN_ViewCurrentTemp, 1000, 300); // пауза до вывода текущей температуры 1000 мс
RTOS_SetTask(FEN_ViewTargetTemp,     0,   0); // отображение заданной температуры (1 раз)
...
}


Ситуация 2. Так уж получилось, что в корпусе от моей станции всего 5 кнопок. При этом надо управлять и паяльником и феном. Тут без определения коротких и длинных нажатий не обойтись. Реализовать это можно по-разному, мой вариант может и кривой, но вполне себе работоспособен. Аппаратных таймеров свободных нет и все кнопки на одном порту. Комментарии в коде.

// функция сканирования кнопок. Вызывается раз в 10 мс.
// счетчики - это глобальные статические переменные
void BUT_Scan()   
{
             //номер кнопки  |   счетчик      | функции при кратком и длинном нажатиях
   TestButton (BUT_FEN_SET,   &fenSetPressed,   FEN_SetMode,   FEN_Off);
   TestButton (BUT_FEN_DOWN,  &fenDownPressed,  FEN_TempDown,  NULL);
   TestButton (BUT_FEN_UP,    &fenUpPressed,    FEN_TempUp,    FEN_SaveTemp);
   TestButton (BUT_SOLD_DOWN, &soldDownPressed, SOLD_TempDown, SOLD_Off);
   TestButton (BUT_SOLD_UP,   &soldUpPressed,   SOLD_TempUp,   SOLD_SaveTemp);
}

void TestButton(u08 butDef, volatile u08 *pressCount, void (*taskShortPress)(void), void (*taskLongPress)(void))
{ 
   if(!IsBit(BUT_PIN, butDef))
   {
      *pressCount = *pressCount+1;
      //отслеживание длинного нажатия (BUT_FACTOR = 5, защита от дребезга)
      if(*pressCount > (BUT_FACTOR * 10))
      {
         BEEP;

         RTOS_SetTask(taskLongPress, 0, 0);     // вызов задачи по длинному нажатию
         RTOS_SetTask(BUT_Scan, 2000, 10);	// 2 секунды не сканируем кнопки, 
                                                // иначе после длинного сработает и короткое нажание.
         *pressCount = 0;
      }
   }
   else
   {
      //отслеживание короткого нажатия. Функция выполняется по ОТжатию кнопки. 
      if(*pressCount > BUT_FACTOR)
      {
         RTOS_SetTask(taskShortPress, 0, 0);    // вызов задачи по короткому нажатию
         BEEP;
      }

      *pressCount = 0;
   }
}

Таким образом функция сама себе запрещает выполняться 2 секунды. Думал будет неудобно, что при коротком нажатии соответствующая функция вызывается по отжатию, но благодаря пищалке все вполне нормально.
Upd: Реализация действительно оказалась кривой. В комментариях приведена исправленная версия. Спасибо всем, кто навел ход мыслей в правильную сторону, в особенности nobody . И все же возможность периодической функции поставить саму себя на паузу решил оставить. На всякий случай.

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

Upd
З.Ы. Народ, кто с Омска, предлагаю скооперироваться на покупку миговских контейнеров для хранения радиодеталей www.mig-rnd.ru/. А то цена доставки кусается.
Больше не актуально. Вот они.
  • 0
  • 15 апреля 2014, 21:01
  • sva_omsk
  • 1
Файлы в топике: rtos.zip

Комментарии (28)

RSS свернуть / развернуть
Не совсем понятен алгоритм антидребезга функции TestButton. допустим выполняется длинное нажатие. Но после того как
*pressCount > BUT_FACTOR

появится помеха — кнопка будет воспринята как отжатая. Т.е. гистерезиса нет?
0
Фильтрация за счёт редкого опроса — 100 Гц. Помеха вряд ли попадёт в момент опроса, а если и попадёт, значит был повод (дрыгали кнопкой). Я примерно так же делаю.
0
Как мне кажется, планировщики эти жрут много ресурса. Я остановился на протопотоках.
0
Я 2 раза пробовал разобраться с этими протопотоками, безуспешно. Ну не могу я понять их прелести, хоть убейте. По-моему, программа еще более нечитабельна с ними.
0
Насколько я помню диспетчер ДиХальта, структура у него несколько лучше, на мой взгляд. По крайней мере я на нем и базировался.
0
  • avatar
  • Vga
  • 16 апреля 2014, 07:23
Таким образом функция сама себе запрещает выполняться 2 секунды
А почему не реализовать вызов функции при длинном нажатии тоже по отпусканию? Например так:

void TestButton(u08 butDef, volatile u08 *pressCount, void (*taskShortPress)(void), void (*taskLongPress)(void))
{ 
    if(!IsBit(BUT_PIN, butDef))
    {
        *pressCount = *pressCount+1;
        //отслеживание длинного нажатия, сообщаем пользователю, что можно отпустить
        if(*pressCount > (BUT_FACTOR * 10)) BEEP;
    }
    else
    {
        if(*pressCount > (BUT_FACTOR * 10))
        {//отслеживание длиного нажатия. Функция выполняется по ОТжатию кнопки. 
            RTOS_SetTask(taskLongPress, 0, 0);     // вызов задачи по длинному нажатию
            BEEP; // На длинное нажатие 2 раза пикнем.
        }
        else if(*pressCount > BUT_FACTOR)
        {//отслеживание короткого нажатия. Функция выполняется по ОТжатию кнопки.
            RTOS_SetTask(taskShortPress, 0, 0);    // вызов задачи по короткому нажатию
            BEEP;
        }
        *pressCount = 0;
    }
}
0
Уже по этой структуре можно заметить первый недостаток планировщика — он ориентирован только на циклические задачи. Нет возможности поставить в очередь задачу для разового выполнения.

Так ить можно ж после первого запуска самоудалиться?=D

Про кнопки — блокировать остальные кнопки из-за длинного нажатия одной? =)
Я делал по отпусканию — если насчитали время нажатия больше порога — длинное.
+1
В том то и дело, что в исходном варианте задачи не могут самоудалиться. Это было реализовано в этом диспетчере в первую очередь
0
Запуск по времени нажатия субъективно приятнее, чем запуск после отпускания. Но блокировка кнопок не нравится. А если я захочу короткое нажатие сразу же после длинного сделать?

Можно же просто не обнулять pressCount, а продолжать считать.
если нажата:
pressCount+1
если pressCount = (BUT_FACTOR * 10), то обрабатываем длинное нажатие

если не нажата:
если (pressCount > BUT_FACTOR) и (pressCount < (BUT_FACTOR*10)) то обрабатываем короткое нажатие
pressCount = 0
Добавить защиту от переполнения счетчика, если нужна.
0
По поводу логики добавления задачи в очередь по таймеру. Текущая реализация не умеет дублировать задачи в очереди и это ограничение функционала представляется как преимущество. Мне не нравится это решение.
Если мы умеем удалять задачи из очереди, то обновление параметров задачи можно выполнить через ее удаление и постановку заново. А при обычной постановке — дублировать.
0
Чтобы была возможность дублировать, достаточно просто удалить первую половину функции RTOS_SetTask. Просто я не смог придумать ни одной ситации, где бы это пригодилось. Зато повсюду придется писать 2 строчки вместо одной.
0
Мне нравится вот этот планировщик. Примитивный, не требовательный, быстрый.
Конечно, фарша в нем нет никакого, но все-равно код намного структурированнее, чем с суперлупом.
0
Переделал функцию проверки нажатия кнопки. Я и сам понимал, что моя реализация была довольно кривой, но на момент написания ничего другого в голову не пришло.

void TestButton(u08 butDef, volatile u08 *pressCount, void (*taskShortPress)(void), void (*taskLongPress)(void))
{
   if(!IsBit(BUT_PIN, butDef))
   {
      *pressCount = *pressCount+1;
      
      if(*pressCount ==  BUT_FACTOR) 
      {
         BEEP;
      }
      else if(*pressCount == (BUT_FACTOR * 10))
      {
         BEEP;
         BEEP;
         BEEP;
      }
   }
   else
   {      
      if(*pressCount > (BUT_FACTOR * 10))          
      {
         RTOS_SetTask(taskLongPress, 0, 0);        // вызов функции при длительном нажатии
      }
      else if(*pressCount > BUT_FACTOR)            
      {
         RTOS_SetTask(taskShortPress, 0, 0);       // вызов функции при коротком нажатии
      }
      *pressCount = 0;
   }
}

Теперь вызовы связанных функций происходят по отжатию кнопки. Но BEEP логичней делать в нажатом состоянии, создается иллюзия, что действия выполняются именно по нажатию.
0
void RTOS_DeleteTask (void (*taskFunc)(void))
А нельзя ли просто хвостовую задачу копировать на место удаленного? По-моему так быстрее будет.
0
Ради интереса написал простенькую программу на С: моргание светодиодом в основном цикле. Размер программы 152 байта. 0 байт занятой ОЗУ.
Затем добавил диспетчер, но установил максимальное количество задач в 0. Размер программы составил 542 байта + 2 байта в оперативке.
После этого выставил размер очереди в 1. Оформил инвертирование состояния порта как функцию и запустил ее через диспетчер. Размер программы 718 байт + 9 байт в ОЗУ.
2 функции и размер очереди 2 — 810 байт + 16 байт в ОЗУ.
Оптимизация установлена как Os. В принципе представление о размере диспетчера это дает.

2Mihail. Неплохая мысль, можно попробовать.
0
попробовал

void RTOS_DeleteTask (void (*taskFunc)(void))
{
   u08 i;
    
   for (i=0; i<arrayTail; i++)                        // проходим по списку задач
   {
      if(TaskArray[i].pFunc == taskFunc)              // если задача в списке найдена
      {
         
         DISABLE_INTERRUPT;
         if(i != (arrayTail - 1))                     // переносим последнюю задачу
         {                                            // на место удаляемой
            TaskArray[i].pFunc  = TaskArray[arrayTail - 1].pFunc;
            TaskArray[i].delay  = TaskArray[arrayTail - 1].delay;
            TaskArray[i].period = TaskArray[arrayTail - 1].period;
            TaskArray[i].run    = TaskArray[arrayTail - 1].run;
         }
         arrayTail--;                                 // уменьшаем указатель "хвоста"
         RESTORE_INTERRUPT;
         return;
      }
   }
}

В тестовом проекте со светодиодами работает. В проекте паяльной станции не могу проверить, у меня там некоторые модификации начались.
0
TaskArray[i].pFunc  = TaskArray[arrayTail - 1].pFunc;
TaskArray[i].delay  = TaskArray[arrayTail - 1].delay;
TaskArray[i].period = TaskArray[arrayTail - 1].period;
TaskArray[i].run    = TaskArray[arrayTail - 1].run;

нельзя ли заменить на:
TaskArray[i] = TaskArray[arrayTail - 1];
0
Позор мне! Не додуматься до самого логичного решения! Спасибо
0
это на каком контроллере?
0
В данном случае для AVR, испытывал на Atmega16. Думаю пойдет и на других AVR. В крайнем случае нужно будет подправить инициализацию под конкретный таймер, надо ДШ смотреть
0
в standalone паке ASF есть пример использования таймера timeout, этакая заготовка для диспетчера. В нем есть готовая настройка таймера для работы. Написана в основном в препроцессоре, знает какого поколения avr используется, правильные регистры настраивает, предделитель считает. Можно подсмотреть.
0
да как то не актуальны AVR уже. У меня вот последние 2 камушка остались. Уже стмки закупил. Буду на них диспетчер переводить. Как раз вроде у Mihail'a (может ошибаюсь) видел в какой то статье настройки таймеров на препроцессоре.
0
Обнаружился неприятный глюк. Если выполняемая функция находится в хвосте очереди и в ней происходит удаление другой функции, то по алгоритму RTOS_DeleteTask, индекс выполняемой изменится. Это может вызвать определенную ошибку: если выполняемая функция вызывает саму себя длдя обновления параметров. Ошибка редкая, но я на нее попал. К счастью.
Пофиксил. Статью слегка обновил.
0
главное, чтобы самая долгая подпрограмма успела выполнится за этот промежуток
… только самая долгая, или (худший сценарий) несколько/все задачи за некоторый период?
… что будет если самая долгая подпрограмма не успеет завершиться за х период?
0
Сам долго думал над этим вопросом. В первоисточнике сказано про самую долгую подпрограмму. Хотя на первый взгляд кажется, что за системный тик должен выполниться сценарий (пусть так называется). Собственно так я и делаю. Пока конфликтов не наблюдалось.
Чтоб не быть голословным, вечером проверю.
0
Проверил на светодиодах. Было несколько задач с разным временем их свечения, в том числе и больше системного тика. Время задавал delay'ями. Период у всех одинаков и равен одному тику.
Если кратко, то неуспевший выполниться за системный тик сценарий сдвигает время выполнения следующего. Никаких других ошибок и сбоев не было.
0
Успешно прикрутил к STM8L.
Кому интересно — пишите.
0
А что там прикручивать? Таймер настроить? ))))))
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.