Какая частота для FreeRTOS оптимальна?

FreeRTOS и частота переключенияКогда я начал работать со FreeRTOS, я уперся в следующую магическую строчку в FreeRTOSConfig.h:
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
Это частота переключения между задачами.
Почему 1000? А почему не 10000 или 100? А если у меня STM32F4, настроенный на 168 MHz? Я могу выбрать другую, большую частоту? Ответы были какие-то туманные. Самый адекватный: «если будет слишком часто переключаться, то планировщик задач будет потреблять слишком много временного ресурса». Слишком много — это сколько? Решил разобраться. Под катом — ответ на вопрос «какая частота подходит для данной ситуации?».

UPD: существенное исправление: добавил количество потоков, изменились результаты!


Постановка задачи


Итак, есть вопрос — какая частота переключения наиболее оптимальна для этой программы в этом железе для этого заказа? Наверное та, когда планировщик задач («менеджер») потребляет минимум времени. В реальной жизни надо задаться этим минимумом — сколько я могу себе позволить. В каких единицах? В миллисекундах?
Хорошая идея, подумал я. Пошел выкапывать доставать цифровой осциллограф по офису и продумывать программу, которая бы во время планировщика задач держала на ноге «0», потом «1». Бредя среди завалов хлама полезного оборудования, я думал далее — допустим, этом будет 1 мксек. Это много или мало? Смотря сколько времени остается задаче. Значит, надо будет мерять два периода — работы программы и работы планировщика. Уже почти докопавшись достав осциллограф, я вдруг понял — меня не интересует абсолютное время работы того и другого, мне нужно относительное время работы планировщика. Значит, мне достаточно понять соотношение времени работы планировщика ко времени выполнения задачи. В относительных единицах. Следовательно, осциллограф мне не обязателен — я могу измерить это время в режиме отладки. Сбросив пару свалившихся на голову клавиатур закрыв ящик с осциллографом, я пошел писать программу.

Структура решения


Как мы будем решать такую задачу?

Я исхожу из того, что у меня STM32F4, который 32х-битный контроллер, настроенный на 168MHz. Также я исхожу из того, что у меня STM32F4Discovery, который микроконтроллер, программатор и встроенный отладчик в одном лице устройстве. Еще я не забываю об Eclipse и OpenOCD, что мне позволяет написать программу и отладить ее в режиме реального времени.

Я решил передать планировщику задач некую функцию Adder, которая будет периодически инкрементировать 32хбитную переменную counter, повесить перехватчик вызова планировщика задач (функция vApplicationTickHook) и в нем смотреть ее значение. «Смотреть» — это запустить плату в режиме отладки и поставить точку остановки в нужное мне место.

По ходу отладки оказалось, что значение counter от вызова к вызову несколько плавает. Поэтому я решил ее усреднять — допустим, сделать CallsAmount переключений задач, потом разделить счетчик counter на количество вызовов CallsAmount. Если количество вызовов будет достаточно большим, то я буду делать целочисленное деление и результат будет точным. Проверил — результат достаточно быстро сходится к одному значению.

В итоге получился такой код:



// во FreeRTOSConfig.h
#define configUSE_TICK_HOOK		1        // используется перехват вызова планировщика
#define configTICK_RATE_HZ		( ( TickType_t ) 1000 )    // в Hz

// в основной программе
#define CallsAmount		1000		// количество измерений

unsigned long counter;		// глобальный счетчик
unsigned long average;		// среднее арифметическое от вызовов
unsigned long calls;		// количество вызовов

void Adder (void *data)	{ while (1) ++counter; }	// интегратор

void vApplicationTickHook (void)
{
	++calls;				// инкремент количества вызовов
	if (calls == CallsAmount)	// произведено нужное количество вызовов?
		average = counter / calls;	// подсчет среднего арифметического
}

int main (void)
{
	counter = 0;				// сброс счетчиков
	calls = 0;
	
	SystemInit ();				// инициализация микроконтроллера
	xTaskCreate (Adder, 0, 200, 0, 1, 0);	// создание задачи
	xTaskCreate (Adder, 0, 200, 0, 1, 0);	// создание задачи
	vTaskStartScheduler ();		// запуск менеджера задачи
}


Что это значит? Это значит, что единица измерения у меня — операция «инкремент». Т. е. я считаю сколько инкрементов выполняется задачей между переключениями задач. Я использую 32х-битный контроллер, в нем эта операция выполняется за один такт. Следовательно, я получаю количество выполненных операций. Это не миллисекунды (хотя их можно перевести в оные). Это плохо, что не миллисекунды? Мне все равно, так как меня интересует соотношение затраченного времени на одно и на другое.

Чудесно, я получил для данной частоты среднее количество инкрементов. Что мне с этим делать дальше?

Математика

Во FreeRTOS производится настройка таймера, соответствующая значению константы configTICK_RATE_HZ. Допустим, наша частота f1 равна 1000Hz. За время T1 между вызовами таймера выполняется задача (за время j1) и выполняется код планировщика задач (за время m).

Теперь, допустим, configTICK_RATE_HZ равна 2346, что соответствует частоте f2 = 2346Hz. Время между переключениями T2 станет меньше, задача j2 будет выполняться дольше, а вот планировщик задач m будет выполняться то же самое время!

Если говорить о нашей единице измерения «инкремент», то мы четко вычисляем j1 и j2 — значение переменной average. При таком подходе мы не знаем T1 и T2, но мы знаем их соотношение (k = T1 / T2). Наша задача — вычислить m. Потом мы можем для fi получить соотношение m / ji. Вот и все!

Математика тут следующая:



Ничего сложного!

Результаты


UPD: по совету я добавил второй поток, цифры все изменились!

Результаты измеренийЯ проделал измерения для нескольких частот. Далее я привожу результаты в частоте (Hz) и в значениях счетчика: 10000 — 1085, 5000 — 2204, 3333 — 3163, 2500 — 4442, 2000 — 5561, 1000 — 11155. Как и следовало ожидать, налицо гиперболическая зависимость.

Далее делаем несложный расчет для любой пары значений. Возьмем два крайних: f1 = 10000 и f2 = 1000, j1 = 1085 и j2 = 11155. Для этих частот соотношение будет k = T1 / T2 = f2 / f1 = 0.1. Тогда m = (0.1 * 11155 — 1085) / (1 — 0.1) = 34.

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

Это много или мало?

Давайте проведем следующий расчет: для частоты fi разделим m на длительность выполнения задачи ji и выразим результат di в процентах: di = m / ji. Результат: 10000Hz — 3.1%, 5000Hz — 1.5%, 3333Hz — 1.1%, 2500Hz — 0.8%, 2000Hz — 0.6%, 1000Hz — 0.3%. Можно считать, что di — это падение производительности для частоты fi.

Из этих цифр можно получить еще один интересный результат: сколько времени (в реальных мико-, милли- или просто секундах) будет выполняться планировщик, а сколько — программа. Для этого для начала посмотрим сколько времени будет работать планировщик: 1000 мсек * di. Получим такой результат: 10000Hz — 31.1msec, 5000Hz — 15.3msec, 3333Hz — 10.7msec, 2500Hz — 7.6msec, 2000Hz — 6.1msec, 1000Hz — 3.0msec. Соответственно, наоборот мы получим время выполнения задачи: 1000 мсек * (1 — di). Получаем следующее: 10000Hz — 969msec, 5000Hz — 985msec, 3333Hz — 989msec, 2500Hz — 992msec, 2000Hz — 994msec, 1000Hz — 997msec.

Такой вот результат. Теперь у меня есть инструментарий для измерения времени выполнения планировщика в текущей программе. Разумеется, планировщик по-разному будет себя вести в случае таймеров, очередей и прочего (сопрограмма — вообще особый случай). Сколько мне выбирать? В каждом конкретном случае ответ будет разным. Насколько я вижу, в простейшей программе я получил для 10KHz переключения на 31msec работы планировщика за секунду, или падение производительности на 1%. Если это управление работой ядерного реактора или самолетом, то стоит выбирать меньшую частоту переключений (а лучше — вообще другой RTOS, например, SafeRTOS). Если же это цвето- и аудиомузыка в кафешке, то это более чем славно.

Успехов!

P. S.Хочу поучаствовать в конкурсе, поэтому добавляю: Спонсоры
  • +4
  • 28 июля 2014, 16:47
  • PICC

Комментарии (30)

RSS свернуть / развернуть
Есть один момент. В вашем тестовом коде только один поток (задача)
xTaskCreate (Adder, 0, 200, 0, 1, 0);   // создание задачи


И планировщик работает в «холостом» режиме. Вызвался, убедился, что делать ему ничего не нужно и завершился (ну еще там из прерывания где-то vApplicationTickHook позвался). Нет самого главного – нет переключения контекста. А это, ИМХО, самый ресурсоемкий фрагмент работы планировщика.
+2
Сделал два раза вызов той же самой функции. Картинка и вправду поменялась. Местами даже существенно. Исправлю сейчас по статье. Спасибо!
0
Нет самого главного – нет переключения контекста
Кстати говоря, вы не правы. Планировщик выгружает контекст, дабы самому быть выполненным. Потом загружает контекст назад — вернуть ту же задачу. Что так что так идет смена контекста.
А вообще — можно составить анализ для разного количества задач и очередей.
0
Кстати говоря, вы не правы. Планировщик выгружает контекст, дабы самому быть выполненным

Я немного не о том. Планировщик сохранит, потом восстановит контекст. Это время — константа. В Вашем случае, если задача одна то он сохранит ее контекст, потом восстановит ее-же контекст.

Но в реальной системе, задач много, и каждая может быть готова или не готова к исполнению, каждая имеет свой приоритет. И планировщик должен пройтись по списку и выбрать ту из них, которой нужно предать управление (на которую нужно переключится). И, ИМХО, это (поиск задачи для переключения) будет съедать ресурсы, и время работы планировщика будет зависть от кол-ва задач в списке. Поэтому, ИМХО, тестирование планировщика на «холостом ходу», на примере с одной задачей (которая всегда готова к выполнению) не показательно.

Или я ошибаюсь (я не вникал в тонкости реализации шедулера FreeRTOS) и время работы шедулера не зависит от кол-ва задач и их состояний?
0
Упс, прочитал Ваш адейт, и понял что цифры изменились. А можете, для сравнения, сделать замеры, например, для одной, пяти, и десяти задач? И показать результаты. Просто интересно, как время работы планировщика зависит от количества задач.
0
Чувствую, что пришло время для новой статьи-статистики, где стоит перебрать несколько вариантов — разные количества задач, всевозможные семафоры и пр…
+1
В линуксе частота переключения задач ядром рекомендуется 1000 Гц для новых ядер.
Для старых было 100 Гц. И я таки не увидел формулу сколько мне необходимо герц :)

Предлагаю такую формулу:
f=n/(0.7*Tr)
где Tr — максимально допустимое время реакции системы, n — число потоков, 0.7 — 30% запас.
При этом необходимо, чтобы критичная ко времени реакции задача выполняла цикл не более чем за Tr. Это если нет приоритетов у задач. Если приоритеты есть, то можно считать n=1 и критичная ко времени задачи должна иметь макс. приоритет
Если критичных ко времени задач несколько, то уже интересней:) возможны компромиссы в зависимости от приоритетов, требуемых времен и др
0
Чтобы не сталкиваться с дробными числами, нельзя частоту менять как угодно… По крайней мере, для расчета задержек макросом portTICK_PERIOD_MS. Т. е. мне кажется это будет не очень удобно.
0
Чтобы не сталкиваться с дробными числами чего?
Не пользовался freeRTOS и что конкретно делает этот макрос не знаю, но почти уверен что он не будет оперировать дробными числами при расчете задержки.
0
Этот макрос часто используется для расчета задержек. Если делать целочисленные деления, а частота, скажем, 2395 Гц, то будут ошибки.
0
не дописал:
Один раз посчитать на калькуляторе руками и округлить частоту в большую сторону до ближайшего целого? Или до десятков-сотен(так же в большую сторону), чтобы обеспечить кратность 1 мс. Все равно это значение не изменяется динамически(хотя подозреваю это возможно при необходимости).

Или какую формулу предлагаете вы? В статье я нашел только определение соотношения времен.

Кстати, а если задач будет не 1 и не 2 а 5, 10 и дальше, то что станет с числом 34?
0
Н-не думал еще так глубоко, честно говоря :-). Если руки доберутся…
0
А ОС по сути превратится в кооперативную)
+1
  • avatar
  • Vga
  • 28 июля 2014, 20:23
Как интересно. Куда-то внезапно исчез и комментарий, на который я отвечал, и ответ на мой комментарий… Если только меня память не подводит, разумеется.
0
Спасибо. Полезная статья. Но я не нашёл ответ на вопрос: Какая же частота оптимальна?
+1
… автор, а вы не могли бы привести и значения «counter» для каждой частоты тика? а то картина не полная
0
Теперь, допустим, configTICK_RATE_HZ равна 2346, что соответствует частоте f2 = 2346Hz. Время между переключениями T2 станет больше
… время между переключениями будет меньше
+1
Спасибо!
0
T2 станет меньше, задача j2 будет выполняться дольше
… раз Т2 станет меньше (поскольку m — константа), то и j2 станет меньше
0
di = m / ji
… а разве не должно быть
di = m / (ji + m)?
0
Если так, как вы пишите, то di будет показывать долю m в общем периоде. Как мне кажется, это не есть «падение производительности». Хотя 100% утверждать не берусь…
0
… если использовать вашу формулу то, если у вас ji станет меньше m, тогда di может быть больше 100%. Естественно что это крайний случай, но даже и в таких условиях система может работать и достаточно стабильно.
0
Довольно интересное у тебя исследование, и полезное.
Для управления критичными к времени задачами вроде управления реактором лучше выбирать не вытесняющую, а кооперативную многозадачность, которая во FreeRTOS тоже есть. Даже лучше будет вместо МК использовать FPGA или вообще ASIC. Дело в том, что в большинстве МК не специального назначения радиационные помехи могут привести к метастабильности и глитчам на выходе.
0
Дело в том, что в большинстве МК не специального назначения радиационные помехи могут привести к метастабильности и глитчам на выходе
Ой, о таком я вообще не задумывался! Как я понимаю, там вычислительный модуль надежно защищается от радиации. Передачу данных я бы оптически развязал.
0
В предпоследнем абзаце наверное микросекунды, а не msec?
0
  • avatar
  • AVF
  • 29 июля 2014, 09:03
Нет, именно миллисекунды. Хотя, впрочем, можно и в микросекундах выразить. Просто миллисекунды для меня в данном случае более выразительные
0
Ух, даже зарегистрировался специально. Спасибо за статью.
Пару комментариев.
«Реальное время» нужно для того, чтобы предсказать или рассчитать время переключения на задачу. Если ОС «жёсткого» реального времени, то она «гарантирует» время переключения на контекст задачи, обычно вне зависимости от обработки прерываний (исключая критические секции конечно), при этом обычно контекст прерывания может быть вытесняемым. Если ОС «мягкого» реального времени, то время плавает, но тоже вычисляется и обычно «гарантируется» в некотором интервале. Системы жёсткого RT — VRTX, VxWorks; мягкого — QNX, Linux, OS-9/9000. FreeRTOS, как я думаю, имеет все характеристики жёсткого RT, но без вытеснения контекста прерываний.
Что касается реакторов и самолетов, обычно, всё взаимодействие с аппаратурой делается в критических секциях и прерываниях, а все рассчеты в тасках и процессах, а взаимодействие между ними происходит через очереди, события и прочее IPC и примитивы ОС.
В реальности таск обычно не занимает все процессорное время, как в статье автора, а переключение контекста происходит или принудительно через sleep (или аналог) или через обращение к системным примитивам. Тогда, шедулер запускается не только по таймеру, но и при освобождении таском своего таймслайса.
Предлагаю немного модернизировать тест, добавив в Adder vTaskDelay(1) или использовать там же очередь. Затем посмотреть на результаты.

Заранее извиняюсь, если сбивчиво написал комментарий, задавайте вопросы, постараюсь пояснить.
+2
В дополнение, попробуйте запустить таски с разным приоритетом и вызовы в Adder vTaskDelay(0) и vTaskYield(). Вот здесь и пригодится осцилограф :-) ну и ссылка www.freertos.org/FreeRTOS_Support_Forum_Archive/December_2012/freertos_vTaskDelay_0_vs_vTaskDelay_1_6518023.html
0
Ну-у, тут вообще много всяких идей пришло на новую статью. Может быть, соберусь, сделаю еще что-нибудь…
0
О! Буду ждать! У самого лежит без дела stm32f4 discovery, вот только осцилографа нет и времени…
А что касается характеристик РВ, то основной параметр не время системного тика, который влияет только на таймслайс, а время реакции на прерывание и время переключения контекста. Другими словами, время на приход события от физического объекта в контекст таска или задачи и время взаимодействия тасков между собой. Тут как раз и проявляются разные эффекты разных ОСРВ, начиная от тормозов в очередях и заканчивая блокировками в NMI обработчиках и невытесняемых тасках.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.