КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 712480 томов
Объем библиотеки - 1400 Гб.
Всего авторов - 274474
Пользователей - 125061

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

С одной стороны - казалось бы вполне «знакомая и местами изьезженная» тема (чуть не сказал - пластинка)) С другой же, именно нюансы порой позволяют отличить очередной «шаблон», от действительно интересной вещи...

В начале

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

Вообще-то если честно, то я даже не собирался брать эту книгу... Однако - отсутствие иного выбора и низкая цена (после 3 или 4-го захода в книжный) все таки "сделали свое черное дело" и книга была куплена))

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

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

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

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
Влад и мир про Черепанов: Собиратель 4 (Боевая фантастика)

В принципе хорошая РПГ. Читается хорошо.Есть много нелогичности в механике условий, заданных самим же автором. Ну например: Зачем наделять мечи с поглощением душ и забыть об этом. Как у игрока вообще можно отнять душу, если после перерождении он снова с душой в своём теле игрока. Я так и не понял как ГГ не набирал опыта занимаясь ремеслом, особенно когда служба якобы только за репутацию закончилась и групповое перераспределение опыта

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).

Операционные системы микроконтроллеров. На примере операционной системы реального времени FreeRTOS [Владимир Мединцев] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

ОПЕРАЦИОННЫЕ СИСТЕМЫ
МИКРОКОНТРОЛЛЕРОВ
Владимир Мединцев

На примере операционной системы реального времени FreeRTOS

© 2023, Владимир В. Мединцев. «Операционные системы
микроконтроллеров». Все права защищены. Ни одна часть этого
документа не может быть воспроизведена или передана каким-либо
образом, электронным, механическим, методом фотокопирования,
записи или как-то ещё без письменного разрешения автора.

УДК 004
ББК 32.973
М42

М42

Мединцев Владимир
Операционные системы микроконтроллеров : На примере
операционной системы реального времени FreeRTOS / Владимир
Мединцев. — [б. м.] : Издательские решения, 2023. — 228 с.
ISBN 978-5-0060-0974-5
УДК 004
ББК 32.973

12+ В соответствии с ФЗ от 29.12.2010 №436-ФЗ

ISBN 978-5-0060-0974-5

Оглавление
Оглавление.................................................................. 3

Введение ...................................................................... 7

Глава 1. Суперцикл .............................................. 10
FreeRTOS™ ................................................................................................. 13
Терминология .............................................................................................. 17

Глава 2. Структура FreeRTOS ......................... 19
Файлы FreeRTOS ........................................................................................ 23
Типы данных и стиль ................................................................................. 28
Имена переменных ..................................................................................... 28
Имена функций........................................................................................... 29
Форматирование ......................................................................................... 29
Макросы ...................................................................................................... 29

Глава 3. Управление памятью ......................... 31
Схема Heap_1 ............................................................................................. 33
Схема Heap_2 ............................................................................................. 35
Схема Heap_3 ............................................................................................. 37
Схема Heap_4 ............................................................................................. 37
Схема Heap_5 ............................................................................................. 40
Функции работы с кучей ........................................................................... 42

Глава 4. Управление задачами ........................ 44
Приоритеты задач .................................................................................... 47
Квантование времени ................................................................................ 48
Реализация задачи ...................................................................................... 50
Создание задачи ......................................................................................... 51
Блокировка задачи ...................................................................................... 52
Блокирующие и не блокирующие задачи .................................................. 55
Задача простоя .......................................................................................... 56
Практические эксперименты ................................................................... 59
Квант времени ............................................................................................ 61
3

Функции управления приоритетами ........................................................ 68
Удаление задач ........................................................................................... 69
Планировщик .............................................................................................. 69
Приоритетное упреждающее планирование ............................................ 70
Упреждающее планирование с приоритетом .......................................... 72
Кооперативная многозадачность .............................................................. 73

Глава 5. Управление очередями ..................... 74
Создание очереди ....................................................................................... 77
Отправка данных в очередь ...................................................................... 77
Получение данных ...................................................................................... 79
Блокировка задач ........................................................................................ 80
Получение из нескольких источников ....................................................... 85
Данные переменной длины......................................................................... 87
Проблема использования очередей ........................................................... 91

Глава 6. Обработка прерываний ..................... 95
Функции API и обработчики прерываний ................................................ 96
Макросы portYIELD_FROM_ISR() и portEND_SWITCHING_ISR() ..... 99
Отложенная обработка прерываний .................................................... 100
Бинарный семафор ................................................................................... 103
Создание бинарного семафора ................................................................ 105
«Взять» семафор xSemaphoreTake() ....................................................... 105
«ДАТЬ» семафор xSemaphoreGiveFromISR() ........................................ 106
Синхронизация прерывания и задачи...................................................... 107
Счетный семафор .................................................................................... 110
Создание счетного семафора .................................................................. 111
Практический пример.............................................................................. 112
Эффективность дизайна ........................................................................ 114
Вложенность прерываний....................................................................... 115

Глава 7. Программные таймеры ...................117
Контекст программного таймера ......................................................... 120
Очередь команд таймера ........................................................................ 121
Создание и запуск программного таймера ............................................ 122
Идентификатор таймера ...................................................................... 125
Изменение периода таймера................................................................... 128
Практическое использование .................................................................. 130
Обработка прерываний в задаче – демоне ............................................ 132
4

Централизация .......................................................................................... 133
Практическое использование демона..................................................... 135

Глава 8. Потокобезопасность ........................138
Критические секции кода ........................................................................ 141
Приостановка планировщика ................................................................. 142

Глава 9. Снижение энергопотребления .....144
Макрос portSUPPRESS_TICKS_AND_SLEEP() ...................................... 145
TickLess Idle на практике ........................................................................ 146
Корректировка времени .......................................................................... 151

Глава 10. Мьютексы ..........................................154
Создание мьютекса ................................................................................. 157
Проблемы использования мьютексов..................................................... 158
Инверсия приоритета ............................................................................... 158
Наследование приоритетов ..................................................................... 160
Пат ............................................................................................................. 161
Рекурсивные мьютексы ........................................................................... 162
Планирование задач ................................................................................. 163
Задача привратник .................................................................................. 166

Глава 11. Группы событий ..............................168
Группы, флаги, биты ............................................................................... 169
Создание группы событий ...................................................................... 170
Установка событий ................................................................................ 170
Ожидание событий ................................................................................. 173
Практика .................................................................................................. 177
Проблемы точки синхронизации............................................................. 180
Создание точки синхронизации .............................................................. 182

Глава 12. Уведомления .....................................184
Использование уведомлений .................................................................... 187
Отправка уведомлений ............................................................................ 187
Получение уведомлений .......................................................................... 189
Уведомления как семафоры .................................................................... 189
Уведомления ............................................................................................. 192
Ожидание уведомлений ........................................................................... 195
5

Еще один пример...................................................................................... 196

Глава 13. Отладка и трассировка .................199
Стороннее Программное обеспечение ................................................... 200
Генератор кода ......................................................................................... 200
Мониторинг и отладка ............................................................................. 203
Средства операционной системы .......................................................... 207
Статистика времени выполнения задачи ............................................... 208
Величина стека ......................................................................................... 212
Функции обратного вызова ..................................................................... 213
Переполнение стека ................................................................................. 215

Глава 14. Макросы..............................................217
Задачи ........................................................................................................ 220
Очереди ..................................................................................................... 222
Таймера ..................................................................................................... 224
Группы событий....................................................................................... 225
Куча ........................................................................................................... 226

Заключение ............................................................227
Об авторе ................................................................228

6

Введение
Программирование встраиваемой электроники во многом
консервативный процесс. Можно наблюдать как в мире персональных
компьютеров с головокружительной скоростью происходит смена одного
языка программирования другим, рождаются и уходят в бесконечность
парадигмы программирования. По сравнению с этим, мир
микроконтроллеров кажется островом стабильности. Отчасти это так.
Созданный в 1972 году язык Си хоть и подвергается регулярным
нападкам, продолжает оставаться надежным и устойчивым стандартом в
отрасли. Именно на этом языке создается более 80% кода для
микроконтроллерных систем. У этого есть вполне логичное объяснение:
1.

2.

3.

4.

5.

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

7

Явные преимущества языка Си заложили основу его долголетия и
популярности в отрасли. Он был и остается базовым стандартом, основой
с которой начинается изучение микроконтроллеров. В связи с удобством
и популярностью среди разработчиков, микроконтроллеры развивались
весьма активно. Появлялись новые и совершенствовались привычные
периферийные блоки. Увеличивался объём памяти, частота системной
шины.
Рост количества применяемых периферийных блоков, увеличение
сложности системы обработки прерываний и появление проектов
большой сложности требует изменения подходов к программированию.
Многим разработчикам нравится многозадачность отчасти потому, что
позволяет одновременно выполнять несколько параллельных потоков
без существенного усложнения кода; потому, что при использовании
многозадачности становится возможным сконцентрироваться на
обслуживании конкретной задачи и на написании кода. Именно такие
потребности привели к тому, что в определенный момент появилось
множество проектов, реализующих функции операционных систем
реального времени для микроконтроллеров.
Для написания этой книги выбрана операционная система FreeRTOS.
Ядро FreeRTOS™ де-факто является стандартом на рынке. Оно
разрабатывалось более 18 лет в сотрудничестве с ведущими мировыми
производителями микроконтроллеров. FreeRTOS распространяется
бесплатно по лицензии MIT с открытым исходным кодом и включает в
себя ядро и растущий набор библиотек, подходящих для использования
во всех отраслях промышленности.
FreeRTOS предлагает более низкие проектные риски и более низкую
общую стоимость владения, чем коммерческие альтернативы, потому что
она полностью поддерживается и документируется.
Работа над этой книгой была достаточно растянутой во времени.
Какие-то главы писались во время формирования первой версии
программы «Инженеров умных устройств» написанной для обучающей
платформы «GeekBrains», какие-то писались позже во время пересмотра
и расширения учебного курса.
Присматриваясь к идеям, заложенным в реализации функций API
операционной системы, можно заметить тенденции в развитии embedded
программирования за последние годы. Например, глава, посвященная
8

мьютексам была добавлена в последние дни работы над книгой. Мне не
хотелось упоминать о мьютексах ввиду достаточно трудно отлаживаемых
и диагностируемых проблем, связанных с их использованием.
Последние версии FreeRTOS показывают, что отношение
разработчиков, их концептуальный взгляд на функциональное
наполнение операционной системы меняется в сторону повсеместного
использования нотификации и постепенного отказа от очередей,
мьютексов и прочих более ресурсоемких средств синхронизации.
Тем не менее книга охватывает практически весь объём
функциональных
возможностей
и
средств
рассматриваемой
операционной системы.
Пользуясь возможностью, хотелось бы поблагодарить всех моих
коллег, кто советами, идеями и примерами помогал создавать эту книгу.
Особую признательность хотел бы выразить Александру Пономаренко,
как человеку, открывшему для меня красоту FreeRTOS и подарившему
идею создания этой книги и, конечно, же Эдуарду Неткачеву за
поддержку проекта.

9

Глава 1. Суперцикл
Функция main() - основная функция создаваемого нами прикладного
программного обеспечения. Именно она получает управление при
запуске микроконтроллера и при нормальном функционировании
создаваемого кода функция не будет завершена все время, пока работает
содержащее микроконтроллер устройство. Логически функцию main()
можно разделить на две части. Однократно выполняемый фрагмент кода
до бесконечного цикла while и сам бесконечный цикл.
int main(void)
{
// Инициализация переменных, периферии
// Однократно выполняемый фрагмент кода
while (1)
{
// Регулярно повторяющиеся действия
}
}

