Общая диаграмма работы ОС

Что из себя представляет задача
Это практически то же самое, что и процедура, вызываемая командой RCALL с тремя отличиями:

  • Вызывается она не мгновенно, а в порядке очереди.
  • Вызов задачи идет не по ее адресу, а по ее порядковому номеру в таблице переходов.
  • Возврат из нее идет не в то же место откуда вызывали, а в цикл диспетчера задач.

Сама задача представляет собой обычную процедуру, записанную без каких либо замудреностей.

Распологается там, где обычно прописаны вызываемые процедуры. В этом отношении ничего не поменялось. В принципе, вызывать можно код и из внешнего файла, главное правильно прописать все это в таблице переходов.

В тестовом примере это выглядит так:

Расположение: Trash-rtos.asm - главный файл программы

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 ;=========================================================;Tasks;========================================================= Idle: RET; Задача пустого цикла, но ничего; не мешает сунуть сюда любой код.; Он будет выполнен. В последнюю очередь.;------------------------------------------------------- Fire: LDS OSRG,U_B; Это код задачи "Fire" OUTI UDR,'A'; Выдрано из реального проекта; Суть данного кода не важна CBI PORTD,7; Поэтому можешь не вникать. Тут NOP; Может быть абсолютно любой NOP; код -- код твоей задачи! SBI PORTD,7; Если любопытно, то тут обычное LDS Counter,PovCT; заполнение сдвигового регистра LDPA Lines; из трех микросхем 74HC164; средствами SPI передатчика CLR OSRG; Оставил его лишь для примера.; Чтобы наглядно показать, что ADD ZL,Counter; Из себя представляет задача. ADC ZH,OSRG LPM OSRG,Z+ OUT SPDR,OSRG Wait0: SBIS SPSR,SPIF RJMP Wait0 INC Counter CPI Counter,150 BRSH Clear STS PovCT,Counter RET Clear: CLR Counter STS PovCT,Counter RET; Выход из задачи только по RET!!!;------------------------------------------------------- Task2: RET; Это пустые заглушки. На этом месте; могла бы быть ваша задача!:);------------------------------------------------------- Task3: RET; Аналогично, надо будет задействую.;------------------------------------------------------- Task4: RET; Названия в стиле Task4 тоже живут; недолго. Обычно переименовываю;------------------------------------------------------- Task5: RET; Как с задачей "Fire";------------------------------------------------------- Task6: RET ;------------------------------------------------------- Task7: RET ;------------------------------------------------------- Task8: RET ;------------------------------------------------------- Task9: RET

Таблица переходов
После всего кода задач распологается таблица переходов и код самой ОС:

Расположение: Trash-rtos.asm - главный файл программы, в самом низу, в конце ПЗУ

12345678910111213141516 ;========================================================================; RTOS Here;========================================================================. include "kerneldef.asm"; Настройки ядра - переменные и ряд макросов.. include "kernel.asm"; Подклчюаем ядро ОС.;Это таблица переходов. TaskProcs:.dw Idle; [00] Она содержит в себе реальные адреса задач. dw Fire; [01] Как видишь, 0 тут у задачи пустого цикла,. dw Task2; [02] 01 у "Fire", ну и дальше. dw Task3; [03] По порядку.. dw Task4; [04]. dw Task5; [05]. dw Task6; [06]. dw Task7; [07]. dw Task8; [08]. dw Task9; [09]

Причем, в таблице задач не обязательно должны быть разные задачи. Можно делать одну, но на разные ячейки, например так:

12345678910 TaskProcs:.dw Idle; [00]. dw Fire; [01]. dw Task2; [02]. dw Task3; [03]. dw Task4; [04]. dw Fire; [05]. dw Task6; [06]. dw Idle; [07]. dw Idle; [08]. dw Task9; [09]

Это иногда бывает удобно.
Таблица переходов нужна для того, чтобы можно было любому адресу в программе дать адрес-смещение относительно начала таблицы перехода. То есть, теперь, чтобы перейти на Task4 нам не нужно знать точный адрес этой задачи, достаточно лишь знать, что ее адрес записан в таблице переходов в четвертой ячейке. Адрес начала таблицы переходов у нас фиксированный, поэтому просто прибавляем к нему смещение (равное номеру задачи*2) и берем оттуда искомый адрес. Благодаря этому, мы можем в очереди задач держать не двубайтные адреса переходов, а однобайтные номера под которыми эти адреса размещены в таблице.

А чтобы не путаться под каким номером какая задача спрятана, то введем для них символическое обозначение:
Расположение: kerneldef.asm - файл макроопределений ядра

12345678910 .equ TS_Idle = 0; Просто нумерация. Не более того. equ TS_Fire = 1; Зато теперь можно смело отправлять в очередь. equ TS_Task2 = 2; задачу с именем TS_Fire и не париться на счет того,. equ TS_Task3 = 3; что запишется что то не то.. equ TS_Task4 = 4; Тут все по порядку, жестко привязано к ячейкам таблицы!. equ TS_Task5 = 5; Так что если в таблице и можно делать одинаковые задачи,. equ TS_Task6 = 6; то тут у них идентификаторы должны быть разные!!!. equ TS_Task7 = 7; А имена можно придумывать любые, они не привязаны ни к чему,. equ TS_Task8 = 8; Главное самому не забыть что где.. equ TS_Task9 = 9

Очередь задач.
Логически выглядит как строка в памяти, где каждый байт это номер задачи. Два числа 0 и FF зарезервированы системой. 0 - это Idle, холостой цикл диспетчера. FF - нет задачи, конец очереди.

В коде это выглядит так:
Расположение: Trash-rtos.asm - главный файл программы, самое начало. Где идет разметка ОЗУ

123 .DSEG.equ TaskQueueSize = 11; Длина очереди задач TaskQueue:.byte TaskQueueSize; Адрес очереди сотытий в SRAM

Длина очереди задается с запасом, чтобы не произошло ее срыва. Располагать ее лучше после остальных данных, чтобы она тянулась навстречу стеку.

Диспетчер задач
Это небольшая процедурка, которая берет из очереди байт, сравнивает его с FF. Если равно, значит очередь пуста и происходит выход в главный цикл. Если же там есть какое либо число отличное от FF, то оно загружается в регистр, происходит вычисление адреса по таблице переходов и прыжок на адрес задачи и выполнение кода. Перед прыжком текущий номер задачи удаляется из очереди, а вся очередь сдвигается на один байт вперед. При следующем заходе в диспетчер все повторяется заново до полного опустошения очереди. При этом в очередь можно добавлять новые задачи.

Главный цикл программы при этом выглядит следующим образом:
Расположение: Trash-rtos.asm - главный файл программы

12345 Main: SEI; Разрешаем прерывания. WDR; Reset Watch DOG RCALL ProcessTaskQueue; Обработка очереди процессов (Диспетчер) RCALL Idle; Простой Ядра RJMP Main

