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

AVR
Для демонстрации основных возможностей и особенностей «Диспетчера задач 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		; поддержка Диспетчеризации Задач по Таймеру	(безусловно отложенный запуск на <X> миллисекунд)
    .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!). Однако, «Очередь Задач», от такой флудилки, безнадёжно переполняется! Поскольку, пока «Диспетчер очереди Задач» успевает отработать только одну Задачу, в очередь добавляется больше одной Задачи:
      Vga заметил, что ещё это называют: «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», для выявления ситуаций переполнения, на этапе отладки устройства.


Исходники:

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

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


  • 0
  • 15 февраля 2014, 22:44
  • Celeron
  • 4

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

RSS свернуть / развернуть
Позволю себе небольшую ремарку.

Что при этом происходит в обычных [функциональных] языках программирования? Бесконечная рекурсия! И итоге, переполнение Стека, и крах с Exception…
Языки программирования здесь не причем. У Вас в примере нет рекурсии — есть просто «отложенное выполнение». Это как обработчик таймера, который перенастраивает таймер, который вызовет тот-же обработчик… Это не рекурсия, и ЯП здесь не причем.

Ели бы у вас была именно рекурсия (вызов «задачи» самой себя через CALL) — то Вы бы получили переполнение стека, как и в других ЯП.

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

P.S. А почему Вы считаете, что функциональные языки «обычные»? Я бы сказал что они скорее «специфические».
+1
  • avatar
  • e_mc2
  • 16 февраля 2014, 18:23
Данная цитата — это абстрактный авторский пример. К теме статьи относится очень опосредованно. Темы «языков программирования», их преимуществ и особенностей, я здесь не касался.
0
Не следует трактовать отдельные фразы столь буквально. Своими построениями я стремлюсь вызвать у читателей некоторые ассоциации, через которые донести то, что я хочу передать.
0
И чего ты хочешь донести до читателя сомнительными ассоциациями? К рекурсивному вызову эта штука имеет довольно мало отношения. Обычная форк-бомба.
+1
что эквивалентно: «Вызову функцией самой себя!» (А что происходит в обычных [или функциональных] языках программирования, при [безусловном] вызове функцией самой себя? Бесконечная рекурсия! И итоге, переполнение Стека, и крах с Exception...)
0
У тебя не рекурсия, а цикл. И тем более совершенно ни при чем тут стек. Также, этот диспетчер спокойно переваривает таск вида

void SomeTask(){
  DoSomething();
  AddTask(idSomeTask);
}

Проблемы возникают только если таск будет планировать себя более чем один раз за запуск, при этом получается классическая форк-бомба. Ну или если диспетчер обрабатывает задачи не в порядке их постановки в очередь.
+1
Диспетчер-то переварит… Но такой Таск — не даст Очереди опустеть никогда! (Потому что будет замещать обработанные вызовы равнозначным количеством запланированного наперёд.) Но ведь, не только SomeTask является источником событий в системе, не так ли?
0
Но такой Таск — не даст Очереди опустеть никогда!
И что? Это не проблема.
0
Другие события добьют очередь — вот это проблема.
0
Очередь должна быть рассчитана на требуемое число задач — это раз.
Зацикливающаяся задача занимает только один слот в любой момент времени, это два. Больше она займет только при коэффициенте размножения более единицы, тогда это уже форк-бомба. Такое возможно, если циклящаяся задача добавляет себя в очередь более одного раза за запуск или если ее добавляют в очередь другие задачи (не считая первого добавления, которое и запускает задачу-цикл).
+2
или если ее добавляют в очередь другие задачи (не считая первого добавления, которое и запускает задачу-цикл)
Да, вот именно это и происходит в этом [неправильно спроектированном] алгоритме! От этого я и хочу предостеречь будущих программистов.
0
При этом ты предостерегаешь не от создания форк-бомб, а от зацикливания — которое вполне имеет право на жизнь (в частности, в задаче такого типа можно обсчитывать нечто в свободное от всех остальных задач время).
+1
Но тут пришёл Vga и всё разъяснил. Хорошо, убедил! Статью подкорректировал… ;)
0
Согласен с Vga , не стоит так использовать сомнительные ассоциации. Иначе текст становится «маркетинговым», как в рекламе :)

она запускает себя «мгновенно», через PLAN_TASK, что эквивалентно: «Вызову функцией самой себя!»