Такая организация очень практична. В однократно выполняемую
часть кода мы можем поместить вызов всех функций инициализации
периферийных блоков микроконтроллера, функций инициализирующих
используемые нами библиотеки и прочие действия, направленные на
подготовку к выполнению основной задачи.
В качестве примера разберемся с прототипом кода, создаваемого
для температурного контроллера. Функции, приведенные в примерах
этой главы, имеют условные названия, передающие суть возложенного на
них функционала.
int main(void)
{
// Инициализация
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while (1)
{
// Выполняемые программой действия
ReadKey();
// Реакция на клавиатуру
ReadSensor();
// Чтение значения сенсора

10

}

}

DisplayData();
PID();
setPWM();

// Вывод данных на экран
// ПИД регулирование
// Формирование управляющего воздействия

У приведенного выше примера есть один недостаток – повторение
цикла происходит слишком быстро, с максимально возможной
скоростью. Это не просто избыточно, иногда это может быть вредно. В
большинстве случаев программист предпринимает меры к уменьшению
скорости повторения цикла за счет добавления задержек.
while (1)
{
ReadKey();
ReadSensor();
DisplayData();
PID();
setPWM();
HAL_Delay(20);
}

// Задержка на 20 миллисекунд

Добавленная задержка в 20 миллисекунд замедляет выполнение
цикла и снижает частоту, с которой будет осуществляться вызов функции
ReadKey() до 50 раз в секунду. Это позволяет реализовать опрос
клавиатуры и борьбу с дребезгом контактов.
Однако, если мы создаем температурный контроллер, управляющий
какими-либо процессами нам вряд ли потребуется получать данные о
температуре и формировать управляющее воздействие с такой высокой
частотой. Как следствие мы должны уменьшить частоту опроса сенсора.
Теперь мы уже не можем использовать какую-либо задержку, но можем
обрабатывать часть кода, пропуская несколько итераций бесконечного
цикла.
while (1)
{
ReadKey();
if (count == 49) // Только 1 раз в 50 циклов
{
count = 0;
ReadSensor();
DisplayData();
PID();
setPWM();

11

}

}
Count++;
HAL_Delay(20);

После таких изменений функция ReadKey() будет вызываться 50 раз в
секунду, что даст возможным функционировать алгоритму борьбы с
дребезгом контактов, а все остальные функции будут вызываться 1 раз в
секунду, что вполне достаточно и для осуществления процесса опроса
датчика и для процесса регулирования.
Но что делать, если в этом коде нам потребуется сделать
подтверждение какой-либо операции, показав пользователю пару
коротких или пару длинных вспышек светодиодом? Что делать, если
потребуется делать несколько операций с разной частотой вызова? К
примеру, выводить информацию 1 раз в секунду, а опрашивать датчик и
проводить фильтрацию считанного значения 3 раза в секунду?
В подавляющем большинстве случаев то, в чем мы действительно
нуждаемся - это планировщик, некий механизм, позволяющий вызывать
различные функции в заданное время или с заданной частотой.
Если посмотреть на примеры сложных проектов, размещенных на
GitHub, вы найдете множество примеров того, как авторы создавали
различного рода планировщики – программные реализации идеи вызова
необходимых функций в необходимое время. Проще всего это можно
реализовать при помощи прерывания от таймера.
Идея создания простейшего «селектора задач» - алгоритма
определяющего какую функцию вызвать в каждый конкретный момент
времени, может быть вполне успешно использована, но потенциально
является источником ряда практически не решаемых проблем:





Если какая-либо функция получающая управление от
простейшего «селектора задач» требует продолжительного
времени для выполнения, все остальные действия будут
отложены;
Нет возможности менять выполняемую функцию (выполняемую
задачу) в режиме реального времени в ответ на внешние
события;
Нет возможности обеспечить выделение пропорционального
времени каждой вызываемой функции.
12

Иными словами, если какой-либо функции вызываемой таким
импровизированным «селектором задач» потребуется дополнительно
время в результате возникшего «таймаута», то это время будет
предоставлено в ущерб другим функциям. Это является главной
проблемой такого рода подхода.

FreeRTOS™

Операционная система реального времени (ОСРВ, англ. Real-time
operating system, RTOS) — тип специализированной операционной
системы, основное назначение которой — предоставление необходимого
и достаточного набора функций для проектирования, разработки и
функционирования систем реального времени на конкретном
аппаратном оборудовании.
FreeRTOS – один из примеров реализации операционных систем
реального времени для приложений, использующих микроконтроллеры.
Первоначально FreeRTOS разрабатывалась компанией Real Time Engineers
Ltd.
Приложения, создаваемые для микроконтроллеров чаще всего,
включают в себя сочетания как жестких, так и мягких требований
реального времени.
Требования мягкого реального времени — это те, которые
устанавливают крайний срок реакции на возникающие события, но
нарушение крайнего срока не делает систему бесполезной. К примеру,
слишком медленная реакция на нажатие клавиши может привести к тому,
что система будет казаться немного заторможенной, но эта особенность
не сделает ее непригодной для использования. Да систем мягкого
реального времени характерно следующее:




Система имеет гарантированное время реакции на внешние
события (прерывания от оборудования);
жёсткая
подсистема
планирования
процессов
(высокоприоритетные задачи не должны вытесняться
низкоприоритетными, за некоторыми исключениями);
повышенные требования к времени реакции на внешние
события или реактивности (задержка вызова обработчика
13

прерывания не более десятков микросекунд, задержка при
переключении задач не более сотен микросекунд).
Жесткие требования реального времени — это те, которые
устанавливают крайний срок реакции на событие, и нарушение этого
срока приведет к полному отказу системы. Например, подушка
безопасности водителя может принести больше вреда, чем пользы, если
она слишком медленно реагирует на сигналы от датчика столкновения.
Спецификация UNIX в редакции 2 даёт следующее определение:
Реальное время в операционных системах — это способность
операционной системы обеспечить требуемый уровень сервиса в
определённый промежуток времени.
Основа FreeRTOS — это ядро реального времени (или планировщик
реального времени), на основе которого можно создавать встроенные
приложения, отвечающие требованиям жесткого реального времени. Это
позволяет организовать приложения как набор независимых потоков
выполнения. На процессоре с одним ядром одновременно может
выполняться только один поток. Ядро решает, какой поток должен
выполняться, изучая приоритет, назначенный каждому потоку
разработчиком приложения. В простейшем случае разработчик
приложения может назначить более высокий приоритет потокам,
реализующим требования жесткого реального времени, и более низкий
приоритет потокам, реализующим требования мягкого реального
времени. Это гарантирует, что потоки жесткого реального времени всегда
будут выполняться раньше потоков мягкого реального времени, однако и
решения о назначении приоритетов не всегда так просты.
В начале этой главы, в качестве примера, иллюстрирующего
необходимость создания некоторого диспетчера обеспечивающего
выполнения определенных программных функций в заданное время,
приведен пример реализующий идеи приоритизации задач. В реальных
проектах использование ядра ОСРВ может принести и другие, менее
очевидные преимущества:


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

14













Расширяемость. Абстрагируя код от деталей синхронизации, мы
порождаем меньшее количество взаимозависимостей между
модулями. Это дает возможность контролируемого развития
ПО.
Модульность. Задачи — это самостоятельные модули, каждый
из которых должен иметь четко определенную цель.
Командная работа. Разделение кода на задачи облегчает
разработку приложения в команде.
Упрощение тестирования. Если задачи представляют собой
четко определенные независимые модули с понятными
интерфейсами, их можно тестировать изолированно.
Реиспользование кода. Благодаря большей модульности и
меньшему количеству взаимозависимостей код можно
повторно использовать.
Повышение эффективности. За счет использования ядра
процессорное время не используется впустую. Задачи, которые
не должны выполняться не будут занимать процессорное время.
Утилизация времени. Задача Idle создается автоматически при
запуске планировщика. Она выполняется всякий раз, когда
появляется свободное (не востребованное другими задачами)
процессорное время. Idle можно использовать для измерения
свободных вычислительных мощностей, выполнения фоновых
проверок или просто для перевода процессора в режим
пониженного энергопотребления.
Управление питанием. Повышение эффективности, достигаемое
при использовании RTOS, позволяет процессору проводить
больше времени в режиме пониженного энергопотребления.
Отложенная обработка прерывания. Обработчики прерываний
можно сделать очень короткими, перенеся обработку либо в
задачу, созданную автором приложения, либо в задачу демона
FreeRTOS.

Самый простой пример, иллюстрирующий подход, предлагаемый
системой реального времени, это приложение, которое включает
клавиатуру и ЖК-дисплей. Пользователь должен получать визуальную
обратную связь о каждом нажатии клавиши в течение разумного периода
времени - если пользователь не видит, что нажатие клавиши было
принято в течение этого периода, программным продуктом будет в
лучшем случае неудобно пользоваться. Если наиболее длительный
15

допустимый период составлял 100 мс - приемлемым будет любой ответ
от 0 до 100 мс. Эта функция может быть реализована как автономная
задача со следующей структурой:
void vKeyHandlerTask( void *pvParameters )
{
// Обработка нажатий на клавиши это непрерывный процесс
// по этой причине задача представляет собой бесконечный
// цикл. Как и большинство задач в реальном времени.
for( ;; )
{
[Ожидание нажатия на клавишу]
[Обработка нажатой клавиши]
}
}

Теперь предположим, что система реального времени также
выполняет функцию управления, основанную на входных данных с
цифровой фильтрацией. Входной сигнал должен быть дискретизирован,
отфильтрован, а цикл управления должен выполняться каждые 2 мс. Для
правильной работы фильтра временная регулярность выборки должна
быть с точностью до 0,5 мс. Эта функция может быть реализована как
автономная задача со следующей структурой:
void vControlTask( void *pvParameters )
{
for( ;; )
{
[Пауза на 2ms с момента прошлого цикла]

}

}

[Получение входных данных]
[Фильтрация входных данных]
[Алгоритм управления]
[Выходное воздействие]

Программист должен назначить задаче управления наивысший
приоритет:
1.

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

16

2.

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

Терминология

Задача - Во FreeRTOS каждый поток выполнения называется
«задачей». В отличии от сообщества Linux/Unix систем в embedded нет
единого мнения относительно терминологии. Это будет особенно
заметно если после прочтения официальной документации FreeRTOS вы
прочитаете документацию Microsoft на ThreadX. Однако, лично я
предпочитаю «задачу» «потоку», поскольку в некоторых областях
применения поток может иметь более конкретное значение.
API
(Application
Programming
Interface)
интерфейс
программирования приложений. Это совокупность инструментов и
функций, которые представляют собой интерфейс для создания новых
приложений. И это замечательное определение из мира ПК. В нашем
случае мы имеем дело с кодом написанным на языке Си для
микроконтроллеров. И операционная система, и драйвера периферийных
устройств (HAL), и создаваемое прикладное программное обеспечение
будут выполняться в едином адресном пространстве микроконтроллера.
По этой причине понятие API сводится к набору функций, определенных в
коде операционной системе и предназначенных для взаимодействия ОС,
и прикладного программного обеспечения.
RTOS - Операционная система реального времени (ОСРВ, англ. realtime operating system) — тип специализированной операционной
системы, основное назначение которой — предоставление необходимого
и достаточного набора функций для проектирования, разработки и
функционирования систем реального времени на конкретном
аппаратном оборудовании.
Портирование – адаптация некоторой программы или её части,
чтобы она работала в другой среде, отличающейся от той среды, под
которую она была изначально написана с максимальным сохранением её
пользовательских свойств. Процесс портирования также называют
портированием или переносом, а результат — портом. Но в любом случае
главной задачей при портировании является сохранение привычных
17

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

18

Глава 2. Структура FreeRTOS
Ядро FreeRTOS и другие библиотеки FreeRTOS распространяются
бесплатно по лицензии MIT с открытым исходным кодом (SPDX-LicenseIdentifier: MIT). Это означает, что вы можете использовать FreeRTOS:








Абсолютно бесплатно;
Даже в коммерческих приложениях – бесплатно;
Никакая гарантия не предоставляется;
Использование FreeRTOS не накладывает на вас обязательств по
открытию исходного кода своего приложения.
Вы не обязаны открывать исходный код своих изменений,
вносимых в ядро FreeRTOS;
Вы не обязаны каким-либо образом документально
подтверждать, что ваш продукт использует FreeRTOS;
Не обязаны предлагать или предоставлять код FreeRTOS
пользователям вашего приложения.

Исходный код FreeRTOS доступен на сайте: https://www.freertos.org
У лицензии MIT есть интересная особенность. Несмотря на тот факт,
что лицензия содержит слова «безвозмездно использовать Программное
Обеспечение без ограничений» для получения разрешения необходимо
выполнить фразу «при соблюдении следующих условий» … «Указанное
выше уведомление об авторском праве и данные условия должны быть
включены во все копии или значимые части данного Программного
Обеспечения.» По сути это означает, что всем пользователям, которым вы
предоставите возможность использовать разрабатываемое вами
программное обеспечение вы должны сообщить, что его часть свободно
распространяется по лицензии MIT, и что эта часть называется FreeRTOS.
Помимо FreeRTOS на сайте доступны еще две версии операционной
системы, реализованные на ее основе:
1.

2.

OpenRTOS — это версия ядра FreeRTOS с коммерческой
лицензией, которая включает возмещение убытков и
специальную поддержку. FreeRTOS и OpenRTOS используют
одну и ту же кодовую базу.
SafeRTOS — это производная версия ядра FreeRTOS, которая
была проанализирована, задокументирована и протестирована
19

на соответствие строгим требованиям промышленного (IEC
61508 SIL 3), медицинского (IEC 62304 и FDA 510(K)),
автомобильного (ISO 26262) и других международных стандарты
безопасности. SafeRTOS включает артефакты документации по
жизненному циклу безопасности, прошедшие независимую
проверку.
FreeRTOS спроектирована так, чтобы быть простой и удобной в
использовании: требуются только 3 исходных файла, которые являются
общими для всех портов RTOS, и один исходный файл для конкретного
микроконтроллера. API файла порта разработан таким образом, чтобы
быть простым и интуитивно понятным. Каждый официальный порт
поставляется с официальным демонстрационным примером, который (по
заверению разработчиков) компилируется и выполняется на аппаратной
платформе, для которой данный пример предназначен, без каких-либо
изменений.
Если рассматривать развертывание операционной системы FreeRTOS
применительно к микроконтроллерам STM32, то вся работа по
подключению
необходимых
файлов
к
проекту,
изменению
конфигурационного файла FreeRTOSConfig.h и созданию основы проекта
будет возложена на конфигуратор кода STM32CubeMX.
НЕОБХОДИМО ОСОБО ОТМЕТИТЬ ВАЖНУЮ ДЕТАЛЬ – При
использовании генератора кода STM32CubeMX или интегрированной
среды разработки STM32CubeIDE для создания проекта помимо
очевидного ядра операционной системы FreeRTOS Kernel Copyright (C)
2020 Amazon.com в проект будут добавлены и файлы проекта CMSIS-RTOS
API. Данные файлы представляют собой ничто иное, как порт
выполненный компанией STMicroelectronics.
В результате расширенного портирования будут доступны как
официально предоставляемые функции API операционной системы, так и
расширенные
функции,
написанные
с
учетом
особенностей
микроконтроллеров, производимых STMicroelectronics. Более подробно с
информацией об особенностях применения CMSIS-RTOS API можно
ознакомиться, прочитав UM1722 - Developing applications on STM32Cube
with RTOS доступный на сайте STMicroelectronics.
Если вы решите использовать STM32CubeMX в качестве генератора
кода, то внедрение файлов операционной системы в создаваемый проект
20

будет осуществлено без каких-либо сложностей. Нужно выполнить
несколько простых действий:
1.
2.

Выберите категорию Middleware и щелкните по библиотеке
FREERTOS.
Выберите интерфейс CMSIS версия 1 или версия 2.

Рисунок 1. Интерфейс программы STM32CubeMX. Подключение FreeRTOS.

3.

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

21

Рисунок 2. Интерфейс программы STM32CubeMX. Подключение FreeRTOS.

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

Рисунок 3. Раздел «Загрузка» сайта freertos.org

22

Файлы FreeRTOS

Структура операционной системы позволяет с легкостью
портировать ее для использования примерно с двадцатью различными
компиляторами и может работать на более чем тридцати различных
процессорных архитектурах. Каждая поддерживаемая комбинация
компилятора и процессора считается отдельным портом FreeRTOS.
Упрощенно, FreeRTOS можно рассматривать как библиотеку,
предоставляющую возможности многозадачности. FreeRTOS поставляется
в виде набора исходных файлов на языке Cи. Некоторые из исходных
файлов являются общими для всех портов, в то время как другие
специфичны для порта.
Интеграция FreeRTOS в проект заключается во включении исходных
файлов операционной системы как часть вашего проекта, тем самым вы
делаете API операционной системы доступным для вашего приложения.
FreeRTOS настраивается с помощью конфигурационного файла
FreeRTOSConfig.h. Этот конфигурационный файл содержит в себе
константы, макросы и определения необходимые для гибкого
конфигурирования операционной системы под нужды конкретного
приложения.
Обратите внимание что для каждого порта операционной системы
написано собственное демонстрационное приложение, которое уже
содержит пример конфигурационного файла FreeRTOSConfig.h. По этой
причине нет необходимости создавать файл FreeRTOSConfig.h с нуля.
Вместо этого рекомендуется начать с FreeRTOSConfig.h, используемого
демонстрационным приложением, предоставленным для используемого
порта FreeRTOS, а затем адаптировать его.
Официально FreeRTOS распространяется в одном zip-файле. ZIP-файл
содержит исходный код для всех портов FreeRTOS и файлы проектов для
всех демонстрационных приложений FreeRTOS. Он также содержит набор
компонентов экосистемы FreeRTOS+ и набор демонстрационных
приложений экосистемы FreeRTOS+. Пусть вас не смущает количество
файлов в дистрибутиве FreeRTOS! В любом приложении требуется очень
небольшое количество файлов.

23

Рисунок 4. Список файлов.

На рисунке 4 показаны файлы операционной системы. Файлы
расположены в каталоге FreeRTOSv202112.00\FreeRTOS\Source.
Давайте начнем знакомится с файлами, общими для всех
существующих портов операционной системы. Основной исходный код
FreeRTOS содержится всего в двух файлах Си. Они называются tasks.c и
list.c и расположены непосредственно в каталоге FreeRTOS/Source.
Помимо этих двух файлов, в том же каталоге находятся следующие
исходные файлы:








queue.c – файл queue.c предоставляет службы очередей и
семафоров и активно используется почти во всех проектах.
timers.c – предоставляет функциональные возможности для
использования программных таймеров. Его нужно включать в
сборку только в том случае, если программные таймеры
действительно будут использоваться.
event_groups.c - предоставляет функциональные возможности
группы событий.
croutine.c - реализует функциональность сопрограммы.
Изначально сопрограммы задумывались для работы на
микроконтроллерах с крайне ограниченными ресурсами, в
настоящее время применяются редко.
stream_buffer.c – это новый функционал в операционной
системе. Потоковые буферы используются для отправки
непрерывного потока данных от одной задачи или прерывания к
другой. Их реализация достаточно компактна, что делает их
особенно подходящими для сценариев отправки данных из
обработчика прерывания в задачу и (или) между ядрами.
24

Помимо описанных выше файлов вам обязательно потребуется файл
порта, соответствующий используемому вами компилятору и архитектуре
микроконтроллера.
Эти
файлы
содержатся
в
каталоге
FreeRTOS/Source/portable. Искомые файлы порта будут находиться в
каталоге FreeRTOS/Source/portable/[compiler]/[architecture].
Как правило в качестве порта, в каталоге будут находиться файлы
port.c и portmacro.c. Эти файлы содержат определения (define) и макросы,
позволяющие операционной системе низкоуровнево взаимодействовать
с микроконтроллером. Это и взаимодействие с системным таймером
SysTick и определения типов, используемых данных, как элемент
переносимости кода под особенности компилятора.
Очень важно заметить, что и взаимодействие между операционной
системой и памятью микроконтроллера также является частью
переносимого уровня. В следующей главе мы обязательно разберемся с
тем, как происходит выделение памяти под задачи.
На текущий момент FreeRTOS предоставляет пять примеров схем
распределения кучи. Пять схем называются от heap_1 до heap_5 и
реализуются исходными файлами от heap_1.c до heap_5.c
соответственно. Примеры схем распределения кучи содержатся в
каталоге FreeRTOS/Source/portable/MemMang. Если вы настроили
FreeRTOS для использования динамического выделения памяти, то
необходимо скопировать один из данных файлов включив их в состав
создаваемого вами проекта.
Необходимо особо отметить, что примеры распределения кучи
имеют смысл только в тех приложениях, которые содержат константу
configSUPPORT_DYNAMIC_ALLOCATION, имеющую значение 1. Константа
должна быть определена в конфигурационном файле FreeRTOSConfig.h.
Как следует из изложенного выше, для интеграции FreeRTOS в
создаваемый вами проект потребуется включить всего три каталога в пути
компилятора. Это:
1.
2.
3.

Путь
к
основным
заголовочным
файлам
FreeRTOS/Source/include
Путь к файлам порта FreeRTOS/Source/portable/[compiler]/[architecture]
Путь к конфигурационному файлу проекта - FreeRTOSConfig.h.
25

-

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

"FreeRTOS.h"
"task.h"
"timers.h"
"queue.h"
"semphr.h"
"event_groups.h"

Первая строка - #include "FreeRTOS.h" добавляется в любом случае.
Все прочие файлы заголовков добавляются только в том случае, если вы
используете их функционал в своем проекте.
Обратите внимание, если вы используете микроконтроллеры
STMicroelectronics и генератор кода ST32CubeMX для создания основы
проекта, то вместо #include "FreeRTOS.h" генератор добавит в ваш проект
только включение заголовочного файла:
#include "cmsis_os.h"

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

Используя выбранные вами инструменты, создайте новый
проект, который еще не включает в себя исходные файлы
FreeRTOS.
Убедитесь в том, что проект собирается, загружается на целевой
микроконтроллер и выполняется.
Добавьте исходные файлы *.c в созданный вами проект. Как
было отмечено выше вам обязательно потребуются файлы tasks.c
и list.c. Остальные файлы необходимо добавить в проект при
необходимости использования их функционала.

26

4.
5.

6.

Скопируйте
из
демонстрационного
примера
порта
конфигурационный файл FreeRTOSConfig.h и разместите его в
своем проекте.
Добавьте в пути вашего проекта маршрут к следующим папкам,
содержащим файлы заголовков:
a. FreeRTOS/Source/include
b. FreeRTOS/Source/portable/[compiler]/[architecture]
c. Каталог
содержащий
конфигурационный
файл
FreeRTOSConfig.h
Сконфигурируйте все необходимые обработчики прерываний
FreeRTOS.
Используйте
веб-страницу
описывающую
используемый вами порт и демонстрационный проект как
образец.

В качестве примера, взгляните на код обработчика прерывания
системного таймера SysTick. Как видите, если планировщик запущен,
будет вызвана функция xPortSysTickHandler().
void SysTick_Handler(void)
{
HAL_IncTick(); /* Увеличение счетчика в библиотеке HAL 0 */
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}

Код ниже демонстрирует возможный код функции main() с запуском
планировщика.
int main( void )
{
/* Конфигурируем и инициализируем периферию. */
mySetupHardware();
/* Создаем задачи, очереди и пр. обьекты ОСРВ */
/* Запускаем планировщик */
vTaskStartScheduler();
/* Мы достигнем этой части кода только в том случае,
если планировщик аварийно завершит свою работу. */
for( ;; );
return 0;
}

27

Типы данных и стиль