Сам обработчик очереди несложен. Распологается в файле kernel.asm

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 ProcessTaskQueue: ldi ZL, low(TaskQueue); Берем адрес начала очереди задач ldi ZH, high(TaskQueue); Напомню, что это в ОЗУ. ld OSRG, Z; Берем первый байт (OSRG = R17 рабочий cpi OSRG, $FF; регистр OS) Сравниваем с FF breq PTQL02; Равно? Значит очередь пуста - выход. clr ZH; Сбрасываем старший байт lsl OSRG; А взятый номер задачи умножаем на 2 mov ZL, OSRG; Так как адреса у нас двубайтные, а значит; И ячейки в таблице перехода двубайтные; Получается смещение по таблице subi ZL, low(-TaskProcs*2); Прибавляем получившееся смещение к адресу sbci ZH, high(-TaskProcs*2); начала таблицы переходов. Ну и что, что AVR; не умеет; складывать регистр с числом + перенос.; Зато умеет вычитать, а минус на минус дают плюс!:); Математика царица всех наук!; Теперь в Z у нас адрес где лежит адрес перехода. lpm; Берем этот адрес! Сначала в R0 mov OSRG, r0; Потом в OSRG ld r0, Z+; Эта команда ничего ценного на грузит, мы ее применили; Ради "Z+" чтобы увеличить адрес в Z и взять второй байт; Целевого адреса по которому мы перейдем. lpm; Берем в R0 второй байт адреса. mov ZL, OSRG; А из OSRG перекладываем в ZL mov ZH, r0; И из R0 в ZH. Теперь у нас в Z полный адрес перехода.; Можно драпать, в смысле IJMP - индексный переход по Z; Но пока рано! Надо же еще очередь в порядок привести! push ZL; Спрячем наш адрес, наше сокровище, в стек... push ZH; Глубоко зароем нашу прелесссть....; Займемся грязной работой. Продвинем очередь. ldi Counter, TaskQueueSize-1; Загрузим длинну очереди. Иначе мы всю память ldi ZL, low(TaskQueue); подвинем. И хапнем в Z начало очереди. ldi ZH, high(TaskQueue) cli; Запретим прерывания. А то если очередь сорвет получим; армагедец. PTQL01: ldd OSRG, Z+1; Грузим из следующего Z+1 байта и перекладываем st Z+, OSRG; все в Z, а Z после этого увеличиваем на 1 dec Counter; Уменьшаем счетчик (там длинна очереди!) brne PTQL01; Если не конец, то в цикле. ldi OSRG, $FF; А если конец, то по последнему адресу записываем FF st Z+, OSRG; Который является признаком конца очереди. sei; Разрешаем прерывания. Можно расслабиться pop ZH; Достаем из стека нашу прелессть... наш адрес перехода pop ZL; Оба байта, старший и младший. ijmp; Переходим в задачу!!!; Обрати внимание - сюда мы пришли по RCALL из главного цикла; Значит в стеке у нас лежит адрес возврата. А ушли мы в задачу по IJMP; который стек не меняет. Но это не страшно! Ведь из задачи мы; выходим по RET! PTQL02: ret

Для большей понятности нарисовал диаграммку со стрелочками разными, про то как формируется адрес в таблице переходов:

Задачи кладутся в очередь другой процедурой:

12 ldi OSRG, TS_Task4; Запускаем в очередь задачу Task4 rcall SendTask

Делать это можно где угодно, хоть в прерывании, хоть в другой задаче.
Для удобства был написан макрос:

1 SetTask [task]

Сама процедура SendTask работает тоже несложно, она всего лишь ставит задачу в очередь.
Расположение: kernel.asm

12345678910111213141516171819202122232425262728293031323334353637 ; OSRG - Event; В рабочем регистре ОС - номер задачи SendTask: push ZL; Сохраняем все что используется push ZH; в стеке push Tmp2 push Counter in Tmp2,SREG; Сохраняем значение флагов push Tmp2 ldi ZL, low(TaskQueue); Грузим в Z адрес очереди задач. ldi ZH, high(TaskQueue) ldi Counter, TaskQueueSize; А в счетчик длинну очереди, чтобы; не начать всю память засаживать. cli; запрещаем прерывания. SEQL01: ld Tmp2, Z+; Грузим в темп байт из очереди cpi Tmp2, $FF; и ищем ближайшее пустое место = FF breq SEQL02; Если нашли, то переходим на сохранение dec Counter; Либо конец очереди по счетчику. breq SEQL03 rjmp SEQL01 SEQL02: st -Z, OSRG; Нашли? Сохраняем в очереди номер задачи. SEQL03: pop Tmp2; Возвращаем флаги. Если там прерывание было out SREG,Tmp2; разрешено, то оно вернется в это значение. pop Counter; Выходим, достав все заныченное. pop Tmp2 pop ZH pop ZL ret

Итак, у нас есть очередь задач и общая логика работы системы. Но одной очереди задач с диспетчером мало. Нужно распределять задачи по времени, задавать интервалы, запускать отложенные задачи. Всем этим будет заниматься служба таймеров.

В чем ее суть ее работы:
Время разбивается на интервалы, скажем, по 1мс. Такой выдержки хватает для большинства задач. Также у нас должна быть очередь программных таймеров, размещенных в ОЗУ. На каждый таймер отводится три байта:
Первый — идентификатор задачи. Два других — выдержка в миллисекундах.

Два байта позволяют организовать выдержку в 65.5 секунд. Конечно, можно сделать и больше, если отвести на временную выдержку три или даже четыре байта, но такие большие временные интервалы пригождаются редко, поэтому проще перехватиться через дополнительную переменную для конкретной задачи, а не нагружать таймерную службу обсчетом дополнительных байт.

Один из свободных аппаратных таймеров программируем на то, чтобы он генерировал прерывание каждые 0.001с

О работе аппаратных таймеров написано в разделе про программирование встроенной периферии. Пока можешь не заморачиваться. Суть лишь в том, что настроенный таймер тикает независимо от главной программы и в заданное время выдает прерывание.

По прерыванию мы берем из очереди таймеров первый байт и сравниваем его с 0xFF, за 0xFF принято неактивное состояние. Если же там не 0xFF, то значит это идентификатор задачи, а таймер активен. Поэтому берем третий байт, декрементируем его, если он стал равен нулю декрементируем второй байт и если оба байта не стали равны нулю переходим к проверке следующего байта. В случае если время истекло, то идентификатор задачи пихается в очередь задач на исполнение.

Обработчик прерывания таймера:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 push OSRG; Прячем OSRG в стек in OSRG,SREG; push OSRG; Сохранение регистра OSRG и регистра состояния SREG push ZL push ZH; сохранение Регистра Z push Counter; сохранение Регистра Counter ldi ZL,low(TimersPool); Загрузка с регистр Z адреса таймерной очереди, ldi ZH,high(TimersPool); по которому находится информация о таймерах ldi Counter,TimersPoolSize; Берем максимальное количество таймеров Comp1L01: ld OSRG,Z; OSRG = [Z]; Получить номер события cpi OSRG,$FF; Проверить на "NOP = FF" breq Comp1L03; Если NOP то переход к следующей позиции clt; Флаг T используется для информации об окончании счёта ldd OSRG,Z+1; Грузим в OSRG первый байт времени subi OSRG,low(1); Уменьшение младшей части счётчика на 1 std Z+1,OSRG; И сохраняем ее обратно туда откуда взяли breq Comp1L02; Если образовался 0 то флаг T не устанавливаем set; А если байт не закончился, то ставим Т Comp1L02: ldd OSRG,Z+2; Берем второй байт времени. sbci OSRG,High(1); Уменьшение старшей части счётчика на 1 std Z+2,OSRG; Сохраняем где взяли brne Comp1L03; Счёт не окончен brts Comp1L03; Счёт не окончен (по T) ld OSRG,Z; Получить номер задачи rcall SendTask; послать в системную очередь задач ldi OSRG,$FF; = NOP (задача выполнена, таймер самоудаляется) st Z, OSRG; Прописываем в заголовок таймера FF Comp1L03: subi ZL,Low(-3); Пропуск таймера. sbci ZH,High(-3); Z+=3 - переход к следующему таймеру dec Counter; счетчик таймеров brne Comp1L01; Если это был не последний таймер, то еще раз pop Counter; восстанавливаем переменные pop ZH pop ZL pop OSRG; Восстанавливаем регистры out SREG,OSRG pop OSRG RETI; Выход из прерывания таймера

