AVR. Учебный Курс. Архитектура Программ Часть 2

Диспетчер
Данная организация программы требует чуть большего количества кода чем флаговый автомат (Хотя это еще как посмотреть. С увеличением числа задач служебный код динамического диспетчра не увеличивается, а вот флаговый автомат разрастается за счет большего числа флагов и проверок этих флагов, плюс быстродейтсвие снижается, чего нет в диспетчере) , но зато лишена ряда недостатков.

Во первых тут очередь выполнения задач не жестко заданная, а динамическая, конвеерного типа. То есть у нас есть в памяти массив из указателей на задачи-функции. Диспетчер берет указатель и, если он не указывает на Idle, осуществлет переход по этому адресу. Предварительно удалив его из очереди и подкинув очередь.

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

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

А теперь подробно распишу тот диспетчер который стоит в 90% моих проектов на Си.

Очередь задач
Основа основ. С нее все начинается.

Первым делом определяем тип TPTR — Task Pointer. Это обычный указатель пустышка. Просто адрес, без типа.

1
typedef void (*TPTR)(void);

Потом рисуем очередь задач. Она у нас как глобальная переменная, защищенная от посягательств оптимизатора.

1
volatile static TPTR   TaskQueue[TaskQueueSize+1];         // очередь указателей

TaskQueueSize это предельное число задач в очереди. Надо делать с некоторым запасом. Задается в дефайнах конфига диспетчера.

Диспетчер задач

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
inline void TaskManager(void)
{
u08      index=0;
TPTR   GoToTask = Idle;      // Инициализируем переменные
 
// Как видишь, тут есть указатель Idle - ведущий на процедуру простоя ядра.
// На нее можно повесить что нибудь совсем фоновое, например отладочные примочки =)  
// И локальная переменная-указатель GoToTask куда мы будем жрать адреса переходов
 
Disable_Interrupt
// Запрещаем прерывания!!! Это макрос. Поэтому без ; в конце. 
// Почему не CLI()? Это команда AVR, а я хотел сделать максимально
// платформонезависимый диспетчер. Прерывания надо запрещать потому, что
// Идет обращение к глобальной очереди диспетчера. Ее могут менять и прерывания
// Поэтому заботимся об атомарности операции. 
 
GoToTask = TaskQueue[0];	// Хватаем первое значение из очереди
 
if (GoToTask==Idle)		// Если там пусто
	{
	Enable_Interrupt		// Разрешаем прерывания
	(Idle)();			// Переходим на обработку пустого цикла
   	}
else
   	{
   	for(index=0;index!=TaskQueueSize;index++)	// В противном случае сдвигаем всю очередь
      		{
		TaskQueue[index]=TaskQueue[index+1];
		}	TaskQueue[TaskQueueSize]= Idle;	// В последнюю запись пихаем затычку Idle
 
	Enable_Interrupt		// Разрешаем прерывания
	(GoToTask)();		// Переходим к задаче
   	}
}

Постановщик в очередь

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
void SetTask(TPTR TS)
{
u08      index = 0;
u08      nointerrupted = 0;
 
if (STATUS_REG & (1<<Interrupt_Flag))  // Если прерывания разрешены, то запрещаем их.
	{
	Disable_Interrupt
	nointerrupted = 1;	// И ставим флаг, что мы не в прерывании.
	}
// Это бодояга вида ATOMIС RESTORSTATE только собственной выделки с закосом под
// мультиплатформенность. Как видишь, тут SREG явно не указыватеся, он прописан в 
// дефайнах. При переносе на другой микроконтроллер, например, на С51 мне только 
// пару файлов поправить. А прерывания надо однозначно запретить. Ибо нужно
// обеспечить атомарность операций. 
 
 
// А вот и постановка задачи в очередь.
while(TaskQueue[index]!=Idle)	// Прочесываем очередь задач на предмет свободной ячейки
	{			// с значением Idle - конец очереди.
	index++;
	if (index==TaskQueueSize+1)	// Если очередь переполнена то выходим не солоно хлебавши
		{
		if (nointerrupted)   Enable_Interrupt	// Если мы не в прерывании, то разрешаем прерывания
		return;		// Раньше функция возвращала код ошибки - очередь переполнена. Пока убрал.
		}
	}
// Если нашли свободное место, то
TaskQueue[index] = TS;		// Записываем в очередь задачу
if (nointerrupted) Enable_Interrupt	// И включаем прерывания если не в обработчике прерывания.
}

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

1
SetTask(Task1);

А сама задача выглядит как обычная функция:

1
2
3
4
void Task1(void)
{
DoSomething();
}

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

Очередь таймеров
Разумеется тут нужен и софтверный таймер. Без которого жизнь уже не мила. Начинается он естстественно с очереди таймеров. Которая сделана в виде массива структур. Глобальная переменная, также защищенная от посягательств оптимизатора.

1
2
3
4
5
6
volatile static struct
                  {
                  TPTR GoToTask;		// Указатель перехода
                  u16 Time;			// Выдержка в мс
                  }
                  MainTimer[MainTimerQueueSize+1];   // Очередь таймеров