Вот ни разу, с точки зрения программирования, даже уловное «мгновенно» не эквивалентно «Вызову функцией самой себя!». Да и аналогия рекурсией в других ЯП притянута за уши. Тем более, к функциональным ЯП. На сколько я помню (здесь я не специалист) эти языки умеют «по особому» оптимизировать рекурсию, по возможности сводя рекурсию к итерациям.
+1
с точки зрения программирования...
А с точки зрения «абстрактного математического моделирования» — я вижу здесь рекурсию. Ведь Задачи для RTOS (на её фундаменте) проектируются уже больше на «абстрактно-математическом» уровне… Понимаете?

Я описываю особенности абстрактного API, чтобы не вникая в подробности работы ядра RTOS, можно было избежать граблей, при проектировании кода Задач.
0
* Точнее: я увидел, в том моменте, аллюзию на рекурсию…
А в ситуации «переполнения Очереди» — аллюзию на «переполнение Стека». Ибо это обе: «динамические структуры данных», хотя и разной алгоритмической организации (одна FIFO, другая LIFO)…
0
Это бесконечный цикл, а не рекурсия. И проблему исчерпания ресурса он, в отличие от бесконечной рекурсии не вызывает. Очередь переполнится только если сделать форк-бомбу, т.е. коэффициент размножения тасков будет более единицы.
Он может вызвать разве что проблему «зависания», если работает на высшем уровне приоритета (к твоей системе это не относится) или если очередь задач работает по принципу FILO, а не FIFO.
+1
А с точки зрения «абстрактного математического моделирования» — я вижу здесь рекурсию.
Нет. Это не рекурсия. Даже с точки зрения «абстрактного математического моделирования».

Если функция вызывает сама себя — это рекурсия. Если, например, мы реализовали рекурсивный алгоритм «обхода дерева» и функция «вызвала сама себя» 100 раз — это рекурсивный алгоритм, это именно рекурсия.

А если мы просто вызвали 100 раз одну и ту-же функцию в цикле — это просто цикл.

Цикл и рекурсия — это принципиально разные вещи (они имеют свои плюсы и минусы), но никак нельзя назвать цикл «эквивалентом рекурсии».

Ведь Задачи для RTOS (на её фундаменте) проектируются уже больше на «абстрактно-математическом» уровне… Понимаете?

Честно говоря, не очень.
То, что я увидел из цикла статей (с точки зрения математики) — это все тот же граф состояний. Только реализация отличается от «классической» реализации автомата состояний (и, сугубо ИМХО, не в лучшую сторону).
+1
Коллега, извиняюсь за занудство, но где-то мы неправильно друг друг-дуга понимаем. Возможно, я некорректно понимаю отдельные моменты Вашей статьи.

У вас есть диспетчер задач, который реагирует на «неправильное» поведение Task4 определенным образом. Это (в данной реализации) на вырывает зацикливания или переполнения стека. Но это «заслуга» реализации а не языка.

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


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

И почему Вы так настаиваете на функциональных языках? ИМХО, они не актуальны для программирования МК. Может Вы перепутали со структурными языками?
0
Кажется, Celeron'ова привычка отвечать самому себе заразна…
0
они не актуальны для программирования МК
Да, я совсем не настаиваю ни на каких языках. И действительно, для программирования МК нужны императивные ЯП! Просто, я — профессиональный программист, и мне аналогии приходят в образах языков программирования…

Если вам это не даёт покоя — ок, я заменю:
в обычных [функциональных] языках
на
в высокоуровневых языках


По теме «функционального программирования»:
В функциональных языках цикл обычно реализуется в виде рекурсии...
тыц, тыц, тыц, тыц.
0
CELERON, Извините! У меня как в старом анекдоте — мужики а ничего что я к Вам спиной сижу (вопрос водителя такси). Я о том что проза моего вопроса может повергнуть в стресс сообщество. Вопрос к автору. Что нужно чтобы этот пример (для начала) мне запустить на плате с «Mega 2560» (там кажись есть даже название ARDUINO, но оно мне сейчас ненужно)? Кроме как изменения подключения по ножкам кнопок и индикатора?
Ну нет у меня других более мелких плат! А на этой я естественно хочу потом в данную ось вставить свои примеры. Запустить дисплей например и т.д.
0
Рецепт!Чтобы портировать проект на другой микроконтроллер, нужно сделать всего две вещи:
1) Включить определение HAL нужного микроконтроллера:
.include "m2560def.inc"
2) И изменить под него Таблицу векторов на обработчики прерываний:
.include "ivectors_mega2560.inc"