Постановка таймера.
При постановке таймера мы вначале шерстим всю очередь таймеров на предмет наличия там записи с таким же идентификатором. Если находим, то обновляем его новым значением. Если не находим, то записываем следующе значение.

Постановка делается макросом из файла kernel_macro.asm

1 SetTimerTask [task],[time]

Сам макрос развертывается в такой код:

1234 ldi OSRG, [Task] ldi XL, Low([Time]); Задержка в милисекундах ldi XH, High([Time]); От 1 до 65535 rcall SetTimer

Как видим, тут используется регистровая пара Х и вызывается функция постановки таймера. Про использование ресурсов в этих макросах и процедурах надо помнить и сохранять их в стеке если постановка таймера идет из прерывания.

Сама функция SetTimer работает просто:
Расположение: kernel.asm

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657 SetTimer:; В OSRG номер задачи. В Х время push ZL; Сохраняем все что используем push ZH push Tmp2 push Counter ldi ZL, low(TimersPool); Берем адрес очереди таймеров ldi ZH, high(TimersPool) ldi Counter, TimersPoolSize; Берем число таймеров STL01: ld Tmp2, Z; Хватаем первый заголовок cp Tmp2, OSRG; Сравниваем с тем который хотим записать breq STL02; Если такой уже есть, идем на апдейт subi ZL, Low(-3); Выбираем следующий sbci ZH, High(-3); Z+=2 dec Counter; Уменьшаем счетчик breq STL03; Если ноль переход к записи нового таймера rjmp STL01 STL02:; Если нашли такой же, то делаем ему апдейт std Z+1, XL; Значения временем из Х std Z+2, XH; Оба байта rjmp STL06; Выходим из процедуры STL03:; Если аналогичного не нашли ldi ZL, low(TimersPool); То делаем добавление нового ldi ZH, high(TimersPool); Заново берем адрес очереди ldi Counter, TimersPoolSize; И ее длинну STL04: ld Tmp2, Z; Хватаем первый заголовок cpi Tmp2, $FF; Пуст? breq STL05; Переходим к записи таймера subi ZL, Low(-3); Если не пуст выбираем следующий таймер sbci ZH, High(-3); Z+=2 dec Counter; Очередь кончилась? breq STL06; Да. Нет таймеров свободных. Увы. Выход; Краша не будет, но задача не выполнится rjmp STL04; Если очередь не вся, то повторяем итерацию STL05: cli; Запрет прерываний перед записью в очередь st Z, OSRG; Сохраняем новый таймер std Z+1, XL; И его время std Z+2, XH sei; Разрешаем прерывания STL06:; Выходим, достав все из стека. pop Counter pop Tmp2 pop ZH pop ZL ret
       

Вот, ничего сложного. Из кода сразу же понятны недостатки данного алгоритма.
Время выполнения зависит от числа таймеров и плавает, особенно на малых выдержках в 1-2мс. Так что точные замеры времени ей поручать нельзя. Для этого придется задействовать другой аппаратный таймер и все нежные манипуляции делать на нем. Но на выдержке в 500мс, глядя осциллографом на тестовый импульс, я особых искажений в показанях не заметил. Т.к. AVR щелкает команды очень быстро и чем быстрей тактовая частота, тем меньше влияние числа таймеров на временную выдержку (растет отношение холостых тактов таймера к времени выполнения процедуры таймерной очереди).
Малые временные интервалы, меньшие чем 1мс этому таймеру тоже недоступны. Конечно, можно взять и понизить планку, сделать срабатывание прерывания не каждую миллисекунду, а каждые 500мкс. Но тут падает точность. Так что если такое потребуется, то делать это на другом таймере.

Но, в целом, несмотря на недостатки, очень удобная служба получилась. А главное вытыкается в момент. Прикол с апдейтом таймера кажется лишним, но реально часто пригождается. Например, когда по условию надо отложить событие. Берешь и перезаписываешь таймер, подобно программному вачдогу. А если надо две одинаковые задачи по таймеру сделать в разное время, то никто не запрещает добавить ее в таблицу переходов на новый идентификатор и будет тебе профит.

Ядро у нас есть, теперь осталось это все хозяйство запихать на МК. Для этого всего лишь надо рассовать нужные части кода в исходник. Показывать буду на примере ATmega8. Для других МК разница минимальная. Может быть с таймером что нибудь помудрить придется, но не более того.
Например, недавно, вкорячивал ту же схему на ATmega168, так пришлось подправить иницилизацию таймера - регистры там зовутся по другому. Пришлось изменить макрос OUTI - так как многие привычные уже регистры перестали загружаться через комадну OUT - выпали из диапазона, только через LDS/STS ну и, собственно, все хлопоты. Потратил минут 20 на переименование регистров и заработало.

Итак. Есть у нас совершенно новый пустой файл NewMega8-rtos.asm

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 ; Добавляем в него первым же делом инклюдник восьмой меги:. include "m8def.inc"; Используем ATMega8; Следом я добавляю файл с моими макроопределениями в котором записаны все символические имена; для ресурсов, вроде регистров, портов, отдельных пинов. ИМХО удобней все держать по разным; файлам, но тут уже дело за вашими привычками.. include "define.asm" ;Потом файл с макросами, он тоже отдельный и кочует из программы в программу. Именно там всякие; левые самодельные команды вроде OUTI прописаны.. include "macro.asm"; Все макросы у нас тут; Следом идет файл макросов ядра ОС. Он должен распологаться в начале программы, иначе компилятор; не поймет. Именно там прописаны макросы таймерной службы, добавления задачи и таймера, там же; заныкан стандартный макрос инциализации UART и многое другое.. include "kernel_macro.asm" ;Дальше прописывается сегмент оперативной памяти в котором заранее определены; все очереди задач и таймеров.. DSEG.equ TaskQueueSize = 11; Размер очереди событий TaskQueue:.byte TaskQueueSize; Адрес очереди сотытий в SRAM. equ TimersPoolSize = 5; Количество таймеров TimersPool:.byte TimersPoolSize*3; Адреса информации о таймерах; Следом уже идет код, начинается кодовый сегмент. Надо заметить, что у меня вся таблица; Прерываний спрятана в vectors.asm и вместо таблицы в коде.include "vectors.asm" это удобно. CSEG.ORG 0x0000; Проц стартует с нуля, но дальше идут вектора RJMP Reset.ORG INT0addr; External Interrupt Request 0 RETI.ORG INT1addr; External Interrupt Request 1 RETI.ORG OC2addr; Timer/Counter2 Compare Match RJMP OutComp2Int ;<<<<<<<< Прерывание ОС!!!. ORG OVF2addr; Timer/Counter2 Overflow RETI.ORG ICP1addr; Timer/Counter1 Capture Event RETI.ORG OC1Aaddr; Timer/Counter1 Compare Match A RETI.ORG OC1Baddr; Timer/Counter1 Compare Match B RETI.ORG OVF1addr; Timer/Counter1 Overflow RETI.ORG OVF0addr; Timer/Counter0 Overflow RETI.ORG SPIaddr; Serial Transfer Complete RETI.ORG URXCaddr; USART, Rx Complete RJMP Uart_RCV.ORG UDREaddr; USART Data Register Empty RETI.ORG UTXCaddr; USART, Tx Complete RJMP Uart_TMT.ORG ADCCaddr; ADC Conversion Complete RETI.ORG ERDYaddr; EEPROM Ready RETI.ORG ACIaddr; Analog Comparator RETI.ORG TWIaddr; 2-wire Serial Interface RETI.ORG SPMRaddr; Store Program Memory Ready RETI.ORG INT_VECTORS_SIZE; Конец таблицы прерываний

