Объяснение примера 1-EventFlag

Как работает этот пример scmRTOS и что вообще там происходит?
Иллюстрированное объяснение.

Объяснение основано на avr-gcc (WinAVR) порте для микроконтроллеров AVR, но может быть полезным для понимания работы любого порта scmRTOS.

Для демонстрации происходящих в системе процессов пример 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 генерирует импульс длиной в один тик системного таймера. Остальные параметры компиляции при изучении процессов в таком масштабе несущественны.


Proc2 and Proc3 live oscillogram

  1. Сигнал IDLEHOOK, являющийся меандром с периодом в несколько микросекунд с короткими перерывами на работу процессов, в данном масштабе отображается сплошной полоcой.
  2. Покольку значение сигнала TIMERHOOK инвертируется в функции OS::SystemTimerUserHook(), на соответствующей линии наблюдается меандр с периодом в два тика системного таймера, около четырёх милисекунд. События на линиях PROC2 и PROC3 происходят синхронно со сменой состояния на линии TIMERHOOK.
  3. В функции TProc2::Exec() есть два вызова фунции засыпания процесса: Sleep(1) определяет длину импульса на линии PROC2, Sleep(9) определяет время между импульсами. Таким образом, Proc2 — это мультивибратор с периодом импульсов 10 тиков системного таймера (20 мс) и длиной импульса 1 тик (2 мс).
  4. Перед опусканием линии PROC2 и засыпанием на десять тиков Proc2 вызовом ef.Signal(); пробуждает Proc3. Получив управление, TProc3::Exec() поднимает уровень на линии PROC3 и опять засыпает, теперь вызовом Sleep(). Время сна выбирается исходя из состояния бита 9 счётчика системных тиков, полученого вызовом OS::GetTickCount();. На протяжении 512 тиков таймера (около секунды) процесс спит два тика таймера, на протяжении другой секунды сон длится три тика таймера.
  5. Просыпаясь после сна по системному таймеру Proc3 опускает сигнал на линии PROC3 и вызовом ef.Wait(); опять засыпает в ожидании сигнала от Proc2. Фактически Proc3 — это одновибратор, запускаемый по спаду импульса на линии PROC2.
  6. Импульсы на линии T1PROC1 не синхронизированы с системным таймером, так как они порождаются в прерывании TIMER1_COMPA_vect(), активирующемся с частотой, не кратной системному таймеру. Подробнее работа таймера 1 и Proc1 рассмотрена ниже.
  7. На линии SWITCH видим маркеры переключения процессов. Часть из них сответствует перепадам на линиях PROC2 и PROC3, часть — на линии T1PROC1.

Переключение с Proc2 на Proc3 — под микроскопом

Параметры компиляции ОС и примера (общая часть для обеих вариантов развития событий):

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.


Proc2 to Proc3 variant 1

  1. При переходе на подпрограмму прерывания от системного таймера прекращается генерация меандра на линии IDLEHOOK в функции IdleProcessUserHook().
  2. После окончания работы системной части подпрограммы прерывания вызывается пользовательская функция SystemTimerUserHook(), в которой инвертируется состояние линии TIMERHOOK.
  3. Происходит выход из подпрограммы прерывания системного таймера и вход в прерывание переключения контекста. Между двумя прерываниями может вполниться одна команда из кода прерванного процесса. На этой осциллограмме случилось так, что этой командой было инвертирование состояния линии IDLEHOOK.
  4. Переключение контекста, которое передаёт управление Proc2 в точку выхода из вызова Sleep(9). Вначале переключения поднимается уровень на линии SWITCH, по окончании переключения линия SWITCH возвращается книзкому уровню. Длина импульса показывает время, необходимое собственно на переключение контекста, без учёта зарат на диспетчеризацию.
  5. В функции TProc2::Exec() поднимается линия PROC2, вызывается ef.Signal() и опускается линия PROC2. По длине импульса можно определить время, необходимое для выполнения функции Signal(). Теперь TProc2::Exec() делает вызов Sleep(10) и засыпает, планировщик scmRTOS ищет готовый к выполнению процесс. Таким процессом есть TProc3, только что получивший сигнал.
  6. Происходит очередное переключение процессов (второй импульс на линии SWITCH), управление получает TProc3::Exec().
  7. Proc3 поднимает линию PROC3, считывает значение системного счётчика тиков и засыпает на соответствующее время как это описано выше в разделе «Взаимная работа Proc2 и Proc3 — с птичьего полёта».
  8. Опять перепланирование процессов и, поскольку других готовых к выполнению процессов нет, управление передаётся на IdleProcess (третий импульс на линии SWITCH).
  9. Управление получила функция 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() есть отличия.
Осциллограммы изменяются следующим образом:


Proc2 to Proc3 variant 2

  1. При переходе на подпрограмму прерывания от системного таймера прекращается генерация меандра на линии IDLEHOOK в функции IdleProcessUserHook().
  2. После окончания системной части подпрограммы прерывания вызывается SystemTimerUserHook(), в которой инвертируется значение линии TIMERHOOK.
  3. Первое прерывание переключения контекста передаёт управление Proc2 в точку выхода из Sleep(9).
  4. В функции TProc2::Exec() поднимается линия PROC2, вызывается ef.Signal(). И тут, в отличие от первого ваианта, возвращение из вызванной системной функции несколько задерживается. В данном варианте приоритет Proc3 выше, чем Proc2, поэтому перепланирование в конце ef.Signal() активирует переключение на Proc3.
  5. Переключение процессов (второй импульс на линии SWITCH), управление получает TProc3::Exec().
  6. Proc3 поднимает линию PROC3, считывает значение системного счётчика тиков и засыпает.
  7. Остаётся ещё один активный процесс — Proc2, контекст переключается назад на TProc2::Exec() (третий импульс на линии SWITCH).
  8. И вот только теперь происходит «возврат» в TProc2::Exec() из функции ef.Signal(). Наконец-то опускается линия PROC2. На этой осциллограмме длина импульса на линии PROC2 намного больше, чем для преддыдущего варианта, в него вошли работа Proc3 и два переключения процессов.
    Теперь TProc2::Exec() опять делает вызов Sleep(9), scmRTOS ищет готовый к выполнению процесс. Но для данного варианта им есть не Proc3, он свою работу уже сделал, а фоновый процесс IdleProcess.
  9. Последнее переключение контекстов, управление передаётся на TIdleProcess::Exec().
  10. Управление получила функция TIdleProcess::Exec(), возобновляется перебрасывание линии IDLEHOOK.

При таком соотношении приоритетов выполнние идёт по пути
IdleProcess → Proc2 → Proc3→ Proc2 → IdleProcess
Имеем четыре переключения процессов.

С одной стороны, работу IdleProcess было прервано на большее время. В реальной системе прерванным мог бы быть процесс, выполняющий более полезную работу, чем перебрасывания состояния IO-вывода в высоком темпе.
С другой стороны, Proc3 получил управление немного раньше. Опять таки, в реальной системе, где между отправкой сигнала при помощи ef.Signal() и засыпанием Proc2 делал бы больше работы, в первом варианте задержкадо начала работы процесса 3 была бы ещё больше.
Какой варинт лучше, зависит от конкретных условий, от логики работы системы. Этот пример только показывает, что от выбора приоритетов процессов будут зависеть задержки от событий для передачи управления нужному процессу и время работы процессов.


Передача сигнала от прерывания таймера 1 до процесса 1

Эта часть примера отличается от предыдущих тем, что передача сигнала через OS::TEventFlag Timer1_Ovf; происходит не от одного процесса к другому, а от подпрограммы обработки прерывания к процессу.


Timer1 to Proc1

  1. Как и на предыдущих осциллограммах, меандр на линии IDLEHOOK прекращается при возникновении прерывания.
  2. На входе в подпрограмму обработки прерывания TIMER1_COMPA_vect() поднимается линия T1PROC1, после чего вызовом Timer1_Ovf.SignalISR(); посылается сигнал Proc1. Планировщик видит необходимость переключения и активирует запрос прерывания переключения контекстов.
  3. После выхода из прерывания TIMER1_COMPA_vect() отрабатывает прерывания переключения контекстов, управление получает TProc1::Exec().
  4. Proc1 опускает уровень на линии T1PROC1 и снова засіпает до прихода сигнала вызовом Timer1_Ovf.Wait();. Длина импульса на линии T1PROC1 даёт представление про минимальное время за держки от аппаратного события до того момента, когда начнёт работать код высокоприоритетного процесса, ожидавшего этого собтия.
  5. В данном случае других готовых к выполнению процессов нет и вторым переключением контекстов управление возвращается к IdleProcess.
  6. TIdleProcess::Exec() возобновляет работу и на лінії IDLEHOOK опять появляется меандр.

Воросы по этому примеру и порту scmRTOS для AVR и компилятора avr-gcc (WinAVR) можно задавать тут, в комментариях к странице.
Вопросы по «scmRTOS вообще» лучше задавать в соответствующем разделе на форуме electronix.ru.


© 2008-2010, Oleksandr Redchuk aka ReAl