Состоит из двубайтного счетчика времени и указателя TPTR.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline void TimerService(void)
{
u08 index;
 
for(index=0;index!=MainTimerQueueSize+1;index++)      // Прочесываем очередь таймеров
	{
	if(MainTimer[index].GoToTask == Idle) continue;  // Если нашли пустышку - щелкаем следующую итерацию
 
	if(MainTimer[index].Time !=1)	// Если таймер не выщелкал, то щелкаем еще раз.
		{			// To Do: Вычислить по тактам, что лучш;е !=1 или !=0.
		MainTimer[index].Time --;	// Уменьшаем число в ячейке если не конец.
		}
   		else
      		{
      		SetTask(MainTimer[index].GoToTask); // Дощелкали до нуля? Пихаем в очередь задачу
      		MainTimer[index].GoToTask = Idle;   // А в ячейку пишем затычку
		}
	}
}

Прерывание таймера
Служба таймеров пихается в обработчик прерывания от таймера. Каким образом будет делаться прерывание это уже частности. Я сделал на ШИМ таймере по достижении сравнения. Впрочем, можно было повесить и на самый глупый таймер, например на таймер0, который только тикать и умеет. А большего нам и не надо. Но тут придется перезагружать его значение в каждом заходе, чтобы поддерживать постоянное время. А на ШИМе это автоматом идет. Разумеется вывод ШИМа не подключен к выводу контроллера. Тикает внутри.

1
2
3
4
ISR(RTOS_ISR)
{
TimerService();
}

RTOS_ISR это тоже макроопределение. У меня оно привязано на TIMER2_COMP_vect, но можно в конфигах диспетчера привязать на что угодно. Сделано так опять же для того чтобы при переносе на другую архитектуру можно было пару строк подправить и все.

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

// Время выдержки в тиках системного таймера.

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
void SetTimerTask(TPTR TS, u16 NewTime)
{
u08  index=0;
u08  nointerrupted = 0;
 
if (STATUS_REG & (1<<Interrupt_Flag))    // Проверка запрета прерывания, аналогично функции выше
	{
	Disable_Interrupt
	nointerrupted = 1;
	}
 
for(index=0;index!=MainTimerQueueSize+1;++index)	//Прочесываем очередь таймеров
	{
	if(MainTimer[index].GoToTask == TS)		// Если уже есть запись с таким адресом
	{
	MainTimer[index].Time = NewTime;	// Перезаписываем ей выдержку
	if (nointerrupted)  Enable_Interrupt	// Разрешаем прерывания (если не были запрещены).
	return;          		// Выходим. Раньше был код успешной операции. Пока убрал
	}
 }
 
// Алгоритм, в данном случае не очень оптимален. В прошлом цикле можно было запомнить 
// положение первого Idle и сейчас не искать его.
for(index=0;index!=MainTimerQueueSize+1;++index) // Если не находим похожий таймер, то ищем любой пустой 
	{
 	if (MainTimer[index].GoToTask == Idle)  
		{
  		MainTimer[index].GoToTask = TS;	// Заполняем поле перехода задачи
  		MainTimer[index].Time = NewTime; 	// И поле выдержки времени
  		if (nointerrupted)  Enable_Interrupt 	// Разрешаем прерывания
		return; 					// Выход. 
  		}
 	}            
// тут можно сделать return c кодом ошибки - нет свободных таймеров
}

Постановка задачи по таймера идет следующим образом:

1
SetTimerTask(Task1,1000);

Вот и все.

Инициализация диспетчера:
В отличии от флагового автомата, где только флаги установить, тут требуется при старте сделать инициализацию очередей и очистку таймеров.
Функция выполняется один раз, потому инлайновая:

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
inline void InitRTOS(void)
{
u08   index;
 
for(index=0;index!=TaskQueueSize+1;index++)   // Во все позиции записываем Idle
	{
	TaskQueue[index] = Idle;
	}
 
for(index=0;index!=MainTimerQueueSize+1;index++) // Обнуляем все таймеры.
	{
	MainTimer[index].GoToTask = Idle;
	MainTimer[index].Time = 0;
	}
}
 
//Потом роисходит запуск диспетчера. Собственно запускать то там надо таймер. 
//RTOS Запуск системного таймера
 
//System Timer Config
#define Prescale		64
#define TimerDivider	(F_CPU/Prescaler/1000)      // 1 mS
 
inline void RunRTOS (void)
{
TCCR2 = 1<<WGM21|4<<CS20;	// Freq = CK/64 - Установить режим и предделитель
				// Автосброс после достижения регистра сравнения
TCNT2 = 0;                    	// Установить начальное значение счётчиков
OCR2  = LO(TimerDivider);	// Установить значение в регистр сравнения
TIMSK = 0<<TOIE0|1<<OCIE2;	// Разрешаем прерывание - запуск диспетчера
 
sei();
}

Главный файл, собственно файл проекта, выглядит так:

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
#include <HAL.h>
#include <EERTOS.h>
 
//RTOS Interrupt
ISR(RTOS_ISR)
{
TimerService();
}
 
// Прототипы задач =========================
void Task1 (void);
void Task2 (void);
void Task3 (void);
//=====================================
 
// Область задач
// Задачки простые, диодиком помигать. Одна зажигает, другая гасит. 
// Зацикливаются они вызовом через диспетчер. 
void Task1 (void)
{
SetTimerTask(Task2,100);  	//Запускаем вторую задачу
LED_PORT  ^=1<<LED1;  	// Зажигаем диодик
}
 