Теорию «что? как? зачем?» — см. в учебном курсе DI HALT'а: «Скелет программы» и «Подпрограммы и прерывания»… Кстати, эти вопросы уже выходят за рамки RTOS и касаются только «Шаблона проекта». Код RTOS, в этом плане, универсален. Для создания прошивок к AVR без RTOS — существует аналогичный пустой «Шаблон нового проекта».

Я портировал: Тот же проект, на последнем релизе RTOS v2.1, и для микроконтроллера ATmega2560: «MaketboardTest_ATmega2560_RTOSv2.1.zip». (У меня нет такой железки, но симуляция в «AVR Studio» работает правильно.)

p.s. Наконец-то, в тему, кроме болтунов-теоретиков, пришли экспериментаторы — я приветствую это! :)
0
«Болтуны-теоретики» давно уже перетащили этот диспетчер на С, опробовали и разочаровались.
0
Ну, раз опробовали — уже не только «теоретики», а также и «экспериментаторы»… Значит, в теме были — «просто болтуны»! ;))

В любом случае: каждое замечание или вопрос — это обратная связь, реакция аудитории — значит, полезно!

Меня этот «Диспетчер задач RTOS» почему увлёк? Понадобилось совместить на одном МК параллельные задачи: Динамическую индикацию, Опрос кнопок (интерфейс), и Отработку основного цикла (прикладной логики) в фоне… Эти задачи решаются также на самых слабых МК (и на ассемблере, конечно). Поэтому, надеюсь, что такая RTOS найдёт свою нишу применения. Позиционирование данной RTOS — я так, сразу, и описал: в разделе «Назначение», головной статьи...
А на Си и на мощных микроконтроллерах — думаю, от такого ограниченного «Диспетчера» будет, действительно, мало толку. Незачем, поскольку там есть целый сонм полноценных кооперативных и вытесняющих RTOS…
0
Распараллелить задачи можно и без подобного диспетчера, и в принципе без RTOS вообще — но RTOS сильно облегчает эту задачу. Но вот как раз в деле облегчения конкретно этот диспетчер скорее дурную службу служит, разбивая задачу на кучу тасков с неясными связями между ними. Диспетчеры с потоками в этом плане куда удобней. В свое время я думал над скрещиванием этого диспетчера и protothreads, но возникает вопрос — а нужен ли тогда этот диспетчер.
Тем не менее, у меня есть проектик с такой недортос.
0
Глянул на «Protothreads» — интересная штука! (лёгкие и кроссплатформенные) Но ничего конкретного про них, пока, сказать не могу — это нужно поработать с ними всерьёз, в рамках практического проекта…

Сложилось предварительное соображение: что функционал «Диспетчера RTOS» прототридам — бесполезен, как «пятая нога», потому что в них уже есть механизм «дочерних тридов» (как замена дроблению Задач):
void PT_SPAWN(struct pt *pt, struct pt *child, thread);
Spawn a child protothread and wait until it exits.

void PT_WAIT_THREAD(struct pt *pt, thread);
Block and wait until a child protothread completes.

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

В архиве с исходниками есть файл «example-codelock.c» — там предлагается некая реализация примитивной «Таймерной службы», но она катит только для WIN32/POSIX систем, потому что опирается на «system clock»:
struct timer { int start, interval; };

static int timer_expired(struct timer *t)
{ return (int)(clock_time() - t->start) >= (int)t->interval; }

static void timer_set(struct timer *t, int interval)
{ t->interval = interval; t->start = clock_time(); }

Хотя, идея интересная! Можно, например, выделить в памяти микроконтроллера 32-битный счётчик, и наращивать его каждую миллисекунду… А в вышеприведенных методах, вместо вызова clock_time() — сравнивать со значением счётчика. Profit!
0
А вот «Таймерной службы» — в прототридах не хватает, да…
Вот примерно по этой причине и хотел скрестить. Прототредс позволяет превратить таск такого диспетчера в более-менее полноценный поток, правда, есть риск его утери при переполнении очереди.
но она катит только для WIN32/POSIX систем, потому что опирается на «system clock»
Не только. В кортексах есть системный таймер примерно для тех же целей. Да и где угодно его не проблема завести — мой вариант этого диспетчера ведет такой счетчик времени.
0
Прототредс позволяет превратить таск такого диспетчера в более-менее полноценный поток
«Мсье знает толк в извращениях!» (с)
Мне сложно вообразить как это можно сделать… Но лучше так не делать. ИМХО, с Прототридами — лучше воспользоваться «таймерным костылём», предложенным авторами и упомянутым выше. Совместно с самодельным «счётчиком времени», вместо clock_time().