После таблицы векторов идут обработчики прерываний. Они короткие, поэтому их размещаю в начале. Первым же обработчиком идет обработчик прерывания от таймера на котором висит таймерная служба ОС. Под таймерную службу желательно отдать самый стремный таймер, на который не завязана ШИМ или еще какая полезная служба. В идеале бы под это дело пустить Timer0, как самый лоховский. Но он не умеет cчитать от 0 до регистра сравнения, только от нуля до 255, впрочем, в обработчик прерывания можно добавить предварительную загрузку таймера0 нужным значением и не расходовать более навороченный таймер. Мне было лень, я повесил все на Таймер2, а в регистр сравнения прописал такое значение, чтобы прерывание было ровно один раз в 1мс. Разумеется, выставив предварительно нужный делитель.

123456789101112131415 ; Interrupts procs; Output Compare 2 interrupt - прерывание по совпадению TCNT2 и OCR2; Main Timer Service - Служба Таймеров Ядра - Обработчик прерывания OutComp2Int: TimerService; Служба таймера OS; Весь код обработчика в виде одного макроса; Просто вставил и все. Куда угодно. Можно извратиться; Подать импульсы с нужной частотой на какой-нибудь; INT0 и службу таймеров повесить на его прерывание; Разумеется, в таблице векторов из вектора прописан;переход сюда RETI; выходим из прерывания Uart_RCV: RETI; Другие прерывания если нужны Uart_TMT: RETI

То все было обязательной подготовкой и разметкой адресов и памяти, а вот тут уже начинается сама программа. Именно отсюда стартует проц. Вписываем следующий код:

12 Reset: OUTI SPL,low(RAMEND); Первым делом инициализируем стек OUTI SPH,High(RAMEND)

Все инициализации у меня спрятана в .include “init.asm”, но тут я распишу ее полностью.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 ; init.asm; Очистка памяти RAM_Flush: LDI ZL,Low(SRAM_START) LDI ZH,High(SRAM_START) CLR R16Flush: ST Z+,R16 CPI ZH,High(RAMEND) BRNE Flush CPI ZL,Low(RAMEND) BRNE Flush CLR ZL CLR ZH; Init RTOS; В исходнике все сделано; INIT_RTOS; вот так вот, одним макросом. Макрос описан;; в файле kernel_macro.asm;; Но я распишу тут подробно. Там идет настройка;; Таймера в работу; Содержимое макроса INIT_RTOS OUTI SREG, 0; Сброс всех флагов rcall ClearTimers; Очистить список таймеров РТОС rcall ClearTaskQueue; Очистить очередь событий РТОС sei; Разрешить обработку прерываний; Настройка таймера 2 - Основной таймер для ядра. equ MainClock = 8000000; CPU Clock. equ TimerDivider = MainClock/64/1000; 1 mS OUTI TCCR2,1<<CTC2|4<<CS20; Установить режим CTC и предделитель =64 OUTI TCNT2,0; Установить начальное значение счётчиков ldi OSRG,low(TimerDivider) out OCR2,OSRG; Установить значение в регистр сравнения; Конец макроса INIT_RTOS OUTI TIMSK,1<<OCF2; Разрешить прерывание по сравнению; Инициализация остальной периферии USART_INIT; Конец init.asm

После инициализации идет секция запуска фоновых приложений. Добавим пока меточку про запас, с нас не убудет.

1 Background: NOP; Пока тут ничего нет

Главный цикл. Его надо скопировать без изменений, как есть.

12345678 Main: SEI; Разрешаем прерывания. wdr; Reset Watch DOG rcall ProcessTaskQueue; Обработка очереди процессов rcall Idle; Простой Ядра rjmp Main; Основной цикл микроядра РТОС; В Idle можно сунуть что нибудь простое, быстрое и некритичное.; Но я обычно оставляю его пустым.

После главного цикла вставляется шаблон под секцию задач. Именно сюда вписывается наш исполняемый код. Тут творится самое интересное

12345678910111213141516171819202122232425262728293031323334 Idle: RET ;----------------------------------------------------------------------------- Task1: RET ;----------------------------------------------------------------------------- Task2: RET ;----------------------------------------------------------------------------- Task3: RET ;----------------------------------------------------------------------------- Task4: RET ;----------------------------------------------------------------------------- Task5: RET ;----------------------------------------------------------------------------- Task6: RET ;----------------------------------------------------------------------------- Task7: RET ;----------------------------------------------------------------------------- Task8: RET ;----------------------------------------------------------------------------- Task9: RET; А после секции задач вставляем шаблонную таблицу переходов и код ядра. include "kerneldef.asm"; Подключаем настройки ядра. include "kernel.asm"; Подклчюаем ядро ОС TaskProcs:.dw Idle; [00]. dw Task1; [01]. dw Task2; [02]. dw Task3; [03]. dw Task4; [04]. dw Task5; [05]. dw Task6; [06]. dw Task7; [07]. dw Task8; [08]. dw Task9; [09]

Готово! Можно компилировать, пока это пустой проект. Но ненадолго.

Итак, теперь то же самое, но по пунктам.
Установка AVR OS

  • Создаем пустой проект
  • Вставляем файлы макроопределений
  • Вставляем разметку памяти под очереди задач/таймеров
  • Вставляем таблицу векторов прерываний
  • Прописываем в таблице векторов прерываний переход на обработчик таймера по переполнению
  • Добавляем обработчик прерываний
  • Прописываем стартовую метку и инициализацию стека
  • Инициализация всего что только можно - портов, периферии, обнуление ОЗУ, обнуление очередей, запуск таймера ОС.
  • Добавляем секцию фоновых задач
  • Добавляем код главного цикла
  • Добавляем шаблонную сетку задач
  • Добавляем код ядра и таблицу переходов.
  • Пишем наш код
  • ...
  • PROFIT

В следующий раз я покажу практический пример работы с этой ОС. В котором будет красочно показано ради чего, собственно, этот геморрой и почему мне он так нравится:)

Отлично, с теорией работы ОС ознакомил. Устанавливать научил, осталось научить использовать весь этот конвеерно таймерный шухер. Чем я сейчас и займусь. Сразу берем быка за рога и формулируем учебно-боевую программу.
Тестовое задание:
Пусть у нас будет ATMega8, с несколькими кнопками. АЦП и подключеним к компу через UART. На меге будет три светодиода.

  • Девайс должен при включении начинать мигать зеленым диодом, мол работаю.
  • При этом раз в секунду сканировать показания АЦП и если показания ниже порога - Моргать красным диодом.
  • По сигналу с UARТ с целью защиты от ошибок сделать по байту ‘R’ установку флага готовности, а потом, в течении 10ms если не придет байт ‘A’ сбросить флаг готовности и игнорировать все входящие байты кроме ‘R’. Если ‘A’ придет в течении 10мс после ‘R’, то отправить в UART ответ и зажечь белый диод на 1 секунду.

