Small Simple Arduino Task Scheduler



Привет. Вот сидишь ты сейчас за компьютером и читаешь этот опус, а там у тебя параллельно музычка играет, торренты всякие качаются, что-то компилируется, картинки смотрятся, может что-то печатается на принтере…. И все это одновременно 🙂 Как же это происходит? Процессор то у нас в компьютере один. А это значит, что одновременно (в конкретный интервал времени) он может выполнять только одну задачу (Знаю, знаю я про существование многоядерных процессоров, но с ними отдельная песня). И, чтобы получить иллюзию «одновременности», все задачи выполняются поочередно небольшими «порциями», а «рулит» всем этим операционная система (ОС). Большинство современных операционных систем (Windows, Linux, OS X, Андроид-ы и т.п.) делают это незаметно для пользователя. При этом, если одна из программ-задач «подвиснет», то все остальные продолжают работать. Обычно в ОС выделяют два подхода к обеспечению многозадачности: вытесняющий и кооперативный. Вытесняющая ОС в состоянии отнять управление у текущей задачи в любой момент времени и передать его другой задаче. Например: появилась готовая к работе более приоритетная задача или текущая задача отработала свой квант времени. Кооперативная ОС — это вариант при котором следующая задача выполняется только после того, как текущая задача явно объявит себя готовой отдать процессорное время другим задачам.

Д-да… наверное тут возникает законный вопрос: «Все это — здорово, но какое это имеет отношение к Arduino?». А вот, имеет самое непосредственное. Вы наверняка замечали, что при написании более-мение серьезного скетча для Ардуино в какой-то момент времени понимаешь, что программа по сути состоит из нескольких сравнительно самостоятельных задач и их нужно просто коммутировать между собой должным образом. Давайте рассмотрим набивший оскомину скетч Blink.

void loop() {
  digitalWrite(13, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);              // wait for a second
  digitalWrite(13, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);              // wait for a second
}

Если «нашинковать» его на мелкие части, то можно выделить три задачи:
1. — Включить светодиод;
2. — «Потупить» 1 секунду;
3. — Выключить светодиод;
4. — Выполнить задачу №2;
и так по кругу.

А если нам нужно, чтобы Ардуинка не просто мигала светодиодом, а еще получала данные с АЦП, опрашивала состояние кнопок, крутила двигатели, передавала данные в СОМ-порт и т.д.? Да еще все это синхронно и с разной периодичностью, разной последовательностью… «Замахаешься» в доску описывать все это «счастье», заводя кучу переменных и временных счетчиков 🙂 Стоп, стоп! Я не говорю, что этого не сделать (более того, ТАК ДЕЛАЮТ!), но есть способы лучше подходящие для таких проектов. Одно из таких решений — использовать операционную систему. Да-да… даже для маленьких микроконтроллеров уже написаны всевозможные ОС-ы, диспетчеры, Task scheduler-ы и т.д. Бери и пользуйся! Но я думаю, что большинству людей (как и мне) ГОРАЗДО приятнее понимать «как что-то устроено и работает» , чем просто пользоваться «черным ящиком». А как известно, лучший способ разобраться с чем-либо — сделать это самому.

Маленькое отступление.
Во-первых. Описанные ниже принципы работы не являются каким-то «НОУ-ХАУ» или моими личными изысканиями («Все украдено до нас» (с) Операция «Ы»). Фактически, я просто объеденил удачные (с моей точки зрения) решения и адаптировал их под платформу Ардуино. А по сему, не пишите пожалуйста в комментариях: «а вот ссылка на похожее решение» «а вот этот кусок кода отсюда взят» и т.п. Тем более, что многие из ссылок я уже видел 🙂
Во-вторых. Я прекрасно понимаю, что называть описанную ниже поделку «операционной системой» — это ОЧЕНЬ громко. Наверное более уместны такие названия: «Task Scheduler», «Task Manager», «Launcher» и т.д. Даже скромняга DiHalt аналогичную систему называет «Диспетчер задач». Но тут как на собеседовании: видел компьютер — программист, нарисовал страничку на HTML-е — Web-дизайнер :). Так что я, с вашего позволения, далее по тексту буду называть сие творение микроОС 🙂


