Пояснення прикладу 1-EventFlag
Як працює цей приклад scmRTOS і що взагалі там відбувається?
Ілюстроване пояснення.
Пояснення базується на avr-gcc (WinAVR) порті системи для мікроконтролерів AVR, але може бути корисним для розуміння будь-якого порта системи.
Для детальної демонстрації процесів, що відбуваються в системі, приклад 1-EventFlag дещо розширено. В функції Exec()
процесів, IdleProcessUserHook()
, SystemTimerUserHook()
, TIMER1_COMPA_vect()
(файл main.cpp) а також в код перемикання контекстів (файл OS_Target_asm.S) додано команди зміни стану виводів мікроконтролера.
Виводи, що керуються з коду користувача scmRTOS, призначаються у файлі main.cpp:
55 56 57 58 59 60 61 62 63 64 65 | //------------------------------------------------------------------- // "Hello, scope!" pins (pin numbers for ATmega168 in DIP28 package) // Define pins in form PORT_LETTER,PORT_PIN,ACTIVE_LEVEL // (Ascold Volkov - type "pin_macro.h" notation) #define TIMER1_ISR_PIN C,4,H // pin 27 #define T1PROC1_PIN B,0,H // pin 14 #define PROC1_PIN B,1,H // pin 15 #define PROC2_PIN B,2,H // pin 16 #define PROC3_PIN B,3,H // pin 17 #define TIMERHOOK_PIN B,4,H // pin 18 #define IDLEHOOK_PIN B,5,H // pin 19 |
Вивід контролера для керування з перемикача контекстів визначається в makefile:
11 12 13 | # Define contect switch "Hello, scope!" pin in form PORT_LETTER,PORT_PIN # C,5 for pin5 of PORTC SWITCH_PIN=C,5 |
Виводи підключено до логічного аналізатора. Контролер тактується від внутрішнього RC-генератора на 8 МГц, один тік системного таймера відповідає приблизно двом мілісекундам.
Приклади в репозиторії scmRTOS та її код можуть змінюватися. Крім того, перемикач контекстів в робочому коді не може містити команд керування виводами мікроконтролера, їх прибрано з коду в репозиторії. Звідси можна завантажити архів 1-EventFlag-explanation.zip з модифікованою scmRTOS та прикладом, які відповідають пропонованому тексту.
Приклад розраховано на компілятор avr-gcc (WinAVR) та на мікросхему з лінійки ATmega48/ATmega88/ATmega168/ATmega328, для інших мікроконтролерів може знадобитися внести зміни відповідно до використовуваних таймерів та виводів.
Розподіл входів аналізатора
D5 | IDLEHOOK |
Перекидається у протилежний стан в
IdleProcessUserHook() . |
---|---|---|
D4 | TIMERHOOK |
Перекидається у протилежний стан в
SystemTimerUserHook() |
D3 | PROC3 |
Піднімається та опускається в
TProc3::Exec() . |
D2 | PROC2 |
Піднімається та опускається в
TProc2::Exec() . |
D1 | SWITCH |
Піднімається першою командою перемикача контексту, опускається перед виконанням команди виходу з перемикача (безпосередньо перед
reti ). |
D0 | T1PROC1 |
Піднімається в прериванні
TIMER1_COMPA_vect() , опускається в функції TProc1::Exec() , яка чекає сигналу від цього переривання. |
Взаємна робота Proc2 та Proc3 — з пташиного польоту
Параметри компіляції прикладу:
main.cpp
86 | #define PROC2_LONG_PULSE 1 |
Proc2 генерує імпульс довжиною в один тік системного таймера. Всі інші параметри компіляції при вивченні системи в такому масштабі неважливі.
-
Сигнал IDLEHOOK, який є меандром з періодом в декілька мікросекунд з короткими перервами на роботу процесів, в цьому масштабі відображається суцільною смугою.
-
Оскільки значення сигналу TIMERHOOK інвертується в функції
OS::SystemTimerUserHook()
, на відповідній лінії спостерігається меандр з періодом в два тіки системного таймера, близько чотирьох мілісекунд. Події на лініях PROC2 та PROC3 відбуваються синхронно зі зіміною стану на лінії TIMERHOOK. -
В функції
TProc2::Exec()
є два виклики функції засинання просесу:Sleep(1)
визначає довжину імпульса на лінії PROC2,Sleep(9)
визначає час між імпульсами. Таким чином, Proc2 є мультивібратором з періодом імпульсів 10 тіків системного таймера (20 мс) та довжиною імпульсу 1 тік (2 мс). -
Перед опусканням лінії PROC2 та засинанням на десять тіків Proc2 викликом
ef.Signal();
пробуджує Proc3. Отримавши керування,TProc3::Exec()
піднімає рівень на лінії PROC3 та знову засинає, тепер по викликуSleep()
. Час сну вибирається виходячи з стану біту 9 лічильника системних тіків, отриманого викликомOS::GetTickCount();
. Протягом 512 тіків таймера (близько секунди) процес спить два тіки таймера, протягом іншої секунди сон триває три тіки таймера. -
Прокидаючись після сну по системному таймеру Proc3 опускає сигнал на лінії PROC3 та викликом
ef.Wait();
знову засинає в очікуванні сигналу від Proc2. Фактично Proc3 є одновібратором, що запускається по спаду імпульса на лінії PROC2. -
Імпульси на лінії T1PROC1 не синхронізовані з системним таймером, бо вони породжуються в прериванні
TIMER1_COMPA_vect()
, яке активується з частотою, не кратною системному таймеру. Детальніше робота таймера 1 та Proc1 розглянута нижче. -
На лінії SWITCH бачимо маркери перемикання процесів. Частина з них відповідає перепадам на лініях PROC2 та PROC3, частина — на лінії T1PROC1.
Перемикання з Proc2 на Proc3 — під мікроскопом
Параметри компіляції OC та прикладу (спільна частина для обох варантів розвитку подій):
scmRTOS_CONFIG.h
124 | #define scmRTOS_CONTEXT_SWITCH_SCHEME 1 |
Перемикання контекстів виконується за допомогою переривання найнижчого рівня пріоритету, запит якого формується програмно. Перемикання, заплановане в результаті обробки інших преривань, починає працювати після виходу з цих преривань.
main.cpp
86 | #define PROC2_LONG_PULSE 0 |
Для детального розгляду з коду Proc2 видалено затримку в один тік системного таймера при генерації імпульсу на лінії PROC2. Довжина імпульсу тепер визначається часом роботи функції ef.Signal()
та, у варіанті 2, часом перемикання задач та роботи Proc3.
Варіант 1
Додаткові параметри компіляції:
main.cpp
85 | #define PROC2_HIGHER_THAN_PROC3 1 |
Пріоритет Proc2 вищий, ніж Proc3.
-
При переході на підпрограму переривання від системного таймера припиняється генерація меандру на лінії IDLEHOOK в функції
IdleProcessUserHook()
. -
Після закінчення роботи системної частини підпрограми переривання викликається функція користувача
SystemTimerUserHook()
, в якій інвертується стан лінії TIMERHOOK. -
Відбувається вихід з підпрограми переривання системного таймеру та вхід в переривання перемикання контексту. Між двома перериваннями може виконатися одна команда з коду перерваного процесу. На цій осцилограмі трапилося так, що цією командою було інвертування стану лінії IDLEHOOK.
-
Перемикання контексту, яке передає керування Proc2 в точку виходу з виклику
Sleep(9)
. На початку перемикання піднімаєтьcя рівень на лінії SWITCH, по закінченні перемикання лінія SWITCH повертається до низького рівня. Довжина імпульсу показує час, необхідний власне для перемиканя контексту, без врахування витрат часу на диспетчеризацію. -
В функції
TProc2::Exec()
піднімається лінія PROC2, викликаєтьсяef.Signal()
і опускається лінія PROC2. По довжині імпульсу можна визначити час, необхідний для виконання функціїSignal()
. ТеперTProc2::Exec()
робить викликSleep(10)
і засинає, планувальник scmRTOS шукає готовий до виконання процес. Таким процесом єTProc3
, який щойно отримав сигнал. -
Відбувається чергове перемикання процесів (другий імпульс на лінії SWITCH), керування отримує
TProc3::Exec()
. -
Proc3 піднімає лінію PROC3, зчитує значення системного лічильника тіків і засинає на відповідний час, як це описано в розділі «Взаємна робота Proc2 та Proc3 — з пташиного польоту».
-
Знову перепланування процесів і, оскільки інших готових до виконання процесів немає, керування передається на
IdleProcess
(третій імпульс на лінії SWITCH). -
Керування отримала функція
TIdleProcess::Exec()
, відновлюється перекидання лінії IDLEHOOK. Довжина паузи в меандрі показує повний час на обробку апаратного переривання, перепланування процесів, перемикання контекстів та виконання процесами своєї роботи, перемикання назад на процесIdleProcess
.
Перемикання процесів відбувається по шляху IdleProcess → Proc2 → Proc3 → IdleProcess, разом три перемикання процесів.
Варіант 2
Додаткові параметри компіляції:
main.cpp
85 | #define PROC2_HIGHER_THAN_PROC3 0 |
Пріоритет Proc2 нижчий, ніж у Proc3. Події розгортаються схожим на «Варіант 1» чином, але є відмінності після виклику ef.Signal()
в TProc2::Exec()
.
Осцилограми змінюються наступним чином:
-
При переході на підпрограму переривання від системного таймера припиняється генерація меандру на лінії IDLEHOOK в функції
IdleProcessUserHook()
. -
Після закінчення роботи системної частини підпрограми переривання викликається
SystemTimerUserHook()
, в якій інвертується значення лінії TIMERHOOK. -
Перше переривання перемикання контексту передає керування Proc2 в точку виходу з виклику
Sleep(9)
. -
В функції
TProc2::Exec()
піднімається лінія PROC2, викликаєтьсяef.Signal()
. І тут, на відміну від першого варіанту, повернення з викликаної системної функції дещо затримується. В даному варіанті пріоритет Proc3 вищий, ніж Proc2, тому перепланування в кінціef.Signal()
активує перемикання на Proc3. -
Перемикання процесів (другий імпульс на лінії SWITCH), керування отримує
TProc3::Exec()
. -
Proc3 піднімає лінію PROC3, зчитує значення системного лічильника тіків і засинає.
-
Залишається ще один активний процес — Proc2, контекст перемикається назад на
TProc2::Exec()
(третій імпульс на лінії SWITCH). -
І от лише тепер відбувається «повернення» в
TProc2::Exec()
з функціїef.Signal()
. Нарешті опускається лінія PROC2. На цій осцилограмі довжина імпульсу на лінії PROC2 набагато більша, ніж для попереднього варіанту, в нього ввійшли робота Proc3 та два перемикання процесів.
ТеперTProc2::Exec()
знову робить викликSleep(9)
, scmRTOS шукає готовий до виконання процес. Але для даного варіанту ним є не Proc3, він вже свою роботу зробив, а фоновий процесIdleProcess
. -
Останнє перемикання контекстів, керування передається на
TIdleProcess::Exec()
. -
Керування отримала функція
TIdleProcess::Exec()
, відновлюється перекидання лінії IDLEHOOK.
При такому співвідношенні пріоритетів виконання іде по шляху
IdleProcess → Proc2 → Proc3→ Proc2 → IdleProcess
Маємо чотири перемикання процесів.
З одного боку, роботу IdleProcess
було перервано на довший час. В реальній системі перерваним міг би бути процес, який виконував би кориснішу роботу, ніж перекидання стану IO-ніжки у високому темпі.
З іншого боку, Proc3 отримав керування дещо раніше. Знову ж таки, в реальній системі, де між відправкою сигналу за допомогою ef.Signal()
та засинанням Proc2 робив би більше роботи, в першому варіанті затримка до початку роботи Proc3 була б ще більша.
Який варіант кращий, залежить від конкретних умов, від логіки роботи системи. Цей приклад лише показує, що від вибору пріоритетів процесів залежатимуть затримки від подій до передачі керування потрібному процесові та час роботи процесів.
Передача сигналу від переривання таймера 1 до процесу 1
Ця частина прикладу відрізняється від попередніх лише тим, що передача сигналу через OS::TEventFlag Timer1_Ovf;
відбувається не від одного процесу до іншого, а від підпрограми обробки переривання до процесу.
-
Як і на попередніх осцилограмах, меандр на лінії IDLEHOOK припиняється при виникненні переривання.
-
На вході в підрограму обробки переривання
TIMER1_COMPA_vect()
піднімається лінія T1PROC1, після чого викликомTimer1_Ovf.SignalISR();
посилається сигнал Proc1. Планувальник бачить необхідність перемикання процесів і активує запит переривання перемикання контекстів. -
Після виходу з переривання
TIMER1_COMPA_vect()
відпрацьовує переривання перемикання контекстів, керування отримуєTProc1::Exec()
. -
Proc1 опускає рівень на лінії T1PROC1 і знову засинає викликом
Timer1_Ovf.Wait();
до приходу сигналу. Довжина імпульсу на лініїT1PROC1
дає уявлення про мінімальний час затримки від апаратної події до того моменту, коли почне працювати код високопріоритетного процесу, що чекав на цю подію. -
В даному випадку інших готових до виконання процесів нема і другим перемиканням контекстів керування повертається до
IdleProcess
. -
TIdleProcess::Exec()
відновлює роботу і на лінії IDLEHOOK знову з’являється меандр.
© 2008-2010, Oleksandr Redchuk aka ReAl
Вы предлагаете все-таки установи scmRTOS_CONTEXT_SWITCH_SCHEME равным 1 и вернуть кодинициализации прерывания от компаратора?
Почему «всё таки» (это еще не «несмотря ни на что», но близко)? В крупном масштабе диаграммы уже немного зависят от способа организации переключателя контекста, поэтому я уточнил. Чаще использовал прерывание от SPM, а не от компаратора, так как бутлоадер не использовал, а компаратор или, по крайней мере, выводы микроконтроллера — нужны.
Заранее извиняюсь если не коректный вопрос. И не в тот раздел)Сколько человек здесь делятся опытом?Спасибо!
В последнее время — пол человека ± четверть.
Читаю тут комментарии через RSS. 🙂 Но сам сейчас чего-либо на AVR не конструирую, хотя остались всякие идеи и “замороженные проекты”. 🙂 Из последнего на тему “embedded” у меня были небольшие Python-скрипты для Raspberry Pi – эксперименты с http-запросами и подключением через GPIO.
Через блокнот WinAvr все оказалось просто. Нужно выбрать в главном меню блокнота File/New/Project и создать проект с любым именем в папке с примером. Затем подключить к проекту все фалы примера, в том числе и файл без расширения makefile. Далее открыть в блокноте makefile и выполнить команду из гланого меню блокнота Tools/[WinAvr]Make All. В результате должна появиться папка exe где и будет требуемый .hex файл для прошивки в контроллер.
По ходу песни возникли некоторые вопросы.
1.В чем всетаки разница между контекстом и процессом. Разве это не одно и то же?
2.И всетаки чем конкретно в данном примере переключаются контексты (речь идет о программном прерывании самого низого уровня). Где можно посмотреть как организовано это прерывание в тексте примера.
3. Получается что программа обработки прерывания по Timer1 это тоже процесс (он может быть короткий, а может быть длинный) или нет? Или программа обработки прерывания только передает управление нужному процессу? Так зачем передавать? Только трата времени. Разве не так?
Да оно и не должно было быть сложно 🙂
Просто я для всего под Windows ипользовал MED и не слазил бы с него, если бы не желание работать в кроссплатформенном редакторе. Сейчас болтаюсь между NetBeans и Code::Blocks, но, похоже, надо плыть в сторону Eclipse…
1. Процесс обслуживается операционной системой. У процесса есть код (в случае AVR — во флеше), есть, скажем так, «описатель» (переменная TProc). И есть все те внутренности ядра микроконтроллера, которые описывают текущее состояние микроконтроллера при выполнении данного процесса — текущий указатель команд, указатель стека, содержимое регистров общего назначения и статусный регистр SREG. Вот эти вещи и называются контекстом, их надо сохранять и восстанавлиать при переключении процессов. Для AVR/GCC порта указатель стека сохраняется в «описателе» процесса, а весь остальной контекст — на стеке процесса.
2. Контексты переключаются либо прямым программным вызовом
os_context_switcher()
, либо прерываниемCONTEXT_SWITCH_ISR_VECTOR:
в зависимости от настроек ОС для данного проекта. Код обеих переключателей находится в файле scmRTOS/AVR/OS_Target_asm.S, нужный вариант выбирается директивами условной компиляции.Обработчик прерывания — не процесс, а, в некотором смысле, «продолжение аппаратуры». По крайней мере, он не подчинён ОС, она не передаёт ему управление. А обработчик меняет состояние объектов ОС, в результате чего она может переключить процессы.
3. Вопрос о том, что делать в обработчике, а что поднимать на уровень процесса, решается для каждого проекта. В обработчике должен сидеть «драйвер» периферийного объекта или, по крайней мере, вся его низкоуровневая часть.
На мой взгляд, всю обработку состояний интерфейса стоит производить в обработчике, отдавая наверх событие «транзакция завершена». Процесс проснётся и обработает только результат успешно/неуспешно, подумает, что делать дальше.
Я бы сказал так — если действия по времени занимают менше пары-тройки переключений с процесса на процесс (одно переключение — две-две с половиной сотни тактов), то нет никакого смысла их поднимать на уровень процесса. Если больше десяти, то врядли стоит занимать столько времени, блокируя тем самым выполнение высокоприоритетных задач, а затраты на переключение уже не так и велики на фоне работы. Между этими границами переходная зона. Чем менее приоритетным процессам нужен результат работы прерываний, тем меньше времени в этом прерывании нужно проводить.
“Чем менее приоритетным процессам нужен результат работы прерываний, тем меньше времени в этом прерывании нужно проводить” – неплохо сказано.
Если не сложно, можно прокомментировать этот код из обработчика пр.таймера Т1
ENABLE_NESTED_INTERRUPTS();
Timer1_Ovf.SignalISR();
Объект
ISRW
— ISR Wrapper. Его создание просто заставляет отработать его конструктор на входе в обработчик и его деструкутор на выходе, а в них помещены те действия, которые нужны для интеграции прерывания в scmRTOS. Т.е. работа со счётчиком вложенных прерываний и запуск перепланировщика при выходе из прерываний в основной код.Далее разрешаются вложенные прерывания. Тут это не нужно, прерывание короткое, но надо же как-то продемонстрировать и проверять работоспособность.
После чего взводится событие таймера — помечаются как готовые все процессы, которые его ожидают.
В приведенном примере кода не хватает для комментирования очень важной строки 🙂
По ней заканчивается область определения переменной
ISRW
и отрабатывает деструктор объекта. В деструкторе вызывается планировщик ОС, просматривающий таблицу процессов и, в данном примере, сразу передающий управление на самый приоритетный процессTProc1
.Хороший пример. Очень хотелось-бы его запустить в реальном железе. Но проблема в том, что я, как и многие другие не знакомые с WinAvr, понятия не имеют как пример запустить на компиляцию в WinAVR. Информации по этому поводу в сети нет или она никуда не годится. Вроде все должно быть не сложно. Но кроме простого открытия файлов в WinAvr дело дальше не пошло. Если не сложно добавьте пожалуйста к статье последовательность как добавить и откомпилировать пример в WinAvr с получением .hex.
При установке WinAVR он прописывает в PATH два пути — к компилятору и к утилитам (из которых интересуют make и sh). Из командной строки всё становится доступным без лишних телодвжений.
Самый простой, по крайней мере для меня :-), путь сборки примера — зайти FAR-ом в каталог 1-EventFlag и набрать команду
make
.Идущим в поставке WinAVR редактором Programmers Notepad я никогда не пользовался, поэтому не могу сказать, как собирать пример из него.