Вот так вот, не сильно сложно. Но мне просто лень делать что либо сложней, а для тестовой задачи сгодится.
Итак, что у нас есть:
Две фоновые задачи, которые выполняются всегда и постоянно:

  • Мигать зеленым диодом
  • Проверять показания с АЦП

И цепочки:

  • Прерывание АЦП - Проверка условия - зажечь диод - погасить диод.
  • Прерывание UART- Проверить на R - взвести флаг (ждать ‘А’ - зажеть диод - погасить диод) - снять флаг

Описываем задачи:

  • OnGreen - зажечь зеленый
  • OffGreen - погасить зеленый
  • OnRed - зажечь красный
  • OffRed - погасить красный
  • OnWhite - зажечь бело-лунный
  • OffWhite - погасить бело-лунный
  • Reset_R - сбросить флаг готовности
  • ADC_CHK - проверить АЦП.

Собственно я это так, для ясности. Обычно я по ходу дела обвешиваю нужным функционалом. Инициализацию портов и всего прочего я опущу — не маленькие уже, DDR всякие сами выставите. Буду указывать код кусками, с пояснениями что где, а всю картину увидите в исходнике.

Итак, начинаем наращивать мясо:
Добавляем первую задачу - Мигать зеленым диодом. Для этого любую удобную секцию из раздела Task берем и переименовываем по своему вкусу. Будем по порядку. Под раздачу пойдет первая - Task1. Но сначала надо прописать ее номер в дефайнах. Поэтому сразу же лезем в kerneldef.asm и вписываем там:

12345 .equ TS_Idle = 0;. equ TS_OnGreen = 1; <<<<. equ TS_Task2 = 2;. equ TS_Task3 = 3;...

Затем возвращаемся к таблице задач. И в поле Task1 вписываем нашу задачу. Переименовывая метку (это не обязательно, но иначе запутаетесь в этих бесконечных Task-n.
Сразу же описываем саму задачу - она простая, просто зажечь диод. Одна команда процессора - SBI
Следом идет макрос SetTimerTask, считай это командой API нашей ОС. Значит что спустя 500мс надо выполнить задачу OffGreen

123456789 ; Tasks Idle: RET ;----------------------------------------------------------------------------- OnGreen: SBI PORTB,1; Зажечь зеленый SetTimerTask TS_OffGreen,500 RET ;----------------------------------------------------------------------------- Task2: RET...

Но у нас нет такой задачи! Не вопрос, добавляем!
Прописываем сначала в дефайнах:

1234 .equ TS_Idle = 0;. equ TS_OnGreen = 1;. equ TS_OffGreen = 2; <<<<...

Потом добавляем ее код в область задач

12345678910111213 ; Tasks Idle: RET ;----------------------------------------------------------------------------- OnGreen: SBI PORTB,1; Зажечь зеленый SetTimerTask TS_OffGreen,500 RET ;----------------------------------------------------------------------------- OffGreen: CBI PORTB,1; Погасить зеленый SetTimerTask TS_OnGreen,500 RET ;----------------------------------------------------------------------------- Task3: RET...

Видишь первая задача ссылается на вторую, а вторая на первую. Одна зажигает, вторая гасит. В итоге, они будут кольцом запускать друг друга, а зеленый диод, повешанный на PB1 будет мигать с интервалом 0.5с.

Теперь надо вписать их в таблицу переходов каждого на свой номер в kerneldef.asm:

12345 TaskProcs:.dw Idle; [00]. dw OnGreen; [01] TS_OnGreen. dw OffGreen; [02] TS_OffGreen. dw Task3; [03]...

Отлично. Фоновая задача сформирована, теперь надо только ее стартануть. Помните секцию Background? Вот я ее держу именно для этих случаев. Так то можно откуда угодно сделать наброс. Но удобней это делать из одного места.
Вот там и делаем:

1 Background: RCALL OnGreen

Задача все равно сформирована как процедура, так что как зайдет так и выйдет, а таймер запустится и дальше по цепи. То есть задачу можно стартануть простым RCALL. Тогда она первый раз выполнится мгновенно. А можно и через отдельный макрос SetTask будет выглядеть так:

1 Background: SetTask TS_OnGreen

И выполнится в порядке общей очереди. Т.к. проц только стартовал и очеред пуста, то практически сразу же.
или по таймеру.

1 Background: SetTimerTask TS_OnGreen,10000

Тогда мигать начнет спустя 10 секунд после запуска. Обрати внимание на то, что прямым RCALL мы указываем на метку, а через API мы передаем идентификатор задачи.

Да, не помешает напомнить, что из себя представляют макросы SetTimerTask и SetTask

123456789101112 .MACRO SetTask ldi OSRG, @0; В OSRG номер задачи rcall SendTask; через событийный диспетчер. ENDM.MACRO SetTimerTask ldi OSRG, @0; В OSRG номер задачи ldi XL, Low(@1); ldi XH, High(@1); Задержка в милисекундах rcall SetTimer; поставить таймер в очередь. ENDM

Видишь тут используется OSRG (R17) и пара Х. Для задания времени. А также функция SetTimer. Функция безопасна - она все значения сохраняет в стеке, а вот макрос нет - при вызове макроса убивается Х и OSRG. Обычно это не критично, но это надо знать и помнить. Особенно когда вызваешь это дело из прерываний.
А еще я показал тебе подробно эти макросы с целью намекнуть на то, что задачи можно ставить не только вручную, а еще и программным способом.
Например, брать из памяти цепочки идентификаторов задач и передавать их через OSRG в SendTask. Получится цифровая мегашарманка:) Главное “вращать барабан” не быстрей чем они выполняются, а то очередь сорвет. А там и до виртуальной машины не далеко… Хотя нет, к черту. Java программистов надо убивать апстену! =)

А дальше в том же ключе:
Добавляем задачу проверки АЦП. Разумеется прописываем ей TS номер в дефайнах, не буду показывать.

1234567891011121314 ; Tasks Idle: RET ;-------------------------------------------------------------------------------------- OnGreen: SBI PORTB,1; Зажечь зеленый SetTimerTask TS_OffGreen,500 RET ;-------------------------------------------------------------------------------------- OffGreen: CBI PORTB,1; Погасить зеленый SetTimerTask TS_OnGreen,500 RET ;-------------------------------------------------------------------------------------- ADC_CHK: SetTimerTask TS_ADC_CHK,1000 OUTI ADCSRA,1<<ADEN|1<<ADIE|1<<ADSC|3<<ADPS0 RET

Как видишь, она сама себя запускает каждые 1000мс, т.е. каждую секунду. А стартует там же, из секции Background

12 Background: RCALL OnGreen RCALL ADC_CHK

Осталось дождаться прерывания, поэтому ставим в код прерывание по выполнению АЦП:

Кладу его рядом с остальными прерываниями:

1234567891011121314 ADC_OK: push OSRG in OSRG,SREG; Спасаем OSRG и флаги. push OSRG IN OSRG,ADCH; Взять показание АЦП CPI OSRG,Treshold; Сравнить с порогом BRSH EXIT_ADC; Если не достигнут выход SetTask TS_RedOn; Запускаем мырг красным EXIT_ADC: pop OSRG; Восстанавливаем регистры out SREG,OSRG pop OSRG RETI; Выходим из прерывания

Появилась еще одна задача - зажечь красный. Не вопрос, добавляем, прописав везде где нужно. Я же тут укажу только исполнительную часть. Сразу же впишу и задачу гашения красного. Чтобы уж в одном флаконе.