Гораздо легче понимать написанное сторонними разработчиками
ПО или библиотеку, если знать используемый стиль именования функций,
переменных и предпочитаемые типы данных. Для операционной системы
FreeRTOS существует набор крайне простых и понятных правил.
Каждый вариант адаптации (port) FreeRTOS под конкретный
микроконтроллер имеет уникальный заголовочный файл portmacro.h,
который содержит (помимо прочего) определения для двух специальных
типов данных, TickType_t и BaseType_t.
TickType_t - Используется для хранения значения счетчика тиков и
переменных, которые задают время блока. TickType_t может быть 16битным без знака или 32-битным без знака, в зависимости от настройки
configUSE_16_BIT_TICKS в конфигурационном файле FreeRTOSConfig.h.
Использование 16-битного типа может значительно повысить
эффективность в 8-битных и 16-битных архитектурах, но сильно
ограничивает максимальный период таймаута, который может быть
задан. Нет причин использовать 16-битный тип данных в 32-битной
архитектуре микроконтроллеров.
BaseType_t - Всегда определяется как наиболее эффективный тип
данных для архитектуры. Обычно это 32-битный тип в 32-битной
архитектуре, 16-битный тип в16-битной архитектуре и 8-битный тип в 8битной архитектуре. BaseType_t обычно используется для переменных,
которые могут принимать только очень ограниченный диапазон
значений, а также для логических значений. Стандартные типы данных,
отличные от char, не используются, вместо этого используются имена
типов, определенные в файле заголовка stdint.h компилятора. Типы
«char» могут указывать только на строки ASCII или ссылаться на
отдельные символы ASCII.

Имена переменных
В качестве первых символов имени переменной используются
префиксы, отражающие тип данной переменной: «c» для char, «s» для
short, «l» для long и «x» для BaseType_t и любых других типов (структуры,
дескрипторы задач, дескрипторы очереди и т. д.).
28

Если переменная беззнаковая, она также имеет префикс «u». Если
переменная является указателем, она также имеет префикс «p».
Следовательно, переменная типа unsigned char будет иметь префикс «uc»,
а переменная типа указатель на char будет иметь префикс «pc».

Имена функций
Имена функций имеют префикс, отражающий как тип данных,
который они возвращают, так и префикс, соответствующий имени файла,
в котором они определены. Например:




vTaskPrioritySet () возвращает void и функция определена в
файле task.c.
xQueueReceive () возвращает значение типа BaseType_t и
определена в файле queue.c.
pvTimerGetTimerID() возвращает указатель (pointer) на void и
определена в файле timers.c.

Функции, используемые исключительно внутри файла, имеют
префикс «prv».

Форматирование
Один отступ табуляции всегда равен 4-м пробелам.

Макросы
Имена большинства макросов пишутся в верхнем регистре и имеют
префикс, набранный строчными буквами. Префикс определяет файл, в
котором дано определение макроса. Рассмотрим примеры префиксов
используемых в именовании макросов:






port (например, portMAX_DELAY) макрос определен в файлах
portable.h или portmacro.h
task (например, taskENTER_CRITICAL()) определен в файле task.h
pd (например, pdTRUE) определен в файле projdefs.h
config (например, configUSE_PREEMPTION) определен в
конфигурационном файле FreeRTOSConfig.h
err (например, errQUEUE_FULL) определен в файле projdefs.h

У описанного есть всего одно исключение. Практически весь API
работы с семафорами написан с использованием макросов, однако
29

имена макросов семафоров используют правила, описанные выше для
функций.
Также существуют глобальные определения доступные во всем
исходном коде FreeRTOS это определения: pdTRUE = 1, pdFALSE = 0,
pdPASS = 1 и pdFAIL = 0.

30

Глава 3. Управление памятью
Начиная с FreeRTOS V9.0.0, мы можем использовать две модели
размещения объектов ядра в памяти: Статически и Динамически.
Статически, когда вся необходимая для размещения объекта память
выделяется ему статически во время компиляции. Динамически – память
выделяется во время выполнения.
Во всех последующих главах этой книги мы будем изучать такие
объекты ядра, как задачи, очереди, семафоры и группы событий. Чтобы
максимально упростить использование FreeRTOS, эти объекты ядра не
размещаются статически во время компиляции, а динамически
распределяются во время выполнения; FreeRTOS выделяет ОЗУ каждый
раз, когда создается объект ядра, и освобождает ОЗУ каждый раз, когда
объект ядра удаляется. Эта политика сокращает усилия по
проектированию и планированию, упрощает API.
В этой главе, мы будем разбираться именно с особенностью
реализации динамического выделения памяти. Необходимо заметить,
что вопрос выделения памяти это всего лишь концепция
программирования на Си. Этот вопрос не является чем-то специфичным
именно для FreeRTOS или многозадачности. Однако, мы говорим об этом
в концепте операционной системы FreeRTOS, поскольку объекты ядра
выделяются динамически, а схемы динамического выделения памяти,
предоставляемые компиляторами общего назначения, не всегда
подходят для приложений реального времени.
Чаще всего, когда возникает дискуссия о выделении и последующем
высвобождении памяти, мы вспоминаем что эти операции могут быть
легко проделаны с помощью стандартных функций malloc() и free()
библиотеки Си. В зависимости от ядра микроконтроллера и версии
компилятора эти функции могут стать не самым лучшим решением по
одной или даже нескольким причинам:




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

31






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

Как отмечалось выше, начиная с FreeRTOS V9.0.0, объекты ядра
могут размещаться статически во время компиляции или динамически во
время выполнения. Теперь FreeRTOS рассматривает выделение памяти
как часть переносимого уровня, как часть порта. Это связано с тем фактом,
что разные встраиваемые системы имеют разные требования к
распределению динамической памяти и времени, поэтому единый
алгоритм распределения динамической памяти будет подходящим
только для подмножества приложений. Кроме того, удаление
динамического выделения памяти из основной кодовой базы позволяет
разработчикам приложений предоставлять свои собственные конкретные
реализации, когда это необходимо.
Это становится особенно удобным при использовании
возможностей некоторых архитектур, когда внешняя оперативная память
может быть включена в адресное пространство микроконтроллера и
использована для динамического размещения компонентов ядра.
Проще говоря, когда FreeRTOS требуется оперативная память,
вместо вызова malloc() она вызывает pvPortMalloc(). Когда ОЗУ
освобождается, вместо вызова free() ядро вызывает vPortFree().
pvPortMalloc() имеет тот же прототип, что и функция malloc() стандартной
библиотеки Си, а vPortFree() имеет тот же прототип, что и функция free()
стандартной библиотеки Си. pvPortMalloc() и vPortFree() являются
общедоступными функциями, поэтому их также можно вызывать из кода
приложения.
FreeRTOS поставляется с пятью примерами реализации
pvPortMalloc() и vPortFree(). В этой главе мы рассмотрим только heap_1.c,
heap_2.c, heap_3.c, heap_4.c. Последняя реализация - heap_5.c достаточно
сильно зависит от особенностей архитектуры и применяемого
компилятора и должна детально рассматриваться применительно к
описываемым особенностям.
32

Все поставляемые примеры определены в исходных файлах и
соответственно,
все
они
расположены
в
каталоге
FreeRTOS/Source/portable/MemMang.

Схема Heap_1

Небольшие проекты обычно обладают достаточным объёмом
оперативной памяти для того, чтобы все планируемые объекты ядра
операционной системы могли свободно разместиться в ней. Как правило,
такие объекты создаются до запуска планировщика задач. В этом случае
память хоть и выделяется динамически, но это происходит до того, как
приложение начинает выполнять какие-либо функции в реальном
времени, и память остается выделенной все время существования
приложения. Это означает, что схема распределения памяти может и не
учитывать какие-либо сложные моменты и проблемы возникающие в
ходе выделения памяти. Например, детерминизм, фрагментация. Взамен
модель предоставляет нам маленький размер кода и простоту
реализации.
Проще говоря, Heap_1.c реализует очень упрощенную версию
pvPortMalloc() и не реализует vPortFree() вообще. Приложения, которые
никогда не удаляют задачу или другой объект ядра, потенциально могут
использовать heap_1.
Вы наверняка знакомы с требованиями некоторых стандартов
относительно кода критически важных с коммерческой точки зрения и
безопасности систем. В большинстве таких систем крайне не
рекомендуется использовать динамическое выделение памяти. Этот
запрет на динамическое выделение памяти связан с тем, что этот процесс
имеет ряд неопределенностей, вызванных фрагментацией памяти и
неудачными выделениями. Heap_1 всегда является детерминированным
и не может фрагментировать память.

33

Рисунок 5. Схема heap_1. Работа с кучей.

Схема heap_1 чрезвычайно проста. В выделенной области памяти
создается массив, который именуется кучей FreeRTOS. Этот массив
делится на более мелкие фрагменты вызовом pvPortMalloc(). Общий
размер массива в байтах определяется константой configTOTAL_HEAP_SIZE
которая должна быть определена в конфигурационном файле
FreeRTOSConfig.h.
Подобный подход имеет и очевидный недостаток – определение
большого массива может привести к тому, что приложение будет
использовать слишком большой объём памяти даже до того, когда
фактически будут созданы компоненты ядра. Чтобы использовать эту
память оптимально, следует на завершающих этапах отладки приложения
измерить количество фактически потребляемого объёма памяти кучи
FreeRTOS и, при необходимости, произвести корректировку выделяемого
объёма памяти.
На рисунке 5: (1) показана вся куча FreeRTOS до того, как в ней будут
размещены компоненты ядра. (2) – состояние кучи после того, как в ней
будет размещена задача. Как видите для каждой задачи выделяются две
области. В одной из них хранится TCB (Task Control Block) – структура,
содержащая информацию о задаче и стек выделяемый самой задаче для
хранения ее локальных переменных. (3) – созданы две задачи. (4) –
состояние кучи FreeRTOS после того как были созданы три задачи.
34

Схема Heap_2

В современной идеологии FreeRTOS схема heap_2 является
устаревшей. Эта схема включается в комплект поставки для обеспечения
обратной совместимости с ранее созданными проектами, но она не
рекомендуется для использования в новых проектах. Разработчики
рекомендуют использовать heap_4 как более расширенную по
функционалу версию управления кучей.
Heap_2.c точно так же оперирует с массивом размер которого
определяется константой configTOTAL_HEAP_SIZE. Основное отличие от
heap_1 заключается в том, что схема heap_2 использует алгоритм,
позволяющий освобождать и повторно использовать память. Точно так
же, как и в случае с heap_1, схема использует статически объявленный
массив, поэтому память будет выделена и задействована даже в том
случае, если она фактически не используется элементами ядра
операционной системы.
Алгоритм, реализованный в схеме heap_2, позволяет функции
pvPortMalloc() найти блок памяти наилучшим образом соответствующий
запрашиваемому объёму памяти. В результате работы алгоритма будет
использован блок памяти, который максимально близок к
запрашиваемому объёму памяти. Например, представим, что куча
содержит три блока свободной памяти с размерами 5, 25 и 100 байт
соответственно. Функция pvPortMalloc() вызывается для выделения 20
байт. Наименьший свободный блок, в который поместится запрошенное
число байтов, — это 25-байтовый блок, поэтому pvPortMalloc() разбивает
25-байтовый блок на один блок из 20 байт и один блок из 5 байт, прежде
чем вернуть указатель на 20-байтовый блок. Новый 5-байтовый блок
остается доступны для будущих вызовов pvPortMalloc().
В отличие от схемы heap_4, heap_2 не умеет объединять соседние
свободные блоки в один блок большего размера, поэтому куча более
подвержена фрагментации.

35

Рисунок 6. Схема heap_2.

На рисунке 6 схематично показан алгоритм, реализованный в
heap_2, на примере задачи, которая была удалена и в последствии снова
создана:
1.
2.
3.

