AVR. Учебный курс. Операционная система. Таймерная служба

Третья часть марлезонского балета описалова самопальной операционной системы для 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мкс. Но тут падает точность. Так что если такое потребуется, то делать это на другом таймере.

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

Продолжение следует…

26 thoughts on “AVR. Учебный курс. Операционная система. Таймерная служба”

  1. В плане самопальных ОС и использования многозадачности, не лишним будет почитать отчет об ошибках, приведших к многочисленным жертвам на медицинском девайсе Therac-25. Там тоже была самопальная псевдо-ОС, и «гонки» (race conditions) на переменных, которые используются для синхронизации задач, и передачи параметров между ними.

    Вот здесь оригинал подробного анализа софта и ошибок http://sunnyday.mit.edu/papers/therac.pdf, к сожалению полного перевода документа на русский с первого раза что-то обнаружить не получается. Есть только «вольные толкования», которые в большинстве неправильны, ибо делают выводы, в которых заинтересован собственно толкователь.

    1. Медицинские девайсы это вообще отдельная тема. Там вообще в идеале все делать на жесткой логике.

    2. Мистер tchicago, если что, все ОС — самопальные. Одно — подточенные напильником чужие разработки, выкинутые на свалку из-за полной бесперспективности. Да, да, я про дядю Билла.
      Другие — самопально сделанные одним чуваком, потом набежало море пацанов с напильниками. Это про семейство таких, с пингвинчиком.
      Из того, что на сарае в Индии, где клепают код, висит надпись — эппл аутсорс, это замечательное строение не перестаёт быть сараем. И так далее. Всё зависит от квалификации, таланта и целеустремлённости одного-нескольких разработчиков.
      Так что не стоит раньше времени вешать ярлыки. Совсем не стоит…
      Всё-таки, если нечто продаётся, совсем не факт, что оно стоит своих денег.
      Если при установке на экране пишет — «теперь ещё надёжнее» — так на сарае тоже много чего пишут.
      Если упаковка красивая — так в соседней лавочке бомжеватый раскосый продавец и не так упаковать может.
      Главное — суть.

      1. То есть ярлык повесим когда будет авария с человеческими жертвами, но на ошибках других учиться ни в коем случае не будем?

        Так и запишем.

        1. А также, когда упадёт астероид. Интересный вывод. Что-то вы передёргиваете. Где у меня написано, что на чужих ошибках учится не надо? Демагогией не занимайтесь.

    3. Прочитал-просмотрел данный документ. Весьма познавательно с позиции надёжности системы, и … грустно, удивила тупость отвественных людей.

      DI HALT: «Медицинские девайсы это вообще отдельная тема. Там вообще в идеале все делать на жесткой логике.»

      Вот в этом-то и была одна из проблем, если я правильно понимаю фразу «жесткоя логика» и смысл прочитанного. Там софт был так заточен, что его не сразу стали подозревать.

      В конце документа есть выводы.
      Для себя я, например, отметил следующие:
      -нельзя полагаться только на софт
      -safe versus friendly user interface
      -надёжный не значит безопасный

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

      1. Знакомая задача. Помню делал блок управления пилорамой. Так там чтобы запустить диск нужно было кучу сигналов подать в нужной последовательности + контроль всего этого через внешние датчики в обход управления. Так, что случайно она не могла запуститься.

  2. Обычно у меня программные таймеры за задачами закреплены жестко, поэтому достаточно в обработчике прерывания делать декремент, если не 0. Это короче. Некоторые программные таймеры делаю и двухбайтными. Задаче остается только бросить какое-то значение в один из таймеров и потом проверять его на 0. Или по обнулению в прерывании выставлять соответствующий флажок, который проверяется задачей.
    Для больших времен в один из программных таймеров, если в нем 0, гружу 999, получаю счет до секунды. При его обнулении также срабатывает проверка и инкремент или декремент программных таймеров, которые ведут счет в секундах, в том числе и часы с календарем (в прерывании можно выставить для часов флажок раз в сек. или минуту, а остальное делать уже в одной из задач главного цикла).

  3. А если такая ситуация, что мы не знаем, через сколько мс должна выполняться след. задача?
    например, текущая задача ждет нажатия кнопки от юзера, и продолжать нельзя, пока юзер не нажмет. Что тогда?

    1. Циклически (скажем 100 раз в секунду) опрашивать кнопку. Как только кнопка будет нажата — передавать управление другой задаче которая будет делать дело.

  4. Что-то я не въехал с FF. когда младшая часть перекидывается через 0 — то будет FF но старшая часть же еще не закончилась, а прога посчитает что счетчика нет.
    Не проще ли было выделить парочку слов, где хранить активность счетчика битом, можно в 2-х битах, для флажка с задач с повторами.
    А на практике чаще вообще нужно другой счетчик по дате и времени.

    1. Так на FF проверяется не задержка, а код задачи в таймерной службе. Если он FF то таймера нет. А задержка тикает как есть

  5. Привет,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, всё равно вычитется, а не складывается. Полная путаница……Выручай!!!

  6. Ура!!! я кажется разобрался что это такое. И кажись теперь догадываюсь на чем построены промышленные контроллеры вроде МЕЛСЕКа ФХ 2Н от Митсубиши. Там что то подобное — дохерища таймеров и выполнение программы в «сканирующем» режиме. Просто пару лет назад пришлось такой программировать на «релейной» логике — конечно интересно но книга — даташит размером в 700 стр просто утомила.
    DI Спасибо за матерьял. Теперь бы еще все в голове по разным кучкам разложить /* и закоментировать*/

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

    1. В этой реализации так не получится. Можно попробовать разбить фильтр на две задачи. Начало и конец и вызывать их очередью. Или заюзать более сложную RTOS где есть возможность полноценного возврата к прерванной процедуре через диспетчер. SCMRtos или SalvoRTOS да много их.

    1. Когда мы со знаком работаем, нам это важно. Так он принудительно константу приводит к нужному разряду.

      1. Вообщем набросал то как я понимаю эту програмку может кто и подправит
        проект Trashduino-rtos
        Первым делом нам нужно инициализировать те задачи которые хотим выполнять
        Это делается в макросом SetTimerTask TS_Fire,1000 там путем вызова функции SetTimer
        Мы устанавливаем в озу в массиве TimersPool информацию на эту команду
        1 байт это номер команды
        2,3 байты это промежуток времени через который будет выполняться эта команда

        При наступления прерывания (через каждую 1 мс) Проверяем TimersPool есть ли там инфа хоть по какой то команде если есть то производим отнимание от времени что мы задали (2 младший и 3 байт старший) так как
        SetTimerTask TS_Fire,1000 так как 1000 то команда Fire будет выполняться через каждую 1 секунду 1000*1мс. И когда эти 2 байта (2,3) обнуляться то программа перейдем на выполнение SendTask
        Где мы устанавливаем в массиве TaskQueue (ОЗУ) номер нашей задачи в порядке очереди(находим ячейку еще не заполненой = 0xff и пишем туда наш номер задачи.
        Затем снова возвращаемся на поиск информации по задачам TimersPool отнимаем время если нули ставим в очередь задач в конце полной проверки 15 байтного TimersPool выходим из прерывания
        В основной программе переходим на выполнение ProcessTaskQueue где проверяем нашу очередь есть ли там какой номер задачи есть ли есть то берем этот номер и по нему находим адрес той метки с которой и будет выполняться наша задача .Также производим сдвиг очереди влево переписывая старшие во младшие (массив TaskQueue) .После этого переходим уже непосредственно на метку нашей команды.

  8. Привет, Di!

    В строках 16,17 кода функции SetTimer написано следующее:

    subi ZL, Low(-3) ; Выбираем следующий
    sbci ZH, High(-3) ; Z+=2

    По-моему, эти строки увеличивают Z на 3, а не на два (как указано в комментарии). Я прав?

  9. Привет!

    Не понимаю в коде следующего:
    Comp1L02: ldd OSRG, z+2
    sbci OSRG, high(1)
    std z+2, OSRG
    brne Comp1L03:
    brts Comp1L03:
    Что вычитается из OSRG?
    Объясните пожалуйста, что такое high(1) ?

Добавить комментарий

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha