Академический Документы
Профессиональный Документы
Культура Документы
FreeRTOS —
операционная система
для микроконтроллеров
Это первая статья из цикла, посвященного операционной системе для ми-
кроконтроллеров FreeRTOS. Статья познакомит читателя с задачами, ко-
торые решают операционные системы (ОС) для микроконтроллеров (МК).
Освещены вопросы целесообразности применения, преимущества и недо-
статки, присущие ОС для МК. Представлены возможности FreeRTOS, опи-
Андрей Курниц саны ее особенности, а также приведена структура дистрибутива FreeRTOS
kurnits@stim.by с кратким описанием назначения входящих в него файлов и директорий.
Что такое ОС для МК? Во‑вторых, микроконтроллер, по сути, мотки электромотора. Гораздо чаще из МК
это однокристальный компьютер с сильно пытаются «выжать» все, на что он способен,
В нынешний век высоких технологий все ограниченными аппаратными ресурсами, а в микроконтроллерное устройство зало‑
профессионалы знакомы с термином «опе‑ хотя диапазон выпускаемых МК по про‑ жить все возможные функции. Количество
рационная система» (ОС). История ОС на‑ изводительности и объемам памяти очень функций-задач, одновременно выполняемых
чинается с 1960‑х годов. Первые ОС пред‑ широк. Встречаются как «карлики», напри‑ МК, может доходить до нескольких десятков.
назначались для больших ЭВМ, а впослед‑ мер 8‑разрядный ATtiny10 с 6 выводами, И вот тут-то и начинаются проблемы.
ствии — и для персональных компьютеров. 32 байт ОЗУ, 1 кбайт ПЗУ и производитель‑ Как организовать мультизадачность
Назначением ОС стало заполнение ниши ностью 12106 операций в секунду (12 MIPS), и поочередное выполнение каждой задачи?
между низкоуровневой аппаратурой и вы‑ так и «гиганты», например 32‑разрядный Как обеспечить запуск задачи через строго
сокоуровневыми программами, они предо‑ TMS320C28346 c 256 выводами, 512 кбайт ОЗУ определенные интервалы времени? Как пере‑
ставляют программам удобный интерфейс и производительностью 600106 операций дать информацию от одной задачи другой?
обращения к системным ресурсам, будь с плавающей точкой в секунду (600 MFLOPS). Обычно эти вопросы не встают перед про‑
то процессорное время, память или устрой‑ Тем не менее все МК имеют существенные ап‑ граммистом в начале разработки, а возникают
ства ввода/вывода. С тех пор технологии паратные ограничения, что предъявляет спец‑ где-то в середине, когда он запрограммировал
шагнули далеко вперед: целую вычислитель‑ ифические требования к ОСРВ для МК. большинство функций будущего устройства,
ную систему (процессор, память, устрой‑ Их основные особенности: используя изобретенные им самим средства
ства ввода/вывода) разместили на одном 1. Низкая производительность. «многозадачности». И тогда заказчик «про‑
кристалле — появились микроконтроллеры 2. Малый объем ОЗУ и ПЗУ. сит» добавить еще несколько «маленьких» де‑
(МК). В соответствии с древним изречени‑ 3. Отсутствие блока управления памятью талей в работу устройства, например сбор ста‑
ем «Природа не любит пустоты» удачная (Memory management unit, MMU), исполь‑ тистики работы и запись ее на какой-нибудь
концепция ОС не могла не быть применена зуемого большинством современных ОС, носитель… Знакомая ситуация?
и к микроконтроллерам. В настоящее время например Windows и UNIX-подобными.
создано и развивается множество ОС, ори‑ 4. Отсутствие аппаратных средств поддержки Преимущества ОСРВ для МК
ентированных на выполнение на МК [1, 6]. многозадачности (например, средств бы‑
Однако МК как платформа для выполнения строго переключения контекста). З де с ь н а п о м о щ ь п р и ход и т О С Р В .
ОС имеет существенные отличия от совре‑ В‑третьих, микроконтроллер сам по себе Рассмотрим преимущества, которые полу‑
менных компьютеров. предназначен для выполнения низкоуровне‑ чил бы наш гипотетический программист,
Прежде всего, МК работает в режиме ре‑ вых задач, будь то опрос состояния кнопок, пе‑ заложив в основу программного обеспечения
ального времени, то есть время реакции ми‑ редача команды по I2C-интерфейсу или вклю‑ своего устройства ОСРВ:
кроконтроллерного устройства на внешнее чение обмотки электромотора. Программа 1. Многозадачность. ОСРВ предоставляет
событие должно быть строго меньше задан‑ для МК, как правило, обращается к перифе‑ программисту готовый, отлаженный ме‑
ной величины и должно быть сопоставимо рии напрямую, программист имеет полный ханизм многозадачности. Теперь каждую
со скоростью протекания внешних процессов. контроль над аппаратной частью, нет необ‑ отдельную задачу можно программиро‑
Типичный пример: время реакции на сраба‑ ходимости в посредниках между аппаратурой вать по отдельности так, как будто осталь‑
тывание датчика давления в промышленной и прикладной программой. Может показать‑ ных задач не существует. Например, мож‑
установке должно быть не более 5 мс, иначе ся, что операционная система для МК вообще но разработать архитектуру программы,
произойдет авария. Таким образом, ОС для не нужна, что любую программу можно на‑ то есть разбить ее на отдельные задачи
МК — это операционная система реально‑ писать и без ОС. На самом деле так оно и есть! и распределить их между командой про‑
го времени (ОСРВ). К ОСРВ предъявляются Но есть один нюанс: микроконтроллер ред‑ граммистов. Программисту не нужно за‑
жесткие временные требования в отличие ко используют только для опроса состояния ботиться о переключении между задачами:
от распространенных ОС общего назначения кнопок, только для передачи команды по I2C- за него это сделает ОСРВ в соответствии
(Windows, UNIX-подобные и др.). интерфейсу или только для включения об‑ с алгоритмом работы планировщика.
2. Временная база. Необходимо отмерять В этом случае можно применить один Существуют так называемые официально
интервалы времени? Пожалуйста, лю‑ из «традиционных» для МК способов орга‑ поддерживаемые аппаратные платформы —
бая ОСРВ имеет удобный программный низации многозадачности. Прежде всего, это официальные порты и неофициальные,
интерфейс для отсчета интервалов вре‑ циклический алгоритм (round robin) [3], ког‑ которые поставляются «как есть» и не под‑
мени и выполнения каких-либо действий да программист помещает все задачи в тело держиваются напрямую. Кроме того, для
в определенные моменты времени. бесконечного цикла. При этом на подпро‑ одного и того же порта могут поддерживать‑
3. О б м е н д а н н ы м и м е ж д у з а д ач а м и . граммы, реализующие задачи, накладывают‑ ся несколько средств разработки. Список
Необходимо передать информацию ся следующие ограничения: официальных портов и средств разработки
от одной задачи к другой без потерь? 1. Подпрограмма не должна содержать ци‑ приведен в таблице 1.
Используйте очередь, которая гарантиру‑ клов ожидания наступления какого-либо Основные характеристики FreeRTOS:
ет, что сообщения дойдут до адресата в том события, например прерывания. 1. Планировщик FreeRTOS поддерживает три
объеме и в той последовательности, в ко‑ 2. Подпрограмма должна лишь проверять, типа многозадачности:
торой были отправлены. наступило ли событие, и как можно бы‑ – вытесняющую;
4. Синхронизация. Разные задачи обращают‑ стрее передавать управление следующей – кооперативную;
ся к одному и тому же аппаратному ресур‑ подпрограмме, то есть завершать свое вы‑ – гибридную.
су? Используйте мьютексы или критиче‑ полнение. 2. Размер ядра FreeRTOS составляет всего
ские секции для организации совместного 3. Подпрограмма должна сохранять свое те‑ 4–9 кбайт, в зависимости от типа платфор‑
доступа к ресурсам. Необходимо выпол‑ кущее состояние (например, в статической мы и настроек ядра.
нять задачи в строгой последовательности или глобальной переменной) до следую‑ 3. FreeRTOS написана на языке Си (исходный
или по наступлении определенного собы‑ щего вызова. код ядра представлен в виде всего лишь че‑
тия? Используйте семафоры или сигналы Таким образом, каждая задача представля‑ тырех Си-файлов).
для синхронизации задач. ется в виде конечного автомата. Дальнейшее 4. Поддерживает задачи (tasks) и сопрограм‑
Кроме этого, одна и та же ОСРВ для МК развитие эта идея получила в SWITCH-тех- мы (co-routines). Сопрограммы специально
может выполняться на множестве архитектур нологии программирования [4, 5]. созданы для МК с малым объемом ОЗУ.
микроконтроллеров. Какое преимущество это 5. Богатые возможности трассировки.
дает? Часто приходится решать задачу не как Резюме 6. Возможность отслеживать факт перепол‑
разработать устройство с требуемой функ‑ Итак, применение ОСРВ оправдано в слу‑ нения стека.
циональностью, а как перенести имеющуюся чае использования достаточно мощного МК 7. Нет программных ограничений на количе‑
разработку на новую аппаратную платформу. при разработке сложного устройства с мно‑ ство одновременно выполняемых задач.
Это может быть связано с завершением про‑ жеством функций, например: 8. Нет программных ограничений на количе‑
изводства того или иного МК (окончание Life 1. Опрос датчиков. ство приоритетов задач.
cycle), с появлением на рынке МК, включаю‑ 2. Интерфейс с пользователем (простейшие 9. Нет ограничений в использовании прио‑
щего в состав некоторые блоки, которые ра‑ клавиатура и дисплей). ритетов: нескольким задачам может быть
нее были реализованы как отдельные микро‑ 3. Выдача управляющего воздействия. назначен одинаковый приоритет.
схемы, и т. д. В случае использования ОСРВ 4. Обмен информацией по нескольким вну‑ 10. Развитые средства синхронизации «зада‑
затраты времени и сил на переход на другую трисхемным шинам I2C, SPI, 1Wire и др. ча – задача» и «задача – прерывание»:
платформу будут заметно ниже за счет того, 5. Обмен информацией с внешними устрой‑ – очереди;
что часть кода, связанная с работой ОСРВ, ствами по интерфейсам RS-232C, RS-485, – двоичные семафоры;
останется без изменений. Изменения коснут‑ CAN, Ethernet, USB и др. – счетные семафоры;
ся только кода, отвечающего за обращение 6. Реализация высокоуровневых протоко‑ – рекурсивные семафоры;
к встроенной периферии (таймеры, АЦП, по‑ лов, например TCP/IP, ProfiBus, ModBus, – мьютексы.
следовательный приемопередатчик и т. д.). CANOpen и др. 11. Мьютексы с наследованием приоритета.
Однако за все надо платить. Использование 7. Поддержка Flash-накопителей и, соответ‑ 12. Поддержка модуля защиты памяти
ОСРВ приводит к определенным накладным ственно, файловой системы. (Memory protection unit, MPU) в процес‑
расходам. Это: сорах Cortex-M3.
1. Дополнительный расход памяти программ Обзор FreeRTOS 13. Поставляется с отлаженными примерами
для хранения ядра ОСРВ. проектов для каждого порта и для каждой
2. Дополнительный расход памяти данных FreeRTOS — это многозадачная, мульти‑ среды разработки.
для хранения стека каждой задачи, сема‑ платформенная, бесплатная операционная 14. FreeRTOS полностью бесплатна, моди‑
форов, очередей, мьютексов и других объ‑ система жесткого реального времени с откры‑ фицированная лицензия GPL позволяет
ектов ядра операционной системы. тым исходным кодом. FreeRTOS была раз‑ использовать FreeRTOS в проектах без
3. Дополнительные затраты времени процес‑ работана компанией Real Time Engineers Ltd. раскрытия исходных кодов.
сора на переключение между задачами. специально для встраиваемых систем. На мо‑ 15. Документация в виде отдельного доку‑
мент написания статьи (версия FreeRTOS 6.1.0) мента платная, но на официальном сайте
Когда можно обойтись ОС официально поддерживает 23 архитек‑ [7] в режиме on-line доступно исчерпы‑
без ОСРВ для МК? туры и 57 платформ (в подавляющем боль‑ вающее техническое описание на англий‑
шинстве — микроконтроллеры) [7]. В те‑ ском языке.
Конечно же, если вам необходимо разра‑ чение 2008 и 2009 годов произошло более Работа планировщика FreeRTOS в режи‑
ботать простейшее устройство, например 77 500 загрузок FreeRTOS с официального ме вытесняющей многозадачности имеет
индикатор температуры, который будет вы‑ сайта, что делает ее одной из самых популяр‑ много общего с алгоритмом переключения
полнять две функции: опрос датчика и ин‑ ных ОСРВ на сегодня. Бóльшая часть кода потоков в современных ОС общего назначе‑
дикацию на 7‑сегментный светодиодный FreeRTOS написана на языке Си, ассемблер‑ ния. Вытесняющая многозадачность пред‑
индикатор, то применение ОСРВ в таком ные вставки минимального объема применя‑ полагает, что любая выполняющаяся задача
устройстве будет непозволительным расто‑ ются лишь там, где невозможно применить с низким приоритетом прерывается готовой
чительством и приведет, в конечном счете, Си из-за специфики конкретной аппаратной к выполнению задачей с более высоким при‑
к удорожанию устройства. платформы. оритетом. Как только высокоприоритетная
Время сохранения/
тактирования, МГц
NXP
Микроконтроллер
Suite, Eclipse
восстановления
LPC2000 (ARM7)
контекста, мкс
контекста, мкс
переключения
RX600/RX62N
Частота
Время
GCC, HEW (High Performance Embedded
Renesas SuperH Workbench), IAR
H8/S
Silicon Labs (бывший Cygnal) Сверхбыстрые i8051 совместимые МК SDCC
STM32 (Cortex-M3)
ATMega323 8 41,8 ~8
ST STR7 (ARM7) IAR, GCC, Keil, Rowley CrossWorks
STR9 (ARM9) PIC18F452 20 66,2 ~10
Texas Instruments MSP430 Rowley CrossWorks, IAR, GCC
PPC405, выполняющийся на Virtex4 FPGA
Xilinx PPC440, выполняющийся на Virtex5 FPGA GCC
буемый для работы FreeRTOS, достаточно
Microblaze привести расчет расхода ОЗУ для следующей
i8086
Любой x86 совместимый процессор в реальном режиме (Real mode) Open Watcom, Borland, Paradigm конфигурации:
Win32 симулятор Visual Studio 1. Порт для процессоров ARM7, среда раз‑
работки IAR STR71x.
задача выполнила свои действия, она завер‑ стоятельно передать управление планиров‑ 2. Полная оптимизация (Full optimization)
шает свою работу или переходит в состояние щику. Таким образом, высокоприоритетная включена.
ожидания, и управление снова получает за‑ задача будет ожидать, пока низкоприоритет‑ 3. Все компоненты FreeRTOS, кроме сопро‑
дача с низким приоритетом. Переключение ная завершит свою работу и отдаст управле‑ грамм и трассировки, включены.
между задачами осуществляется через рав‑ ние планировщику. Время реакции системы 4. 4 приоритета задач.
ные кванты времени работы планировщи‑ на внешнее событие становится неопреде‑ Объемы расхода ОЗУ для такой конфигу‑
ка, то есть высокоприоритетная задача, как ленным и зависит от того, как долго текущая рации приведены в таблице 3.
только она стала готова к выполнению, ожи‑ задача будет выполняться до передачи управ‑ Расход ОЗУ будет существенно ниже при
дает окончания текущего кванта, после чего ления. Кооперативная многозадачность при‑
управление получает планировщик, который менялась в семействе ОС Windows 3.x. Таблица 3. Объемы ОЗУ,
передает управление высокоприоритетной Вытесняющая и кооперативная концеп‑ требуемые для работы FreeRTOS
задаче. ции многозадачности объединяются вместе
Таким образом, время реакции FreeRTOS в гибридной многозадачности, когда вызов Объект Расход ОЗУ, байт
на внешние события в режиме вытесняющей планировщика происходит каждый квант Планировщик (sheduler) 236
многозадачности — не больше одного кванта времени, но, в отличие от вытесняющей Каждая дополнительная очередь 76 + память для хранения
всех элементов очереди
времени планировщика, который можно за‑ многозадачности, программист имеет воз‑ (queue) (зависит от размера очереди)
давать в настройках. По умолчанию он равен можность сделать это принудительно в теле Каждая дополнительная задача 64 + стек задачи
(task)
1 мс. задачи. Особенно полезен этот режим, ког‑
Если готовы к выполнению несколько за‑ да необходимо сократить время реакции си‑
дач с одинаковым приоритетом, то в таком стемы на прерывание. Допустим, в текущий работе FreeRTOS на 8‑ и 16‑битных архитек‑
случае планировщик выделяет каждой из них момент выполняется низкоприоритетная за‑ турах.
по одному кванту времени, по истечении дача, а высокоприоритетная ожидает насту‑ Кроме самой FreeRTOS, существуют
которого управление получает следующая пления некоторого прерывания. Далее про‑ также ее коммерческие версии: SafeRTOS
задача с таким же приоритетом, и так далее исходит прерывание, но по окончании ра‑ и OpenRTOS. SafeRTOS — это ОСРВ, соот‑
по кругу. боты обработчика прерываний выполнение ветствующая уровню функциональной без‑
Кооперативная многозадачность отлича‑ возвращается к текущей низкоприоритетной опасности SIL3, имеющая такую же функ‑
ется от вытесняющей тем, что планировщик задаче, а высокоприоритетная ожидает, пока циональную модель, что и FreeRTOS, и ори‑
самостоятельно не может прервать выполне‑ закончится текущий квант времени. Однако ентированная на применение в системах
ние текущей задачи, даже если появилась го‑ если после выполнения обработчика преры‑ с высокими требованиями к безопасности,
товая к выполнению задача с более высоким вания передать управление планировщику, например в медицинской и аэрокосмической
приоритетом. Каждая задача должна само‑ то он передаст управление высокоприори‑ отраслях. OpenRTOS отличается от FreeRTOS
лишь тем, что поставляется под коммерче‑ 1. tasks.c — планировщик, реализация меха‑ «с нуля» понадобятся поддиректории /Source/
ской лицензией, с гарантией производителя низма задач. Portable/GCC/MSP430F449 и /Source/Portable/
и отменяет некоторые несущественные огра‑ 2. queue.c — реализация очередей. MemMang. Все остальные поддиректории из
ничения, присущие FreeRTOS. Подробно 3. list.c — внутренние нужды планировщи‑ директории /Source/Portable не нужны и мо‑
с особенностями SafeRTOS и OpenRTOS ка, однако функции могут использоваться гут быть удалены.
можно ознакомиться в [8]. и в прикладных программах. Если же планируется модифицировать
Конечно, FreeRTOS — это далеко не един‑ 4. croutine.c — реализация сопрограмм (мо‑ существующий демонстрационный проект
ственный выбор для разработчика. В на‑ жет отсутствовать в случае, если сопро‑ (что, собственно, и рекомендуется сделать
стоящее время существует множество дру‑ граммы не используются). в начале изучения FreeRTOS), то понадобят‑
гих ОСРВ для МК, среди которых можно Заголовочные файлы, которые находятся ся также поддиректории /Demo/msp430_GCC
назвать uC/OS-II, μClinux, Salvo, jacOS и др. в директории Source/Include: и /Demo/Common. Остальные поддиректо‑
[6]. Однако обсуждение достоинств и недо‑ 1. tasks.h, queue.h, list.h, croutine.h — заголо‑ рии, находящиеся в /Demo, не нужны и могут
статков этих ОС выходит за рамки данной вочные файлы соответственно для одно- быть удалены.
статьи. именных файлов с кодом. При создании приложения рекомендует‑
2. FreeRTOS.h — содержит препроцессорные ся использовать makefile (или файл проек‑
С чего начать? директивы для настройки компиляции. та среды разработки) от соответствующего
3. mpu_wrappers.h — содержит переопреде‑ демонстрационного проекта как отправную
Начать разработку микроконтроллерного ления функций программного интерфейса точку. Целесообразно исключить из сборки
устройства, работающего под управлением (API-функций) FreeRTOS для поддержки (build) файлы из директории /Demo, заменив
FreeRTOS, можно с загрузки ее последней модуля защиты памяти (MPU). их своими, а файлы из директории /Source
версии по адресу [9]. Дистрибутив FreeRTOS 4. portable.h — платформенно-зависимые на‑ оставить нетронутыми. Это гарантия того,
доступен в виде обычного или самораспа‑ стройки. что все исходные файлы ядра FreeRTOS бу‑
ковывающегося ZIP-архива. Дистрибутив 5. projdefs.h — некоторые системные опреде‑ дут включены в сборку и настройки компи‑
с оде р ж и т н е п о с р е д с т в е н н о код я д р а ления. лятора останутся корректными.
(в виде нескольких заголовочных файлов 6. semphr.h — определяет API-функции для Следует упомянуть также о заголовочном
и файлов с исходным кодом) и демонстраци‑ работы с семафорами, которые реализо‑ файле FreeRTOSConfig.h, который находит‑
онные проекты (по одному проекту на каж‑ ваны на основе очередей. ся в каждом демонстрационном проекте.
дую среду разработки для каждого порта). 7. StackMacros.h — содержит макросы для FreeRTOSConf ig.h содержит определения
Далее следует распаковать архив в любое контроля переполнения стека. (#define), позволяющие произвести настрой‑
подходящее место на станции разработки. Каждая аппаратная платформа требу‑ ку ядра FreeRTOS:
Несмотря на достаточно большое количе‑ ет небольшой части кода ядра, которая реа‑ 1. Набор системных функций.
ство файлов в архиве (5062 файла для вер‑ лизует взаимодействие FreeRTOS с этой плат‑ 2. Использование сопрограмм.
сии 6.1.0), структура директорий на самом формой. Весь платформенно-зависимый код 3. Количество приоритетов задач и сопро‑
деле проста. Если планируется проектировать находится в поддиректории /Source/Portable, грамм.
устройства на 2–3 архитектурах в 1–2 средах где он систематизирован по средам разработ‑ 4. Размеры памяти (стека и кучи).
разработки, то бóльшая часть файлов, относя‑ ки (IAR, GCC и т. д.) и аппаратным платфор‑ 5. Тактовая частота МК.
щихся к демонстрационным проектам и раз‑ мам (например, AtmelSAM7S64, MSP430F449). 6. Период работы планировщика — квант
личным средам разработки, не понадобится. К примеру, поддиректория /Source/Portable/ времени, выделяемый каждой задаче для
Подробная структура директорий приве‑ GCC/ATMega323 содержит файлы port.c выполнения, который обычно равен 1 мс.
дена на рисунке. и portmacro.h, реализующие сохранение/вос‑ Отключение некоторых системных функ‑
Весь исходный код ядра находится в ди‑ становление контекста задачи, инициализа‑ ций и уменьшение количества приоритетов
ректории /Source. Его составляют следующие цию таймера для создания временной базы, позволяет уменьшить расход памяти про‑
файлы: инициализацию стека каждой задачи и дру‑ грамм и данных.
гие аппаратно-зависимые функции для ми‑ В дистрибутив FreeRTOS включены так‑
кроконтроллеров семейства mega AVR и ком‑ же средства для конвертирования трас‑
пилятора WinAVR (GCC). сировочной информации, полученной
Отдельно следует выделить поддиректо‑ от планировщика, в текстовую форму (ди‑
рию /Source/Portable/MemMang, в которой со‑ ректория /TraceCon) и текст лицензии (ди‑
держатся файлы heap_1.c, heap_2.c, heap_3.c, ректория /License).
реализующие 3 различных механизма вы‑
деления памяти для нужд FreeRTOS, которые Выводы
будут подробно описаны позже.
В директории /Demo находятся готовые С помощью первой статьи цикла читатель
к компиляции и сборке демонстрационные мог познакомиться с операционной системой
проекты (Demo 1, Demo 2, …, Demo N на ри‑ для микроконтроллеров FreeRTOS. Показаны
сунке). Общая часть кода для всех демонстра‑ ее основные особенности. Описано содер‑
ционных проектов выделена в поддиректо‑ жимое дистрибутива FreeRTOS. Приведены
рию /Demo/Common. основные шаги, с которых следует начинать
Чтобы использовать FreeRTOS в своем про‑ разработку устройства, работающего под
екте, необходимо включить в него файлы ис‑ управлением FreeRTOS.
ходного кода ядра и сопутствующие заголо‑ В следующих публикациях внимание бу‑
вочные файлы. Нет необходимости модифи‑ дет уделено механизму многозадачности,
цировать их или понимать их реализацию. а именно задачам и сопрограммам. Будет
Рисунок. Структура директорий FreeRTOS
Например, если планируется использо‑ приведен образец работы планировщика
после установки на станцию разработки вать порт для микроконтроллеров MSP430 на примере микроконтроллеров AVR фирмы
и GCC-компилятор, то для создания проекта Atmel и компилятора WinAVR (GCC). n
Литература 4. http://ru.wikipedia.org/wiki/Switch-технология
5. Татарчевский В. Применение SWITCH- технологии при разработке
1. Сорокин С. Как много ОСРВ хороших… // Современные технологии авто‑ прикладного программного обеспечения для микроконтроллеров //
матизации. 1997. № 2. Компоненты и технологии. 2006. № 11.
2. Борисов‑Смирнов А. Операционные системы реального времени для ми‑ 6. http://ru.wikipedia.org/wiki/Список_операционных_систем
кроконтроллеров // Chip news. 2008. № 5. 7. http://www.freertos.org
3. Сорокин С. Системы реального времени // Современные технологии авто‑ 8. http://www.freertos.org/index.html? http://www.freertos.org/a00114.html
матизации. 1997. № 2. 9. http://sourceforge.net/projects/freertos/files/FreeRTOS/
Основы работы ОСРВ выполнение трех задач. В реальном же про- одна задача. Говорят, что она находится
цессоре при работе ОСРВ выполнение задач в состоянии выполнения. Остальные зада-
Прежде чем говорить об особенностях носит периодический характер: каждая за- чи в этот момент не выполняются, ожидая,
FreeRTOS, следует остановиться на основных дача выполняется определенное время, после когда планировщик выделит каждой из них
принципах работы любой ОСРВ и пояснить чего процессор «переключается» на следую- процессорное время. Таким образом, задача
значение терминов, которые будут приме- щую задачу (рис. 2). может находиться в двух основных состоя-
няться в дальнейшем. Эта часть статьи будет Планировщик (Scheduler) — это часть ядра ниях: выполняться и не выполняться.
особенно полезна читателям, которые не зна- ОСРВ, которая определяет, какая из задач, го- Кроме того, что выполнение задачи может
комы с принципами, заложенными в ОСРВ. товых к выполнению, выполняется в данный быть приостановлено планировщиком при-
Основой ОСРВ является ядро (Kernel) опе- конкретный момент времени. Планировщик нудительно, задача может сама приостано-
рационной системы. Ядро реализует осново- может приостанавливать, а затем снова воз- вить свое выполнение. Это происходит в двух
полагающие функции любой ОС. В ОС об- обновлять выполнение задачи в течение всего случаях. Первый — это когда задача «хочет»
щего назначения, таких как Windows и Linux, ее жизненного цикла (то есть с момента соз- задержать свое выполнение на определенный
ядро позволяет нескольким пользователям дания задачи до момента ее уничтожения). промежуток времени (в таком случае она пере-
выполнять множество программ на одном Алгоритм работы планировщика ходит в состояние сна (sleep)). Второй — когда
компьютере одновременно. (Scheduling policy) — это алгоритм, по ко- задача ожидает освобождения какого-либо
Каждая выполняющаяся программа пред- торому функционирует планировщик для аппаратного ресурса (например, последова-
ставляет собой задачу (Task). Если ОС позволя- принятия решения, какую задачу выполнять тельного порта) или наступления какого-то
ет одновременно выполнять множество задач, в данный момент времени. Алгоритм работы события (event), в этом случае говорят, что за-
она является мультизадачной (Multitasking). планировщика в ОС общего назначения за- дача блокирована (block). Блокированная или
Большинство процессоров могут выпол- ключается в предоставлении каждой задаче «спящая» задача не нуждается в процессорном
нять только одну задачу в один момент вре- процессорного времени в равной пропорции. времени до наступления соответствующего
мени. Однако при помощи быстрого пере- Алгоритм работы планировщика в ОСРВ от- события или истечения определенного интер-
ключения между задачами достигается эф- личается и будет описан ниже. вала времени. Функции измерения интерва-
фект параллельного выполнения всех задач. Среди всех задач в системе в один мо- лов времени и обслуживания событий берет
На рис. 1 показано истинно параллельное мент времени может выполняться только на себя ядро ОСРВ.
Рис. 1. Истинно параллельное выполнение задач Рис. 2. Распределение процессорного времени между несколькими задачами в ОСРВ
Литература
Рис. 2. Включение DOS в список целевых ОС Рис. 3. Успешная сборка демонстрационного проекта в среде Open Watcom
быть выполнены из интерпретатора команд Windows (cmd.exe). В ка- Кроме этого, необходимо произвести настройку ядра, отредакти-
честве альтернативы можно использовать бесплатный эмулятор ОС ровав заголовочный файл FreeRTOSConfig.h:
DOS под названием DOSBox, который позволит выполнять примеры
не только из-под Windows, но и из-под UNIX-подобных (FreeBSD, #ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
Fedora, Gentoo Linux) и некоторых других ОС [2].
Загрузить последнюю версию пакета Open Watcom можно с офици- #include <i86.h>
#include <conio.h>
ального сайта [2]. На момент написания статьи это версия 1.9. Файл для
скачивания: open-watcom-c-win32-1.9.exe. Во время инсталляции пакета #define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
следует включить в установку 16‑разрядный компилятор для DOS и до- #define configUSE_TICK_HOOK 0
бавить DOS в список целевых ОС (рис. 1 и 2). #define configTICK_RATE_HZ ( ( portTickType ) 1000 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 256 )
После установки пакета Open Watcom нужно выполнить переза- /* This can be made smaller if required. */
грузку рабочей станции. Далее можно проверить работу компиля- #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 32 * 1024 ) )
#define configMAX_TASK_NAME_LEN ( 16 )
тора, открыв демонстрационный проект, входящий в дистрибутив #define configUSE_TRACE_FACILITY 1
FreeRTOS. Проект располагается в C:/FreeRTOSV6.1.0/Demo/PC/ (в слу- #define configUSE_16_BIT_TICKS 1
#define configIDLE_SHOULD_YIELD 1
чае установки FreeRTOS на диск C:/). Далее следует открыть файл про- #define configUSE_CO_ROUTINES 0
екта Open Watcom, который называется rtosdemo.wpj, и выполнить #define configUSE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
сборку проекта, выбрав пункт меню Targets -> Make. Сборка должна #define configUSE_ALTERNATIVE_API 1
пройти без ошибок (рис. 3). #define configUSE_RECURSIVE_MUTEXES 1
#define configCHECK_FOR_STACK_OVERFLOW 0 /* Do not use this option on the PC port. */
При этом в директории демонстрационного проекта появится ис- #define configUSE_APPLICATION_TASK_TAG 1
полнимый файл rtosdemo.exe, запустив который можно наблюдать #define configQUEUE_REGISTRY_SIZE 0
результаты работы демонстрационного проекта в окне интерпретато- #define configMAX_PRIORITIES ( ( unsigned portBASE_TYPE ) 10 )
ра команд Windows (рис. 4). #define configMAX_CO_ROUTINE_PRIORITIES ( 2 )
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_uxTaskPriorityGet 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskCleanUpResources 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_vTaskDelay 1
#define INCLUDE_uxTaskGetStackHighWaterMark 0 /* Do not use this option on the PC port. */
#endif /* FREERTOS_CONFIG_H */
Рис. 4. Работа демонстрационного проекта в среде Windows На этом подготовительный этап можно считать завершенным.
Как говорилось в [1], при создании задачи с помощью API-функции
xTaskCreate() есть возможность передать в функцию, реализующую
В демонстрационный проект включена демонстрация всех воз- задачу, произвольный параметр.
можностей FreeRTOS. Для наших целей, чтобы продолжить изучение Разработаем учебную программу № 1, которая будет создавать два
задач, не вникая в остальные возможности FreeRTOS, необходимо экземпляра одной задачи. Чтобы каждый экземпляр задачи выполнял
исключить из проекта все исходные и заголовочные файлы, кроме уникальное действие, передадим в качестве параметра строку симво-
файлов ядра FreeRTOS и файла main.c (рис. 5). лов и значение периода, которое будет сигнализировать о том, что
задача выполнена. Для этого следует отредактировать файл main.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include “FreeRTOS.h”
#include “task.h”
/*-----------------------------------------------------------*/
/* Функция, реализующая задачу */
void vTask( void *pvParameters )
{
volatile long ul;
volatile TaskParam *pxTaskParam;
/* Преобразование типа void* к типу TaskParam* */
pxTaskParam = (TaskParam *) pvParameters;
for( ;; )
Рис. 5. Минимально необходимый набор исходных и заголовочных файлов {
в среде Open Watcom /* Вывести на экран строку, переданную в качестве параметра при создании задачи */
puts( (const char*)pxTaskParam->string );
/*-----------------------------------------------------------*/
/* Точка входа. С функции main() начнется выполнение программы. */
short main( void )
{ Рис. 7. Разделение процессорного времени между задачами в учебной программе № 1
/* Заполнение полей структуры, передаваемой Задаче 1 */
strcpy(xTP1.string, “Task 1 is running”);
xTP1.period = 10000000L;
Планировщик гарантирует, что среди всех задач, находящихся в со-
/* Заполнение полей структуры, передаваемой Задаче 2 */
strcpy(xTP2.string, “Task 2 is running”);
стоянии готовности к выполнению, перейдет в состояние выполнения
xTP2.period = 30000000L; та задача, которая имеет наивысший приоритет. Если в программе соз-
/* Создание Задачи 1. Передача ей в качестве параметра указателя на структуру xTP1 */
даны несколько задач с одинаковым приоритетом, то они будут вы-
xTaskCreate( vTask, /* Функция, реализующая задачу */ полняться в режиме разделения времени [1]. То есть задача выполняется
( signed char * ) “Task1”,
configMINIMAL_STACK_SIZE,
в течение системного кванта времени, после чего планировщик перево-
(void*)&xTP1, /* Передача параметра */ дит ее в состояние готовности и запускает следующую задачу с таким же
1,
NULL );
приоритетом, и далее по кругу. Таким образом, задача выполняется
за один квант времени и находится в состоянии готовности к выполне-
/* Создание Задачи 2. Передача ей указателя на структуру xTP2 */
xTaskCreate( vTask, ( signed char * ) “Task2”, configMINIMAL_STACK_SIZE, (void*)&xTP2, 1, NULL );
нию (но не выполняется) в течение стольких квантов времени, сколько
имеется готовых к выполнению задач с таким же приоритетом.
/* Запуск планировщика */
vTaskStartScheduler();
На рис. 7 показано, как задачи разделяют процессорное вре-
мя в учебной программе № 1. Кроме хода выполнения двух задач,
return 1;
}
на рис. 7 показано выполнение кода планировщика каждый систем-
ный квант времени. Выполнение кода планировщика приводит к пе-
реключению на следующую задачу с одинаковым приоритетом.
Выполнив сборку проекта и запустив на выполнение полученный Модифицируем учебную программу № 1 так, чтобы задачам на-
исполнимый файл rtosdemo.exe, можно наблюдать результат работы значался разный приоритет. Пусть Задача 2 получит приоритет,
учебной программы № 1 (рис. 6). равный 2, а приоритет Задачи 1 останется прежним — равным 1.
Для этого следует отредактировать вызов API-функции xTaskCreate()
для создания Задачи 2:
…
xTaskCreate( vTask, ( signed char * ) “Task2”, configMINIMAL_STACK_SIZE, (void*)&xTP2, 2, NULL );
…
Приоритеты задач
В предыдущей статье [1] читатель познакомился с механизмом Рис. 8. Результат работы учебной программы
приоритетов задач. Далее будет показано, как значение приоритета в случае назначения Задаче 2 более высокого приоритета
влияет на выполнение задачи.
При создании задачи ей назначается приоритет. Приоритет за-
дается с помощью параметра uxPriority функции xTaskCreate(). Задача 2, как и Задача 1, все время находится в состоянии готовно-
Максимальное количество возможных приоритетов определяется сти к выполнению. За счет того, что Задача 2 имеет приоритет выше,
макроопределением configMAX_PRIORITIES в заголовочном файле чем Задача 1, каждый квант времени планировщик будет отдавать
FreeRTOSConfig.h. В целях экономии ОЗУ необходимо задавать наи- управление именно ей, а Задача 1 никогда не получит процессорного
меньшее, но достаточное значение configMAX_PRIORITIES. Нулевое времени (рис. 9).
значение приоритета соответствует наиболее низкому приоритету, Этот пример показывает необходимость пользоваться приорите-
значение (configMAX_PRIORITIES-1) — наиболее высокому (в ОС тами осмотрительно, так как никакого алгоритма старения в плани-
семейства Windows наоборот — приоритет 0 наивысший). ровщике не предусмотрено (как в ОС общего назначения). Поэтому
возможна ситуация «зависания» задачи с низким приоритетом, кото- Блокированное состояние задачи
рая никогда не выполнится. Программисту необходимо тщательно
проектировать прикладные программы и благоразумно задавать Если задача ожидает наступления события, то она находится в бло-
уровни приоритетов, чтобы избежать такой ситуации. Далее будет кированном состоянии (рис. 5 в КиТ № 3`2011, стр. 111). Во FreeRTOS
показано, как избежать «зависания» низкоприоритетных задач, ис- существуют два вида событий:
пользуя механизм событий для управления ходом их выполнения. 1. Временное событие — это событие, связанное с истечением вре-
Следует отметить, что FreeRTOS позволяет динамически менять менного промежутка или наступлением определенного момента
приоритет задачи во время выполнения программы. Для получе- абсолютного времени. Например, задача может войти в блокиро-
ния и задания приоритета задачи во время выполнения служат API- ванное состояние, пока не пройдет 10 мс.
функции uxTaskPriorityGet() и vTaskPrioritySet() соответственно. 2. Событие синхронизации (внешнее по отношению к задаче) — это
событие, которое генерируется в другой задаче или в теле обра-
Подсистема времени FreeRTOS ботчика прерывания МК. Например, задача блокирована, когда
ожидает появления данных в очереди. Данные в очередь поступают
Подробнее остановимся на системном кванте времени. Планировщик от другой задачи.
получает управление каждый квант времени, это происходит по преры- События синхронизации могут быть связаны с множеством объек-
ванию от таймера. Продолжительность системного кванта определяется тов ядра, такими как очереди, двоичные и счетные семафоры, рекур-
периодом возникновения прерываний от таймера и задается в файле сивные семафоры и мьютексы, которые будут описаны в дальнейших
FreeRTOSConfig.h макроопределением configTICK_RATE_HZ. публикациях.
configTICK_RATE_HZ определяет частоту отсчета системных Во FreeRTOS есть возможность заблокировать задачу, заставив ее
квантов в герцах, например значение configTICK_RATE_HZ, равное ожидать события синхронизации, но определить при этом тайм-аут
100 (Гц), определяет продолжительность системного кванта, равную ожидания. То есть выход задачи из блокированного состояния возмо-
10 мс. Следует отметить, что в большинстве демонстрационных про- жен как при наступлении события синхронизации, так и по проше-
ектов продолжительность системного кванта устанавливается равной ствии времени тайм-аута, если событие синхронизации так и не про-
1 мс (configTICK_RATE_HZ = 1000). изошло. Например, задача ожидает появления данных из очереди.
Все API-функции, связанные с измерением временных интерва- Тайм-аут при этом установлен равным 10 мс. В этом случае выход
лов, в качестве единицы измерения времени используют системный задачи из блокированного состояния возможен при выполнении
квант. Используя макроопределение portTICK_RATE_MS, можно двух условий:
получить продолжительность системного кванта в миллисекундах. • Данные в очередь поступили.
Но для задания длительности кванта нужно использовать макро- • Данные не поступили, но вышло время тайм-аута, равное 10 мс.
определение configTICK_RATE_HZ.
Следует также упомянуть о счетчике квантов — это системная Реализация задержек
переменная типа portTickType, которая увеличивается на едини- с помощью API-функции vTaskDelay()
цу по прошествии одного кванта времени и используется ядром
FreeRTOS для измерения временных интервалов. Значение счетчи- Вернемся к рассмотрению учебной программы № 1. Задачи в этой
ка квантов начинает увеличиваться после запуска планировщика, программе выполняли полезное действие (в нашем случае — вывод
то есть после выполнения функции vTaskStartScheduler(). Текущее текстовой строки на экран), после чего ожидали определенный про-
значение счетчика квантов может быть получено с помощью API- межуток времени, то есть выполняли задержку на какое-то время.
функции xTaskGetTickCount(). Реализация задержки в виде пустого цикла не эффективна. Один
из основных недостатков мы продемонстрировали, когда задачам
События как способ управления выполнением задач был назначен разный приоритет. А именно, когда высокоприоритет-
ная задача все время остается в состоянии готовности к выполнению
В учебных программах, приведенных выше, задачи были реали- (не переходит ни в блокированное, ни в приостановленное состоя-
зованы так, что они постоянно нуждались в процессорном времени. ние), она поглощает все процессорное время, и низкоприоритетные
Даже когда задача ничего не выводила на экран, она занималась от- задачи никогда не выполняются.
счетом времени с помощью пустого цикла for. Для корректной реализации задержек средствами FreeRTOS следует
Такая реализация задачи целесообразна только при назначении за- применять API-функцию vTaskDelay(), которая переводит задачу,
даче самого низкого приоритета. В противном случае наличие такой вызывающую эту функцию, в блокированное состояние на заданное
постоянно готовой к выполнению задачи с довольно высоким прио- количество квантов времени. Ее прототип:
ритетом приведет к тому, что другие задачи, имеющие более низкий
приоритет, никогда не будут выполняться. void vTaskDelay( portTickType xTicksToDelay );
Гораздо эффективнее управлять выполнением задач с помощью
событий. Управляемая событием задача выполняется только после
того, как некоторое событие произошло. Если событие не произошло Единственным аргументом является xTicksToDelay, который непо-
и задача ожидает его наступления, то она НЕ находится в состоя- средственно задает количество квантов времени задержки.
/* Запуск планировщика */
vTaskStartScheduler();
return 1;
} Рис. 11. Разделение процессорного времени между задачами в учебной программе № 2
Рис. 12. Ход выполнения циклической задачи. Рис. 13. Ход выполнения циклической задачи.
Задержка реализована API-функцией vTaskDelay() Задержка реализована API-функцией vTaskDelayUntil()
Рис. 1. Разделение процессорного времени между задачами в учебной программе № 1 Рис. 2. Результат выполнения учебной программы № 1
получают процессорное время за счет перио- вала удаленная задача. К программам, в кото- /* Динамически (после старта планировщика) создать
дического изменения приоритета Задачи 2 рых происходит создание и удаление задач, Задачу 2 с приоритетом 2.
Она сразу же получит управление */
(он становится то ниже, то выше приоритета предъявляется следующее требование. Если xTaskCreate( vTask2, “Task 2”, 1000, NULL, 2, NULL );
Задачи 1). разработчик использует функцию-ловушку
/* Пока выполняется Задача 2 с более высоким
В момент запуска планировщика обе за- задачи Бездействие [1, № 4], то время выпол- приоритетом, Задача 1 не получает процессорного
дачи готовы к выполнению (в учебной нения этой функции должно быть меньше времени. Когда Задача 2 уничтожила сама себя,
управление снова получает Задача 1 и переходит
программе № 1 они вообще не переходят времени выполнения задачи Бездействие в блокированное состояние на 100 мс. Так что в системе
в блокированное состояние, то есть либо вы- (то есть времени, пока нет ни одной задачи, не остается задач, готовых к выполнению, и выполняется
задача Бездействие */
полняются, либо находятся в состоянии го- готовой к выполнению). vTaskDelay( 100 );
товности к выполнению). Управление полу- Следует отметить, что при уничтожении }
vTaskDelete( NULL );
чает Задача 1, так как ее приоритет (равен 2) задачи ядро освобождает лишь системную, }
больше, чем приоритет Задачи 2. После не доступную прикладному программисту
/*-----------------------------------------------------------*/
сигнализации о своей работе она вызывает память, связанную с задачей. Вся память /* Функция Задачи 2 */
API-функцию vTaskPrioritySet(), вследствие и другие ресурсы, которые программист ис- void vTask2( void *pvParameters )
{
чего Задача 2 получает приоритет выше, чем пользовал в задаче, также явно должны быть /* Задача 2 не делает ничего, кроме сигнализации о своем
Задача 1 (он становится равным 3). освобождены. выполнении, и сама себя уничтожает. Тело функции
не содержит бесконечного цикла, так как в нем нет
Вызов vTaskPrioritySet() помимо изменения Прототип API-функции vTaskDelete(): необходимости. Тело функции Задачи 2 выполнится 1 раз,
приоритета приводит к тому, что управление после чего задача будет уничтожена. */
puts( “Task2 is running and about to delete itself” );
получает планировщик, который запускает void vTaskDelete( xTaskHandle pxTaskToDelete ); vTaskDelete( NULL );
Задачу 2, так как приоритет у нее теперь выше. }
/*-----------------------------------------------------------*/
Получив управление, Задача 2 сигнализиру-
ет о своем выполнении. После чего она пони- Единственный параметр pxTaskToDelete —
жает свой приоритет на две единицы, так, что- это дескриптор задачи, которую необходимо Перед запуском планировщика создает-
бы он стал меньше приоритета Задачи 1 (стал уничтожить. Если необходимо уничтожить ся Задача 1 с приоритетом 1. В теле Задачи 1
равен 1). После этого управление снова по- задачу, которая вызывает API-функцию динамически создается Задача 2 с более вы-
лучает планировщик и так далее. Разделение vTaskDelete(), то в качестве параметра соким приоритетом. Задача 2 сразу же после
процессорного времени в учебной программе pxTaskToDelete следует задать NULL. создания получает управление, сигнализиру-
№ 1 показано на рис. 1, а результат ее выпол- Учебная программа № 2 демонстрирует ди- ет о своем выполнении и сама себя уничто-
нения — на рис. 2. намическое создание и уничтожение задач: жает. После чего снова управление получает
Следует отметить, что задачи сменяют друг Задача 1.
друга с частотой, превышающей частоту си- #include <stdlib.h> Следует обратить внимание на тело функ-
#include <stdio.h>
стемных квантов. Частота их следования зави- #include <string.h> ции Задачи 2. В нем отсутствует бесконечный
сит от быстродействия рабочей станции, и со- #include “FreeRTOS.h” цикл, что вполне допустимо, так как функ-
#include “task.h”
общения выводятся на экран с очень большой ция завершается вызовом API-функции уни-
скоростью, поэтому для того, чтобы увидеть /* Прототипы функций, которые реализуют задачи. */ чтожения этой задачи. Задача 2 в отличие
void vTask1( void *pvParameters );
изображение на дисплее, соответствующее void vTask2( void *pvParameters ); от Задачи 1 является спорадической [5].
рис. 2, необходимо искусственно приостано- Разделение процессорного времени между
/*-----------------------------------------------------------*/
вить выполнение учебной программы. задачами в учебной программе № 2 показа-
int main( void ) но на рис. 3, а результат ее выполнения —
{
Уничтожение задач /* Статическое создание Задачи 1 с приоритетом 1 */ на рис. 4.
xTaskCreate( vTask1, “Task 1”, 1000, NULL, 1, NULL ); Задача 2 существует в системе на протя-
/* Запустить планировщик. Задача 1 начнет выполняться */
Задача может уничтожить саму себя или vTaskStartScheduler(); жении короткого промежутка времени, пока
любую другую задачу в программе с помо- она выполняется. Таким образом, используя
Рис. 3. Разделение процессорного времени между задачами в учебной программе № 2 Рис. 4. Результат выполнения учебной программы № 2
динамическое создание/уничтожение задач получить доступ к структуре tskTCB, необхо- API-функции pvPortMalloc() и vPortFree()
в реальной программе, удастся достичь эко- димо включить в исходный файл строку: имеют такие же прототипы, как и стандарт-
номии памяти, так как память не задейству- ные функции malloc() и free() [7]. Реализация
ется под потребности задачи, пока полезные #include “..\\tasks.c” API-функций pvPortMalloc() и vPortFree()
действия этой задачи не нужны. и представляет собой ту или иную схему вы-
деления памяти.
Выделение памяти Для учебных программ, приводимых в этой Следует отметить, что API-функции
при создании задачи статье и ранее [1], размер структуры tskTCB pvPortMalloc() и vPortFree() можно беспре-
составляет 70 байт. пятственно использовать и в прикладных
Каждый раз при создании задачи (равно целях, выделяя память для хранения своих
как и при создании других объектов ядра — Схемы переменных.
очередей и семафоров) ядро FreeRTOS вы- выделения памяти FreeRTOS поставляется с тремя стандарт-
деляет задаче блок памяти из системной ными схемами выделения памяти, которые
кучи — области памяти, доступной для ди- Функции динамического выделения/осво- содержатся соответственно в исходных фай-
намического размещения в ней переменных. бождения памяти malloc() и free(), входящие лах heap_1.c, heap_2.c, heap_3.c. В дальней-
Блок памяти, который выделяется задаче, в стандартную библиотеку языка Си, в боль- шем будем именовать стандартные схемы
складывается из: шинстве случаев не могут напрямую исполь- выделения памяти согласно именам файлов
1. Стека задачи. Задается как параметр API- зоваться ядром FreeRTOS, так как их исполь- с исходным кодом, в которых они определе-
функции xTaskCreate() при создании за- зование сопряжено с рядом проблем: ны. Разработчику предоставляется возмож-
дачи. • Они не всегда доступны в упрощенных ность использовать любой алгоритм выделе-
2. Блока управления задачей (Task Control компиляторах для микроконтроллеров. ния памяти из поставки FreeRTOS или реали-
Block), который представлен структурой • Их реализация достаточно громоздка, что зовать свой собственный.
tskTCB и содержит служебную информа- приводит к дополнительному расходу па- Выбор одной из стандартных схем выде-
цию, используемую ядром. Размер струк- мяти программ. ления памяти осуществляется в настройках
туры tskTCB зависит от: • Они редко являются реентерабельными компилятора (или проекта, если использует-
– настроек FreeRTOS; [6], то есть одновременный вызов этих ся среда разработки) добавлением к списку
– платформы, на которой она функций из нескольких задач может при- файлов с исходным кодом одного из файлов:
выполняется; вести к непредсказуемым результатам. heap_1.c, heap_2.c или heap_3.c.
– используемого компилятора. • Время их выполнения не является детер-
Размер блока памяти, который выделяет- минированным, то есть от вызова к вызову Схема выделения памяти
ся задаче, на этапе выполнения программы оно будет меняться, например, в зависимо- heap_1.c
полностью определяется размером отводи- сти от степени фрагментации кучи. Часто программа для микроконтроллера
мого задаче стека, так как размер структуры • Они могут усложнить конфигурацию ком- допускает только создание задач, очередей
tskTCB жестко задан на этапе компиляции поновщика. и семафоров и делает это перед запуском
программы и остается неизменным во время Разные приложения предъявляют раз- планировщика. В этом случае память дина-
ее выполнения. личные требования к объему выделяемой мически выделяется перед началом выполне-
Получить точный размер структуры памяти и временным задержкам при ее вы- ния задач и никогда не освобождается. Такой
tskTCB для конкретных условий можно, на- делении. Поэтому единую схему выделения подход позволяет исключить такие потен-
пример, добавив в текст программы следую- памяти невозможно применить ко всем плат- циальные проблемы при динамическом вы-
щую инструкцию: формам, на которые портирована FreeRTOS. делении памяти, как отсутствие детерминиз-
Вот почему реализация алгоритма выделе- ма и фрагментация, что важно для обеспе-
printf(“%d”, sizeof(tskTCB)); ния памяти не входит в состав ядра, а выде- чения заданного времени реакции системы
лена в платформенно-зависимый код (в ди- на внешнее событие.
ректорию \Source\portable\MemMang). Это Схема heap_1.c предоставляет очень
И далее следует прочесть ее размер с какого- позволяет реализовать свой собственный простую реализацию API-функции
либо устройства вывода (в данном случае — алгоритм выделения памяти для конкретной pvPortMalloc() и не содержит реализации API-
с дисплея). При этом нужно учесть, что, так платформы. функции vPortFree(). Поэтому такую схему
как структура tskTCB используется ядром Когда ядро FreeRTOS запрашивает память следует использовать, если задачи в програм-
в собственных целях, то доступа к этой струк- для своих нужд, происходит вызов API- ме никогда не уничтожаются. Время выпол-
туре из текста прикладных исходных файлов функции pvPortMalloc(), когда память осво- нения API-функции pvPortMalloc() в этом
(main.c в том числе) изначально нет. Чтобы бождается — происходит вызов vPortFree(). случае является детерминированным.
а б в а б в
Рис. 5. Распределение памяти кучи при использовании схемы heap_1.c Рис. 6. Распределение памяти кучи при использовании схемы heap_2.c
Вызов pvPortMalloc() приводит к вы- находиться несколько отдельных участков стека таким же, как был у Задачи 2. В соответ-
делению блока памяти для размещения свободной памяти (фрагментация). Для на- ствии с алгоритмом наилучших подходящих
структуры tskTCB и стека задачи из кучи хождения подходящего участка свободной фрагментов Задаче 4 выделен блок, который
FreeRTOS. Выделяемые блоки памяти рас- памяти, в который с помощью API-функции раньше занимала Задача 2, фрагментации
полагаются последовательно друг за другом pvPortMalloc() будет помещен, например, кучи не произошло.
(рис. 5). Куча FreeRTOS представляет со- блок памяти задачи, используется алгоритм Время выполнения функций pvPortMalloc()
бой массив байт, определенный как обыч- наилучших подходящих фрагментов (the и vPortFree() для схемы heap_2.c не является
ная глобальная переменная. Размер этого best fit algorithm). детерминированной величиной, однако их
массива в байтах задается макроопределе- Работа алгоритма наилучших подходящих реализация значительно эффективнее стан-
нием conf igTOTAL_HEAP_SIZE в файле фрагментов заключается в следующем. Когда дартных функций malloc() и free().
FreeRTOSConfig.h. pvPortMalloc() запрашивает блок памяти Более подробно с существующими алго-
Разработчик должен учесть, что объем до- заданного размера, происходит поиск сво- ритмами выделения памяти можно познако-
ступной памяти для размещения перемен- бодного участка, размер которого как можно миться в [8].
ных, связанных с решением прикладных за- ближе к размеру запрашиваемого блока и,
дач, уменьшается на размер кучи FreeRTOS. естественно, больше его. Например, струк- Схема выделения памяти
Поэтому размер кучи FreeRTOS следует за- тура кучи представляет собой 3 свободных heap_3.c
давать минимальным, но достаточным для участка памяти размером 5, 25 и 100 байт. Схема heap_3.c использует вызовы функ-
размещения всех объектов ядра. Далее будет Функция pvPortMalloc() запрашивает блок ций выделения/освобождения памяти
показано, как получить объем оставшейся памяти 20 байт. Тогда наименьший подходя- malloc() и free() из стандартной библиоте-
свободной памяти в куче FreeRTOS на этапе щий по размеру участок памяти — участок ки языка Си. Однако с помощью останова
выполнения программы. размером 25 байт. 20 байт из этого участка планировщика на время выполнения этих
На рис. 5а изображена куча FreeRTOS в мо- будут выделены, а оставшиеся 5 байт оста- функций достигается псевдореентерабель-
мент, когда ни одна задача еще не создана. нутся свободными. ность (thread safe) этих функций, то есть пре-
На рис. 5б и в отображено размещение бло- Реализация алгоритма наилучших подходя- дотвращается одновременный вызов этих
ков памяти задач при их последовательном щих фрагментов в FreeRTOS не предусматри- функций из разных задач.
создании и, соответственно, уменьшение вает слияния двух примыкающих друг к дру- Макроопределение configTOTAL_HEAP_
объема свободной памяти кучи. гу свободных участков в один большой сво- SIZE не влияет на размер кучи, который те-
Очевидно, что за счет того, что задачи бодный участок. Поэтому при использовании перь задается настройками компоновщика.
не уничтожаются, эффект фрагментации па- схемы heap_2.c возможна фрагментация кучи.
мяти кучи исключен. Однако фрагментации можно не опасаться, Получение объема
если размер выделяемых и освобождаемых свободной памяти кучи
Схема выделения памяти впоследствии блоков памяти не изменяется
heap_2.c в течение выполнения программы. Начиная с версии V6.0.0 в FreeRTOS добав-
Как и в схеме heap_1.c, память для за- Схема выделения памяти heap_2.c подхо- лена API-функция xPortGetFreeHeapSize(),
дач выделяется из кучи FreeRTOS размером дит для приложений, где создаются и уни- с помощью которой можно получить объем
configTOTAL_HEAP_SIZE байт. Однако схе- чтожаются задачи, причем размер стека доступной для выделения свободной памяти
ма heap_2.c в отличие от heap_1.c позволяет при создании задач целесообразно остав- кучи. Ее прототип:
уничтожать задачи после запуска планиров- лять неизменным.
щика, соответственно, она содержит реализа- На рис. 6а изображена куча FreeRTOS, бло- size_t xPortGetFreeHeapSize( void );
цию API-функции vPortFree(). ки памяти под три задачи располагаются по-
Так как задачи могут уничтожаться, то бло- следовательно. На рис. 6б Задача 2 уничтоже-
ки памяти, которые они использовали, будут на, куча содержит два свободных участка па- Однако следует учесть, что API-функция
освобождаться, следовательно, в куче может мяти. На рис. 6в создана Задача 4 с размером xPortGetFreeHeapSize() доступна только при
использовании схем heap_1.c и heap_2.c. При мени реакции системы на внешние события тивной многозадачности каждой задаче необ-
использовании схемы heap_3.c получение разработчик должен соответствующим обра- ходим собственный стек для хранения своего
объема доступной памяти становится нетри- зом назначить приоритеты задачам и опреде- контекста.
виальной задачей. лить частоту их выполнения. Преимущества кооперативной многоза-
Так как FreeRTOS относится к ОСРВ с пла- дачности:
Резюме нированием с фиксированными приорите- 1. Меньшее потребление памяти стека при пе-
по вытесняющей тами, то рекомендованной стратегией назна- реключении контекста задачи, соответствен-
многозадачности в FreeRTOS чения приоритетов является использование но, более быстрое переключение контекста.
принципа «чем меньше период выполнения С точки зрения компилятора вызов плани-
Подведя итог по вытесняющей многоза- задачи, тем выше у нее приоритет» (Rate ровщика «выглядит» как вызов функции,
дачности в FreeRTOS, можно выделить сле- Monotonic Scheduling, RMS) [4, 5]. поэтому в стеке автоматически сохраняются
дующие основные принципы: Основная идея принципа RMS состоит регистры процессора и нет необходимости
1. Каждой задаче назначается приоритет. в следующем. Все задачи разделяются по тре- их повторного сохранения в рамках сохра-
2. Каждая задача может находиться в одном буемому времени реакции на соответствую- нения контекста задачи.
из нескольких состояний (выполнение, щее задаче внешнее событие. Каждой задаче 2. Существенно упрощается проблема со-
готовность к выполнению, блокирован- назначается уникальный приоритет (то есть вместного доступа нескольких задач
ное состояние, приостановленное состоя- в программе не должно быть двух задач с оди- к одному аппаратному ресурсу. Например,
ние). наковым приоритетом), причем приоритет не нужно опасаться, что несколько задач
3. В один момент времени только одна задача тем выше, чем короче время реакции задачи одновременно будут модифицировать
может находиться в состоянии выполне- на событие. Частота выполнения задачи уста- одну переменную, так как операция моди-
ния. навливается тем больше, чем больше ее прио- фикации не может быть прервана плани-
4. Планировщик переводит в состояние вы- ритет. Таким образом, самой «ответственной» ровщиком.
полнения готовую к выполнению задачу задаче назначаются наивысший приоритет Недостатки:
с наивысшим приоритетом. и наибольшая частота выполнения. 1. Программист должен в явном виде вы-
5. Задачи могут ожидать наступления со- Принцип RMS гарантирует, что система зывать API-функцию taskYIELD() в теле
бытия, находясь в блокированном состоя- будет иметь детерминированное время ре- задачи, что увеличивает сложность про-
нии. акции на внешнее событие [5]. Однако тот граммы.
6. События могут быть двух основных ти- факт, что задачи могут изменять свой прио- 2. Одна задача, которая по каким-либо причи-
пов: ритет и приоритет других задач во время вы- нам не вызвала API-функцию taskYIELD(),
– временные события; полнения, и то, что не все задачи реализуют- приводит к «зависанию» всей программы.
– события синхронизации. ся как циклически выполняющиеся, делают 3. Трудно гарантировать заданное время ре-
7. Временные события чаще всего связаны это утверждение в общем случае неверным, акции системы на внешнее событие, так
с организацией периодического выполне- и разработчик вынужден прибегать к допол- как оно зависит от максимального вре-
ния каких-либо полезных действий или нительным мерам обеспечения заданного менного промежутка между вызовами
с отсчетом времени тайм-аута. времени реакции. taskYIELD().
8. События синхронизации чаще всего свя- 4. Вызов taskYIELD() внутри циклов может
заны с обработкой асинхронных событий Кооперативная многозадачность замедлить выполнение программы.
внешнего мира, например, с получением во FreeRTOS Для выбора режима кооперативной много-
информации от периферийных (по отно- задачности необходимо задать значение ма-
шению к процессору) устройств. До этого момента при изучении FreeRTOS кроопределения configUSE_PREEMPTION
Такая схема называется вытесняющим мы использовали режим работы ядра с вы- в файле FreeRTOSConfig.h равным 0:
планированием с фиксированными приори- тесняющей многозадачностью. Тем не менее,
тетами (Fixed Priority Preemptive Scheduling). кроме вытесняющей, FreeRTOS поддержи- #define configUSE_PREEMPTION 0
Говорят, что приоритеты фиксированы, вает кооперативную и гибридную (смешан-
потому что планировщик самостоятельно ную) многозадачность.
не может изменить приоритет задачи, как это Самое весомое отличие кооперативной Значение configUSE_PREEMPTION, рав-
происходит при динамических алгоритмах многозадачности от вытесняющей — то, что ное 1, дает предписание ядру FreeRTOS ра-
планирования [5]. Приоритет задаче назна- планировщик не получает управление каж- ботать в режиме вытесняющей многозадач-
чается в явном виде при ее создании, и так же дый системный квант времени. Вместо этого ности.
в явном виде он может быть изменен этой же тело функции, реализующей задачу, должно Если включить режим кооперативной
или другой задачей. Таким образом, про- содержать явный вызов API-функции плани- многозадачности в учебной программе № 1
граммист целиком и полностью контролиру- ровщика taskYIELD(). [1, № 4] так, как показано выше, выполнить
ет приоритеты задач в системе. Результатом вызова taskYIELD() может сборку проекта и запустить на выполнение
быть как переключение на другую задачу, так полученный исполнимый файл rtosdemo.exe,
Стратегия назначения и отсутствие переключения, если других за- то можно наблюдать ситуацию, когда все вре-
приоритетов задачам дач, готовых к выполнению, нет. Вызов API- мя выполняется один экземпляр задачи, а вто-
функции, которая переводит задачу в блоки- рой никогда не получает управления (рис. 8,
Как было сказано ранее, программа, вы- рованное состояние, также приводит к вызо- см. КиТ № 4`2011, стр. 98).
полняющаяся под управлением FreeRTOS, ву планировщика. Это происходит из-за того, что плани-
представляет собой совокупность взаимо- Следует отметить, что отсчет квантов ровщик никогда не получает управления
действующих задач. Чаще всего задача реа- времени ядро FreeRTOS выполняет при ис- и не может запустить на выполнение другую
лизуется как какое-либо полезное действие, пользовании любого типа многозадачности, задачу. Теперь обязанность запуска плани-
которое циклически повторяется с заданной поэтому API-функции, связанные с отсчетом ровщика ложится на программиста.
частотой/периодом. Каждой задаче назнача- времени, корректно работают и в режиме Если добавить в функцию, реализующую
ются приоритет и частота ее циклического кооперативной многозадачности. Как и для задачу, явный вызов планировщика API-
выполнения. Для достижения заданного вре- вытесняющей, в случае применения коопера- функцией taskYIELD():
/*-----------------------------------------------------------*/
/* Функция, реализующая задачу */
void vTask( void *pvParameters )
{
volatile long ul;
volatile TaskParam *pxTaskParam;
/* Преобразование типа void* к типу TaskParam* */
pxTaskParam = (TaskParam *) pvParameters;
for( ;; )
{
/* Вывести на экран строку, переданную в качестве
параметра при создании задачи */
puts( (const char*)pxTaskParam->string );
Рис. 7. Разделение процессорного времени между задачами при кооперативной многозадачности
/* Задержка на некоторый период Т2*/ при явном вызове taskYIELD()
for( ul = 0; ul < pxTaskParam->period; ul++ )
{
}
/* Принудительный вызов планировщика. обработчика прерывания производят вызов впустую на выполнение кода планировщика
Другой экземпляр задачи получит управление
и будет выполняться, пока не вызовет taskYIELD()
планировщика, что приводит к переключе- каждый квант времени.
или блокирующую API-функцию */ нию на задачу, ожидающую наступления Для использования этого типа много-
taskYIELD();
этого прерывания. задачности макроопределение configUSE_
} API-функция portYIELD_FROM_ISR() слу- PREEMPTION в файле FreeRTOSConf ig.h
vTaskDelete( NULL );
}
жит для вызова планировщика из тела обра- должно быть равным 0 и каждый обработчик
ботчика прерывания. Более подробно о ней прерывания должен содержать явный вызов
будет рассказано позже, при изучении двоич- планировщика portYIELD_FROM_ISR().
то процессорное время теперь будут полу- ных семафоров.
чать оба экземпляра задачи. Результат вы- Какого-либо специального действия для Выводы
полнения программы не будет отличаться включения режима гибридной многозадач-
от приведенного на рис. 6 (см. КиТ № 4’2011, ности не существует. Достаточно разрешить На этом изучение задачи как базовой еди-
стр. 98). Но разделение процессорного вре- вызов планировщика каждый квант времени ницы программы, работающей под управле-
мени между задачами будет происходить (макроопределение configUSE_PREEMPTION нием FreeRTOS, можно считать завершенным.
иначе (рис. 7). в файле FreeRTOSConf ig.h должно быть Каждая задача представляет собой отдельную
На рис. 7 видно, что теперь Задача 1, как равным 1) и в явном виде вызывать плани- подпрограмму, которая работает независимо
только начала выполняться, захватывает ровщик в функциях, реализующих задачи, от остальных. Однако задачи не могут функ-
процессор на длительное время, до тех пор и в обработчиках прерываний с помощью ционировать изолированно. Они должны об-
пока в явном виде не вызовет планировщик API-функций taskYIELD() и portYIELD_ мениваться информацией и координировать
API-функцией taskYIELD() (момент вре- FROM_ISR() соответственно. свою совместную работу. Во FreeRTOS основ-
мени N). После вызова планировщика он ным средством обмена информацией между
передает управление Задаче 2, которая тоже Вытесняющая многозадачность задачами и средством синхронизации задач
удерживает процессор в своем распоряжении без разделения времени является механизм очередей.
до вызова taskYIELD() (момент времени M). Ее идея заключается в том, что вызов Поэтому следующие публикации будут
Планировщик теперь не вызывается каждый планировщика происходит только в обра- посвящены очередям. Подробно будет рас-
квант времени, а «ждет», когда его вызовет ботчиках прерываний. Задача выполняется сказано:
одна из задач. до тех пор, пока не произойдет какое-либо • как создать очередь;
прерывание. После чего она вытесняется за- • каким образом информация хранится
Гибридная многозадачность во FreeRTOS дачей, ответственной за обработку внешнего и обрабатывается очередью;
Гибридная многозадачность сочетает события, связанного с этим прерыванием. • как передать данные в очередь;
в себе автоматический вызов планиров- Таким образом, задачи не сменяют друг дру- • как получить данные из очереди;
щика каждый квант времени, а также воз- га по прошествии кванта времени, это про- • как задачи блокируются, ожидая возмож-
можность принудительного, явного вызова исходит только по внешнему событию. ности записать данные в очередь или по-
планировщика. Полезной гибридная мно- Такой тип многозадачности более эффек- лучить их оттуда;
гозадачность может оказаться, когда необ- тивен в отношении производительности, чем • какой эффект оказывает приоритет задач
ходимо сократить время реакции системы вытесняющая многозадачность с разделени- при записи и чтении данных в/из очереди. n
на прерывание. В этом случае в конце тела ем времени. Процессорное время не тратится
Литература
Необходимость явлена глобальная переменная lVal и две за- void vModifyTask(void *pvParameters) {
использования очередей дачи: задача, которая модифицирует общую /* Бесконечный цикл */
переменную, — vModifyTask(), и задача, кото- for (;;) {
Самый простой способ организовать об- рая проверяет значение этой переменной, — /* Модифицировать переменную lVal так,
* чтобы ее значение не изменилось */
мен информацией между задачами — ис- vCheckTask(). Модификация производится lVal += 10;
пользовать общую глобальную переменную. так, чтобы итоговое значение глобальной lVal -= 10;
}
Доступ к такой переменной осуществляется переменной после окончания вычислений }
одновременно из нескольких задач. Такой не изменялось. В случае если значение общей /*------------------------------------------------------------------------*/
подход был продемонстрирован в [1, КиТ переменной отличается от первоначального, /* Функция, реализующая задачу, которая проверяет значение
№ 4] в учебной программе № 3. задача vCheckTask() выдает соответствующее * переменной */
void vCheckTask(void *pvParameters) {
Однако такой подход имеет существен- сообщение на экран.
ный недостаток: при совместном досту- Текст учебной программы № 1: /* Бесконечный цикл */
for (;;) {
пе нескольких задач к общей переменной if (lVal != 100) {
возникает ситуация, когда выполнение одной #include <stdlib.h> puts(“Variable lVal is not 100!”);
#include <stdio.h> }
задачи прерывается планировщиком именно #include <string.h> vTaskDelay(100);
в момент модификации общей переменной, #include “FreeRTOS.h” }
#include “task.h” }
когда та содержит не окончательное (иска- #include “queue.h”
женное) значение. При этом результат рабо- /*------------------------------------------------------------------------*/
/* Глобальная переменная, доступ к которой будет /* Точка входа. С функции main() начнется выполнение
ты другой задачи, которая получит управле- * осуществляться из нескольких задач */ * программы. */
ние и обратится к этой переменной, также long lVal = 100; int main(void) {
/* Создать задачи с равным приоритетом */
окажется искаженным. /*------------------------------------------------------------------------*/ xTaskCreate(vModifyTask, “Modify”, 1000, NULL, 1, NULL);
Продемонстрировать этот эффект позво- /* Функция, реализующая задачи, которая модифицирует xTaskCreate(vCheckTask, “Check”, 1000, NULL, 1, NULL);
* глобальную переменную */ /* Запуск планировщика. Задачи начнут выполняться. */
ляет учебная программа № 1, в которой объ- vTaskStartScheduler();
for (;;);
}
Характеристики очередей
Блокировка при чтении из очереди xQueueHandle в случае успешного создания Назначение параметров и возвращаемое
Когда задача пытается прочитать данные очереди: значение:
из очереди, которая не содержит ни одного • xQueue — дескриптор очереди, в которую
элемента, то задача переходит в блокирован- xQueueHandle xQueueCreate(unsigned portBASE_TYPE uxQueueLength, будет записан элемент. Дескриптор очере-
unsigned portBASE_TYPE uxItemSize );
ное состояние. Такая задача вернется в состо- ди может быть получен при ее создании
яние готовности к выполнению, если другая API-функцией xQueueCreate().
задача (или прерывание) поместит данные Ее параметры и возвращаемое значение: • pvItemToQueue — указатель на элемент,
в очередь. • uxQueueLength — определяет размер оче- который будет записан в очередь. Размер
Выход из блокированного состояния воз- реди, то есть максимальное количество элемента зафиксирован при создании оче-
можен также при истечении тайм-аута, если элементов, которые может хранить оче- реди, так что для побайтового копирова-
очередь на протяжении этого времени оста- редь. ния элемента достаточно иметь указатель
валась пуста. • uxItemSize — задает размер одного эле- на него.
Данные из очереди могут читать сра- мента очереди в байтах, его легко получить • xTicksToWait — максимальное количество
зу несколько задач. Когда очередь пуста, с помощью оператора sizeof(). квантов времени, в течение которого задача
то все они находятся в блокированном со- • Возвращаемое значение — дескриптор оче- может пребывать в блокированном состоя-
стоянии. Когда в очереди появляется элемент реди. Равен NULL, если очередь не создана нии, если очередь полна и записать новый
данных, начнет выполняться та задача, кото- по причине отсутствия достаточного объема элемент невозможно. Для представления
рая имеет наибольший приоритет. памяти в куче FreeRTOS. Ненулевое значе- времени в миллисекундах следует исполь-
Возможна ситуация, когда равноприори- ние свидетельствует об успешном создании зовать макроопределение portTICK_RATE_
тетные задачи ожидают появления данных очереди, в этом случае оно должно быть со- MS [1, КиТ № 4]. Задание xTicksToWait
в очереди. В этом случае при поступлении хранено в переменной типа xQueueHandle равным «0» приведет к тому, что задача
данных в очередь управление получит та для дальнейшего обращения к очереди. не перейдет в блокированное состояние,
задача, время ожидания которой было наи- При создании очереди ядро FreeRTOS выде- если очередь полна, а продолжит свое вы-
большим. Остальные же останутся в блоки- ляет блок памяти из кучи для ее размещения. полнение. Установка xTicksToWait рав-
рованном состоянии. Этот блок памяти используется как для хра- ным константе portMAX_DELAY приведет
нения элементов очереди, так и для хранения к тому, что выхода из блокированного со-
Блокировка при записи в очередь служебной структуры управления очередью, стояния по истечении тайм-аута не будет.
Как и при чтении из очереди, задача мо- которая представлена структурой xQUEUE. Задача будет сколь угодно долго «ожидать»
жет находиться в блокированном состоя- Получить точный размер структуры возможности записать элемент в очередь,
нии, ожидая возможность записи в очередь. xQUEUE для конкретной платформы и ком- пока такая возможность не появится.
Это происходит, когда очередь полностью пилятора можно, получив значение следую- При этом макроопределение INCLUDE_
заполнена и в ней нет свободного места для щего выражения: vTaskSuspend в файле FreeRTOSConfig.h
записи нового элемента данных. До тех пор должно быть равно «1».
пока какая-либо другая задача не прочита- sizeof(xQUEUE) • Возвращаемое значение — может возвра-
ет данные из очереди, задача, которая пишет щать 2 значения:
в очередь, будет «ожидать», находясь в бло- – pdPASS — означает, что данные успеш-
кированном состоянии. При этом следует учесть, что структу- но записаны в очередь. Если определена
В одну очередь могут писать сразу несколь- ра xQUEUE используется ядром в собствен- продолжительность тайм-аута (пара-
ко задач, поэтому возможна ситуация, ког- ных целях и доступа к этой структуре из тек- метр xTicksToWait не равен «0»), то воз-
да несколько задач находятся в блокирован- ста прикладных исходных файлов (main.c врат значения pdPASS говорит о том, что
ном состоянии, ожидая завершения опера- в том числе) изначально нет. Чтобы полу- свободное место в очереди появилось
ции записи в одну очередь. Когда в очереди чить доступ к структуре xQUEUE, необходи- до истечения тайм-аута и элемент был
появится свободное место, получит управле- мо включить в исходный файл строку: помещен в очередь.
ние задача с наивысшим приоритетом. В слу- – errQUEUE_FULL — означает, что дан-
чае если запись в очередь ожидали равно- #include “..\\queue.c” ные не записаны в очередь, так как
приоритетные задачи, управление получит очередь заполнена. Если определена
та, которая находилась в блокированном со- продолжительность тайм-аута (пара-
стоянии дольше остальных. Для платформы x86 и компилятора Open метр xTicksToWait не равен «0» или
Watcom (которые используются в учебных portMAX_DELAY), то возврат значения
Работа с очередями программах) размер структуры xQUEUE со- errQUEUE_FULL говорит о том, что
ставляет 58 байт. тайм-аут завершен и свободное место
Теперь целесообразно рассмотреть кон- в очереди так и не появилось.
кретные API-функции FreeRTOS для рабо- Запись элемента в очередь Следует отметить, что API-функции
ты с очередями. Все API-функции для ра- Для записи элемента в конец очереди xQueueSendToBack() и xQueueSendTo
боты с очередями используют переменную используется API-функция Front() нельзя вызывать из тела обработ-
типа xQueueHandle, которая служит в ка- xQueueSendToBack(), для записи элемента чика прерывания. Для этой цели служат
честве дескриптора (идентификатора) кон- в начало очереди — xQueueSendToFront(). специальные версии этих API-функций —
кретной очереди из множества всех очередей Так как запись в конец очереди применяет- xQueueSendToBackFromISR()и xQueueSend
в программе. Дескриптор очереди можно по- ся гораздо чаще, чем в начало, то вызов API- ToFrontFromISR() соответственно. Более
лучить при ее создании. функции xQueueSend() эквивалентен вызо- подробно об использовании API-функций
ву xQueueSendToBack(). Прототипы у всех FreeRTOS в теле обработчика прерывания бу-
Создание очереди трех API-функций одинаковы: дет рассказано в дальнейших публикациях.
Очередь должна быть явно создана перед
первым ее использованием. API-функция portBASE_TYPE xQueueSendXXXX (xQueueHandle xQueue, Чтение элемента из очереди
const void * pvItemToQueue,
xQueueCreate() служит для создания оче- portTickType xTicksToWait ); Чтение элемента из очереди может быть
реди, она возвращает переменную типа произведено двумя способами:
• Элемент считывается из очереди (создается – errQUEUE_EMPTY — означает, что лизована очередь для хранения элементов
его побайтовая копия в другую перемен- элемент не прочитан из очереди, так типа long. Данные в очередь записывают две
ную), после чего он удаляется из очереди. как очередь пуста. Если определена задачи-передатчика, а считывает данные одна
Именно такой способ считывания проде- продолжительность тайм-аута (пара- задача-приемник.
монстрирован на рис. 2. метр xTicksToWait не равен «0» или
• Создается побайтовая копия элемента, при portMAX_DELAY), то возврат значения #include <stdlib.h>
#include <stdio.h>
этом элемент из очереди не удаляется. errQUEUE_FULL говорит о том, что #include <string.h>
#include “FreeRTOS.h”
Для считывания элемента с удалением тайм-аут завершен и никакая другая за- #include “task.h”
его из очереди используется API-функция дача или прерывание не записали эле- #include “queue.h”
xQueueReceive(). Ее прототип: мент в очередь. /* Объявить переменную-дескриптор очереди.
Как и в случае с записью элемента * Эта переменная будет использоваться
portBASE_TYPE xQueueReceive( * для работы с очередью из тела всех трех задач. */
xQueueHandle xQueue,
в очередь, API-функции xQueueReceive() xQueueHandle xQueue;
const void * pvBuffer, и xQueuePeek() нельзя вызывать из тела об-
portTickType xTicksToWait); /*------------------------------------------------------------------------*/
работчика прерывания. Для этих целей слу- /* Функция, реализующая задачи-передатчики */
жит API-функция xQueueReceiveFromISR(), void vSenderTask(void *pvParameters) {
/* Переменная, которая будет хранить значение, передаваемое
Для считывания элемента из очереди без которая будет описана в следующих публи- * в очередь */
его удаления используется API-функция кациях. long lValueToSend;
/* Переменная, которая будет хранить результат выполнения
xQueuePeek(). Ее прототип: * xQueueSendToBack() */
Состояние очереди portBASE_TYPE xStatus;
portBASE_TYPE xQueuePeek( /* Будет создано несколько экземпляров задачи. В качестве
xQueueHandle xQueue,
Получить текущее количество записанных * параметра задачи выступает число, которое задача будет
const void * pvBuffer, элементов в очереди можно с помощью API- * записывать в очередь */
portTickType xTicksToWait); lValueToSend = (long) pvParameters;
функции uxQueueMessagesWaiting(): /* Бесконечный цикл */
for (;;) {
unsigned portBASE_TYPE uxQueueMessagesWaiting(xQueueHan /* Записать число в конец очереди.
dle xQueue); * 1-й параметр — дескриптор очереди, в которую будет
Назначение параметров и возвращаемое * производиться запись, очередь создана до запуска
* планировщика, и ее дескриптор сохранен в глобальной \
значение для API-функций xQueueReceive() * переменной xQueue.
и xQueuePeek() одинаковы: Назначение параметров и возвращаемое * 2-й параметр — указатель на переменную, которая будет
• xQueue — дескриптор очереди, из которой значение: * записана в очередь, в данном случае — lValueToSend.
* 3-й параметр — продолжительность тайм-аута.
будет прочитан элемент. Дескриптор оче- • xQueue — дескриптор очереди, состояние * В данном случае задана равной 0, что соответствует
* отсутствию времени ожидания, если очередь полна.
реди может быть получен при ее создании которой необходимо получить. Дескриптор * Однако из-за того, что задача-приемник сообщений имеет
API-функцией xQueueCreate(). очереди может быть получен при ее созда- * более высокий приоритет, чем задачи-передатчики,
• pvBuffer — указатель на область памя- нии API-функцией xQueueCreate(). * в очереди не может находиться более одного элемента.
* Таким образом, запись нового элемента будет всегда
ти, в которую будет скопирован элемент • Возвращаемое значение — количество эле- * возможна. */
xStatus = xQueueSendToBack(xQueue, &lValueToSend, 0);
из очереди. Объем памяти, на которую ментов, которые хранит очередь в момент if (xStatus != pdPASS) {
ссылается указатель, должен быть не мень- вызова uxQueueMessagesWaiting(). Если /* Если попытка записи не была успешной —
* индицировать ошибку. */
ше размера одного элемента очереди. очередь пуста, то возвращаемым значени- puts(“Could not send to the queue.\r\n”);
• xTicksToWait — максимальное количе- ем будет «0». }
/* Сделать принудительный вызов планировщика, позволив,
ство квантов времени, в течение которого Как и в случаях с чтением и запи- * таким образом, выполняться другой задаче-передатчику.
задача может пребывать в блокирован- сью элемента в очередь, API-функцию * Переключение на другую задачу произойдет быстрее,
* чем окончится текущий квант времени. */
ном состоянии, если очередь не содержит uxQueueMessagesWaiting() нельзя вызывать taskYIELD();
ни одного элемента. Для представления вре- из тела обработчика прерывания. Для этих }
}
мени в миллисекундах следует использо- целей служит API-функция uxQueueMessage
вать макроопределение portTICK_RATE_MS sWaitingFromISR(). /*------------------------------------------------------------------------*/
/* Функция, реализующая задачу-приемник */
[1, КиТ № 4]. Задание xTicksToWait равным void vReceiverTask(void *pvParameters) {
«0» приведет к тому, что задача не перейдет Удаление очереди /* Переменная, которая будет хранить значение, полученное
* из очереди */
в блокированное состояние, а продолжит Если в программе использована схема рас- long lReceivedValue;
свое выполнение, если очередь в данный пределения памяти, допускающая удаление /* Переменная, которая будет хранить результат выполнения
* xQueueReceive() */
момент пуста. Установка xTicksToWait рав- задач [1, КиТ № 5], то полезной окажется portBASE_TYPE xStatus;
ным константе portMAX_DELAY приведет возможность удалить и очередь, которая ис- /* Бесконечный цикл */
for (;;) {
к тому, что выхода из блокированного со- пользовалась для взаимодействия с удален- /* Индицировать состояние, когда очередь пуста */
стояния по истечении тайм-аута не будет. ной задачей. Для удаления очереди служит if (uxQueueMessagesWaiting(xQueue) != 0) {
puts(“Queue should have been empty!\r\n”);
Задача будет сколь угодно долго «ожидать» API-функция vQueueDelete(). Ее прототип: }
появления элемента в очереди. При этом /* Прочитать число из начала очереди.
* 1-й параметр — дескриптор очереди, из которой будет
макроопределение INCLUDE_vTaskSuspend void vQueueDelete(xQueueHandle xQueue); * происходить чтение, очередь создана до запуска
в файле FreeRTOSConfig.h должно быть * планировщика, и ее дескриптор сохранен в глобальной
* переменной xQueue.
равно «1». * 2-й параметр — указатель на буфер, в который будет
• Возвращаемое значение — может возвра- Единственный аргумент — это дескриптор * помещено число из очереди.
* В данном случае — указатель на переменную lReceivedValue.
щать 2 значения: удаляемой очереди. При успешном заверше- * 3-й параметр — продолжительность тайм-аута, в течение
– pdPASS — означает, что данные успешно нии API-функция vQueueDelete() освободит * которого задача будет находиться в блокированном
* состоянии, пока очередь пуста. В данном случае
прочитаны из очереди. Если определена всю память, выделенную как для размеще- * макроопределение portTICK_RATE_MS используется
продолжительность тайм-аута (пара- ния служебной структуры управления очере- * для преобразования времени 100 мс в количество
* системных квантов.
метр xTicksToWait не равен «0»), то воз- дью, так и для размещения самих элементов */
врат значения pdPASS говорит о том, что очереди. xStatus = xQueueReceive(xQueue, &lReceivedValue, 100 /
portTICK_RATE_MS);
элемент в очереди появился (или уже Рассмотреть процесс обмена сообщениями if (xStatus == pdPASS) {
был там) до истечения тайм-аута и был между несколькими задачами можно на при- /* Число успешно принято, вывести его на экран */
printf(“Received = %ld\r\n”, lReceivedValue);
успешно прочитан. мере учебной программы № 2, в которой реа-
Рис. 3. Результат выполнения учебной программы № 2 Рис. 4. Последовательность выполнения задач в учебной программе № 2
} else { кая именно — точно сказать нельзя, так как Использование очередей
/* Данные не были прочитаны из очереди на протяжении
* тайм-аута 100 мс. они имеют одинаковый приоритет, в нашем для передачи составных типов
* При условии наличия нескольких задач-передатчиков случае пусть это будет задача-передатчик 1. Одним из типичных способов организа-
* означает аварийную ситуацию
*/ В момент времени (2) задача-передатчик ции обмена между задачами с применени-
puts(“Could not receive from the queue.\r\n”); 1 записывает число 100 в очередь. В этот мо- ем очередей является организация несколь-
}
} мент выходит из блокированного состояния ких задач-источников сообщений и одной
}
задача-приемник, так как она «ожидала» появ- задачи-приемника сообщений (как и в учеб-
ления данных в очереди и приоритет ее выше. ной программе выше). При этом полезной
/*------------------------------------------------------------------------*/
/* Точка входа. С функции main() начнется выполнение Прочитав данные из очереди, она вновь бло- окажется возможность знать, какая именно
* программы. */ кируется, так как очередь снова пуста (момент задача-источник поместила данные в оче-
int main(void) {
/* Создать очередь размером 5 элементов для хранения времени (3)). Управление возвращается пре- редь, чтобы понять, какие именно действия
* переменных типа long. рванной задаче-передатчику 1, которая вы- нужно выполнить с этими данными.
* Размер элемента установлен равным размеру переменной
* типа long. полняет API-функцию вызова планировщика Простой способ достижения этого — ис-
* Дескриптор созданной очереди сохранить в глобальной taskYIELD(), в результате чего управление по- пользовать в качестве элемента очереди
* переменной xQueue.
*/ лучает равноприоритетная задача-передатчик структуру, в которой хранятся как сами
xQueue = xQueueCreate(5, sizeof(long)); 2 (момент времени (4)). Когда она записывает данные, так и указан источник сообщения.
/* Если очередь успешно создана (дескриптор не равен NULL) */
if (xQueue != NULL) { значение 200 в очередь, снова разблокируется На рис. 5 показана организация обмена ин-
/* Создать 2 экземпляра задачи-передатчика. Параметр,
* передаваемый задаче при ее создании, используется для
высокоприоритетная задача-приемник — мо- формацией между задачами в абстрактной
* передачи экземпляру конкретного значения, которое мент времени (5), и цикл повторяется. программе, реализующей контроллер двига-
* экземпляр задачи будет записывать в очередь.
* Задача-передатчик 1 будет записывать значение 100.
Следует отметить, что в ранее приведенном теля с CAN-интерфейсом.
* Задача-передатчик 2 будет записывать значение 200. примере, когда задача-приемник имеет более На рис. 5 изображена также структу-
* Обе задачи создаются с приоритетом 1.
*/ высокий приоритет, чем задачи-передатчики, ра xData, которая выступает в данном случае
xTaskCreate(vSenderTask, “Sender1”, 1000, (void *) 100, 1, NULL); очередь не может быть заполнена более чем типом элементов очереди. Структура содер-
xTaskCreate(vSenderTask, “Sender2”, 1000, (void *) 200, 1, NULL);
/* Создать задачу-приемник, которая будет считывать числа на 1 элемент данных. жит два целочисленных значения:
* из очереди.
* Приоритет = 2, т.е. выше, чем у задач-передатчиков.
*/
xTaskCreate(vReceiverTask, “Receiver”, 1000, NULL, 2, NULL);
/* Запуск планировщика. Задачи начнут выполняться. */
vTaskStartScheduler();
} else {
/* Если очередь не создана */
}
/* При успешном создании очереди и запуске планировщика
* программа никогда “не дойдет” до этого места. */
for (;;)
;
}
• iMeaning — значение, смысл передаваемо- /* Определить массив из двух структур, которые будут * При условии, что очередь должна быть полна, означает
го через очередь параметра; * записываться в очередь */ * аварийную ситуацию */
• iValue — числовое значение параметра. static const xData xStructsToSend[ 2 ] =
{ }
puts(“Could not receive from the queue.\r\n”);
Рис. 6. Результат выполнения учебной программы № 3 Рис. 7. Последовательность выполнения задач в учебной программе № 3
образом место в очереди. Как только в оче- Использование очередей что несколько задач не будут одновре-
реди появилось свободное место, планиров- для передачи больших объемов данных менно обращаться к памяти, на которую
щик выведет из состояния блокировки ту за- Если размер одного элемента очереди ссылается указатель. В идеальном случае
дачу из числа «ожидавших», которая дольше достаточно велик, то предпочтительно ис- только задача-передатчик должна иметь
остальных пребывала блокированной. В на- пользовать очередь для хранения не самих доступ к памяти, пока указатель на эту
шем случае это задача-передатчик 2 (момент элементов, а для хранения указателей на эле- память находится в очереди. Когда же
времени (7)). Так как приоритет у нее выше, менты (например, на массивы или на струк- указатель прочитан из очереди, только
она вытеснит задачу-приемник и запишет туры). задача-приемник должна иметь возмож-
следующий элемент в очередь. После чего Преимущества такого подхода: ность доступа к памяти.
она вызовет планировщик API-функцией • Экономия памяти. Память при созда- • Память, на которую ссылается указатель,
taskYIELD(). Однако готовых к выполнению нии очереди выделяется под все элемен- должна существовать. Это требование ак-
задач с более высоким или равным приори- ты очереди, даже если очередь пуста. туально, если указатель ссылается на дина-
тетом на этот момент нет, поэтому пере- Использование небольших по объему за- мически выделенную память. Только одна
ключения контекста не произойдет, и задача- нимаемой памяти указателей вместо объ- задача должна быть ответственна за осво-
передатчик 2 продолжит выполняться. Она емных структур или массивов позволяет бождение динамически выделенной памя-
попытается записать в очередь еще один достичь существенной экономии памяти. ти. Задачи не должны обращаться к памя-
элемент, но очередь заполнена, и задача- • Меньшее время записи элемента в очередь ти, если та уже была освобождена.
передатчик 2 перейдет в блокированное со- и чтения его из очереди. При записи/чте- • Нельзя использовать указатель на пере-
стояние (момент времени (8)). нии элемента из очереди происходит его менные, расположенные в стеке задачи,
Снова сложилась ситуация, когда все вы- побайтовое копирование. Копирование то есть указатель на локальные перемен-
сокоприоритетные задачи-передатчики бло- указателя выполняется быстрее копирова- ные задачи. Данные, на которые ссылается
кированы, поэтому управление получит ния объемных структур данных. указатель, будут неверными после очеред-
низкоприоритетная задача-приемник (8). Тем не менее использование указателей ного переключения контекста.
Однако на этот раз после появления свобод- в качестве элементов очереди сопряжено
ного места в очереди разблокируется задача- с некоторыми трудностями, преодоление Выводы
передатчик 1, так как теперь ее время пребы- которых ложится на плечи программиста.
вания в блокированном состоянии превыша- Для достижения корректной работы про- В этой части статьи был подробно описан
ет время задачи-передатчика 2, и т. д. граммы должны быть выполнены следую- механизм очередей как средства межзадач-
Следует отметить, что в ранее приведен- щие условия: ного взаимодействия. Показаны основные
ном примере, когда задачи-передатчики • У памяти, адресуемой указателем, в каж- способы организации такого взаимодей-
имеют более высокий приоритет, чем задача- дый момент времени должна быть одна ствия. Однако существуют еще несколько
приемник, в очереди в любой момент вре- четко определенная задача-хозяин, ко- API-функций для работы с очередями, кото-
мени не может быть более одного свободного торая может обращаться к этой памя- рые используются только для отладки ядра
места. ти. То есть необходимо гарантировать, FreeRTOS. О них будет рассказано в дальней-
ших публикациях, посвященных возможно-
стям отладки и трассировки. В следующей же
публикации внимание будет сконцентриро-
вано на особенностях обработки прерываний
микроконтроллера в среде FreeRTOS. ■
Литература
Рис. 1. Отложенная обработка прерывания с использованием двоичного семафора Во FreeRTOS механизм семафоров основан
на механизме очередей. По большому счету
API-функции для работы с семафорами пред-
исходит прерывание при возникновении На рис. 2 показано, как обработчик пре- ставляют собой макросы — «обертки» других
какого-то внешнего события. В момент вре- рывания отдает семафор, вне зависимости API-функций для работы с очередями. Здесь
мени (2) управление получает обработчик от того, был ли он захвачен до этого. Задача- и далее для простоты будем называть их API-
прерывания, который, используя механизм обработчик в свою очередь захватывает сема- функциями для работы с семафорами.
двоичного семафора, выводит из блоки-
рованного состояния задачу-обработчик а
прерывания. Так как приоритет задачи-
обработчика выше приоритета прикладной
задачи, то задача-обработчик вытесняет при-
кладную задачу, которая остается в состоя-
нии готовности к выполнению (3). В момент
времени (4) задача-обработчик блокируется,
ожидая возникновения следующего преры-
вания, и управление снова получает низко-
приоритетная прикладная задача. б
В теории многопоточного программиро-
вания [1] двоичный семафор определен как
переменная, доступ к которой может быть
осуществлен только с помощью двух атомар-
ных функций (то есть тех, которые не могут
быть прерваны планировщиком):
1) wait() или P() — означает захват семафора,
если он свободен, и ожидание, если занят.
В примере выше функцию wait() реализует в
задача-обработчик прерывания.
2) signal() или V() — означает выдачу семафо-
ра, то есть после того как одна задача выда-
ет семафор, другая задача, которая ожидает
возможности его захвата, может его захва-
тить. В примере выше функцию signal()
реализует обработчик прерывания.
Легко заметить, что операция выдачи
двоичного семафора напоминает операцию г
помещения элемента в очередь, а операция
захвата семафора — чтения элемента из оче-
реди. Если установить размер очереди рав-
ным одному элементу, то очередь превра-
щается в двоичный семафор. Наличие эле-
мента в очереди означает, что одна (а может,
и несколько) задача произвела(и) выдачу
семафора, и теперь другая задача может его
захватить. Пустая же очередь означает ситуа-
д
цию, когда семафор уже был захвачен, и за-
дача, которая «хочет» его захватить, вынуж-
дена ожидать (находясь в блокированном со-
стоянии), пока другая задача или обработчик
прерывания произведут выдачу семафора.
В именах API-функций FreeRTOS для ра-
боты с семафорами используются термины
Take — эквивалентен функции wait(), то есть
захват двоичного семафора, и Give — экви-
валентен функции signal(), то есть означает Рис. 2. Синхронизация прерывания и задачи-обработчика с помощью двоичного семафора
выдачу семафора.
Рис. 4. Результаты выполнения учебной программы № 1 Рис. 5. Последовательность выполнения задач в учебной программе № 1
это время реакции системы на прерывание, ко- /*-----------------------------------------------------------*/ В демонстрационных целях использовано
/* Задача-обработчик */
торое может составлять до одного системного static void vHandlerTask(void *pvParameters) { не аппаратное, а программное прерывание
кванта: величина dT на рис. 3. /* Как и большинство задач, реализована как бесконечный цикл */ MS-DOS, которое «вручную» вызывается
for (;;) {
Далее в учебной программе № 1 будет /* Реализовано ожидание события с помощью двоичного из служебной периодической задачи каждые
приведен пример использования значения семафора. Семафор после создания становится 500 мс. Заметьте, что сообщение на дисплей
доступен (так, как будто его кто-то отдал).
*pxHigherPriorityTaskWoken для принуди- Поэтому сразу после запуска планировщика задача
выводится как до генерации прерывания, так
тельного переключения контекста. захватит его. Второй раз сделать это ей не удастся, и после него, что позволяет проследить по-
и она будет ожидать, находясь в блокированном
В случае использования API-функции состоянии, пока семафор не отдаст обработчик
следовательность выполнения задач (рис. 4).
xSemaphoreGive() переключение контекста прерывания. Время ожидания задано равным Следует обратить внимание на использо-
бесконечности, поэтому нет необходимости проверять
происходит автоматически, и нет необходи- возвращаемое функцией xSemaphoreTake() значение. */
вание параметра xHigherPriorityTaskWoken
мости в его принудительном переключении. xSemaphoreTake(xBinarySemaphore, portMAX_DELAY); в API-функции xSemaphoreGiveFromISR().
/* Если программа “дошла” до этого места, значит,
Рассмотрим учебную программу № 1, в ко- семафор был успешно захвачен.
До вызова функции ему присваивается значе-
торой продемонстрировано использование Обработка события, связанного с семафором. ние pdFALSE, а после вызова — проверяется
В нашем случае – индикация на дисплей. */
двоичного семафора для синхронизации puts(“Handler task - Processing event.\r\n”);
на равенство pdTRUE. Таким образом отсле-
прерывания и задачи-обработчика этого пре- } живается необходимость принудительного
рывания: } переключения контекста. В данной учебной
/*-----------------------------------------------------------*/ программе такая необходимость возникает
#include <stdlib.h> /* Точка входа. С функции main() начнется выполнение
#include <stdio.h>
каждый раз, так как в системе постоянно на-
программы. */
#include <string.h> int main(void) { ходится более высокоприоритетная задача-
#include <dos.h>
#include “FreeRTOS.h”
/* Перед использованием семафор необходимо создать. */ обработчик, которая ожидает возможности
vSemaphoreCreateBinary(xBinarySemaphore);
#include “task.h” /* Связать прерывание MS-DOS с обработчиком прерывания захватить семафор.
#include “semphr.h”
#include “portasm.h”
vExampleInterruptHandler(). */ Для принудительного переключения кон-
_dos_setvect(0x82, vExampleInterruptHandler);
/* Если семафор успешно создан */ текста служит API-макрос portSWITCH_
/* Двоичный семафор – глобальная переменная */
xSemaphoreHandle xBinarySemaphore;
if (xBinarySemaphore != NULL) { CONTEXT(). Однако для других платформ
/* Создать задачу-обработчик, которая будет
синхронизирована с прерыванием. имя макроса будет иным, например, для ми-
/*-----------------------------------------------------------*/
/* Периодическая задача */
Приоритет задачи-обработчика выше, кроконтроллеров AVR это будет taskYIELD(),
чем у периодической задачи. */
static void vPeriodicTask(void *pvParameters) {
xTaskCreate(vHandlerTask, “Handler”, 1000, NULL, 3, NULL); для ARM7 — portYIELD_FROM_ISR(). Узнать
for (;;) {
* Эта задача используется только с целью генерации /* Создать периодическую задачу, которая будет точное имя макроса можно из демонстраци-
генерировать прерывание с некоторым интервалом.
прерывания каждые 500 мс */
Ее приоритет – ниже, чем у задачи-обработчика. */ онного проекта для конкретной платформы.
vTaskDelay(500 / portTICK_RATE_MS);
/* Сгенерировать прерывание. xTaskCreate(vPeriodicTask, “Periodic”, 1000, NULL, 1, NULL); Переключение между задачами в учебной
/* Запуск планировщика. */
Вывести сообщение до этого и после. */
vTaskStartScheduler();
программе № 1 приведено на рис. 5.
puts(“Periodic task - About to generate an interrupt.\r\n”);
__asm {int 0x82} /* Сгенерировать прерывание MS-DOS */ } Бóльшую часть времени ни одна задача
puts(“Periodic task - Interrupt generated.\r\n\r\n\r\n”); /* При нормальном выполнении программа до этого места
“не дойдет” */
не выполняется (бездействие), но каждые 0,5 с
}
} for (;;) управление получает периодическая задача
;
/*-----------------------------------------------------------*/ }
(1). Она выводит первое сообщение на экран
/* Обработчик прерывания */ и принудительно вызывает прерывание, об-
static void __interrupt __far vExampleInterruptHandler( void )
{
static portBASE_TYPE xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
/* Отдать семафор задаче-обработчику */
x S e m a p h o r e G i v e F r o m I S R ( x B i n a r y S e m a p h o r e ,
&xHigherPriorityTaskWoken );
if( xHigherPriorityTaskWoken == pdTRUE )
{
/* Это разблокирует задачу-обработчик. При этом
приоритет задачи-обработчика выше приоритета
выполняющейся в данный момент периодической
задачи. Поэтому переключаем контекст
принудительно – так мы добьемся того, что после
выполнения обработчика прерывания управление
получит задача-обработчик.*/
portSWITCH_CONTEXT();
Счетные семафоры
portTickType xLastExecutionTime;
unsigned portLONG ulValueToSend = 0;
int i;
/* Переменная xLastExecutionTime нуждается в инициализации
текущим значением счетчика квантов.
Это единственный случай, когда ее значение задается явно.
В дальнейшем ее значение будет автоматически
модифицироваться API-функцией vTaskDelayUntil(). */
xLastExecutionTime = xTaskGetTickCount();
for (;;) {
/* Это периодическая задача. Период выполнения – 200 мс. */
vTaskDelayUntil(&xLastExecutionTime, 200 / portTICK_RATE_MS);
/* Отправить в очередь № 1 5 чисел от 0 до 4. Числа будут
считаны из очереди в обработчике прерывания.
Обработчик прерывания всегда опустошает очередь, поэтому
запись 5 элементов будет всегда возможна – в переходе
в блокированное состояние нет необходимости */
for (i = 0; i < 5; i++) {
Рис. 10. Обмен данными между задачами и прерыванием в учебной программе № 2 xQueueSendToBack(xIntegerQueue, &ulValueToSend, 0);
ulValueToSend++;
}
/* Принудительно вызвать прерывание. Отобразить
сообщение до его вызова и после. */
3. pxTaskWoken — значение *pxTaskWoken Гораздо эффективнее использовать один puts(“Generator task - About to generate an interrupt.”);
устанавливается равным pdTRUE, если су- из следующих подходов: __asm {int 0x82} /* Эта инструкция сгенерирует прерывание. */
puts(“Generator task - Interrupt generated.\r\n”);
ществует задача, которая «хочет» записать 1. Внутри обработчика прерывания поме- }
данные в очередь, и приоритет у нее выше, щать каждый принятый символ в простой }
чем у задачи, выполнение которой прервало буфер, а когда сообщение будет принято /*-----------------------------------------------------------*/
прерывание. Если таковой задачи нет, то зна- полностью или обнаружится окончание /* Обработчик прерывания */
static void __interrupt __far vExampleInterruptHandler( void )
чение *pxTaskWoken остается неизменным. передачи, использовать двоичный семафор {
static portBASE_TYPE xHigherPriorityTaskWoken;
Проанализировав значение *pxTaskWoken для разблокировки задачи-обработчика, static unsigned long ulReceivedNumber;
после выполнения xQueueReceiveFromISR(), которая произведет интерпретацию при- /* Массив строк определен как static, значит, память для его
размещения выделяется как
можно сделать вывод о необходимости нятого сообщения. для глобальной переменной (он хранится не в стеке). */
принудительного переключения контекста 2. Интерпретировать сообщение внутри об- static const char *pcStrings[] =
{
в конце обработчика прерывания. В этом работчика прерывания, а очередь использо- “String 0”,
случае управление сразу перейдет разбло- вать для передачи интерпретированной ко- “String 1”,
“String 2”,
кированной высокоприоритетной задаче. манды (как показано на рис. 5, КиТ № 6`2011, “String 3”
4. Возвращаемое значение — может прини- стр. 102). Такой подход допускается, если };
/* Аргумент API-функции xQueueReceiveFromISR(), который
мать 2 значения: интерпретация не содержит сложных алго- устанавливается в pdTRUE, если операция с очередью
– pdTRUE — означает, что данные успеш- ритмов и занимает немного процессорного разблокирует более высокоприоритетную задачу.
Перед вызовом xQueueReceiveFromISR() должен
но прочитаны из очереди. времени. принудительно устанавливаться в pdFALSE */
– pdFALSE — означает, что данные не про- Рассмотрим учебную программу № 2, в ко- xHigherPriorityTaskWoken = pdFALSE;
/* Считывать из очереди числа, пока та не станет пустой. */
читаны, так как очередь пуста. торой продемонстрировано применение while( xQueueReceiveFromISR( xIntegerQueue,
&ulReceivedNumber,
Следует обратить внимание, что в отличие API-функций xQueueSendToBackFromISR() &xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
от версий API-функций для работы с очередя- и xQueueReceiveFromISR() внутри обработ- {
/* Обнулить в числе все биты, кроме последних двух.
ми, предназначенными для вызова из тела за- чика прерываний. В программе реализована Таким образом, полученное число будет принимать
дачи, описанные выше API-функции не име- задача — генератор чисел, которая отвечает значения от 0 до 3. Использовать полученное число
как индекс в массиве строк. Получить таким образом
ют параметра portTickType xTicksToWait, за генерацию последовательности целых чи- указатель на строку, который передать в очередь № 2 */
который задает время ожидания задачи в бло- сел. Целые числа по 5 штук помещаются в оче- ulReceivedNumber &= 0x03;
xQueueSendToBackFromISR( xStringQueue,
кированном состоянии. Что и понятно, так редь № 1, после чего происходит программное &pcStrings[ ulReceivedNumber ],
как обработчик прерывания — это не задача, прерывание (для простоты оно генерируется &xHigherPriorityTaskWoken );
}
и он не может переходить в блокированное из тела задачи — генератора чисел). Внутри /* Проверить, не разблокировалась ли более высокоприоритетная
состояние. Поэтому если чтение/запись из/в обработчика прерывания происходит чте- задача при записи в очередь. Если да, то выполнить
принудительное переключение контекста. */
очередь невозможно выполнить внутри об- ние числа из очереди № 1 с помощью API-
if( xHigherPriorityTaskWoken == pdTRUE )
работчика прерывания, то соответствующая функции xQueueReceiveFromISR(). Далее это {
API-функция вернет управление сразу же. число преобразуется в указатель на строку, ко- /* Макрос, выполняющий переключение контекста.
На других платформах имя макроса может быть другое! */
торый помещается в очередь № 2 с помощью portSWITCH_CONTEXT();
Эффективное API-функции xQueueSendToBackFromISR(). }
}
использование очередей Задача-принтер считывает указатели из очере-
ди № 2 и выводит соответствующие им строки /*-----------------------------------------------------------*/
/* Задача-принтер. */
Бóльшая часть демонстрационных про- на экран (рис. 10). static void vStringPrinter(void *pvParameters) {
ектов из дистрибутива FreeRTOS содержит Текст учебной программы № 2: char *pcString;
/* Бесконечный цикл */
пример работы с очередями, в котором оче- for (;;) {
редь используется для передачи каждого от- #include <stdlib.h> /* Прочитать очередной указатель на строку из очереди № 2.
#include <stdio.h> Находится в блокированном состоянии сколь угодно долго,
дельного символа, полученного от универ- #include <string.h> пока очередь № 2 пуста. */
сального асинхронного приемопередатчика #include <dos.h> xQueueReceive(xStringQueue, &pcString, portMAX_DELAY);
#include “FreeRTOS.h” /* Вывести строку, на которую ссылается указатель на дисплей. */
(UART), где символ записывается в очередь #include “task.h” puts(pcString);
#include “queue.h” }
внутри обработчика прерывания, а считыва- #include “portasm.h” }
ется из нее в теле задачи.
/* Дескрипторы очередей – глобальные переменные */ /*-----------------------------------------------------------*/
Передача сообщения побайтно при помощи xQueueHandle xIntegerQueue; /* Точка входа. С функции main() начнется выполнение
очереди — это очень неэффективный метод xQueueHandle xStringQueue; программы. */
int main(void) {
обмена информацией (особенно на высоких /*-----------------------------------------------------------*/ /* Как и другие объекты ядра, очереди необходимо создать
скоростях передачи) и приводится в демонстра- /* Периодическая задача — генератор чисел */ до первого их использования. Очередь xIntegerQueue будет
static void vIntegerGenerator(void *pvParameters) { хранить переменные типа unsigned long. Очередь
ционных проектах лишь для наглядности.
Выводы
Рис. 13. Возможность вызова API-функций в обработчиках прерываний
В любой операционной системе реального
времени с вытесняющей многозадачностью
существует потенциальный источник оши- При этом результат обращения к ресурсу Литература
бок и сбоев работы системы — это едино- в обеих задачах окажется ошибочным, ис-
временное обращение сразу нескольких задач каженным. 1. Эндрюс Г. Р. Основы многопоточного, парал-
к одному ресурсу. В качестве ресурса может К счастью, во FreeRTOS существуют встро- лельного и распределенного программирования.
выступать множество видов объектов: енные на уровне ядра механизмы обеспечения Пер. с англ. М.: ИД «Вильямс», 2003.
• память; совместного доступа к одному аппаратному 2. Курниц А. FreeRTOS — операционная система
• периферийные устройства; ресурсу. С применением счетных семафоров для микроконтроллеров // Компоненты и тех-
• библиотечные функции и др. для управления доступом к ресурсам читатель нологии. 2011. № 2–6.
Проблема возникает, когда одна задача уже познакомился. В следующей публикации 3. Barry R. Using the FreeRTOS real time kernel:
начинает какие-либо действия с ресурсом, внимание будет сконцентрировано на сред- A Practical Guide. 2009.
но не успевает их закончить, когда проис- ствах FreeRTOS обеспечения безопасного до- 4. http://www.freertos.org
ходит переключение контекста и управление ступа к ресурсам. К таковым относятся: 5. http://www.ignatova-e-n.narod.ru/mop/zag6.
получает другая задача, которая обращается • мьютексы и двоичные семафоры; html
к тому же самому ресурсу, состояние которо- • счетные семафоры; 6. http://ru.wikipedia.org/wiki/Прерывание
го носит промежуточный, не окончательный • критические секции; 7. http://www.mikrocontroller.net/attachment/
характер (из-за воздействия первой задачи). • задачи-сторожа (gatekeeper tasks). n 95930/FreeRTOSPaper.pdf
/* Код на Си */
Под ресурсами микроконтроллерной системы понимают как физи- PORTG ^= (1 << PG3);
чески существующие устройства внутри микроконтроллера (области /* Скомпилированный машинный код и инструкции ассемблера */
544: 80 91 65 00 lds r24, 0x0065 ; Загрузить PORTG в регистр общего назначения
оперативной памяти и периферийные устройства), так и внешние 548: 98 e0 ldi r25, 0x08 ; Бит PG3 — в другой регистр
по отношению к микроконтроллеру устройства (другие микрокон- 54a: 89 27 eor r24, r25 ; Операция Исключающее ИЛИ
54c: 80 93 65 00 sts 0x0065, r24 ; Результат — обратно в PORTG
троллеры, контроллеры протоколов, дисплеи и т. д.). К этим группам
можно свести все примеры ресурсов, приводимые ниже.
Потенциальная причина сбоев и ошибок в мультизадачных систе- Для микроконтроллеров ARM7:
мах — это неправильно организованный совместный доступ к ресурсам
из нескольких задач и/или прерываний. Одна задача получает доступ /* Код на Си. */
PORTA |= 0x01;
к ресурсу, начинает выполнять некоторые действия с ним, но не завер- /* Скомпилированный машинный код и инструкции ассемблера */
шает операции с ресурсом до конца. В этот момент может произойти: 0x00000264 481C LDR R0,[PC,#0x0070] ; Получить адрес PORTA
0x00000266 6801 LDR R1,[R0,#0x00] ; Считать значение PORTA в R1
• Переключение контекста задачи, то есть процессор начнет выполнять 0x00000268 2201 MOV R2,#0x01 ; Поместить 1 в R2
другую задачу. 0x0000026A 4311 ORR R1,R ; Лог. И регистра R1 (PORTA) и R2 (константа 1)
0x0000026C 6001 STR R1,[R0,#0x00] ; Сохранить новое значение в PORTA
• Прерывание действия микроконтроллера, вследствие чего про-
цессор займется выполнением обработчика соответствующего
прерывания. И в первом, и во втором случае последовательность действий сводится:
Если другая задача или обработчик возникшего прерывания обратятся • к копированию значения порта микроконтроллера в регистр обще-
к этому же самому ресурсу, состояние которого носит промежуточный го назначения,
характер из-за воздействия первой задачи, то результат работы програм- • к модификации регистра общего назначения,
мы будет отличаться от ожидаемого. Рассмотрим несколько примеров. • к обратному копированию результата из регистра общего назна-
чения в порт.
Доступ к внешней периферии Такую последовательность действий называют операцией чте-
Рассмотрим сценарий, когда две задачи — задача А и задача Б — ния/модификации/записи.
выводят информацию на ЖКИ-дисплей. Задача А ответственна Теперь рассмотрим случай, когда сразу две задачи выполняют опе-
за вывод значения каких-либо параметров на дисплей. Задача Б от- рацию чтения/модификации/записи одного и того же порта.
вечает за вывод экстренных сообщений об авариях: 1) Задача А загружает значение порта в регистр.
2) В этот момент ее вытесняет задача Б, при стров процессора. Если функция использует ваться механизмом взаимного исключения
этом задача А не «успела» модифициро- переменные, расположенные только в стеке (mutual exclusion).
вать и записать данные обратно в порт. или в регистрах процессора, то она является Механизм взаимного исключения гаранти-
3) Задача Б изменяет значение порта и, на- реентерабельной. Напротив, функция, кото- рует, что если задача начала выполнять неко-
пример, блокируется. рая сохраняет свое состояние между вызова- торые действия с ресурсом, то никакая другая
4) Задача А продолжает выполняться с точки, ми в статической или глобальной перемен- задача (или прерывание) не сможет получить
в которой ее выполнение было прервано. ной, не является реентерабельной. доступ к данному ресурсу, пока операции
При этом она продолжает работать с ко- Таким образом, функция, которая зависит с ним не будут завершены первой задачей.
пией порта в регистре, выполняет какие-то только от своих параметров, не использует FreeRTOS предлагает несколько способов
действия над ним и записывает значение глобальные и статические переменные и вы- реализации механизма взаимного исключе-
регистра обратно в порт. зывает только реентерабельные функции, ния:
Можно видеть, что в этом случае результат будет реентерабельной [4]. • критические секции;
воздействия задачи Б на порт окажется потерян Одновременный вызов нереентерабель- • мьютексы;
и порт будет содержать неверное значение. ной функции из нескольких задач может • задачи-сторожа.
О подобных операциях чтение/модифи- привести к непредсказуемому результату. Однако наилучшая реализация взаимно-
кация/запись говорят, что они не являются Реентерабельными функциями можно поль- го исключения — это написание програм-
атомарными. Атомарными же операциями зоваться, не опасаясь одновременного их вы- мы, в которой ресурсы не разделяются меж-
называют те, выполнение которых не может зова из нескольких задач. ду несколькими задачами и доступ к одному
быть прервано планировщиком. Приводя Рассмотрим пример реентерабельной ресурсу выполняет единственная задача или
пример из архитектуры AVR, можно назвать функции: прерывание.
инструкции процессора cbi и sbi, позволя-
ющие сбросить/установить бит в регистре /* Параметр передается в функцию через регистр общего назначе- Критические секции
ния или стек. Это безопасно, т. к. каждая задача имеет свой набор
специальных функций. Разумеется, опера- регистров и свой стек. */
ция длиной в одну машинную инструкцию long lAddOneHundered( long lVar1 ) Сразу следует отметить, что критические
{
не может быть прервана планировщиком, /* Объявлена локальная переменная. Компилятор расположит ее секции — это очень грубый способ реализа-
то есть является атомарной. или в регистре или в стеке в зависимости от уровня оптимизации. ции взаимного исключения.
Каждая задача и каждое прерывание, вызывающее эту функцию,
Неатомарными могут быть не только опе- будет иметь свою копию этой локальной переменной. */ Критическая секция — это часть програм-
рации с регистрами специальных функций. long lVar2; мы, которую в один момент времени может
/* Какие-то действия над аргументом и локальной переменной.
Операция над любой переменной языка Си, */ выполнять только одна задача или прерыва-
физический размер которой превышает раз- lVar2 = lVar1 + 100; ние. Обычно защищаемый критической сек-
/* Обычно возвращаемое значение также помещается либо в стек,
рядность микроконтроллера, является не- либо в регистр. */ цией участок кода начинается с инструкции
атомарной. Например, операция инкремента return lVar2; входа в критическую секцию и заканчивается
}
глобальной переменной типа unsigned long инструкцией выхода из нее.
на 8‑битной архитектуре AVR выглядит так: Во FreeRTOS, в отличие от более сложных
Теперь рассмотрим несколько нереентера- операционных систем, существует одна гло-
/* Код на Си */ бельных функций: бальная критическая секция. Если одна за-
unsigned long counter = 0;
counter++; дача вошла в критическую секцию, то ника-
/* Скомпилированный машинный код и инструкции ассемблера */ /* В этом случае объявлена глобальная переменная. Каждая зада- кая другая задача не будет выполняться, пока
618: 80 91 13 01 lds r24, 0x0113 ча, вызывающая функцию, которая использует эту переменную,
61c: 90 91 14 01 lds r25, 0x0114 будет «иметь дело» с одной и той же копией этой переменной */ не произойдет выход из критической секции.
620: a0 91 15 01 lds r26, 0x0115 long lVar1; FreeRTOS допускает два способа реализа-
624: b0 91 16 01 lds r27, 0x0116
628: 01 96 adiw r24, 0x01 ; 1 /* Нереентерабельная функция 1 */ ции критической секции:
62a: a1 1d adc r26, r1 long lNonReentrantFunction1( void ) • запрет прерываний;
62c: b1 1d adc r27, r1 {
62e: 80 93 13 01 sts 0x0113, r24 /* Какие-то действия с глобальной переменной. */ • приостановка планировщика.
632: 90 93 14 01 sts 0x0114, r25 lVar1 += 10;
636: a0 93 15 01 sts 0x0115, r26
63a: b0 93 16 01 sts 0x0116, r27 return lVar1; Запрет прерываний
} Во FreeRTOS вход в критическую сек-
/* Нереентерабельная функция 2 */ цию, реализованную запретом прерываний,
Если другая задача или прерывание обра- void lNonReentrantFunction2( void ) сводится к запрету всех прерываний про-
{
тятся к этой же переменной в течение этих /* Переменная, объявленная как статическая. Компилятор рас- цессора или (в зависимости от конкретно-
11 инструкций, результат окажется искажен- положит ее не в стеке. Значит, каждая задача, вызывающая эту го порта FreeRTOS) к запрету прерываний
функцию, будет «иметь дело» с одной и той же копией этой
ным. переменной. */ с приоритетом равным и ниже макроопре-
Следует отметить, что неатомарными яв- static long lState = 0; деления configMAX_SYSCALL_INTERRUPT_
switch( lState ) { /* … */};
ляются также операции с составными типа- } PRIORITY.
ми — структурами, когда модифицируется Во FreeRTOS участок кода, защищаемый
/* Нереентерабельная функция 3 */
сразу несколько членов структуры. long lNonReentrantFunction3( void ) критической секцией, которая реализо-
{ вана запретом прерываний, — это участок
/* Функция, которая вызывает нереентерабельную функцию,
Реентерабельность функций также является нереентерабельной. */ кода, окруженный вызовом API-макросов:
Функция называется реентерабельной, return lNonReentrantFunction1() + 100; taskENTER_CRITICAL() — вход в критиче-
}
если она корректно работает при одновре- скую секцию и taskEXIT_CRITICAL() — вы-
менном ее вызове из нескольких задач и/или ход из критической секции.
прерываний. Под одновременным вызовом Механизм Переключение контекста при вытесняю-
понимается вызов функции из одной задачи взаимного исключения щей многозадачности происходит по преры-
в тот момент, когда та уже вызвана из другой ванию (обычно от таймера), поэтому задача,
задачи, но еще не выполнена до конца. Доступ к ресурсу, операции с которым которая вызвала taskENTER_CRITICAL(), бу-
Во FreeRTOS каждая задача имеет свой соб- одновременно выполняют несколько задач дет оставаться в состоянии выполнения, пока
ственный стек и свой набор значений реги- и/или прерываний, должен контролиро- не вызовет taskEXIT_CRITICAL().
Участки кода, находящиеся внутри кри- контекста каждый системный квант вре- должен отдать мьютекс обратно. Только когда
тической секции, должны быть как можно мени не происходит, задача, которая вы- мьютекс освободился (возвращен какой-либо
короче и выполняться как можно быстрее. звала vTaskSuspendAll(), будет выполнять- задачей), другая задача может его захватить
Иначе использование критических сек- ся сколь угодно долго до запуска плани- и безопасно выполнить свои операции с об-
ций негативно скажется на времени реакции ровщика. API-функция vTaskSuspendAll() щим для нескольких задач ресурсом. Задаче
системы на прерывания. не влияет на прерывания: если до вызова не разрешено выполнять операции с ресур-
FreeRTOS допускает вложенный вызов ма- vTaskSuspendAll() они были разрешены, сом, если в данный момент она не является
кросов taskENTER_CRITICAL() и taskEXIT_ то при возникновении прерываний их об- владельцем мьютекса. Процессы, происходя-
CRITICAL(), их реализация позволяет со- работчики будут выполняться. щие при взаимном исключении доступа с ис-
хранять глубину вложенности. Выход про- Если же обработчик прерывания выполнил пользованием мьютекса, приведены на рис. 1.
граммы из критической секции происходит, макрос принудительного переключения кон- Обе задачи нуждаются в доступе к ресурсу,
только если глубина вложенности станет текста (portSWITCH_CONTEXT(), taskYIELD(), однако только задача-владелец мьютекса мо-
равной нулю. Каждому вызову taskENTER_ portYIELD_FROM_ISR() и др. — в зависимости жет его получить (рис. 1а). Задача А пытается
CRITICAL() должен соответствовать вызов от порта FreeRTOS), то запрос на переключение захватить мьютекс, в этот момент он свобо-
taskEXIT_CRITICAL(). контекста будет выполнен, как только работа ден, поэтому она становится его владельцем
Пример использования критической секции: планировщика будет возобновлена. (рис. 1б). Задача А выполняет некоторые дей-
Другие API-функции FreeRTOS нельзя вы- ствия с ресурсом. В этот момент задача Б пы-
/* Чтобы доступ к порту PORTA не был прерван никакой другой зывать, когда планировщик приостановлен тается захватить тот же самый мьютекс, однако
задачей, входим в критическую секцию. */
taskENTER_CRITICAL(); вызовом vTaskSuspendAll(). это ей не удается, потому что задача А все еще
/* Переключение на другую задачу не может произойти, когда Для возобновления работы планировщи- является его владельцем. Соответственно, пока
выполняется код, окруженный вызовом taskENTER_CRITICAL()
и taskEXIT_CRITICAL(). ка служит API-функция xTaskResumeAll(), ее задача А выполняет операции с ресурсом, за-
Прерывания здесь могут происходить, только если микро- прототип: дача Б не может получить к нему доступ и пе-
контроллер допускает вложение прерываний. Прерывание
выполнится, если его приоритет выше константы configMAX_ реходит в блокированное состояние (рис. 1в).
SYSCALL_INTERRUPT_PRIORITY. Однако такие прерывания не portBASE_TYPE xTaskResumeAll( void ); Задача А до конца завершает операции с ресур-
могут вызывать FreeRTOS API-функции. */
PORTA |= 0x01; сом и возвращает мьютекс обратно (рис. 1г).
/* Неатомарная операция чтение/модификация/запись завершена. Это приводит к разблокировке задачи Б, теперь
Сразу после этого выходим из критической секции. */
taskEXIT_CRITICAL(); Возвращаемое значение может быть равно: она получает доступ к ресурсу (рис. 1д). При
• pdTRUE — означает, что переключение завершении действий с ресурсом задача Б обя-
контекста произошло сразу после возоб- зана отдать мьютекс обратно (рис. 1е).
Рассматривая пример выше, следует от- новления работы планировщика. Легко заметить, что мьютексы и двоичные
метить, что если внутри критической сек- • pdFALSE — во всех остальных случаях. семафоры очень похожи в использовании.
ции произойдет прерывание с приоритетом Возможен вложенный вы- Отличие заключается в том, что мьютекс по-
выше configMAX_SYSCALL_INTERRUPT_ з о в A P I - ф у н к ц и й v Ta s k S u s p e n d A l l ( ) сле захвата обязательно должен быть возвра-
PRIORITY, которое, в свою очередь, обратит- и xTaskResumeAll(). При этом ядро автома- щен, иначе другие задачи не смогут получить
ся к порту PORTA, то принцип взаимного ис- тически подсчитывает глубину вложенности. доступ к разделяемому ресурсу. Двоичный
ключения доступа к ресурсу будет нарушен. Работа планировщика будет возобновлена, семафор, используемый в целях синхрони-
если глубина вложенности станет равна 0. зации выполнения задач (и прерываний), на-
Приостановка/запуск планировщика Этого можно достичь, если каждому вызо- оборот — не должен возвращаться задачей,
Еще один способ реализации критической ву vTaskSuspendAll() будет соответствовать которая его захватила.
секции в FreeRTOS — это приостановка рабо- вызов xTaskResumeAll(). Важным моментом является то, что непосред-
ты планировщика (suspending the scheduler). ственно мьютекс не защищает ресурс от одно-
В отличие от реализации критической Мьютексы временного доступа нескольких задач. Вместо
секции с помощью запрета прерываний (ма- этого реализация всех задач в системе должна
кросы taskENTER_CRITICAL() и taskEXIT_ Взаимное исключение называют также быть выполнена так, чтобы перед инструкцией
CRITICAL()), которые защищают участок мьютексом (mutex — MUTual EXclusion), доступа к ресурсу следовал вызов API-функции
кода от доступа как из задач, так и из преры- этот термин чаще используется в операцион- захвата соответствующего мьютекса. Эта обя-
ваний, реализация с помощью приостановки ных системах Windows и Unix-подобных [5]. занность ложится на программиста.
планировщика защищает участок кода толь- Мьютекс во FreeRTOS представляет собой
ко от доступа из другой задачи. Все прерыва- специальный тип двоичного семафора, ко- Работа с мьютексами
ния микроконтроллера остаются разрешены. торый используется для реализации совмест- Мьютекс представляет собой специальный
Операция запуска планировщика после при- ного доступа к ресурсу двух или большего вид семафора, поэтому доступ к мьютексу
остановки выполняется существенно дольше числа задач. При использовании в качестве осуществляется так же, как и к семафору: с по-
макроса taskEXIT_CRITICAL(), это немаловаж- механизма взаимного исключения мьютекс мощью дескриптора (идентификатора) мью-
но с точки зрения сокращения времени выпол- можно представить как семафор, относя- текса — переменной типа xSemaphoreHandle.
нения критических секций в программе. Этот щийся к ресурсу, доступом к которому необ- Для того чтобы API-функции для рабо-
момент следует учитывать при выборе способа ходимо управлять. ты с мьютексами были включены в про-
организации критических секций. В отличие от семафора мьютекс во FreeRTOS грамму, необходимо установить макро-
Приостановка планировщика выполняется предоставляет механизм наследования при- определение configUSE_MUTEXES в файле
API-функцией vTaskSuspendAll(). Ее прото- оритетов, о котором будет рассказано ниже. FreeRTOSConfig.h равным «1».
тип: Также следует отметить, что использование Мьютекс должен быть явно создан перед
мьютекса из тела обработчика прерыва- первым его использованием. API-функция
void vTaskSuspendAll( void ); ния невозможно. xSemaphoreCreateMutex() служит для созда-
Чтобы корректно получить доступ к ресур- ния мьютекса:
су, задача должна предварительно захватить
После вызова vTaskSuspendAll() плани- мьютекс, стать его владельцем. Когда владелец xSemaphoreHandle xSemaphoreCreateMutex( void );
ровщик останавливается, переключения семафора закончил операции с ресурсом, он
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include “FreeRTOS.h”
#include “task.h”
#include “semphr.h”
/*-----------------------------------------------------------*/
/* Точка входа. С функции main() начнется выполнение
е программы. */
short main( void )
{
/* Создание мьютекса. */
xMutex = xSemaphoreCreateMutex();
/* Создание задач, если мьютекс был успешно создан. */
if (xMutex != NULL) {
/* Создать два экземпляра одной задачи. Каждому
экземпляру задачи передать в качестве аргумента свою
строку. Приоритет задач задать разным, чтобы имело
место вытеснение задачи 1 задачей 2.
*/ xTaskCreate(prvPrintTask, “Print1”, 1000,
“Task 1 **************************************\r\n”, 1,
NULL);
xTaskCreate(prvPrintTask, “Print2”, 1000,
Рис. 1. Использование мьютекса для управления доступом к ресурсу “Task 2 ----------------------------------\r\n”, 2,
NULL);
/* Запуск планировщика. */
vTaskStartScheduler();
Возвращаемое значение — дескриптор тексу. Если мьютекс не создан по причине от- }
мьютекса, он должен быть сохранен в пере- сутствия достаточного объема памяти, воз- return 1;
}
менной для дальнейшего обращения к мью- вращаемым значением будет NULL.
Рис. 2. Результат работы учебной программы № 1 без использования мьютекса Рис. 3. Результат работы учебной программы № 1 с применением мьютекса
Как видно по результатам работы (рис. 2), мьютекса. Рассмотрим пример рекурсивного xSemaphoreHandle xRecursiveMutex;
/* ... */
совместный доступ к консоли без примене- захвата обычного мьютекса: xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
ния какого-либо механизма взаимного ис- /* ... */
ключения приводит к тому, что некоторые xSemaphoreHandle xMutex; /* Функция, реализующая задачу. */
/* ... */
сообщения, которые выводят на консоль за- xMutex = xSemaphoreCreateMutex();
void vTask(void *pvParameters) {
for (;;) {
дачи, оказываются повреждены. /* ... */ /* Захват мьютекса */
Теперь защитим консоль от одновремен- /* Функция, реализующая задачу. */
xSemaphoreTakeRecursive( xRecursiveMutex, portMAX_DELAY );
/* Действия с ресурсом */
ного доступа с помощью мьютекса, заменив void vTask(void *pvParameters) { /* ... */
for (;;) {
реализацию функции prvNewPrintString() /* Захват мьютекса */
/* Вызов функции, которая выполняет операции с этим же
ресурсом. */
на следующую: xSemaphoreTake( xMutex, portMAX_DELAY ); vSomeFunction();
/* Действия с ресурсом */ /* Действия с ресурсом */
/* ... */ /* ... */
/* Функция посимвольно выводит строку на консоль. /* Вызов функции, которая выполняет операции с этим же /* Действия с ресурсом закончены. Освободить мьютекс. */
Консоль, как ресурс, защищена от совместного доступа ресурсом. */ xSemaphoreGiveRecursive( xRecursiveMutex );
из нескольких задач с помощью мьютекса. */ vSomeFunction(); }
static void prvNewPrintString(const portCHAR *pcString) { /* Действия с ресурсом */ }
portCHAR *p; /* ... */
int i; /* Действия с ресурсом закончены. Освободить мьютекс. */ /* Функция, которая вызывается из тела задачи vTask*/
xSemaphoreGive( xMutex ); void vSomeFunction(void) {
/* Указатель — на начало строки */ } /* Захватить тот же самый мьютекс.
p = pcString; } При этом состояние мьютекса никоим образом не изменится.
/* Захватить мьютекс. Время ожидания в блокированном Задача не заблокируется, действия с ресурсом внутри этой
состоянии, если мьютекс недоступен, сколь угодно долго. /* Функция, которая вызывается из тела задачи vTask*/ функции будут выполнены. */
Возвращаемое значение xSemaphoreTake() должно проверяться, void vSomeFunction(void) { if (xSemaphoreTakeRecursive( xRecursiveMutex, portMAX_DELAY
если указано время пребывания в блокированном состоянии, /* Захватить тот же самый мьютекс. ) == pdTRUE ) {
отличное от portMAX_DELAY */ Т. к. тайм-аут не указан, то задача «зависнет» в ожидании, /* Действия с ресурсом внутри функции */
xSemaphoreTake( xMutex, portMAX_DELAY ); { пока мьютекс не освободится. /* ... */
/* Пока не дошли до нулевого символа — конца строки. */ Однако это никогда не произойдет! */ /* Освободить мьютекс */
while (*p) { if (xSemaphoreTake( xMutex, portMAX_DELAY ) == pdTRUE ) xSemaphoreGiveRecursive( xRecursiveMutex );
/* Вывод на консоль символа, на который ссылается указатель. */ { }
putchar(*p); /* Действия с ресурсом внутри функции */ }
/* Указатель — на следующий символ в строке. */ /* ... */
p++; /* Освободить мьютекс */
/* Вывести содержимое буфера экрана на экран. */ xSemaphoreGive( xMutex );
fflush(stdout); }
/* Небольшая пауза */ } В этом случае программа будет работать
for (i = 0; i < 10000; i++) ; корректно. При повторном захвате мьютекса
}
} API-функцией xSemaphoreTakeRecursive() за-
/* Когда вывод ВСЕЙ строки на консоль закончен, Такое использование обычного мьютекса дача не перейдет в блокированное состояние,
освободить мьютекс. Иначе другие задачи не смогут
обратиться к консоли! */ приведет к краху программы. При попытке и эта же задача останется владельцем мью-
xSemaphoreGive( xMutex ); повторно захватить мьютекс внутри функ- текса. Вместо этого увеличится на единицу
}
ции vSomeFunction() задача vTask перейдет внутренний счетчик, который определяет,
в блокированное состояние, пока мьютекс сколько операций «захват» было применено
Теперь при выполнении учебной программы не будет возвращен. Другие задачи смогут вы- к мьютексу, действия с ресурсом внутри функ-
№ 1 сообщения от разных задач не накладыва- полнить возврат мьютекса только после того, ции vSomeFunction() будут выполнены, так
ются друг на друга: совместный доступ к ресур- как сами его захватят. Однако мьютекс уже за- как задача vTask остается владельцем мьютек-
су (консоли) организован правильно (рис. 3). хвачен, поэтому задача vTask заблокируется са. При освобождении мьютекса (при вызове
на бесконечно долгое время («зависнет»). API-функции xSemaphoreGiveRecursive())
Рекурсивные мьютексы Если при повторном вызове API-функции из тела одной и той же задачи внутренний
xSemaphoreTake() было указано конечное счетчик уменьшается на единицу. Когда этот
Помимо обычных мьютексов, рассмотрен- время тайм-аута, «зависания» задачи не про- счетчик станет равен нулю, это будет озна-
ных выше, FreeRTOS поддерживает также изойдет. Вместо этого действия с ресурсом, чать, что текущая задача больше не являет-
рекурсивные мьютексы (Recursive Mutexes) выполняемые внутри функции, никогда ся владельцем мьютекса и теперь он может
[6]. Их основное отличие от обычных мью- не будут произведены, что также являет- быть захвачен другой задачей.
тексов заключается в том, что они коррек- ся недопустимой ситуацией. Таким образом, каждому вызову API-
тно работают при вложенных операциях за- Когда программа проектируется так, что ф у н к ц и и xSemaphoreTakeRecursive()
хвата и освобождения мьютекса. Вложенные операции захват/освобождение мьютекса яв- внутри тела одной и той же задачи дол-
операции захвата/освобождения мьютекса ляются вложенными, следует использовать жен соответствовать вызов API-функции
допускаются только в теле задачи-владельца рекурсивные мьютексы: xSemaphoreGiveRecursive().
Инверсия приоритетов
Вернемся к рассмотрению учебной программы № 1. Возможная
последовательность выполнения задач приведена на рис. 4. Такая
последовательность имела бы место, если во FreeRTOS не был бы Рис. 5. Наихудший случай влияния инверсии приоритетов
реализован механизм наследования приоритетов, о котором будет
рассказано ниже.
Пусть в момент времени (1) низкоприоритетная задача 1 вытес- наихудшим, так как ко времени, когда высокоприоритетная задача
нила задачу Бездействие, так как закончился период пребывания ожидает освобождения мьютекса, будет добавлено время выполне-
задачи 1 в блокированном состоянии (рис. 4). Задача 1 захватывает ния среднеприоритетных задач (рис. 5).
мьютекс (становится его владельцем) и начинает посимвольно вы- Низкоприоритетная задача стала владельцем мьютекса ранее.
водить свою строку на дисплей (2). В момент времени (3) разбло- Происходит некоторое событие, за обработку которого отвечает высо-
кируется высокоприоритетная задача 2, при этом она вытесняет коприоритетная задача. Она разблокируется и пытается захватить мью-
задачу 1, когда та еще не закончила вывод строки на дисплей. Задача текс (1), это ей не удается, и она блокируется — момент времени (2)
2 пытается захватить мьютекс, однако он уже захвачен задачей 1, на рис. 5. Управление снова возвращается низкоприоритетной задаче,
поэтому задача 2 блокируется в ожидании, когда мьютекс станет которая в момент времени (3) вытесняется задачей, приоритет кото-
доступен. Управление снова получает задача 1, она завершает вы- рой выше (среднеприоритетной задачей). Среднеприоритетная задача
вод строки на дисплей — операция с ресурсом завершена. Задача может выполняться продолжительное время, в течение которого вы-
1 возвращает мьютекс обратно — мьютекс становится доступен сокоприоритетная будет ожидать, пока мьютекс не будет освобожден
(момент времени (4)). Как только мьютекс становится доступен, низкоприоритетной задачей (4). Время реакции на событие при этом
разблокируется задача 2, которая ожидала его освобождения. Задача значительно удлиняется — величина dT на рис. 5.
2 захватывает мьютекс (становится его владельцем) и выводит свою В итоге инверсия приоритетов может значительно ухудшить время
строку на дисплей. Приоритет задачи 2 выше, чем у задачи 1, поэто- реакции микроконтроллерной системы на внешние события.
му задача 2 выполняется все время, пока полностью не выведет Для уменьшения (но не полного исключения) негативного влияния
свою строку на дисплей, после чего она отдает мьютекс обратно инверсии приоритетов во FreeRTOS реализован механизм наследова-
и блокируется на заданное API-функцией vTaskDelay() время — мо- ния приоритетов (Priority Inheritance). Его работа заключается во вре-
мент времени (5). Задача 1 снова получает управление, но на непро- менном увеличении приоритета низкоприоритетной задачи-владельца
должительное время — пока также не перейдет в блокированное мьютекса до уровня приоритета высокоприоритетной задачи, которая
состояние, вызвав vTaskDelay(). в данный момент пытается захватить мьютекс. Когда низкоприори-
Учебная программа № 1 и рис. 4 демонстрируют одну из возмож- тетная задача освобождает мьютекс, ее приоритет уменьшается до зна-
ных проблем, возникающих при использовании мьютексов для реа- чения, которое было до повышения. Говорят, что низкоприоритетная
лизации механизма взаимного исключения, — проблему инверсии задача наследует приоритет высокоприоритетной задачи.
приоритетов (Priority Inversion) [7]. На рис. 4 представлена ситуация, Рассмотрим работу механизма наследования приоритетов на примере
когда высокоприоритетная задача 2 вынуждена ожидать, пока низ- программы с высоко-, средне- и низкоприоритетной задачами (рис. 6).
коприоритетная задача 1 завершит действия с ресурсом и возвратит
мьютекс обратно. То есть на некоторое время фактический приори-
тет задачи 2 оказывается ниже приоритета задачи 1: происходит ин-
версия приоритетов.
В реальных программах инверсия приоритетов может оказывать
еще более негативное влияние на выполнение высокоприоритетных
задач. Рассмотрим пример. В программе могут существовать также
задачи со «средним» приоритетом — ниже, чем у высокоприоритет-
ной, которая ожидает освобождения мьютекса, но выше, чем у низ-
коприоритетной, которая в данный момент захватила мьютекс и вы-
полняет действия с разделяемым ресурсом. Среднеприоритетные Рис. 6. Уменьшение влияния инверсии приоритетов
задачи могут разблокироваться на протяжении интервала, когда низ- при работе механизма наследования приоритетов
коприоритетная задача владеет мьютексом. Такой сценарий является
Рис. 7. Результат работы учебной программы № 2 Рис. 8. Взаимная блокировка двух задач
Функция vApplicationTickHook() разделяющие общий ресурс, обращаются /* Создать задачу-сторож. Только она будет иметь
к задаче-сторожу, используя безопасные ме- непосредственный доступ к консоли. */
xTaskCreate(prvStdioGatekeeperTask, “Gatekeeper”, 1000, NULL,
Прежде чем продолжить изучение меха- ханизмы межзадачного взаимодействия 0, NULL);
низмов взаимного исключения, стоит об- FreeRTOS. Непосредственно действия с ре-
/* Запуск планировщика. */
ратить внимание на еще одну возможность сурсом выполняет задача-сторож. vTaskStartScheduler();
FreeRTOS. Как известно, подсистема вре- В отличие от мьютексов, работать с кото- }
return 0;
мени FreeRTOS [1, № 4] основывается на си- рыми могут только задачи, к задаче-сторожу }
стемном кванте времени. По прошествии могут обращаться как задачи, так и обработ- /*-----------------------------------------------------------*/
каждого кванта времени ядро FreeRTOS вы- чики прерываний. static void prvStdioGatekeeperTask(void *pvParameters) {
полняет внутренние системные действия, Рассмотрим использование задачи- char *pcMessageToPrint;
связанные как с работой планировщика, так сторожа на примере учебной программы /* Задача-сторож. Только она имеет прямой доступ к консоли.
и с отсчетом произвольных временных про- № 3. Как и в учебной программе № 1, здесь * Когда другие задачи “хотят” вывести строку на консоль,
они записывают указатель на нее в очередь.
межутков. разделяемым ресурсом выступает консоль. * Указатель из очереди считывает задача-сторож
Программисту предоставляется возмож- В программе созданы две задачи, каждая и непосредственно выводит строку */
for (;;) {
ность определить свою функцию, которая из которых выводит свое сообщение на кон- /* Ждать появления сообщения в очереди. */
будет вызываться каждый системный квант соль. Кроме того, сообщения выводит функ- xQueueReceive(xPrintQueue, &pcMessageToPrint, portMAX_
DELAY);
времени. Такая возможность может оказать- ция, вызываемая каждый системный квант /* Непосредственно вывести строку. */
ся полезной, например, для реализации меха- времени, это демонстрирует возможность printf(“%s”, pcMessageToPrint);
fflush(stdout);
низма программных таймеров. обращения к разделяемому ресурсу из тела /* Вернуться к ожиданию следующей строки. */
Чтобы задать свою функцию, которая бу- обработчика прерывания: }
}
дет вызываться каждый системный квант /*-----------------------------------------------------------*/
времени, необходимо в файле настроек ядра #include “FreeRTOS.h”
#include “task.h” /* Задача, которая автоматически вызывается каждый системный
FreeRTOSConfig.h задать макроопределение #include “semphr.h” квант времени.
conf igUSE_TICK_HOOK равным 1. Сама #include <stdlib.h> * Макроопределение configUSE_TICK_HOOK должно быть равно 1. */
#include <stdio.h> void vApplicationTickHook(void) {
функция должна содержаться в программе static int iCount = 0;
и иметь следующий прототип: /* Прототип задачи, которая выводит сообщения на консоль, portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
передавая их задаче-сторожу.
* Будет создано 2 экземпляра этой задачи */ /* Выводить строку каждые 200 квантов времени.
void vApplicationTickHook( void ); static void prvPrintTask(void *pvParameters); Строка не выводится напрямую, указатель на нее помещается
в очередь и считывается задачей-сторожем. */
/* Прототип задачи-сторожа */ iCount++;
static void prvStdioGatekeeperTask(void *pvParameters); if (iCount >= 200) {
/* Используется API-функция, предназначенная для вызова
Как и функция задачи Бездействие, /* Таблица строк, которые будут выводиться на консоль */ из обработчиков прерываний!!! */
функция vApplicationTickHook() являет- static char *pcStringsToPrint[] = { xQueueSendToFrontFromISR(xPrintQueue, &(pcStringsToPrint[2]),
“Task 1 ****************************************************\r\ &xHigherPriorityTaskWoken);
ся функцией-ловушкой или функцией об- n”, iCount = 0;
ратного вызова (callback function). Поэтому “Task 2 ----------------------------------------------------\r\n”, }
“Message printed from the tick hook interrupt ##############\r\n” }
в программе не должны встречаться явные }; /*-----------------------------------------------------------*/
вызовы этой функции.
/*-----------------------------------------------------------*/ static void prvPrintTask(void *pvParameters) {
Отсчет квантов времени во FreeRTOS int iIndexToString;
реализован за счет использования преры- /* Объявить очередь, которая будет использоваться для передачи
сообщений от задач и прерываний к задаче-сторожу. */ /* Будет создано 2 экземпляра этой задачи. В качестве параметра
вания от одного из аппаратных таймеров xQueueHandle xPrintQueue; при создании задачи выступает номер строки в таблице строк. */
микроконтроллера, вследствие чего функ- iIndexToString = (int) pvParameters;
int main(void) {
ция vApplicationTickHook() вызывается /* Создать очередь длиной макс. 5 элементов типа for (;;) {
из обработчика прерывания. Поэтому к ней “указатель на строку” */ /* Вывести строку на консоль. Но не напрямую, а передав
xPrintQueue = xQueueCreate(5, sizeof(char *)); указатель на строку задаче-сторожу.*/
предъявляются следующие требования: xQueueSendToBack(xPrintQueue, &(pcStringsToPrint[iIndexT
• Она должна выполняться как можно бы- /* Проверить, успешно ли создана очередь. */ oString]), 0);
if (xPrintQueue != NULL) {
стрее. /* Создать два экземпляра задачи, которые будут выводить /* Блокировать задачу на промежуток времени случайной
• Должна использовать как можно меньше строки на консоль, передавая их задаче-сторожу. длины: от 0 до 500 квантов. */
В качестве параметра при создании задачи передается vTaskDelay((rand() % 500));
стека. номер строки в таблице. Задачи создаются с разными /* Вообще функция rand() не является реентерабельной.
• Не должна содержать вызовы API- приоритетами. */ Однако в этой программе это неважно. */
xTaskCreate(prvPrintTask, “Print1”, 1000, (void *) 0, 1, NULL); }
функций, кроме предназначенных для вы- xTaskCreate(prvPrintTask, “Print2”, 1000, (void *) 1, 2, NULL); }
зова из обработчика прерывания (то есть
чьи имена заканчиваются на FromISR или
FROM_ISR).
Задачи-сторожа
(Gatekeeper tasks)
Выводы
Литература
Что представляет собой на сопрограммы. В этом случае программа – Набор операций с очередями ограничен
сопрограмма? будет представлять собой совокупность неза- по сравнению с набором операций для
висимых друг от друга и взаимодействую- задач.
В предыдущих публикациях [1] мы говори- щих друг с другом сопрограмм. – Сопрограмму после создания нельзя уни-
ли о FreeRTOS как о многозадачной операци- Сопрограммам по сравнению с задачами чтожить или изменить ее приоритет.
онной системе, и в центре нашего внимания присущ ряд существенных ограничений, по- 5. Ограничения в использовании:
находилась задача (task) — базовая единица этому использование сопрограмм оправдано – Внутри сопрограмм нельзя использовать
программы, работающей под управлением только в случаях, когда объема оперативной локальные переменные.
FreeRTOS. Речь шла о том, что программа, памяти оказывается недостаточно. Следует – Существуют строгие требования к ме-
работающая под управлением FreeRTOS, отметить, что в программе допускается со- сту вызова API-функций внутри сопро-
разбивается на совокупность задач. Задача вместное использование как задач, так и со- грамм.
представляет собой отдельный поток команд программ.
процессора и реализуется в виде функции Особенности сопрограмм: Экономия оперативной памяти
языка Си. Каждая задача отвечает за неболь- 1. Использование стека. Все сопрограммы при использовании сопрограмм
шую часть функциональности всей програм- в программе используют один и тот же
мы. Каждая задача выполняется независимо стек, это позволяет добиться значительной Оценим объем оперативной памяти, ко-
от остальных, но взаимодействует с осталь- экономии оперативной памяти по сравне- торый можно сэкономить, применяя сопро-
ными задачами через механизмы межзадач- нию с использованием задач, но налагает граммы вместо задач.
ного взаимодействия. ряд ограничений при программировании Пусть в качестве платформы выбран ми-
Начиная с версии v4.0.0 во FreeRTOS по- сопрограмм. кроконтроллер семейства AVR. Настройки
явилась поддержка сопрограмм (co-routines). 2. Планирование и приоритеты. Сопрограммы ядра FreeRTOS идентичны настройкам де-
Сопрограмма сходна с задачей, она также пред- в отличие от задач выполняются в режиме монстрационного проекта, который входит
ставляет собой независимый поток команд кооперативной многозадачности с приори- в дистрибутив FreeRTOS. Рассмотрим два
процессора, и ее можно использовать как базо- тетами. Кооперативная многозадачность случая. В первом случае вся функциональ-
вую единицу программы. То есть программа, в отношении сопрограмм автоматически ность программы реализована десятью зада-
работающая под управлением FreeRTOS, мо- устраняет проблему реентерабельности чами, во втором — десятью сопрограммами.
жет состоять из совокупности сопрограмм. функций, но негативно сказывается на вре- Оперативная память, потребляемая одной
Когда следует использовать сопрограммы? мени реакции микроконтроллерной систе- задачей, складывается из памяти стека и па-
Главное преимущество сопрограмм перед мы на внешние события. мяти, занимаемой блоком управления зада-
задачами — это то, что использование со- 3. Сочетание с задачами. Сопрограммы мо- чей. Для условий, приведенных выше, раз-
программ позволяет достичь значительной гут выполняться одновременно с задачами, мер блока управления задачей составляет
экономии оперативной памяти по сравне- которые обслуживаются планировщиком 33 байт, а рекомендованный минимальный
нию с использованием задач. с вытесняющей многозадачностью. При размер стека — 85 байт. Таким образом, име-
Каждой задаче для корректной работы этом задачи выполняются в первую очередь, ем 33+85 = 118 байт на каждую задачу. Для
ядро выделяет участок памяти, в которой и только если нет готовых к выполнению за- создания 10 задач потребуется 1180 байт.
размещаются стек задачи и структура управ- дач, процессор занят выполнением сопро- Оперативная память, потребляемая одной
ления задачей (Task Control Block). Размер грамм. Важно, что во FreeRTOS не существу- сопрограммой, складывается только из памя-
этого участка памяти за счет размещения ет встроенного механизма взаимодействия ти, занимаемой блоком управления сопро-
в нем стека оказывается значительным. Так между задачами и сопрограммами. граммой. Размер блока управления сопро-
как объем оперативной памяти в микрокон- 4. Примитивность. По сравнению с задача- граммой для данных условий равен 26 байт.
троллерах ограничен, то его может оказать- ми сопрограммы не допускают целый ряд Как упоминалось выше, стек для всех сопро-
ся недостаточно для размещения всех задач. операций. грамм общий, примем его равным рекомендо-
В таких случаях одним из возможных ре- – Операции с семафорами и мьютексами ванному, то есть 85 байт. Для создания 10 со-
шений будет замена всех (или части) задач не представлены для сопрограмм. программ потребуется 1026+85 = 345 байт.
Состояния сопрограммы
// Глобальная переменная:
unsigned int uGlobalVar;
граммы. Прототип API-функции crQUEUE_ Запись элемента в очередь (которая исполь- мощью API-функции crQUEUE_RECEIVE_
RECEIVE(): зуется только в сопрограммах) из обработчи- FROM_ISR(). Ее прототип:
ка прерывания осуществляется с помощью
void crQUEUE_RECEIVE(
xCoRoutineHandle xHandle,
API-функции crQUEUE_SEND_FROM_ISR(). portBASE_TYPE crQUEUE_RECEIVE_FROM_ISR(
xQueueHandle pxQueue,
xQueueHandle pxQueue, Ее прототип: void *pvBuffer,
void *pvBuffer, portBASE_TYPE * pxCoRoutineWoken
portTickType xTicksToWait, )
portBASE_TYPE *pxResult portBASE_TYPE crQUEUE_SEND_FROM_ISR(
) xQueueHandle pxQueue,
void *pvItemToQueue,
portBASE_TYPE xCoRoutinePreviouslyWoken Аргументы и возвращаемое значение:
)
Аргументы API-функции crQUEUE_ 1. pxQueue — дескриптор очереди, в которую
RECEIVE(): будет записан элемент. Дескриптор очере-
1. xHandle — дескриптор вызывающей со- Ее аргументы и возвращаемое значение: ди может быть получен при ее создании
программы. Автоматически передается 1. pxQueue — дескриптор очереди, в которую API-функцией xQueueCreate().
в функцию, реализующую сопрограмму, будет записан элемент. Дескриптор очере- 2. pvItemToQueue — указатель на область па-
в виде первого ее аргумента. ди может быть получен при ее создании мяти, в которую будет скопирован элемент
2. pxQueue — дескриптор очереди, из кото- API-функцией xQueueCreate(). из очереди. Объем памяти, на которую
рой будет прочитан элемент. Дескриптор 2. pvItemToQueue — указатель на элемент, ссылается указатель, должен быть не мень-
очереди может быть получен при ее созда- который будет записан в очередь. Размер ше размера одного элемента очереди.
нии API-функцией xQueueCreate(). элемента зафиксирован при создании оче- 3. pxCoRoutineWoken — указатель на пере-
3. pvBuffer — указатель на область памя- реди. Именно это количество байт будет менную, которая в результате вызова
ти, в которую будет скопирован элемент скопировано с адреса, на который ссылает- crQUEUE_RECEIVE_FROM_ISR() примет
из очереди. Участок памяти, на которую ся указатель pvItemToQueue. значение pdTRUE, если одна или несколько
ссылается указатель, должен быть не мень- 3. xCoRoutinePreviouslyWoken — этот аргу- сопрограмм ожидали возможности поме-
ше размера одного элемента очереди. мент необходимо устанавливать в pdFALSE, стить элемент в очередь и теперь разбло-
4. xTicksToWait — максимальное количе- если вызов API-функции crQUEUE_SEND_ кировались. Если таковых сопрограмм нет,
ство квантов времени, в течение которого FROM_ISR() является первым в обработчи- то значение *pxCoRoutineWoken останется
сопрограмма может пребывать в блоки- ке прерывания. Если же в обработчике пре- без изменений.
рованном состоянии, если очередь пуста рывания происходит несколько вызовов 4. Возвращаемое значение:
и считать элемент из очереди нет возмож- crQUEUE_SEND_FROM_ISR() (несколь- – pdTRUE, если элемент был успешно про-
ности. Для представления времени в мил- ко элементов помещается в очередь), читан из очереди;
лисекундах следует использовать макро- то аргумент xCoRoutinePreviouslyWoken – pdFALSE — в противном случае.
определение portTICK_RATE_MS [1, № 4]. следует устанавливать в значение, кото-
Задание xTicksToWait равным 0 приведет рое было возвращено предыдущим вы- Учебная программа № 2
к тому, что сопрограмма не перейдет в бло- зовом crQUEUE_SEND_FROM_ISR().
кированное состояние, если очередь пуста, Этот аргумент введен для того, чтобы Рассмотрим учебную программу № 2, в ко-
и управление будет возвращено сразу же. в случае, когда несколько сопрограмм ожи- торой продемонстрирован обмен инфор-
5. pxResult — указатель на переменную типа дают появления данных в очереди, только мацией между сопрограммами и обработ-
portBASE_TYPE, в которую будет поме- одна из них выходила из блокированного чиками прерываний. В программе имеются
щен результат выполнения API-функции состояния. 2 обработчика прерывания и 2 сопрограммы,
crQUEUE_RECEIVE(). Может принимать 4. Возвращаемое значение. Равно pdTRUE, которые обмениваются друг с другом сооб-
следующие значения: если в результате записи элемента в оче- щениями, помещая их в очередь (в програм-
– pdPASS — означает, что данные успешно редь разблокировалась одна из сопрограмм. ме созданы 3 очереди). В демонстрационных
прочитаны из очереди. Если определено В этом случае необходимо выполнить пере- целях в качестве прерываний используются
время тайм-аута (параметр xTicksToWait ключение на другую сопрограмму после программные прерывания MS-DOS, а слу-
не равен 0), то возврат значения pdPASS выполнения обработчика прерывания. жебная сопрограмма выполняет вызов этих
говорит о том, что новый элемент Чтение элемента из очереди (которая ис- прерываний.
в очереди появился до истечения вре- пользуется только в сопрограммах) из обра- В графическом виде обмен информацией
мени тайм-аута. ботчика прерывания осуществляется с по- в учебной программе № 2 показан на рис. 5.
– Код ошибки errQUEUE_FULL, опреде-
ленный в файле ProjDefs.h.
Как и при записи элемента в очередь
из тела сопрограммы, при чтении элемента
из очереди также нет возможности заблоки-
ровать сопрограмму на бесконечный проме-
жуток времени.
Запись/чтение в очередь
из обработчика прерывания
Для организации обмена между обработ-
чиками прерываний и сопрограммами пред-
назначены API-функции crQUEUE_SEND_
FROM_ISR() и crQUEUE_RECEIVE_FROM_
ISR(), вызывать которые можно только
из обработчиков прерываний. Причем оче-
редь можно использовать только в сопро- Рис. 5. Обмен сообщениями между сопрограммами и обработчиками прерываний в учебной программе № 2
граммах (но не в задачах).
Текст учебной программы № 2: /* Получить сообщение из Очереди № 2 от Сопрограммы № 1. /* Точка входа. С функции main() начнется выполнение про-
* Если очередь пуста — заблокироваться на время граммы. */
portMAX_DELAY квантов. */ int main(void) {
#include <stdlib.h>
crQUEUE_RECEIVE( /* Создать 3 очереди для хранения элементов типа unsigned long.
#include <stdio.h>
xHandle, * Длина каждой очереди — 3 элемента. */
#include <string.h>
xQueue2, xQueue1 = xQueueCreate(3, sizeof(unsigned long));
#include <dos.h>
(void *)&i, xQueue2 = xQueueCreate(3, sizeof(unsigned long));
#include "FreeRTOS.h"
portMAX_DELAY, xQueue3 = xQueueCreate(3, sizeof(unsigned long));
#include "task.h"
#include "queue.h" &xResult);
if (xResult == pdTRUE) { /* Создать служебную сопрограмму.
#include "portasm.h"
puts("CoRoutine 2 has received a message from CoRoutine 1."); * Приоритет = 1. */
#include "croutine.h"
} xCoRoutineCreate(vIntCoRoutine, 1, 0);
/* Передать это же сообщение в обработчик прерывания /* Создать сопрограммы № 1 и № 2 как экземпляры одной
/* Дескрипторы очередей — глобальные переменные */
№ 2 через Очередь № 3. */ сопрограммы.
xQueueHandle xQueue1;
crQUEUE_SEND( * Экземпляры различаются целочисленным параметром,
xQueueHandle xQueue2;
xHandle, * который передается сопрограмме при ее создании.
xQueueHandle xQueue3;
xQueue3, * Приоритет обеих сопрограмм = 2. */
(void *)&i, xCoRoutineCreate(vTransferCoRoutine, 2, 1);
/* Служебная сопрограмма. Вызывает программные прерывания.
portMAX_DELAY , xCoRoutineCreate(vTransferCoRoutine, 2, 2);
* Приоритет = 1.*/
void vIntCoRoutine( xCoRoutineHandle xHandle, unsigned &xResult);
if (xResult == pdTRUE) { /* Связать прерывания MS-DOS с соответствующими об-
portBASE_TYPE uxIndex ) {
puts("CoRoutine 2 has sent a message to Interrupt 1."); работчиками прерываний. */
crSTART( xHandle );
} _dos_setvect(0x82, vReceiveInterruptHandler);
for(;;) {
} _dos_setvect(0x83, vSendInterruptHandler);
/* Эта инструкция сгенерирует прерывание № 1. */
__asm {int 0x83} }
crEND(); /* Запуск планировщика. */
/* Заблокировать сопрограмму на 500 мс */
} vTaskStartScheduler();
crDELAY(xHandle, 500);
/*-----------------------------------------------------------*/ /* При нормальном выполнении программа до этого места
/* Эта инструкция сгенерирует прерывание № 2. */
"не дойдет" */
__asm {int 0x82}
for (;;) ;
/* Заблокировать сопрограмму на 500 мс */
/* Обработчик Прерывания 1*/ }
crDELAY(xHandle, 500);
static void __interrupt __far vSendInterruptHandler( void ) /*-----------------------------------------------------------*/
}
crEND(); {
static unsigned long ulNumberToSend; /* Функция, реализующая задачу Бездействие,
}
должна присутствовать в программе и содержать вызов
/*-----------------------------------------------------------*/
if (crQUEUE_SEND_FROM_ISR( xQueue1, vCoRoutineSchedule() */
&ulNumberToSend, void vApplicationIdleHook(void) {
/* Функция, реализующая Сопрограмму № 1 и Сопрограмму № 2,
pdFALSE ) == pdPASS) { /* Так как задача Бездействие не выполняет других действий,
* то есть будет создано два экземпляра этой сопрограммы. */
puts("Interrupt 1 has sent a message!"); то вызов vCoRoutineSchedule() размещен внутри бесконеч-
void vTransferCoRoutine( xCoRoutineHandle xHandle, unsigned
} ного цикла.*/
portBASE_TYPE uxIndex ) {
} for (;;) {
static long i;
/*-----------------------------------------------------------*/ vCoRoutineSchedule();
portBASE_TYPE xResult;
}
crSTART( xHandle ); /* Обработчик Прерывания 2*/ }
for(;;) { static void __interrupt __far vReceiveInterruptHandler( void )
/* Если выполняется Сопрограмма № 1*/ {
static portBASE_TYPE pxCoRoutineWoken ;
if (uxIndex == 1) {
static unsigned long ulReceivedNumber; По результатам работы учебной програм-
/* Получить сообщение из Очереди № 1 от Прерывания № 1.
* Если очередь пуста — заблокироваться на время мы (рис. 6) можно проследить, как сообщение
/* Аргумент API-функции crQUEUE_RECEIVE_FROM_ISR(),
portMAX_DELAY квантов */
который устанавливается в pdTRUE, генерируется сначала в Прерывании № 1, за-
crQUEUE_RECEIVE(
xHandle, если операция с очередью разблокирует более тем передается в Сопрограмму № 1 и далее —
высокоприоритетную сопрограмму.
Перед вызовом crQUEUE_RECEIVE_FROM_ISR() в Сопрограмму № 2, которая в свою очередь
xQueue1, следует установить в pdFALSE. */ отсылает сообщение Прерыванию № 2.
(void *)&i, pxCoRoutineWoken = pdFALSE;
portMAX_DELAY,
&xResult); if (crQUEUE_RECEIVE_FROM_ISR( Время реакции системы
if (xResult == pdTRUE) {
puts("CoRoutine 1 has received a message from Interrupt 1.");
xQueue3,
&ulReceivedNumber, на события
} &pxCoRoutineWoken ) == pdPASS) {
/* Передать это же сообщение в Очередь № 2 Сопрограмме № 2 */ puts("Interrupt 2 has received a message!\n");
crQUEUE_SEND( } Продемонстрируем недостаток кооператив-
xHandle, ной многозадачности по сравнению с вытес-
xQueue2, /* Проверить, нуждается ли в разблокировке более
(void *)&i, * высокоприоритетная сопрограмма, няющей с точки зрения времени реакции си-
portMAX_DELAY , * чем та, что была прервана прерыванием. */ стемы на прерывания. Для этого заменим реа-
&xResult); if( pxCoRoutineWoken == pdTRUE ) {
if (xResult == pdTRUE) { /* В текущей версии FreeRTOS нет средств для корректного лизацию служебной сопрограммы в учебной
puts("CoRoutine 1 has sent a message to CoRoutine 2."); * переключения на другую сопрограмму из тела обработчика программе № 2 vIntCoRoutine() на следующую:
} * прерывания! */
} }
/* Если выполняется Сопрограмма № 2 */ } /* Служебная сопрограмма. Вызывает программные прерывания.
else if (uxIndex == 2) { /*-----------------------------------------------------------*/ * Приоритет = 1.*/
void vIntCoRoutine( xCoRoutineHandle xHandle, unsigned
portBASE_TYPE uxIndex ) {
/* Все переменные должны быть объявлены как static. */
static long i;
crSTART( xHandle );
for(;;) {
/* Эта инструкция сгенерирует Прерывание № 1. */
__asm {int 0x83}
/* Грубая реализация задержки на какое-то время.
* Служебная сопрограмма при этом не блокируется! */
for (i = 0; i < 5000000; i++);
/* Эта инструкция сгенерирует Прерывание № 2. */
__asm {int 0x82}
/* Грубая реализация задержки на какое-то время.
* Служебная сопрограмма при этом не блокируется! */
for (i = 0; i < 5000000; i++);
}
crEND();
}
/*-----------------------------------------------------------*/
Рис. 6. Результат выполнения учебной программы № 2 В этом случае низкоприоритетная служеб-
ная сопрограмма не вызывает блокирующих
Выводы
Литература
FreeRTOS —
операционная система
для микроконтроллеров
Что представляет собой времени, чем использование API-функций Таймер переходит в активное состояние по-
программный таймер? vTaskDelay() и vTaskDelayUntil(), которые сле того, как к нему в явном виде применили
переводят задачу в блокированное состояние операцию запуска таймера. Таймер, находя-
В версии FreeRTOS V7.0.0 по сравне- на заданный промежуток времени [1, № 4]. щийся в активном состоянии, рано или поздно
нию с предыдущими версиями появилось вызовет свою функцию таймера. Промежуток
существенное нововведение — встроен- Принцип работы времени от момента запуска таймера до мо-
ная реализация программных таймеров. программного таймера мента, когда он автоматически вызовет свою
Программный таймер (далее по тексту — функцию, называется периодом работы тай-
таймер) во FreeRTOS — это инструмент, Как и прочие объекты ядра FreeRTOS, мера. Период таймера задается в момент его
позволяющий организовать выполнение программный таймер должен быть создан создания, но может быть изменен в ходе вы-
подпрограммы в точно заданные моменты до первого своего использования в програм- полнения программы. Момент времени, когда
времени. ме. При создании таймера с ним связывается таймер вызывает свою функцию, будем назы-
Часть программы, выполнение которой функция таймера, выполнение которой он вать моментом срабатывания таймера.
инициирует таймер, в программе представ- будет инициировать. Рассматривая таймер в упрощенном виде,
лена в виде функции языка Си, которую Таймер может находиться в двух состоя- можно сказать, что к таймеру, находящему-
в дальнейшем мы будем называть функцией ниях: пассивном (Dorman state) и активном ся в пассивном состоянии, применяют опе-
таймера. Функция таймера является функ- (Active state). рацию запуска, в результате которой таймер
цией обратного вызова (callback function). Пассивное состояние таймера характе- переходит из пассивного состояния в актив-
Механизм программных таймеров обеспечи- ризуется тем, что таймер в данный момент ное и начинает отсчитывать время. Когда
вает вызов функции таймера в нужные мо- не отсчитывает временной интервал. Таймер, с момента запуска таймера пройдет проме-
менты времени. находящийся в пассивном состоянии, никог- жуток времени, равный периоду работы тай-
Программные таймеры предоставляют да не вызовет свою функцию. Сразу после мера, то таймер сработает и автоматически
более удобный способ привязки выпол- создания таймер находится в пассивном со- вызовет свою функцию таймера (рис. 1).
нения программы к заданным моментам стоянии. К таймеру могут быть применены следую-
щие операции:
1. Создание таймера — приводит к выде-
лению памяти под служебную структуру
управления таймером, связывает таймер
с его функцией, которая будет вызываться
при срабатывании таймера, переводит тай-
мер в пассивное состояние.
2. Запуск — переводит таймер из пассивного
состояния в активное, таймер начинает от-
счет времени.
3. Останов — переводит таймер из активного
состояния в пассивное, таймер прекраща-
ет отсчет времени, функция таймера так
и не вызывается.
4. Сброс — приводит к тому, что таймер на-
чинает отсчет временного интервала с на-
чала. Подробнее об этой операции расска-
жем позже.
5. Изменение периода работы таймера.
6. Удаление таймера — приводит к освобож-
Рис. 1. Операции с таймером, состояния таймера и переходы между ними дению памяти, занимаемой служебной
структурой управления таймером.
Периодический таймер
Характеризуется тем, что после срабатывания таймера он остается
в активном состоянии и начинает отсчет временного интервала с на-
чала. Можно сказать, что после срабатывания периодический таймер
сам автоматически запускается заново. Таким образом, единожды
запущенный периодический таймер реализует циклическое выпол- б
нение функции таймера с заданным периодом (рис. 2б).
Периодический таймер применяют, когда необходимо организо-
вать циклическое, повторяющееся выполнение определенных дей-
ствий с точно заданным периодом.
Режим работы таймера задается в момент его создания и не может
быть изменен в процессе выполнения программы.
Реализация программных зывать задачей обслуживания программных де, где программные таймеры играют роль
таймеров во FreeRTOS таймеров. Вызов функции таймера выполняет совместно используемого ресурса.
именно задача обслуживания таймеров. Очередь команд недоступна для прямого
Функция таймера Задача обслуживания таймеров недоступ- использования в программе, доступ к ней
При срабатывании таймера автоматиче- на программисту напрямую (нет доступа к ее имеют только API-функции работы с тай-
ски происходит вызов функции таймера. дескриптору), она автоматически создается мерами. Рис. 5 поясняет процесс передачи
Функция таймера реализуется в программе во время запуска планировщика, если на- команды от прикладной задачи к задаче об-
в виде функции языка Си, она должна иметь стройки FreeRTOS предусматривают исполь- служивания программных таймеров.
следующий прототип: зование программных таймеров. Как видно на рис. 5, прикладная програм-
Большую часть времени задача обслужи- ма не обращается к очереди напрямую, вме-
void vTimerCallbackFunction( xTimerHandle xTimer ); вания таймеров пребывает в блокированном сто этого она вызывает API-функцию сброса
состоянии, она разблокируется лишь тогда, таймера, которая помещает команду сброса
когда будет вызвана API-функция работы таймера в очередь команд программных тай-
В отличие от функций, реализующих за- с таймерами или сработал один из таймеров. меров. Задача обслуживания программных
дачи и сопрограммы, функция таймера таймеров считывает эту команду из очереди
не должна содержать бесконечного цикла. Ограничение на вызов API-функций и непосредственно сбрасывает таймер.
Напротив, ее выполнение должно происхо- из функции таймера Важно, что таймер отсчитывает промежу-
дить как можно быстрее: Так как функция таймера вызывается ток времени с момента, когда была вызвана
из задачи обслуживания таймеров, а переход соответствующая API-функция, а не с мо-
void vTimerCallbackFunction( xTimerHandle xTimer ) последней в блокированное состояние и вы- мента, когда команда была считана из очере-
{
// Код функции таймера ход из него напрямую связан с отсчетом вре- ди. Это достигается за счет того, что в очередь
return; мени таймерами, то функция таймера никог- команд помещается информация о значении
}
да не должна пытаться заблокировать задачу счетчика системных квантов.
обслуживания прерываний, то есть вызывать
Единственный аргумент функции тай- блокирующие API-функции. Дискретность отсчета времени
мера — дескриптор таймера, срабатывание Например, функция таймера никогда Программные таймеры во FreeRTOS реали-
которого привело к вызову этой функции. не должна вызывать API-функции vTaskDelay() зованы на основе уже имеющихся объектов
Функция таймера является функцией обрат- и vTaskDelayUntil(), а также API-функции до- ядра: на основе задачи и очереди, управле-
ного вызова (Callback function), это значит, ступа к очередям, семафорам и мьютексам ние которыми осуществляет планировщик.
что ее вызов происходит автоматически. с ненулевым временем тайм-аута. Работа планировщика жестко привязана к си-
Программа не должна содержать явные вы- стемному кванту времени. Поэтому нет ни-
зовы функции таймера. Дескриптор таймера Очередь команд таймеров чего удивительного в том, что програм-мные
автоматически копируется в аргумент функ- Для совершения операций запуска, оста- таймеры отсчитывают промежутки времени,
ции таймера при ее вызове и может быть ис- нова, сброса, изменения периода и удале- кратные одному системному кванту.
пользован в теле функции таймера для опе- ния таймеров во FreeRTOS предоставляется То есть минимальный промежуток време-
раций с этим таймером. набор API-функций, которые могут вызы- ни, который может быть отсчитан програм-
Указатель на функцию таймера задан в виде ваться из задач и обработчиков прерываний, мным таймером, составляет один системный
макроопределения tmrTIMER_CALLBACK. а также из функций таймеров. Вызов этих квант времени.
API-функций не воздействует напрямую
Задача обслуживания на задачу обслуживания таймеров. Вместо Эффективность реализации
программных таймеров этого он приводит к записи команды в оче- программных таймеров
Немаловажно и то, что механизм програм- редь, которую в дальнейшем мы будем на- Подведя промежуточный итог, можно вы-
мных таймеров фактически не является частью зывать очередью команд таймеров. Задача делить основные тезисы касательно реализа-
ядра FreeRTOS. Все программные таймеры обслуживания таймеров считывает команды ции программных таймеров во FreeRTOS:
в программе отсчитывают время и вызывают из очереди и выполняет их. 1. Для всех программных таймеров в про-
свои функции за счет того, что в программе Таким образом, очередь команд выступа- грамме используется одна-единственная
выполняется одна дополнительная сервисная ет средством безопасного управления про- задача обслуживания таймеров и одна-
задача, которую в дальнейшем мы будем на- граммными таймерами в многозадачной сре- единственная очередь команд.
2. Функция таймера выполняется в контексте типа xTIMER_MESSAGE, размер которой выполнит. Размер очереди зависит от ко-
задачи обслуживания таймеров, а не в кон- равен 8 байт. Пусть используется очередь личества вызовов API-функций для рабо-
тексте обработчика прерывания микрокон- длиной 10 команд, тогда для размещения их ты с таймерами во время, когда функция
троллера. в памяти потребуется 810 = 80 байт. В сумме обслуживания таймеров не выполняется.
3. Процессорное время не расходуется зада- получаем 58+80 = 138 байт. А именно когда:
чей обслуживания таймеров, когда проис- Каждый таймер в программе обслужива- • Планировщик еще не запущен или при-
ходит отсчет времени. Задача обслужива- ется с помощью структуры управления тай- остановлен.
ния таймеров получает управление, лишь мером xTIMER, ее размер составляет 34 байт. • Происходит несколько вызовов API-
когда истекает время, равное периоду ра- Так как таймеров в программе 10, то памяти функций для работы с таймерами из об-
боты одного из таймеров. потребуется 3410 = 340 байт. работчиков прерываний, так как когда
4. Использование программных таймеров Итого при условиях, оговоренных выше, процессор занят выполнением обработ-
не добавляет никаких вычислений в обра- для добавления в программу 10 програм- чика прерывания, ни одна задача не вы-
ботчик прерывания от аппаратного таймера мных таймеров потребуется 582+138+340 = полняется.
микроконтроллера, который используется = 1060 байт оперативной памяти. • Происходит несколько вызовов API-
для отсчета системных квантов времени. функций для работы с таймерами из за-
5. Программные таймеры реализованы Настройки FreeRTOS дачи (задач), приоритет которых выше,
на существующих механизмах FreeRTOS, для использования таймеров чем у задачи обслуживания таймеров.
поэтому использование программных тай- 4. conf igTIMER_TASK_STACK_DEPTH.
меров в программе повлечет минимальное Чтобы использовать программные тай- Задает размер стека задачи обслуживания
увеличение размера скомпилированной меры в своей программе, необходимо сде- таймеров. Задается не в байтах, а в словах,
программы. лать следующие настройки FreeRTOS. Файл равных разрядности процессора. Тип дан-
6. Программные таймеры пригодны лишь с исходным кодом программных тайме- ных слова, которое хранится в стеке, задан
для отсчета временных промежутков, крат- ров /Source/timers.c должен быть включен в виде макроопределения portSTACK_TYPE
ных одному системному кванту времени. в проект. Кроме того, в исходный текст про- в файле portmacro. h. Функция таймера вы-
граммы должен быть включен заголовочный полняется в контексте задачи обслужива-
Потребление оперативной памяти файл croutine.h, содержащий прототипы ния таймеров, поэтому размер стека зада-
при использовании таймеров API-функций для работы с таймерами: чи обслуживания таймеров определяется
потреблением памяти стека функциями
Оперативная память, задействованная #include “timers.h” таймеров.
для программных таймеров, складывается
из 3 составляющих: Работа с таймерами
1. Память, используемая задачей обслужива- Также в файле конфигурации
ния таймеров. Ее объем не зависит от ко- FreeRTOSConfig.h должны присутствовать Как и для объектов ядра, таких как зада-
личества таймеров в программе. следующие макроопределения: чи, сопрограммы, очереди и др., для работы
2. Память, используемая очередью команд 1. configUSE_TIMERS. Определяет, включе- с программным таймером служит дескрип-
программных таймеров. Ее объем также ны ли программные таймеры в конфигу- тор (handle) таймера.
не зависит от количества таймеров. рацию FreeRTOS: 1 — включены, 0 — ис- Дескриптор таймера представляет собой
3. Память, выделяемая для каждого вновь ключены. Помимо прочего определяет, переменную типа xTimerHandle. При соз-
создаваемого таймера. В ней размещается будет ли автоматически создана задача дании таймера FreeRTOS автоматически на-
структура управления таймером xTIMER. обслуживания таймеров в момент запуска значает ему дескриптор, который далее ис-
Объем этой составляющей пропорциона- планировщика. пользуется в программе для операций с этим
лен числу созданных в программе тайме- 2. configTIMER_TASK_PRIORITY. Задает при- таймером.
ров. оритет задачи обслуживания таймеров. Как Функция таймера автоматически получает
Рассчитаем объем памяти, который потре- и для всех задач, приоритет задачи обслужи- дескриптор таймера в качестве своего аргу-
буется для добавления в программу 10 про- вания таймеров может находиться в преде- мента. Для выполнения операций с тайме-
граммных таймеров. В качестве платформы лах от 0 до (configMAX_PRIORITIES–1). ром внутри функции этого таймера следует
выбран порт FreeRTOS для реального режи- Значение приоритета задачи обслужи- использовать дескриптор таймера, получен-
ма x86 процессора, который используется вания таймеров необходимо выбирать ный в виде аргумента.
в учебных программах в этом цикле статей. с осторожностью, учитывая требования Дескриптор таймера однозначно опреде-
Настройки ядра FreeRTOS идентичны на- к создаваемой программе. Например, если ляет таймер в программе. Тем не менее при
стройкам демонстрационного проекта, кото- задан наивысший в программе приоритет, создании таймера ему можно назначить
рый входит в дистрибутив FreeRTOS. то команды задаче обслуживания таймеров идентификатор. Идентификатор представ-
Память, используемая задачей обслужи- будут передаваться без задержек, а функция ляет собой указатель типа void*, что под-
вания таймеров, складывается из памяти, таймера будет вызываться сразу же, когда разумевает использование его как указате-
занимаемой блоком управления задачей время, равное периоду таймера, истекло. ля на любой тип данных. Идентификатор
tskTCB, — 70 байт и памяти стека, примем Наоборот, если задаче обслуживания тайме- таймера следует использовать лишь тогда,
его равным минимальному рекомендованно- ров назначен низкий приоритет, то переда- когда необходимо связать таймер с произ-
му configMINIMAL_STACK_SIZE = 256 слов ча команд и вызов функции таймера будут вольным параметром. Например, можно
(16‑битных), что равно 512 байт. В сумме по- задержаны по времени, если в данный мо- создать несколько таймеров с общей для
лучаем 70 + 512 = 582 байт. мент выполняется задача с более высоким них всех функцией таймера, а идентифика-
Память, используемая очередью ко- приоритетом. тор использовать внутри функции таймера
манд таймеров, складывается из памяти 3. conf igTIMER_QUEUE_LENGTH. Размер для определения того, срабатывание какого
для размещения блока управления очере- очереди команд — устанавливает макси- конкретно таймера привело к вызову этой
дью xQUEUE — 58 байт и памяти, в которой мальное число невыполненных команд, функции. Такое использование идентифи-
разместятся элементы очереди. Элемент оче- которые могут храниться в очереди, пре- катора будет продемонстрировано ниже
реди команд представляет собой структуру жде чем задача обслуживания таймеров их в учебной программе.
валентны. Две различные API-функции вве- • pdTRUE, если таймер находится в актив- обслуживания программных таймеров,
дены скорее для наглядности. Предполагается, ном состоянии. и в результате вызова API-функции в оче-
что API-функцию xTimerStart() следует при- • pdFALSE, если таймер находится в пассив- редь команд программных таймеров была
менять к таймеру в пассивном состоянии, ном состоянии. помещена команда, вследствие чего зада-
xTimerReset() — к таймеру в активном со- API-функция xTimerIsTimerActive() пред- ча обслуживания таймеров разблокиро-
стоянии. Однако это требование совершен- назначена для вызова только из тела задачи валась. В обработчике прерывания после
но необязательно, так как обе эти функции или функции таймера. вызова одной из вышеперечисленных API-
приводят к записи одной и той же команды функций необходимо отслеживать значе-
в очередь команд таймеров. Получение идентификатора таймера ние *pxHigherPriorityTaskWoken, и если
API-функция xTimerReset() предназначена При создании таймеру присваивается оно изменилось на pdTRUE, то необходимо
для вызова из тела задачи или функции тай- идентификатор в виде указателя void*, что выполнить принудительное переключение
мера. Существует версия этой API-функции, позволяет связать таймер с произвольной контекста задачи. Вследствие чего управле-
предназначенная для вызова из обработчика структурой данных. ние сразу же получит более высокоприори-
прерывания, о ней будет сказано ниже. API-функцию pvTimerGetTimerID() мож- тетная задача обслуживания таймеров.
но вызывать из тела функции таймера для
Изменение периода работы таймера получения идентификатора, в результате Учебная программа
Независимо от того, в каком состоянии срабатывания которого была вызвана эта
в данный момент находится таймер: в ак- функция таймера. Прототип API-функции Продемонстрировать использование про-
тивном или в пассивном, период его работы pvTimerGetTimerID(): граммных таймеров позволяет следующая
можно изменить посредством API-функции учебная программа, в которой происходит
xTimerChangePeriod(). Ее прототип: void *pvTimerGetTimerID( xTimerHandle xTimer ); создание, запуск, изменение периода, а также
удаление таймера.
portBASE_TYPE xTimerChangePeriod( xTimerHandle xTimer, В программе будет создан периодический
portTickType xNewPeriod, portTickType xBlockTime );
Аргументом является дескриптор таймера, таймер с периодом работы 1 с. Функция это-
идентификатор которого необходимо полу- го таймера каждый раз при его срабатывании
Аргументы и возвращаемое значение: чить. pvTimerGetTimerID() возвращает ука- будет увеличивать период работы на 1 секун-
1. xTimer — дескриптор таймера, полу- затель на сам идентификатор. ду. Кроме того, в программе будут созданы
ченный при его создании API-функцией 3 интервальных таймера с периодом работы
xTimerCreate(). Работа с таймерами 12 секунд каждый.
2. xNewPeriod — новый период работы тай- из обработчиков прерываний Сразу после запуска планировщика от-
мера, задается в системных квантах. счет времени начнут периодический таймер
3. xBlockTime — определяет время тайм- Есть возможность выполнять управление и первый интервальный таймер. Через 12 с,
аута — максимальное время нахождения таймерами из обработчиков прерываний ми- когда сработает первый интервальный тай-
вызывающей xTimerChangePeriod() задачи кроконтроллера. Для рассмотренных выше мер, его функция запустит второй интер-
в блокированном состоянии, если очередь API-функций xTimerStart(), xTimerStop(), вальный таймер, еще через 12 с функция вто-
команд полностью заполнена и нет воз- xTimerChangePeriod() и xTimerReset() су- рого интервального таймера запустит третий.
можности поместить в нее команду об из- ществуют версии, предназначенные для Функция третьего же интервального таймера
менении периода таймера. вызова из обработчиков прерываний: еще через 12 с удалит периодический таймер.
4. Возвращаемое значение — может прини- xTimerStartFromISR(), xTimerStopFromISR(), Таким образом, отсчет времени таймерами
мать два значения: xTimerChangePeriodFromISR() будет продолжаться 36 с. В моменты вызова
• pdFAIL — означает, что команда об из- и xTimerResetFromISR(). Их прототипы: функций таймеров на дисплей будет выво-
менении периода таймера так и не была диться время, прошедшее с момента запуска
помещена в очередь команд, и время portBASE_TYPE xTimerStartFromISR( xTimerHandle xTimer, планировщика.
portBASE_TYPE *pxHigherPriorityTaskWoken );
тайм-аута истекло. portBASE_TYPE xTimerStopFromISR( xTimerHandle xTimer, Исходный текст учебной программы:
• pdPASS — означает, что команда об из- portBASE_TYPE *pxHigherPriorityTaskWoken );
portBASE_TYPE xTimerChangePeriodFromISR( xTimerHandle
менении периода успешно помещена xTimer, portTickType xNewPeriod, portBASE_TYPE
#include <stdlib.h>
#include <conio.h>
в очередь команд. *pxHigherPriorityTaskWoken );
#include “FreeRTOS.h”
portBASE_TYPE xTimerResetFromISR( xTimerHandle xTimer,
API-функция xTimerChangePeriod() предна- portBASE_TYPE *pxHigherPriorityTaskWoken );
#include “task.h”
#include “timers.h”
значена для вызова из тела задачи или функ- /*-----------------------------------------------------------*/
ции таймера. Существует версия этой API-
/* Количество интервальных таймеров */
функции, предназначенная для вызова из обра- По сравнению с API-функциями, предна- #define NUMBER_OF_TIMERS 3
ботчика прерывания, о ней будет сказано ниже. значенными для вызова из задач, в версиях /* Целочисленные идентификаторы интервальных таймеров */
#define ID_TIMER_1 111
API-функций, предназначенных для вызова #define ID_TIMER_2 222
Получение текущего состояния таймера из обработчиков прерываний, произошли #define ID_TIMER_3 333
/*-----------------------------------------------------------*/
Для того чтобы узнать, в каком состоя- следующие изменения в их аргументах:
нии — в активном или в пассивном — в дан- 1. Аргумент, который задавал время тайм- /* Дескриптор периодического таймера */
xTimerHandle xAutoReloadTimer;
ный момент находится таймер, служит API- аута, теперь отсутствует, что объясняет- /* Массив дескрипторов интервальных таймеров */
функция xTimerIsTimerActive(). Ее прототип: ся тем, что обработчик прерывания — xTimerHandle xOneShotTimers[NUMBER_OF_TIMERS];
/* Массив идентификаторов интервальных таймеров */
не задача и не может быть заблокирован const unsigned portBASE_TYPE uxOneShotTimersIDs
portBASE_TYPE xTimerIsTimerActive( xTimerHandle xTimer ); на какое-то время. [NUMBER_OF_TIMERS] = { ID_TIMER_1, ID_TIMER_2,
ID_TIMER_3 };
2. П о я в и л с я д о п о л н и т е л ь н ы й а р г у - /* Период работы периодического таймера = 1 секунда */
мент pxHigherPriorityTaskWoken. API- unsigned int uiAutoReloadTimerPeriod = 1000 / portTICK_RATE_MS;
/*-----------------------------------------------------------*/
Аргументом API-функции является де- ф у н к ц и и у с т а н а в л и в а ю т з н ач е н и е
скриптор таймера, состояние которого необ- *pxHigherPriorityTaskWoken в pdTRUE, /* Функция периодического таймера.
* Является функцией обратного вызова.
ходимо выяснить. xTimerIsTimerActive() мо- если в данный момент выполняется за- * В программе не должно быть ее явных вызовов.
жет возвращать два значения: дача с приоритетом меньше, чем у задачи
Для этой цели вполне достаточно использовать дескриптор тайме- сделать вывод о том, какой конкретно таймер инициировал вызов
ра. В таком случае функция интервальных таймеров в модифициро- этой функции таймера.
ванной учебной программе примет вид: Результат выполнения модифицированной учебной программы
ничем не будет отличаться от приведенного на рис. 6, что подтверж-
/* Функция интервальных таймеров. дает, что для однозначной идентификации таймера вполне достаточ-
* Нескольким экземплярам интервальных таймеров соответствует одна-единственная функция.
* Эта функция автоматически вызывается при истечении времени любого из связанных с ней таймеров. но иметь его дескриптор.
* Для того чтобы выяснить, время какого таймера истекло, используется идентификатор таймера. */
void vOneShotTimersFunction(xTimerHandle xTimer) {
/* Различные действия в зависимости от того, какой таймер вызывал функцию */ Выводы
/* Сработал интервальный таймер 1? */
if (xTimer == xOneShotTimers[0]) {
/* Индикация работы + текущее время */ Подводя итог можно сказать, что их применение оправдано в слу-
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / чаях, когда к точности отмеряемых временных интервалов не предъ-
configTICK_RATE_HZ);
xTimerChangePeriod(xAutoReloadTimer, 6000, 0); является высоких требований, так как активность других задач в про-
/* Запустить интервальный таймер 2 */
грамме может существенно повлиять на точность работы програм-
xTimerStart(xOneShotTimers[1], 0); мных таймеров. Кроме того, немаловажным ограничением является
/* Сработал интервальный таймер 2? */
} else if (xTimer == xOneShotTimers[1]) {
дискретность работы таймеров величиной в один системный квант
/* Индикация работы + текущее время */ времени.
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() /
configTICK_RATE_HZ); В дальнейших публикациях речь пойдет о способах отладки про-
/* Запустить интервальный таймер 3 */ граммы, которая выполняется под управлением FreeRTOS. Внимание
xTimerStart(xOneShotTimers[2], 0);
/* Сработал интервальный таймер 3? */ будет сконцентрировано на:
} else if (xTimer == xOneShotTimers[2]) { • способах трассировки программы;
/* Индикация работы + текущее время */
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / • получении статистики выполнения в реальном времени;
configTICK_RATE_HZ); • способах измерения потребляемого задачей объема стека и спосо-
puts(“\n\r\t\t\t\tAbout to delete AutoReload timer!”);
fflush(); бах защиты от его переполнения. n
/* Удалить периодический таймер.
* После этого активных таймеров в программе не останется. */
xTimerDelete(xAutoReloadTimer, 0); Литература
}
}
1. Курниц А. FreeRTOS — операционная система для микроконтроллеров //
Компоненты и технологии. 2011. № 2–9.
Дескрипторы созданных интервальных таймеров хранятся в гло- 2. www.freertos.org
бальном массиве. Кроме того, дескриптор таймера, который привел 3. http://ru.wikipedia.org/wiki/FreeRTOS
к вызову функции таймера, передается в эту функцию в виде ее ар- 4. http://electronix.ru/forum/index.php?showforum=189
гумента. Поэтому выполняя сравнение аргумента функции таймера 5. http://sourceforge.net/projects/freertos/files/FreeRTOS/
с дескриптором, который хранится в глобальной переменной, можно 6. http://www.ee.ic.ac.uk/t.clarke/rtos/lectures/RTOSlec2x2bw.pdf