Показана куча после того, как были созданы три задачи.
Одна из задач была удалена, в результате чего, занимаемый
блок памяти был освобожден.
Показана ситуация после создания другой задачи. Создание
задачи привело к двум вызовам pvPortMalloc(): один для
выделения нового TCB (Task Control Block), и один для
выделения стека задач.

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

36

Схема heap_2 не является детерминированной, но работает
быстрее, чем большинство стандартных библиотечных реализаций
malloc() и free().

Схема Heap_3

Схема heap_3.c использует стандартные библиотечные функции
malloc() и free(), доступные в используемой реализации компилятора и
языка. В связи с этим, размер кучи определяется конфигурацией
компоновщика, и конфигурационный параметр configTOTAL_HEAP_SIZE не
имеет значения.
В то же время, heap_3 не просто «оборачивает» вызов malloc()
внутрь функции pvPortMalloc(). Схема делает вызовы malloc() и free()
потокобезопасными. Это достигается за счет временной приостановки
работы планировщика FreeRTOS.
По моему мнению, данная схема является худшим решением для
прикладного применения в системах реального времени именно из-за
приостановки работы планировщика и возникающей зависимости от
времени выполнения библиотечных реализаций malloc() и free().

Схема Heap_4

Подобно ранее описанным схемам heap_1 и heap_2, heap_4
производит выделение памяти путем разделения статически
выделенного массива на более мелкие блоки. Как и прежде размер
массива определяется константой конфигурации - configTOTAL_HEAP_SIZE.
Схема heap_4 умеет объединять смежные свободные блоки в
единый блок памяти, что очевидным образом способствует снижению
риска фрагментации памяти, а используемый алгоритм подбора
гарантирует, что pvPortMalloc() использует первый свободный блок
памяти, достаточно большой для размещения запрошенного количества
байтов. Например, рассмотрим сценарий, в котором куча содержит три
блока свободной памяти, которые в порядке их появления в массиве
составляют 5, 200 и 100 байт соответственно. Если произойдет вызов
функции pvPortMalloc() с запросом на предоставление блока в 20 байт, то
37

первый свободный блок, в который поместится запрошенное количество
байтов, — это 200-байтовый блок. В результате pvPortMalloc() разбивает
200-байтовый блок на один блок из 20 байт и один блок из 180 байт.
Функция вернет указатель на 20-байтный блок, а новый 180-байтовый
блок остается доступным для будущих вызовов pvPortMalloc().

Рисунок 7. Схема heap_4.

На рисунке 7 представлены ситуации, отображающие реакцию
схемы heap_4 на определенные изменения:
1.
2.

3.
4.

5.
6.

Показывает состояние кучи после того, как в ней была выделена
память под три созданных задачи.
Показан массив после удаления одной из задач. Обратите
внимание, что два блока памяти, высвободившиеся после
удаления TCB и стека задачи, были объединены в единый
массив.
Показывает ситуацию после того, как в результате вызова
функции xQueueCreate(), была выделена память для
размещения элементов очереди.
При выполнении прикладного программного обеспечения
произошел вызов функции pvPortMalloc() напрямую, без
использования API FreeRTOS. Блок, запрошенный приложением,
был достаточно мал, чтобы поместиться в первый свободный
блок.
Ситуация после того, как была удалена очередь.
Высвобождение выделенной прикладным программным
обеспечением области памяти, используемой как буфер.
38

Несмотря на то, что схема heap_4 не является детерминированной,
она работает быстрее, чем большинство стандартных библиотечных
реализаций функций malloc() и free().
В момент написания этой книги схема heap_4 является самой
популярной среди разработчиков. Она является схемой по умолчанию и в
силу своей универсальности подходит для большинства приложений.
Давайте рассмотрим некоторые необязательные параметры и настройки,
позволяющие расширить возможности этой схемы.
Самой частой потребностью, возникающей у разработчиков,
является желание разместить массив схемы heap_4, по определенному
адресу памяти. Например, если используется несколько типов
оперативной
памяти
в
едином
адресном
пространстве
микроконтроллера.
По умолчанию массив, используемый heap_4, объявлен внутри
исходного файла heap_4.c, а его начальный адрес автоматически
устанавливается компоновщиком в процессе сборки проекта. Однако, вы
можете
использовать
константу
конфигурации
configAPPLICATION_ALLOCATED_HEAP определив ее в конфигурационном
файле FreeRTOSConfig.h и присвоив ей значение 1. В этом случае массив
должен быть объявлен пользовательским приложением, использующим
FreeRTOS, и у программиста появляется возможность управлять
размещением этого массива в оперативной памяти.
Если для configAPPLICATION_ALLOCATED_HEAP установлено значение
1, то в одном из исходных файлов приложения должен быть объявлен
массив uint8_t с именем ucHeap и размерами, заданными параметром
сconfigTOTAL_HEAP_SIZE. Например, давайте рассмотрим примеры
объявления данного массива для компиляторов GCC и IAR.
Пример для GCC
uint8_t ucHeap[configTOTAL_HEAP_SIZE]
__attribute__ ((section(".my_heap")));

Пример для IAR
uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] @ 0x20000000;

39

Схема Heap_5

В рамках настоящей книги мы не будем глубоко вдаваться в
подробности использования схемы heap_5 из-за того, что конкретная
реализация работы данной схемы сильно зависит от особенностей
используемого вами компилятора и компоновщика.
Алгоритм, реализованный в коде схемы heap_5 для выделения и
освобождения памяти, полностью идентичен алгоритму, используемому
heap_4. Однако в отличие от heap_4, heap_5 не ограничивается
выделением памяти из одного статически объявленного массива; heap_5
может выделять память из нескольких областей памяти. Heap_5 полезен,
когда ОЗУ, предоставленное системой, в которой работает FreeRTOS, не
отображается как единый непрерывный блок адресного пространства
микроконтроллера.
Чтобы выделение памяти в нескольких раздельных областях стало
возможным heap_5 должна быть явно инициализирована перед вызовом
pvPortMalloc().
Для
инициализации
используется
API-функция
vPortDefineHeapRegions(). Эта функция должна быть вызвана до создания
каких-либо элементов ядра операционной системы (задач, очередей,
семафоров и т. д.), т.е. до того, как потребуется выделение памяти.
Прототип функции vPortDefineHeapRegions() имеет следующий вид:
void vPortDefineHeapRegions( const HeapRegion_t *
const pxHeapRegions );

Как вы наверняка уже догадались vPortDefineHeapRegions()
используется для указания начального адреса и размера каждой
отдельной области памяти, которые вместе составляют общую память,
используемую heap_5. Каждая отдельная область памяти описывается
структурой типа HeapRegion_t. Описание всех доступных областей памяти
передается в vPortDefineHeapRegions() в виде массива структур
HeapRegion_t.
Единственным
параметром,
передаваемым
в
функцию
vPortDefineHeapRegions(), является pxHeapRegions - указатель на начало
массива структур HeapRegion_t. Каждая структура в массиве описывает

40

начальный адрес и длину области памяти, которая будет частью кучи при
использовании heap_5.
typedef struct HeapRegion
{
/* Начальный адрес блока памяти.*/
uint8_t *pucStartAddress;
/* Размер блока памяти в байтах. */
size_t xSizeInBytes;
} HeapRegion_t;

В качестве примера рассмотрим пример, в котором имеются три
независимых области памяти, обозначенные как RAM1 размером 32
килобайта, RAM2 и RAM3 размерами 32 килобайта каждый.
#define
#define
#define
#define
#define
#define

RAM1_START_ADDRESS ( ( uint8_t * ) 0x01000000 )
RAM1_SIZE ( 64 * 1024 )
RAM2_START_ADDRESS ( ( uint8_t * ) 0x01030000 )
RAM2_SIZE ( 32 * 1024 )
RAM3_START_ADDRESS ( ( uint8_t * ) 0x02000000 )
RAM3_SIZE ( 32 * 1024 )

/* Массив определений HeapRegion_t регионов. */
const HeapRegion_t xHeapRegions[] =
{
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Конец массива. */
};
int main( void )
{
/* Инициализация схемы heap_5. */
vPortDefineHeapRegions( xHeapRegions );
}

/* Прочий код приложения... */

На рисунке 8 показано расположение выделяемых областей памяти
в адресном пространстве.

41

Рисунок 8. Выделяемые области памяти.

Функции работы с кучей

В процессе анализа (отладки) работы прикладного программного
обеспечения порой требуется оценить какой объём кучи остается
свободным. FreeRTOS API содержит две функции позволяющие в режиме
реального времени получать информацию о использовании кучи.
size_t xPortGetFreeHeapSize( void );

Функция xPortGetFreeHeapSize() возвращает количество байт в куче,
свободных на момент вызова функции. Это значение может быть
использовано для оптимизации выделяемой под кучу памяти. Например,
если вызов xPortGetFreeHeapSize() осуществленный после того, как все
элементы ядра будут созданы возвращает значение 4096, то можно
задуматься о том, что размер кучи может быть уменьшен.
Примите во внимание, что данная функция будет недоступна при
использовании схемы heap_3.
42

Вторая
функция
более
универсальна
xPortGetMinimumEverFreeHeapSize() возвращает минимальное количество
нераспределенных байт, которые когда-либо существовали в куче с
момента начала выполнения приложения FreeRTOS.
size_t xPortGetMinimumEverFreeHeapSize( void );

Универсальность данной функции заключается в том, что она
производит замер свободного пространства кучи не в момент своего
вызова, а показывает, насколько близко приложение когда-либо
подходило к исчерпанию пространства в куче.
Функция xPortGetMinimumEverFreeHeapSize() доступна только при
использовании схем heap_4 и heap_5.

43

Глава 4. Управление задачами
Это будет самая подробная глава книги, ведь представленные здесь
концепции являются основополагающими для понимания того, как
функционирует FreeRTOS и как ведут себя приложения, создаваемые с
использованием этой операционной системы. Управление задачами,
предоставление им доступа к ядру микроконтроллера является
приоритетной и важнейшей функцией операционной системы. Для
принятия решения о том, какой задаче предоставить доступ и когда,
планировщик должен знать состояние каждой задачи, существующей в
контексте операционной системы.
Создаваемое нами прикладное программное обеспечение может
состоять из неопределенного множества задач. Вместе с тем, если
микроконтроллер содержит только одно ядро, то в любой момент
времени может выполняться только одна задача. Таким образом все
задачи могут быть разделены на две категории «Running» (выполняется) и
«Not Running» (не выполняется). Когда задача находится в состоянии
«выполняется», процессор выполняет ее код. Когда задача «не
выполняется» мы можем говорить о том, что ее состояние сохранено и
сама задача ожидает того момента, когда планировщик решит, что он
может предоставить ей доступ к процессору. Когда задача вновь получит
этот доступ, ее выполнение будет продолжено с того же момента, в
котором задача находилась в предыдущий раз, когда имела доступ к
процессору. Т.е. с точки зрения задачи процесс переключения между
задачами и разделения ресурсов процессора происходит незаметно.
В операционной системе FreeRTOS только планировщик может
осуществлять переключение задач.
В главе 1 вопрос достаточного для каждой задачи времени уже
обсуждался. Подавляющее большинство задач вовсе не нуждаются в том,
чтобы иметь доступ к процессору постоянно. Всегда возможно
определить индивидуальный ритм, в котором задача будет выполняться.
Зачастую выполнение задачи может быть связано не только с некоторыми
временными циклами, но и с наступлением каких-либо событий.
Например, задача обрабатывающая поток входящих данных по
интерфейсу UART. Не существует какой-либо работы для выполнения в те
периоды времени, когда данные не поступают. И таких примеров может
44

