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

Для демонстрации основных возможностей и особенностей «Диспетчера задач RTOS 2.0» был собран демонстрационный макет, на основе «Универсальной макетной платы для МК в DIP-корпусе». В качестве прикладной задачи: мигаем светодиодами, в разных режимах…

Соберём аппаратный макет (hardware)

  1. В основе схемы был использован микроконтроллер ATtiny2313 (простой, дешёвый, и в то же время, не самый примитивный — удобен для простых макетов, и спалить не жалко)
  2. Весь PORTB микроконтроллера (PB0..PB7, пины 12..19) выведен на линейку «Шкального светодиодного индикатора» (он будет отображать значение байтового Счётчика, в двоичном коде)
  3. Вдобавок, на светодиодный индикатор, отдельно, выведен ещё один «сигнальный бит», через PORTD (PD4, пин 8) — это индикатор «простаивания» ядра микроконтроллера
  4. Также, для управления работой системы, нам понадобятся две «пользовательские кнопки»: подключены на PORTD (PD5 и PD6, пин 9 и 11).
  5. Тактирование микроконтроллера будем осуществлять от «внутреннего генератора», настроенного на 8МГц (программатором следует сконфигурировать фьюзы, соответствующим образом)

Чтобы не паять простые типовые схемы, всякий раз, для таких точечных экспериментов — существует замечательная традиция: использовать разные «Универсальные макетные платы»… Здесь, я собрал макет (без пайки) на авторской макетной плате собственной разработки, придуманной и созданной совсем недавно, как раз для таких случаев.

Создадим прошивку (firmware)

Порядок действий, по созданию прошивки с нуля, следующий:

  1. Скачаем с GitHub релиз «Диспетчера задач RTOS 2.0»… И распакуем его в локальную папку с проектами «AVR Studio» (для других IDE, возможно, придётся пересоздать файл проекта).
  2. В коде проекта, первым делом, редактируем файл «RTOS_macro.inc» (API) — директивами условной компиляции выбираем возможности RTOS, которые будем использовать:
    .EQU	RTOS_FEATURE_PLANTASK = 1		; поддержка Базовой диспетчеризации задач	(последовательный запуск, в порядке очереди)
    .EQU	RTOS_FEATURE_PLANTIMER = 1		; поддержка Диспетчеризации Задач по Таймеру	(безусловно отложенный запуск на  миллисекунд)
    .EQU	RTOS_FEATURE_PLANWAITER = 1		; поддержка Флаговой автоматизации		(диспетчеризация Задач по состоянию бита в памяти РОН, Управляющем Регистре/Порте, или в ячейке SRAM:  запуск задачи, если/как-только бит в байте установлен?  или если/как-только бит в байте снят?)
    ;.EQU	RTOS_OPTIMIZE_FOR_SMALL_MEMORY = 1	; используйте это только для самых младших МК!
  3. Вторым шагом, в файле «RTOS_data.inc» (данные), конфигурируем размеры «Очереди Задач» и «Пулов Таймеров» и «Выжидателей»:
    .equ 	RTOS_TimersPoolSize 	= 5	; Длина пула Таймеров    [1..85] (максимально возможное число одновременно существующих "отложенных задач" в системе)
    .equ 	RTOS_WaitersPoolSize 	= 5	; Длина пула Выжидателей [1..85] (максимально возможное число одновременно существующих "отложенных задач" в системе)
    .equ 	RTOS_TaskQueueSize 	= 11	; Длина очереди Задач   [1..255] (задается с запасом, чтобы не произошло ее переполнения)
  4. Так как мы будем использовать функционал Таймеров и Выжидателей, то нужно сконфигурировать «Службу таймеров RTOS», в файле «RTOS_macro.inc» (API):
    • «Служба таймеров» вешается на прерывание одного из выделенных аппаратных таймеров. При использовании аппаратного таймера Timer/Counter0 — настройка его аппаратных регистров, конфигурация режимов, производится полностью автоматически (в макросе «RTOS_INIT» прописан универсальный код инициализации). Данный метод рекомендуется по-умолчанию! Чтобы его использовать — включите это определение:
      .EQU	RTOS_USE_TIMER0_OVERFLOW = 1
    • Настройка «Службы таймеров» на аппаратном таймере Timer/Counter0 — очень проста! Достаточно лишь, в коде макроса RTOS_INIT, установить константы скорости работы микроконтроллера (CPU clock):
      .equ CMainClock		= 8000000				; тактовая частота "CPU Clock" (Hz)
      .equ CTimerPrescaler	= 64					; Предделитель аппаратного таймера: 1, 8, [32,] 64, [128,] 256, 1024
      .equ CTimerDivider	= CMainClock/CTimerPrescaler/1000	; итоговый "делитель тактовой частоты", т.е. коэффициент её преобразования во время ~ 1ms
      ; Примечание: значение CTimerDivider должно получиться целым числом! И как можно меньше - чтобы помещаться в разрядную сетку таймера: для 8-bit <=255 / для 16-bit <=65535...