11 Responses to “Объяснение примера 1-EventFlag”

  1. Виктор says:

    Вы предлагаете все-таки установи scmRTOS_CONTEXT_SWITCH_SCHEME равным 1 и вернуть кодинициализации прерывания от компаратора?

    • ReAl says:

      Почему «всё таки» (это еще не «несмотря ни на что», но близко)? В крупном масштабе диаграммы уже немного зависят от способа организации переключателя контекста, поэтому я уточнил. Чаще использовал прерывание от SPM, а не от компаратора, так как бутлоадер не использовал, а компаратор или, по крайней мере, выводы микроконтроллера — нужны.

  2. durgeadhefe says:

    Заранее извиняюсь если не коректный вопрос. И не в тот раздел)Сколько человек здесь делятся опытом?Спасибо!

    • ReAl says:

      В последнее время — пол человека ± четверть.

      • alexey_laa says:

        Читаю тут комментарии через RSS. 🙂 Но сам сейчас чего-либо на AVR не конструирую, хотя остались всякие идеи и “замороженные проекты”. 🙂 Из последнего на тему “embedded” у меня были небольшие Python-скрипты для Raspberry Pi – эксперименты с http-запросами и подключением через GPIO.

  3. Юрий says:

    Через блокнот WinAvr все оказалось просто. Нужно выбрать в главном меню блокнота File/New/Project и создать проект с любым именем в папке с примером. Затем подключить к проекту все фалы примера, в том числе и файл без расширения makefile. Далее открыть в блокноте makefile и выполнить команду из гланого меню блокнота Tools/[WinAvr]Make All. В результате должна появиться папка exe где и будет требуемый .hex файл для прошивки в контроллер.

    По ходу песни возникли некоторые вопросы.
    1.В чем всетаки разница между контекстом и процессом. Разве это не одно и то же?
    2.И всетаки чем конкретно в данном примере переключаются контексты (речь идет о программном прерывании самого низого уровня). Где можно посмотреть как организовано это прерывание в тексте примера.
    3. Получается что программа обработки прерывания по Timer1 это тоже процесс (он может быть короткий, а может быть длинный) или нет? Или программа обработки прерывания только передает управление нужному процессу? Так зачем передавать? Только трата времени. Разве не так?

    • ReAl says:

      Да оно и не должно было быть сложно 🙂
      Просто я для всего под 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. Вопрос о том, что делать в обработчике, а что поднимать на уровень процесса, решается для каждого проекта. В обработчике должен сидеть «драйвер» периферийного объекта или, по крайней мере, вся его низкоуровневая часть.
      На мой взгляд, всю обработку состояний интерфейса стоит производить в обработчике, отдавая наверх событие «транзакция завершена». Процесс проснётся и обработает только результат успешно/неуспешно, подумает, что делать дальше.
      Я бы сказал так — если действия по времени занимают менше пары-тройки переключений с процесса на процесс (одно переключение — две-две с половиной сотни тактов), то нет никакого смысла их поднимать на уровень процесса. Если больше десяти, то врядли стоит занимать столько времени, блокируя тем самым выполнение высокоприоритетных задач, а затраты на переключение уже не так и велики на фоне работы. Между этими границами переходная зона. Чем менее приоритетным процессам нужен результат работы прерываний, тем меньше времени в этом прерывании нужно проводить.

      • Юрий says:

        “Чем менее приоритетным процессам нужен результат работы прерываний, тем меньше времени в этом прерывании нужно проводить” – неплохо сказано.
        Если не сложно, можно прокомментировать этот код из обработчика пр.таймера Т1

            OS::TISRW_SS ISRW;
            ENABLE_NESTED_INTERRUPTS();
            Timer1_Ovf.SignalISR();
        • ReAl says:

          Объект ISRW — ISR Wrapper. Его создание просто заставляет отработать его конструктор на входе в обработчик и его деструкутор на выходе, а в них помещены те действия, которые нужны для интеграции прерывания в scmRTOS. Т.е. работа со счётчиком вложенных прерываний и запуск перепланировщика при выходе из прерываний в основной код.

          Далее разрешаются вложенные прерывания. Тут это не нужно, прерывание короткое, но надо же как-то продемонстрировать и проверять работоспособность.

          После чего взводится событие таймера — помечаются как готовые все процессы, которые его ожидают.

          В приведенном примере кода не хватает для комментирования очень важной строки 🙂

          }

          По ней заканчивается область определения переменной ISRW и отрабатывает деструктор объекта. В деструкторе вызывается планировщик ОС, просматривающий таблицу процессов и, в данном примере, сразу передающий управление на самый приоритетный процесс TProc1.

  4. Юрий says:

    Хороший пример. Очень хотелось-бы его запустить в реальном железе. Но проблема в том, что я, как и многие другие не знакомые с WinAvr, понятия не имеют как пример запустить на компиляцию в WinAVR. Информации по этому поводу в сети нет или она никуда не годится. Вроде все должно быть не сложно. Но кроме простого открытия файлов в WinAvr дело дальше не пошло. Если не сложно добавьте пожалуйста к статье последовательность как добавить и откомпилировать пример в WinAvr с получением .hex.

    • ReAl says:

      При установке WinAVR он прописывает в PATH два пути — к компилятору и к утилитам (из которых интересуют make и sh). Из командной строки всё становится доступным без лишних телодвжений.
      Самый простой, по крайней мере для меня :-), путь сборки примера — зайти FAR-ом в каталог 1-EventFlag и набрать команду make.

      Идущим в поставке WinAVR редактором Programmers Notepad я никогда не пользовался, поэтому не могу сказать, как собирать пример из него.

Leave a Reply

[flagcounter image]