Описание «в крупную клетку». Ознакомившись с уже существующими вариантами реализаций, я выделил для себя ключевые моменты и основные блоки будущей микроОС (MkOS), а так же ряд «упрощений» и «плюшек»:
— это будет кооперативный планировщик (это вариант когда задача выполняется от начала и до конца);
— основные компоненты такой ОС: очередь задач, таймерный сервис и «запускатель» (он же лаунчер);
— все задачи будут бесприоритетные, т.е. равнозначные;
— задачи могут выполняться не сразу, а через некий интервал времени (отложенный запуск);
— предусмотрим возможность циклического автозапуска задач.

Итак. Как это должно работать? Имеется очередь (массив) с задачами [3]. У каждой задачи есть свои параметры. Один из которых — «счетчик тиков перед запуском». Далее ядро, оно же Kernel. Состоит из двух служебных функций: Time Service [1] и Task Launcher [2].

Time Service.
Функция срабатывает при каждом системном ТИКе, т.е. по сути это некий обработчик прерываний. Следовательно, «тяжелые» вычислительные задачи не для него. При каждом своем вызове он должен пробегаться по всем задачам, находящимся в очереди и уменьшать значение параметра «задержка перед запуском». При достижении этим параметром нулевого значения нужно устанавливать признак того, что эта задача готова к запуску. И все. Эта служба должна работать четко и быстро.

Task Launcher.
Этот компонент тоже активно работает с очередью задач. Только он просматривает очередь на предмет наличия задач с установленным флагом запуска. Если такая задача найдена, то в зависимости от ее типа (цикличная или разовая) далее два варианта поведения: для разовой задачи — удаление ее из очереди и запуск; для циклической задачи — обновление параметров (значение параметра отвечающего за периодичность тупо копируется в поле задержки) и тоже запуск.
Вот собственно и все. Согласитесь, что ничего сложного и непонятного тут нет.

Обратили внимание, что очень часто используется понятие «задача» и «многозадачность»? А что же понимается под этими терминами? Google нам радостно сообщает: Задача — некий самостоятельный блок программного кода (последовательность команд) который выполняется в своем окружении. Вроде как все верно. Но тут тоже у каждого человека свое мнение и видение: кто-то считает весь скетч как одну задачу: «мигание светодиодом», кто-то считает, что «включение светодиода» — одна задача, «выключение светодиода» — вторая. А кому-то все это выглядит еще мельче: «присвоить переменной х номер порта» — первая задача, «подготовить порт на выход» — вторая, «записать в порт значение» — третья и т.д. Давайте определимся и решим, что под задачей будем понимать некую логически осмысленную, законченную функцию, выполнение которой будет происходит за относительно короткий промежуток времени. Например, для BLINK-скетча разумно выделить две задачи:
1) Включить светодиод (ledOn())
2) Выключить светодиод (ledOff())
А вот паразитные delay(1000) лишние. Эту функцию будет реализовывать ОС — на то и нужен отложенный запуск, чтобы пока одна задача находится в режиме ожидания, другая могла исполняться.


Теперь рассмотрим компоненты микроОС более детально.
Для описания одной задачи создадим новую структуру. Что будет входить в описание одной задачи?
1) нам нужно знать как эту задачу вызвать. Значит нужен ее адрес. Или указатель;
2) некоторые задачи должны запускаться сразу, а некоторые — спустя какое-то время. Значит, нужно знать это время;
3) для периодичных задач нужно еще знать периодичность ее запуска;
4) ну и собственно признак того, что задачу УЖЕ нужно выполнить. (Этот признак можно совместить с пунктом 2. Но я так не делал).

// структура, описывающая ОДНУ задачу
typedef struct task
{
   void (*pTask) (void);
   unsigned long start_delay;
   unsigned long period;
   byte ready_run;
}task;