На этом, конфигурация RTOS завершена! Далее, приступаем к разработке прикладной логики:

  1. Обычно, я начинаю разработку с модели прикладных данных. В файле «data.inc» (данные), в секции «.DSEG», прописываем:
    			.DSEG
    DCnt:	.byte	1		; Некоторые переменные в памяти (для решения прикладной задачи)
  2. Следующий непременный шаг — это программирование кода специфической инициализации… (Код инициализации RTOS уже прописан в шаблоне — об этом думать не нужно.) Осталось добавить Инициализацию Портов ввода/вывода:
    ;***** BEGIN Internal Hardware Init ****************************************
    OUTI	PORTB,	0		; обнулить регистр выходных данных ПортаB (начальное положение)
    OUTI	DDRB,	0b11111111	; все пины на "выход" (OUT)
    
    OUTI	PORTD,	0		; обнулить регистр выходных данных ПортаD (начальное положение)
    OUTI	DDRD,	(1<<DDD4)	; один пин на "выход" (OUT), остальные пины на "вход" (IN)
    ;***** END Internal Hardware Init 
    
  3. Теперь, самое время запрограммировать прикладную логику (в файле «project2.asm»). Здесь, я не буду подробно разбирать код — читайте комментарии, всё должно быть ясно… У меня получились такие определённые Задачи:
    ;***** BEGIN "RTOS Tasks" section ******************************************
    ;---------------------------------------------------------------------------
    ; Системная задача "холостой цикл" (запускается при пустом конвейере)
    Task_Idle:
    		; (состояние: обнаружено простаивание МК!)
    		SETB		PORTD,	PORTD4		; Зажечь сигнальный бит	"простаивание"
    		PLAN_TIMER	TS_Task1,	1	; и запланировать погасить его через 1ms
    		; (замечу: если простаивание МК продолжится, то бит так и не погаснет - за счёт постоянного откладывания, перепланирования "операции гашения")
    		RET
    ;---------------------------------------------------------------------------
    ;-----------------------------------------------------------------------------
    Task1:
    		CLRB		PORTD,	PORTD4		; Погасить сигнальный бит "простаивание"
    		RET
    ;---------------------------------------------------------------------------
    ;-----------------------------------------------------------------------------
    Task2:
    		; Каждую 1сек - Автоматически наращиваем счётчик:
    		INC8M	DCnt				; инкрементировать счётчик
    		OUTR	PORTB,	temp			; и вывести текущее значение счётчика в порт
    		
    		PLAN_TIMER	TS_Task2,	1000	; зациклить эту шарманку (по таймеру, через 1сек)
    		RET
    ;---------------------------------------------------------------------------
    ;-----------------------------------------------------------------------------
    Task3:
    		; Каждую 1мс, При нажатии/удерживании кнопки:
    		PLAN_TASK	TS_Task2		; "Мгновенный" запуск Задачи, невзирая на её Таймер (пропускаем задержку... вернее, меняем задержку на 1мс)
    		
    		.if PIND < 0x40				; Приводим адрес I/O регистра к валидному для LD-инструкции...
    		.equ	PIND_address = PIND+0x20	; здесь, вводим коррекцию (если аппаратный порт PIND входит в границы 0x3F адресации, предназначенной для IN/OUT-инструкций)
    		.else
    		.equ	PIND_address = PIND
    		.endif
    		PLAN_WAITER	TS_Task3, PIND_address, PIND6, 0	; зациклить эту шарманку (при нажатии кнопки, пин порта продавится на "землю")
    		
    		PLAN_TASK	TS_Task4		; И также, запускаем особое действие (дочернюю циклическую задачу)...
    		RET
    ;---------------------------------------------------------------------------
    ;-----------------------------------------------------------------------------
    Task4:
    		; Непрерывно, Пока кнопка удерживается - зафлудим очередь задач, перегружая МК, чтобы погас сигнальный бит "простаивание":
    		STOREB	PIND,	PIND6			; прочитать текущий статус кнопки -> T
    		BRTS	Exit__Task4			; если кнопка не нажата, то идём на выход...
    		PLAN_TASK	TS_Task4		; иначе, если кнопка нажата, то зацикливаем эту задачу.
    
    		; Причём, если нажата также и вторая кнопка - устроим армагедец:
    		STOREB	PIND,	PIND5			; прочитать текущий статус "второй кнопки" -> T
    		BRTS	Exit__Task4			; если вторая кнопка не нажата, то идём на выход...
    		PLAN_TASK	TS_Task4		; иначе, если удерживаются обе кнопки - сделаем жестокую вещь: переполним очередь задач! 
    							; 	Теперь даже таймеры и выжидатели не смогут сработать вовремя!	(т.е. не смогут, при срабатывании, добавить свои Задачи в очередь)
    							; 	Причём, опасность: это НАРУШИТ ЦИКЛЫ САМОПОДДЕРЖИВАНИЯ Задач: Task2 и Task3!	(до следующего RESET)
    							; 	Причина: Таймеры/Выжидатели отключатся, но не активируются вновь, т.к. подпрограммы их задач не отработают "вовремя" - и перезапуск Таймеров осуществлён не будет.
    							; 	Это нужно иметь в виду...
    							; 
    							; 	Вывод: Переполнение очереди и пулов - деструктивно, для логики многих Задач! 
    							; 	Что делать? Используйте ловушку RTOS_METHOD_ErrorHandler, для выявления таких ситуаций, на этапе отладки устройства.
    							; 	А также, в эту ловушку можно поместить дежурный код, перезапускающий критические Процессы, в production-устройствах...
    	Exit__Task4:
    		RET
    ;-----------------------------------------------------------------------------
  4. Наконец, все определённые выше Подпрограммы — нужно внести в Таблицу «индексных переходов на реальные адреса Задач (RTOS_TaskProcs)». В стандартном шаблоне эта таблица уже определена, с зарегистрированными в ней 10 Задачами, для примера… И поскольку я не менял названия Подпрограмм (так и остались: «Task1», «Task2″…), то содержимое этого определения остаётся как есть:
    RTOS_TaskProcs:
    	 	.dw	Task_Idle	; [  0]	системная задача "холостой цикл"  (важно: должна быть расположена по индексу=0 - используется системой RTOS)
    		.dw	Task1		; [  1]
    		.dw	Task2		; [  2]
    		.dw	Task3		; [  3]
    		.dw	Task4		; [  4]
    		.dw	Task5		; [  5]
    		.dw	Task6		; [  6]
    		.dw	Task7		; [  7]
    		.dw	Task8		; [  8]
    		.dw	Task9		; [  9]
    		;...
    		;.dw	Task255		; [255] последняя возможная задача, зарегистрированная в системе
    ;***** END "RTOS Tasks" section
  5. Ещё, по логике работы прикладных Задач, некоторые из них требуется стартануть «пинком» в первый раз (а дальше, «шарманка» уже закрутится: запуская следующие Задачи, при отработке предыдущих шагов):
    ;***** BEGIN "Run once" section (запуск фоновых процессов) *****************
    ; Инициализация и первичный запуск "Кооперативных Задач"
    RCALL		Task2		; Обрати внимание на то, что прямому RCALL мы указываем на метку, 
    PLAN_TASK	TS_Task3	; а через API мы передаем идентификатор задачи.
    ;***** END "Run once" section