123456789101112131415161718192021 ; Tasks Idle: RET ;-------------------------------------------------------------------------------------- OnGreen: SBI PORTB,1; Зажечь зеленый SetTimerTask TS_OffGreen,500 RET ;-------------------------------------------------------------------------------------- OffGreen: CBI PORTB,1; Погасить зеленый SetTimerTask TS_OnGreen,500 RET ;-------------------------------------------------------------------------------------- ADC_CHK: SetTimerTask TS_ADC_CHK,1000 OUTI ADCSRA,1<<ADEN|1<<ADIE|1<<ADSC|3<<ADPS0 RET ;-------------------------------------------------------------------------------------- OnRed: SBI PORTB,2 SetTimerTask TS_OffRed,300 RET ;-------------------------------------------------------------------------------------- OffRed: CBI PORTB,2 RET

Как видишь, Красный зажигается от пинка с прерывания АЦП, а гаснет по собственному пинку через 300мс. Так как АЦП проверяется раз в секунду, то если порог будет ниже, то каждую секунду будет вызов задачи RedOn и светодиод будет моргать 0.3 секундной вспышкой. Если сделать длительность вспышки больше чем частота вызова прерывания АЦП, то диод будет просто гореть, так как прерывание АЦП будет постоянно обновлять ему таймер. До тех пор пока входное напряжение на АЦП не будет выше порога, тогда диод моргнет на свою выдержку и затихнет.

Так, что там у нас следующее? UART? Ну не вопрос! Считаем, что UART у нас уже проинициализирован и готов к работе. Не верите? Глянте в секцию init.asm Так что добавляем прерывание на прием:

1234567891011121314151617181920212223242526272829303132333435 Uart_RCV: push OSRG in OSRG,SREG; Спасаем OSRG и флаги. push OSRG PUSH XL; SetTimerTask юзает Х!!! PUSH XH; Поэтому прячем его! PUSH Tmp2; Ну и Tmp2 нам пригодится IN OSRG,UDR CPI OSRG,'R'; Проверяем принятый байт BREQ Armed; Если = R - идем взводить флаг LDS Tmp2,R_flag; Если не R и флага готовности нет CPI Tmp2,0 BREQ U_RCV_EXIT; То переход на выход CPI OSRG,'A'; Мы готовы. Это 'А'? BRNE U_RCV_EXIT; Нет? Тогда выход! SetTask TS_OnWhite; Зажечь бело-лунный! U_RCV_EXIT: POP Tmp2 POP XH; Процедура выхода из прерывания POP XL pop OSRG; Восстанавливаем регистры out SREG,OSRG pop OSRG RETI; <<<<<< Выходим Armed: LDI OSRG,1; Взводим флаг готовности STS R_flag,OSRG; Сохраняем его в ОЗУ; Запускаем по таймеру задачу которая сбросит флаг через 10мс SetTimerTask TS_ResetR,10 RJMP U_RCV_EXIT; Переход к выходу

Не смотрите что прерывание такое страшное, просто тут проверка по кучи условий. Также не смущайтесь того, что выход из прерывания не в конце кода, а в самой заднице середине обработчика. Какая, собственно, разница? Никуда она из JMP не выберется. Зато сэкономил на лишнем переходе:) А в коде можно и отбивку сделать:)))))
Ну и обратите внимание на то что я сохраняю в стеке. А именно рабочий регистр OSRG, Пару Х, и дополнительный Tmp регистр. Забудешь что нибудь из этого - схватишь трудно уловимый глюк который вылезти может только через несколько месяцев агрессивного тестинга.

Что там осталось? Добавить задачи в сетку? Ну тут все просто:

1234567891011 ;-------------------------------------------------------------------------------------- OnWhite: SBI PORTB,3 SetTimerTask TS_OffWhite,1000 RET ;-------------------------------------------------------------------------------------- OffWhite: CBI PORTB,3 RET ;-------------------------------------------------------------------------------------- ResetR: CLR OSRG STS R_Flag,OSRG; Сбросить флаг готовности RET

Элементарно! Вроде бы ничего не забыл. Задача решена, еще куча ресурсов свободных осталось. Еще без проблем, не затрагивая уже написаное, можно повесить опрос клавиатуры, вывод на индикацию.
Скомпильнул, прошил, все с первого раза заработало как надо. А мозг включать даже не пришлось.

Как оценить загруженность микроконтроллера? С памятью все понятно — размеры занимаемого кода и оперативной памяти показывает компилятор, а что делать с процессорным временем? Конечно, в линейной программе можно взять и посчитать время выполнения каждой процедуры и станет ясно успеет микроконтроллер выполнить все на него повешанное или слажает в каком-нибудь критичном месте.
Куда сложней оценивать время в кооперативной операционной системе реального времени. Тут задачка получается нетривиальной — у нас куча процессов скачут через диспетчер. В ходе программирования задачи навешиваешь одну за другой, как бусинки на нить — каждый процесс обработки чего либо составляет подобную цепочку, а всего их может быть просто тьма. Ядро же у контроллера всего одно, а значит выполнять можно всего одну задачу за раз и если у нас в диспетчере скопится много критичных ко времени процессов (вообще их лучше развешивать на прерывания, но бывает и прерываний на всех не напасешься), то возможно либо переполнение очереди диспетчера, либо превышение времени ожидания, что тоже не праздник.
Самое западло в том, что умозрительно отлаживать такие вещи довольно сложно. Единственный вариант — рисовать временные диаграммы запуска каждой задачи и смотреть где у нас узкие места. Еще можно попробовать в AVR Studio поставить Break Point на переполнение диспетчера, но студия не сэмулирует всю ту прорву периферии, а в пошаговой отладке этого не увидеть — да и момент надо подобрать так, чтобы все навалилось.

В один момент мне пришла в голову одна идея — а почему бы не заставить рисовать временные диаграммы работы задач сам контроллер? Это же просто! Берем и в диспетчере, перед вызовом задачи выставляем бит порта в 1. А когда диспетчер задач опустошается полностью, то есть выполняется переход на Idle — сбрасываем бит в 0. В результате, у нас на выходе будет подобие ШИМ. Если постоянно крутится Idle — будут нули перманентно. Если же проц в поте лица гонит через себя непрерывно код, то будут высокий уровнь сплошняком. А если все прерывисто — что то ШИМообразное. Причем чем больше загрузка процессора тем выше заполнение. Можно поставить интегрирующую RC цепочку и получим аналоговый сигнал. Хоть на стрелочный индикатор заводи:). Сказано — сделано.

Получилось вот так:

Пока задача шлет раз в секунду байт по UART, а остальное время Idle, т.е. бездельничает.

А вот мы добавили опрос клавиатуры. Стало бодрей — иголки увеличились числом. Каждая иголка запуск процесса.

Вот одна иголка крупным планом.

Вызваю процедуру записи в EEPROM — во как, сразу же сожралось куча времени на выполнение. А записалось всего 6 байт. Обратите внимание на масштаб времени. Насколько запись в EEPROM дольше выполнения обычного кода.

Но одними иголками сыт не будешь. Не прикольно. Как бы задачки эти выделить. Чтобы можно было понять кто есть кто. Решение элементарное — выделить для отладочных целей не один бит, а поболее. У меня тут в запасе нашлось целых 8 ног. Соответственно я сразу же по заходу в задачу вывел в порт ее номер, а из порта загнал в R-2R ЦАП. Картина стала наглядней — теперь высота иголки у всех задач стала разной.