Для унификации описания задач давайте определимся, что указатель void (*pfunc) (void) будет указывать на функцию, которая не принимает никакие аргументы и ничего не возвращает. Если у вас есть потребность в коммуникации задач между собой, нужно обеспечить ее с помощью глобальных переменных. Так гораздо проще. А как же различать задачи? В нашем простом варианте реализации мы откажемся от возможности поместить одну задачу в очередь дважды (реализация этой возможности потребовала бы ведение некого ID для задач). А вместо этого, при попытке добавить задачу, которая уже есть в очереди, мы просто обновим ее параметры. Хорошо это или плохо? Однозначно ответить сложно. С одной стороны снижается гибкость системы, с другой стороны — можно четко прогнозировать размер очереди задач. Бац-бац… Ничья.
А, чуть не забыл. Время указывается не в привычных минутах и секундах, а в неких «попугаях» — системных ТИКах.

Структура — это всего лишь описание шаблона-заготовки для одной задачи. А где же будут храниться непосредственно данные о задачах? Следующий, не менее важный компонент будущей ОС- очередь (или массив) задач.

Очередь будет предопределенного размера (пропорционально планируемому количеству задач), который задается при инициализации

#define MAX_TASKS 16
volatile static task TaskQueue[MAX_TASKS];

Параметр MAX_TASKS лучше подбирать под каждый проект индивидуально. Есть у вас в скетче 8 задач — следовательно и размер более 8 ставить не имеет смысла.
Если так разобраться, то все остальные компоненты такого диспетчера задач — набор неких операций с этой очередью. Мне попадались разные варианты организации работы очереди, но я выбрал самый удобный (ИМХО) вариант, при котором очередь не фрагментируется и все операции становятся легкими и элегантными. А поможет нам в этом переменная — «хвост» очереди.

По своей сути — это номер (или индекс) первой СВОБОДНОЙ ячейки массива задач.

volatile static byte queueTail;

В чем же легкость и элегантность?
1) всегда известно текущее количество задач (queueTail)
2) простая инициализация очереди (queueTail=0)
3) простое и быстрое добавление и удаление задач.
Последнее утверждение лучше всего рассмотреть на примере с картинками.

Исходное состояние очереди, в которой находится 4 задачи (Task0 и Task2 — в режиме ожидания и Task1 и Task3 — уже готовые выполниться). Указатель «хвоста» queueTail=4.
Task Launcher в цикле от 0 до (queueTail-1) проверят задачи на факт установки флага запуска. Первая из задач, которая соответствует эту условию — Task1. Она не «циклическая», следовательно, ее нужно удалить, предварительно запомнив адрес ее вызова. После удаления на ее месте образуется «дырка». С этим нужно что-то делать. Можно сдвинуть всю очередь влево.

Т.е., Task2 — копируется на место бывшей задачи Task1, задача Task3 — на место где была задача Task2. Для нашего случая это две переброски. А если очередь значительно длиннее, то и копирований гораздо больше. Фиговый вариант. Давайте попробуем по другому.