Пояснение прикладной логики:

  • Как мы видим, здесь, работают две основные параллельные Нити: «Task2» (каждую 1сек, наращивает Счётчик и выводит его значение в Порт) и «Task3» (обработка ввода через пользовательскую «кнопку №1» на PD6). Напомню: статус Кнопки проверяется каждую 1мс, но Задача «Task3» запускается только если кнопка нажата.
  • При нажатии «кнопки №1», Нить образованная задачей «Task3» — запускает ещё одну Нить: образованную задачей «Task4», которая крутится пока кнопка удерживается, и реализует некую «прикладную реакцию системы» на нажатие кнопки. Здесь, такой реакцией является: форсирование запуска Задачи «Task2», что приводит к наращиванию Счётчика каждую ~1мс.
    При этом, всякий раз при запуске Задачи «Task2», благодаря функции «перепланирования Таймера в Пуле» — таймер нормального хода (посекундного) постоянно откладывается… так что, когда «кнопка №1» будет отпущена, то естественный ход Счётчика восстановится сам собой.
  • Нить «Task4» — это очень подлая Задача. Наверное, она представляет собой пример неправильно организованной логики, приводящей к отказам типа DOS, вследствие недостатка ресурсов…
    • Нить «Task4» воспроизводится подобно и двум другим Задачам, рассмотренным ранее: запускает сама себя, при каждой отработке тела (включая простую проверку: пока не будет отпущена «кнопка №1»). Но здесь, есть важное отличие: Задача «Task4» запускает себя рекуррентно не через «Таймерное событие», и не через «Выжидатель» — а она запускает себя «мгновенно», через PLAN_TASK, что эквивалентно: «Вызову функцией самой себя!»

      По аналогии: Что при этом происходит в высокоуровневых языках программирования? Рекуррентные вычисления могут быть реализованы: Рекурсивно или Итеративно… В первом случае, возникает Бесконечная рекурсия — в итоге, переполнение Стека, и крах с Exception… Во втором случае, последствия чуть мягче: краха Стека не происходит, но зацикливание и зависание — неизбежны!

    • В системе RTOS (здесь) не используется прямой RCALL, а запуск Задачи производится всё-таки через «Очередь Задач» (через PLAN_TASK) — поэтому Стек не переполняется, краха системы не происходит (Profit!). Однако, «Очередь Задач», от такой флудилки, безнадёжно переполняется! Поскольку, пока «Диспетчер очереди Задач» успевает отработать только одну Задачу, в очередь добавляется больше одной Задачи:

      заметил, что ещё это называют: «fork-бомба». Хотя, это не POSIX система, здесь нет полноценных Thread, и Задачи порождаются не системным вызовом fork()… но аналогия та же.

      Пока «Task4» крутится, очередь равнозначно замещается новыми задачами: сколько отработалось, столько и добавил. Но дело в том что, кроме «Task4», существует ещё «Таймерная Служба», в которой периодически срабатывают Таймеры или Выжидатели — при этом, они также добавляют свои Задачи в очередь, на выполнение. Так что, очень быстро, очередь переполняется и уже не может принять новых Задач — некоторые запросы PLAN_TASK игнорируются (вы можете увидеть это на программной симуляции, в «AVR Studio», поставив breakpoint на ловушку «RTOS_METHOD_ErrorHandler»)…

      Анализ: Почему же, уже прям сейчас, система не коллапсирует? Исключительно, от удачи: в программном коде «Task3», я случайно написал инструкцию «PLAN_TASK TS_Task2» ДО инструкции «PLAN_TASK TS_Task4». Что в конечном итоге, приводит к тому что, шарманка кодов, бегающих по очереди задач, разбавляется одним кодом «Task2 или Task3» (все остальные позиции, до отказа, заполнены кодом Task4). Периодически, возникает «переполнение очереди», но вытесняется именно код Task4 (это не заметно, потому что этой Задачей и так очередь забита под завязку).
      Если изменить код «Task3»: и прописать инструкцию «PLAN_TASK TS_Task2» ПОСЛЕ инструкции «PLAN_TASK TS_Task4», то система сколлапсирует прямо сейчас! Но это было бы не интересно… Хорошо, что она именно так глючит — мы имеем почти упавшую систему, но она ещё держится на грани.

    • Чтобы окончательно добить такую Систему: в Задачу «Task4» была добавлена ещё одна инструкция «PLAN_TASK TS_Task4» (управляемая «кнопкой №2» на PD5) — теперь, заполнение очереди идёт в прогрессии: +2-1+2-1… И очередь задач переполняется уже надёжно: то единственное окошко, которое удерживалось кодом «Task2 или Task3» — моментально забивается, и более, пустых мест в очереди не будет.
      Сработавшие Таймеры и Выжидатели — игнорируются. Нити «Task2» и «Task3» — не смогут самоподдерживаться. Эти Задачи «выпадут из жизненного цикла», каждая, через период своего воспроизводства: «Task2» — через 1сек; «Task3» быстрее — через 1мс!
  • Вдобавок к вышеназванному, Задачи «Task_Idle» и «Task1» управляют «сигнальным битом» (PORTD4), отражающим состояние заполненности «очереди задач» (суть: если «Task_Idle» не запускается уже в течение целой миллисекунды — это верный признак того, что микроконтроллер перегружен задачами, и очередь никогда не опустошается)…

Вот такой вот глюкодром: «Приятного аппетита!» — как говорится. ;)

Короче, выводы:

  1. Чтобы запрограммированная вами система Задач жила, и была работоспособной — никогда не используйте «прямых рекуррентных цепочек». Т.е. никогда не устанавливайте циклические цепочки через PLAN_TASK! Обязательно, планируйте последующие запуски той же Задачи — через Таймер (PLAN_TIMER) или Выжидатель (PLAN_WAITER), т.е. отложенно (непрямое планирование). Шарманка должна успевать прокручиваться и обрабатывать наваленные Задачи…
  2. Планирование Задачи к «мгновенному запуску» (через PLAN_TASK) — предназначено только для порождения древесных (ациклических) зависимостей.
  3. Переполнение «Очереди Задач», «Пулов Таймеров» и «Выжидателей» — деструктивно, для логики многих Задач! (Если в некоторый момент МК будет перегружен, «Очередь Задач» переполнена, и тут сработает Таймер или просто вызывается метод PLAN_TASK из прикладного кода — то Задачу добавить некуда, она игнорируется!) ОС это не завалит, но может привести к сбоям или «зависаниям» отдельных Задач… Что делать? Используйте Ловушку Исключений «RTOS_METHOD_ErrorHandler», для выявления ситуаций переполнения, на этапе отладки устройства.

Исходники:

На этом, прошивка готова — можно заливать в микроконтроллер и тестировать…

А теперь — Слайды!

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