void Task2 (void)
{
SetTimerTask(Task1,100);  	// Запускаем первую задачу
LED_PORT  &= ~(1<<LED1); 	// Гасим диод. 
}
 
// Результатом стал цикл из двух задач. Одна запускает другую по таймеру. 
// первая зажигает диод, вторая гасит. В результате он мигает. 
// А чтобы добавить, например, сканирование клавы мы добавляем еще одну задачу
 
void KeyScan()
{
SetTimerTask(KeyScan,50);  	// И зацикливаем ее через диспетчер саму на себя
Scan();    			// Делаем полезную вещь. 
}
 
int main(void)
{
InitAll();  	// Инициализируем периферию
InitRTOS(); 	// Инициализируем ядро
RunRTOS();  	// Старт ядра.
 
// Запуск фоновых задач. Для того чтобы задача завертелась кто то должен
// Запустить ее вручную хотя бы раз.  Делаем это перед главным циклом.
 
SetTask(Task1);
SetTask(KeyScan); 
 
while(1)       	// Главный цикл диспетчера
{
wdt_reset();   	// Сброс собачьего таймера
TaskManager(); // Вызов диспетчера
}
 
return 0;
}

Прикладываю проект с уже поднятым диспетчером на базе ATMega16.

Там все раскидано по файликам.

1
2
3
4
5
6
7
8
9
10
11
12
13
GCC_RTOS.c		Это главный файл проекта.
 
EERTOS.c		Файл с ядром диспетчером. 
EERTOS.h		Заголовочный файл ядра
 
EERTOSHAL.h		Файлы аппаратной абстракции и конфигурации ядра. 
EERTOSHAL.с		Там я описал работу с прерывания для AVR  Файл с функциями 
			аппаратной абстрацкии ядра. Запуск диспетчера.  
 
HAL.c		Аппартная абстракция проекта. По возможности стараюсь сделать так, чтобы все
HAL.h		аппаратно зависимые фишки были описаны в отдельном файле. 
		В главном проекте только  основной алгоритм. 
		Не зависящий от типа контроллера.

Разгорелась целая дискуссия по поводу оптимальности как подхода в целом, так и конкретных реализаций. Есть много здравых мыслей. Я, в свою очередь, не претендую на оптимальность своего решения (порой бывает избыточной такая конструкция), но получилось достаточно удобно и когда надо быстро скреативить прошивку, да так чтобы отлаживать не пришлось почти, то эта конвеерная молотилка как нельзя кстати приходится. Плюс тут очень многое поддается оптимизации. Можно сделать очередь на указателях в виде кольцевого буффера, можно оптимизировать таймер. Например, сделав его статичным.

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

Файл с примером кода на диспетчере

90 thoughts on “AVR. Учебный Курс. Архитектура Программ Часть 2”

  1. Взялся делать диспетчер на асме. Вместо привычного флагового автомата.. нравицо :) пока что.

    в принципе, выделил основные процы:

    Добавить адрес процы, которую нужно выполнить в массив. Есть основные 8 процедур, которые нужно выполнять циклически, но в зависимости от некоторых событий (таМ, связь есть\нету, по таймеру, ыбли ошибки или не и тд.)
    По ка что в ОЗУ выделил масив на байт 20. (т.е. 10 адресов)
    Добавление событий: если «ошибка данных» — добавить в массив задач адрес процы обработки ошибок.
    Если ошибка «нету связи» — добавить адрес процы подключения.
    Принят пакет ЮАРТом — добавить адрес обработки данных и тп.

    Что собсно являецо главным циклом: перебор ячеек массива на признак отличия от нуля. Если 0х0000 — пустой цикл. если чето другое — то icall с очисткой «чего-то другого».
    Значение Х к примеру, инкрементируется каждым циклом (т.е. не массив сдвигается, а указатель бегает по кругу).

    Собсно вопрос:
    Я не понял вообще предназначение очереди таймеров. Это составная часть модели диспетчера задач или просто часть примера его функционирования?

    1. Ну без службы таймера диспетчер не полноценен. Равно как и таймер без диспетчера. А моя реализация его на ассемблере уже готова.

      Да, лучше делать не icall а ijmp и выходить из самой процедуры через RET. Экономия стека! =)

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

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

          1. Ну как:
            есть мейн_лууп. в нем крутится диспетчер.
            Прерывание от какого либо юарта или таймера добавляет в очередь адрес процедуры.
            За один оборот главного цикла диспетчер выполнит только 1 процедуру.
            В след. цикле следующую.. и т.д.
            Эта процудура какбе вставится в главный цикл, какой бы длинны она не была.
            т.е. я не предполагаю, что процедура будет выполнятся там 100мс, после чего можно запустить другую, а тупо говорю: после этой процедуры — выполнить следующую в очереди.

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

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

                1. Ну да, я не спорю, что потребуется. В этом проекте уже достаточно много написано, не хочу особо экспериментировать, чтоб ненароком все не сломать и не терять время :)

                  > Ну вот смотри. Тебе надо сделать десять событий каждое с разным
                  > интервалом. Как ты это будешь делать?

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

                  А вот если много и с разным интервалом — то, естественно полный вариант с таймером с Вашего примера. Он там себя оправдает :)

                  А так, я пока задался целью уменшить к-во флажков, а то путаться цже начал.

  2. Первое, что бросилось в глаза. (Поработаю в роли «Оптимизатора» на халяву):

    Поскольку очередь не приоритетная (добавление всегда в конец, извлечение из начала), то нет смысла при добавлении задачи сканировать всю очередь, проще хранить указатель на «хвост» (последнюю задачу в очереди).
    В этом случае просто смотрим: если «хвост» меньше конца, — инкрементируем «хвост» и кладем на него задачу, иначе — очередь полная, выход.
    При извлечении задачи если «хвост» равен началу, выполняем «затычку», иначе сдвигаем к началу все задачи по «хвост», и декрементируем «хвост».
    Затраты времени должны уменьшиться, так как проверить указатель на «хвост» проще, чем перебирать и проверять на Idle все указатели задач.
    Затычку Idle в самой очереди вообще не храним, (используется по умолчанию при пустой очереди), а инициализация очереди сводится к установке указателя «хвоста» на начало.

    В свое время мне очень понравилась книга С. Кейслер. Проектирование операционных систем для малых ЭВМ. Москва «Мир» 1986г. 679 стр.
    Оригинал — THE DESIGN OF OPERATING SYSTEMS FOR SMALL COMPUTER SYSTEMS Stefen H.Rfisler U.S. Central Intelligence Agency. 1983г.
    Там как раз очень подробно расписана диспетчеризация задач, в т.ч. и приоритетных, управление процессами и событиями, и прочее, и прочее…
    Но сканировать ее мне в лом, толстая она… Проще в Интернете поискать.

  3. Кстати, тут написано, что вычислять делитель для usart по формуле

    bauddivider (F_CPU/(16*baudrate)-1)

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

    http://www.pic24.ru/doku.php/osa/articles/encoding_without_errors?do=backlink#%D1%86%D0%B5%D0%BB%D0%BE%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5

    1. Согласен. Работать будет, но на больших скоростях будут сбои.

      Старая копипаста, все поправить руки не доходят. Она, кстати, идет из даташита на AVR, там она записана как FOSC/16/BAUDRATE-1 что, собственно, погоды неменяет. Но поголовно присутствует везде :)

      Я на эту статью, кстати, ссылался где то уже.

  4. Спасибо за статью, понял, что жил неправильно.

    Появился вопрос — если при работе с очередью задач, когда прерывания отключены, и в самом деле натикает таймер, то этот тик будет пропущен? Или запрос на прерывание будет выполнен после разрешения прерываний?

    Пусть это будет переполнение таймера. Курение даташита показало, что прерывание происходит при установке трёх битов — (SREG,I), (TIMSK,TOIE), (TIFR,TOV0). TOV0 устанавливается при переполнении и очищается при выполнении запроса на прерывание, либо записью в него 1. Причём не указано, что для установки этого флага таймером необходимо разрешение прерываний. Если при выключенном I-бите будет установлен TOV0, а потом будет установлен I-бит, то все условия для выполнения запроса на прерывание будут выполнены. Значит, по идее, тик пропущен не будет. Это действительно так?

    Это важно — хотелось бы использовать тики не только для выполнения отложенных задач, но и измерения времени с точностью до тика.

    1. Прерывание выполнится сразу же как его разрешат.

      Для измерения времени с точностью до тика (чтобы прям точно что ваще) прерывания запрещать нельзя, да и на конвейере ничего не должно быть в этот момент. Если это так критично, то заведите на это дело второй таймер.

      1. Я об этом потом тоже подумал…
        Собираюсь такую-же туме внедрить в свой проект.
        У тебя был опыт с существующими нароботками по RTOS, типа той-же Salvo RTOS или FreeRTOS (http://www.freertos.org/) ? Стоит ли оно того на AVR ?
        Я совмещаю софтверный USB от obdev, LCD и регулятор мотора l293d на pwm для мониторинга и охлажнения мини-сервака — домашней файловой помойки. Решил воткнуть туда диспечер, ибо уже стало все усложняться =) Пока делаю на ПинБорде, полет нормальный =)

        1. На авр это имхо избыточно. Слишком мало ресурсов. Хотя на мега128, думаю, вполне потянет та же scmRTOS или AVRX сам я их пока не юзал. Только раскуривал, читал мануалы и вполне представляю их кухню изнутри. Мне пока вполне хватает своего диспетчера.

  5. После беглого знакомства появились вопросы.
    1.Из функции SetTimerTask можно выйти не восстановив прерывания. Это правильно?
    2. TimerService. После шедевра: if(MainTimer[index].Time !=1){…} в .Time после отработки таймера останется величина 1. Но даже не это интересно. Правильно ли я понял, что вначале .Time инициализуется нулем, и вот этот if сравнивая с 1, может запустить декремент через FFFF, FFFE…, особо не мешая, но отъедая времечко? Тогда совет: сравнения не делайте жесткими типа if(А!=B). А лучше помягше, через (A>B), или (A<B).

    1. 1. Нет. Где там такое?
      2. Да остается 1. Я эту единичку иногда как флажок использую при отладке — что таймер нормально отработал. Но декремента через FFFF не будет, т.к. активность таймера сравнивается не по нулям в счетчике, а по наличию указателя на задачу. А при постановке в очередь таймера один фиг время заново записывается. Ну и 1 там сделан намеряно. Т.к. если бы было сравнение с нулем (ну или больше меньше нуля, что не суть важно). То при вносе 1 у нас задержка была бы в полтора два раза больше 1мс (ожидание первой сработки таймера, а потом ожидание своего срабатывания). А при 1 отработка произойдет по первому таймеру. Статистически оно как то ближе к 1мс получается. =)

  6. Извините, это опять я… Сторонний, так сказать.
    3. Функция SetTask(). Комментарий: // И ставим флаг, что мы не в прерывании.
    Тут же пространно объясняется, зачем вся эта бодяга. Чтобы кросс, значит, платформенность, не зацикливаться на AVR. В объяснении привлекается платформа С51. Но ведь в С51 в прерывании не сбрасывается флаг глобального разрешения IE, там же приоритетные прерывания. Значит, комментарий типа «ага, мы поняли, что мы не в прерывании» звучит не очень убедительно. Или не так?

    1. Дело даже не столько в прерывании мы, а столько в том, в прерываемом блоке кода мы в данный момент вызваны или нет. Чтобы случайно не разрешить прерывания раньше времени и не получить трудно ловимый глюк. В АВР непрерываемый блок кода возникает в обработчиках прерываний. В С51 или Арм он может быть задуман программером.

  7. Спасибо за диспетчер, работает отлично. Но вот иногда возникает необходимость обнулить таймер, ну например идёт опрос пары-тройки кнопок и значит такая примерно функция:
    void scan_keys(void)
    {
    check_button1
    {
    SetTask(do_something);
    return;
    }
    check_button2
    {

    }
    SetTimerTask(scan_keys, 300);
    }

    Ну и соответственно когда будет обрабатываться функция do_something(), то тут неоткуда не возьмись срабатывает scan_keys(), что и понятно. Нет ну конечно никто не запрещает написать вместо SetTask(do_something) вот так SetTimerTask(do_something, 400), но не всегда комильфо. Поэтому я дополнил EERTOS.c такой функцией:

    void ClearTimerTask(TPTR TS)
    {
    u08 index=0;
    u08 nointerrupted = 0;

    if (STATUS_REG & (1<<Interrupt_Flag))
    {
    Disable_Interrupt
    nointerrupted = 1;
    }

    for(index=0; index!=MainTimerQueueSize+1; ++index)
    {
    if(MainTimer[index].GoToTask == TS)
    {
    MainTimer[index].GoToTask = Idle;
    MainTimer[index].Time = 0; // Обнуляем время
    if (nointerrupted) Enable_Interrupt
    return;
    }
    }
    }

    Ну и в дефайнах extern void ClearTimerTask(TPTR TS);
    Так-то проверил, работает, да и собственно чего бы не работало. Может кому пригодиться.

  8. Вот допустим у меня система на основе данного диспетчера задач (проект упрощённый, но с потолка).
    Имеет три режима: измерение температуры, отображение времени, проигрывание музыки.
    Естественно время считает постоянно, но на дисплей выводится либо время (тогда термометр отключён) или температура. Проигрывание музыки — тоже может включаться или отключаться.

    Соответственно для термометра 2 процедуры:
    void ReadI2cTermotask() {
    прочитать I2c термометр при помощи i2C_ultimate… по окончанию — I2cReadedTermo()
    }
    void I2cReadedTermo() {
    На дисплей результат…
    settimertask(ReadI2cTermotask, 0.5сек)
    }

    Для отображения времени :
    void UpdateTime() {
    На дисплей время…
    settimertask(UpdateTime, 0.25сек)
    }
    (Сама процедура времени висит жёстко на прерывании таймера — ибо есть риск потерять одно переполнение таймера, если очередь задач окажется забита… мелочи)

    Для музыки
    void playNote(){
    Определение очередной ноты, и её длительности;
    Установить ноту на проигрывание;
    SetTimerTask(playNote,длительность ноты)
    }

    Соответственно есть процедура, которая вызывается при нажатии кнопки вкл/выкл музыка
    и процедура переключения режима отображения:
    void OnSwitchMusikButton(){
    //Сюда приходит управление при нажатии кнопки музыка вкл/выкл
    }
    void OnChangeDispButton(){
    //Сюда приходит управление при нажатии кнопки термометр/время
    }

    Теперь вопрос — как лучше реализовать возможность программного включения и выключения задач (что лучше прописать в эти процедуры)?

    1) Делать простые флаги, которые будут меняться при нажатии кнопки? (соответственно непосредственно задачи, выполняющие действия должны будут тупо ничего не делать кроме settimertask, если функция отключена)
    2) Или процедуры реализующие кнопки должны убивать поставленные в очередь задачи касаемые той функции, которую надо отключить и добавлять в очередь те, которые надо включить?

    Есть ли какой-нибудь оптимальный вариант?
    Оптимальный в том числе и по глюко-безопасности, потому как второй вариант оптимален по чистоте в очереди задач, но неоптимален по возможным глюкам: например I2C работает и в этот момент кнопкой вырубается функция термометра и включается отображение времени, то никаких I2C задач в очереди висеть не будет, однако когда I2C закончит чтение термометра — появится задача I2cReadedTermo(), которая не должна быть)
    Да и KillTask() как такового — ещё нет… хотя это уже мелочи…

    В общем — что посоветуете?

    1. Неплохо сочетать с автоматами. Тогда будет проще. Выставил нужное состояние автомата и все. Не играет задача. Потом переключил в нужное — играет. Автоматы же еще и хорошо внутри задачи логику разруливают.

  9. Вставлю и свои 5 копеек :) Избавился от очереди задач TaskQueue, вместо этого в диспетчере задач выполняются задачи из очереди MainTimer, которые уже «выщелкали», соответственно количество кода очень уменьшилось. Изменен только код eertos.c
    http://easyelectronics.ru/repository.php?act=view&id=72

      1. Ну, это я сумел увидеть:) Мне непонятно не конкретно в этом случае для чего оно, а синтаксически как понимать такую запись.
        Это типа мы сопоставляем void с конструкцией (*TPTR)(void), или как? И что тогда подумает компилятор, когда в коде увидит просто TPTR? Ну не понимаю я :(

        1. Мы просто говорим, что TPTR это указатель типа void, наглядней получается в итоге.

          А просто TPTR ты не сможешь использовать. Это не переменная, а тип. Ты можешь использовать просто char или просто u08? Нет, не можешь. Только указав, что char это вот переменная А или еще чо, но не отдельно.

              1. Чем тогда отличается
                typedef void (*TPTR)(void);
                от
                typedef void* TPTR;
                ? Это типа чтоб сказать компилятору, что этот тип будет использоваться для функций, а не для переменных?
                Извини за назойливость, я еще только учусь:) В указателях пока слабоват.

                1. Обрати внимание еще на то, что в скобочках указан еще и (void) это тип данных передаваемых в адресуемую функцию. Т.е. адресуемая функция не обязательно имеет void на входе, можешь указать и другой тип. Они будут передаваться в стеке (классически) или в регистрах (для авр)

                    1. в случае с void передаваемыми параметрами то будет как раз просто *а (адрес он и есть адрес, какая разница), хотя компилер может ошибку типа сказать, варнингами закидать. Если же есть параметры,то да.

  10. создал новый проект.
    обозвал по своему.
    выбрал в качестве камня мегу 32
    скопировал все файлы из примера выложенного здесь кроме GCC-RTOS.c — из него я скопировал содержимое в свой файл MMk_Rtos_C.c.
    присоединил хидеры и остальные *.с файлы
    разобрался с большинством ошибок и варнингов
    но эти варнинги поставили меня в тупик:

    D:\Work\AVR\MMk_Rtos_C\./EERTOS.h:8: warning: ‘Idle’ declared inline after being called
    D:\Work\AVR\MMk_Rtos_C\./EERTOS.h:8: warning: previous declaration of ‘Idle’ was here
    что ему надо?)

          1. D:\Work\AVR\MMk_Rtos_C\EERTOS.h
            Line 8: extern void Idle(void);

            D:\Work\AVR\MMk_Rtos_C\EERTOS.c
            Line 35: inline void Idle(void)

            убираю сишный:
            D:\Work\AVR\MMk_Rtos_C\default/../EERTOS.c:22: undefined reference to `Idle’
            D:\Work\AVR\MMk_Rtos_C\default/../EERTOS.c:22: undefined reference to `Idle’
            D:\Work\AVR\MMk_Rtos_C\default/../EERTOS.c:28: undefined reference to `Idle’
            D:\Work\AVR\MMk_Rtos_C\default/../EERTOS.c:28: undefined reference to `Idle’
            EERTOS.o: In function `SetTask’:
            D:\Work\AVR\MMk_Rtos_C\default/../EERTOS.c:51: undefined reference to `Idle’
            EERTOS.o:D:\Work\AVR\MMk_Rtos_C\default/../EERTOS.c:51: more undefined references to `Idle’ follow
            make: *** [MMk_Rtos_C.elf] Error 1

            убираю хишный:
            ../EERTOS.c:22: error: ‘Idle’ undeclared (first use in this function)
            ../EERTOS.c:22: error: (Each undeclared identifier is reported only once
            ../EERTOS.c:22: error: for each function it appears in.)

            от Вашего примера этот проект отличается только названием файла .с и проекта.
            в примере всё норм а у меня вот это…

  11. DI HALT, наверняка ты считал сколько в среднем тактов жрет эта реализация при среднем количестве задач? Было бы интересно посмотреть на эти цифры.
    Интересует насколько хорошо это будет работать, если таймер установить на 50-100мкс при 16МГц?

    1. Плохо будет работать. Там довольно многое можно оптимизировать, среднее выполнение блока таймерной службы сейчас около 600 тактов. Ну или около того. Столько же в среднем занимает перебор очереди, установка таймера порядка 700 тактов, установка задачи около 400 тактов. Да можешь сам в студии погонять да посмотреть как оно вертится. Реально тайминг снизить до 500мкс ,но делать очень коротки задачи, иначе таймер лажать будет.

  12. Здравствуйте. Спасибо за статью. Сижу разбираюсь с кодом и лично у меня возникает много вопросов. Даже не знаю с чего начать или лучше сделать тему на форуме для последующего разбора.
    Вот например ф-ия TimerService(Обслуживание таймера) как я понял необходима для проверки в реальном времени через каждые 1мс на факт выполнения задачи. В ней сначала прочесываем очередь таймеров(очередь задач) for(index=0;index!=MainTimerQueueSize+1;index++). И если, как я понял первая запись пустая (if(MainTimer[index].GoToTask == Idle) continue;), то выходим из ф-ии, естественно из прерывания и тикаем и дожидаемся следующего прерывания.
    Если первая запись непустышка, то идем дальше и встречается следующее:

    if(MainTimer[index].Time !=1)// Если таймер не выщелкал, то щелкаем еще раз.
    {// To Do: Вычислить по тактам, что лучше !=1 или !=0.
    MainTimer[index].Time --;// Уменьшаем число в ячейке если не конец.
    }
    else
    {
    SetTask(MainTimer[index].GoToTask);// Дощелкали до нуля? Пихаем в очередь задачу
    MainTimer[index].GoToTask = Idle;// А в ячейку пишем затычку
    }
    }

    Здесь не понятно многое:
    1. if(MainTimer[index].Time !=1)// Если таймер не выщелкал, то щелкаем еще раз.
    Каким образом таймер не выщелкал? не подошла его очередь по времени и ему необходимо дотикать еще 1, 2 .. 100мс?
    2. MainTimer[index].GoToTask = Idle;// А в ячейку пишем затычку
    Тоесь как я понял затирается данная задача.
    3. Какая я понял ф-ия отвечает за непосредстенное выполнение задачи это диспетчер задач. Которая вытаскивает значение и крутит его до выполнения если непустышка.
    4. wdt_reset(); // Сброс собачьего таймера — ф-ии такой нету вообще в исходниках. Необходимо делать сброс 2го таймера? или он сам обновляется?
    5. Очередь в данном проекте ограничена 20ю задачами
    #define TaskQueueSize 20. Но их можно увеличит и гораздо больше, к примеру 200?
    6. В диспетчере в конце Enable_Interrupt// Разрешаем прерывания
    (GoToTask)();// Переходим к задаче, если есть значение.
    А может быть ситуация, когда прерывание наступит во время задачи, тем самым недав ей свершиться?
    7. Непонятно, как поисходит расчет до 1мс.
    #define TimerDivider (F_CPU/Prescaler/1000) // 1 mS
    Потом OCR2 = LO(TimerDivider);. Но ведь с частотой 8000000Гц и предделителем 64(4<<CS20) 1 тик = 0,000008с. Так какже вы получили 1мс.
    Не спрашивая и не получишь ответы, поэтому заранее спасибо за ответы. С уважением Дмитрий.

    1. 1. Таймер сервис работает очень просто. У него есть несколько записей таймеров. В каждой записи число = времени в милисекундах от момента записи его туда. Таймерсервис вызывается раз в милисекунду и у каждого из этих чисел вычитает единицу. Если число при этом стало равным 1, то запускается задача связанная с этим таймером. 0 — таймер не активен.

      2. Да, затирается

      3. Да, вызов функции идет через диспетчер, который берет его из очереди задач. А вот в очередь они попадают разными путями. С таймера и с функции SetTask.

      4. Это стандартная библиотечная функция определена в wdt.h транслируется в одну ассемблерную команду WDR

      5. Можно, но смотри чтобы памяти хватило. Сам прикидывай сколько надо памяти все это хранить.

      6. Конечно может. А что в этом плохого? Прерывание отработает и вернет управление задаче. Они для того и задуманы.

      7. Это же значение до OCR т.е. смотри что получается. Частота 8мгц, после делителя частота тика 125000гц. Так? А нам надо получить килогерц. Делим на 1000, получем 125. Это будет значение для OCR. Верхняя граница таймера. Т.е. каждые 125 тиков таймера будет сработка прерывания OCR на которой висит таймер сервис и это будет раз в 1мс.

    2. И не путай. Задача таймерсервиса не отследить выполнение задачи. А запустить задачу в заданное время. Т.е. мы записываем туда связку ЗАДАЧА:ВРЕМЯ и от момента записи в число милисекунд ВРЕМЯ будет запущена ЗАДАЧА.

      1. Спасибо за ответ многое прояснилось. Только вопросы пока есть:
        1. После окончания очереди задач, код отработает , значит надо как-то циклить те задачи, которые нам нужны и добавлять их в очередь постоянно.
        2. Диспетчер задач тикает постоянно и с частотой в 1000 раз быстрее. С учетом команд в нем его цикл примерно 0,2 с(в 50 раз быстрее) грубо говоря, с учетом кода(на все задачи по 1 или 2 тика).
        Он вытаскивает первую задачу в очереди и проверяет ее на выполнение, если она есть, то он ее выолняет, остальные задачи он сдвигает по индексу(тоесть при следующем вызове первой задачей будет вторая).
        Как он рассматривает время выдержки, ведь он выполняет задачу первую в очереди, которая стоит в очереди.

        1. 1. Именно так. Потому если задача циклическая, то в конце ее я ставлю SetTask на ее же. Или SetTimerTask.
          2. А он и не рассматривает. Диспетчер обрабатывает очередь, все что в очереди должно выполниться немедленно. Время выдержки рассматривает служба таймеров. И как только нужный таймер дотикает служба таймеров ставит задачу в очередь на немедленное исполнение.

  13. Спасибо DI HALT разобрался. Пытаюсь сделать ШИМ для LED1, чтобы 10мс он горел, потом 90мс был отключен. Написал вот такой код, только в Proteuse сигнал медленно снижается. Неподскажите в чем дело?

    u08 t_on = 90;//включение
    u08 t_off = 10;//выключение
    void Task1 (void) {
    LED_PORT |= (1<<LED1);//Зажигаем
    SetTimerTask(Task2,t_off);//Через 10мс выключить
    }
    void Task2 (void) {
    LED_PORT &=~ (1<<LED1);//Отключаем
    SetTimerTask(Task1,t_on);//Через 90 включаем
    }

  14. Скачал проект, скомпилировал под Attiny2313 (что было под рукой).
    Получилось:
    Program: 734 bytes (35.8% Full)
    Data: 106 bytes (82.8% Full)
    При таком заполнении оперативки срывает стек. Опытным путем, уменьшая TaskQueueSize и MainTimerQueueSize определил, что срыв стека прекращается при заполнении оперативки менее 75-80%. А ведь если периодически выводить через УАРТ образ оперативки, то должно быть видно насколько данные близки к стеку?

    1. Вот тут нет, а в принципе ничего не мешает. Только параметры надо где-то хранить, обычно это в стекеили регистрах , а с задачей надо будет придумать как их оттуда достать и где хранить. Можешь покурить исходники диспетчера какой нибудь freertos.

  15. Пытаюсь разобраться в логике обработки прерываний используя планировщик. Писал такой вопрос на форуму, может здесь кто сможет ответить?

    При нажатии кнопки выставляется флаг и добавляется задача обработки нажатия
    через время дребезга контактов BOUNCE_TIME.

    // -- Обработка прерывания кнопка вверх ----------------------------------------
    void EXTI0_IRQHandler(void)
    {
    if(EXTI_GetITStatus(EXTI_LineButtonUp)!=RESET)
    {
    if(GPIO_ReadInputDataBit(GPIOButtonUp, GPIO_Pin_ButtonUp))
    {
    EnabledButtonUp = 0;
    }else
    {
    EnabledButtonUp = 1;
    SetTimerTask(StartWorkUp, BOUNCE_TIME);
    }

    EXTI_ClearITPendingBit(EXTI_LineButtonUp);
    }
    }
    // -----------------------------------------------------------------------------

    void StartWorkUp()
    {
    if(EnabledButtonUp == 1) // Если нажата кнопка "вверх"
    {
    SetTask(WorkUp);
    }
    }

    Или же правильнее в прерывании сразу добавлять задачу обработки нажатия кнопки не меняя флаг, чтобы
    флаг менялся только в планировщике.

    // -- Обработка прерывания кнопка вверх ----------------------------------------
    void EXTI0_IRQHandler(void)
    {
    if(EXTI_GetITStatus(EXTI_LineButtonUp)!=RESET)
    {
    SetTask(ButtonUp);

    EXTI_ClearITPendingBit(EXTI_LineButtonUp);
    }
    }
    // -----------------------------------------------------------------------------

    // -- Обработка нажатия кнопки вверх -------------------------------------------
    void ButtonUp()
    {
    if(GPIO_ReadInputDataBit(GPIOButtonUp, GPIO_Pin_ButtonUp))
    {
    EnabledButtonUp = 0;
    }else
    {
    EnabledButtonUp = 1; // Флаг нажатия кнопки ВВЕРХ
    SetTimerTask(StartWorkUp, BOUNCE_TIME); // Добавление задачи проверки нажатия кнопки после времени дребезга контактов (BOUNCE_TIME)
    }
    }
    // -----------------------------------------------------------------------------

    void StartWorkUp()
    {
    if(EnabledButtonUp == 1) // Если нажата кнопка "вверх"
    {
    SetTask(WorkUp);
    }
    }

  16. А если захочется передавать вызываемым через диспетчер процедурам параметры, указатели на массивы — как правильно это реализовать? Теоретически/алгоритмически?
    Как бы вы сделали?

    1. Если это надо всегда, то можно добавить параметр передаваемый как переменная в задачу. Ведь задача это по сути дела функция, не обязательно она void(void) может и данные принимать.

      Можно статичную глобальную переменную задать, видимую извне, но это не очень хорошо.

  17. подогнал для STM32F103. Работает и очень нравится)) Есть вопрос: зачем эта проверка в SetTask и SetTimerTask?
    if (STATUS_REG & (1<<Interrupt_Flag)) .
    {
    Disable_Interrupt
    nointerrupted = 1;
    }
    Не нашел (пока?) у STM флагов глобальных прерываний, оставил просто
    Disable_Interrupt
    nointerrupted = 1;

  18. Тема все же старая, но все же, хочу спросить у автора.
    Данный диспетчер еще актуален и используется? Я про то что не выложено где-то более новой версии и тп. Видел про диспетчер на асме, но мне нужно на си, много внешних либ будет использоваться.
    И если это актуальная версия, то такой вопрос, почему бы для списка задач не использовать кольцевой буфер, по ощущениям он должен работать быстрее чем перебор элементов массива.

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

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

  19. Серьезно, не могу понять, объясните, пожалуйста, зачем такая конструкция:
    if (STATUS_REG & (1<<Interrupt_Flag))
    {
    Disable_Interrupt
    nointerrupted = 1;
    }

    if (nointerrupted) Enable_Interrupt
    что измениться, если оставим без проверок? просто:
    Disable_Interrupt

    Enable_Interrupt

  20. А можно как-то реализовать delay в задачах, чтобы во время их диспетчер мог другие задачи прокрутить, а по истечению задержки вернуться к прерванной задаче? Например есть процедура обновления LCD, там задержки на строб, съедают 100-150 мс времени, ужас…

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

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