Мы последнюю задачу Task3, которая в массиве значиться как TaskQueue[queueTail — 1] сразу скопируем на место образовавшейся «дырки». А значение «хвоста» банально уменьшим на 1. Всего одно копирование. Дотошный читатель может справедливо заметить, что при таком варианте нарушается последовательность выполнения задач готовых к запуску (очередь-то сканируется всегда с начала). К сожалению, в данном варианте с этим придется смириться :(.
Эти все «движняки» — скрытый функционал микроОс.

Осталось описать «РУЛЬ И ПЕДАЛИ» нашей системы, т.е. те функции, которыми придется активно пользоваться. Их всего две: Добавить/обновить задачу и удалить задачу.

Добавление задачи в очередь

void addTask (void (*taskFunc)(void), unsigned long taskDelay, unsigned long taskPeriod)


Ничего сложного в этой процедуре нет. Если такая задача есть уже в очереди — то просто обновим ее параметры (пауза до выполнения и период), а если нет — делаем запись в первую свободную ячейку, на которую указывает «хвост» очередиTaskQueue[queueTail]. Ну и инкрементируем «хвост»: queueTail=queueTail+1. Понятное дело, все это справедливо если есть свободное место в очереди.

// ==== add Task =======
void addTask (void (*taskFunc)(void), unsigned long taskDelay, unsigned long taskPeriod)
{
   for(byte i = 0; i < queueTail; i++)             // поиск задачи в текущем списке
   {
      if(TaskQueue[i].pTask == taskFunc)        // если нашли, то обновляем переменные
      {
         TaskQueue[i].start_delay = taskDelay;
         TaskQueue[i].period = taskPeriod;
         TaskQueue[i].ready_run = 0;
         return;                                      // обновив, выходим
      }
   }
   if (queueTail < MAX_TASKS)          // если такой задачи в списке нет
   {                                                  // и есть место,то добавляем
      TaskQueue[queueTail].pTask = taskFunc;
      TaskQueue[queueTail].start_delay = taskDelay;
      TaskQueue[queueTail].period = taskPeriod;
      TaskQueue[queueTail].ready_run = 0;
      queueTail++;                                    // увеличиваем "хвост"
    }
}// ==== End add Task =======

Удаление задачи в очереди

void DeleteTask (void (*taskFunc)(void))


Опять же пробегаемся по очереди в поисках задачи (в качестве идентификатора используя адрес функции) и, если задача найдена, удаляем ее [1]. Если это не последняя задача в очереди — делаем переброску последней задачи на освободившееся место [2]. Ну и уменьшаем значение «хвост» очереди [3]: queueTail=queueTail-1

// ==== Delete Task =======
void DeleteTask (void (*taskFunc)(void))
{
   for (byte i=0; i<queueTail; i++)           // проходим по списку задач
   {
      if(TaskQueue[i].pTask == taskFunc)   // если задача в списке найдена
      {
       if(i != (queueTail - 1))                     //  и она не последняя, то переносим последнюю задачу
         {                                                   // на место удаляемой
          TaskQueue[i].pTask = TaskQueue[queueTail - 1].pTask;
          TaskQueue[i].start_delay =TaskQueue[queueTail - 1].start_delay;
          TaskQueue[i].period = TaskQueue[queueTail - 1].period;
          TaskQueue[i].ready_run = TaskQueue[queueTail - 1].ready_run;
         }
         queueTail--;                                 // уменьшаем указатель "хвоста"
         return;
      }
   }
}
// ==== End Delete Task =======

================================
Фу… с теорией на пальцах и картинками-пояснялками, надеюсь, закончили. Теперь займемся делом и перейдем непосредственно к реализации задуманного в виде кода. Сразу же предлагаю разобраться с глобальными настройками будущей системы. Первым делом определимся с размером системного ТИКа. Самый лучший способ отслеживать равные отрезки времени на Arduino — использовать таймер. Таймеры — это простые счетчики, которые считают с некоторой частотой, получаемой из системных 16Мгц. Мы можем сконфигурировать предделитель для получения требуемой частоты и различных режимов счета. Мы также можем настроить их для генерации прерываний при достижении таймером некоторых заданных значений. Но не следует забывать, что таймеры являются ограниченным ресурсом. На Arduino UNO их всего 3, и они используются для многих вещей. Если вы запутались с конфигурацией таймера, некоторые вещи могут перестать работать. Например, на Arduino nano (с микроконтроллером Atmega328):

Мой выбор пал на Timer2. Однако код легко адаптировать на использование любого из них. Я для себя решил, что за единицу измерения системного ТИКа для большинства задач вполне будет достаточно одной сотой секунды. Это значение взято не совсем «с потолка». Во-первых, при таком варианте очень удобно делать пересчет (100 тиков = 1 секунде). И во-вторых, помните, что ОЧЕНЬ желательно чтобы каждая задача успевала выполниться за один системный ТИК. Только при таком условии можно рассчитывать на некую «реалтаймовость» системы.

Предварительно я провел небольшие тесты. На тот момент в моем проекте самой тяжелой задачей был вывод информации на LCD дисплей, который подключался по протоколу I2C. Я написал маленький оценочный скетч, примерно такого содержания:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); //инициализация библиотеки LCD
unsigned long start_time; // время перед запуском
//==== Setup =======
void setup() {
lcd.init(); // инициализация lcd
 Serial.begin(9600); //инициализация COM-порта
} //end setup
//==== main loop =====
void loop() {
  start_time=millis(); //запоминаем время старта
  lcd_screen(); //тестируемая процедура
  Serial.print("Time: ");Serial.println(millis()-start_time);//вывод времени выполнения (ТЕК. ВРЕМЯ - ВРЕМЯ СТАРТА)
  delay(2000); //задержка, между замерами
  Serial.println("===== End ========");//
}// end main loop
//==== test code =====
void lcd_screen() //тестируемая процедура
{
   lcd.setCursor(0,0);
   lcd.print("-MkOS TestTime-");
   lcd.setCursor(0,1);
   lcd.print("time: ");lcd.print(millis());
}