Чем меньше номер, тем меньше напряжение с ЦАП. Мало того, если задачи вызываются последовательно, а не через очередь таймеров то будут не иголки, а лесенки.

Вот что мы видим. Иголки номер 1 - это сканирование клавиатуры. Те что повыше, номер 2 — это пинг, отсыл одного байта по UART, а вот средние — отправка пачек данных через тот же UART.

Вообще применение осциллографа это могучее средство для реалтаймовой отладки. Не обязательно таким замудреным способом как у меня. Это я больше для прикола и наглядности всякие АЦП вешаю. Достаточно же просто бит выводить и смотреть на осциллографе. Еще можно добавлять программные ловушки. Например, переполнилась критическая переменная — выдали бит. А чтобы не ловить на экране осциллографа этих тараканов, проще этот бит завести на какую-либо следящую систему, да хоть на триггер. Взвел следилку и гоняй свою отлаживаемую программу в хвост и в гриву, нагружай пока не позеленеет от натуги. А если где то что то сорвет, то выскочит твой бит, триггер его запомнит и там хоть сирену включай — ахтунг! Error!!!

Бывает такая ситуация, когда надо на один периферийный девайс повесить много разных задач, а он всего один и что то надо с этим делать.

Простой пример — таймер и его прерывание по переполнению.
Мы можем задавать выдержку и по прерыванию делать какие-нибудь операции. Но если в один момент времени мы хотим чтобы таймер по прерванию сделал одну операцию, а потом другую, третью. Да сколько угодно, в зависимости от состояния. А вектор один.

Или, например, USART. Нам запросто может потребоваться, чтобы в зависимости от режима на прерывание по приходу байта выполнялся разный код. В одном режиме - выдача приветствия, в другом посыл матом в баню. В третьем удар в голову. А вектор один.

Конечно, можно добавить в обработчик прерывания switch-case конструкцию и по выбору режима перейти на нужный участок кода, но это довольно громоздко, а самое главное — время перехода будет разное, в зависимости от того в каком порядке будет идти опрос-сравнение switch-case структуры.

То есть в свитче вида:

1234567 switch(x) { 1: Действие 1 2: Действие 2 3: Действие 3 4: Действие 4 }

Будет последовательное сравнение х вначале с 1, потом с 2, потом с 3 и так до перебора всех вариантов. А в таком случае реакция на Действие 1 будет быстрей чем реакция на Действие 4. Особо важно это при расчете точных временных интервалов на таймере.

Но есть простое решение этой проблемы — индексный переход. Достаточно перед тем как мы начнем ожидать прерывание предварительно загрузить в переменные (а можно и сразу в индексный регистр Z) направление куда нам надо перенаправить наш вектор и воткнуть в обработчик прерывания индексный переход. И вуаля! Переход будет туда куда нужно, без всякого сравнения вариантов.

В памяти создаем переменные под плавающий вектор:

12 Timer0_Vect_L:.byte 1; Два байта адреса, старший и младший Timer0_Vect_H:.byte 1

Подготовка к ожиданию прерывания проста, мы берем и загружаем в нашу переменную нужным адресом

1234567 CLI; Критическая часть. Прерывания OFF LDI R16,low(Timer_01); Берем адрес и сохраняем STS Timer0_Vect_L,R16; его в ячейку памяти. LDI R16,High(Timer_01); Аналогично, но уже со старшим вектором STS Timer0_Vect_H,R16 SEI; Прерывания ON

Все, можно запускать таймер и ждать нашего прерывания. С другими случаями аналогично.

А обработчик получается вида:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 ;=============================; Вход в прерывание по переполнению от Timer0;============================= TIMER_0: PUSH ZL; сохраняем индексный регистр в стек PUSH ZH; т.к. мы его используем PUSH R2; сохраняем R2, т.к. мы его тоже портим IN R2,SREG; Извлекем и сохраняем флаговый регистр PUSH R2; Если не сделать это, то 100% получим глюки LDS ZL,Timer0_Vect_L; загружаем адрес нового вектора LDS ZH,Timer0_Vect_H; оба байта. CLR R2; Очищаем R2 OR R2,ZL; Проверяем вектор на ноль. Иначе схватим аналог OR R2,ZH; reset'a. Проверка идет через операцию OR BREQ Exit_Tm0; с накоплением результата в R2; так мы не портим содержимое Z и нам не придется; загружать его снова IJMP; Уходим по новому вектору; Выход из прерывания. Exit_Tm0: POP R2; Достаем и восстанавливаем регистр флагов OUT SREG,R2 POP R2; восстанавливаем R2 POP ZH; Восстанавливаем Z POP ZL RETI; Дополнительный вектор 1 Timer_01: NOP; Это наши новые вектора NOP; тут мы можем творить что угодно NOP; желательно недолго - в прерывании же NOP; как никак. Если используем какие другие NOP; регистры, то их тоже в стеке сохраняем RJMP Exit_Tm0; Это переход на выход из прерывания; специально сделал через RJMP чтобы; Дополнительный вектор 2; сэкономить десяток байт на коде возврата:))) Timer_02: NOP NOP NOP NOP NOP RJMP Exit_Tm0; Дополнительный вектор 3 Timer_03: NOP NOP NOP NOP NOP RJMP Exit_Tm0

Реализация для RTOS
Но что делать если у нас программа построена так, что весь код вращается по цепочкам задач через диспетчер RTOS? Просчитать в уме как эти цепочки выполняются относительно друг друга очень сложно. И каждая из них может попытаться завладеть таймером (конечно не самовольно, с нашей подачи, мы же программу пишем, но отследить по времени как все будет сложно).
В современных больших осях на этот случай есть механизм Mutual exclusion — mutex. Т.е. это своего рода флаг занятости. Если какой нибудь процесс общается, например, с UART то другой процесс туда байт сунуть не смеет и покорно ждет пока первый процесс освободит UART, о чем просемафорит флажок.

В моей RTOS механизмов взаимоисключений нет, но их можно реализовать. По крайней мере сделать некоторое минимальное подобие. Полноценную реализацию всего этого барахла я делать не хочу, т.к. моей целью является удержания размера ядра на уровне 500-800 байт.
Проще всего зарезервировать в памяти еще один байт — переменную занятости. И когда один процесс захватывает ресурс, то в эту переменную он записывает время когда ориентировочно он его освободит. Время идет в тиках системного таймера которое у меня 1ms.
Если какой либо другой процесс попытается обратиться к этому же аппаратному ресурсу, то он вначале посмотрит на состояние его занятости, считает время в течении которого будет занято и уйдет покурить на этот период — загрузит сам себя в очередь по таймеру. Там снова проверит и так далее. Это простейший вариант.

Проблема тут в том, что если на один вектор много желающих будет, то процессы так и будут бегать вокруг да около, словно бухая молодежь вокруг единственного сортира на площади в период праздничных гуляний. У кого нибудь да мочевой пузырь не выдержит — запорет алгоритм. А у кого тут фиг угадаешь, т.к. промоделировать это будет сложновато.

Решение проблемы — добавление еще одной очередной цепочки, на этот раз уже на доступ к ресурсу. Чтобы он не простаивал вообще. Т.е. один выскочил, тут же второй, третий и так далее пока все процессы не справят свою нужду в какой нибудь там USART.
Недостаток очевиден — еще одна очередь это дополнительная память, дополнительный код, дополнительное время. Можно, конечно, извратиться и на очередь к вектору натравить код диспетчера основной цепи. Но тут надо все внимательно отлаживать, ведь вызываться он будет по прерыванию! Да и громоздко, требуется лишь тогда, когда у нас много желающих.

