Третья часть марлезонского балета описалова самопальной операционной системы для AVR.
Итак, у нас есть очередь задач и общая логика работы системы. Но одной очереди задач с диспетчером мало. Нужно распределять задачи по времени, задавать интервалы, запускать отложенные задачи. Всем этим будет заниматься служба таймеров.
В чем ее суть ее работы:
Время разбивается на интервалы, скажем, по 1мс. Такой выдержки хватает для большинства задач. Также у нас должна быть очередь программных таймеров, размещенных в ОЗУ. На каждый таймер отводится три байта:
Первый — идентификатор задачи. Два других — выдержка в миллисекундах.
Два байта позволяют организовать выдержку в 65.5 секунд. Конечно, можно сделать и больше, если отвести на временную выдержку три или даже четыре байта, но такие большие временные интервалы пригождаются редко, поэтому проще перехватиться через дополнительную переменную для конкретной задачи, а не нагружать таймерную службу обсчетом дополнительных байт.
Один из свободных аппаратных таймеров программируем на то, чтобы он генерировал прерывание каждые 0.001с
О работе аппаратных таймеров написано в разделе про программирование встроенной периферии. Пока можешь не заморачиваться. Суть лишь в том, что настроенный таймер тикает независимо от главной программы и в заданное время выдает прерывание.
По прерыванию мы берем из очереди таймеров первый байт и сравниваем его с 0xFF, за 0xFF принято неактивное состояние. Если же там не 0xFF, то значит это идентификатор задачи, а таймер активен. Поэтому берем третий байт, декрементируем его, если он стал равен нулю декрементируем второй байт и если оба байта не стали равны нулю переходим к проверке следующего байта. В случае если время истекло, то идентификатор задачи пихается в очередь задач на исполнение.
Обработчик прерывания таймера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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] |
Сам макрос развертывается в такой код:
1 2 3 4 | ldi OSRG, [Task] ldi XL, Low([Time]) ; Задержка в милисекундах ldi XH, High([Time]) ; От 1 до 65535 rcall SetTimer |
Как видим, тут используется регистровая пара Х и вызывается функция постановки таймера. Про использование ресурсов в этих макросах и процедурах надо помнить и сохранять их в стеке если постановка таймера идет из прерывания.
Сама функция SetTimer работает просто:
Расположение: kernel.asm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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мкс. Но тут падает точность. Так что если такое потребуется, то делать это на другом таймере.
Но, в целом, несмотря на недостатки, очень удобная служба получилась. А главное вытыкается в момент. Прикол с апдейтом таймера кажется лишним, но реально часто пригождается. Например, когда по условию надо отложить событие. Берешь и перезаписываешь таймер, подобно программному вачдогу. А если надо две одинаковые задачи по таймеру сделать в разное время, то никто не запрещает добавить ее в таблицу переходов на новый идентификатор и будет тебе профит.
Продолжение следует…