У меня получилось, что вывод информации на LCD занимает примерно 45 миллисекунд. Т.е., это 4.5 запланированного системного ТИКа. Плохо. Но не смертельно. Тут либо разбираться с процедурой вывода на экран и «дробить» ее на мелкие части, либо постараться реже вызывать данную процедуру. Но разбираться с выводом на экранчик в тот момент не входило в мои планы и данную задачу просто реже использую.
Кстати, я до сих пор часто использую этот скетч для оценки скорости выполнения той или иной части кода.

Установка Timer2 на переполнение каждые 0,01 сек (100 Hz).
Предделитель принимаем =1024. Следовательно, за 1 секунду счетчик насчитает (16000000/1024)=15625 значений. А это означает, что за 0.01сек. значение счетчика будет равно 156. Угу… Получается, что до переполнения (напомню, что этот счетчик 8-ми битный) он не досчитал (256- 156)=100 значений. Это число мы и будем указывать в качестве стартового в обработчике прерываний.

Инициализация и настройка Timer2 (выполняется однократно):

void Timer2init()
{
    TCCR2B = 0x00;// сбрасываем регистры таймер счетчика
    TCNT2  = 100; // Reset Timer Count  (256-156)
    TIFR2  = 0x00;// очищаем флаг переполнения (Overflow Flag)
    TIMSK2 = 0x01;// разрешаем сработку по переполнению (Overflow Interrupt Enable)
    TCCR2A = 0x00;// Timer2 Control Reg A: Wave Gen Mode normal
    TCCR2B = 0x07;// установка предделителя в 1024
}

Часики запущены и каждое свое срабатывание будет генерироваться прерывание. А в обработчике данного прерывания будет жить служба «ТаймерСервис». Код обработчика прерываний:

ISR(TIMER2_OVF_vect) 
{
for (byte i=0; i<queueTail; i++)         //сканируем массив задач до хвоста
{
    TCNT2 = 100;      // устанавливаем стартовое значение счетчика
    TIFR2 = 0x00;    // очищаем флаг переполнения (Overflow Flag)
     if  (TaskQueue[i].start_delay == 0) //если пришло время запуска задачи
           TaskQueue[i].ready_run = 1;   //ставим флажок запуска
      else TaskQueue[i].start_delay--;   // если время задержки больше нуля, уменьшаем значение задержки
 }
};

Чуть не забыл. Еще один немаловажный момент: Какие временные интервалы нам доступны?
Напомню запланированную структуру задачи:

typedef struct task
{
   void (*pTask) (void); // указатель на функцию
   unsigned long start_delay; // задержка перед первым запуском задачи
   unsigned long period; // период запуска задачи
   byte ready_run; // флаг готовности задачи к запуску
}task;