Второе решение — выкинуть переменную времени занятости, оставив только флаг “Занято!”. А процесс который пытается обратиться не убегает покурить, а отскакивает на пару шагов назад — на конец очереди задач и сразу же ломится обратно. Народ вокруг сортира не вокруг бегает, а толкется локтями у входа по принципу кто первый пролезет.
Недостаток другой — большая нагрузка на главный конвеер, куча запросов на постановку в очередь так недолго распухнуть на всю оперативку и повстречаться со стеком, а это черевато глобальным апокалипсисом.

Разумеется таймер тут приведен для примера, большую часть задач можно решить системным таймером RTOS, но если нужна вдруг меньшая дискретность или высокая скорость реакции на событие (а не пока главный конвеер дотащит задачу до исполнения), то механим управляемых прерываний, ИМХО, то что доктор прописал.

С внешним миром микроконтроллер общается через порты ввода вывода. Схема порта ввода вывода указана в даташите:

Но новичку там разобраться довольно сложно. Поэтому я ее несколько упростил:

Итак, что же представляет собой один вывод микроконтроллера. Вначале на входе стоит небольшая защита из диодов, она призвана защитить ввод микроконтроллера от превышения напряжения. Если напряжение будет выше питания, то верхний диод откроется и это напряжение будет стравлено на шину питания, где с ним будет уже бороться источник питания и его фильтры. Если на ввод попадет отрицательное (ниже нулевого уровня) напряжение, то оно будет нейтрализовано через нижний диод и погасится на землю. Впрочем, диоды там хилые и защита эта помогает только от микроскопических импульсов от помех. Если же ты по ошибке вкачаешь в ножку микроконтроллера вольт 6-7 при 5 вольтах питания, то никакой диод его не спасет.

Конденсатор, нарисованный пунктиром, это паразитная емкость вывода. Хоть она и крошечная, но присутствует. Обычно ее не учитывают, но она есть. Не забивай голову, просто знай это, как нибудь я тебе даже покажу как её можно применить;)

Дальше идут ключи управления. Это я их нарисовал рубильниками, на самом деле там стоят полевые транзисторы, но особой сути это не меняет. А рубильники наглядней.
Каждый рубильник подчинен логическому условию которое я подписал на рисунке. Когда условие выполняется — ключ замыкается. PIN, PORT, DDR это регистры конфигурации порта.

Есть в каждом контроллере AVRPIC есть тоже подобные регистры, только звать их по другому).

Например, смотри в даташите на цоколевку микросхемы:

Видишь у каждой почти ножки есть обозначение Pxx. Например, PB4 где буква “B” означает имя порта, а цифра — номер бита в порту. За порт “B” отвечают три восьмиразрядных регистра PORTB, PINB, DDRB, а каждый бит в этом регистре отвечает за соответствующую ножку порта. За порт “ А ” таким же образом отвечают PORTA, DDRA, PINA.

PINх
Это регистр чтения. Из него можно только читать. В регистре PINx содержится информация о реальном текущем логическом уровне на выводах порта. Вне зависимости от настроек порта. Так что если хотим узнать что у нас на входе — читаем соответствующий бит регистра PINx Причем существует две границы: граница гарантированного нуля и граница гарантированной единицы — пороги за которыми мы можем однозначно четко определить текущий логический уровень. Для пятивольтового питания это 1.4 и 1.8 вольт соответственно. То есть при снижении напряжения от максимума до минимума бит в регистре PIN переключится с 1 на 0 только при снижении напруги ниже 1.4 вольт, а вот когда напруга нарастает от минимума до максимума переключение бита с 0 на 1 будет только по достижении напряжения в 1.8 вольта. То есть возникает гистерезис переключения с 0 на 1, что исключает хаотичные переключения под действием помех и наводок, а также исключает ошибочное считывание логического уровня между порогами переключения.

При снижении напряжения питания разумеется эти пороги также снижаются, график зависимости порогов переключения от питающего напряжения можно найти в даташите.

DDRx
Это регистр направления порта. Порт в конкретный момент времени может быть либо входом либо выходом (но для состояния битов PIN это значения не имеет. Читать из PIN реальное значение можно всегда).

  • DDRxy=0 — вывод работает как ВХОД.
  • DDRxy=1 вывод работает на ВЫХОД.

PORTx
Режим управления состоянием вывода. Когда мы настраиваем вывод на вход, то от PORT зависит тип входа (Hi-Z или PullUp, об этом чуть ниже).
Когда ножка настроена на выход, то значение соответствующего бита в регистре PORTx определяет состояние вывода. Если PORTxy=1 то на выводе лог1, если PORTxy=0 то на выводе лог0.
Когда ножка настроена на вход, то если PORTxy=0, то вывод в режиме Hi-Z. Если PORTxy=1 то вывод в режиме PullUp с подтяжкой резистором в 100к до питания.

Есть еще бит PUD (PullUp Disable) в регистре SFIOR он запрещает включение подтяжки сразу для всех портов. По дефолту он равен 0. Честно говоря, я даже не знаю нафиг он нужен — ни разу не доводилось его применять и даже не представляю себе ситуацию когда бы мне надо было запретить использование подтяжки сразу для всех портов. Ну да ладно, инженерам Atmel видней, просто знай что такой бит есть. Мало ли, вдруг будешь чужую прошивку ковырять и увидишь что у тебя подтяжка не работает, а вроде как должна. Тогда слазаешь и проверишь этот бит, вдруг автор прошивки заранее где то его сбросил.

Общая картина работы порта показана на рисунке:

Теперь кратко о режимах:

  • Режим выхода
    Ну тут, думаю, все понятно — если нам надо выдать в порт 1 мы включаем порт на выход (DDRxy=1) и записываем в PORTxy единицу — при этом замыкается верхний ключ и на выводе появляется напряжение близкое к питанию. А если надо ноль, то в PORTxy записываем 0 и открывается уже нижний вентиль, что дает на выводе около нуля вольт.
  • Вход Hi-Z — режим высокоимпендансного входа.
    Этот режим включен по умолчанию. Все вентили разомкнуты, а сопротивление порта очень велико. В принципе, по сравнению с другими режимами, можно его считать бесконечностью. То есть электрически вывод как бы вообще никуда не подключен и ни на что не влияет. Но! При этом он постоянно считывает свое состояние в регистр PIN и мы всегда можем узнать что у нас на входе — единица или ноль. Этот режим хорош для прослушивания какой либо шины данных, т.к. он не оказывает на шину никакого влияния. А что будет если вход висит в воздухе? А в этом случае напряжение будет на нем скакать в зависимости от внешних наводок, электромагнитных помех и вообще от фазы луны и погоды на Марсе (идеальный способ нарубить случайных чисел!). Очень часто на порту в этом случае нестабильный синус 50Гц — наводка от сети 220В, а в регистре PIN будет меняться 0 и 1 с частотой около 50Гц
  • Вход PullUp — вход с подтяжкой.
    При DDRxy=0 и PORTxy=1 замыкается ключ подтяжки и к линии подключается резистор в 100кОм, что моментально приводит неподключенную никуда линию в состояние лог1. Цель подтяжки очевидна — недопустить хаотичного изменения состояния на входе под действием наводок. Но если на входе появится логический ноль (замыкание линии на землю кнопкой или другим микроконтроллером/микросхемой), то слабый 100кОмный резистор не сможет удерживать напряжение на линии на ур

Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: