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

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

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

Запись опубликована в рубрике AVR. Учебный курс с метками , , , , . Добавьте в закладки постоянную ссылку.

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

  1. tchicago говорит:

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

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

    • DI HALT говорит:

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

    • ArgusB говорит:

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

      • tchicago говорит:

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

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

        • ArgusB говорит:

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

    • pspost говорит:

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

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

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

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

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

      • DI HALT говорит:

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

  2. SWG говорит:

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

  3. enq говорит:

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

    • DI HALT говорит:

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

  4. par говорит:

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

  5. AlexSkv говорит:

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

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