FreeRTOS для чайников. Краткое описание.


Бытует мнение, что RTOS это некий хардкор для избранных. Что там все сложно, замудрено и новичкам туда соваться бестолку. Отчасти тут есть доля истины, такие системы крайне сложны в отладке, но и то лишь тогда, когда вы забиваете контроллер под завязку и работаете на пределе оперативной памяти и быстродействия. Тогда да, словить какой-нибудь dead lock или пробой стека можно на раз. И попробуй найти где это случилось в этой асинхронной системе. Но простые задачи на RTOS реализуются еще проще и с меньшим количеством мозга.
 

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

▌FreeRTOS?
Почему именно она? Она популярна, она Free и она портирована на огромное количество архитектур, под нее существуют плагины для Keil и IAR и всякие примочки для PC. При этом она довольно легкая и функциональная.
 

Я не буду вам сейчас тут расписывать все эти прототипы функций, порядок записи, технические тонкости и прочее. Это все есть в технической документации и в замечательном цикле статей Андрей Курница, что был в журнале Компоненты и Технологии в 2011 году. PDF статьи вы найдете в конце.
 

Я лишь на пальцах и псевдокоде быстро распишу те инструменты которыми владеет FreeRTOS, чтобы когда вы будете читать более подробную документацию за деревьями не потеряли лес :)
 

Ну и все сказанное тут справедливо и для большинства других RTOS. Т.к. механизмы в целом все одни и те же и никто ничего нового еще не придумал.
 

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


 

Каждый тик это вызов прерывания таймера, в котором вызывается диспетчер, чьими усилиями проворачиваются шестеренки ОС. Это время еще называют квантом, говоря, что задаче выделяется квант времени.
 

▌Задача
Краеугольным камнем любой RTOS является задача. Задача выглядит как функция которая крутит бесконечный цикл делающий какую-либо относительно простую процедуру. Представьте, что вы сделали на микроконтроллере программу которая только опрашивает кнопки и ничего больше не делает.
 

Ее код будет выглядеть примерно так:
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void KeyScan(void)
{
	while(1)
	{
	 if (Button1 == Pressed) 
 		{
		do_action_1();
		}
 
 	if (Button2 == Pressed) 
 		{
		do_action_2();
		}
	}
}

 

Если в классическом стиле программирования бесконечные циклы были зло которое могло затупить всю программу, то в случае RTOS (она должна быть вытесняющей) это нормально. Так и должно быть. Можно даже быдлокодить и делать тупые задержки на циклах. Например так:
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
void LedBlink(void)
{
	while(1)
	{
	 LED_ON();		// Зажечь диод
 
	for (i=0;i<1000000;i++)	// Выдержка в 1000000 тактов.
		{
		_NOP();
		}
 
	 LED_OFF();		// Погасить диод
 
	for (i=0;i<1000000;i++)	// Выдержка в 1000000 тактов. 
		{
		_NOP();
		}
	}
}

 

Вот примерно так может выглядеть алгоритм этой программы с точки зрения программиста, не углубляясь в дебри ОС. Как видно, связей между задачами нет. Им и не нужно, они работают каждая сама по себе.

 

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


 

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

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

В нужный момент задача создается командой xTaskCreate(……) где в длинном перечне аргументов мы указываем на нашу задачу, ее приоритет, имя для отладки и сколько мы под нее памяти выделяем. В результате под нее выделяется кусок памяти, заводится свой стек и она запускается в свободную жизнь. В которой может быть в нескольких состояниях:
 

  • READY Задача запущена и готова принять на себя управление. Ждет только момента когда на нее обратит внимание диспетчер. Как только дойдет ее очередь так сразу же задача перейдет в режим RUN.
  • RUN Т.е. диспетчер переключил управление на нее, процессор прогоняет непосредственно ее код через себя в данный момент. В этот момент задача живет, потребляет процессорное время и делает полезную работу ради которой она была записана.
  • WAIT Задача в спячке. Т.к. ждет некого события, например, пока таймер натикает, или пока что-нибудь в системе не случится, на что эта задача должна среагировать. При этом диспетчер не переключается на нее, процессорное время не тратится. Как только ожидаемое событие произойдет, то RTOS назначит этой задаче состояние READY.
  • SUSPEND Выключено. Т.е. задача не выгружена из памяти, данные ее все сохранены, но она неактивна. Ни на какие события не реагирует и сама из этого состояния не выйдет. Вывести ее из этого состояния можно только API командой ОС, вручную.

 
Ну и задачу можно грохнуть командой vTaskDelete(…). При этом она будет выгружена из памяти, освободит оперативку, но ее текущее состояние и локальные переменные будут потеряны. Однако ничего не мешает запустить ее вновь, сначала.
 

У задачи есть такой важный параметр как приоритет. Он задается при создании и его можно на лету вручную менять через API функции RTOS . Приоритет определяет в каком порядке будут работать задачи.
 

Т.е. если есть две задачи в статусе Ready, но у одной приоритет выше другой. Задача с низким приоритетом в таком случае не получит управление до тех пор, пока высокоприоритетная задача не свалится в WAIT. Диспетчер всегда будет выбирать ту READY задачу у которой приоритет выше.
 

А если READY задач нет, то будет вращать IDLE цикл. В котором происходит обслуживание памяти, зачистка неиспользованной оперативки, удаление ошметков от удаленных задач и прочей служебной фигней. Ну и туда же (на IDLE) можно повесить свою callback функцию, в которой, например, контроллер будет отправляться в режим энергосбережения.
 

API функции управления задачами, кратко. Аргументы посмотрите в технической документации:
 

  • xTaskCreate — создает новую задачу, выделяя под нее память и натравливая на нее диспетчер.
  • vTaskDelete — удаляет задачу. Память потом освобождает IDLE задача.
  • vTaskDelay(N) — эта функция вызывает диспетчер, который переводит задачу в WAIT на N системных тиков. Можно на ней лепить всякие простые задержки, вроде опроса кнопок.
  • vTaskDelayUntil(N) — функция аналогичная предыдущей, но считает время N не от момента ее срабатывания, а от момента прошлого пробуждения задачи.
  • uxTaskPriorityGet — возвращает приоритет задачи. Т.е. можно посмотреть приоритет текущей или любой другой задачи заголовок (handle) которой мы знаем.
  • uxTaskPrioritySet — устанавливает приоритет задачи. Т.е. можно приоритет менять.
  • vTaskSuspend — глушит задачу, что она перестает отвечать на события. Перестает работать, но не выгружается из памяти, а зависает в текущем состоянии.
  • vTaskResume — возврат задачи из SUSPEND состояния. Эту функцию нельзя выполнять из обработчика прерывания.
  • vTaskResumeFromISR — аналогичная команда, но ее как раз можно выполнять из обработчика прерывания, но нельзя запускать вне него. Там еще есть ряд особенностей, о которых я расскажу ниже отдельно, когда буду описывать все *FromISR функции оптом.

 

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

  • xTaskGetCurrentTaskHandle — узнать Handle текущей задачи. Зная заголовок можно можно менять ее приоритет, запускать, удалять и так далее.
  • xTaskGetTickCount — выдает количество тиков с момента запуска планировщика. Это этакий глобальный таймер, отсчитывающий время с начала времен.
  • xTaskGetSchedulerState — выдает состояние диспетчера. Запущен, работает, выключен и так далее.
  • uxTaskGetNumberOfTasks — показывает количество загруженных задач. Например если надо определить хватит ли памяти. Или для отладки.
  • vTaskList — отладочная утилита. В итоговую программу совать ее не следует. При запуске делает большой такой отчет в котором записывает досье на все запущенные задачи. Там и заголовки, имена, состояние, сколько есть памяти, какая глубина стека.
  • vTaskStartTrace — запуск отладочной трассировки. Данные тоже пишутся в большой такой лог. Который можно по UART скинуть на комп, расшифровать специальной утилиткой и посмотреть что происходило в недрах вашей программы.
  • ulTaskEndTrace — остановка трассировки.

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

▌Очереди. Обмен данными между задачами
Окей, задачи это такие программы сами в себе. Крутятся себе в бесконечном цикле. Но как между ними гонять данные? Ведь считанную кнопку надо кому-то передать, чтобы другая задача провернула с ней некий экшн.
 

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

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


 

Одни задачи кладут данные в очередь, а другие оттуда читают.
 

А диспетчер следит за тем, чтобы из очереди нельзя было прочитать до того как там все нормально запишется. Также он следит за тем, чтобы в очередь нельзя было записать если она переполнена. В случае если очередь пуста/переполнена, то та задача которая хочет считать/записать в очередь сваливается в WAIT и диспетчер ее разбудит когда очередь будет готова отдать/принять данные.
 

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


 

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

Очереди в нашем упрощенном примере в псевдокоде работать будут так (псевдокод):
 

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
 
//Задача KeyScan опрашивает кнопки и шлет данные через очередь другой задаче.
// Если очередь будет переполнена, то на функции QueueSend задача упадет в Wait до 
// освобождения очереди. 
void KeyScan(void)
{
	while(1)
	{
	 if (Button1 == Pressed) 
 		{
		QueueSend(Button2Led,1);	// Шлем в очередь Button2Led номер кнопки "1".
		}
 
 	if (Button2 == Pressed) 
 		{
		QueueSend(Button2Led,2);	// Шлем в очередь Button2Led номер кнопки "2".
		}
 
	TaskDelay(10);			// Задержка средствами диспетчера. 
	}
}
 
 
// Задача LedBlink получает данные от задачи KeyScan
void LedBlink(void)
{
	while(1)
	{
	Button = QueueReceive(Button2Led);	// Тут задача упадет в Wait пока данные не появятся в очереди. 
 
	switch(Button)			// А как только проснется в Button будет загружены данные.
		{
		case 1: LED_1_ON(); 	// Зажигаем соответствующий светодиод. 
		case 2: LED_1_ON(); 
		}
	}
}

 

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

  • xQueueCreate — создает очередь. При этом выделяется память. Если памяти не хватит, то очередь создана не будет и будет возвращена ошибка.
  • vQueueDelete — удаляет очередь, освобождая память. Память зачищается в IDLE процессе.
  • xQueueReset — обнуление очереди.
  • uxQueueMessagesWaiting — показывает сколько у нас в очереди элементов.
  • xQueueSend/xQueueSendToBack — два имени для удобства. А так они одинаковые. Кладет данные в конец очереди. В прерывании использовать нельзя.
  • xQueueSendToFront — кладет данные в начало очереди. В прерывании использовать нельзя.
  • xQueueReceive — берет данные из очереди, освобождая ячейку. В прерывании использовать нельзя.
  • xQueuePeek — просто считывает данные из очереди, но ячейку не освобождает. Как наблюдатель. Может использоваться, например, для отладки. Чтобы слать содержимое очереди в UART, показывая последовательность данных. В прерывании использовать нельзя.
  • xQueueSendToBackFromISR/xQueueSendFromISR — то же самое, что и простая очередь, но для чтения из прерываний.
  • xQueueSendToFrontFromISR — аналогично, все то же самое, но для прерываний. Ну и есть разница. О них ниже.
  • xQueueReceiveFromISR — аналогично, версия для использования в обработчиках прерываний.

 

▌Семафоры
Иногда надо не передать данные между задачами, а просто дать отмашку. Мол тут у нас нажата кнопка, ты знаешь что делать, вперед! Для этого служат семафоры. С точки зрения внутреннего устройства семафор это та же самая очередь. Только содержимое ячейки никого не волнует, главное что она не пуста.
 

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

Т.е. если переделать нашу задачу с кнопками то можно показать это так:
 

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
void KeyScan(void)
{
	while(1)
	{
	 if (Button1 == Pressed) 
 		{
		SemaphoreGive(RunTask1);	// Машем семафором RunTask1  задаче 1
		}
 
 	if (Button2 == Pressed) 
 		{
		SemaphoreGive(RunTask2);	// Машем семафором RunTask2  задаче 2
		}
 
	TaskDelay(10);			// Задержка средствами диспетчера. 
	}
}
 
// Задача 1
void Task1(void)
{
	while(1)
	{
	SemaphoreTake(RunTask1);		// Программа тут свалится в WAIT до тех пор пока не появится семафор
	Action1();			// А как семафор появится, отомрет и выполнит Action1
	}	
}
 
// Задача 2
void Task2(void)
{
	while(1)
	{
	SemaphoreTake(RunTask2);		// Программа тут свалится в WAIT до тех пор пока не появится семафор
	Action2();			// А как семафор появится, отомрет и выполнит Action2
	}	
}

 

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

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

▌Mutex
Mutual exclusion — система взаимного исключения. Механизм обеспечивающий уникальный доступ многих задач к единственному ресурсу.
 

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

Второй пример:
Есть две задачи. Одна шлет в UART «ABCDEF» Другая шлет «123456». Без разделения их может на выходе получиться шняга вида A1BCD123E45F6, что совсем не похоже на нужный нам результат.
 


 

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

Если доступ к UART, например, защищен мутексом, то никакой мутекс не запретит писать напрямую в порты периферии и тем самым устроить конфликт.
 

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

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

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

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

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

За семафоры и мутексы отвечают следующие API фукнции:
 

  • xSemaphoreCreateBinary — создать простой бинарный семафор.
  • xSemaphoreCreateCounting — создать счетный семафор
  • xSemaphoreCreateMutex — создать мутекс
  • xSemaphoreCreateRecursiveMutex — создать рекурсивный мутекс
  • vSemaphoreDelete — удалить семафор/мутекс.
  • xSemaphoreGetMutexHolder — узнать какая сволочь держит мутекс.
  • xSemaphoreTake — Взять семафор/мутекс
  • xSemaphoreTakeFromISR — взять семафор/мутекс из прерывания
  • xSemaphoreTakeRecursive — взять семафор/мутекс рекурсивный.
  • xSemaphoreGive — выдать семафор/мутекс
  • xSemaphoreGiveRecursive — выдать рекурсивный мутекс
  • xSemaphoreGiveFromISR — выдать семафор/мутекс из прерывания.

 

▌Системный таймер
Это программный таймер. Он позволяет запустить какую либо функцию по таймеру. Но, в отличии от задачи, она не вертится в бесконечном цикле, а запускается один раз или периодически. Но отработала и вышла. Функция вызываемая таймером не должна быть зацикленной. Дискретность времени таймера — системный тик. Таймер после отработки выгружается из памяти. Его не нужно принудительно удалять, только если он не периодический.
 

Вот основные таймерные команды
 

  • xTimerCreate — создать таймер. В этой API функции мы связываем таймер с указателем на его функцию.
  • xTimerDelete — удалить таймер
  • xTimerStart — запустить таймер
  • xTimerStop — остановить таймер
  • xTimerChangePeriod — изменить период временной выдержки
  • xTimerReset — сбросить таймер

 

Ну и те же функции, но отдельно для прерываний:
 

  • xTimerResetFromISR
  • xTimerChangePeriodFromISR
  • xTimerStartFromISR
  • xTimerStopFromISR

 

▌Диспетчер и Многозадачность
Над всеми задачами, очередями, мутексами и семафорами темным властелином стоит диспетчер. Он в тени, мы его можем только запустить или остановить. Он висит обычно на каком-нибудь таймере, в ARM Cortex-M3 его вешают на SysTick таймер. Таймер генерирует системные тики, скажем раз в 1мс. И каждый такой тик вызывается диспетчер который тасует задачи и обслуживает все функции ядра. А пока не будет вызван диспетчер ничего в системе не меняется. Диспетчер это суть и душа всей RTOS.
 

Задача не обязательно прерывается диспетчером по тику таймера. Она может и самостоятельно передать управление. Например, на время ожидания. Т.е. если наш тупой код с задержками на for (i=0;i<1000000;i++) переписать. Заменив задержку тупым for циклом, на задержку средствами OS. Такую как
 

1
vTaskDelay(1000);

То задача не будет уже впустую крутить миллион итераций цикла, а данной API функцией попросит диспетчера разбудить ее черезе 1000 системных тиков и свалится в WAIT, вызвав диспетчер, который передаст управление другой READY задаче (если такая есть). А через 1000 тиков диспетчер переведет ее снова в READY и продолжит со следующей строчки.
 

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

Но в зависимости от конфигурации ядра FreeRTOS многозадачность может быть и кооперативной.
 

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

 

Т.е. в этом случае диспетчер уже не вызывается каждый системный тик. А вызывается ТОЛЬКО вручную программистом. Т.е. теми сами командам переводящими систему в WAIT. Это черевато тем, что высокоприоритетная задача не может перебить низкоприоритетную до тех пор, пока та сама не отдаст управление. Что накладывает ограничения на стиль написания. Нужно будет стараться не тупить и как можно быстрей отдавать управление диспетчеру. Тупой наглый цикл тут не прокатит, зато грамотное использование кооперативной многозадачности позволяет экономить память. Т.к. OS переключается в конкретные, заранее известные, моменты, а значит сохранять надо несколько меньше данных.
 

API ядра

  • taskYIELD — отдать управление диспетчеру принудительно, не дожидаясь тика.
  • taskENTER_CRITICAL — начало критической секции. В критической секции не вызывается диспетчер. Это как бы запрет прерываний, но только для диспетчера.
  • taskEXIT_CRITICAL — конец критической секции. Сами критические секции могут быть вложенными. Т.е. насколько мы в нее углубились столько же раз надо из нее выйти.
  • taskDISABLE_INTERRUPTS — запрет прерываний.
  • taskENABLE_INTERRUPTS — разрешение прерываний. Тоже может быть вложенным.
  • vTaskStartScheduler — запуск диспетчера. Собственно с этой команды все и начинает вертеться.
  • vTaskEndScheduler — остановка диспетчера.
  • vTaskSuspendAll — заглушить все задачи
  • xTaskResumeAll — восстановить все заглушенные задачи

 

▌Таймауты, обработка ошибок, и что же не так с прерываниями?
Почти все API функции что нибудь да возвращают. Обычно они возвращают PASS или ERROR на предполагаемое действие. Т.е. считав после функции значение мы можем понять выполнилась ли команда или нет. Ошибка может быть по разным причинам, например, памяти не хватило для выделения чего бы то ни было.
 

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

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

1
2
3
4
5
6
7
8
9
10
11
// Получить сообщение из очереди Queue.  Ждем сообщение не более 10 тиков
if(xQueueReceive(Queue,&(pxRxedMessage),( TickType_t)10 ) )
	{
	// pcRxedMessage теперь указывает на полученное сообщение
	// Обрабатываем его. 
	}
else
	{
	// Error! сообщение не пришло за 10 тиков
	// Решаем эту проблему. 
	}

 

Ну и очевидно, что в прерывании таймауты зло. Какой нафиг таймаут когда нам надо срочно делать свои дела и выскакивать из прерывания.
 

Поэтому существуют специальный функции которые [****]FromISR. Таймаута там нет, там просто есть ответ считано или нет значение. А еще есть один важный параметр pxHigherPriorityTaskWoken.
 

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

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

Но вот если такая задача есть… То мы можем ничего не делать. И тогда прерывание отдаст управление прерванной задаче, та дотикает до конца кванта времени (системного тика), а дальше диспетчер сам решит кому там дать управление.
 


 

Либо мы можем прям из обработчика прерывания вызвать диспетчер командой taskYIELD и он сразу же выполнит высокоприоритетную задачу, а потом вернет управление той, которую мы прервали.
 


 

Это позволит обработать быстрей важные вещи.
 

Пример такого подхода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Обработчик прерывания */
static void __interrupt __far vExampleInterruptHandler( void )
{
static unsigned long ulReceivedNumber;
static const char *pcStrings[] = {“String 0,“String 1,“String 2,“String 3};
 
/* Создадим переменную куда будет положен результат API-функции xQueueReceiveFromISR(), 
он станет pdTRUE, если операция с очередью разблокирует более высокоприоритетную задачу.*/
static portBASE_TYPE xHigherPriorityTaskWoken;
 
/*Перед вызовом xQueueReceiveFromISR() должен принудительно устанавливаться в pdFALSE */
xHigherPriorityTaskWoken = pdFALSE;
 
/* Считывать из очереди числа, пока та не станет пустой. */
while( xQueueReceiveFromISR( xIntegerQueue,&ulReceivedNumber,&xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
	{
	ulReceivedNumber &= 0x03;
	xQueueSendToBackFromISR( xStringQueue,&pcStrings[ ulReceivedNumber ],&xHigherPriorityTaskWoken );
	}
 
/* Проверить, не разблокировалась ли более высокоприоритетная задача при записи в очередь. 
Если да, то выполнить принудительное переключение контекста. */
if( xHigherPriorityTaskWoken == pdTRUE ) taskYIELD();
}

 

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

Суть в чем. Есть у нас прерывание диспетчера. И в этом прерывании он что-то делает со своими структурами (очередями, задачами и прочей системной фигней). Но диспетчер это не самая важная вещь на свете, поэтому его часто вешают на самое зачуханное прерывание с самым низким приоритетом. И его все могут перебивать.
 
Но есть нюанс. Допустим диспетчер что-то делает с очередью, смотрит кого там можно разблокировать по ее состоянию, пишет туда что-то свое, а тут опа прерывание и в нем некая [****]FromISR записала в ту самую очередь. Что у нас будет? У нас будет лажа. Т.к. на выходе из прерывания диспетчер похерит все что предыдущее прерывание туда писало — он то запись до конца не провел. Т.е. налицо классическое нарушение атомарности.
Чтобы этого не было, диспетчер на время критических записей запрещает прерывания. Чтобы ему никто не мог помешать делать свое дело. Но запрещает он не все прерывания, а только определенную группу. Скажем все прерывания с приоритетом (меньше число, старше приоритет) 15 по 11, а 1 по 10 нет. В результате на 1 по 10 мы можем вешать что то ну очень сильно важное и никакой диспетчер это не перебьет. Но пользоваться API RTOS в этих (1-10) прерываниях ни в коем случае уже нельзя — они могут диспетчер скукожить. Для настройки этих групп есть конфиг специальный. Для STM32 он выглядит так:
 

1
2
3
4
5
6
/* This is the raw value as per the Cortex-M3 NVIC.  Values can be 255 (lowest) to 0 (1?) (highest). */
 
#define configKERNEL_INTERRUPT_PRIORITY              255
#define configMAX_SYSCALL_INTERRUPT_PRIORITY     191 /* equivalent to 0xb0, or priority 11. */
 
  /* This is the value being used as per the ST library which permits 16 priority values, 0 to 15.  This must correspond to the configKERNEL_INTERRUPT_PRIORITY setting.  Here 15 corresponds to the lowest NVIC value of 255. */

 

Мы указываем приоритет ядра KERNEL = 255. И задаем планку максимального приоритета для прерываний которые имеют право юзать [****]FromISR API функции = 191. В статье о NVIC я писал, что у STM32 в байте с приоритетом играет роль только старшая тетрада, т.е. у нас есть 16 уровней приоритетов от старшего к младшему: 0х00, 0х10, 0х20, 0х30…0xF0
 

Т.е. 255 это уровень 0xF0 — самый младший, а 191 уровень 0xB0 и, таким образом, все прерывания в которых мы можем использовать API фукнции должны быть сконфигурированы с приоритетом от 0xF0 до 0xB0, не старше. Иначе будет трудноловимвый глюк. Прерывания же не использующие API могут быть с каким угодно приоритетом от самого низкого до самого старшего.
 

▌Сопрограммы aka Co-Routines
Но даже этих механизмов разработчикам FreeRTOS было мало и они добавили в нее сопрограммы. Это, по сути дела, еще одна маленькая виртуальная RTOS с кооперативной многозадачностью, работающая внутри системы на правах обычной задачи.
 

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

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

Вот так, вкратце, я расписал вам основные механизмы и принципы FreeRTOS. Оставив за кадром отладочные механизмы. За более полными подробностями обращайтесь во-первых к официальной документации, а во-вторых, к циклу статей Андрея Курница. Там, по сути, будет то же самое, что и тут, но с подробным расписыванием всех параметров. Подробными примерами, но несколько тяжеловесно все, от обилия инфы глаз замыливается. А вот как краткий справочник юзать самое то.
 

 

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

46 thoughts on “FreeRTOS для чайников. Краткое описание.”

  1. Спасибо, замечательная статья, давно присматриваюсь к FreeRTOS. Но в данный момент экспериментирую с комактным диспетчером Рона Креймборга — случайно увидел ссылку в комментариях одной из статей.

    Вопрос, собственно, такой — как делать задержки МЕНЬШЕ системного тика — ну например для шины 1 Wire? Сама шина дикий трэш для временных задержек — от микросекунд до секунд.

    1. Если не повышать частоту системных тиков, то нужно выделять отдельный таймер, который взводится на нужный интервал и по прерыванию дает семафор задаче, при этом также нужно вручную вызвать диспетчер при помощи taskYIELD.

    2. Как-то я интересовался этой темой. Согласен с bigdigital .Тут, похоже, нужно все-таки переходить от уровня FreeRTOS на уровень системных таймеров, далее семафорами и taskYIELD решать вопрос.
      Повысить частоту за пределы 1KHz тяжеловато — нужно перелопачивать код (у меня STM32F4, 168 MHz — замучался! Остановился на вышеуказанном решении)

    3. >как делать задержки МЕНЬШЕ системного тика
      Использовать аппаратные ресурсы — те же никсы и окна имеют частоту системного таймера 100-1000 Гц, но это не мешает им работать с езернетами и прочим гигагерцовыми шинами.
      1-wire хорошо реализуется через UART например.

    4. То был мой, видимо, комментарий.
      Из личного практического опыта хочу предупредить, что такой диспетчер можно с успехом использовать только для проектов небольшой сложности. Главные его недостатки, ИМХО:
      — невозможность удаления задачи (это можно дописать самостоятельно, но при удалении задачи придется два раза шерстить всю очередь и пересчитывать задержки каждой из следующих задач). Из-за этого приходится опять изобретать велосипеды с флагами или сообщениями по которым одна задача сигнализирует другой о том, что она должна себя удалить при следующем выполнении.
      — так как это не кооперативка, и никакой контекст не сохраняется — задача будет каждый раз выполняться с самого начала, т.о. любое ожидание события приводит к вынужденному дроблении одной задачи на кучу более мелких. На читаемости кода это сказывается не лучшим образом. Выходом может быть, также, ввести для задач состояния и построить на этой основе конечные автоматы. Но когда их много и они большие, то это опять превращается в спагетти из флагов и состояний.

      Посмотрите еще в сторону protothreads. Они дают определенный уровень абстракции, который позволяет писать задачи подобно тому, как это делается в кооперативке, но с мизерным overhead-ом.

  2. Я сделал себе небольшую такую оболочку на С++ для FreeRTOS — вышло очень удобно! Ну вот, например, прерывание по нажатию на кнопку выдает в очередь длительность, дергает семафор:

    extern "C" void EXTI0_IRQHandler (void)
    {
    static const dword delWith = 100;
    static const dword delWithout = 1000;
    QueueISR _del (del); // подключились к очереди из прерывания с типом dword (unsigned long)
    SemaphoreISR _sem (btnClicked); // подключились из прерывания к семафору

    if (Button)
    _del << delWith; // выдали в очередь
    else
    _del << delWithout;
    --_sem; // освободили семафор

    Объявляю задачу: Task2 l3 (Led, 2, «Led 3», 60); 2 параметра, имя функции Led, приоритет 2, имя «Led 3», глубина стека 60 (последние 3 параметра есть по умолчанию).
    Реализую задачу:

    void Led (Pin *pin, dword start_del)
    {
    Pin &led = *pin;
    dword cur_delay = start_del;
    dword max_stack = Task::getCurrentTask().maxStack ();
    bool to_on = true;

    while (1)
    {
    if (to_on)
    ++led;
    else
    --led;
    to_on = !to_on;

    del.wait (cur_delay);
    if (del)
    del >> cur_delay;
    }
    }

    Создаю задачу: l3.init (&LedBlue, 1000);
    Думаю написать пост для ценителей С++…

  3. Особенно радует статическое выделение памяти в куче под стеки всех задач. И хуки — на TickHook очень удобно опросы кнопок вешать. Поделитесь кто на чем использовал FreeRTOS — у меня на 8 меге завелась:3

      1. Да почти все, мне только очереди нужны были.
        configMINIMAL_STACK_SIZE 100 байт,
        Как я понял, каждая функция хSomethingCreate жрет по 100 байт и еще на шедулер 100 уходит.

  4. Спасибо, статья очень в тему. Опишите, пожалуйста, подробно процесс добавления и настройки FreeRTOS, для STM32 на вашей плате например.

  5. Важное уточнение для [***]FromISR() функций. Любое прерывание, которое использует подобную функцию, должно иметь численное значение приоритета такое же или выше (а логически — такое же или ниже), чем значение configMAX_SYSCALL_INTERRUPT_PRIORITY.

    Например:
    NVIC_SetPriority(USART1_IRQn, configMAX_SYSCALL_INTERRUPT_PRIORITY>>4);

    Также необходимо задать группировку приоритетов до запуска шедулера так:
    NVIC_SetPriorityGrouping(3); // Это для тех, кто не юзает StdPeriphLib
    или так:
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // Это для тех, кто юзает StdPeriphLib

    Более подробно на буржуйском: http://www.freertos.org/RTOS-Cortex-M3-M4.html. Не зная этих особенностей можно словить трудновыцепляемые грабли.

    Кстати, кто-нибудь пользуется возможностями трассировки? Есть библиотека FreeRTOS+Trace, но она платная. Может быть кто знает бесплатный аналог?

    1. Да, спасибо. Замечание не лишнее. Подробно это у Курница расписано, но, думаю, тут не лишним будет упомянуть.

    2. Тока два момента:
      Нафига сдвигать вправо на 4? Ведь configMAX_SYSCALL_INTERRUPT_PRIORITY итак указан полным байтом.

      И зачем определять группы если их не используешь?

      1. В данном случае 4, потому что используется контроллер с 4 битами приоритета.
        На самом деле я думаю правильней было бы задавать приоритет так NVIC_SetPriority(USART1_IRQn, (configMAX_SYSCALL_INTERRUPT_PRIORITY >> (8 — __NVIC_PRIO_BITS))+0, или какоето число если нужно чтобы прерывания обрабатывались системой);
        Ось устроена так что она не работает с подприоритетами, поэтому контроллер настраивается на 4 группу приоритетов, где есть только 16 приоритетов и нет подприоритетов прерываний
        Здесь Scorpion_ak47 http://forum.easyelectronics.ru/viewtopic.php?f=49&t=13911, хорошо расписал работу особенности настройки приоритетов Cortex M3 для FreeRTOS

        1. Не, тут дело в том, что это на SPL, а эти клоуны приоритет задают числом от 0 до 15, а изначальный параметр записан как надо, в целый байт. И чтобы подвести его к формату SPL его надо сдвинуть, чтобы SPL его сдвинула обратно.

      2. Разобрался. Верно, группы в общем случае задавать не надо (читай нельзя), т.к. после ресета группировка равна 0b000, что для STM32 эквивалентно 0b011. Как уже написал bigdigital, ось работает без подприоритетов, все прерывания должны быть вытесняющими, иначе что-то может пойти не так.

    1. Да, везде все одно и то же. Разница только в инициализации и устновке ОС. Ну и конфигах ее. Т.к. прерывания разные для шедулера, модель памяти и кучи и так далее.

      1. То есть, чтобы разобраться достаточно будет твоего примера для stm32 и любого работающего примера на avr

        1. Н авр все уже давно портировано, единственный минус — 16 битный таймер используется для работы диспетчера. Переделать можно, но в исходниках ядра копаться не самое приятное занятие.

  6. по моему, это нарушение того, для чего сделаны простые контроллеры: есть задача-есть контроллер. Это уже шаги к x86 в безопасном режиме.

    1. Нет, почему же? Задачи же жестко прописаны в прошивке. Это просто способ организации программы. Облегчающий разработку.

      1. Хм. Читал Вашу статью по jtag, где Вы высказались в конце про «развращение эмбедеров». Что ж, Вы правы,

          1. Мне вот тоже интересно. Сложно ли сделать такой карманный компьютер вообще без ничего, только скажем с одним usb или ethernet портом, куда можно поставить голый линукс с башем и подключатся по ssh? Никто не знает, существуют такие diy pocket linux на arm для нубов?) Спс.

  7. Добрый вечер!
    Хотелось бы услышать исчерпывающий ответ на вопрос о контролировании переполнения стека. Насколько я понял, при создании задачи мы указываем под нее размер стековой области.
    Но что будет, если этот размер переполнится? Как пример, конструкция вида:

    void TaskPowerControl(void)
    {
    // создание локальных переменных, которые и будут расположены в предоставленной задаче стековой области
    // некоторый код инициализации
    // сам бесконечный цикл задачи
    while(1)
    {
    int a = 0;
    int b;
    b = PowerGet(); // тут что угодно, не суть, главное, на что я хотел обратить ваше внимание, это объявление переменных в теле бесконечного цикла. Ведь обычной жизни это вызвало бы переполнение стека, а тут как поведет себя система? Что последует?
    }
    }

    1. Все рухнет. Только и всего.

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

      1. Т.е. необходимо действительно следить за структурой задачи? Сначала объявить все необходимые переменные, затем уже пихать их использование в бесконечный цикл?
        Читал еще ветку про переполнение стека во FreeRTOS. При вытесняющей многозадачности действительно произойдет страшное — стек переполнится и все рухнет. А в режиме кооперативной многозадачности стек общий (так отписался один человек) и поэтому локальные переменные при переключении задач удаляются. Так ли это? И можно ли тогда в режиме кооперативной многозадачности объявлять переменные внутри бесконечных циклов?
        PS. Я прекрасно понимаю, что в при нормальном порядке вещей такого беспорядочного разбрасывания объявлений быть не должно, но интерес зашкаливает =)

        1. Там разные модели памяти можно выбирать. Почитай про них, подробно все расписано.

          Локальные переменные вроде бы не дохнут, но я точно не помню.

          Но везде за соблюдением стека надо следить самому. Т.к. у МК нет никаких аппаратных механизмов которые бы тебе запретили насовать в стек сколько угодно данных, что обрушит все что угодно.

  8. Если тупо написать свою ОС типа

    while(1)
    {task1;
    task2;
    task3;
    task4;}

    Будет не тот же ли самый результат? Или повесить группу задач на системный таймер

    SysTick()
    {task1;
    task2;
    task3;
    task4;
    Sleep;}

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

    Естественно каждая задача должна сама ограничивать свое время выполнения, не портить ресурсы других задач, но суть таже. Прерывания в микроконтроллере вроде как и выполняют роль диспетчера задач, а тут еще один диспетчер. Когда прерываний не хватает, на одно прерывывание же вешают несколько задач (даже на ПК), примерно тоже самое. Для сложных проектов ОС нужна, а для простых, типа мигания светодиодом нет. Для энергосбережения, даже с ОС надо максимально быстро отдавать управление диспетчеру задач, по сути ручная оптимизация кода. Понятно есть свои плюсы, и свои минусы. И что для STM32F4 более нужна ОС, а для atmega8 это что-то странное будет.
    Думаю можно даже статью написать по простенькой ОС, типа пишем ОС с нуля, например оптимизируя по быстродействию или памяти, интересно почитать было бы. Не изобретая велосипед и не конкурируя с FreeRTOS. Простой диспетчер задач что дергает другие задачи, дает и забирает у них управление.

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

      1. Эххх..Занятная была статья про этот твой диспетчер, давно, конечно, но интересно. Сейчас вспоминаю — зачитывался…. ))

  9. Мысль не дает покоя…
    Предположим, есть ядро AVR…
    Есть несколько задач, через каждый тик управление отбирается у одной задачи и передается другой, то есть простейшая система переключения контекста…
    На assembler все понятно, ложим в стек SREG, регистры, сохраняем указатель вершины стека, потом направляем указатель вершины на стек другой задачи, опять вынимаем регистры и SREG, как ни странно, это все работает, проверено…
    Теперь думаем, как это все на C реализовать. Assembler вставки, а так все тоже самое… СТОП! Локальные переменные, что с ними делать?! Как их обнаружить?!

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

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

    1. RTOS включает в свою библиотеку основной файл конфигурации FreeRTOSConfig.h. В этом файле есть дефайн — #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 (для CORTEX M3), определяющий приоритеты прерываний для использования API функций , в вашем случае приоритеты прерываний зависят от конкретной микросхемы.
      Я использовал стандартную библиотеку USB от производителя STM. В ней есть файл конфигурации «железяки» — hw_config.c, в нем надо указать приоритет прерываний USB не превышающий configMAX_SYSCALL_INTERRUPT_PRIORITY.
      В библиотеке от STM есть удобный файл usb_lib.h, в который я добавил #include «FreeRTOS.h»
      #include «task.h»
      #include «queue.h»
      #include «semphr.h»,
      также, как и в основной проект, и, с помощью семафора, использую USB.

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

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

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