быть множество - это и вывод информации на экран только в том случае,
если имеется обновление данных; и задача, обрабатывающая события от
органов управления.
Давайте рассмотрим этот вопрос еще раз. Если задачи,
существующие в нашей системе, будут использовать все имеющееся в
наличии доступное процессорное время, то распределение нагрузки
возможно только за счет разделения времени, предлагаемого
планировщиком. Т.е. задачи с наивысшим существующим приоритетом
будут использовать все имеющееся время, и планировщик разделит его
поровну между задачами. В то же время, задачи с более низким
приоритетом попросту не получат времени для выполнения.
Если же мы реализуем ситуацию, в которой задачи самостоятельно
выбирают темп выполнения, определяют временные интервалы, в
течение которых они не нуждаются в доступе к ресурсам процессора, то у
планировщика появляются свободные «тики», которые он может
распределять для задач с низким приоритетом. Мы еще вернемся к этому
вопросу немного позднее.
Для реализации концепта управления задач, при котором они могут
быть приостановлены в ожидании какого-либо события в операционной
системе FreeRTOS реализована расширенная модель состояний задач:
«Выполнение» Running — состояние, в котором задача фактически
выполняется и использует ядро микроконтроллера. Соответственно, если
микроконтроллер, на котором работает ОСРВ, имеет только одно ядро, то
в любой момент времени может быть только одна задача в состоянии
«Выполнение».
«Готова» Ready. В данном статусе находятся задачи, которые могут
выполняться (они не находятся в состоянии «Заблокировано» или
«Приостановлено»), но в настоящее время не выполняются, поскольку
другая задача с таким же или более высоким приоритетом уже находится
в состоянии «Выполняется» и использует процессор. Сразу после
создания задача находится в состоянии «готова», и, если у планировщика
нет более приоритетных задач, вновь созданная задача, получит
отведенный ей квант времени при следующем же вызове планировщика.
«Заблокирована» Blocked — задача находится в заблокированном
состоянии, если она в настоящее время ожидает события (событием
может быть период времени или внешнее событие). Например, если
45

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

46

Рисунок 9. Состояния задач.

Приоритеты задач

Каждой задаче назначается приоритет. Первоначально, в момент
создания задачи, приоритет определяется параметром uxPriority APIфункции xTaskCreate(). После того, как планировщик FreeRTOS будет
запущен приоритет задач можно изменять используя функцию
vTaskPrioritySet().
Каждой задаче назначается приоритет от 0 до (configMAX_PRIORITIES
- 1), где configMAX_PRIORITIES определяется в файле FreeRTOSConfig.h. Не
стоит увлекаться и устанавливать слишком большое значение для
константы configMAX_PRIORITIES. Количество приоритетов, определённых
в системе, влияет на размер используемой оперативной памяти.
Чем меньше число, тем ниже приоритет задачи. Неактивная задача
имеет нулевой приоритет (tskIDLE_PRIORITY). Планировщик FreeRTOS
47

гарантирует, что задачам в состоянии готовности или выполнения всегда
будет отдаваться процессорное время (ЦП). Любое количество задач
может иметь один и тот же приоритет. Если configUSE_TIME_SLICING не
определён или если configUSE_TIME_SLICING установлен в 1, то задачи
состояния готовности с равным приоритетом будут совместно
использовать доступное время обработки с использованием схемы
циклического планирования с временным разделением.
Планировщик FreeRTOS всегда гарантирует, что задачей с
наивысшим приоритетом, которая может быть запущена, является задача,
выбранная для перехода в состояние «Выполняется». Если может
выполняться более одной задачи с одинаковым приоритетом,
планировщик по очереди переводит каждую задачу в состояние
«Выполняется» и из него.

Квантование времени

Если существует две и более задачи с одинаковым приоритетом
ожидается, что, выполняя свои функции планировщик предоставит
каждой из них сопоставимые отрезки времени для доступа к ядру
микроконтроллера. Тем самым будет реализовано «квантование
времени» его разбивка на равные промежутки. В начале кванта времени
выбранная планировщиком задача будет получать доступ к процессору,
переходя в состояние «выполняется», в конце кванта возвращаться в
состояние «готова».
Для того, чтобы иметь возможность выбрать задачу для
последующего запуска, планировщик должен получать управление. В
большинстве операционных систем это происходит за счет использования
периодических прерываний. Так называемых «tick interrupt».
Длина, размер кванта времени фактически устанавливается частотой
с которой генерируются тиковые прерывания. Эта частота может быть
установлена как константой configTICK_RATE_HZ определяемой в
конфигурационном файле FreeRTOSConfig.h, так и в конфигураторе кода
STM32CubeMX при создании проекта.

48

Рисунок 10. Основные настройки ОС в программе STM32CubeMX.

В некоторых функциях API операционной системы FreeRTOS
требуется указать длительность или временной интервал в «тиках». Эти
значения всегда необходимо указывать в виде кратных периодов.

Рисунок 11. Квантование времени в реальной системе.

Давайте внимательно разберемся с тем, как происходит
квантование времени в реальной системе. В примере показаны две
задачи. Задача 1 и Задача 2. В интервалы времени t1 – t4 в системе
происходят
тиковые
прерывания,
являющиеся
механизмом,
запускающим выполнение планировщика. Планировщик, реализуя
выбранную стратегию, распределяет доступное время между задачами.
Обратите внимание, что на работу самого планировщика тоже
расходуется малая часть процессорного времени. С тем, насколько много
ресурсов расходуется на работу самого планировщика, мы обязательно
разберемся немного позже.
По умолчанию configTICK_RATE_HZ = 1000, что соответствует
времени тика равному 1 миллисекунде. Если вам потребуется изменить
это значение и использовать частоту тиков отличную от значения по
умолчанию, вы можете воспользоваться макросом pdMS_TO_TICKS(),
49

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

Реализация задачи

Задачи реализованы как функции
особенностью является их прототип:

Си,

единственной

их

void ATaskFunction( void *pvParameters );

Задача, реализуемая как функция Си должна возвращать параметр
void и принимать параметр указателя void.
Каждая задача представляет собой небольшую «самостоятельную»
программу. У задачи есть точка входа, есть собственный бесконечный
цикл.
void vUserTaskFunction (void * pvParameters)
{
// Обьявление локальных переменных
// Однократно выполняемый фрагмент кода
for( ;; )
{
// Код задачи
}
}

vTaskDelete (NULL);

Важно, чтобы функция, являющаяся воплощением задачи, не
содержала оператора return. Если задача более не требуется она должна
быть явно удалена вызовом функции vTaskDelete(). Представленный
выше прототип задачи вызывает эту функцию, если бесконечный цикл for
будет по каким-либо причинам прерван.
Очень удобным является тот факт, что одно объявление функции –
задачи может использоваться для создания любого количества задач.
Каждая создаваемая задача является отдельным экземпляром, имеет
свой собственный стек, локальные переменные.

50

Создание задачи

Задачи создаются при помощи функции xTaskCreate(). Прототип
данной функции выглядит следующим образом:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )

pvTaskCode

— это указатель на функцию, реализующую задачу (по
сути, просто имя функции).

pcName

- имя задачи. Имя не используется операционной
системой и указывается только для отладки.

usStackDepth

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

pvParameters

- параметр, передаваемый функции воплощающей в
себе задачу. По сути значение, присвоенное
pvParameters, — это значение, переданное в задачу.

uxPriority

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

pxCreatedTask

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

Параметр

Существуют только два возвращаемых значения: pdPASS - задача
создана успешно, и pdFAIL – указывающее на то, что задача не была
создана. Последнее может свидетельствовать о том, что недостаточно
памяти кучи, доступной для FreeRTOS.
Рассмотрим пример кода задачи:
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";

51

}

volatile uint32_t ul;
for( ;; )
{
vPrintString( pcTaskName );
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
__NOP();
}
}

Пример создания задачи:
int main( void )
{
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
vTaskStartScheduler();
}

for( ;; );

Приведенные выше примеры справедливы, если вы используете
операционную систему, имплементировав ее в проект самостоятельно.
Если проект создается генератором кода STM32CubeMX, то будет
интегрирована реализация FreeRTOS - CMSIS-RTOS адаптированная к
особенностям архитектуры микроконтроллеров, работающих на ядре
ARM Cortex. За счет этой адаптации некоторые функции могут выглядеть
непривычно. Например, создание задачи, имплементированное в проект
генератором кода STM32CubeMX, выглядит следующим образом:
osThreadDef(defaultTask, StartDefaultTask,
osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

Блокировка задачи

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

состояния задач, реализованной во FreeRTOS, мы можем использовать
статус «Заблокирована» (англ. Blocked) для того чтобы обозначить период
времени, в течении которого задача не нуждается в доступе к ресурсам.
Переход задачи в заблокированное состояние происходит после
вызова API-функции vTaskDelay(). Данная функция доступна только если
константа INCLUDE_vTaskDelay определена в конфигурационном файле
FreeRTOSConfig.h и имеет значение 1.
void vTaskDelay( TickType_t xTicksToDelay );

xTicksToDelay

- количество тиковых прерываний, в течение которых
вызывающая задача будет оставаться в состоянии
«Заблокировано».

vTaskDelay() - это функция API предоставляемая FreeRTOS. Значение,
передаваемое в качестве параметра, исчисляет время блокировки в
«тиках». Реализацией CMSIS RTOS, которая распространяется вместе с
генератором кода STM32CubeMX, предлагается аналогичная по
функционалу функция osDelay(), принимающая в качестве параметра
время блокировки определенное в миллисекундах.
osStatus osDelay (uint32_t millisec);

Это различие не имеет никакого значения vTaskDelay() при
использовании частоты «тиков» по умолчанию, когда 1 тик = 1
миллисекунде, но должно учитываться при нестандартной частоте тиков.
Функции osDelay() и vTaskDelay() отсчитывают время относительно
момента, когда они были вызваны. Их параметры определяют временной
отрезок между вызовом функции, в результате чего задача была
заблокирована и тем временем, когда задача вновь вернется в состояние
готовности к выполнению.
Вместо этого параметры функции vTaskDelayUntil() определяют
точное значение счетчика тиков, при котором вызывающая задача
должна быть переведена из состояния Blocked в состояние Ready.
vTaskDelayUntil() — это функция API, которую следует использовать, когда
требуется фиксированный период выполнения (когда вы хотите, чтобы
ваша задача выполнялась периодически с фиксированной частотой),
53

поскольку время разблокировки вызывающей задачи является
абсолютным, а не относительным когда была вызвана функция.
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime,
TickType_t xTimeIncrement );

pxPreviousWakeTime

Исходя из того, что vTaskDelayUntil() используется
для реализации задачи, которая выполняется
периодически и с фиксированной частотой.
Параметр pxPreviousWakeTime отражает время,
когда задача в последний раз переходила из
состояния
«Заблокировано».
Это
время
используется в качестве точки отсчета для
определения времени выхода задачи из состояния
блокировки.

xTimeIncrement

Параметр указывает на какое значение в тиках
задача
должна
быть
переведена
в
заблокированное состояние. Можно использовать
макрос pdMS_TO_TICKS() для преобразования
времени, указанного в миллисекундах во время,
указанное в тиках.

Основное различие между идеями заложенными в описанные выше
функции vTaskDelay() и vTaskDelayUntil() на первый взгляд не столь
очевидно. Обе эти функции могут быть использованы для реализации
периодических задач. Однако задача, реализованная с использованием
функции vTaskDelay(), не гарантирует, что частота выполнения будет
строго фиксированной, поскольку время выхода задач из состояния
Blocked зависит от времени, когда она вызвала vTaskDelay(). В то же время
задача, вызвавшая функцию vTaskDelayUntil() вместо vTaskDelay(), не
зависит от этой потенциальной проблемы, ведь ориентируется на
абсолютное время.
void vTask01( void *pvParameters )
{
char *pcTaskName;
TickType_t xLastWakeTime;
pcTaskName = ( char * ) pvParameters;
xLastWakeTime = xTaskGetTickCount();

54

}

for( ;; )
{
vPrintString( pcTaskName );
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS(250) );
}

Блокирующие и не блокирующие задачи

Рассматривая концепцию организации процесса разделения
времени между задачами, мы уже рассматривали два возможных
сценария. В первом случае мы можем организовать задачи с одинаковым
приоритетом, не предусматривая в их коде какой-либо функции,
блокирующей их выполнение. В этом случае планировщик организует
разделение имеющегося в его распоряжении доступного процессорного
времени, разделив его между задачами. Во втором случае мы можем
организовать задачи таким образом, чтобы они реализовывали модель
периодической
блокировки,
направленной
на
поддержание
необходимого темпа выполнения кода задач. Тем самым задача,
переходя в состояние блокировки освобождает ресурсы для их
распределения между другими задачами.
Попробуем провести эксперимент. Создадим три задачи. Задачи 1 и
2 будут работать в неблокирующем режиме. Не будут вызывать какихлибо функций API для перехода в состояние блокировки.
Задача 3 будет иметь небольшой «нагрузочный код», имитирующий
полезную нагрузку, выполняемую периодически. Раз в 3 миллисекунды.
Задача будет иметь более высокий, по сравнению с задачами 1 и 2
приоритет.
void StartTask01(void const * argument)
{
for(;;)
{
__NOP();
}
}
void StartTask02(void const * argument)
{
for(;;)
{

55

}

}

__NOP();

void StartTask03(void const * argument)
{
TickType_t xLastWakeTime;
const TickType_t xDelay3ms = pdMS_TO_TICKS( 3 );
for(;;)
{
for (uint32_t x = 0; x < 0xFFe; x++) __NOP();
vTaskDelayUntil( &xLastWakeTime, xDelay3ms );
}
}

Запустим логический анализатор и посмотрим на процесс
переключения задач. Как видно из диаграммы, задача 3 выполняется
строго периодически и получает управление раз в 3 миллисекунды. На
графике 5 приведены миллисекундные отсечки, совпадающие с началом
очередного тика. Задачи 1 и 2 равномерно разделяют оставшееся,
неиспользованное задачей 1, время.

Рисунок 12. Задачи с различным приоритетом.

Задача простоя

Задача простоя (Idle Task) создаётся автоматически при запуске
планировщика RTOS, это необходимо для того, чтобы гарантировать, что
всегда существует хотя бы одна задача, которая может быть запущена –
переведена в статус «выполняется». Для того, чтобы задача простоя не
мешала и не использовала процессорное время при наличии любых
других задач, теоретически способных выполняться, она создаётся с
минимально возможным приоритетом.
56

Нужно заметить, что ничего не мешает разработчику создать любую
другую задачу с нулевым приоритетом, в результате чего она будет
разделять доступное время с задачей простоя. Однако реальных причин
поступать таким образом мною найдено не было.
Обратите внимание, что задача простоя, помимо своей очевидной
функции (утилизации свободного процессорного времени) имеет еще и
важное значение для очистки ресурсов ядра операционной системы
после удаления какой-либо задачи вызовом функции vTaskDelete(). Таким
образом, если вы используете функцию удаления задач в своем
программном обеспечении, то должны позаботиться и о том, чтобы Idle
Task периодически имела время для выполнения и очистки мусора.
Задача простоя существует всегда, она является необходимым
компонентом и было бы глупо не воспользоваться ее уникальными
свойствами. При желании мы можем добавить собственный код в задачу
простоя с помощью функции обратного вызова (Callback). Эта функция
автоматически вызывается задачей бездействия один раз за итерацию
цикла бездействующей задачи.
На практике, существует много примеров, которые могут быть
реализованы при помощи встраивания пользовательского кода в задачу
простоя:





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

Существует ряд ограничений, действий, которые категорически
нельзя выполнять в задаче простоя:


Категорически запрещается использовать любые функции API
операционной системы, способные перевести задачу в
состояние блокировки. В этом случае в системе не останется ни
одной задачи способной выполняться;
57



Если создаваемое вами приложение использует API-функцию
удаления задач vTaskDelete(), то необходимо гарантировать, что
в течение разумного периода времени появится квант времени
не занятый другимизадачами, во время которого произойдет
вызов задачи простоя. Это связано с тем, что задача Idle отвечает
за очистку ресурсов ядра после удаления задачи.

Прототип функции обратного вызова задачи простоя:
void vApplicationIdleHook( void );

В этой главе, я неоднократно пользовался функцией обратного
вызова для того, чтобы показать на графиках время, остающееся
невостребованным другими задачами. Для этого был использован весьма
простой код:
void vApplicationIdleHook(void)
{
GPIOA->BSRR = GPIO_PIN_4;
__NOP();
GPIOA->BSRR = (uint32_t)GPIO_PIN_4 BSRR = GPIO_PIN_5;
__NOP();
GPIOA->BSRR = (uint32_t)GPIO_PIN_5 BSRR = GPIO_PIN_4;
__NOP();
GPIOA->BSRR = (uint32_t)GPIO_PIN_4 BSRR = GPIO_PIN_3;
xQueueSendFromISR(myQueue01Handle, &Item, &pxHigherPriorityTaskWoken);
GPIOA->BSRR = (uint32_t)GPIO_PIN_3 BSRR = GPIO_PIN_3;
xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(myBinarySem01Handle, &xHigherPriorityTaskWoken);

}

GPIOA->BSRR = (uint32_t)GPIO_PIN_3 BSRR = GPIO_PIN_2;
for(uint32_t x=0; xBSRR = (uint32_t)GPIO_PIN_2 BSRR = GPIO_PIN_3;
xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(myCountingSem01Handle, &xHigherPriorityTaskWoken);
xSemaphoreGiveFromISR(myCountingSem01Handle, &xHigherPriorityTaskWoken);
xSemaphoreGiveFromISR(myCountingSem01Handle, &xHigherPriorityTaskWoken);

GPIOA->BSRR = (uint32_t)GPIO_PIN_3 BSRR = GPIO_PIN_2;
for(uint32_t x=0; xBSRR = (uint32_t)GPIO_PIN_2 BSRR = GPIO_PIN_3;
// Действия таймера
__NOP();
GPIOA->BSRR = (uint32_t)GPIO_PIN_3 BSRR = GPIO_PIN_2;
__NOP();

135

GPIOA->BSRR = (uint32_t)GPIO_PIN_2 BSRR = GPIO_PIN_3;
for (uint32_t x = 0; x < 2000; x++) __NOP();
GPIOA->BSRR = (uint32_t)GPIO_PIN_3 BSRR = GPIO_PIN_1;

138

Однако, если мы посмотрим на ассемблерный код,
получившийся в результате компиляции (использован Keil
uVision), то увидим следующие строки:
MOVS
LDR
STR

r1,#0x02
r2,[pc,#24] ; @0x080012E0
r1,[r2,#0x18]

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

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

uint64_t p64 = 0x0F0F0F0F0F0F0F0F;

ассемблерный код:
MOV
LDR
STR
STR

r1,#0xF0F0F0F
r2,[pc,#20] ; @0x08001138
r1,[r2,#0x00]
r1,[r2,#0x04]

4.
Реентерабельность
функций.
Функция
является
«реентерабельной», если данную функцию можно безопасно
вызывать более чем из одной задачи одновременно. Т.е.
функция построена таким образом, что данные не будут
повреждены, а логические операции выполнены правильно.
Вы уже знакомы с тем, что каждая задача во FreeRTOS
поддерживает свой собственный стек и собственный набор
значений регистров процессора. Если функция не обращается ни
139

к каким данным, кроме данных, хранящихся в стеке или в
регистре,
то
функция
является
реентерабельной
и
потокобезопасной. Пример такой функции вы видите ниже.
float Sum(const float first, const float second)
{
float Sum;
Sum = first + second;
return Sum;
}

В качестве примера не реентерабельной функции можно
привести функцию, результат которой зависит от внешних
данных, которые могут быть изменены без какого-либо
уведомления.
uint32_t var1;
uint64_t Inc100(void)
{
uint64_t res;
res = var1 + 100;
return res;
}

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

140

Критические секции кода

Выполнение кода создаваемой нами задачи может быть прервано в
любой момент и заранее предсказать, когда задача продолжит свое
выполнение порой достаточно сложно. В то же время существуют
периферийные устройства, при работе с которыми мы должны
придерживаться некоторых таймингов. Бывают и протоколы, требующие
чтобы реакция или ответ, были сформированы в течении строго
определенного временного промежутка.
Для решения этих задач операционная система предоставляет нам
два макроса taskENTER_CRITICAL() и taskEXIT_CRITICAL(), определяющих
критическую секцию кода выполнение которой не может быть прервано.
Эти макросы не имеют каких-либо параметров или возвращаемых
значений.
/* Начало критической секции кода */
taskENTER_CRITICAL();
GPIOA->BSRR |= 0x01;
/* Конец критической секции кода. */
taskEXIT_CRITICAL();

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

то есть когда один вызов taskEXIT_CRITICAL() был выполнен для каждого
предыдущего вызова taskENTER_CRITICAL().
Существуют
версии
макросов
taskENTER_CRITICAL()
и
taskEXIT_CRITICAL() позволяющие создавать критические секции кода и в
обработчиках
прерываний.
taskENTER_CRITICAL_FROM_ISR()
и
taskEXIT_CRITICAL_FROM_ISR(). Эти макросы доступны только для портов
FreeRTOS, которые реализуют вложенность прерываний.

Приостановка планировщика

Развивая вопросы реализации критических секций кода,
безотносительно того, являются ли они хорошим решением или выбором
«меньшего зла» из возможного, мы можем создать участки кода
свободные от переключения контекста за счет временной приостановки
функционирования планировщика. Приостановка планировщика иногда
также называется «блокировкой» планировщика.
Существенная разница заключается в том, что критические секции,
описанные разделом ранее, защищают область кода от доступа другими
задачами и прерываниями. А критическая секция, реализованная путем
приостановки планировщика, защищает только область кода от доступа
других задач, поскольку прерывания остаются разрешенными. Об этой
особенности нужно помнить и учитывать ее при планировании структуры
создаваемого прикладного программного обеспечения.
Просто давайте посмотрим на это еще один раз. Только не с точки
зрения фрагмента кода, который мы хотим выполнить, а с точки зрения
следования основным принципам ОСРВ. И если для создаваемого ПО
важно сохранить функционирование системы обработки прерываний, то
правильным выбором будет именно приостановка планировщика.
void vTaskSuspendAll( void );

Планировщик приостанавливается вызовом vTaskSuspendAll().
Приостановка планировщика прекращает переключение контекста, но
оставляет прерывания включенными. Если прерывание запрашивает
переключение контекста, в то время как планировщик приостановлен, то

142

запрос будет отложен, и выполнен только при возобновлении работы
планировщика.
Логично, что и функции API FreeRTOS нельзя вызывать, пока работа
планировщика
приостановлена.
Возобновить
функционирование
планировщика можно вызовом функции xTaskResumeAll().

BaseType_t xTaskResumeAll( void );

Вызовы vTaskSuspendAll() и xTaskResumeAll() могут быть
вложенными. Ядро операционной системы производит подсчет глубины
вложенности. Работа планировщика будет возобновлена только тогда,
когда глубина вложенности вернется к нулю, то есть когда вызов
xTaskResumeAll() был выполнен для каждого предыдущего вызова
vTaskSuspendAll().

143

Глава 9. Снижение
энергопотребления
Возможно, это одна из самых ожидаемых глав книги. В моем
понимании специалисты эмбедеры делятся на две группы. Те, кто
разобрался
с
методами
снижения
энергопотребления
микроконтроллеров и те, кто мечтает найти подробное руководство.
Следует оговориться сразу. Не существует единого правильного
подхода
к
снижению
энергопотребления
и
повышению
энергоэффективности. Это всегда сложный, комплексный подход,
включающий как аппаратные решения (см. главу по энергопотреблению в
«Микроконтроллеры. Практический курс»), так и программные
алгоритмы
работы
с
периферийными
блоками
и
ядром
микроконтроллера для достижения оптимальных значений потребления
в различных режимах работы.
Самый простой и одновременно самый неэффективный метод - это
воспользоваться механизмом idle task. Эта задача всегда существует в
нашей системе, и мы легко можем инициировать функцию обратного
вызова (Callback) которая будет вызываться каждый раз, когда
планировщик не имеет других задач. С прототипом этой функции вы уже
знакомы.
void vApplicationIdleHook( void );

В этой функции вы можете реализовать переход микроконтроллера
в режим с пониженным энергопотреблением, который будет прерван
любым
прерыванием.
Например,
прерыванием,
извещающим
планировщик о том, что очередной тик времени закончен.
void vApplicationIdleHook(void)
{
__wfi();
}

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

энергопотреблением ко времени проведенном в состоянии сна весьма
невелико, а, следовательно, и не приносит большой экономии. Кроме
того, такой упрощенный подход может использоваться только с
режимами «неглубокого сна» (аналогично режиму Sleep в архитектуре
ARM Cortex-M).
Между тем, планировщик FreeRTOS достаточно много знает о
задачах, выполняемых под его контролем. Это дает возможность
реализовать более эффективные алгоритмы. В частности, операционная
система поддерживает режим «простоя без прерываний» - tickles idle.
В этом режиме планировщик FreeRTOS выявляет промежутки
времени, когда какие-либо задачи, находящиеся в состоянии «Готова»
отсутствуют. Если в результате планирования будет определено, что
задачи заблокированы и в ближайшей перспективе не изменят своего
состояния, то планировщик сможет инициировать переход в режим сна
на более длительное время, а как следствие мы можем использовать
более эффективные режимы энергопотребления. Для реализации
режима простоя без прерываний операционная система использует
макрос portSUPPRESS_TICKS_AND_SLEEP().

Макрос portSUPPRESS_TICKS_AND_SLEEP()

Для использования режима простоя без прерываний необходимо в
конфигурационном файле FreeRTOSConfig.h определить константу
configUSE_TICKLESS_IDLE. Возможны три варианта значения константы.
Константа не определена или имеет значение «0» - TICKLESS IDLE не
используется. Константа имеет значение «1» - используется встроенная
реализация функции portSUPPRESS_TICKS_AND_SLEEP(), константа имеет
значение «2» - будет использована пользовательская реализация
portSUPPRESS_TICKS_AND_SLEEP().
Если режим TICKLESS IDLE активирован, ядро вызовет макрос
portSUPPRESS_TICKS_AND_SLEEP(), если будут выполнены два условия:
1.
2.

Задача idle task - единственная задача, которая может
выполняться, поскольку все остальные задачи либо
заблокированы, либо приостановлены.
Планировщик планирует, что имеется не менее n полных
квантов времени, прежде чем какая-либо задача выйдет из
145

состояния
блокировки.
n

задается
константой
configEXPECTED_IDLE_TIME_BEFORE_SLEEP, которая должна быть
определена в конфигурационном файле FreeRTOSConfig.h.
Предложенный механизм реализации условий вхождения в
portSUPPRESS_TICKS_AND_SLEEP() очень прост и логичен. Он позволяет не
пытаться
ввести
микроконтроллер
в
режим
пониженного
энергопотребления при каждом вызове idle task, а дождаться ситуации, в
которой планирование ожидает не менее n квантов времени подряд, что
позволит избежать накладных расходов, связанных с потерей времени на
вход-выход из режима пониженного энергопотребления.

TickLess Idle на практике

Определим константу configUSE_TICKLESS_IDLE присвоив
значение 1 и посмотрим на работу режима tickles idle на практике:

ей

Рисунок 53. Режим tickles idle.

Внимательно посмотрите на представленную диаграмму. Задачи 1, 2
и 3 – периодические задачи, они имеют период 30, 31 и 32 миллисекунды
соответственно. Выполняют свой код и переходят в состояние
блокировки. График Sleep показывает время, в течении которого
микроконтроллер
находится
в
режиме
с
пониженным
энергопотреблением. Idle – короткие моменты времени, когда
планировщик вызывает задачу простоя. Tick – кванты времени.

146

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

Рисунок 54. Коррекция задач для примера с переходом в режим tickles idle.

Теперь вы можете видеть, что периоды времени, в течении которых
наш микроконтроллер реально имеет возможность перейти в режим сна,
возникают весьма нерегулярно и имеют разную продолжительность. Мы
уже говорили о том, что короткие промежутки сна не выгодны. Здесь и
можно применить константу configEXPECTED_IDLE_TIME_BEFORE_SLEEP,
описанную выше. Установим для нее значение 5.
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 5

147

Рисунок 55. Корректный момент перехода в режим пониженного энергопотребления.

Теперь, как видите, короткие по времени промежутки сна исчезли и
наш
микроконтроллер
переходит
в
режим
пониженного
энергопотребления только в те промежутки времени, когда это
действительно целесообразно. Разумеется, в каждом конкретном проекте
вам придется подбирать значение этой константы экспериментально.
А теперь давайте разберемся с тем, как за несколько строк кода
получить такой замечательный результат. Как и отмечалось ранее нам
необходимо только модифицировать конфигурационный файл
FreeRTOSConfig.h добавив в него определение двух констант:
#define configUSE_TICKLESS_IDLE
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP

1
5

Первая указывает на необходимость использовать реализацию
tickles idle, предложенную портом FreeRTOS, вторая указывает на то, что
не стоит переходить в состояние сна, если прогнозируемая
продолжительность сна менее 5 (пяти) тиков.
Разумеется программисту интересно разобраться с макросом
portSUPPRESS_TICKS_AND_SLEEP() немного подробнее. Посмотреть на то,
как реализована предлагаемая портом процедура перехода в режим
tickles idle. В качестве примера рассмотрим, как этот макрос определен
для микроконтроллера STM32F40x. Файл portmacro.h содержит
следующие строки:
/* Tickless idle/low power functionality. */
#ifndef portSUPPRESS_TICKS_AND_SLEEP
extern void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime);
#define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime )

148

#endif

vPortSuppressTicksAndSleep( xExpectedIdleTime )

Здесь #define подменяет portSUPPRESS_TICKS_AND_SLEEP() на
vPortSuppressTicksAndSleep().
Функция
vPortSuppressTicksAndSleep()
определяется здесь как внешняя (extern). Ввиду большого объёма кода не
вижу смысла приводить ее на страницах книги. Читатель сможет найти
код этой функции в файле port.c. Заметьте, что она обозначена как
переопределяемая (__weak) и может быть заменена пользовательской
реализацией без какой-либо модификации порта.
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )

Говоря о процедуре подготовки микроконтроллера к переходу в
режим пониженного энергопотребления, необходимо понимать, что этот
процесс занимает определенное время. Сначала планировщик
определяет,
что
выполняются
условия
вызова
portSUPPRESS_TICKS_AND_SLEEP(). Затем, уже в коде макроса происходит
сохранение текущего времени, перенастройка источника прерываний и
прочее. Существует шанс, что между моментом времени, когда было
принято решение о переходе в режим сна и фактическим вызовом
инструкции __wfi может произойти изменение состояния какой-либо
задачи или возникнет аппаратное прерывание. Для того, чтобы
своевременно отреагировать на подобные события существует функция
API проверяющая статус системы непосредственно перед переходом в
сон.
eSleepModeStatus eTaskConfirmSleepModeStatus( void );

Функция умеет возвращать одно из значений, перечисленных в
файле task.h:
typedef enum
{
eAbortSleep = 0,
eStandardSleep,
eNoTasksWaitingTimeout
} eSleepModeStatus;

/* Прервать вход в сон. */
/* Перейти в сон на указанное время. */
/* Перейти в сон до внешнего прерывания. */

149

Таким
образом,
перед
тем,
как
внутри
макроса
portSUPPRESS_TICKS_AND_SLEEP() будет выполнен переход в состояние
сна, мы должны вызвать eTaskConfirmSleepModeStatus() и получить
обновленную информацию о статусе. Функция вернет значение
eAbortSleep – если за время подготовки появилась какая-либо задача в
состоянии готовности; eStandardSleep – если ничего не изменилось и
микроконтроллер готов перейти в состояние сна на определенный
промежуток времени; eNoTasksWaitingTimeout – если микроконтроллер
можно перевести в состояние глубокого сна.
С отменой перехода в сон все понятно, это не более чем механизм
для предотвращения перехода в сон, если за время подготовки к этому
процессу произошло какое-либо прерывание и появилась какая-либо
задача в состоянии «Готова». А вот возвращаемое значение
eNoTasksWaitingTimeout открывает перед программистом достаточно
любопытные возможности. Прежде всего, нужно знать, что функция
eTaskConfirmSleepModeStatus() вернет значение eNoTasksWaitingTimeout
только при выполнении следующих условий:
1.
2.

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

Также, код portSUPPRESS_TICKS_AND_SLEEP() содержит вызов двух
макросов:
#define configPRE_SLEEP_PROCESSING
#define configPOST_SLEEP_PROCESSING

PreSleepProcessing
PostSleepProcessing

Один из них вызывается непосредственно перед, другой сразу после
перехода в состояние с пониженным энергопотреблением. Взгляните на
небольшой фрагмент кода portSUPPRESS_TICKS_AND_SLEEP():
configPRE_SLEEP_PROCESSING(xModifiableIdleTime);
if( xModifiableIdleTime > 0 )
{
__dsb( portSY_FULL_READ_WRITE );
__wfi();

150

__isb( portSY_FULL_READ_WRITE );
}
configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

Благодаря этим макросам вы можете выполнять собственные
фрагменты кода и готовить периферийные блоки микроконтроллера к
изменению режимов энергопотребления. При подготовке данной главы,
описываемые макросы были использованы для визуализации процесса
перехода в сон:
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
GPIOA->BSRR = GPIO_PIN_3;
}
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
GPIOA->BSRR = (uint32_t)GPIO_PIN_3 ucStaticallyAllocated == ( uint8_t ) pdFALSE )
{
vPortFree( pxQueue );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

217

В теле функции вы можете увидеть вызов макроса – ловушки
трассировки traceQUEUE_DELETE( pxQueue ). Он начинается с префикса
trace. Как и все макросы он объявлен в файле FreeRTOS.h:
#ifndef traceQUEUE_DELETE
#define traceQUEUE_DELETE( pxQueue )
#endif

Именно механизм макросов – ловушек был использован автором
книги для иллюстрации процесса переключения задач и визуализации
основных временных процессов в данной книге. Для этого, в файле
FreeRTOSConfig.h были переопределены два макроса вызывающиеся при
входе и выходе из задачи. Макросы теперь ссылаются на функции, а в
качестве параметров в функции передаются метки, содержащиеся в
контрольном блоке задачи (TCB) и идентифицирующие их.
#define traceTASK_SWITCHED_IN() TaskSwitchedIn((int)pxCurrentTCB->pxTaskTag);
#define traceTASK_SWITCHED_OUT() TaskSwitchedOut((int)pxCurrentTCB->pxTaskTag);

Для полноты примера рассмотрим на то, как выглядят функции
TaskSwitchedIn() и TaskSwitchedOut().
void TaskSwitchedIn(int tag)
{
switch (tag)
{
case 1:
GPIOA->BSRR = GPIO_PIN_0;
break;
case 2:
GPIOA->BSRR = GPIO_PIN_1;
break;
case 3:
GPIOA->BSRR = GPIO_PIN_2;
break;
case 4:
GPIOA->BSRR = GPIO_PIN_3;
break;
}
}
void TaskSwitchedOut(int tag)
{
switch (tag)
{

218

}

}

case 1:
GPIOA->BSRR
break;
case 2:
GPIOA->BSRR
break;
case 3:
GPIOA->BSRR
break;
case 4:
GPIOA->BSRR
break;

= (uint32_t)GPIO_PIN_0