В плане самопальных ОС и использования многозадачности, не лишним будет почитать отчет об ошибках, приведших к многочисленным жертвам на медицинском девайсе Therac-25. Там тоже была самопальная псевдо-ОС, и «гонки» (race conditions) на переменных, которые используются для синхронизации задач, и передачи параметров между ними.
Вот здесь оригинал подробного анализа софта и ошибок , к сожалению полного перевода документа на русский с первого раза что-то обнаружить не получается. Есть только «вольные толкования», которые в большинстве неправильны, ибо делают выводы, в которых заинтересован собственно толкователь.
Медицинские девайсы это вообще отдельная тема. Там вообще в идеале все делать на жесткой логике.
Мистер tchicago, если что, все ОС — самопальные. Одно — подточенные напильником чужие разработки, выкинутые на свалку из-за полной бесперспективности. Да, да, я про дядю Билла.
Другие — самопально сделанные одним чуваком, потом набежало море пацанов с напильниками. Это про семейство таких, с пингвинчиком.
Из того, что на сарае в Индии, где клепают код, висит надпись — эппл аутсорс, это замечательное строение не перестаёт быть сараем. И так далее. Всё зависит от квалификации, таланта и целеустремлённости одного-нескольких разработчиков.
Так что не стоит раньше времени вешать ярлыки. Совсем не стоит…
Всё-таки, если нечто продаётся, совсем не факт, что оно стоит своих денег.
Если при установке на экране пишет — «теперь ещё надёжнее» — так на сарае тоже много чего пишут.
Если упаковка красивая — так в соседней лавочке бомжеватый раскосый продавец и не так упаковать может.
Главное — суть.
То есть ярлык повесим когда будет авария с человеческими жертвами, но на ошибках других учиться ни в коем случае не будем?
Так и запишем.
А также, когда упадёт астероид. Интересный вывод. Что-то вы передёргиваете. Где у меня написано, что на чужих ошибках учится не надо? Демагогией не занимайтесь.
Прочитал-просмотрел данный документ. Весьма познавательно с позиции надёжности системы, и … грустно, удивила тупость отвественных людей.
DI HALT: «Медицинские девайсы это вообще отдельная тема. Там вообще в идеале все делать на жесткой логике.»
Вот в этом-то и была одна из проблем, если я правильно понимаю фразу «жесткоя логика» и смысл прочитанного. Там софт был так заточен, что его не сразу стали подозревать.
В конце документа есть выводы.
Для себя я, например, отметил следующие:
-нельзя полагаться только на софт
-safe versus friendly user interface
-надёжный не значит безопасный
Параллельно с софтом должна быть и «железная» защита. Например, у меня сейчас стоит задача написать прошивку так, чтоб не сгорели дорогие детальки))
Знакомая задача. Помню делал блок управления пилорамой. Так там чтобы запустить диск нужно было кучу сигналов подать в нужной последовательности + контроль всего этого через внешние датчики в обход управления. Так, что случайно она не могла запуститься.
Обычно у меня программные таймеры за задачами закреплены жестко, поэтому достаточно в обработчике прерывания делать декремент, если не 0. Это короче. Некоторые программные таймеры делаю и двухбайтными. Задаче остается только бросить какое-то значение в один из таймеров и потом проверять его на 0. Или по обнулению в прерывании выставлять соответствующий флажок, который проверяется задачей.
Для больших времен в один из программных таймеров, если в нем 0, гружу 999, получаю счет до секунды. При его обнулении также срабатывает проверка и инкремент или декремент программных таймеров, которые ведут счет в секундах, в том числе и часы с календарем (в прерывании можно выставить для часов флажок раз в сек. или минуту, а остальное делать уже в одной из задач главного цикла).
А если такая ситуация, что мы не знаем, через сколько мс должна выполняться след. задача?
например, текущая задача ждет нажатия кнопки от юзера, и продолжать нельзя, пока юзер не нажмет. Что тогда?
Циклически (скажем 100 раз в секунду) опрашивать кнопку. Как только кнопка будет нажата — передавать управление другой задаче которая будет делать дело.
Что-то я не въехал с FF. когда младшая часть перекидывается через 0 — то будет FF но старшая часть же еще не закончилась, а прога посчитает что счетчика нет.
Не проще ли было выделить парочку слов, где хранить активность счетчика битом, можно в 2-х битах, для флажка с задач с повторами.
А на практике чаще вообще нужно другой счетчик по дате и времени.
Так на FF проверяется не задержка, а код задачи в таймерной службе. Если он FF то таймера нет. А задержка тикает как есть
Привет,Di! Не понятен один момент, чисто математический
Вот он:
subi ZL,Low(-3)
sbci ZH,High(-3)
Что не ясно: Здесь ты реализуешь сложение с учётом переноса( идея как и зачем понятно,но результат мне кажется не совсем верным):
Subi Zl,Low(-3) свою работу делает — складывает, но при этом всегда встаёт флаг переноса C, даже когда он и не нужен 3-(-3)=6, переноса то нет, но флаг при этом C=1( встал:))) Впринципе, как получается флаг я понимаю, но для правильности общего сложения он не нужен, ведь если просто 3+3=6 (флага нет), а если 3-(-3)=6 (флаг есть). Идём дальше
sbci ZH, High(-3) Почему так? Ведь High(-3)=11111111. К тому же перенос С=1, всё равно вычитется, а не складывается. Полная путаница……Выручай!!!