AVR. Учебный курс. Операционная система. Диспетчер задач.

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

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

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

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

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

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

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

Расположение: Trash-rtos.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
58
;=========================================================
;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 — главный файл программы, в самом низу, в конце ПЗУ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;========================================================================
; 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]

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

1
2
3
4
5
6
7
8
9
10
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 — файл макроопределений ядра

1
2
3
4
5
6
7
8
9
10
.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 — главный файл программы, самое начало. Где идет разметка ОЗУ

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

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

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

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

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

Сам обработчик очереди несложен. Распологается в файле 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
58
59
60
61
62
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

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

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

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

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

1
		SetTask	 [task]

Сама процедура SendTask работает тоже несложно, она всего лишь ставит задачу в очередь.
Расположение: 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
; 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

Архив с исходниками и работающим проектом для ATMega8

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

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

71 thoughts on “AVR. Учебный курс. Операционная система. Диспетчер задач.”

  1. Возможно, забегу на перёд, но вот такие вот вопросы появились. Я так понял, что тут «задача» и «событие» — это синонимы? И задача выполняется полность, т.е. пока не упрётся в свой RET, проц будет полностью отдан ей?

    После публикации первой затравочной части, я тоже задумался, как бы сделать ОС на мк. Можно было бы отдать один таймер чисто под ОС, и по его интерапту делать квантование времени, отдаваемого под задачу (по ходу сохраняя PC). Т.е. делать прыжки по очереди задач через определённые промежутки времени, не важно, завершилась задача или нет. Память и стек у каждой задачи свои (внешняя SRAM на крайняк). А размер самой задачи не имел бы значения, хоть там цикл крутить. Праавда, отсюда доп. правила на написание задач…

    Жаль в исходниках нет везде таких подробных комментов, как в статье)

    1. Фактически да. Синонимы.

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

      Квантовать с полноценным отрубом текущей задачи на АВР крайне сложно. Приходится очень многое сохранять, а ОЗУ мало. Одним PC тут не ограничешься, нужно еще все регистры сохранять, флаги. Опять же стэк у каждого свой, выделенная память. Я думал над этим, но все получается слишком жирно, а хочется иметь универсальную крошечную ОС, способную развернуться даже на Тини13.

      1. Да, ещё и регистры. Можно и не все, это уже зависит от логики ОС. И, в общем, выделать под задачу в сумме байт по 30-60.

        Просто в данном примере описан, скорее, менеджер очереди, чем ОС. ОС, в моём понимании, предполагает захват и круговую оборону всего железа. Для задач — API.

        …крошечную ОС. Просто само словосочетание «Операционная система» слишком большое чтобы засунуть его в Тайни).

        1. А ОС начинается с диспетчера задач. Тут вопрос скорей терминологий, где у крокодила отрубить хвост :)
          Сюда же можно довесить и полноценный API и приоритеты и много чего еще. Если это действительно надо.

      2. Вообще — то для малых контроллеров, особенно типа Tiny 13,15, — изменяемая очередь задач, по — моему, избыточна. Другое дело — к примеру, на Меге 128, с файловой системой, консолью, дисплеем, флэшкой на гигобайт…
        А для простых я в главном цикле просто просматриваю задачи по очереди (их редко бывает больше десятка), проверяя их флаги состояния, и если задача неактивна — пропускаю ее. Это занимает меньше ресурсов, чем перезапись очереди.
        Сами же задачи делаю более крупными, но выполняю их поэтапно, выставляя по ходу флажки в слове состояния задачи, запуская и проверяя контрольные таймеры.
        Критичные события работают по прерываниям, совершая минимум действий (например, считывание принятого с UART байта в буфер), и выставляя флаги для обработки их в соответствующей задаче. В результате кольцо задач крутится довольно быстро.

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

        1. Для 13-15 избыточно, да. Но возможно извратиться. А вот уже на Тини2313 очень удобно. А главное получается единый шаблон в который можно уложить практически любую программу. Резко ускоряется написание логики, а оверхед получается очень незначительным, т.к. всю структуру в любом случае пришлось бы с нуля писать, а тут уже все готовое. Где то избыточное, но в большинстве случаев это оправдано. А по поводу проверки флажков… если у тебя на каждую задачу по три флажка, десяток задач, то это сколько же проверок условий будет. Вряд ли оно будет быстрей чем сдвиг очереди. Тем более очередь может быть и короткой, на считаные единицы задач.

          1. Флажки я обьединяю в слова состояния задачи. Если байт слова состояния задачи = 0, пропускаю. Это всего 1-2 команды: загрузка слова состояния в аккумулятор и пропуск вызова, если 0, (или, наоборот, вызов, если не 0 — смотря какой контроллер). Подпрограмма задачи побитно проверяет остальные флажки, соответствующие этапам выполнения задачи, и выполняет соответствующие действия, сбрасывая и устанавливая флажки. Обработчик прерывания обрабатывает срочные события и также выставляет или сбрасывает флажки в слове состояния соответствующей задачи, а также крутит программные таймеры. Если флажков у задачи много, делаю 2-3 байта, располагая в порядке нужности. (В главном цикле проверяется только основной бйт).
            У PICов, например, короткий стек (кроме старших моделей), зато есть команды проверки бит с переходом (пропуском) по результатам, для них мой подход получается проще. Для I8080 и I8048 когда-то тоже получалось неплохо.
            А сдвинуть очередь даже из 5 задач — это переписать с места на место до десятка байт, а это 2-3 десятка команд… Добавить в очередь — тоже одной командой не обойдешься, да и длину надо скорректировать. Не, для десятка задач флажки проще. Тем более что и тебе тоже без флажков не обойтись, особенно если сами задачи короткие, — тогда будет больше взаимодействия между ними.

            1. 1
              2
              3
              4
              5
              6
              
              PTQL01:		ldd 	OSRG, Z+1 	; Грузим из следующего Z+1 байта и перекладываем 
              		st 	Z+, OSRG	; все в Z, а Z после этого увеличиваем на 1
              		dec 	Counter		; Уменьшаем счетчик (там длинна очереди!)
              		brne 	PTQL01		; Если не конец, то в цикле.
              		ldi 	OSRG, $FF	; А если конец, то по последнему адресу записываем FF
              		st 	Z+, OSRG	; Который является признаком конца очереди.

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

              1. Строчек — то немного, а вот количество реально исполняемых машинных команд — десятки. Опять же, для простых программ вот такая структура агоритма проще и нагляднее:
                Start:
                if Sost1 > 0 then call Zad_1;
                if Sost2 > 0 then call Zad_2;
                if Sost3 > 0 then call Zad_3;
                if Sost4 > 0 then call Zad_4;
                if Sost5 > 0 then call Zad_5;
                ….
                Goto Start;

                Zad_1:
                if Sost1.0 > 0 then call Zad_1_0;
                if Sost1.1 > 0 then call Zad_1_1;
                if Sost1.2 > 0 then call Zad_1_2;
                ….
                Return;

                Zad_2:
                if Sost2.0 > 0 then call Zad_2_0;
                if Sost2.1 > 0 then call Zad_2_1;
                if Sost2.2 > 0 then call Zad_2_2;
                ….
                Return;

                Zad_1_1:
                {Выполняемый кусок задачи}
                ….
                Sost1.1 := 0; Sost1.2 := 1;
                Return;

                И так далее.

                1. В принципе, можно и так. Да, будет быстро.

                  А по поводу наглядности… фиг знает, я в таких ветвлениях быстро путаюсь. Мне проще эвентов в кучу накидать. Тем более под каждую задачу делать дополнительную проверку, по сути один и тот же код дописывать.

                  А тут единое ядро, подключил и забыл.

                  1. Ты — то забыл, а контроллер его постоянно крутит, тратя ресурсы, которых в младших моделях и так немного, особенно памяти. Вообще, приличная операционка действительно нужна там, где есть чем рулить, и где кроме жестко заложенных задач, есть еще подгружаемые, например, с флэшки, в виде файлов. Вот на обслуживание файловой системы, консольного ввода — вывода, диалога с оператором, WEB — сервисов, USB — тут действительно уже без операционки труба. По крайней мере, без нее проще не будет. Но это уже начиная примерно с Меги32. Меньше — уже не всегда необходимо, зависит от задач. А на Меге 128 и выше — система нужна однозначно («мигалки» на них лепить никто не будет).

                2. Сразу узнается программная реализация конечного автомата (он же FSM — Finite State machine) :-).

  2. Уххх!!! Вот это сила! Еще одна бессонная ночь гарантирована мне)) Теперь надо переварить прочитанное и поэкспериментировать!
    з.ы. завтра прочту еще и статью в журнале для полного счастья

      1. Вся эта система с добовлением в очередь и диспетчером напомнила мне State Machine в LabVIEW и там есть еще замечательная фишка добавление событий в конец и в начало очереди по приоритетам. Надеюсь это будет в планах)

        1. Задумка с началом и концом уже давно на примете, правда пока не было задачи где бы она понадобилась, а так наверное реализую.

  3. Полностью согласен с позицией «Лишнее всё это». Обычно количество квазипараллельных процессов ограничено, и известно изначально. Требования по каждому из процессов в плане времени реакции и возникновения возможных тормозов тоже изначально известны, и не меняются в одном проекте.
    Поэтому я лично предпочитаю набор из нескольких конечных автоматов. Изначально линейные процедуры режутся в местах возникновения тормозов. Например, при передачи строки через uart имеем два фрагмента — (1)проверка готовности регистра передатчика и (2)выборка элемента строки с запховыванием его в UDR.
    Рулится это переменной «фаза», которая может принимать любое значение из подмножества состояния автомата. Каждый из фрагментиков автомата выполняет своё действие, и по его результатам воздействует на значение переменной «фаза». В результате следующее вхождение в автомат вызовет либо повтор текущего действия, либо следующее действие.

    А несколько автоматов вставляются в основное тело программы, которое представляет собой бесконечный цикл.

    Ох, это проще показать, чем на пальцах объяснить.

    Преимущества такие: практически нулевой расход памяти и низкий расход процессорного времени на работу собственно переключателя.
    Недостатки — тему надо вкурить.
    После понимания принципа всё становится ясно и просто, но до понимания — программа похожа на жуткую мешанину кода. Особенный ступор это зрелище вызывает, когда его видишь на дизассемблерном листинге.
    С другой стороны это даже плюс. Труднее похачить. Ну и новое знание оно это. Пользительно бывает.

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

  4. Во, вспомнил!
    Есть хорошая книжка:
    М. Дамке Операционные системы микроЭВМ.
    Перевод с английского В.Л. Григорьева,
    Москва, «Финансы и статистика», 1985г.
    Оригинал:
    MICROCOMPUTER OPERATING SYSTEMS, Mark Dahmke,
    BYTE BOOKS, Subsidiary of McGraw-Hill, 1982.
    В свое время я ее до дыр зачитал, хоть были и другие, посолидней. В ней все разжевано от и до, начиная от учебной системы — монитора на Z80 с матричной клавиатурой и семисегментными индикаторами (схема выглядит, как вполне современная, с микроконтроллером!), и через принципы построения — до подробного описания устройства CP/M80 и UNIX.
    Весьма рекомендую для прочтения! Много вопросов сразу отпадет, и изобретать велосипед станет намного проще и увлекательнее. Думаю, в Интернете найти ее в электронном виде — не проблема, а то мне в лом 250 страниц сканить.

    Еще есть хорошая толстая книжка: (680стр)
    С.Кейслер Проектирование операционных систем для малых ЭВМ
    Москва, «Мир», 1986г.
    Оригинал — THE DESIGN OF OPERATING SYSTEMS FOR SMALL COMPUTER SYSTEMS, Stephen Kaisler, 1983г.
    В ней тоже очень подробно и доходчиво описаны принципы построения операционок — диспетчеризация очередей задач, распределение ресурсов, человеко-машинные интерфейсы. Правда, она несколько более академична по сравнению с предыдущей, рассчитанной больше на практиков. Но тоже читал с удовольствием.

    1. М. Дамке Операционные системы микроЭВМ.
      Перевод с английского В.Л. Григорьева,
      Москва, “Финансы и статистика”, 1985г.
      Оригинал:
      MICROCOMPUTER OPERATING SYSTEMS, Mark Dahmke,
      BYTE BOOKS, Subsidiary of McGraw-Hill, 1982.

      Господа, ну не могу найти эту книгу в инете!
      У кого нибудь это получилось? — Дайте ссылку плиззз…

        1. Я интереса ради глянул тогда в поисковиках — есть в нескольких местах у букинистов сама книжка, но за деньги. Деньги невелики, 200-300р, да морока с оплатой, доставкой… В электронном виде вроде не попалась, хотя упоминаний — сотни.
          Вот, например: http://yandex.ru/yandsearch?text=%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5+%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D1%8B+%D0%BC%D0%B8%D0%BA%D1%80%D0%BE%D0%AD%D0%92%D0%9C%2C+%D0%94%D0%B0%D0%BC%D0%BA%D0%B5
          или: http://www.google.kz/search?hl=ru&q=%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5+%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D1%8B+%D0%BC%D0%B8%D0%BA%D1%80%D0%BE%D0%AD%D0%92%D0%9C%2C+%D0%94%D0%B0%D0%BC%D0%BA%D0%B5&btnG=%D0%9F%D0%BE%D0%B8%
          Толи мне и вправду свою отсканить да выложить? Книжка — то интересная, и без «воды»… Может, за ночь управлюсь…

        2. Отсканировал для начала 50 страниц и содержание. ОС_МИКРО_ЭВМ.djvu Size: 1144 KB
          __1054___1057____1052___1048___1050___1056___1054____1069___1042___1052_.djvu
          Постепенно отсканирую остальное. Сегодня уже лень, да и устал мало-мало…

          1. Отсканировал еще полсотни страниц книги М. Дамке Операционные системы микроЭВМ.
            Поскольку обьем файла в формате DJVU невелик, и чтобы не собирать книжку по кусочкам, выложил вместе с предыдущими страницами.
            OS_MICRO_EWM.djvu Size: 2650 KB http://rapidshare.com/files/226850446/OS_MICRO_EWM.djvu
            Теперь имеем 100 страниц книги + содержание. Продолжение следует…

  5. эх… хорошая эта штука ртос))) тока вот асм…))
    тут такая бредовая идейка пришла — чо если создать проект на gcc — и в нем везде сделать ассемблерные вставки с этой ртос))) а сами задачи писать на си ? =)

  6. Только сейчас дошел до OS. И возник вопрос: что будет с задачей, если в момент добавления ее в очередь, сработает таймер и тоже попытается добавить задачу?
    Например в момент прохода по списку при поиске конца очереди?
    SEQL01: ld Tmp2, Z+ ; Грузим в темп байт из очереди
    >>>>>>>>>>>>>>> Тут произошло прерывание <<<<<<<<<<<<<<<<<
    cpi Tmp2, $FF ; и ищем ближайшее пустое место = FF
    breq SEQL02 ; Если нашли, то переходим на сохранение

    dec Counter ; Либо конец очереди по счетчику.
    breq SEQL03
    rjmp SEQL01

    1. В момент добавления задачи в очередь прерывания должны быть запрещены. Эта операция должна быть атомарной. По крайней мере на том этапе пока возможно вклинивание другой задачи в процесс. Если у меня этого нет (или там закомменчены эти строчки, не помню уже. ЧТо то такое я делал для отладки, мог забыть вернуть) то это ошибка.

  7. Как то жизнь повернулась, что я вернулся к микроконтроллерам. В качестве жертвы был избран AVR.
    Спасибо автору! Хоть для меня не много нового, но дело в нюансах и мелочах именно для AVR. Поэтому читаю с удовольствием, правда параллельно с книжкой.
    Ну к сути проблемы.
    И все же что бы ядро выглядело более универсальным, я бы добавил к нему многозадачность. И то что это занимает много памяти, автор не прав.
    1. если ввести два режима: монопольный и многозадачный, то многозадачность не сожрет всю память, а будет использоваться только для избранных.
    2. работа со спулингом тогда существенно упростится не влияя на другие фоновые задачи.
    Да и переделка не большая. немного перепишется диспетчер, появится слово состояния задачи. Ну конечно таймер нужно забить для кванта времени, да и задача должна сама себя убирать из очереди во время завершения или генерировать сигнал (если ввести систему сигнализации)
    З.Ы.
    Для тех кто хочет изучить основы ОС, я бы порекомендовал следующие книжки. Могу ошибиться с названиями, а искать среди кучи книжного хлама заткнутого по всей квартире, честно в лом.
    1. Дейтл то ли введение в операционные системы, то Операционные системы.
    2. Кейслер. Что-то Проектирование ОС для малых ЭВМ. могу ошибаться… Вот это мощь! тооолстый. приведены коды на псевдоязыке русском.
    Правда староваты… но классика. В кейслере даже есть подробные описания блоков управления процессом, памятью и т.д. Хорошо расписаны оверлеи. Правда зачем уж они… хотя мир вращается по кругу, или по спирали (в какой проекции смотреть )) )
    Помнится пришлось мне работать СМ1234 на 589 (был монстр с микропрограммированием — это вам не AVR — это круче геморой), голой, без ОС с программированием кнопках на морде процессора. Пол дня забивал монитор вручную… периферии тогда у ней не было ((
    Пол дня мучал свою плату. Рядом стоял монстр СМ2 на ферритовой памяти с сдвоенным процессором и к нему было книжка. Описание ейной ОС — ДОС АСПО. ОС дрянь — но книжка прелесть. Описание всяких полей блоков управления, инструкции по написанию драйверов.
    Во как в СССР документацию делали! СМ2 списали, тем более на ней БПФ не шло… ошибка с плавающей точкой была в процессоре, которая выскакивала только при определеных условиях )). Ну а книжку ценную я слямзил, до сих пор у меня валяется где-то.
    Книги, это как любовь — оторвать от сердца можно только с мясом!

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

  8. Не подскажите как использовать в ОС написанной на асме функции которые описаны в Си?
    А конкретнее можно ли задачи Fire, Task2, Task3 и т.д. написать в Си?

    1. Один в один не получится, не предназначена она для этого. Возьми лучше ту же самую систему, но написанную уже на Си. В цикле статей про архитектуру программ был пример. Вот она легко портируется вообще на любую архитектуру. Я ее за 20 минут на арм перетащил

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

    1. Да уже не помню. Писалось все еще для малых тини у которых была только LPM потом еще достраивалась и переделывалась.

      1. Ну у совсем мелкой (11/12) нет LD Rd,Z+ а у всех постарше есть adiw :)

        20 лет не писал на ассемблере (всмысле как на языке, а не конкретно на atm), а руки помнят. :)

        В любом случае спасибо :)

  9. Ни как не могу поняить как работает этот участок кода:

    PTQL01: ldd OSRG, Z+1 ; Грузим из следующего Z+1 байта и перекладываем
    st Z+, OSRG ; все в Z, а Z после этого увеличиваем на 1
    dec Counter ; Уменьшаем счетчик (там длинна очереди!)
    brne PTQL01 ; Если не конец, то в цикле.
    ldi OSRG, $FF ; А если конец, то по последнему адресу записываем FF
    st Z+, OSRG ; Который является признаком конца очереди.

    Поясните пожалуйста.

    1. Потому что оно постоянно обновляется и дополняется от проекта к проекту, а статьи я не переписываю под каждый чих. Там главное не сам код, а концепция. Код может быть и весьма глючным, так что смотри в оба :)

  10. И ещё неясность:
    ldi Counter,TaskQueueSize-1 получается что это команда грузит 10 в Counter при каждом сдвигом очереди??????? или на делает что-то другое??

  11. в подпрограмме ProcessTaskQueue:

    clr ZH ; Сбрасываем старший байт
    lsl OSRG ; А взятый номер задачи умножаем на 2
    mov ZL, OSRG ; Так как адреса у нас двубайтные, а значит
    ; И ячейки в таблице перехода двубайтные
    ; Получается смещение по таблице

    но когда мы сдвигаем влево, тобишь умножаем на 2, мы перенос не учитываем. В ZH в любом случае у нас записывается 0! не ошибка ли это?

    1. оказывается это нужно если очень много задач(больше 128), но с таким количеством не получится работать, так что вопрос некорректен :)

  12. Ни как не могу поняить как работает этот участок кода:

    PTQL01: ldd OSRG, Z+1 ; Грузим из следующего Z+1 байта и перекладываем
    st Z+, OSRG ; все в Z, а Z после этого увеличиваем на 1
    dec Counter ; Уменьшаем счетчик (там длинна очереди!)
    brne PTQL01 ; Если не конец, то в цикле.
    ldi OSRG, $FF ; А если конец, то по последнему адресу записываем FF
    st Z+, OSRG ; Который является признаком конца очереди. Насколько я понимаю ,то этот кусок кода лишь добирается до конеца очереди .Какой смысл нам что-то загружать и оставлять в Z ,если далее по коду мы выгружаем содержимое из стека в то же Z . И не понятно, как происходит смещение и выбор следующей задачи .если начало подпрограммы — ProcessTaskQueue:
    ldi ZL, low(TaskQueue) ; Берем адрес начала очереди задач
    ldi ZH, high(TaskQueue) ; Напомню, что это в ОЗУ . Т.е. мы всегда грузим в Z первый байт ProcessTaskQueue . Где же тут относительное смещение?

    1. Смещение там и не нужно, мы же в цикле по всей очереди пробегаем от первого до последнего значения. А вместо тупления в код лучше всего взять и протрассировать под отладчиком, благо код ассемблерный и легко отлаживается в AVR Studio тогда все будет понятно и очевидно.

  13. DI HALT, здравствуйте еще раз!! Со смещением в диспечере задач вроде разобрался. Появился следующий вопрос — в стартовой инициализации подпрограмма RAM_ FLUSH выставляет нули во всех ячейках ОЗУ. А SendTask запихивая первую задачу в очередь не может найти ячейки с содержимым $FF. Может я что-то опять упустил?

  14. Понял!!! сначала запускается дипечер,он то и запишет после условия counter=0, в последнюю ячейку очереди эту FF

  15. В ProcessTaskQueue нужен атомарный доступ:
    cli
    cpi OSRG, $FF ; регистр OS) Сравниваем с FF
    breq PTQL02 ; Равно? Значит очередь пуста — выход.
    sei
    иначе при добавлении задачи в прерывании на шаге cpi (при пустой очереди) в Z запишется FF со всеми вытекающими.
    Опробовано на своей шкуре =).

  16. В комментариях написано что система не переключается от задачи к задаче. А ждет когда каждая из них выполнится. А что тогда она делает? Еще вы не сказали для чего нужна команда WDR. Что она делает и что будет если не применить.

    1. Если задач нет — крутит холостой цикл. WDR сбрасывает watchdog таймер если он включен. Если собаку не пинать, то она сбрасывает контроллер. Это защита от зависаний.

      1. Все равно не понимаю. Почему в других листингах нет команды wdr? Что за таймер вообще. Откуда он тут взялся?

        1. Он там всегда был. Кто то его использует, кто то нет. Если не использовать, то wdr не нужен. Я иногда использую, потому команду оставил.

  17. Здравствуйте! Не могу разобраться как получается адрес в диспетчере задач. Не понимаю и все тут. Трассирую программу, смотрю в память, в регистры и не вдупляю как получается адрес. Понимаю только до subi. Все дальше тупеж. DI-HALT обьясни если не сложно.

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

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

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