Обратите внимание на тип данных для задержки и периодичности unsigned long [0 to 4,294,967,295]. Тут конечно ОГРОМНЫЙ запас. Наша таймерная служба отсчитывает интервалы равные 0.01 секунды. Т.е., максимальные значения для отложенного запуска и периодичности равны 4294967295*0.01=42949672 секунды (или 715827 минуты или 11930 часа или 497 суток). Если не планируется задавать такой интервал времени, то можно заменить на unsigned int [0 to 65’535]. Тогда максимально задаваемые значения будут 65535*0.01=655 секунд (или примерно 11 минут). Я решил что не буду «экономить на спичках» и использую unsigned long.

Еще немного терпения 🙂 Осталось представить реализацию кода «Запускателя задач», Task Launcher

// ==== Task Launcher =======
// Основной цикл работает всегда. Проверяет необходимость запуска задач
// Ну и делает собственно запуск
void TaskLauncher()
{
   void (*function) (void);                 // указатель на функцию, которую если-что, нужно запускать
   for (byte i=0; i<queueTail;)              // сканируем список задач
   {
      if (TaskQueue[i].ready_run == 1)                // если флаг на выполнение взведен,
      {                                                
         function = TaskQueue[i].pTask;               // запоминаем задачу
                                                      
         if(TaskQueue[i].period == 0)                 
           {                                       // если у задачи период равен 0
              DeleteTask(TaskQueue[i].pTask);      // удаляем задачу из списка
           } //end if (TaskQueue[i].period == 0)
         else
         {
            TaskQueue[i].start_delay = TaskQueue[i].period; // и копируем значение периодичности в задержку
            TaskQueue[i].ready_run = 0;                     // иначе снимаем флаг запуска
            i++;   // переходим к следующей задаче
         } //end else
         (*function)();                               // выполняем задачу
      } // end if (TaskQueue[i].ready_run == 1)
      else   // флаг запуска не взведен
         i++;    // переходим к следующей задаче в списке
   }// end for. 
} // ==== End Task Launcher =======


Вот и все. Можно пользоваться.
Буквально пару рекомендаций, для тех кто решит использовать эту mkOS в своих проектах:
— Не используйте в своем коде задержек типа delay() и т.п.;
— старайтесь не запрещать прерывания без крайней необходимости;
— мини-задачи делайте быстрыми или разбивайте на несколько коротких частей;
— старайтесь избегать вызовов одной задачи непосредственно из другой;
— циклические «тяжелые» задачи постарайтесь вызывать как можно реже;
— хотя бы одна задача должна запускаться при инициализации (в блоке SETUP);
— параметр MAX_TASKS выбирайте исходя из необходимого количества задач
;

SETUP

//==== Setup =======
void setup() {
queueTail = 0; // устанавливаем указатель "хвоста" очереди в 0
Timer2init();// инициализируем счетчик
//==== users starting tasks =====
addTask(mySuperTask,100,100); //

В блоке SETUP должен быть запуск хотя бы одной задачи, которая может добавлять/удалять другие, согласно логике скетча.

Основной цикл. Здесь у нас будет ТОЛЬКО Task Launcher. Больше ничего сюда добавлять не нужно!!!
Еще раз!!! В блоке основного цикла должен быть ТОЛЬКО TaskLauncher!!! Все стартовые «пендели» делаем в блоке SETUP!!!

//============= main loop =========
void loop()
{
  TaskLauncher();
}// end main loop

Выглядит непривычно? Да, согласен. Небольшая «ломка» привычного подхода к написанию скетчей.
Давайте вернемся к нашему BLINK скетчу. Теперь с использованием MkOS он будет такой:
Пример BLINK.

/* Blink with MkOS
Две задачи: ledOn() и ledOff().
Каждая задача добавляет в очередь другую.*/
//==== Setup =======
void setup() {
queueTail = 0; // устанавливаем указатель "хвоста" очереди в 0
Timer2init();// инициализируем счетчик
//==== users starting tasks =====
addTask (ledOn,0,0);
} //end setup
void loop()
{
  TaskLauncher();
}// end main loop
//================= Simple my task =====
void ledOn()
{
  digitalWrite(ledPin, HIGH);
  addTask(ledOff, 50, 0);
}
//============================================
void ledOff()
{
  digitalWrite(ledPin, LOW);
  addTask(ledOn, 50, 0);
}

//============================================
Спешу заметить, что при компиляции данный скетч занимает 1636 байт, против 1030 от примера Blink 🙂 Весьма неплохо, учитывая заложенный потенциал.
Кстати, по началу весь код функционала mkOs я просто КопиПастил из проекта в проект, добавляя нужные мне задачи. Получалась огромная «простыня-исходник» в которой разбираться было не очень удобно. Частично вопрос кое-как решался при использовании альтернативного редактора, который умеет сворачивать куски кода (например Geany):

Так и продолжалось некоторое время пока мой приятель Александр не поинтересовался: «А что это у тебя за фигня в коде?»
Пользуясь случаем, ОГРОМНАЯ ему благодарность за НЕОЦЕНИМУЮ помощь в переделке скетча в библиотеку

Теперь вообще все как «у взрослых»!
При использовании библиотеки все «внутренности» mkOS прячутся и не мешают созидательному процессу. Саму библиотеку нужно положить в каталог X:\Arduino\libraries\. С использованием библиотеки наш скетч выглядит так:

//подключение библиотеки
#include <mk_os.h>
#define ledPin 13
void setup() {
  pinMode(ledPin,OUTPUT);
  MkOS.addTask(ledOn,0,0); //добавление задачи
}

void loop() {
  MkOS.TaskLauncher();//запуск
} // end main loop

//**********************************************
//Users code
//==============================================
void ledOn() //задача ВКЛЮЧИТЬ светодиод
{
  digitalWrite(ledPin, HIGH);
  MkOS.addTask(ledOff, 50, 0); //добавляем задачу выключить светодиод через 0.5 секунды
}
//============================================
void ledOff() //задача ВЫКЛЮЧИТЬ светодиод
{
  digitalWrite(ledPin, LOW);
  MkOS.addTask(ledOn, 50, 0); //а тут добавляем задачу на включение светодиода через 0.5 сек
}

Ну и напоследок видео демонстрации работы (она у меня на столе трудилась пару недель без сбоев).
1) Простенькая анимация на Nokia LCD (ну да, волк получился кривовато)
2) мигаем встроенным светодиодом на 13-ом пине
3) «пульсируем» зеленым светодиодом
4) раз в 2 сек. измеряем температуру воздуха (с помощью делителя на терморезисторе)
5) раз в секунду мигаем красным светодиодом
6) раз в 5 сек отправляем данные в СОМ-порт
7) выводим данные на 2-х строчный индикатор (подключен по i2c)
8) вывод на 7-сег. индикатор раз в 1 секунду (подключение по одному проводу. Смотри тут)

Не судите строго, моя первая попытка видеомонтажа 🙂
=====================
Весь материал (библиотеку, описанные демо-скетчи, примеры использования) забираем ОТСЮДА.
=====================
Подведем итоги.
Первоначальная цель (разобраться как работают подобные диспетчеры) — выполнена. (Этот код уже реально работает в нескольких моих проектах). Понятное дело, пихать эту микроОС во все проекты — абсолютно не нужно. Код вполне работоспособен, хотя и имеет ряд недостатков и упрощений над которыми имеет смысл еще поработать. Напоминаю, что эта микроОС — черновой вариант для домашних поделок. Я не ручаюсь за ее стабильную и точную работу. И поэтому не стоит применять данную операционную систему для управления ядерным реактором, воздушным истребителем, медицинским оборудованием и т.п.


0 комментариев на «“Small Simple Arduino Task Scheduler”»

Добавить комментарий

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Arduino Bluetooth CraftDuino DIY Google IDE iRobot Kinect LEGO OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение