AVRASM: Диспетчер задач RTOS 2.0 (псевдо кооперативная ОС)

Operating system placementОтрефакторил код «Диспетчера задач RTOS» (псевдо кооперативной ОС), оптимизировал и универсализировал, добавил новые фичи, декларировал чёткое API, и опубликовал на GitHub… Фактически, весь код был переписан сызнова, по прототипу DI HALTа.

Назначение

«Диспетчер задач RTOS» предназначен для решения нескольких независимых параллельных задач на одном микроконтроллере (реализация многозадачности).

Главной особенностью этой примитивной ОС «Диспетчер задач RTOS» является: исключительная простота и малый объём служебного кода! Позволяющие использовать данную ОС даже на самых слабых микроконтроллерах (вплоть до семейства AVR8L CPU: ATtiny10, ATtiny20, ATtiny40).

Быстрый и легкий в освоении «Диспетчер задач RTOS» предназначен для решения относительно простых задач, с небольшим объёмом кода. Для сложных задач, с тяжёлой математикой и сложной логикой — лучше использовать другие полноценные промышленные кооперативные RTOS (e.g. Salvo, SCM или AVRX)…

Новые функции (анонс)

1) Универсальный код инициализации RTOS_INIT: единственное что нужно, это вручную сконфигурировать несколько констант (режимных параметров). При использовании аппаратного таймера Timer/Counter0 (по-умолчанию) — настройка его аппаратных регистров, конфигурация режимов, производится полностью автоматически.
Поддерживаются ВСЕ МИКРОКОНТРОЛЛЕРЫ AVR (кроме семейства ATxmega): от самых мелких (ATtiny10), до разумно больших (ATmega64). Добавлена поддержка семейства AVR8L CPU… Разумеется, полная универсальность и совместимость со всеми МК сейчас не гарантируется — код нужно ещё проверить, и возможно допилить, с помощью Сообщества.

2) Вдобавок к «Базовой диспетчеризации задач» (последовательный запуск, в порядке очереди) и «Диспетчеризации Задач по Таймеру» (безусловно отложенный запуск на миллисекунд), добавлена поддержка «Флаговой автоматизации» (Диспетчеризация Задач по состоянию бита в Управляющем Регистре/Порте или в ячейке SRAM: запуск, если бит в байте установлен? Или, если бит в байте снят?)

3) Директивами условной компиляции можно отключать отдельные возможности RTOS, уменьшая размер ядра и используемую оперативную память (оптимизация под простые задачи и слабые МК).

4) Чётко декларированный API, для разработки прикладных Задач (читай комментарии в коде, чтобы здесь не повторяться).

И многое другое…

Код

Код «RTOS» опубликован на GitHub (это веб-сервис для хостинга IT-проектов и их совместной разработки), на условиях лицензии MIT (разрешительной opensource, т.е. практически без ограничений к использованию). Авторство и Copyright (c) 2014 «Сообщество EasyElectronics.ru». Ответвляйтесь!

Релизы:

Kickstart Kit: Обратите внимание, что код RTOS поставляется в комплекте с «Шаблоном нового проекта» — интегрировано, для мгновенного старта разработки программной прошивки (firmware) в среде «AVR Studio 4″… Здесь, чистый «Шаблон + ОС», без прикладного кода. Код содержит единый стиль форматирования, и комментарии с рекомендациями и описанием секций кода…
Код самого «Диспетчера задач RTOS» расположен в трёх отдельных файлах: «RTOS_data.inc» (данные), «RTOS_macro.inc» (API), «RTOS_kernel.inc» (ядро).

Зависимости: После рефакторинга Celeron, в коде RTOS используется, и требует подключения, нестандартная внешняя «Библиотека базовых Макроопределений (macrobaselib.inc)»… Последнюю версию которой, можно скачать на GitHub

Примечание: GitHub был выбран для распространения кода — как наиболее прогрессивный, удобный и функциональный метод взаимодействия opensource-разработчиков. Развивайте и дополняйте код — затем, сможете легко контрибутить…

Ликбез для неискушённых пользователей: те кто не используют системы управления версиями, могут просто скачать архив с кодом: нажав на кнопку «Download ZIP» на странице репозитория GitHub, по ссылкам выше.

Настройка системы

  1. Основной файл, в котором производится настройка режимов Системы: «RTOS_macro.inc» (API). Здесь, директивами условной компиляции, нужно выбрать те функции RTOS, которые требуются для решения задачи — это обусловливает всю дальнейшую разработку прикладного кода (какие методы API будут доступны; как разделяются РОН между прикладным кодом и системными методами; и т.п. фундаментальные вещи). Это нужно выбрать в первую очередь!
  2. Также, требуется настроить: размеры системной «Очереди Задач» и «Пулов Таймеров» и «Выжидателей», в файле «RTOS_data.inc» (данные).
    Замечу: Переполнение очереди и пулов — деструктивно, для логики многих Задач! ОС это не завалит, но может привести к сбоям или «зависаниям» отдельных задач… Что делать? Используйте ловушку RTOS_METHOD_ErrorHandler, для выявления ситуаций переполнения, на этапе отладки устройства.
  3. А ещё нужно подкрутить: константы Настройки Скорости МК (CPU clock) — в макросах инициализации RTOS_INIT, USART_INIT… (их невозможно настроить автоматически, нужно править вручную)

Итак, директивами условной компиляции можно отключать отдельные возможности RTOS, уменьшая размер ядра (оптимизация под простые задачи и слабые МК):
.EQU RTOS_FEATURE_PLANTASK = 1 ; поддержка Базовой диспетчеризации задач
.EQU RTOS_FEATURE_PLANTIMER = 1 ; поддержка Диспетчеризации Задач по Таймеру
.EQU RTOS_FEATURE_PLANWAITER = 1 ; поддержка Флаговой автоматизации

;.EQU RTOS_OPTIMIZE_FOR_SMALL_MEMORY = 1 ; используйте это только для самых младших МК!

«Базовая диспетчеризация задач» (RTOS_FEATURE_PLANTASK) строго необходима для работы всех функций RTOS! При её отключении — отключаются и все другие функции RTOS. «Базовая диспетчеризация задач» обеспечивает: инициализацию RTOS; поддержку «Очереди Задач»; функцию «Добавления Задачи в очередь»; и «Диспетчер очереди Задач» (системный метод, который обрабатывая Очередь, непосредственно запускает Задачи на выполнение). Замечу: «Служба таймеров RTOS» (занимающая аппаратный таймер) не требуется в этом «базовом режиме», и отключена.

Вдобавок к базовому функционалу, Задачи можно «диспетчеризовать по Таймеру» (RTOS_FEATURE_PLANTASK): выполнять безусловно отложенный запуск на фиксированное время, равное целому числу миллисекунд. Эта функция особенно часто востребована при решении задач — используется вместо тупой задержки «пустым циклом CPU».

Наконец, поддержка простейших «Флаговых автоматов» (RTOS_FEATURE_PLANWAITER) — призвана заменить Архитектуры, построенные на Суперцикле + Флаговый автомат. Позволяет диспетчеризовать Задачи по состоянию одного конкретного бита в оперативной памяти (в РОН, Управляющем Регистре/Порте, или в ячейке SRAM). Запуск задачи откладывается на неопределённое время, пока этот указанный бит не будет «установлен» или «снят» (условия запуска программируются при постановке Задачи в Пул). Уточню: таким образом, здесь, управление Задачей не может осуществляться группой бит, в одном или нескольких байтах — нет, автомат простейший, способный отслеживать только один бит…

Код каждого из трёх вышеозначенных режимов подвержен модификатору «Оптимизации под малую память» (RTOS_OPTIMIZE_FOR_SMALL_MEMORY) — рекомендуется использовать его только для самых младших МК, с малым объёмом памяти! Включение «Оптимизации под малую память»: отключает в системных методах RTOS защиту «временных регистров» стеком; отключает некритичные проверки целостности данных; выкидывает второстепенные методы из ядра; отключает всё что только можно — чтобы минимизировать объём кода RTOS, и главное, минимизировать используемый/требуемый объём ОЗУ/стека.

RTOS v2.1:
В режиме со включенной оптимизацией RTOS_OPTIMIZE_FOR_SMALL_MEMORY, для работы Системы — в Стеке требуется гарантированно оставить [всего лишь] 8 байт (восемь) байт! При этом, из кода Задач, можно вызывать Подпрограммы одного уровня вложенности (в т.ч. PLAN_TASK-методы)… Напомню: Ещё требуется выделить SRAM под «Очередь Задач», «Пулы Таймеров» и «Выжидателей» (настраиваются в )… Оставшееся ОЗУ — можно использовать для прикладных данных.

Например: на МК ATtiny10 (при 32 байтах SRAM), можно использовать RTOS:
с одним Таймером (-3байта)
и одним Флаговым Автоматом (-3байта),
и очередь глубиной 5шт. Задач (-5байт),
не забыть про Счётчик "таймерной службы" (-1байт);
ещё (-8 байт) на Стек для RTOS;
остаётся 12 байт SRAM на прикладную логику (10 на данные + 2 про запас). Profit!
/Замечу: при этом, в сегменте кода, вся система будет занимать 350-450 байт - остальное на прикладную логику./

Итого, выходит 6 вариантов (фрагментов) кода ядра: 3 фичи по 2 варианта (полный и обрезанный). И 8 режимов компиляции… При этом, код грамотно обёрнут макросами условной компиляции так, что при любой комбинации режимов — стандартный Дистрибутив будет компилироваться без ошибок. При отключении всех фич RTOS — функционал будет эквивалентен простому «Шаблону нового проекта».

Результирующий объём ядра RTOS в сегменте кода, в зависимости от выбранных режимов компиляции (сводка):

RTOS v2.0:
Результирующий объём ядра RTOS v2.0 в сегменте кода, в зависимости от выбранных режимов компиляции (сводка)
RTOS v2.1:
Результирующий объём ядра RTOS v2.1 в сегменте кода, в зависимости от выбранных режимов компиляции (сводка)
Примечание: К этому нужно прибавить ещё код базовой инициализации МК (это исключая инициализацию RTOS), таблицу прерываний и т.п. (+80..100 байт). Не забудьте также, кроме кода Задач (+X байт), прибавить размер «индексной таблицы» RTOS_TaskProcs (в базовом шаблоне: 10 задач = +20 байт).

Доступные методы прикладного программного интерфейса RTOS (API), в зависимости от выбранных режимов компиляции (сводка):

Доступные методы прикладного программного интерфейса RTOS (API), в зависимости от выбранных режимов компиляции (сводка)

Идеология системы

«ЗАДАЧИ кооперативной RTOS» — это особые Подпрограммы:

1) ОФОРМЛЕНИЕ
Как и обычные процедуры — они имеют «метку входа» (адрес/имя), а выход из них осуществляется только через RET (нельзя использовать RETI — это не обработчики прерываний!)

2) ПАРАМЕТРЫ
Но «Задачи кооперативной RTOS» не могут иметь «регистровых параметров»! Т.к. они запускаются опосредованно, через «Диспетчер Задач RTOS», в порядке очереди — то между запусками разных запланированных задач проходит неопределённое время, и происходит множество неконтролируемых операций с РОН.
Таким образом, Задачи должны управляться только данными в SRAM и в регистрах ввода/вывода! (Примечание: исключение могут составлять выделенные Задаче «регистровые переменные», но это редкость — см. обсуждаются в п.5.1)

3) ЗАПУСК
«Задачи RTOS» никогда не запускаются непосредственно, через CALL <адрес>, как обычные подпрограммы! Все Задачи запускаются только опосредованно — через механизмы RTOS («очередь задач» и «диспетчер задач»).

