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

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

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

69 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. Уххх!!! Вот это сила! Еще одна бессонная ночь гарантирована мне)) Теперь надо переварить прочитанное и поэкспериментировать!
    з.ы. завтра прочту еще и статью в журнале для полного счастья

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

  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. Для избранных кого? Задач? Ну так и делается, только многозадачный диспетчер все равно жрет очень много памяти, ведь он силой переключает контекст и сгружает все в память.

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

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

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

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

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

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

    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. Потому что оно постоянно обновляется и дополняется от проекта к проекту, а статьи я не переписываю под каждый чих. Там главное не сам код, а концепция. Код может быть и весьма глючным, так что смотри в оба :)

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

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

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

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

    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 тогда все будет понятно и очевидно.

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

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

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

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

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