Между «Диспетчером задач» и «Прототридами» — гораздо больше различий, чем аналогий:
* Нити прототридов — это как детерминированные «верёвки». А Задачи диспетчера — это как «бусины бисера» (они настолько фрагментарные, по сравнению с Нитями, что их следует рассматривать как «вышивку» с многочисленными «сетевыми связями»).
* В Диспетчере: Таймеры управляют/запускают Задачами (а Таймеры безличны). Но в Прототридах, наоборот: Нити управляют/проверяют Таймерами (и Таймеры выделенные, идентифицируются адресом). Соответственно, у их Таймеров разные не только движки, но и интерфейсы заточены под совсем разные модели
* В «Диспетчере задач» всё заточено под оптимизацию использования памяти, минимизацию вычислений, уменьшение объёма кода. А Прототриды — компромисны: наперёд всего «гибкость» и прозрачность кода (простота), а «лёгкость» (малый объём кода и стека) — желательна, но не в ущерб первому…
В общем, как-то так.
0
Мне сложно вообразить как это можно сделать… Но лучше так не делать. ИМХО, с Прототридами — лучше воспользоваться «таймерным костылём», предложенным авторами
Нет, на самом деле именно на это прототреды и рассчитаны — интеграцию с внешним планировщиком потоков. Дихальтов диспетчер вполне подходит.
И прототреды позволяют избавиться от главного его недостатка — разбиения задачи на мелкие и не особо логичные куски.
Нити управляют/проверяют Таймерами (и Таймеры выделенные, идентифицируются адресом)
Это только один из вариантов. Есть и другие, более оптимальные — задействовать средства планировщика, например.
а «лёгкость» (малый объём кода и стека) — желательна, но не в ущерб первому…
Возможно, ты плохо читал описание прототредов, но там определенно написано:
Protothreads are a extremely lightweight, stackless threads
И, в общем, учитывая оверхед в 2 байта на поток — я склонен с этим утверждением согласиться.
0
Возможно, ты плохо читал...
Ну, не всё-то слово в слово пишется. :) Тут, пока не попробую, пока не разберусь в тонкостях реализации (какой там ещё объём служебного кода) — сравнить не смогу. Я сейчас ещё не готов обсуждать Прототриды…
0
Как такового служебного кода в PT нет. Это набор хитрых макросов, встраивающихся в твою функцию. Кода там вставлется весьма немного.
Но это, разумеется, только С и С++.
0
Можно, например, выделить в памяти микроконтроллера 32-битный счётчик, и наращивать его каждую миллисекунду… А в вышеприведенных методах, вместо вызова clock_time() — сравнивать со значением счётчика. Profit!

Можно, но не все так просто. Возникнут проблемы с атомарностью операций чтения и инкремента счетчика (если мы его инкрементируем из порывания). «В лоб» (через неатомарное чтение переменной счетчика из памяти), работать с таким счетчиком нельзя.
0
«В лоб» работать с таким счетчиком нельзя.
Я думаю, что ОС на таком счётчике — может работать только на 32-битный микроконтроллерах. Там, где (int) также 32-битный. Иначе, возникает слишком много побочных эффектов, большой оверхед. Хочу уточнить (я не в курсе, ещё не работал): в 32-битных микроконтроллерах же есть атомарные операции с памятью читать/писать 32-битные числа?

Т.е. для 8-битных микроконтроллеров такая система «Прототридовских Таймеров» не подойдёт (плохо втуливается). Если рассчитывать на «атомарные нативные операции», то даже 24-битные микроконтроллеры — ещё мало (max_int = 2^24mc = 4.66часов, а потом наступит переполнение счётчика, а у таймеров «проблема 2000»).

Для 32-битных МК, уже лучше: max_int = 2^32mc = 49суток (т.е. раз месяц, как минимум, МК обязан быть перезагружен! можно реализовать специальным watchdog-ом)

p.s. Эти свойства — есть побочный эффект того, что рассмотренная выше «таймерная служба» опирается на один централизованный счётчик. В «Диспетчере задач RTOS» же: у каждого Таймера свой счётчик. Но эти счётчики приходится все декрементировать, каждую миллисекунду — там счётчики «активные»: пропускать циклы декрементации нельзя! За счёт этого, возникает некий вычислительный оверхед (требуется больше процессорного времени и кода). Но на 8-битных МК — иначе нельзя (вернее, можно, но ещё сложнее).
0
Хочу уточнить (я не в курсе, ещё не работал): в 32-битных микроконтроллерах же есть атомарные операции с памятью читать/писать 32-битные числа?

Да, есть. И на 32-битной архитектуре, по идее доступ к такой переменной будет атомарен. «По идее» ибо есть некоторые специфические нюансы архитектур и компиляторов.

Вот, немного вырожденный пример для ARM7 когда доступ к 32-битной переменой не является атомарным.

#pragma pack(push, 1)
typedef struct {
        int some_int;
} some_struct;
#pragma pack(pop)

int foo(some_struct * bar) {
        return bar->some_int;   
}
arm-none-eabi-gcc.exe -mcpu=arm7tdmi -Os -Wall -c test.c

Вот что сгенерил компилятор:
00000000 <foo>:
   0:   e5d02001        ldrb    r2, [r0, #1]
   4:   e5d03000        ldrb    r3, [r0]
   8:   e1833402        orr     r3, r3, r2, lsl #8
   c:   e5d02002        ldrb    r2, [r0, #2]
  10:   e5d00003        ldrb    r0, [r0, #3]
  14:   e1833802        orr     r3, r3, r2, lsl #16
  18:   e1830c00        orr     r0, r3, r0, lsl #24
  1c:   e12fff1e        bx      lr


В данном случае компилятор не знает выровнена ли some_int на границу 4 байт, поэтому читает ее побайтно. Ели убрать #pragma pack(push, 1), то доступ будет атомарен

00000000 <foo>:
   0:   e5900000        ldr     r0, [r0]
   4:   e12fff1e        bx      lr
0
Для 32-битных МК, уже лучше: max_int = 2^32mc = 49суток (т.е. раз месяц, как минимум, МК обязан быть перезагружен!
На самом деле, в большинстве задач переполнение таймера проблемой не является. В том числе и для подобных макросов. Нужно только писать условия с учетом этого (и чтобы они не превращались в развесистое дерево if'ов — использовать особенности дискретной арифметики).
Кроме того, атомарно вычитать 32-битный счетчик на AVR немногим медленнее, чем неатомарно. Правда, для этого придется кратковременно глушить прерывания.
0
Там, где (int) также 32-битный. Иначе, возникает слишком много побочных эффектов, большой оверхед.
Огромное число эффектов — автор жжёшь!!!
я не в курсе, ещё не работал
Видно что не в курсе.
т.е. раз месяц, как минимум, МК обязан быть перезагружен!
Откуда такое требование?! С чего это вдруг — обязан?
Программы не получается без глюков писать? Поэтому постулируем перезагрузку раз в месяц?
можно реализовать специальным watchdog-ом
Можно если нужно. Если не нужно то программа должна годами работать.
За счёт этого, возникает некий вычислительный оверхед
Выбирать МК нужно соответственно задаче. Тогда не нужно будет с оверхедами на ровном месте боротся…
Но на 8-битных МК — иначе нельзя
Не слишком категорично?
0
Спасибо! Предлагаю перевести диспут в конструктивную форму — направить силы на обучение менее грамотного представителя в моем лице, в профессиональной подготовке всех присутствующих я не сомневаюсь и конкурировать не собираюсь. Могу ли я автора спросить по делу, если будут трудности при эксперименте!? Обещаю сильно не доставать. И может лучше в личку?
0
Будут вопросы — пиши. У нас тут, конечно, научат — главное, успевай пригибаться да уворачиваться.
Я в электронике слабоват (например, LCD-дисплеи ещё не крутил), а больше — по программно-алгоритмической части… Поэтому задавай вопросы здесь (без обиняков) — кто захочет, ответит.
0
Спасибо! Прорвемся… В свое время в наладке народ описание на аппаратуру прятал от конкурентов новичков (было и такое) — это тем кто о совке мечтает и думает что там было самое гуманное общество — гуманное к дуракам и бездельникам! Приходилось рано вставать и приходить первым и уходить последним, чтобы что0то понять…
0
Автор топика запретил добавлять комментарии