3.1) Задачи могут быть «запланированы к запуску» (по их порядковому номеру в индексной таблице «RTOS_TaskProcs»):

  • либо «как можно скорее» (через PLAN_TASK),
  • или «отложенный запуск, спустя заданное время, в мс» (через PLAN_TIMER, см. ниже п.7),
  • или «при наступлении внешнего события, состояния аппаратного устройства, состояния флага в памяти» (через PLAN_WAITER, см. ниже п.8).

В любом случае, передача управления между задачами никогда не происходит мгновенно — между ними происходит, как минимум один, проход цикла «Диспетчера задач»…

3.2) Поэтому, все Задачи необходимо предварительно зарегистрировать в системе: внести <адрес> в индексную таблицу «RTOS_TaskProcs», под определённым порядковым номером <индекс>; и для облегчения последующей идентификации, можно присвоить этот номер символической константе: .equ TS_TaskX = <индекс>.

3.3) Исключение составляет: системная задача «холостой цикл» Task_Idle, под номером =0 в индексной таблице. Никогда не планируйте/не добавляйте задачу Task_Idle в очередь на выполнение! (она сама, особым образом, обрабатывается RTOS: запускается при пустой «очереди задач»)

4) ХОД ВЫПОЛНЕНИЯ
Во время исполнения «кооперативной Задачи» управление не может быть насильно передано никакой другой «Задаче», ни службам RTOS! (только аппаратные прерывания всё-ещё могут прервать эксклюзивный ход выполнения Задачи)

4.1) Поэтому «кооперативная Задача», во время своего выполнения, может не опасаться за сохранность данных в РОН, SRAM, и регистрах ввода/вывода (состояниях аппаратной периферии, ПУ):

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

4.2) С другой стороны, во время исполнения Задачи, всё-ещё может произойти «запуск прерывания»! Но код всех прерываний, безусловно, должен сберегать содержимое всех используемых РОН, через стек — они защищены. Однако, поскольку прерывание может влиять на содержимое иных разделяемых ресурсов (содержимое SRAM и состояния ПУ) — при работе с критическими аппаратными ресурсами (например, при записи в EEPROM), в коде задач допускается использовать CLI/SEI экранирование отдельных секций!

4.3) Впрочем, Задачам недозволенно запрещать прерывания надолго (дольше 1мс), чтобы не нарушать работу RTOS! Также, Задача не может запретить прерывания перманентно — как только завершится текущая Задача, то RTOS автоматически разрешит прерывания (SEI).

Например, невозможно реализовать функционал: Задача1 запретила прерывания, а запущенная следом Задача2 их вновь разрешила. Но это и бессмысленно: если запретить прерывания, то остановится «таймерная служба RTOS» — все «отложенные Задачи» будут заблокированы, не исполнятся никогда; и прерывания также никакие не возникнут — поэтому нет смысла так разбивать Задачу на подЗадачи…

Все «критические секции» необходимо отрабатывать в рамках одной Задачи! Вставляйте пустые циклы задержки, между CLI/SEI, если требуется («тупить фиксированное число циклов» или «ожидать бит в системном регистре»).

5) ОСОБЕННОСТИ ПРОГРАММИРОВАНИЯ
У «кооперативных Задач», есть важная привилегия, которой не обладают (асинхронно запускаемые) обработчики прерываний: Задачи могут не защищать СОДЕРЖИМОЕ РЕГИСТРОВ (РОН) через стек, вообще, как будто задачи выполняются на процессоре эксклюзивно. (Повторюсь: не требуется сохранять в стек никакие используемые регистры перед исполнением кода Задачи, и затем восстанавливать после выполнения перед выходом!) Но при этом, всё пространство регистрового файла РОН следует рассматривать только как «временные переменные»!

6) МЕТОДИКА ПРОЕКТИРОВАНИЯ
Функционально, алгоритм решения сложной прикладной задачи разбивается на подзадачи — каждая из которых оформляется в код отдельной «кооперативной Задачи». Таковые Задачи могут выполняется относительно других: последовательно, или условно (отдельные ветки IF-THEN-ELSE можно оформить как отдельные Задачи, и запускать через PLAN_TASK), или спустя определённую задержку времени (Задачи, планируемые через PLAN_TIMER), или спустя неопределённое ожидание внешнего события или состояния аппаратного устройства: приход данных в порт, завершение операции с ADC или EEPROM, событие компаратора и т.п. (Задачи, планируемые через PLAN_WAITER)

6.1) Код каждой Задачи должен быть атомарен. Каждая Задача при запуске: вычитывает, всё что нужно её алгоритму, из SRAM и регистров ввода/вывода; делает полезную работу; сохраняет результаты в SRAM; завершается (RET).

6.2) Код каждой Задачи должен выполняться максимально быстро, как и обработчики прерывания. Никогда не используйте «тупые задержки» в коде! Вместо этого, разделите Задачу на две подЗадачи: в первой части, сохраните в SRAM все промежуточные данные, нужные алгоритму второй части, и запланируйте запуск второй подзадачи (через PLAN_TIMER)…

6.3) Разумеется, для решения каждой отдельной прикладной задачи — потребуется определить целую серию подпрограмм «кооперативных Задач». Чтобы упорядочить этот колхоз — рекомендуется использовать разные трюки:

  • Имена давать Задачам, сформированные особым образом, префиксно: TS_TaskX_Begin, TS_TaskX_Check, TS_Task_Finish…
  • Перечислять Задачи в индексной таблице «RTOS_TaskProcs» последовательно, группами…
  • Давать описания коду и разделять его по секциям… и т.п.

7) Порядок установки Таймера:

  1. Добавить новый Таймер, вызвав API-метод: PLAN_TIMER tasknumber,delay
    • Если в пуле уже существует Таймер для задачи <tasknumber>, то его текущий «счётчик времени» будет переписан на значение <delay>. Прикол с апдейтом таймера кажется лишним, но реально часто пригождается. Например, когда по условию надо отложить событие. Берешь и перезаписываешь таймер, подобно программному watchdog.
    • Иначе, при наличии в пуле свободного места, регистрируется новый Таймер: для запуска задачи <tasknumber>, через время <delay> = [0..65535] ms.
  2. При необходимости перепланировать таймер на другое время, до того как он сработал — вновь вызвать API-метод: PLAN_TIMER tasknumber,delay
  3. Перепланировывать таймер можно неограниченное количество раз, лишь бы — делать это до того, как он сработает…]
  4. При необходимости же вовсе отменить запланированный запуск задачи — удалить таймер, вызвав API-метод: REMOVE_TIMER tasknumber
  5. Каждую ~1ms реального времени, по прерыванию, запускается «Служба таймеров RTOS», которая:
    • Декрементирует счётчики, всем активным Таймерам в пуле, на (-1)…
    • Если, при этом, значение счётчика достигает =0 ? То связанная Задача <tasknumber> добавляется в очередь RTOS_TaskQueue, на выполнение; А сам сработавший Таймер, при этом, деактивируется/самоудаляется, освобождая Элемент пула…
  6. Задача, посланная в «системную очередь» RTOS_TaskQueue, будет выполнена в порядке общей очереди…

Побочные эффекты:

  • API-методы PLAN_TIMER/REMOVE_TIMER, при исполнении, портят содержимое в некоторых регистрах: в зависимости от режима RTOS_OPTIMIZE_FOR_SMALL_MEMORY, либо только «параметровые регистры», либо также и «временные регистры» (см. комментарии в секции «Subroutine Register Variables», соответствующего системного метода).
  • Для управления Таймерами предлагаются также API-методы: SAFE_PLAN_TIMER/SAFE_REMOVE_TIMER, которые гарантированно не портят вообще никаких регистров, но они доступны только в режиме выключенной оптимизации RTOS_OPTIMIZE_FOR_SMALL_MEMORY (при этом, используется Стек — нужна дополнительная оперативная память).

8) Порядок установки Выжидателя:

  1. Добавить новый Выжидатель, вызвав API-метод: PLAN_WAITER tasknumber,address,bit,state
    • Если в пуле уже существует Выжидатель для задачи <tasknumber>, то его текущие «условия запуска» будет переписаны на значения <address,bit,state>.
    • Иначе, при наличии в пуле свободного места, регистрируется новый Выжидатель: для запуска задачи <tasknumber>, при наступлении «заданных условий» (значения бита в памяти), которое может произойти через любое неопределённое время (проверка состояния бита осуществляется периодически, каждые 1ms).
  2. При необходимости перепланировать выжидатель, на новые условия, до того как он сработал — вновь вызвать API-метод: PLAN_WAITER tasknumber,address,bit,state
  3. Перепланировывать выжидатель можно неограниченное количество раз, лишь бы — делать это до того, как он сработает…]
  4. При необходимости же вовсе отменить запланированный запуск задачи — удалить выжидатель, вызвав API-метод: REMOVE_WAITER tasknumber
  5. Каждую ~1ms реального времени, по прерыванию, запускается «Служба таймеров RTOS», которая:
    • Проходясь по всем активным Выжидателям в пуле, вычитывает байт из памяти по адресу <address>, и проверяет в нём значение бита номер <bit>…
    • Если значение бита = <state> ? То связанная Задача <tasknumber> добавляется в очередь RTOS_TaskQueue, на выполнение; А сам сработавший Выжидатель, при этом, деактивируется/самоудаляется, освобождая Элемент пула…
  6. Задача, посланная в «системную очередь» RTOS_TaskQueue, будет выполнена в порядке общей очереди…

Побочные эффекты:

  • API-методы PLAN_WAITER/REMOVE_WAITER, при исполнении, портят содержимое в некоторых регистрах: в зависимости от режима RTOS_OPTIMIZE_FOR_SMALL_MEMORY, либо только «параметровые регистры», либо также и «временные регистры» (см. комментарии в секции «Subroutine Register Variables», соответствующего системного метода).
  • Для управления Выжидателями предлагаются также API-методы: SAFE_PLAN_WAITER/SAFE_REMOVE_WAITER, которые гарантированно не портят вообще никаких регистров, но они доступны только в режиме выключенной оптимизации RTOS_OPTIMIZE_FOR_SMALL_MEMORY (при этом, используется Стек — нужна дополнительная оперативная память).

Теория RTOS

Дополнительную документацию по теме — можно найти по ссылкам:

Курс от DI HALT (лучшее):
http://easyelectronics.ru/tag/rtos

Статьи в Блоге Сообщества:
http://we.easyelectronics.ru/blog/os-rtos/
http://we.easyelectronics.ru/tag/RTOS/

Смотри также следующую статью:
Пример использования «Диспетчера задач RTOS 2.0» (установка и настройка)…

Предпосылки

Разбирая исходники RTOS, меня не покидала одна навязчивая мысль: «DI HALT — либо гений, либо учителя у него хорошие были… Но в исходниках у него — говнокод ещё тот!» Т.е. много светлых идей, а framework до конца не доведен. Я не смог использовать такой код, пока не привёл его к Завершённой Форме. Пришлось потратить много времени и усилий… Но результатом я доволен — красивый инструмент!

Чтобы сэкономить время и усилия другим — делюсь своей редакцией кода RTOS, присваиваю ей номер: релиз 2.0. Она полностью совместима с версией DI HALT’а, по API на уровне макросов! (Хотя, замечу, прототипы системных методов были изменены и дополнены.) Можете мигрировать на неё свои проекты.

Пожалуйста, пробуйте и тестируйте этот код. При нахождении ошибок в коде (багов), или неуниверсальности на некоторых микроконтроллерах (самому всего не протестировать) — пишите багрепорты: здесь в комментариях, или на багтрекере GitHub. Постараюсь исправить…

Комментарии закрыты.