Вы находитесь на странице: 1из 66

96 компоненты микроконтроллеры

FreeRTOS —
операционная система
для микроконтроллеров
Это первая статья из цикла, посвященного операционной системе для ми-
кроконтроллеров FreeRTOS. Статья познакомит читателя с задачами, ко-
торые решают операционные системы (ОС) для микроконтроллеров (МК).
Освещены вопросы целесообразности применения, преимущества и недо-
статки, присущие ОС для МК. Представлены возможности FreeRTOS, опи-
Андрей Курниц саны ее особенности, а также приведена структура дистрибутива FreeRTOS
kurnits@stim.by с кратким описанием назначения входящих в него файлов и директорий.

Что такое ОС для МК? Во‑вторых, микроконтроллер, по сути, мотки электромотора. Гораздо чаще из МК
это однокристальный компьютер с сильно пытаются «выжать» все, на что он способен,
В нынешний век высоких технологий все ограниченными аппаратными ресурсами, а в микроконтроллерное устройство зало‑
профессионалы знакомы с термином «опе‑ хотя диапазон выпускаемых МК по про‑ жить все возможные функции. Количество
рационная система» (ОС). История ОС на‑ изводительности и объемам памяти очень функций-задач, одновременно выполняемых
чинается с 1960‑х годов. Первые ОС пред‑ широк. Встречаются как «карлики», напри‑ МК, может доходить до нескольких десятков.
назначались для больших ЭВМ, а впослед‑ мер 8‑разрядный ATtiny10 с 6 выводами, И вот тут-то и начинаются проблемы.
ствии — и для персональных компьютеров. 32 байт ОЗУ, 1 кбайт ПЗУ и производитель‑ Как организовать мультизадачность
Назначением ОС стало заполнение ниши ностью 12106 операций в секунду (12 MIPS), и поочередное выполнение каждой задачи?
между низкоуровневой аппаратурой и вы‑ так и «гиганты», например 32‑разрядный Как обеспечить запуск задачи через строго
сокоуровневыми программами, они предо‑ TMS320C28346 c 256 выводами, 512 кбайт ОЗУ определенные интервалы времени? Как пере‑
ставляют программам удобный интерфейс и производительностью 600106 операций дать информацию от одной задачи другой?
обращения к системным ресурсам, будь с плавающей точкой в секунду (600 MFLOPS). Обычно эти вопросы не встают перед про‑
то процессорное время, память или устрой‑ Тем не менее все МК имеют существенные ап‑ граммистом в начале разработки, а возникают
ства ввода/вывода. С тех пор технологии паратные ограничения, что предъявляет спец‑ где-то в середине, когда он запрограммировал
шагнули далеко вперед: целую вычислитель‑ ифические требования к ОСРВ для МК. большинство функций будущего устройства,
ную систему (процессор, память, устрой‑ Их основные особенности: используя изобретенные им самим средства
ства ввода/вывода) разместили на одном 1. Низкая производительность. «многозадачности». И тогда заказчик «про‑
кристалле — появились микроконтроллеры 2. Малый объем ОЗУ и ПЗУ. сит» добавить еще несколько «маленьких» де‑
(МК). В соответствии с древним изречени‑ 3. Отсутствие блока управления памятью талей в работу устройства, например сбор ста‑
ем «Природа не любит пустоты» удачная (Memory management unit, MMU), исполь‑ тистики работы и запись ее на какой-нибудь
концепция ОС не могла не быть применена зуемого большинством современных ОС, носитель… Знакомая ситуация?
и к микроконтроллерам. В настоящее время например Windows и UNIX-подобными.
создано и развивается множество ОС, ори‑ 4. Отсутствие аппаратных средств поддержки Преимущества ОСРВ для МК
ентированных на выполнение на МК [1, 6]. многозадачности (например, средств бы‑
Однако МК как платформа для выполнения строго переключения контекста). З де с ь н а   п о м о щ ь п р и ход и т О С Р В .
ОС имеет существенные отличия от совре‑ В‑третьих, микроконтроллер сам по себе Рассмотрим преимущества, которые полу‑
менных компьютеров. предназначен для выполнения низкоуровне‑ чил бы наш гипотетический программист,
Прежде всего, МК работает в режиме ре‑ вых задач, будь то опрос состояния кнопок, пе‑ заложив в основу программного обеспечения
ального времени, то есть время реакции ми‑ редача команды по I2C-интерфейсу или вклю‑ своего устройства ОСРВ:
кроконтроллерного устройства на внешнее чение обмотки электромотора. Программа 1. Многозадачность. ОСРВ предоставляет
событие должно быть строго меньше задан‑ для МК, как правило, обращается к перифе‑ программисту готовый, отлаженный ме‑
ной величины и должно быть сопоставимо рии напрямую, программист имеет полный ханизм многозадачности. Теперь каждую
со скоростью протекания внешних процессов. контроль над аппаратной частью, нет необ‑ отдельную задачу можно программиро‑
Типичный пример: время реакции на сраба‑ ходимости в посредниках между аппаратурой вать по отдельности так, как будто осталь‑
тывание датчика давления в промышленной и прикладной программой. Может показать‑ ных задач не существует. Например, мож‑
установке должно быть не более 5 мс, иначе ся, что операционная система для МК вообще но разработать архитектуру программы,
произойдет авария. Таким образом, ОС для не нужна, что любую программу можно на‑ то есть разбить ее на отдельные задачи
МК — это операционная система реально‑ писать и без ОС. На самом деле так оно и есть! и распределить их между командой про‑
го времени (ОСРВ). К ОСРВ предъявляются Но есть один нюанс: микроконтроллер ред‑ граммистов. Программисту не нужно за‑
жесткие временные требования в отличие ко используют только для опроса состояния ботиться о переключении между задачами:
от распространенных ОС общего назначения кнопок, только для передачи команды по I2C- за него это сделает ОСРВ в соответствии
(Windows, UNIX-подобные и др.). интерфейсу или только для включения об‑ с алгоритмом работы планировщика.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 2 '2011


микроконтроллеры компоненты 97

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 написана на языке Си, ассемблер‑ ния. Вытесняющая многозадачность пред‑
индикатор, то применение ОСРВ в таком ные вставки минимального объема применя‑ полагает, что любая выполняющаяся задача
устройстве будет непозволительным расто‑ ются лишь там, где невозможно применить с низким приоритетом прерывается готовой
чительством и приведет, в конечном счете, Си из-за специфики конкретной аппаратной к выполнению задачей с более высоким при‑
к удорожанию устройства. платформы. оритетом. Как только высокоприоритетная

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 2 '2011 www.kite.ru


98 компоненты микроконтроллеры

тетной задаче, что позволяет значительно со‑


Таблица 1. Список официальных портов FreeRTOS и средств разработки кратить время реакции системы на прерыва‑
Производитель Поддерживаемые семейства (ядра) Поддерживаемые средства разработки
ние, связанное с внешним событием.
Altera Nios II Nios II IDE, GCC
Для оценки затрат времени, вносимых
SAM3 (Cortex-M3) планировщиком FreeRTOS, можно срав‑
SAM7 (ARM7) нить два распространенных семейства МК:
Atmel SAM9 (ARM9) IAR, GCC, Keil, Rowley CrossWorks PIC и AVR. Затраты времени складываются
AT91
AVR32 UC3
из времени переключения контекста, когда
Cortus APS3 Cortus IDE, GCC планировщик определяет задачу для выпол‑
Energy Micro EFM32 (Cortex-M3) IAR нения в следующем кванте времени, и вре‑
Coldfire V2 мени сохранения/восстановления контекста,
Coldfire V1
Freescale другие Coldfire Codewarrior, GCC, Eclipse
когда текущее состояние задачи (регистры
HCS12 процессора) сохраняется/извлекается из сте‑
PPC405 & PPC440 ка (таблица 2). Замеры приведены для ком‑
Fujitsu
32 бит (например, MB91460)
Softune пиляторов MPLAB PIC18 compiler и WinAVR
16 бит (например, MB96340 16FX)
Luminary Micro / Keil, IAR, Code Red, CodeSourcery GCC,
соответственно, уровень оптимизации —
Все МК Stellaris на основе ядра Cortex-M3
Texas Instruments Rowley CrossWorks максимальный по скорости.
PIC32 Для того чтобы оценить объем ОЗУ, тре‑
PIC24 MPLAB C32, MPLAB C30,
Microchip
dsPIC MPLAB C18, wizC
PIC18 Таблица 2. Расход времени на переключение
V850 (32 бит) между задачами
NEC IAR
78K0R (16 бит)
LPC1700 (Cortex-M3) GCC, Rowley CrossWorks, IAR, Keil, Red

Время сохранения/
тактирования, МГц
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

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 2 '2011


микроконтроллеры компоненты 99

лишь тем, что поставляется под коммерче‑ 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

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 2 '2011 www.kite.ru


100 компоненты микроконтроллеры

Литература 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/

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 2 '2011


микроконтроллеры компоненты 109

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров

В предыдущей части статьи [1] читатель познакомился с операционной систе-


мой реального времени (ОСРВ) для микроконтроллеров (МК) FreeRTOS. Были
изложены достоинства и недостатки использования ОСРВ в основе про-
граммного обеспечения микроконтроллерных устройств. Произведена
оценка FreeRTOS с точки зрения потребления памяти и дополнительных за-
трат процессорного времени. В сокращенном виде была описана структура
дистрибутива FreeRTOS и назначение отдельных файлов, входящих в дис-
трибутив. Во второй части статьи будут затронуты как основы теории работы
ОСРВ в целом, так и продолжено изучение конкретной реализации ОСРВ
для МК — FreeRTOS. Уделено особое внимание задачам как базовой единице
Андрей Курниц программы для FreeRTOS. Приведен пример простейшей программы для МК
kurnits@stim.by AVR ATmega128, работающей под управлением FreeRTOS.

Основы работы ОСРВ выполнение трех задач. В реальном же про- одна задача. Говорят, что она находится
цессоре при работе ОСРВ выполнение задач в состоянии выполнения. Остальные зада-
Прежде чем говорить об особенностях носит периодический характер: каждая за- чи в этот момент не выполняются, ожидая,
FreeRTOS, следует остановиться на основных дача выполняется определенное время, после когда планировщик выделит каждой из них
принципах работы любой ОСРВ и пояснить чего процессор «переключается» на следую- процессорное время. Таким образом, задача
значение терминов, которые будут приме- щую задачу (рис. 2). может находиться в двух основных состоя-
няться в дальнейшем. Эта часть статьи будет Планировщик (Scheduler) — это часть ядра ниях: выполняться и не выполняться.
особенно полезна читателям, которые не зна- ОСРВ, которая определяет, какая из задач, го- Кроме того, что выполнение задачи может
комы с принципами, заложенными в ОСРВ. товых к выполнению, выполняется в данный быть приостановлено планировщиком при-
Основой ОСРВ является ядро (Kernel) опе- конкретный момент времени. Планировщик нудительно, задача может сама приостано-
рационной системы. Ядро реализует осново- может приостанавливать, а затем снова воз- вить свое выполнение. Это происходит в двух
полагающие функции любой ОС. В ОС об- обновлять выполнение задачи в течение всего случаях. Первый — это когда задача «хочет»
щего назначения, таких как Windows и Linux, ее жизненного цикла (то есть с момента соз- задержать свое выполнение на определенный
ядро позволяет нескольким пользователям дания задачи до момента ее уничтожения). промежуток времени (в таком случае она пере-
выполнять множество программ на одном Алгоритм работы планировщика ходит в состояние сна (sleep)). Второй — когда
компьютере одновременно. (Scheduling policy) — это алгоритм, по ко- задача ожидает освобождения какого-либо
Каждая выполняющаяся программа пред- торому функционирует планировщик для аппаратного ресурса (например, последова-
ставляет собой задачу (Task). Если ОС позволя- принятия решения, какую задачу выполнять тельного порта) или наступления какого-то
ет одновременно выполнять множество задач, в данный момент времени. Алгоритм работы события (event), в этом случае говорят, что за-
она является мультизадачной (Multitasking). планировщика в ОС общего назначения за- дача блокирована (block). Блокированная или
Большинство процессоров могут выпол- ключается в предоставлении каждой задаче «спящая» задача не нуждается в процессорном
нять только одну задачу в один момент вре- процессорного времени в равной пропорции. времени до наступления соответствующего
мени. Однако при помощи быстрого пере- Алгоритм работы планировщика в ОСРВ от- события или истечения определенного интер-
ключения между задачами достигается эф- личается и будет описан ниже. вала времени. Функции измерения интерва-
фект параллельного выполнения всех задач. Среди всех задач в системе в один мо- лов времени и обслуживания событий берет
На рис. 1 показано истинно параллельное мент времени может выполняться только на себя ядро ОСРВ.

Рис. 1. Истинно параллельное выполнение задач Рис. 2. Распределение процессорного времени между несколькими задачами в ОСРВ

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 3 '2011 www.kite.ru


110 компоненты микроконтроллеры

в течение которого планировщик не вмеши-


вается в выполнение задачи. По истечении
кванта времени планировщик получает воз-
можность приостановить текущую задачу
и возобновить следующую, готовую к вы-
полнению. Далее квант времени работы
планировщика будет называться систем-
ным квантом. Для отсчета системных кван-
тов в МК обычно используется прерывание
от таймера/счетчика. Системный квант ис-
пользуется как единица измерения интерва-
Рис. 3. Переключение между задачами, которые используют один и тот же аппаратный ресурс лов времени средствами ОСРВ.
Уменьшая продолжительность системного
кванта, можно добиться более быстрой реак-
Пример перехода задачи в блокированное ет задачу 2 и восстанавливает задачу 3 (6). ции программы на внешние события, однако
состояние показан на рис. 3. Задача 3 пытается получить доступ к тому же это приведет к увеличению частоты вызова
Задача 1  исполняется на  протяжении самому аппаратному ресурсу, который занят планировщика, что скажется на производи-
определенного времени (1). В момент вре- задачей 2. В результате чего задача 3 блокиру- тельности вычислительной системы в целом.
мени (2) планировщик приостанавливает ется — момент времени (7). Через некоторое Подводя итог, можно выделить три основ-
задачу 1 и возобновляет выполнение зада- время управление снова получает задача 2, ные функции ядра любой ОСРВ:
чи 2 (момент времени (3)). Во время свое- которая завершает работу с аппаратным ре- 1. Работа планировщика, благодаря которой
го выполнения (4) задача 2  захватывает сурсом и освобождает его (9). Когда управле- создается эффект параллельного выпол-
определенный аппаратный ресурс для свое- ние получает задача 3, она обнаруживает, что нения нескольких задач за счет быстрого
го единоличного использования. В момент аппаратный ресурс свободен, захватывает его переключения между ними.
времени (5) планировщик приостанавлива- и выполняется до того момента, пока не бу- 2. Переключение контекста, благодаря кото-
дет приостановлена планировщиком (10). рому выполнение одной задачи не сказы-
Когда задача выполняется, она, как и любая вается на остальных задачах (задачи рабо-
программа, использует регистры процессора, тают независимо).
память программ и память данных. Вместе эти 3. Временная база, основанная на системном
ресурсы (регистры, стек и др.) образуют кон- кванте как единице измерения времени.
текст задачи (task execution context). Контекст Вышеприведенное описание основ ОСРВ
задачи целиком и полностью описывает теку- является очень обобщенным. Существует
щее состояние процессора: флаги процессора, еще целый ряд понятий, таких как приорите-
какая инструкция сейчас выполняется, какие ты задач, средства синхронизации, передача
значения загружены в регистры процессора, информации между задачами и др., которые
где в памяти находится вершина стека и т. д. будет раскрыты позже на примере конкрет-
Задача «не знает», когда ядро ОСРВ прио- ной ОСРВ — FreeRTOS.
становит ее выполнение или, наоборот, воз-
обновит. Соглашения о типах данных
На рис. 4а показан абстрактный процессор, и именах идентификаторов
который выполняет задачу 1, частью которой
является операция сложения. Операнды за- Как упоминалось в [1], бóльшая (подавляю-
гружены в регистры Reg1 и Reg2 (инструкции щая) часть FreeRTOS написана на языке Си.
LDI). Пусть перед инструкцией сложения Имена идентификаторов в исходном коде
ADD ядро приостановило задачу 1 и отдало ядра и демонстрационных проектах подчиня-
управление задаче 2, которая использует ре- ются определенным соглашениям, зная кото-
гистры Reg1 и Reg2 для своих нужд (рис. 4б). рые проще понимать тексты программ [5].
В какой-то момент времени ядро возобновит Имена переменных и функций представ-
выполнение задачи 1 с места, где она была лены в префиксной форме (так называемая
приостановлена: с инструкции ADD (рис. 4в). Венгерская нотация): имена начинаются
Однако для задачи 1 изменение ее контекста с одной или нескольких строчных букв —
(регистров Reg1 и Reg2) останется незамечен- префикса.
ным, произойдет сложение, но его результат Для переменных префикс определяет тип
«с точки зрения» задачи 1 окажется неверным. переменной согласно таблице 1.
Таким образом, одна из основных функ- Например, ulMemCheck — переменная
ций ядра ОСРВ — это обеспечение идентич- типа unsigned long, pxCreatedTask — пере-
ности контекста задачи до ее приостановки менная типа «указатель на структуру».
и после ее восстановления. Когда ядро при- API-функции FreeRTOS имеют префиксы,
останавливает задачу, оно должно сохранить обозначающие тип возвращаемого значения,
контекст задачи, а при ее восстановлении — как и для переменных. Системные функции,
восстановить. Процесс сохранения и восста- область видимости которых ограничена фай-
новления контекста задачи называется пере- лом исходного кода ядра (то есть имеющие
ключением контекста (context switching). спецификатор static), имеют префикс prv.
Рис. 4. Переключение между задачами
Немаловажным понятием является квант Следом за префиксом функции следу-
без переключения контекста времени работы планировщика (tick) — это ет имя модуля (файла с исходным кодом),
жестко фиксированный отрезок времени, в  котором она определена. Например,

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 3 '2011


микроконтроллеры компоненты 111

2. portBASE_TYPE определяет тип, актив- ет» об активности планировщика, то он отве-


Таблица 1. Префиксы переменных но используемый в коде ядра FreeRTOS. чает за переключение контекста при смене вы-
Префикс Операции с типом portBASE должны вы- полняющейся задачи. Для достижения этого
Ее тип
переменной полняться как можно более эффективно каждая задача имеет свой собственный стек.
c char
на данном МК, поэтому разрядность типа При смене задачи ее контекст сохраняется в ее
s short
l long portBASE_TYPE устанавливается идентич- собственном стеке, что позволяет восстано-
f float ной разрядности целевого МК. Например, вить контекст при возобновлении задачи [4].
d double для 8‑битных МК это будет char, для Как было сказано выше, при грубом при-
v void
e Перечисляемый тип (enum)
16‑битных — short. ближении задача может находиться в двух со-
x Структуры (struct) и др. типы Идентификаторы макроопределений так- стояниях: выполняться и не выполняться. При
p Указатель (дополнительно к вышеперечисленным) же начинаются с префикса, который опреде- подробном рассмотрении состояние «задача
u Беззнаковый (дополнительно к вышеперечисленным) ляет, в каком файле этот макрос находится не выполняется» подразделяется на несколько
(табл. 3). различных состояний в зависимости от того,
vTaskStartScheduler() — функция, возвра- как она была остановлена (рис. 5).
щающая тип void, которая определена в фай- Таблица 3. Префиксы макросов,
Подробно рассмотрим состояния зада-
ле task.c, uxQueueMessagesWaiting() — воз- используемых в FreeRTOS чи в FreeRTOS. Говорят, что задача выпол-
вращает некий беззнаковый целочисленный няется (running), если в данный момент
тип, определена в файле queue. c. Префикс Где определен Пример макроопределения времени процессор занят ее выполнением.
Встроенные типы данных (short, char и т. д.) port portable.h portMAX_DELAY Состояние готовности (ready) характеризу-
не используются в исходном коде ядра. Вместо tsk, task task.h taskENTER_CRITICAL() ет задачу, готовую к выполнению, но не вы-
этого используется набор специальных типов, pd projdefs.h pdTRUE полняющуюся, так как в данный момент вре-
которые определены индивидуально для каж- config FreeRTOSConfig.h configUSE_PREEMPTION мени процессор занят выполнением другой
err projdefs.h errQUEUE_FULL
дого порта в файле portmacro.h и начинаются задачи. Готовые к выполнению задачи (с оди-
с префикса port. Список специальных типов наковым приоритетом) по очереди переходят
FreeRTOS приведен в таблице  2. Задачи в состояние выполнения и пребывают в нем
в течение одного системного кванта, после
Таблица 2. Специальные типы FreeRTOS Любая программа, которая выполняется чего возвращаются в состояние готовности.
под управлением FreeRTOS, представляет со- Задача находится в блокированном состоя-
Специальный тип
FreeRTOS Соответствующий встроенный тип бой множество отдельных независимых задач. нии, если она ожидает наступления временно-
Каждая задача выполняется в своем собствен- го или внешнего события (event). Например,
portCHAR char
portSHORT short
ном контексте без случайных зависимостей вызвав API-функцию vTaskDelay(), задача
portLONG long от других задач и ядра FreeRTOS. Только одна переведет себя в блокированное состояние
portTickType Тип счетчика системных квантов задача из множества может выполняться до тех пор, пока не пройдет временной период
portBASE_TYPE Наиболее употребительный тип во FreeRTOS в один момент времени, и планировщик от- задержки (delay): это будет временное событие.
ветственен, какая именно. Планировщик оста- Задача блокирована, если она ожидает собы-
Это сделано для обеспечения независимо- навливает и возобновляет выполнение всех тия, связанного с другими объектами ядра —
сти кода ядра от конкретных компилятора задач по очереди, чтобы достичь эффекта од- очередями и семафорами: это будет внешнее
и МК. В демонстрационных проектах так же новременного выполнения нескольких задач (по отношению к задаче) событие. Нахождение
использованы только специальные типы на одном процессоре. Так как задача «не зна- задачи в блокированном состоянии ограниче-
FreeRTOS, однако в своих проектах можно
использовать встроенные типы данных. Это
окажется полезным для разграничения иден-
тификаторов, относящихся к ядру FreeRTOS,
от  идентификаторов, использующихся
в прикладных задачах. Напротив, исполь-
зование типов данных FreeRTOS позволит
добиться большей кроссплатформенности
создаваемого кода.
Подробнее следует остановиться на типах
portTickType и portBASE_TYPE:
1. portTickType может быть целым беззна-
ковым 16‑ или 32‑битным. Он определяет
тип системной переменной, которая ис-
пользуется для подсчета количества си-
стемных квантов, прошедших с момента
старта планировщика. Таким образом,
portTickType задает максимальный вре-
менной интервал, который может быть
отсчитан средствами FreeRTOS. В случае
16‑битного portTickType максимальный
интервал составляет 65 536 квантов, в слу-
чае 32‑битного — 4 294 967 296 квантов.
Использование 16‑битного счетчика кван-
тов оправдано на 8‑ и 16‑битных платфор-
мах, так как позволяет значительно повы- Рис. 5. Состояния задачи в FreeRTOS
сить их быстродействие.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 3 '2011 www.kite.ru


112 компоненты микроконтроллеры

но тайм-аутом. То есть если ожидаемое внеш- for( ;; )


4. pvParameters — произвольный параметр,
нее событие не наступило в течение тайм-аута, { передаваемый задаче при ее создании.
/* Код, реализующий функциональность задачи */
то задача возвращается в состояние готовности }
Задается в виде указателя на void, в теле за-
к выполнению. Это предотвращает «подвиса- /* Если все-таки произойдет выход из бесконечного цикла, дачи может быть преобразован в указатель
то задача должна быть уничтожена ДО конца функции.
ние» задачи при ожидании внешнего события, Параметр NULL обозначает, что уничтожается задача,
на любой другой тип. Передача параметра
которое по каким-то причинам никогда не на- вызывающая API-функцию vTaskDelete() */ оказывается полезной возможностью при
vTaskDelete( NULL );
ступит. Блокированная задача не получает про- }
создании нескольких экземпляров одной
цессорного времени. задачи.
Приостановленная (suspended) задача так- 5. uxPriority — определяет приоритет соз-
же не получает процессорного времени, од- Задачи создаются API-функцией даваемой задачи. Нуль соответствует са-
нако, в отличие от блокированного состоя- xTaskCreate(), а уничтожаются xTaskDelete(). мому низкому приоритету, (configMAX_
ния, переход в приостановленное состояние Функция  xTaskCreate() является одной PRIORITIES  — 1 )   — н а и в ы с ш е м у .
и выход из него осуществляется в явном из наиболее сложных API-функций. Ее про- Значение аргумента uxPriority большее,
виде вызовом API-функций vTaskSuspend() тотип: чем (configMAX_PRIORITIES — 1), при-
и xTaskResume(). Тайм-аут для приостанов- ведет к назначению задаче приоритета
portBASE_TYPE xTaskCreate(pdTASK_CODE pvTaskCode,
ленного состояния не предусмотрен, и задача const signed portCHAR * const pcName,
(configMAX_PRIORITIES — 1).
может оставаться приостановленной сколь unsigned portSHORT usStackDepth, 6. pxCreatedTask — может использоваться для
void *pvParameters,
угодно долго [5]. unsigned portBASE_TYPE uxPriority,
получения дескриптора (handle) создавае-
В любой программе реального вре- xTaskHandle *pxCreatedTask мой задачи, который помещается по адресу
);
мени есть как менее, так и более ответствен- pxCreatedTask после успешного создания
ные задачи. Под «ответственностью» задачи задачи. Дескриптор можно использовать
здесь понимается время реакции программы xTaskCreate() в случае успешного созда- в дальнейшем для различных операций
на внешнее событие, которое обрабатывает- ния задачи возвращает pdTRUE. Если же над задачей, например изменения приори-
ся задачей. Например, ко времени реакции объема памяти кучи недостаточно для раз- тета задачи или ее уничтожения. Если в по-
на срабатывание датчика в производственной мещения служебных структур данных лучении дескриптора нет необходимости,
установке предъявляются куда более строгие и стека задачи, то xTaskCreate() возвращает то pxCreatedTask должен быть установлен
требования, чем ко времени реакции на на- errCOULD_NOT_ALLOCATE_REQUIRED_ в NULL.
жатие клавиши на клавиатуре. Для обеспече- MEMORY. Функции xTaskCreate() передают- По сложившейся традиции первая про-
ния преимущества на выполнение более от- ся следующие аргументы: грамма в учебнике по любому языку про-
ветственных задач во FreeRTOS применяется 1. pvTaskCode — указатель на функцию, реа- граммирования для компьютеров выводит
механизм приоритетов задач (Task priorities). лизующую задачу (фактически — иденти- на экран монитора фразу “Hello, world!”.
Среди всех задач, находящихся в состоянии фикатор функции в программе). Рискнем предположить, что для микрокон-
готовности, планировщик отдаст управление 2. pcName — нуль-терминальная (заканчива- троллеров первая программа должна пере-
той задаче, которая имеет наивысший приори- ющаяся нулем) cтрока, определяющая имя ключать логический уровень на своих вы-
тет. Задача будет выполняться до тех пор, пока функции. Ядром не используется, а служит водах с некоторой частотой (проще говоря,
она не будет блокирована или приостановлена лишь для наглядности при отладке. мигать светодиодами).
или пока не появится готовая к выполнению 3. usStackDepth — глубина (размер) собствен- Что ж, пришло время написать первую
задача с более высоким приоритетом. ного стека создаваемой задачи. Размер зада- программу под управлением FreeRTOS.
Каждой задаче назначается приоритет от 0 ется в словах, хранящихся в стеке, а не в бай- Программа будет содержать две задачи.
до (configMAX_PRIORITIES — 1). Меньшее тах. Например, если стек хранит 32‑битные Задача 1 будет переключать логический уро-
значение приоритета соответствует меньшему слова, а значение usStackDepth задано рав- вень на одном выводе МК, задача 2 — на дру-
приоритету. Наиболее низкий приоритет у за- ным 100, то для размещения стека задачи гом. Частота переключения для разных вы-
дачи «бездействие», значение которого опреде- будет выделено 4100 = 400 байт. Размер водов будет разной.
лено в tskIDLE_PRIORITY как 0. Изменяя зна- стека в байтах не должен превышать мак- В качестве аппаратной платформы бу-
чение configMAX_PRIORITIES, можно опре- симального значения для типа size_t. Размер дет использоваться МК AVR ATmega128L,
делить любое число возможных приоритетов, стека, необходимый для корректной работы установленный на  мезонинный модуль
однако уменьшение configMAX_PRIORITIES задачи, которая ничего не делает (содержит WIZ200WEB фирмы WIZnet (рис. 6) [7]. Как
позволяет уменьшить объем ОЗУ, потребляе- только пустой бесконечный цикл, как за- отправная точка будет взят демонстрацион-
мый ядром. дача ATaskFunction выше), задается макро- ный проект, компилятор — WinAVR, версия
Задачи в FreeRTOS реализуются в виде Си- сом configMINIMAL_STACK_SIZE. Не ре- 2010.01.10.
функций. Обязательное требование к функ- комендуется создавать задачи с меньшим Прежде всего необходимо загрузить
ции, реализующей задачу: она должна иметь размером стека. Если же задача потребляет и установить компилятор WinAVR [8]. Далее
один аргумент типа указатель на void и ни- большие объемы стека, то необходимо за- с официального сайта [9] загрузить дистри-
чего не возвращать (void). Указатель на та- дать большее значение usStackDepth. Нет бутив FreeRTOS и распаковать в удобное ме-
кую функцию определен как pdTASK_CODE. простого способа определить размер сте- сто (в статье это C:/).
Каждая задача — это небольшая программа ка, необходимого задаче. Хотя возможен Демонстрационный проект распола-
со своей точкой входа, которая содержит бес- точный расчет, большинство програм- гается в  C:/FreeRTOSV6.1.0/Demo/AVR_
конечный цикл: мистов находят золотую середину между ATMega323_WinAVR/ и предназначен
требованиями выделения достаточного для выполнения на МК ATmega323. Файл
void ATaskFunction( void *pvParameters )
{
размера стека и эффективного расхода па- makefile, находящийся в директории про-
/* Переменные могут быть объявлены здесь, как и в обычной мяти. Существуют встроенные механизмы екта, содержит все настройки и правила
функции. Каждый экземпляр этой задачи будет иметь свою
собственную копию переменной iVariableExample. Если
экспериментальной оценки объема ис- компиляции и, в том числе, определяет,
объявить переменную со спецификатором static, то будет пользуемого стека, например API-функция для какого МК компилируется проект. Для
создана только одна переменная iVariableExample,
доступная из всех экземпляров задачи */
uxTaskGetStackHighWaterMark(). О воз- того чтобы целевой платформой стал МК
int iVariableExample = 0; можностях контроля переполнения стека ATmega128, необходимо в файле makef ile
/* Тело задачи реализовано как бесконечный цикл */
будет рассказано позже. отыскать строку:

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 3 '2011


микроконтроллеры компоненты 113

Рис. 6. Мезонинный модуль WIZ200WEB Рис. 7. Окно редактора Programmers Notepad

MCU = atmega323 Подготовительный этап закончен. Теперь short main( void )


{
можно переходить к редактированию фай-
ла main. c. Его содержимое должно принять /* Биты 0, 1 порта PORTF будут работать как ВЫХОДЫ */
DDRF |= (1 << DDF0) | (1 << DDF1);
и заменить ее на вид:
/* Создать задачу 1, заметьте, что реальная программа должна
/* проверять возвращаемое значение, чтобы убедиться,
MCU = atmega128 #include <stdlib.h>
/* что задача создана успешно */
#include <string.h>
xTaskCreate(vTask1, /* Указатель на функцию,
/* реализующую задачу */
#ifdef GCC_MEGA_AVR
(signed char *) “Task1”, /* Текстовое имя задачи.
/* EEPROM routines used only with the WinAVR compiler. */
Для редактирования файлов можно #include <avr/eeprom.h>
/* Только для наглядности */
configMINIMAL_STACK_SIZE, /* Размер стека –
применить простой, но удобный тексто- #endif
/* минимально необходимый*/
вый редактор Programmers Notepad, кото- /* Необходимые файлы ядра */
NULL, /* Параметр, передаваемый задаче, –
/* не используется */
рый поставляется вместе с компилятором #include “FreeRTOS.h”
1, /* Приоритет = 1 */
#include “task.h”
WinAVR (рис. 7), запустить который мож- #include “croutine.h”
NULL ); /* Получение дескриптора задачи – не используется */

но выполнив Пуск → Все программы → /* Создать задачу 2 */
/*-----------------------------------------------------------*/
WinAVR-20100110 → Programmers Notepad /* Функция задачи 1 */
x Ta s k C r e a t e ( v Ta s k 2 , ( s i g n e d c h a r * ) “ Ta s k 2 ” ,
configMINIMAL_STACK_SIZE, NULL, 1, NULL );
[WinAVR] или C:/WinAVR-20100110/pn/pn.exe void vTask1( void *pvParameters )
{
(в случае установки WinAVR на диск C:/). /* Квалификатор volatile запрещает оптимизацию
/* Запустить планировщик. Задачи начнут выполняться. */
vTaskStartScheduler();
Помимо прочего Programmers Notepad по- /* переменной ul */
volatile unsigned long ul;
зволяет производить сборку (buid) проекта /* Как и большинство задач, эта задача содержит
return 0;
}
прямо из окна редактора. /* бесконечный цикл */
/*-----------------------------------------------------------*/
for( ;; )
Далее необходимо исключить из компиляции {
большинство исходных файлов проекта, отве- /* Инвертировать бит 0 порта PORTF */
PORTF ^= (1 << PF0);
чающих за демонстрацию всех возможностей /* Задержка на некоторый период Т1*/ Для сборки проекта из среды Programmers
FreeRTOS, оставив лишь основной файл main.c. for( ul = 0; ul < 4000L; ul++ ) Notepad необходимо выбрать пункт меню
{
То есть заменить фрагмент файла makefile: /* Это очень примитивная реализация задержки, Tools → [WinAVR] Make all ( р и с .   8 ) .
/* в дальнейших примерах будут использоваться Сообщение об отсутствии ошибок (Errors:
SRC = \ /* API-функции */
main.c \ } none) означает успешную сборку и получе-
ParTest/ParTest.c \ } ние файла прошивки rtosdemo.hex, который
serial/serial.c \ /* Уничтожить задачу, если произошел выход
regtest.c \ /* из бесконечного цикла (в данной реализации выход должен появиться в директории проекта.
$(SOURCE_DIR)/tasks.c \ /* заведомо не произойдет) */ Используя любой программатор, необхо-
$(SOURCE_DIR)/queue.c \ vTaskDelete( NULL );
$(SOURCE_DIR)/list.c \ } димо загрузить файл прошивки в целевой
$(SOURCE_DIR)/croutine.c \ МК. Автор использовал для этой цели аналог
$(SOURCE_DIR)/portable/MemMang/heap_1.c \ /*-----------------------------------------------------------*/
$(PORT_DIR)/port.c \ /* Функция задачи 2, подобная задаче 1 */ отладчика JTAG ICE (рис. 9). Возможна за-
$(DEMO_DIR)/crflash.c \ void vTask2( void *pvParameters ) грузка и через интерфейс SPI.
$(DEMO_DIR)/integer.c \ {
$(DEMO_DIR)/PollQ.c \ volatile unsigned long ul; Подключив осциллограф к выводам 1, 2
$(DEMO_DIR)/comtest.c for( ;; ) разъема J2 — они подключены к выводам
{
/* Инвертировать бит 1 порта PORTF */ PF0 и PF1 ATmega128 соответственно (обо-
на: PORTF ^= (1 << PF1); значены красным на рис. 9), можно наблю-
/* Задержка на некоторый период Т2*/
for( ul = 0; ul < 8000L; ul++ ) дать совместную работу двух независимых
SRC = \ { задач (рис. 10).
main.c \ }
$(SOURCE_DIR)/tasks.c \ Рассмотрим подробнее, что происходит.
$(SOURCE_DIR)/queue.c \ } Пусть после старта планировщик первой
$(SOURCE_DIR)/list.c \ vTaskDelete( NULL );
$(SOURCE_DIR)/croutine.c \ } запустит задачу 1 (рис. 11). Она выполняет-
$(SOURCE_DIR)/portable/MemMang/heap_1.c \ ся на протяжении 1 системного кванта вре-
$(PORT_DIR)/port.c /*-----------------------------------------------------------*/
мени, который задан равным 1 мс в файле

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 3 '2011 www.kite.ru


114 компоненты микроконтроллеры

Рис. 8. Успешное завершение сборки проекта Рис. 9. Загрузка файла прошивки

Пока выполняется задача 1, она увеличи- фикаторов и типах данных, используемых


вает свой счетчик ul. Когда планировщик в исходном коде ядра FreeRTOS. Большое
переводит задачу 1 в состояние готовности, внимание уделено задаче как базовой еди-
переменная ul сохраняется в собственном нице программы для FreeRTOS. Подробно
стеке задачи 1 и не увеличивается, пока вы- рассмотрены состояния задачи, дано объяс-
полняется задача 2. Как только переменная ul нение понятию приоритета задачи. Описана
достигает значения 4000, она обнуляется API-функция создания задачи xTaskCreate().
(момент времени t1), а логический уровень Приведен пример наипростейшей про-
на выводе PF0 инвертируется, однако это мо- граммы, выполняющейся под управлением
жет произойти только в течение кванта вре- FreeRTOS, приведены результаты тестиро-
мени выполнения задачи 1. Аналогично ведет вания и описаны происходящие процессы
себя задача 2, но ее счетчик обнуляется по до- без углубления во внутреннюю реализацию
Рис. 10. Напряжение на выводах PF0 и PF1 ATmega128L
стижении значения 8000. Таким образом, эта FreeRTOS.
(сверху вниз), полученное цифровым осциллографом простейшая программа генерирует меандр В следующих публикациях будет продол-
с «плавающим» полупериодом, а разброс жено рассмотрение задач. Подробно будет
продолжительности полупериода достигает рассказано о приоритетах задач, показано,
FreeRTOSConfig.h. В это время задача 2 на- одного системного кванта, то есть 1 мс. каким образом можно менять приоритеты
ходится в состоянии готовности. После чего во время выполнения программы. Внимание
вызывается планировщик, который перево- Выводы будет уделено правильному способу приоста-
дит задачу 1 в состояние готовности, а зада- навливать задачи на заданное время и фор-
чу 2 — в состояние выполнения, так как зада- В статье были рассмотрены основ- мировать задержки. Будет рассказано о задаче
чи имеют одинаковый приоритет и задача 1 ные принципы, заложенные во все ОСРВ. «бездействие» и о функции, вызываемой каж-
уже отработала один квант времени. Описаны соглашения об именах иденти- дый системный квант времени. Будет показа-
но, как правильно уничтожать задачи. Весь
материал будет снабжен подробными при-
мерами. n

Литература

1. Курниц А. FreeRTOS — операционная система


для микроконтроллеров // Компоненты и тех-
нологии. 2011. № 2.
2. http://www.freertos.org/implementation/index.
html
3. http://www.freertos.org/a00015.html
4. http://www.freertos.org/taskandcr.html
5. http://www.freertos.org/a00017.html
6. Barry R. Using the freertos real time kernel:
A Practical Guide. 2009.
7. http://www.wiznet.co.kr/Sub_Modules/en/
product/Product_Detail.asp? cate1=5&cate2=44
&cate3=0&pid=1025
8. http://winavr.sourceforge.net/download.html
Рис. 11. Работа программы во времени 9. http://sourceforge.net/projects/freertos/files/
FreeRTOS/

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 3 '2011


96 компоненты микроконтроллеры

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров

В предыдущих статьях [1] читатель познакомился с операционной систе-


мой реального времени (ОСРВ) для микроконтроллеров (МК) FreeRTOS.
В данной статье будет продолжено изучение базовой единицы любой
программы, работающей под управлением FreeRTOS, — задачи. Будет
рассказано, как передать в задачу в момент ее создания произвольный
параметр и как создать несколько экземпляров одной задачи. Будет по-
казано, как блокировать задачу на определенное время и заставить ее
циклически выполняться с заданной частотой. Автор использует удобную
Андрей Курниц для демонстрации возможностей FreeRTOS платформу — порт FreeRTOS
kurnits@stim.by для x86 совместимых процессоров.

Подготовка к выполнению FreeRTOS на платформе x86

В предыдущей части [1] был приведен пример создания простой


программы, работающей под управлением FreeRTOS. Платформой
служил МК фирмы AVR ATmega128. Продолжать подробное рас-
смотрение и демонстрацию возможностей FreeRTOS на платформе
реального МК не всегда удобно. Гораздо удобнее использовать в ка-
честве платформы любой x86 совместимый настольный компьютер,
используя соответствующий порт FreeRTOS. Все последующие при-
меры будут приведены для порта для x86 совместимых процессоров,
работающих в реальном режиме. Мы используем бесплатный пакет
Open Watcom, включающий Си-компилятор и среду разработки [2],
об особенностях установки которого будет сказано ниже. Получаемые
в результате компиляции и сборки исполнимые (exe) файлы могут

Рис. 1. Включение 16‑разрядного компилятора

Рис. 2. Включение DOS в список целевых ОС Рис. 3. Успешная сборка демонстрационного проекта в среде Open Watcom

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011


микроконтроллеры компоненты 97

быть выполнены из интерпретатора команд 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 )

/* Set the following definitions to 1 to include the API function, or zero


to exclude the API function. */

#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”

/* Структура, содержащая передаваемую в задачу информацию */


typedef struct TaskParam_t {
char string[32]; /* строка */
long period; /* период */
} TaskParam;

/* Объявление двух структур TaskParam */


TaskParam xTP1, xTP2;

/*-----------------------------------------------------------*/
/* Функция, реализующая задачу */
void vTask( void *pvParameters )
{
volatile long ul;
volatile TaskParam *pxTaskParam;

/* Преобразование типа void* к типу TaskParam* */
pxTaskParam = (TaskParam *) pvParameters;

for( ;; )
Рис. 5. Минимально необходимый набор исходных и заголовочных файлов {
в среде Open Watcom /* Вывести на экран строку, переданную в качестве параметра при создании задачи */
puts( (const char*)pxTaskParam->string );

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011 www.kite.ru


98 компоненты микроконтроллеры

/* Задержка на некоторый период Т2*/


for( ul = 0; ul < pxTaskParam->period; ul++ )
{
}

}
vTaskDelete( NULL );
}

/*-----------------------------------------------------------*/
/* Точка входа. С функции 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 );

Выполнив сборку модифицированной учебной программы и за-


пустив ее на выполнение, можно наблюдать ситуацию, когда все время
будет выполняться Задача 2, а Задача 1 никогда не получит управление
(рис. 8).

Рис. 6. Результат выполнения учебной программы № 1 в среде Windows

Задача 2 выводит сообщение о своей работе в три раза реже, чем


Задача 1. Это объясняется тем, что в Задачу 2 было передано значение
периода в 3 раза большее, чем в Задачу 1. Таким образом, передача
различных параметров в задачи при их создании позволила добиться
различной функциональности отдельных экземпляров одной задачи.

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

В предыдущей статье [1] читатель познакомился с механизмом Рис. 8. Результат работы учебной программы
приоритетов задач. Далее будет показано, как значение приоритета в случае назначения Задаче 2 более высокого приоритета
влияет на выполнение задачи.
При создании задачи ей назначается приоритет. Приоритет за-
дается с помощью параметра uxPriority функции xTaskCreate(). Задача 2, как и Задача 1, все время находится в состоянии готовно-
Максимальное количество возможных приоритетов определяется сти к выполнению. За счет того, что Задача 2 имеет приоритет выше,
макроопределением configMAX_PRIORITIES в заголовочном файле чем Задача 1, каждый квант времени планировщик будет отдавать
FreeRTOSConfig.h. В целях экономии ОЗУ необходимо задавать наи- управление именно ей, а Задача 1 никогда не получит процессорного
меньшее, но достаточное значение configMAX_PRIORITIES. Нулевое времени (рис. 9).
значение приоритета соответствует наиболее низкому приоритету, Этот пример показывает необходимость пользоваться приорите-
значение (configMAX_PRIORITIES-1) — наиболее высокому (в ОС тами осмотрительно, так как никакого алгоритма старения в плани-
семейства Windows наоборот — приоритет 0 наивысший). ровщике не предусмотрено (как в ОС общего назначения). Поэтому

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011


микроконтроллеры компоненты 99

нии ГОТОВНОСТИ к выполнению, а следовательно, не может быть


выполнена планировщиком. Планировщик распределяет процес-
сорное время только между задачами, ГОТОВЫМИ к выполнению.
Таким образом, если высокоприоритетная задача ожидает наступле-
ния некоторого события, то есть не находится в состоянии готов-
ности к выполнению, то планировщик отдаст управление готовой
к выполнению более низкоприоритетной задаче.
Таким образом, применение событий для управления ходом выпол-
Рис. 9. Разделение времени между задачами,
нения задач позволяет создавать программы с множеством различных
когда Задача 2 имеет более высокий приоритет, чем Задача 1 приоритетов задач, и программист может не опасаться того, что высо-
коприоритетная задача «заберет» себе все процессорное время.

возможна ситуация «зависания» задачи с низким приоритетом, кото- Блокированное состояние задачи
рая никогда не выполнится. Программисту необходимо тщательно
проектировать прикладные программы и благоразумно задавать Если задача ожидает наступления события, то она находится в бло-
уровни приоритетов, чтобы избежать такой ситуации. Далее будет кированном состоянии (рис. 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, который непо-
и задача ожидает его наступления, то она НЕ находится в состоя- средственно задает количество квантов времени задержки.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011 www.kite.ru


100 компоненты микроконтроллеры

Например, пусть задача вызвала функ-


цию xTicksToDelay(100) в момент времени,
когда счетчик квантов был равен 5000. Задача
сразу же блокируется, планировщик отдаст
управление другой задаче, а вызывающая за-
дача вернется в состояние готовности к вы-
полнению, только когда счетчик квантов до-
стигнет значения 5100. В течение времени,
пока счетчик квантов будет увеличиваться
от 5000 до 5100, планировщик будет выпол-
нять другие задачи, в том числе задачи с бо-
лее низким приоритетом.
Следует отметить, что программисту нет
необходимости отслеживать переполнение
счетчика квантов времени. API-функции, Рис. 10. Результат работы учебной программы № 2
связанные с отсчетом времени (в том числе
и vTaskDelay()), берут эту обязанность на себя.
Рассмотрим учебную программу № 2, ко- По результатам работы учебной програм- чи, готовой к выполнению, поэтому плани-
торая выполняет те же функции, что и про- мы № 2 (рис. 10) видно, что процессорное ровщик вызывает системную задачу, которая
грамма № 1, но для создания задержек в ней время теперь получает как высокоприори- не выполняет никакой полезной работы, —
используется API-функция vTaskDelay(). тетная задача 2, так и низкоприоритетная за- задачу Бездействие (4). Подробнее о задаче
Кроме того, задаче при ее создании передает- дача 1. Бездействие будет сказано ниже.
ся не абстрактное значение периода, а значе- Выполнение задач в учебной программе На протяжении времени, когда Задача 1
ние периода в миллисекундах: № 2 приведено на рис. 11. Выполнение кода и Задача 2 находятся в блокированном состоя-
планировщика в целях упрощения рисунка нии, кроме выполнения задачи Бездействие,
#include <stdlib.h>
#include <stdio.h> не приводится. Бóльшую часть времени про- ядро FreeRTOS отсчитывает кванты време-
#include <string.h> цессор бездействует, а следовательно, теперь ни, прошедшие с моментов вызовов API-
#include “FreeRTOS.h”
#include “task.h” задачи очень экономно расходуют процес- функции vTaskDelay(). Как только ядро от-
/* Структура, содержащая передаваемую в задачу информацию */
сорное время. считает 1000 квантов (1000 мс), оно переведет
typedef struct TaskParam_t { В момент времени (1) происходит запуск Задачу 1 из блокированного в состояние готов-
char string[32]; /* строка */
long period; /* период, миллисекунды*/
планировщика. На этот момент Задача 1 ности к выполнению (момент времени (5)).
} TaskParam; и Задача 2 находятся в состоянии готовности Планировщик отдаст ей управление, она вы-
/* Объявление двух структур TaskParam */ к выполнению, однако приоритет Задачи 2 полнит полезную работу и снова перейдет
TaskParam xTP1, xTP2; выше, поэтому именно ей планировщик в блокированное состояние на время 1000 мс
/*-----------------------------------------------------------*/ передает управление. Задача 2 выполняет и т. д. Задача 2 будет находиться в блокирован-
/* Функция, реализующая задачу */
void vTask( void *pvParameters )
полезную работу (выводит строку “Task 2 ном состоянии на протяжении 3000 мс. В мо-
{ is running”) (рис. 10), после чего выполняет мент времени (7) из блокированного состоя-
volatile TaskParam *pxTaskParam;
API-функцию vTaskDelay(), в результате чего ния в состояние готовности к выполнению
/* Преобразование типа void* к типу TaskParam* */ Задача 2 переходит в блокированное состоя- перейдут обе задачи, однако планировщик
pxTaskParam = (TaskParam *) pvParameters;
ние. После вызова функции vTaskDelay() вы- запустит (переведет в состояние выполнения)
for( ;; ) полняемая в данный момент Задача 2 пере- Задачу 2, так как приоритет у нее выше.
{
/* Вывести на экран строку, переданную в качестве параметра шла в блокированное состояние и не нуж-
при создании задачи */
puts( (const char*)pxTaskParam->string ); дается в процессорном времени, поэтому API-функция vTaskDelayUntil ()
/* Задержка на время, заданное в миллисекундах */ для того чтобы занять процессор другой за-
/* pxTaskParam->period задан в миллисекундах /*
/* Разделив его на кол-во мс в кванте, получим кол-во квантов */ дачей, функция vTaskDelay() вызывает пла- API-функция vTaskDelayUntil() служит для
vTaskDelay(pxTaskParam->period / portTICK_RATE_MS); нировщик. Теперь в списке готовых к вы- тех же целей, что и vTaskDelay(), — для пере-
}
vTaskDelete( NULL ); полнению задач осталась только Задача 1, вода задачи в блокированное состояние на за-
}
которой планировщик и отдает управление данное время. Однако она имеет некоторые
/*-----------------------------------------------------------*/ (момент времени (2)). Задача 1 выполня- особенности, позволяющие с меньшими уси-
/* Точка входа. С функции main() начнется выполнение программы. */
short main( void ) ет свою полезную работу: также вызыва- лиями реализовать циклическое выполнение
{ ет API-функцию vTaskDelay() и переходит кода задачи с точно заданным периодом.
/* Заполнение полей структуры, передаваемой Задаче 1 */
strcpy(xTP1.string, “Task 1 is running”); в блокированное состояние (момент вре- Часто перед программистом стоит зада-
xTP1.period = 1000L; /* 1000 мс */
мени (3)). В этот момент нет ни одной зада- ча циклического выполнения какого-либо
/* Заполнение полей структуры, передаваемой Задаче 2 */
strcpy(xTP2.string, “Task 2 is running”);
xTP2.period = 3000L; /* 3000 мс */

/* Создание Задачи 1 с приоритетом 1. Передача ей в качестве


параметра указателя на структуру xTP1 */
xTaskCreate( vTask, ( signed char * ) “Task1”, configMINIMAL_
STACK_SIZE, (void*)&xTP1, 1, NULL );

/* Создание Задачи 2 с приоритетом 2. Передача ей указателя


на структуру xTP2 */
xTaskCreate( vTask, ( signed char * ) “Task2”, configMINIMAL_
STACK_SIZE, (void*)&xTP2, 2, NULL );

/* Запуск планировщика */
vTaskStartScheduler();

return 1;
} Рис. 11. Разделение процессорного времени между задачами в учебной программе № 2

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011


микроконтроллеры компоненты 101

Рис. 12. Ход выполнения циклической задачи. Рис. 13. Ход выполнения циклической задачи.
Задержка реализована API-функцией vTaskDelay() Задержка реализована API-функцией vTaskDelayUntil()

действия с четко фиксированной частотой и, следовательно, перио- Задача Бездействие


дом. API-функция vTaskDelay() переводит задачу в блокированное
состояние на промежуток времени, который отсчитывается от мо- Пока на МК подано питание и он находится не в режиме энергос-
мента вызова vTaskDelay(). В случае реализации циклически повто- бережения, МК должен выполнять какой-либо код. Поэтому хотя бы
ряющегося действия период его выполнения dt3 будет складываться одна задача должна постоянно находиться в состоянии готовности
из времени его выполнения dt1 и задержки dt2, создаваемой функци- к выполнению. Однако, как показано в учебной программе № 2, мо-
ей vTaskDelay() (рис. 12). жет сложиться ситуация, когда все задачи в программе могут быть
Если стоит цель обеспечить циклическое выполнение с точно за- заблокированы.
данным периодом dt3, то необходимо знать время выполнения тела В этом случае МК будет выполнять задачу Бездействие (Idle task).
задачи dt1, чтобы скорректировать величину задержки dt2. Это созда- Задача Бездействие создается автоматически при запуске плани-
ет дополнительные сложности. ровщика API-функцией vTaskStartScheduler(). Задача Бездействие
Для таких целей предназначена API-функция vTaskDelayUntil(). постоянно находится в состоянии готовности к выполнению.
Программист в качестве ее параметра задает период dT, который от- Ее приоритет задается макроопределением tskIDLE_PRIORITY как
считывается с момента t1 — момента выхода задачи из блокирован- самый низкий в программе (обычно 0). Это гарантирует, что задача
ного состояния (рис. 13). Бездействие не будет выполняться, пока в программе есть хотя бы
Прототип функции vTaskDelayUntil(): одна задача в состоянии готовности к выполнению. Как только поя-
вится любая готовая к выполнению задача, задача Бездействие будет
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement ); вытеснена ею.
Программисту предоставляется возможность добавить свою функ-
циональность в задачу Бездействие. Для этих целей есть возмож-
Функции vTaskDelayUntil() передаются следующие аргументы: ность определить функцию-ловушку (Idle hook function, которая
1. pxPreviousWakeTime — указатель на переменную, в которой хра- является функцией обратного вызова — Callback function), реали-
нится значение счетчика квантов в момент последнего выхода за- зующую функциональность задачи Бездействие (далее будем назы-
дачи из блокированного состояния (момент времени t1 на рис. 13). вать ее функцией задачи Бездействие). Функция задачи Бездействие
Этот момент используется как отправная точка для отсчета вре- отличается от функции, реализующей обычную задачу. Функция
мени, на которое задача переходит в блокированное состояние. задачи Бездействие не содержит бесконечного цикла, а автоматически
Переменная, на которую ссылается указатель pxPreviousWakeTime, вызывается ядром FreeRTOS множество раз, пока выполняется задача
автоматически обновляется функцией vTaskDelayUntil(), поэтому Бездействие, то есть ее тело помещается внутрь бесконечного цикла
при типичном использовании эта переменная не должна моди- средствами ядра.
фицироваться в теле задачи. Исключение составляет начальная Добавление своей функциональности в функцию задачи Бездейст-
инициализация, как показано в примере ниже. вие окажется полезным в следующих случаях:
2. xTimeIncrement — непосредственно задает период выполнения 1. Для реализации низкоприоритетных фоновых задач.
задачи. Задается в квантах; для задания в миллисекундах может 2. Для измерения резерва МК по производительности. Во время вы-
использоваться макроопределение portTICK_RATE_MS. полнения задачи Бездействие процессор не занят полезной рабо-
Типичное применение API-функции vTaskDelayUntil() в теле той, то есть простаивает. Отношение времени простоя процессора
функции, реализующей задачу: ко всему времени выполнения программы даст представление о ре-
зерве процессора по производительности, то есть о возможности
/* Функция задачи, которая будет циклически выполняться с жестко заданным периодом в 50 мс */
void vTaskFunction( void *pvParameters )
добавить дополнительные задачи в программу.
{ 3. Для снижения энергопотребления микроконтроллерного устрой-
/* Переменная, которая будет хранить значение счетчика квантов
в момент выхода задачи из блокированного состояния */ ства. Во многих МК есть возможность перехода в режим пони-
portTickType xLastWakeTime; женного энергопотребления для экономии электроэнергии.
/* Переменная xLastWakeTime нуждается в инициализации текущим значением счетчика квантов.
Это единственный случай, когда ее значение задается явно. Это актуально, например, в случае проектирования устройства с ба-
В дальнейшем ее значение будет автоматически модифицироваться API-функцией vTaskDelayUntil(). */ тарейным питанием. Выход из режима энергосбережения во многих
xLastWakeTime = xTaskGetTickCount();
/* Бесконечный цикл */ МК возможен по прерыванию от таймера. Если настроить МК так,
for( ;; ) чтобы вход в режим пониженного энергопотребления происходил
{
/* Какая-либо полезная работа */ в теле функции задачи Бездействие, а выход — по прерыванию
/* ... */ от того же таймера, что используется ядром FreeRTOS для форми-

/* Период выполнения этой задачи составит 50 мс. рования квантов времени, то это позволит значительно понизить
Разделив это значение на кол-во миллисекунд в 1 кванте portTICK_RATE_MS,
получим кол-во квантов периода, что и является аргументом vTaskDelayUntil().
энергопотребление устройства во время простоя процессора.
Переменная xLastWakeTime автоматически модифицируется внутри vTaskDelayUntil(), Есть некоторые ограничения на реализацию функции задачи
поэтому нет необходимости делать это в явном виде. */
vTaskDelayUntil( &xLastWakeTime, ( 50 / portTICK_RATE_MS ) ); Бездействие:
} 1. Задачу Бездействие нельзя пытаться перевести в блокированное
}
или приостановленное состояние.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011 www.kite.ru


102 компоненты микроконтроллеры

2. Если программа допускает использо-


вание API-функции уничтожения за-
дачи vTaskDelete(), то  функция задачи
Бездействие должна завершать свое вы-
полнение в течение разумного периода
времени. Это требование объясняется
тем, что функция задачи Бездействие от-
ветственна за освобождение ресурсов ядра
после уничтожения задачи. Таким образом,
временная задержка в теле функции задачи
Бездействие приведет к такой же задержке
в очистке ресурсов, связанных с уничто-
женной задачей, и ресурсы ядра не будут
освобождены вовремя.
Чтобы задать свою функцию задачи Рис. 14. Результат работы учебной программы № 3
Бездействие, необходимо в файле настрой-
ки ядра FreeRTOSConf ig.h задать макро-
определение configUSE_IDLE_HOOK рав- { Однако в учебной программе № 3 задача
/* Бесконечный цикл */
ным 1. В одном из файлов исходного кода for( ;; ) Бездействие не может прервать операцию
{
должна быть определена функция задачи /* Вывести на экран значение переменной ulIdleCycleCount */
с  общей переменной в  теле задачи ото-
Бездействие, которая имеет следующий про- printf( “ulIdleCycleCount = %lu\n\r”, ulIdleCycleCount ); бражения, так как задача Бездействие бу-
/* Задержка на 250 мс */
тотип: vTaskDelay( 250 / portTICK_RATE_MS );
дет выполняться лишь тогда, когда задача
} отображения завершит действия с общей
vTaskDelete( NULL );
void vApplicationIdleHook( void ); } переменной (вывод ее на экран функцией
printf()) и перейдет в блокированное состо-
/*-----------------------------------------------------------*/
/* Точка входа. С функции main() начнется выполнение программы. */ яние вызовом API-функции vTaskDelay().
Значение conf igUSE_IDLE_HOOK, рав- short main( void ) Одновременный совместный доступ, таким
{
ное 0, используется, когда не нужно добав- /* Создание задачи с приоритетом 1. Параметр не передается */ образом, исключен. Поэтому дополнитель-
лять дополнительную функциональность. xTaskCreate( vTaskFunction, ( signed char * ) “Task”, ных мер для обеспечения совместного до-
configMINIMAL_STACK_SIZE, NULL, 1, NULL );
Создадим учебную программу № 3, демон- /* Запуск планировщика */ ступа к общему ресурсу в учебной програм-
стрирующую использование функции зада- vTaskStartScheduler(); ме № 3 не предпринимается.
чи Бездействие. В программе будет опреде- return 1;
лена глобальная переменная-счетчик, задача }
Выводы
Бездействие будет инкрементировать значе- /* Функция, реализующая задачу Бездействие.
Ее имя ОБЯЗАТЕЛЬНО должно быть vApplicationIdleHook.
ние этой переменной. Также будет создана Аргументов не получает. Ничего не возвращает */ В статье описан способ передачи произ-
задача вывода значения переменной-счетчика void vApplicationIdleHook( void ) вольного параметра в задачу при ее создании.
{
на экран каждые 250 мс. /* Увеличить переменную-счетчик на 1 */ Внимание было уделено механизму приори-
Текст учебной программы № 3: ulIdleCycleCount++; тетов и тому, как значение приоритета влияет
}
на ход выполнения задачи. Рассказано о воз-
#include <stdlib.h>
#include <stdio.h>
можностях FreeRTOS для реализации задер-
#include <string.h> Результат выполнения учебной программы жек и периодического выполнения задачи.
#include “FreeRTOS.h”
#include “task.h” № 3 приведен на рис. 14. Видно, что за 250 мс, Изучена задача Бездействие и возможности,
пока задача вывода значения на экран пребы- которые она предоставляет.
/* Глобальная переменная-счетчик, которая будет увеличиваться на 1
при каждом вызове функции задачи Бездействие */ вает в блокированном состоянии, функция В следующих публикациях будет подроб-
volatile unsigned long ulIdleCycleCount = 0; задачи Бездействие «успевает выполниться» но описан процесс принудительного измене-
/*-----------------------------------------------------------*/ большое количество раз. ния приоритета задач в ходе их выполнения,
/* Функция, реализующая задачу вывода на экран значения Учебная программа №  3  затрагивает показано, как динамически создавать и уни-
ulIdleCycleCount
каждые 250 мс */ еще один очень важный аспект написания чтожать задачи. Будет подведен итог по вы-
void vTaskFunction( void *pvParameters ) программ, работающих под управлением тесняющей многозадачности во FreeRTOS
ОСРВ, — одновременное использование и рассказано о возможностях кооперативной
одного аппаратного ресурса различными за- многозадачности. Далее внимание будет сфо-
дачами. В нашем случае в качестве такого кусировано на взаимодействии и передаче ин-
ресурса выступает глобальная переменная, формации между задачами и между преры-
доступ к которой осуществляет как задача ваниями и задачами средствами FreeRTOS. n
Бездействие, так и задача отображения зна-
чения этой переменной. При совместном Литература
доступе нескольких задач к общей перемен-
ной возможна ситуация, когда выполнение 1. Курниц А. FreeRTOS — операционная система
одной задачи прерывается планировщиком для микроконтроллеров // Компоненты и тех-
именно в тот момент, когда задача модифи- нологии. 2011. № 2–3.
цирует общую переменную, когда та еще со- 2. http://www.openwatcom.org/index.php/
держит не окончательное (искаженное) зна- Download
чение. При этом результат работы другой 3. http://www.dosbox.com
задачи, которая получит управление и об- 4. Barry R. Using the FreeRTOS real time kernel:
ратится к этой переменной, также окажется A Practical Guide. 2009.
искаженным. 5. http://www.freertos.org

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 4 '2011


микроконтроллеры компоненты 97

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров

В этой статье будет продолжено изучение FreeRTOS — операционной систе-


мы для микроконтроллеров. Здесь описан процесс принудительного изме-
нения приоритета задач в ходе их выполнения, показано, как динамически
создавать и уничтожать задачи. Рассмотрен вопрос о том, как расходуется
память при создании задач. Подведен итог по вытесняющей многозадачности
во FreeRTOS и рассказано о стратегии назначения приоритетов задачам под
названием Rate Monotonic Scheduling. Далее мы обсудим тему кооператив-
ной многозадачности, ее преимущества и недостатки и приведем пример про-
граммы, использующей кооперативную многозадачность во FreeRTOS. Автор
Андрей Курниц уделил внимание и альтернативным схемам планирования: гибридной много-
kurnits@stim.by задачности и вытесняющей многозадачности без разделения времени.

Динамическое Назначение параметров и возвращаемое /* Функция Задачи 1 */


изменение приоритета значение: void vTask1( void *pvParameters )
{
1. pxTask — дескриптор задачи, приоритет ко- unsigned portBASE_TYPE uxPriority;
При создании задачи ей назначается опре- торой необходимо получить. Если необхо-
/* Получить приоритет Задачи 1. Он равен 2 и не изменяется
деленный приоритет. Однако во FreeRTOS димо получить приоритет задачи, которая на протяжении всего времени
есть возможность динамически изменять вызывает API-функцию uxTaskPriorityGet(), работы учебной программы № 1 */
uxPriority = uxTaskPriorityGet( NULL );
приоритет уже созданной задачи, даже после то в качестве параметра pxTask следует за-
запуска планировщика. Для динамического дать NULL. for( ;; )
{
изменения приоритета задачи служит API- 2. Возвращаемое значение — непосредствен- /* Сигнализировать о выполнении Задачи 1 */
функция vTaskPrioritySet(). Ее прототип: но значение приоритета. puts(“Task1 is running” );

Наглядно продемонстрировать исполь- /* Сделать приоритет Задачи 2 на единицу больше


void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_ зование API-функций vTaskPrioritySet() приоритета Задачи 1 (равным 3).
TYPE uxNewPriority ); Получить доступ к Задаче 2 из тела Задачи 1 позволяет
и uxTaskPriorityGet() позволяет учебная про- дескриптор Задачи 2, который сохранен в глобальной
грамма № 1: переменной xTask2Handle*/
puts( “To raise the Task2 priority” );
Назначение параметров: vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
#include <stdlib.h>
1. pxTask — дескриптор (handle) задачи, #include <stdio.h> /* Теперь приоритет Задачи 2 выше. Задача 1
приоритет которой необходимо изменить. #include <string.h> продолжит свое выполнениение лишь тогда,
#include “FreeRTOS.h” когда приоритет Задачи 1 будет уменьшен. */
Дескриптор задачи может быть получен при #include “task.h” }
создании экземпляра задачи API-функцией vTaskDelete( NULL );
/* Прототипы функций, которые реализуют задачи. */ }
xTaskCreate() (параметр pxCreatedTask [1, void vTask1( void *pvParameters );
№ 3]). Если необходимо изменить приори- void vTask2( void *pvParameters ); /*-----------------------------------------------------------*/
/* Функция Задачи 2 */
тет задачи, которая вызывает API-функцию /* Глобальная переменная для хранения приоритета Задачи 2 */ void vTask2( void *pvParameters )
vTaskPrioritySet(), то в качестве параметра xTaskHandle xTask2Handle; {
unsigned portBASE_TYPE uxPriority;
pxTask следует задать NULL. /*-----------------------------------------------------------*/
2. uxNewPriority — новое значение при- int main( void )
/* Получить приоритет Задачи 2. Так как после старта
планировщика Задача 1 имеет более высокий приоритет,
оритета, который будет присвоен за- {
то если Задача 2 получает управление, значит, ее приоритет
/* Создать Задачу 1, присвоив ей приоритет 2.
даче. При задании приоритета больше Передача параметра в задачу, как и получение дескриптора
был повышен до 3 */
uxPriority = uxTaskPriorityGet( NULL );
(configMAX_PRIORITIES — 1) приоритет задачи, не используется */
xTaskCreate( vTask1, “Task 1”, 1000, NULL, 2, NULL );
будет установлен равным (conf igMAX_ for( ;; )
{
PRIORITIES — 1). /* Создать Задачу 2 с приоритетом = 1, меньшим,
/* Сигнализировать о выполнении Задачи 2 */
чем у Задачи 1. Передача параметра не используется.
Прежде чем изменить приоритет какой-либо Получить дескриптор создаваемой задачи в переменную
puts( “Task2 is running” );
задачи, может оказаться полезной возмож- xTask2Handle */
/* Задача 2 понижает свой приоритет на 2 единицы
xTaskCreate( vTask2, “Task 2”, 1000, NULL, 1, &xTask2Handle );
ность предварительно получить значение ее (становится равен 1). Таким образом, он становится ниже
приоритета Задачи 1, и Задача 1 получает управление */
приоритета. API-функция uxTaskPriorityGet() /* Запустить планировщик. Задачи начнут выполняться.
puts( “To lower the Task2 priority” );
Причем первой будет выполнена Задача 1 */
позволяет это сделать. Ее прототип: vTaskStartScheduler(); vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
return 0; vTaskDelete( NULL );
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask ); } }
/*-----------------------------------------------------------*/ /*-----------------------------------------------------------*/

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 5 '2011 www.kite.ru


98 компоненты микроконтроллеры

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

В программе создана Задача 1 с приорите- щью API-функции vTaskDelete(). Удаленная return 0;


том 2 и Задача 2 с приоритетом 1. Задача 1 задача физически не существует и, следова- }
/*-----------------------------------------------------------*/
повышает приоритет Задачи 2 так, чтобы тельно, никогда не выполняется. Нет воз- /* Функция Задачи 1 */
он превысил приоритет Задачи 1. Задача 2, можности восстановить удаленную задачу, void vTask1( void *pvParameters )
{
в свою очередь, понижает свой же приоритет единственный выход — создать новую.
так, чтобы он стал ниже приоритета Задачи 1. Ядро FreeRTOS устроено так, что внутрен- for( ;; )
{
Задача 1, как и Задача 2, не переходит в блоки- няя реализация задачи Бездействие отвечает /* Сигнализировать о выполнении Задачи 1 */
рованное состояние. Обе задачи поочередно за освобождение памяти, которую использо- puts(“Task1 is running” );

получают процессорное время за счет перио- вала удаленная задача. К программам, в кото- /* Динамически (после старта планировщика) создать
дического изменения приоритета Задачи 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(); жении короткого промежутка времени, пока
любую другую задачу в программе с помо- она выполняется. Таким образом, используя

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 5 '2011


микроконтроллеры компоненты 99

Рис. 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 '2011 www.kite.ru


100 компоненты микроконтроллеры

а б в а б в

Рис. 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() доступна только при

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 5 '2011


микроконтроллеры компоненты 101

использовании схем 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():

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 5 '2011 www.kite.ru


102 компоненты микроконтроллеры

/*-----------------------------------------------------------*/
/* Функция, реализующая задачу */
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
на прерывание. В этом случае в конце тела ем времени. Процессорное время не тратится
Литература

1. Курниц А. FreeRTOS — операционная система


для микроконтроллеров // Компоненты и тех-
нологии. 2011. № 2–4.
2. Barry R. Using the freertos real time kernel:
A Practical Guide. 2009.
3. http://www.freertos.org
4. http://en.wikipedia.org/wiki/Rate-monotonic_
scheduling
5. http://www.4stud.info/rtos/lecture3.html
6. http://ru.wikipedia.org/wiki/Реентерабель-
ность
7. http://ru.wikipedia.org/wiki/Malloc
8. http://peguser.narod.ru/translations/files/tlsf.
pdf

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 5 '2011


98 компоненты микроконтроллеры

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров

Мы продолжаем изучение FreeRTOS — операционной системы для микро-


контроллеров. В пятой части статьи основное внимание сфокусировано
на очередях — безопасном механизме взаимодействия задач друг с дру-
гом. Будут показаны опасности организации взаимодействия между зада-
чами «напрямую» и обосновано применение очередей, а также рассказано
об основных принципах, заложенных в функционирование очередей.
Читатель узнает о том, как создать очередь, как записать данные в очередь
и прочитать их оттуда. Будут освещены вопросы целесообразного выбора
Андрей КУРНИЦ типа данных, хранящихся в очереди, и назначения приоритетов задачам,
kurnits@stim.by которые записывают и считывают данные из очереди.

Необходимость явлена глобальная переменная 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 (;;);
}

Результаты работы показывают (рис. 1),


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

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011


микроконтроллеры компоненты 99

пасным в многозадачной среде (thread safe).


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

Характеристики очередей

Хранение информации в очереди


б
Информация хранится в очереди в виде
элементов (items) — блоков памяти фик-
сированного размера. В качестве элемента
очереди может выступать любая переменная
языка Си. В случае если это переменная типа
char, размер блока будет равен 1 байту, если
это структура или массив, размер блока бу-
дет равен, соответственно, размеру структу-
ры или массива.
Элементы очереди в контексте обмена ин-
в
формацией между задачами будем называть
сообщениями.
Запись элемента в очередь приводит к со-
зданию побайтовой копии элемента в оче-
реди. Побайтовое копирование происходит
и при чтении элемента из очереди.
Очередь может хранить в себе конечное
число элементов фиксированного размера.
Максимальное число элементов, которое
может хранить очередь, называется разме- г
ром очереди. Как размер элемента, так и раз-
мер очереди задаются при создании очереди
и остаются неизменными до ее удаления.
Важно отметить, что память выделяется
сразу под все элементы очереди, то есть пу-
стая и заполненная очередь не отличаются
друг от друга по объему занимаемой памяти.
При записи элементов в очередь динамиче-
ского выделения памяти не происходит.
Очередь функционирует по принципу д
«первым вошел — первым вышел» (First In
First Out, FIFO), то есть элемент, который
раньше остальных был помещен в очередь
(в конец очереди), будет и прочитан раньше
остальных (рис. 2). Обычно элементы запи-
сываются в конец («хвост») очереди и считы-
ваются с начала («головы») очереди.
На рис. 2а показаны очередь длиной 5 эле-
ментов для хранения целочисленных пере-
менных, Задача 1, которая будет записывать Рис. 2. Запись и чтение элементов из очереди по принципу FIFO
элементы в очередь, и Задача 2, которая будет
считывать элементы из очереди. В исходном
состоянии очередь не содержит ни одного «первым вошел — первым вышел», так как ципу «последним вошел — первым вышел»
элемента, то есть пуста. элемент «15» первым записан в очередь и, со- (Last In First Out, LIFO).
На рис. 2б Задача 1 записывает число «15» ответственно, первым из нее считан. Теперь
в конец очереди. Так как теперь очередь со- очередь снова содержит один элемент («69») Доступ из множества задач
держит 1 элемент, то он является одновре- в начале очереди, который и будет считан Очередь — это самостоятельный объект
менно и началом, и концом очереди. при следующем чтении из очереди Задачей 2 ядра, она не принадлежит ни одной конкрет-
На рис. 2в Задача 1 записывает еще один (рис. 2д). ной задаче. Напротив, любое количество за-
элемент («69») в конец очереди. Теперь оче- Следует отметить, что на рис. 2 показано дач могут как читать, так и записывать данные
редь содержит 2 элемента, причем элемент использование API-функций для работы в одну и ту же очередь. Следует отметить, что
«15» находится в начале очереди, а элемент с очередями в упрощенном виде. Корректное ситуация, когда в очередь помещают данные
«69» — в конце. их применение будет описано ниже. сразу несколько задач, является «обычным де-
На рис. 2г Задача 2 считывает элемент, на- Также имеется возможность помещать лом» для программ под управлением ОСРВ,
ходящийся в начале очереди, то есть элемент элементы в начало очереди, тогда очередь однако чтение данных несколькими задачами
«15». Таким образом, выполняется принцип превращается в стек, работающий по прин- из одной очереди встречается редко.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011 www.kit3e.ru


100 компоненты микроконтроллеры

Блокировка при чтении из очереди 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 ); Чтение элемента из очереди может быть
реди, она возвращает переменную типа произведено двумя способами:

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011


микроконтроллеры компоненты 101

• Элемент считывается из очереди (создается – 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, в которой реа-

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011 www.kit3e.ru


102 компоненты микроконтроллеры

Рис. 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 (;;)
;
}

Результат выполнения учебной програм-


мы № 2 показан на рис. 3, на котором видно,
что задача-приемник получает сообщения
от обеих задач-передатчиков.
В момент времени (0) (рис. 4) происходит
запуск планировщика, который переводит
в состояние выполнения задачу с наивысшим
приоритетом — задачу-приемник. В момент
времени (1) задача-приемник пытается прочи-
тать элемент из очереди, однако очередь после
создания пуста, и задача-приемник переходит
в блокированное состояние до момента появ-
ления данных в очереди либо до момента исте-
чения тайм-аута 100 мс. В состояние выполне- Рис. 5. Пример организации обмена информацией между задачами
ния переходит одна из задач-передатчиков, ка-

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011


микроконтроллеры компоненты 103

• iMeaning — значение, смысл передаваемо- /* Определить массив из двух структур, которые будут * При условии, что очередь должна быть полна, означает
го через очередь параметра; * записываться в очередь */ * аварийную ситуацию */
• iValue — числовое значение параметра. static const xData xStructsToSend[ 2 ] =
{ }
puts(“Could not receive from the queue.\r\n”);

Задача ПИД-регулятора частоты враще- { 100, mainSENDER_1 }, /* Используется задачей-передатчиком 1 */ }


{ 200, mainSENDER_2 } /* Используется задачей-передатчиком 2 */ }
ния двигателя ответственна за главную };
функцию устройства — поддержание ча- /*------------------------------------------------------------------------*/
/*-----------------------------------------------------------*/ /* Точка входа. С функции main() начнется выполнение программы. */
стоты вращения на заданном уровне. Задача /* Функция, реализующая задачи-передатчики */ int main(void) {
ПИД-регулятора должна соответствующим void vSenderTask(void *pvParameters) { /* Создать очередь размером 3 элемента для хранения
/* Будет создано несколько экземпляров задачи. В качестве * структуры типа xData.
образом реагировать на действия оператора * параметра задаче будет передан указатель на структуру xData. */ * Размер элемента установлен равным размеру структуры xData.
и команды по CAN-интерфейсу, она полу- /* Переменная, которая будет хранить результат выполнения * Дескриптор созданной очереди сохранить в глобальной
* xQueueSendToBack(): */ * переменной xQueue. */
чает информацию о внешних воздействиях, portBASE_TYPE xStatus; xQueue = xQueueCreate(3, sizeof(xData));
считывая сообщения из очереди. /* Если очередь успешно создана (дескриптор не равен NULL) */
/* Бесконечный цикл */ if (xQueue != NULL) {
Задача обслуживания CAN-интерфейса for (;;) { /* Создать 2 экземпляра задачи-передатчика. Параметр,
отвечает за обработку входящих по CAN- /* Записать структуру в конец очереди. * передаваемый задаче при ее создании, указатель на структуру,
* 1-й параметр — дескриптор очереди, в которую будет * которую экземпляр задачи-передатчика
шине сообщений, декодирует их и посылает * производиться запись, очередь создана до запуска * будет записывать в очередь.
сообщение в виде структуры xData в задачу * планировщика, и ее дескриптор сохранен в глобальной * Задача-передатчик 1 будет постоянно записывать структуру
* переменной xQueue. * xStructsToSend[ 0 ].
ПИД-регулятора. Значение члена структу- * 2-й параметр — указатель на структуру, которая будет * Задача-передатчик 2 будет постоянно записывать структуру
ры iMeaning «установка скорости» позволяет * записана в очередь, в данном случае указатель передается * xStructsToSend[ 1 ].
* при создании экземпляра задачи (pvParameters). * Обе задачи создаются с приоритетом 1.
задаче ПИД-регулятора определить, что зна- * 3-й параметр — продолжительность тайм-аута, в течение */
чение iValue, равное 600, есть не что иное, как * которого задача будет находиться в блокированном xTaskCreate(vSenderTask, “Sender1”, 1000, ( void * ) &(
* состоянии, ожидая появления свободного места в очереди. xStructsToSend[ 0 ] ), 2, NULL);
новое значение уставки скорости вращения. * Макроопределение portTICK_RATE_MS используется для xTaskCreate(vSenderTask, “Sender2”, 1000, ( void * ) &(
* преобразования времени 100 мс в количество системных xStructsToSend[ 1 ] ), 2, NULL);
Задача обслуживания человеко-машин- * квантов. */ /* Создать задачу-приемник, которая будет считывать числа
ного интерфейса ответственна за взаимодей- xStatus = xQueueSendToBack(xQueue, pvParameters, 100 / * из очереди.
portTICK_RATE_MS); * Приоритет = 2, то есть выше, чем у задач-передатчиков.
ствие оператора с контроллером двигателя. if (xStatus != pdPASS) { */
Оператор может вводить значения параме- /* Запись в очередь не произошла по причине того, что xTaskCreate(vReceiverTask, “Receiver”, 1000, NULL, 1, NULL);
* очередь на протяжении тайм-аута оставалась заполненной. /* Запуск планировщика. Задачи начнут выполняться. */
тров, давать команды контроллеру, наблюдать * Такая ситуация свидетельствует об ошибке, так как vTaskStartScheduler();
его текущее состояние. Когда оператор нажал * очередь-приемник создаст свободное место в очереди, } else {
* как только обе задачи-передатчика перейдут /* Если очередь не создана */
кнопку аварийной остановки двигателя, за- * в блокированное состояние */ }
дача обслуживания человеко-машинного ин- puts(“Could not send to the queue.\r\n”); /* При успешном создании очереди и запуске планировщика
} * программа никогда “не дойдет” до этого места. */
терфейса сформировала соответствующую /* Сделать принудительный вызов планировщика, позволив, for (;;)
структуру xData. Поле iMeaning указывает * таким образом, выполняться другой задаче-передатчику. ;
* Переключение на другую задачу произойдет быстрее, чем }
на нажатие оператором некоторой кноп- * окончится текущий квант времени. */
ки, а поле iValue — уточняет какой именно: taskYIELD();
}
кнопки аварийного останова. Такого рода со- } Результат выполнения учебной програм-
общения (связанные с возникновением ава- мы № 3 показан на рис. 6, на котором видно,
/*------------------------------------------------------------------------*/
рийной ситуации) целесообразно помещать /* Функция, реализующая задачу-приемник */ что теперь задача-приемник владеет инфор-
не в конец, а в начало очереди, так, чтобы за- void vReceiverTask(void *pvParameters) { мацией о том, какая именно задача передала
/* Структура, в которую будет копироваться прочитанная из
дача ПИД-контроллера обработала их раньше * очереди структура */ то или иное сообщение.
остальных находящихся в очереди, сократив, xData xReceivedStructure; В момент времени (1) (рис. 7) управление
/* Переменная, которая будет хранить результат выполнения
таким образом, время реакции системы. * xQueueReceive() */ получает одна из задач-передатчиков, так как
Рассмотрим учебную программу № 3, в ко- portBASE_TYPE xStatus; приоритет их выше, чем у задачи-приемника.
/* Бесконечный цикл */
торой, как и в учебной программе № 2, будет for (;;) { Пусть это будет задача-передатчик 1. Она
две задачи-передатчика сообщений и одна /* Эта задача выполняется, только когда задачи-передатчики записывает первый элемент в пустую оче-
* находятся в блокированном состоянии, а за счет того, что
задача-приемник. Однако в качестве едини- * приоритет у них выше, блокироваться они могут, только редь и вызывает планировщик (момент
цы передаваемой информации на этот раз * если очередь полна. Поэтому очередь в этот момент должна времени (2)). Планировщик передает управ-
* быть полна. То есть текущее количество элементов
выступает структура, которая содержит све- * очереди должно быть равно ее размеру — 3. */ ление другой задаче с таким же приорите-
if (uxQueueMessagesWaiting(xQueue) != 3) {
дения о задаче, которая передала это сообще- puts(“Queue should have been full!\r\n”);
том, то есть задаче-передатчику 2. Та запи-
ние. Кроме того, продемонстрирована дру- } сывает еще один элемент в очередь (теперь
/* Прочитать структуру из начала очереди.
гая схема назначения приоритетов задачам, * 1-й параметр — дескриптор очереди, из которой будет
в очереди 2 элемента) и отдает управление
когда задача-приемник имеет более низкий * происходить чтение, очередь создана до запуска задаче-передатчику 1 (момент времени (3)).
* планировщика, и ее дескриптор сохранен в глобальной
приоритет, чем задачи-передатчики. * переменной xQueue.
Задача-передатчик 1 записывает 3-й элемент
* 2-й параметр — указатель на буфер, в который будет в очередь, теперь очередь заполнена. Когда
#include <stdlib.h> * скопирована структура из очереди. В данном случае —
#include <stdio.h> * указатель на структуру xReceivedStructure. управление передается задаче-передатчику 2,
#include <string.h> * 3-й параметр — продолжительность тайм-аута. В данном она обнаруживает, что не может запи-
#include “FreeRTOS.h” * случае задана равной 0, что означает задача не будет
#include “task.h” * “ожидать”, если очередь пуста. Однако так как эта задача сать новый элемент в очередь, и переходит
#include “queue.h” * получает управление, только если очередь полна, то чтение в блокированное состояние (момент вре-
* элемента из нее будет всегда возможно.
/* Номера функций-передатчиков сообщений */ */ мени (5)). Управление снова получает задача-
#define mainSENDER_1 1 xStatus = xQueueReceive(xQueue, &xReceivedStructure, 0); передатчик 1, однако очередь по-прежнему
#define mainSENDER_2 2 if (xStatus == pdPASS) {
/* Структура успешно принята, вывести на экран название заполнена, и задача-передатчик 1 также бло-
/* Объявить переменную-дескриптор очереди. Эта переменная * задачи, которая эту структуру поместила в очередь, кируется в ожидании освобождения места
* будет использоваться для ссылки на очередь после ее создания. */ * и значение абстрактного параметра */
xQueueHandle xQueue; if (xReceivedStructure.ucSource == mainSENDER_1) { в очереди (момент времени (6)).
/* Определить структуру, которая будет элементом очереди */
printf(“From Sender 1 = %d\r\n”, xReceivedStructure.ucValue); Так как все задачи с приоритетом 2 теперь
} else {
typedef struct printf(“From Sender 2 = %d\r\n”, xReceivedStructure.ucValue); блокированы, управление получает задача-
{ }
unsigned char ucValue;
приемник, приоритет которой ниже и ра-
} else {
unsigned char ucSource; /* Данные не были прочитаны из очереди. вен «1» (момент времени (6)). Она считывает
} xData;
один элемент из очереди, освобождая таким

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011 www.kit3e.ru


104 компоненты микроконтроллеры

Рис. 6. Результат выполнения учебной программы № 3 Рис. 7. Последовательность выполнения задач в учебной программе № 3

образом место в очереди. Как только в оче- Использование очередей что несколько задач не будут одновре-
реди появилось свободное место, планиров- для передачи больших объемов данных менно обращаться к памяти, на которую
щик выведет из состояния блокировки ту за- Если размер одного элемента очереди ссылается указатель. В идеальном случае
дачу из числа «ожидавших», которая дольше достаточно велик, то предпочтительно ис- только задача-передатчик должна иметь
остальных пребывала блокированной. В на- пользовать очередь для хранения не самих доступ к памяти, пока указатель на эту
шем случае это задача-передатчик 2 (момент элементов, а для хранения указателей на эле- память находится в очереди. Когда же
времени (7)). Так как приоритет у нее выше, менты (например, на массивы или на струк- указатель прочитан из очереди, только
она вытеснит задачу-приемник и запишет туры). задача-приемник должна иметь возмож-
следующий элемент в очередь. После чего Преимущества такого подхода: ность доступа к памяти.
она вызовет планировщик API-функцией • Экономия памяти. Память при созда- • Память, на которую ссылается указатель,
taskYIELD(). Однако готовых к выполнению нии очереди выделяется под все элемен- должна существовать. Это требование ак-
задач с более высоким или равным приори- ты очереди, даже если очередь пуста. туально, если указатель ссылается на дина-
тетом на этот момент нет, поэтому пере- Использование небольших по объему за- мически выделенную память. Только одна
ключения контекста не произойдет, и задача- нимаемой памяти указателей вместо объ- задача должна быть ответственна за осво-
передатчик 2 продолжит выполняться. Она емных структур или массивов позволяет бождение динамически выделенной памя-
попытается записать в очередь еще один достичь существенной экономии памяти. ти. Задачи не должны обращаться к памя-
элемент, но очередь заполнена, и задача- • Меньшее время записи элемента в очередь ти, если та уже была освобождена.
передатчик 2 перейдет в блокированное со- и чтения его из очереди. При записи/чте- • Нельзя использовать указатель на пере-
стояние (момент времени (8)). нии элемента из очереди происходит его менные, расположенные в стеке задачи,
Снова сложилась ситуация, когда все вы- побайтовое копирование. Копирование то есть указатель на локальные перемен-
сокоприоритетные задачи-передатчики бло- указателя выполняется быстрее копирова- ные задачи. Данные, на которые ссылается
кированы, поэтому управление получит ния объемных структур данных. указатель, будут неверными после очеред-
низкоприоритетная задача-приемник (8). Тем не менее использование указателей ного переключения контекста.
Однако на этот раз после появления свобод- в качестве элементов очереди сопряжено
ного места в очереди разблокируется задача- с некоторыми трудностями, преодоление Выводы
передатчик 1, так как теперь ее время пребы- которых ложится на плечи программиста.
вания в блокированном состоянии превыша- Для достижения корректной работы про- В этой части статьи был подробно описан
ет время задачи-передатчика 2, и т. д. граммы должны быть выполнены следую- механизм очередей как средства межзадач-
Следует отметить, что в ранее приведен- щие условия: ного взаимодействия. Показаны основные
ном примере, когда задачи-передатчики • У памяти, адресуемой указателем, в каж- способы организации такого взаимодей-
имеют более высокий приоритет, чем задача- дый момент времени должна быть одна ствия. Однако существуют еще несколько
приемник, в очереди в любой момент вре- четко определенная задача-хозяин, ко- API-функций для работы с очередями, кото-
мени не может быть более одного свободного торая может обращаться к этой памя- рые используются только для отладки ядра
места. ти. То есть необходимо гарантировать, FreeRTOS. О них будет рассказано в дальней-
ших публикациях, посвященных возможно-
стям отладки и трассировки. В следующей же
публикации внимание будет сконцентриро-
вано на особенностях обработки прерываний
микроконтроллера в среде FreeRTOS. ■

Литература

1. Курниц А. FreeRTOS — операционная система


для микроконтроллеров // Компоненты и тех-
нологии. 2011. № 2–5.
2. Barry R. Using the FreeRTOS real time kernel.
A Practical Guide. 2009.
3. www.freertos.org
4. http://ru.wikipedia.org/wiki/Очередь_(програм-
мирование)

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 6 '2011


микроконтроллеры 23

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров

В этой статье мы продолжаем знакомить читателя с созданием программ,


Андрей Курниц работающих под управлением FreeRTOS — операционной системы для
kurnits@stim.by микроконтроллеров.

Введение 3) Как обработчики прерываний связаны на прерывание обработчик прерывания вы-


с остальным кодом и как организовать полняет только первичные действия, напри-
Шестая часть статьи посвящена взаимо- программу, чтобы обеспечить наибы- мер считывает данные. Затем львиную долю
действию прерываний с остальной частью стрейшую обработку асинхронных собы- обработки берет на себя задача-обработчик
программы и поможет читателям ответить тий внешнего мира? прерывания. Такая организация обработки
на следующие вопросы: FreeRTOS не предъявляет никаких требо- прерываний называется отложенной обра-
1) Какие API-функции и макросы можно ис- ваний к организации обработки событий, боткой. При этом обработчик прерывания
пользовать внутри обработчиков преры- однако предоставляет удобные возможности выполняет только самые «экстренные» дей-
ваний? для такой организации. ствия, а основная обработка «откладывает-
2) Как реализовать отложенную обработку Прерывание (interrupt) — это событие ся», пока ее не выполнит задача-обработчик
прерываний? (сигнал), заставляющее микроконтроллер прерывания.
3) Как создавать и использовать двоичные изменить текущий порядок исполнения ко-
и счетные семафоры? манд. При этом выполнение текущей после- Двоичные семафоры
4) Как использовать очереди для передачи довательности команд приостанавливается, Двоичные семафоры предназначены для
информации в обработчик прерывания и управление передается обработчику пре- эффективной синхронизации выполне-
и из него? рывания — подпрограмме, которую можно ния задачи с возникновением прерывания.
5) Каковы особенности обработки вложен- представить функцией языка Си. Обработчик Они позволяют переводить задачу из со-
ных прерываний во FreeRTOS? прерывания реагирует на событие и обслу- стояния блокировки в состояние готовности
живает его, после чего возвращает управле- к выполнению каждый раз, когда происходит
События и прерывания ние в прерванный код [6]. Прерывания ини- прерывание. Это дает возможность перене-
циируются периферией микроконтроллера, сти бóльшую часть кода, отвечающего за об-
Встраиваемые микроконтроллерные си- например прерывание от таймера/счетчика работку внешнего события, из обработчика
стемы функционируют, отвечая действиями или изменение логического уровня на выво- прерывания в тело задачи, выполнение кото-
на события внешнего мира. Например, полу- де микроконтроллера. рой синхронизировано с соответствующим
чение Ethernet-пакета (событие) требует обра- Следует заметить, что во FreeRTOS все прерыванием. Внутри обработчика прерыва-
ботки в задаче, которая реализует TCP/IP-стек API-функции и макросы, имена которых ния останется лишь небольшой, быстро вы-
(действие). Обычно встраиваемые системы заканчиваются на FromISR или FROM_ISR, полняющийся фрагмент кода. Говорят, что
обслуживают события, которые приходят предназначены для использования в обра- обработка прерывания отложена и непосред-
от множества источников, причем каждое со- ботчиках прерываний и должны вызываться ственно выполняется задачей-обработчиком.
бытие имеет свое требование по времени ре- только внутри них. Если прерывание происходит при возник-
акции системы и расходам времени на его об- новении особенно критичного к времени ре-
работку. При разработке встраиваемой Отложенная обработка акции внешнего события, то имеет смысл на-
микроконтроллерной системы необходимо прерываний значить задаче-обработчику достаточно вы-
подобрать свою стратегию реализации обслу- сокий приоритет, чтобы при возникновении
живания событий внешнего мира. При этом При проектировании встраиваемой прерывания она вытесняла другие задачи
перед разработчиком возникает ряд вопросов: микроконтроллерной системы на  осно- в системе. Это произойдет, когда завершит
1) Каким образом события будут регистриро- ве FreeRTOS необходимо учесть, насколько свое выполнение обработчик прерывания.
ваться? Обычно применяют прерывания, долго продолжается процесс обработки пре- Выполнение задачи-обработчика начинается
однако возможен и опрос состояния вы- рывания. В самом простом случае, когда при сразу же после окончания выполнения об-
водов микроконтроллера. обработке прерывания повторные прерыва- работчика прерывания. Создается впечатле-
2) В случае использования прерываний необ- ния запрещены, временные задержки в об- ние, что весь код, отвечающий за обработку
ходимо решить, какую часть программного работчике прерываний могут существенно внешнего события, реализован внутри об-
кода, реализующего обработку события, по- ухудшить время реакции системы на собы- работчика прерывания (рис. 1).
местить внутри обработчика прерывания, тия. Тогда для выполнения продолжитель- На рис. 1 видно, что прерывание преры-
а какую — вне обработчика. Обычно стара- ных действий по обработке прерывания вает выполнение одной задачи и возвращает
ются сократить размер обработчика преры- вводится так называемый «отложенный» ре- управление другой. В момент времени (1)
вания настолько, насколько это возможно. жим их выполнения [5]. В процессе реакции выполняется прикладная задача, когда про-

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011 www.kite.ru


24 микроконтроллеры

фор, но никогда не отдает его обратно. Такой


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

Работа с двоичными семафорами

Рис. 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. Синхронизация прерывания и задачи-обработчика с помощью двоичного семафора
выдачу семафора.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011


микроконтроллеры 25

Все API-функции работы с  семафора-


ми сосредоточены в заголовочном файле
/Source/Include/semphr.h, поэтому следует а
убедиться, что этот файл находится в списке
включенных (#include) в проект.
Доступ ко всем семафорам во FreeRTOS
(а не только к двоичным) осуществляется
с помощью дескриптора (идентификато-
ра) — переменной типа xSemaphoreHandle.
б
Создание двоичного семафора
Семафор должен быть явно создан перед
первым его использованием. API-функция
vSemaphoreCreateBinary() служит для созда-
ния двоичного семафора.

void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );

Рис. 3. Результат вызова xSemaphoreGiveFromISR():


а) без принудительного переключения контекста; б) с принудительным переключением контекста
Единственным аргументом является де-
скриптор семафора, в него будет возвращен
дескриптор в случае успешного создания portMAX_DELAY приведет к тому, что вы- 1. xSemaphore — дескриптор семафора, ко-
семафора. Если семафор не создан по при- хода из блокированного состояния по ис- торый должен быть в явном виде создан
чине отсутствия памяти, вернется значение течении времени тайм-аута не произойдет. до первого использования.
NULL. Так как vSemaphoreCreateBinary() Задача будет сколь угодно долго «ожидать» 2. pxHigherPriorityTaskWoken — значение
представляет собой макрос, то аргумент возможности захватить семафор, пока та- по  адресу pxHigherPriorityTaskWoken
xSemaphore следует передавать напрямую, кая возможность не появится. Для этого устанавливает сама API-функция
то есть нельзя использовать указатель на де- макроопределение INCLUDE_vTaskSuspend xSemaphoreGiveFromISR() в зависимости
скриптор и операцию переадресации. в файле FreeRTOSConfig.h должно быть от того, разблокирована ли более высоко-
равно «1». приоритетная задача в результате выдачи
Захват семафора • Возвращаемое значение — возможны два семафора. Подробнее об этом будет сказа-
Осуществляется API-функцией варианта: но далее.
xSemaphoreTake() и может вызываться толь- – pdPASS — свидетельствует об успешном 3. Возвращаемое значение — возможны два
ко из задач. В классической терминологии захвате семафора. Если определено вре- варианта:
[1] соответствует функции P() или wait(). мя тайм-аута (параметр xTicksToWait – pdPASS — вызов xSemaphoreGiveFromISR()
Чтобы задача смогла захватить семафор, он не равен 0), то возврат значения pdPASS был успешным, семафор отдан.
должен быть отдан другой задачей или об- говорит о том, что семафор стал досту- – pdFAIL — означает, что семафор в мо-
работчиком прерывания. Все типы семафо- пен до истечения времени тайм-аута мент вызова xSemaphoreGiveFromISR()
ров за исключением рекурсивных (о них — и был успешно захвачен. уже был доступен, то есть ранее отдан
в  следующей публикации) могут быть – pdFALSE — означает, что семафор недо- другой задачей или прерыванием.
захвачены с помощью xSemaphoreTake(). ступен (никто его не отдал). Если опре- Если после выдачи семафора в теле обра-
API-функцию xSemaphoreTake() нельзя вы- делено время тайм-аута (параметр ботчика прерывания была разблокирована
зывать из обработчиков прерываний. xTicksToWait не равен 0 или portMAX_ более высокоприоритетная задача, чем та, что
Прототип: DELAY), то возврат значения pdFALSE была прервана обработчиком прерывания,
говорит о том, что время тайм-аута ис- то API-функция xSemaphoreGiveFromISR()
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, текло, а семафор так и не стал доступен. установит *pxHigherPriorityTaskWoken рав-
portTickType xTicksToWait );
ным pdTRUE. В противном случае значение
Выдача семафора *pxHigherPriorityTaskWoken останется без
Назначение параметров и возвращаемое из обработчика прерывания изменений.
значение: Все типы семафоров во FreeRTOS, исклю- Значение *pxHigherPriorityTaskWoken
• xSemaphore  — дескриптор семафора. чая рекурсивные, могут быть выданы из тела необходимо отслеживать для того, чтобы
Должен быть получен с помощью API- обработчика прерывания при помощи API- «вручную» выполнить переключение контекста
функции создания семафора. функции xSemaphoreGiveFromISR(). задачи в конце обработчика прерывания, если
• xTicksToWait — максимальное количество API-функция xSemaphoreGiveFromISR() в результате выдачи семафора была разблоки-
квантов времени, в течение которого за- представляет собой специальную версию рована более высокоприоритетная задача. Если
дача может пребывать в блокированном API-функции xSemaphoreGive(), которая этого не сделать, то после выполнения обра-
состоянии, если семафор невозможно предназначена для вызова из тела обработчи- ботчика прерывания выполнение продолжит
захватить (семафор недоступен). Для ка прерывания. та задача, выполнение которой были прервано
представления времени в миллисекундах Прототип API-функции этим прерыванием (рис. 3). Ничего «страшно-
следует использовать макроопределение xSemaphoreGiveFromISR(): го» в этом случае не произойдет: текущая за-
portTICK_RATE_MS [2, КиТ № 4]). Задание дача будет выполняться до истечения текущего
xTicksToWait равным 0 приведет к тому, portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle кванта времени, после чего планировщик вы-
xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken );
что задача не перейдет в блокированное полнит переключение контекста (которое он
состояние, если семафор недоступен, выполняет каждый системный квант), и управ-
а продолжит свое выполнение сразу же. Назначение параметров и возвращаемое ление получит более высокоприоритетная за-
Установка xTicksToWait равным константе значение: дача (рис. 3а). Единственное, что пострадает, —

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011 www.kite.ru


26 микроконтроллеры

Рис. 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();
}
} Рис. 6. Результаты выполнения учебной программы № 1 при отсутствии принудительного переключения контекста

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011


микроконтроллеры 27

второе свое сообщение на дисплей и блокиру-


а ется на время 0,5 с. Система снова переходит
в состояние бездействия.
Если не выполнять принудительного пе-
реключения контекста, то есть исключить
из программы строку:

portSWITCH_CONTEXT();

б то можно наблюдать описанный ранее эф-


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

Счетные семафоры

Организация обработки прерываний с по-


мощью двоичных семафоров — отличное
решение, если частота возникновения одно-
го и того же прерывания не превышает неко-
торый порог. Если это же самое прерывание
е
возникнет до того, как задача-обработчик за-
вершит его обработку, то задача-обработчик
не перейдет в блокированное состояние по за-
вершении обработки предыдущего прерыва-
ния, а сразу же займется обслуживанием сле-
дующего. Предыдущее прерывание окажется
потерянным. Этот сценарий показан на рис. 7.
Таким образом, с использованием двоич-
ных семафоров из цепочки быстро следую-
Рис. 7. «Потеря» прерывания при обработке с помощью двоичного семафора щих друг за другом событий может быть об-
служено максимум одно событие.
Решить проблему обслуживания серии
работчик которого начинает выполняться переключению контекста задача-обработчик быстро следующих друг за другом событий
сразу же (2). Обработчик прерывания отда- получает управление (3). Задача-обработчик можно используя счетные семафоры.
ет семафор, поэтому разблокируется задача- выводит свое сообщение на дисплей и пы- В отличие от двоичных семафоров со-
обработчик, которая ожидала возможности тается снова захватить семафор, который стояние счетного семафора определяется
захватить этот семафор. Приоритет у задачи- уже недоступен, поэтому она блокируется. не значениями отдан/захвачен, а представ-
обработчика выше, чем у периодической за- Управление снова получает низкоприоритет- ляет собой целое неотрицательное число —
дачи, поэтому благодаря принудительному ная периодическая задача (4). Она выводит значение счетного семафора. И если двоич-

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011 www.kite.ru


28 микроконтроллеры

ный семафор — это, по сути, очередь дли-


ной в 1 элемент, то счетный семафор можно а
представить очередью в несколько элемен-
тов. Причем текущее значение семафора
представляет собой длину очереди, то есть
количество элементов, которые в данный
момент находятся в очереди. Значение эле-
ментов, хранящихся в очереди, когда она ис-
пользуется как счетный (или двоичный) се-
мафор, не важно, а важно само наличие или
отсутствие элемента. б
Существует два основных применения
счетных семафоров:
1. Подсчет событий. В этом случае обработчик
прерывания будет отдавать семафор, то есть
увеличивать его значение на единицу, ког-
да происходит событие. Задача-обработчик
будет захватывать семафор (уменьшать его
значение на единицу) каждый раз при обра-
ботке события. Текущее значение семафора в
будет представлять собой разность между
количеством событий, которые произошли,
и количеством событий, которые обрабо-
таны. Такой способ организации взаимо-
действия показан на рис. 8. При создании
счетного семафора для подсчета количества
событий следует задавать начальное его зна-
чение, равное нулю.
2. Управление доступом к ресурсам. В этом г
случае значение счетного семафора пред-
ставляет собой количество доступных ре-
сурсов. Для получения доступа к ресурсу
задача должна сначала получить (захва-
тить) семафор — это уменьшит значение
семафора на единицу. Когда значение се-
мафора станет равным нулю, это означает,
что доступных ресурсов нет. Когда задача
завершает работу с данным ресурсом, она д
отдает семафор — увеличивает его значе-
ние на единицу. При создании счетного се-
мафора для управления ресурсами следует
задавать начальное его значение равным
количеству свободных ресурсов. В даль-
нейших публикациях будет более подроб-
но освещена тема управления ресурсами
во FreeRTOS.
е
Работа со счетными семафорами

Создание счетного семафора


Как и другие объекты ядра, счетный сема-
фор должен быть явно создан перед первым
его использованием:

xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_


TYPE uxMaxCount,
unsigned portBASE_TYPE uxInitialCount );
Рис. 8. Подсчет событий с помощью счетного семафора

Назначение параметров и возвращаемое


значение: ступных ресурсов, если семафор использу- чать, что ни одного события еще не про-
1. uxMaxCount — задает максимально воз- ется для управления ресурсами. изошло. Если семафор используется для
можное значение семафора. Если проводить 2. uxInitialCount — задает значение сема- управления доступом к ресурсам, то сле-
аналогию с очередями, то он эквивалентен фора, которое он принимает сразу после дует установить uxInitialCount равным
размеру очереди. Определяет максимальное создания. Если семафор используется для максимальному значению — параметру
количество событий, которые может обра- подсчета событий, следует установить uxMaxCount. Это будет означать, что все
ботать семафор, или общее количество до- uxInitialCount равным 0, что будет озна- ресурсы свободны.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011


микроконтроллеры 29

что первая помещает элемент в начало очере-


ди, а вторая — в конец. В остальном их поведе-
ние идентично.
Рассмотрим их прототипы:
portBASE_TYPE xQueueSendToFrontFromISR( xQueueHandle
x Q u e u e , vo i d * p v I t e m To Q u e u e p o r t B A S E _ T Y P E
*pxHigherPriorityTaskWoken );

portBASE_TYPE xQueueSendToBackFromISR( xQueueHandle


x Q u e u e , vo i d * p v I t e m To Q u e u e p o r t B A S E _ T Y P E
*pxHigherPriorityTaskWoken );

Аргументы и возвращаемое значение:


1. xQueue — дескриптор очереди, в которую
Рис. 9. Обработка быстро следующих событий будет записан элемент. Дескриптор очере-
ди может быть получен при ее создании
API-функцией xQueueCreate().
3. Возвращаемое значение — равно NULL, /* Перед использованием счетный семафор необходимо создать. 2. pvItemToQueue — указатель на элемент, ко-
если семафор не создан по причине отсут- Семафор сможет обработать максимум 10 событий. Начальное торый будет записан в очередь. Размер эле-
значение = 0. */
ствия требуемого объема свободной памя- xBinarySemaphore = xSemaphoreCreateCounting( 10, 0 ); мента зафиксирован при создании очереди,
ти. Ненулевое значение означает успешное так что для побайтового копирования эле-
создание счетного семафора. Это значе- мента достаточно иметь указатель на него.
ние необходимо сохранить в переменной В модифицированном варианте искус- 3. pxHigherPriorityTaskWoken — значение
типа xSemaphoreHandle для обращения ственно создаются три быстро следующих *pxHigherPriorityTaskWoken устанавли-
к семафору в дальнейшем. друг за другом события. Каждому событию вается равным pdTRUE, если существует
API-функции выдачи (инкремента, уве- соответствует операция выдачи (инкремен- задача, которая «хочет» прочитать данные
личения на единицу) и захвата (декремен- та) счетного семафора. Задача-обработчик, из очереди, и приоритет у нее выше, чем
та, уменьшения на единицу) счетного сема- как и ранее, обрабатывает события, выполняя у задачи, выполнение которой прервало
фора ничем не отличаются от таковых для операцию захвата (декремента) семафора. прерывание. Если таковой задачи нет, то
двоичных семафоров: xSemaphoreTake() — Результат выполнения модифицированной значение *pxHigherPriorityTaskWoken оста-
з а х в а т с е м а ф о р а ; xSemaphoreGive(), учебной программы № 1 приведен на рис. 9. ется неизменным. Проанализировав зна-
xSemaphoreGiveFromISR() — выдача семафо- Судя по результатам работы (рис. 9), все чение *pxHigherPriorityTaskWoken после
ра, соответственно, из задачи и из обработ- три события были обработаны задачей- выполнения xQueueSendToFrontFromISR()
чика прерывания. обработчиком. Если же изменить тип исполь- или xQueueSendToBackFromISR(), можно
Продемонстрировать работу со счетными зуемого в программе семафора на двоичный, сделать вывод о необходимости принуди-
семафорами можно слегка модифициро- то результат выполнения программы не бу- тельного переключения контекста в конце
вав учебную программу № 1, приведенную дет отличаться от приведенного на рис. 4. Это обработчика прерывания. В этом случае
выше. Изменению подвергнется функция, будет свидетельствовать о том, что двоичный управление сразу перейдет разблокирован-
реализующая прерывание: семафор в отличие от счетного не может за- ной высокоприоритетной задаче.
фиксировать более одного события. 4. Возвращаемое значение — может прини-
/*-----------------------------------------------------------*/ мать 2 значения:
/* Обработчик прерывания */
static void __interrupt __far vExampleInterruptHandler( void ) Использование очередей – pdPASS — означает, что данные успешно
{ в обработчиках прерываний записаны в очередь.
static portBASE_TYPE xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE; – errQUEUE_FULL — означает, что данные
/* Отдать семафор задаче-обработчику несколько раз. Как было показано выше, семафоры пред- не записаны в очередь, так как очередь
Таким образом симулируется быстро следующая группа
событий, с которыми связано прерывание. Первая выдача назначены для передачи факта наступления заполнена.
разблокирует задачу-обработчик. Последующие будут события между задачами и прерываниями. API-функция xQueueReceiveFromISR()
“запомнены” счетным семафором и обработаны позже.
“Потери” событий не происходит. */ Очереди же можно использовать как для пе- служит для чтения данных с начала очереди.
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 );
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 , Ранее [2, КиТ № 6] мы говорили об API- чиков прерываний.
&xHigherPriorityTaskWoken ); функциях для работы с  очередями: Ее прототип:
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 ); xQueueSendToFront(), xQueueSendToBack()
if( xHigherPriorityTaskWoken == pdTRUE ) и xQueueReceive(). Использование их вну- portBASE_TYPE xQueueReceiveFromISR(
{ xQueueHandle pxQueue,
три тела обработчика прерывания приведет void *pvBuffer,
/* Макрос, выполняющий переключение контекста. к краху программы. Для этого существу- portBASE_TYPE *pxTaskWoken
* На других платформах имя макроса может быть другое! */ );
portSWITCH_CONTEXT(); ют версии этих функций, предназначен-
} ные для вызова из  обработчиков пре-
}
рываний: xQueueSendToFrontFromISR(), Аргументы и возвращаемое значение:
xQueueSendToBackFromISR() 1. xQueue — дескриптор очереди, из которой
API-функцию создания двоичного сема- и  xQueueReceiveFromISR(), причем вы- будет считан элемент. Дескриптор очереди
фора в главной функции main(): зов их из  тела задачи запрещен. API- может быть получен при ее создании API-
функция xQueueSendFromISR() яв- функцией xQueueCreate().
/* Перед использованием семафор необходимо создать. */ ляется полным эквивалентом функ- 2. pvBuffer — указатель на область памя-
vSemaphoreCreateBinary(xBinarySemaphore);
ции xQueueSendToBackFromISR(). ти, в которую будет скопирован элемент
Функции xQueueSendToFrontFromISR(), из очереди. Объем памяти, на которую
следует заменить функцией создания счетно- xQueueSendToBackFromISR() служат для запи- ссылается указатель, должен быть не мень-
го семафора: си данных в очередь и отличаются лишь тем, ше размера одного элемента очереди.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011 www.kite.ru


30 микроконтроллеры

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. Очередь
ционных проектах лишь для наглядности.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011


микроконтроллеры 31

ния еще не завершился, а возникает высоко-


приоритетное прерывание, и процессор начи-
нает выполнять его программу-обработчик.
Большинство портов FreeRTOS допу-
скает вложение прерываний. Эти порты
требуют задания одного или двух конфи-
гурационных макроопределений в файле
FreeRTOSConfig.h:
1. configKERNEL_INTERRUPT_PRIORITY —
задает приоритет прерывания, исполь-
зуемого для отсчета системных квантов
FreeRTOS. Если порт не использует ма-
кроопределение conf igMAX_SYSCALL_
Рис. 11. Результаты выполнения учебной программы № 2 INTERRUPT_PRIORITY, то для обеспече-
ния вложенности прерываний все прерыва-
ния, в обработчиках которых встречаются
API-функции FreeRTOS, должны иметь
этот же приоритет.
2. conf igMAX_SYSCALL_INTERRUPT_
PRIORITY — задает наибольший приоритет
прерывания, из обработчика которого мож-
но вызывать API-функции FreeRTOS (чтобы
прерывания могли быть вложенными).
Получить модель вложенности прерыва-
ний без каких-либо ограничений можно задав
значение configMAX_SYSCALL_INTERRUPT_
Рис. 12. Последовательность выполнения задач и прерываний в учебной программе № 2 PRIORITY выше, чем conf igKERNEL_
INTERRUPT_PRIORITY.
Рассмотрим пример. Пусть некий микро-
xStringQueue будет хранить переменные типа char* –
указатели на нуль-терминальные строки.
редь № 2 разблокирует высокоприоритетную контроллер имеет 7 возможных приоритетов
Обе очереди создаются размером 10 элементов. задачу-принтер (3). Задача-принтер считыва- прерываний. Значение приоритета 7 соответ-
Реальная программа должна проверять значения xIntegerQueue,
xStringQueue, чтобы убедиться, что очереди успешно созданы. */
ет указатели на строки из очереди № 2, пока ствует самому высокоприоритетному пре-
xIntegerQueue = xQueueCreate(10, sizeof(unsigned long)); они там есть, и выводит соответствующие рыванию, 1 — самому низкоприоритетному.
xStringQueue = xQueueCreate(10, sizeof(char *));
/* Связать прерывание MS-DOS с обработчиком прерывания строки на экран. Как только очередь № 2 опу- Зададим значение conf igMAX_SYSCALL_
vExampleInterruptHandler(). */ стошится, задача-принтер переходит в бло- INTERRUPT_PRIORITY = 3, а  значение
_dos_setvect(0x82, vExampleInterruptHandler);
/* Создать задачу — генератор чисел с приоритетом 1. */ кированное состояние (4). Управление снова configKERNEL_INTERRUPT_PRIORITY = 1
xTaskCreate(vIntegerGenerator, “IntGen”, 1000, NULL, 1, NULL); получает низкоприоритетная задача — ге- (рис. 13).
/* Создать задачу-принтер с приоритетом 2. */
xTaskCreate(vStringPrinter, “String”, 1000, NULL, 2, NULL); нератор чисел, которая также блокируется Прерывания с приоритетом 1–3 не будут
/* Запуск планировщика. */ на время ~200 мс, так что система снова пере- выполняться, пока ядро или задача выпол-
vTaskStartScheduler();
/* При нормальном выполнении программа до этого места ходит в состояние бездействия (5). няют код, находящийся в критической сек-
“не дойдет” */ ции, но могут при этом использовать API-
for (;;)
; Вложенность прерываний функции. На время реакции на такие преры-
}
вания будет оказывать влияние активность
Во многих архитектурах микроконтролле- ядра FreeRTOS.
Заметьте, что для эффективного распреде- ров прерывания имеют приоритеты, которые На прерывания с приоритетом 4 и выше
ления ресурсов памяти данных (как и реко- могут быть жестко заданы, но может суще- не влияют критические секции, так что ниче-
мендовалось в [2, КиТ № 6]) очередь № 2 хра- ствовать возможность и конфигурировать го, что делает ядро в данный момент, не поме-
нит не сами строки, а лишь указатели на стро- уровни приоритетов прерываний. шает выполнению обработчика такого пре-
ки, которые содержатся в отдельном массиве. Важно различать приоритет задач и прио- рывания. Обычно те прерывания, которые
Такое решение вполне допустимо, так как со- ритет прерываний. Приоритеты прерываний имеют самые строгие временны′ е требования
держимое строк в программе не изменяется. аппаратно фиксированы в архитектуре микро- (например, управление током в обмотках
По результатам выполнения (рис. 11) вид- контроллера (или определены при его кон- двигателя), должны иметь приоритет выше,
но, что в результате возникновения прерыва- фигурации), а приоритеты задач — это про- чем conf igMAX_SYSCALL_INTERRUPT_
ния была разблокирована высокоприоритет- граммная абстракция на уровне ядра FreeRTOS. PRIORITY, чтобы гарантировать, что ядро
ная задача-принтер, после чего управление Приоритет прерываний задает преимущество не внесет дрожание (jitter) во время реакции
снова возвращается низкоприоритетной за- на выполнение того или иного обработчика на прерывание.
даче — генератору чисел (рис. 12). прерывания при возникновении сразу несколь- И наконец, прерывания, которые не вы-
Задача-бездействие выполняется бóльшую ких прерываний. Задачи не выполняются зывают никаких API-функций, могут иметь
часть времени. Каждые 200 мс она вытесняет- во время выполнения обработчика прерыва- любой из возможных приоритетов.
ся задачей — генератором чисел (1). Задача — ния, поэтому приоритет задач не имеет ника- Критическая секция в FreeRTOS — это уча-
генератор чисел записывает в очередь № 1 кого отношения к приоритету прерываний. сток кода, во время выполнения которого за-
пять целых чисел, после чего принудитель- Под вложенностью прерываний понима- прещены прерывания процессора и, соответ-
но вызывает прерывание (2). Обработчик ется корректная работа FreeRTOS при одно- ственно, не происходит переключение кон-
прерывания считывает числа из очереди временном возникновении сразу нескольких текста каждый квант времени [7]. Подробнее
№ 1 и записывает в очередь № 2 указатели прерываний с разными приоритетами, когда о критических секциях — в следующей пу-
на соответствующие строки. Запись в оче- обработчик низкоприоритетного прерыва- бликации.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011 www.kite.ru


32 микроконтроллеры

Следует отметить, что в популярном се-


мействе микроконтроллеров ARM Cortex M3
(как и в некоторых других) меньшие значения
приоритетов прерываний соответствуют ло-
гически бóльшим приоритетам. Если вы хоти-
те назначить прерыванию более высокий при-
оритет, вы назначаете ему приоритет с более
низким номером. Одна из возможных причин
краха программы в таких случаях — назначе-
ние прерыванию номера приоритета меньше-
го, чем configMAX_SYSCALL_INTERRUPT_
PRIORITY, и вызов из него API-функции.
Пример корректной настройки файла
FreeRTOSConfig.h для микроконтроллеров
ARM Cortex M3:

#define configKERNEL_INTERRUPT_PRIORITY 255


#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191

Выводы
Рис. 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

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 7 '2011


132 компоненты микроконтроллеры

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров
Эта статья продолжает знакомить читателя с созданием программ, ра-
ботающих под управлением FreeRTOS — операционной системы для
микроконтроллеров. На этот раз речь пойдет о проблемах организации
Андрей Курниц совместного доступа нескольких задач и/или прерываний к одному ре-
kurnits@stim.by сурсу в среде FreeRTOS.

Введение • Выполняется задача А и начинает выводить очередной параметр


на дисплей: «Температура = 25 °C».
Статья поможет читателям ответить на следующие вопросы: • Задача А вытесняется задачей Б в момент, когда на дисплей вы-
1) Что означает термин «ресурс»? ведено лишь «Темпе».
2) Когда и почему необходимо управление доступом к ресурсам? • Задача Б выводит на дисплей экстренное сообщение «Превышено
3) Что такое механизм взаимного исключения и способы его реали- давление!!!», после чего переходит в блокированное состояние.
зации? • Задача А возобновляет свое выполнение и выводит оставшуюся
4) Что такое критическая секция и способы ее реализации во FreeRTOS? часть сообщения на дисплей: «ратура = 25 °C».
5) Как применять мьютексы для реализации механизма взаимного В и т о ге н а   д и с п л е е п о я в и т с я и с к а же н н о е с о о б щ е н и е :
исключения? «ТемпеПревышено давление!!! ратура = 25 °C».
6) Что такое инверсия приоритетов и как наследование приоритетов
позволяет уменьшить (но не устранить) ее воздействие? Неатомарные операции чтение/модификация/запись
7) Другие потенциальные проблемы, возникающие при использова- Пусть стоит задача установить (сбросить, инвертировать —
нии мьютексов. не имеет значения) один бит в регистре специальных функций, в дан-
8) Задачи-сторожа — создание и использование. ном случае — в регистре порта ввода/вывода микроконтроллера.
9) Функция, вызываемая каждый системный квант времени. Рассмотрим пример кода на языке Си и полученную в результате
компиляции последовательность инструкций ассемблера.
Ресурсы и доступ к ним Для микроконтроллеров AVR:

/* Код на Си */
Под ресурсами микроконтроллерной системы понимают как физи- 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) Задача А загружает значение порта в регистр.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011


микроконтроллеры компоненты 133

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().

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011 www.kite.ru


134 компоненты микроконтроллеры

Участки кода, находящиеся внутри кри- контекста каждый системный квант вре- должен отдать мьютекс обратно. Только когда
тической секции, должны быть как можно мени не происходит, задача, которая вы- мьютекс освободился (возвращен какой-либо
короче и выполняться как можно быстрее. звала 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 );
ровщик останавливается, переключения семафора закончил операции с ресурсом, он

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011


микроконтроллеры компоненты 135

Операции захвата и возврата (выдачи)


а мьютекса выполняются с помощью анало-
гичных API-функций для работы с семафора-
ми — xSemaphoreTake() и xSemaphoreGive(),
которые были рассмотрены в [1, № 7].
Рассмотрим, как применение мьютекса по-
зволяет решить проблему совместного досту-
па к ресурсу, на примере учебной программы
№ 1. В качестве разделяемого ресурса выступает
консоль, две задачи выводят свое сообщение
б
на дисплей. Обратите внимание на реализацию
вывода строки на консоль: вместо стандартной
функции используется посимвольный вывод.
Сначала рассмотрим учебную программу
№ 1 без использования мьютекса:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include “FreeRTOS.h”
#include “task.h”
#include “semphr.h”

в /* Дескриптор мьютекса — глобальная переменная*/


volatile xSemaphoreHandle xMutex;

/* Функция посимвольно выводит строку на консоль.


Консоль, как ресурс, никаким образом не защищена от совместного
доступа из нескольких задач. */
static void prvNewPrintString(const portCHAR *pcString) {
portCHAR *p;
int i;

/* Указатель — на начало строки */


p = pcString;
/* Пока не дошли до нулевого символа — конца строки. */
while (*p) {
/* Вывод на консоль символа, на который ссылается указатель. */
г putchar(*p);
/* Указатель — на следующий символ в строке. */
p++;
/* Вывести содержимое буфера экрана на экран. */
fflush(stdout);
/* Небольшая пауза */
for (i = 0; i < 10000; i++) ;
}
}

/* Функция, реализующая задачу.


Будет создано 2 экземпляра этой задачи.
Каждый получит строку символов в качестве аргумента
при создании задачи. */
static void prvPrintTask(void *pvParameters) {
char *pcStringToPrint;
д pcStringToPrint = (char *) pvParameters;
for (;;) {
/* Для вывода строки на консоль используется своя
функция prvNewPrintString(). */
prvNewPrintString(pcStringToPrint);
/* Блокировать задачу на промежуток времени случайной
длины: от 0 до 500 мс. */
vTaskDelay((rand() % 500));
/* Вообще функция rand() не является реентерабельной.
Однако в этой программе это неважно. */
}
}

/*-----------------------------------------------------------*/
/* Точка входа. С функции 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.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011 www.kite.ru


136 компоненты микроконтроллеры

Рис. 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().

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011


микроконтроллеры компоненты 137

Для того чтобы использовать рекурсивные мьютексы в про-


грамме, необходимо установить макроопределение conf igUSE_
RECURSIVE_MUTEXES в файле FreeRTOSConfig.h равным «1».
Как и обращение к обычному мьютексу, обращение к рекурсивно-
му мьютексу осуществляется с помощью дескриптора (идентифика-
тора) мьютекса — переменной типа xSemaphoreHandle.
API-функции для работы с рекурсивными мьютексами:
• xSemaphoreCreateRecursiveMutex() — создание рекурсивного мью-
текса;
• xSemaphoreTakeRecursive() — захват рекурсивного мьютекса;
• xSemaphoreGiveRecursive() — освобождение (возврат) рекурсив- Рис. 4. Переключение между задачами в учебной программе № 1 
ного мьютекса. без механизма наследования приоритетов
Набор параметров и возвращаемое значение этих API-функций
ничем не отличаются от соответствующих API-функций для рабо-
ты с обычными мьютексами. Стоит помнить лишь о том, что API-
функции для работы с рекурсивными мьютексами нельзя применять
к обычным мьютексам и наоборот.

Проблемы при использовании мьютексов

Инверсия приоритетов
Вернемся к рассмотрению учебной программы № 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. Уменьшение влияния инверсии приоритетов
задачи могут разблокироваться на протяжении интервала, когда низ- при работе механизма наследования приоритетов
коприоритетная задача владеет мьютексом. Такой сценарий является

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011 www.kite.ru


138 компоненты микроконтроллеры

Рис. 7. Результат работы учебной программы № 2 Рис. 8. Взаимная блокировка двух задач

Низкоприоритетная задача стала владель- xSemaphoreTake(xMutex, portMAX_DELAY); Взаимная блокировка


/* Какие-то действия. За это время высокоприоритетная
цем мьютекса ранее. Происходит некоторое задача попытается захватить мьютекс. */ Взаимная блокировка (Deadlock или Deadly
событие, за обработку которого отвечает вы- for (i = 0; i < 100000L; i++) Embrace) — это ситуация в многозадачной си-
;
сокоприоритетная задача. Она разблокиру- /* Если приоритет этой задачи изменился (был унаследован стеме, когда несколько задач находятся в со-
ется и пытается захватить мьютекс (1), это от задачи 2). */ стоянии бесконечного ожидания доступа к ре-
if (uxTaskPriorityGet(NULL) != 1) {
ей не удается, и она блокируется — момент printf(“Inherited priority = %d\n\r”, uxTaskPriorityGet(NULL)); сурсам, занятым самими этими задачами [8].
uxIsPriorityInherited = pdTRUE;
времени (2) на рис. 6. Однако в результате по- }
Простейший пример взаимной блокиров-
пытки высокоприоритетной задачи захватить /* Освободить мьютекс. */ ки включает две задачи — задачу А и задачу Б
xSemaphoreGive(xMutex);
мьютекс низкоприоритетная задача-владелец /* Вывести значение приоритета ПОСЛЕ освобождения
и два мьютекса — мьютекс 1 и мьютекс 2.
мьютекса наследует приоритет этой высоко- мьютекса. */ Взаимная блокировка может произойти при
if (uxIsPriorityInherited == pdTRUE) {
приоритетной задачи. Теперь низкоприори- printf(“Priority after ‘giving’ the mutex = %d\n\r”, такой последовательности событий:
тетная задача не может быть вытеснена сред- uxTaskPriorityGet(NULL)); • Выполняется задача А, которая успешно
}
неприоритетной задачей. Поэтому в момент /* Блокировать задачу на промежуток времени случайной захватывает мьютекс 1.
времени (3), когда низкоприоритетная задача длины: от 0 до 500 мс. */ • Задача Б вытесняет задачу А.
vTaskDelay((rand() % 500));
завершила операции с ресурсом и возвращает } • Задача Б успешно захватывает мьютекс 2,
мьютекс, разблокируется, захватывает мью- } после чего пытается захватить и мьютекс 1.
текс и начинает выполняться высокоприори- /* Высокоприоритетная задача 2. Приоритет = 2. */ Это ей не удается, и она блокируется в ожи-
static void prvTask2(void *pvParameters) {
тетная задача (4). Приоритет же низкоприори- for (;;) {
дании освобождения мьютекса 1.
тетной задачи при этом возвращается к свое- xSemaphoreTake( xMutex, portMAX_DELAY ); • Управление снова получает задача А. Она
xSemaphoreGive( xMutex );
му «нормальному» значению. /* Интервал блокировки короче — от 0 до 50 мс */
пытается захватить мьютекс 2, однако он
Таким образом, механизм наследования vTaskDelay((rand() % 50)); уже захвачен задачей Б. Поэтому задача
}
приоритетов уменьшает время реакции си- } А блокируется в ожидании освобождения
стемы на событие, когда происходит инверсия мьютекса 2.
/*-----------------------------------------------------------*/
приоритетов (сравните величину dT на рис. 5 /* Точка входа. С функции main() начнется выполнение программы. */ В итоге получаем ситуацию, когда задача А
и рис. 6). short main( void ) заблокирована в ожидании освобождения мью-
{
Продемонстрировать работу механизма на- /* Создание мьютекса. */ текса 2, захваченного задачей Б. Задача Б забло-
следования приоритетов во FreeRTOS позволяет xMutex = xSemaphoreCreateMutex(); кирована в ожидании освобождения мьютек-
/* Создание задач, если мьютекс был успешно создан. */
учебная программа № 2. В программе выпол- if (xMutex != NULL) { са 1, захваченного задачей А. Графически эта
няются две задачи: низкоприоритетная задача 1 xTaskCreate(prvTask1, “prvTask1”, 1000, NULL, 1, NULL); ситуация представлена на рис. 8.
xTaskCreate(prvTask2, “prvTask2”, 1000, NULL, 2, NULL);
с приоритетом 1 и высокоприоритетная задача 2 /* Запуск планировщика. */ Впрочем, в состояние взаимной блокиров-
vTaskStartScheduler();
с приоритетом 2. Обе задачи пытаются захватить }
ки может попасть любое количество задач,
один и тот же мьютекс. Низкоприоритетная за- return 1; находящихся в круговой зависимости друг
}
дача сигнализирует на дисплей, если ее приори- от друга. Если ситуация взаимной блокиров-
тет изменился (повысился): ки единожды наступила, то выход из этой си-
По результатам выполнения учебной про- туации невозможен.
#include <stdlib.h> граммы № 2 (рис. 7) видно, что приоритет Как и в случае с инверсией приоритетов,
#include <stdio.h>
#include <string.h> задачи 1 временно увеличивается со значе- лучший способ избежать взаимной блоки-
#include “FreeRTOS.h”
#include “task.h”
ния 1 до значения 2, когда задача 2 пытается ровки задач — это исключить такую возмож-
#include “semphr.h” захватить мьютекс, который уже захвачен за- ность на этапе проектирования программы,
/* Дескриптор мьютекса — глобальная переменная*/ дачей 1. После того как задача 1 освобождает то есть не создавать круговой зависимости
volatile xSemaphoreHandle xMutex; мьютекс, ее приоритет возвращается к перво- задач друг от друга.
/* Низкоприоритетная задача 1. Приоритет = 1. */ начальному значению. Следует отметить, что помимо рассмо-
static void prvTask1(void *pvParameters) { Следует отметить, что механизм насле- тренных выше проблем совместного доступа
long i;
/* Логическая переменная. Определяет, произошло ли дования приоритетов во FreeRTOS только к ресурсам существуют еще такие, как голо-
наследование приоритетов. */ уменьшает, однако не устраняет полно- дание (Starvation) и разновидность взаимной
unsigned portBASE_TYPE uxIsPriorityInherited = pdFALSE;
/* Бесконечный цикл */ стью негативное влияние инверсии приори- блокировки, при которой задачи не блокиру-
for (;;) { тетов. Поэтому рекомендуется проектиро- ются, но и не выполняют полезной работы
/* Наследования приоритетов еще не было */
uxIsPriorityInherited = pdFALSE; вать программу так, чтобы избегать ситуа- (Livelock). Подробнее с ними можно ознако-
/* Захватить мьютекс. */
ции инверсии приоритетов. миться в [9].

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011


микроконтроллеры компоненты 139

Функция 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)

Задачи-сторожа предоставляют простой


и прозрачный метод реализации механизма
взаимного исключения, которому не прису-
щи проблемы инверсии приоритетов и вза-
имной блокировки.
Задача-сторож — это задача, которая
имеет единоличный доступ к разделяемо-
му ресурсу. Никакая другая задача в про-
грамме не имеет права обращаться к ре- Рис. 9. Результат выполнения учебной программы № 3
сурсу напрямую. Вместо этого все задачи,

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011 www.kite.ru


140 компоненты микроконтроллеры

Результат работы учебной программы № 3 приведен на рис. 9,


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

Выводы

В статье освещены вопросы организации совместного досту-


па к разделяемым ресурсам микроконтроллера. В дальнейших
публикациях речь пойдет о сопрограммах — способе реали-
зации многозадачной среды на микроконтроллерах с неболь-
шим объемом оперативной памяти. Также внимание будет
уделено нововведению версии FreeRTOS V7.0.0 — встроен-
ной реализации программных таймеров. n

Литература

1. Курниц А. FreeRTOS — операционная система для микроконтрол-


леров // Компоненты и технологии. 2011. № 2–7.
2. Barry R. Using the FreeRTOS real time kernel: A Practical Guide. 2009.
3. http://www.freertos.org
4. http://ru.wikipedia.org/wiki/Реентерабельность
5. http://ru.wikipedia.org/wiki/Мьютекс
6. http://en.wikipedia.org/wiki/Reentrant_mutex
7. http://www.qnxclub.net/files/articles/invers/invers.pdf
8. http://ru.wikipedia.org/wiki/Взаимная_блокировка
9. http://www.ee.ic.ac.uk/t.clarke/rtos/lectures/RTOSlec2x2bw.pdf

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 8 '2011


микроконтроллеры компоненты 97

Продолжение. Начало в № 2`2011


FreeRTOS —
операционная система
для микроконтроллеров
Автор этой статьи продолжает знакомить читателя с созданием программ,
работающих под управлением FreeRTOS — операционной системы для
микроконтроллеров. На этот раз речь пойдет об альтернативном способе
реализации многозадачной среды, когда в программе вместо задач ис-
Андрей Курниц пользуются сопрограммы. Мы оценим достоинства и недостатки исполь-
kurnits@stim.by зования сопрограмм.

Что представляет собой на сопрограммы. В этом случае программа – Набор операций с очередями ограничен
сопрограмма? будет представлять собой совокупность неза- по сравнению с набором операций для
висимых друг от друга и взаимодействую- задач.
В предыдущих публикациях [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 со-
шений будет замена всех (или части) задач не представлены для сопрограмм. программ потребуется 1026+85 = 345 байт.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011 www.kite.ru


98 компоненты микроконтроллеры

Таким образом, используя сопрограммы,


удалось достичь экономии оперативной па-
мяти 1180–345 = 835 байт, что составляет
приблизительно 71%.

Состояния сопрограммы

Как и задача, сопрограмма может пребывать


в одном из нескольких возможных состояний.
Для сопрограмм этих состояний три:
1. Выполнение (Running). Говорят, что сопро-
грамма выполняется, когда в данный момент
времени процессор занят непосредственно
ее выполнением. В  любой момент вре-
мени только одна сопрограмма в системе мо-
жет находиться в состоянии выполнения. Рис. 1. Состояния сопрограммы
2. Готовность к выполнению (Ready). Говорят,
что сопрограмма готова к выполнению,
если она не блокирована, однако в данный только тогда, когда нет готовых к выполне- мать значения от  0  до  (conf igMAX_CO_
момент процессор занят выполнением нию задач. ROUTINE_PRIORITIES — 1). Большее
другой сопрограммы или какой-то задачи. Важно, что преимущество на выполнение значение соответствует более высокому
Сопрограмма может находиться в состоя- не означает, что если в системе появилась приоритету. Макроопределение configMAX_
нии готовности к выполнению по одной готовая к выполнению сопрограмма с более CO_ROUTINE_PRIORITIES задает общее
из следующих причин: высоким приоритетом, чем та, что выполня- число приоритетов сопрограмм в програм-
– Другая сопрограмма в данный момент ется в данный момент, то управление полу- ме и определено в конфигурационном фай-
находится в состоянии выполнения. чит эта высокоприоритетная сопрограмма. ле FreeRTOSConf ig.h. Изменяя значение
– Одна из задач находится в состоянии Сопрограммы выполняются в режиме ко- conf igMAX_CO_ROUTINE_PRIORITIES,
выполнения, если в программе одно- оперативной многозадачности. Это означает, можно определить любое число возможных
временно используются и сопрограммы, что одна сопрограмма сменяет другую лишь приоритетов сопрограмм, однако следует
и задачи. тогда, когда выполняющаяся в данный мо- стремиться уменьшить число приоритетов
3. Блокированное состояние (Blocked). мент сопрограмма сама передает управление до минимально достаточного для экономии
Сопрограмма блокирована, когда ожида- другой сопрограмме посредством вызова оперативной памяти, потребляемой ядром.
ет наступления некоторого события. Как API-функции. Причем если в момент переда-
и в случае с задачами, событие может быть чи управления в состоянии готовности к вы- Реализация
связано с отсчетом заданного временного полнению находятся несколько сопрограмм, сопрограммы
интервала — временное событие, а мо- то управление получит самая высокоприори-
жет быть связано с ожиданием внешнего тетная среди них. Как и задача, сопрограмма реализуется
по отношению к сопрограмме события. Итак, сопрограмма прерывает свое выпол- в виде функции языка Си. Указатель на эту
Например, если сопрограмма вызовет нение только при выполнении одного из сле- функцию следует передавать в качестве аргу-
API-функцию crDELAY(), то она перейдет дующих условий: мента API-функции создания сопрограммы,
в блокированное состояние и пробудет 1. Сопрограмма перешла в блокированное о которой будет сказано ниже. Пример функ-
в нем на протяжении заданного интервала состояние, вызвав соответствующую API- ции, реализующей сопрограмму:
времени. Блокированные сопрограммы функцию.
void vACoRoutineFunction(xCoRoutineHandle xHandle, unsigned
не получают процессорного времени. 2. Сопрограмма выполнила принудительное portBASE_TYPE uxIndex)
Графически состояния сопрограммы и пе- переключение на другую сопрограмму {
crSTART( xHandle );
реходы между ними представлены на рис. 1. (аналог принудительного переключения
В отличие от задач у сопрограмм нет при- контекста задачи). for(;; )
{
остановленного (suspended) состояния, од- 3. Сопрограмма была вытеснена задачей, ко- // Код, реализующий функциональность сопрограммы,
нако оно может быть добавлено в будущих торая до этого находилась в приостанов- размещается здесь.
}
версиях FreeRTOS. ленном или блокированном состоянии.
Сопрограмма не может быть вытеснена crEND();
Выполнение сопрограмм другой сопрограммой, однако появившаяся }

и их приоритеты готовая к выполнению задача вытесняет лю-


бую сопрограмму. Аргументы функции, реализующей сопро-
Как и при создании задачи, при создании Для корректного выполнения со- грамму:
сопрограммы ей назначается приоритет. программ необходимо организо- 1. xHandle — дескриптор сопрограммы.
Сопрограмма с высоким приоритетом имеет вать в  программе периодический вы- Автоматически передается в функцию, ре-
преимущество на выполнение перед сопро- зов API-функции vCoRoutineSchedule(). ализующую сопрограмму, и в дальнейшем
граммой с низким приоритетом. Рекомендованное место для вызова API- используется при вызове API-функций для
Следует помнить, что приоритет сопро- функции vCoRoutineSchedule() — тело за- работы с сопрограммами.
граммы дает преимущество на выполнение дачи Бездействие, подробнее об этом бу- 2. uxIndex — произвольный целочисленный
одной сопрограммы только перед другой со- дет написано ниже. После первого вызова параметр, который передается в сопро-
программой. Если в программе используют- vCoRoutineSchedule() управление получает грамму при ее создании.
ся как задачи, так и сопрограммы, то задачи сопрограмма с наивысшим приоритетом. Указатель на функцию, реализующую со-
всегда будут иметь преимущество перед со- Приоритет сопрограммы задается це- программу, определен в виде макроопределе-
программами. Сопрограммы выполняются л ы м ч и с ло м , ко т о р о е м оже т п р и н и - ния crCOROUTINE_CODE.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011


микроконтроллеры компоненты 99

К функциям, реализующим сопрограммы, void vApplicationIdleHook( void )


FreeRTOSConfig.h должен содержать ма-
предъявляются следующие требования: { кроопределение вида:
vCoRoutineSchedule( void );
1. Функция должна начинаться с вызова API- }
функции crSTART(). #define configMAX_CO_ROUTINE_PRIORITIES ( 3
2. Функция должна завершаться вызовом
API-функции crEND(). Если задача Бездействие не выполняет ни-
3. Как и в случае с задачей, функция никогда каких других функций, то более эффектив- Учебная программа № 1
не должна заканчивать свое выполнение, ной будет следующая ее реализация:
весь полезный код сопрограммы должен Рассмотрим учебную программу № 1, в ко-
void vApplicationIdleHook( void )
быть заключен внутри бесконечного цикла. {
торой создаются 2 сопрограммы и реализова-
4. Сопрограммы выполняются в режиме ко- for( ;; ) но их совместное выполнение. Каждая сопро-
{
оперативной многозадачности. Поэтому vCoRoutineSchedule( void );
грамма сигнализирует о своем выполнении,
если в программе используется несколько } после чего реализуется временная задержка
}
сопрограмм, то для того, чтобы процессор- с помощью пустого цикла, далее происходит
ное время получали все сопрограммы в про- принудительное переключение на другую со-
грамме, бесконечный цикл должен содержать Даже если в программе не используется программу. Приоритет сопрограмм установ-
вызовы блокирующих API-функций. ни одной задачи, задача Бездействие автома- лен одинаковым.
тически создается при запуске планировщика.
Создание сопрограммы Вызов API-функции vCoRoutineSchedule() #include "FreeRTOS.h"
#include "task.h"
внутри задачи Бездействие позволяет легко #include "croutine.h"
Для создания сопрограммы следует до за- сочетать в одной программе как задачи, так #include <stdlib.h>
#include <stdio.h>
пуска планировщика вызвать API-функ- и сопрограммы. При этом сопрограммы бу-
цию xCoRoutineCreate(), прототип которой дут выполняться, только если нет готовых /* Функция, реализующая Сопрограмму 1.
Параметр, передаваемый в сопрограмму при ее создании,
приведен ниже: к выполнению задач с приоритетом выше не используется. Сопрограмма сигнализирует о своем
выполнении, после чего блокируется на 500 мс. */
приоритета задачи Бездействие (который void vCoRoutine1( xCoRoutineHandle xHandle, unsigned portBASE_
portBASE_TYPE xCoRoutineCreate( обычно равен 0). TYPE uxIndex ) {
crCOROUTINE_CODE pxCoRoutineCode, /* Все переменные должны быть объявлены как static. */
unsigned portBASE_TYPE uxPriority, В принципе вызов API-функции static long i;
unsigned portBASE_TYPE uxIndex vCoRoutineSchedule() возможен в любой /* Сопрограмма должна начинаться с вызова crSTART().
); Дескриптор сопрограммы xHandle получен автоматически
задаче, а не только в задаче Бездействие. в виде аргумента функции, реализующей сопрограмму
Обязательным требованием является vCoRoutine1().*/
crSTART( xHandle );
Аргументы и возвращаемое значение: то, чтобы задача, из  которой вызывает- /* Сопрограмма должна содержать бесконечный цикл. */
for(;;) {
1. pxCoRoutineCode — указатель на функ- ся vCoRoutineSchedule(), имела самый низкий /* Сигнализировать о выполнении */
цию, реализующую сопрограмму (факти- приоритет. Иначе если существуют задачи puts("Co-routine #1 runs!");
/* Пауза, реализованная с помощью пустого цикла */
чески — идентификатор функции в про- с более низким приоритетом, то они не будут for (i = 0; i < 5000000; i++);
грамме). получать процессорное время. /* Выполнить принудительное переключение на другую со-
программу */
2. uxPriority — приоритет создаваемой со- Важно, что стек, общий для всех сопро- crDELAY( xHandle, 0 );
программы. Если задано значение боль- грамм, является стеком той задачи, которая вы- }
/* Сопрограмма должна завершаться вызовом crEND(). */
ше, чем (conf igMAX_CO_ROUTINE_ зывает API-функцию vCoRoutineSchedule(). crEND();
}
PRIORITIES — 1), то  сопрограмма Если вызов vCoRoutineSchedule() располага-
получит приоритет? равный (configMAX_ ется в теле задачи Бездействие, то все сопро- /* Функция, реализующая Сопрограмму 2.
Сопрограмма 2 выполняет те же действия, что и Сопрограмма 1.*/
CO_ROUTINE_PRIORITIES — 1). граммы используют стек задачи Бездействие. void vCoRoutine2( xCoRoutineHandle xHandle, unsigned portBASE_
3. uxIndex — целочисленный параметр, Размер стека задачи Бездействие задается ма- TYPE uxIndex ) {
static long i;
который передается сопрограмме при ее кроопределением configMINIMAL_STACK_ crSTART( xHandle );
создании. Позволяет создавать несколько SIZE в файле FreeRTOSConfig.h. for(;;) {
/* Сигнализировать о выполнении */
экземпляров одной сопрограммы. puts("Co-routine #2 runs!");
4. Возвращаемое значение. Равно pdPASS, Настройки FreeRTOS /* Пауза, реализованная с помощью пустого цикла */
for (i = 0; i < 5000000; i++);
если сопрограмма успешно создана и до- для использования сопрограмм /* Выполнить принудительное переключение на другую со-
программу */
бавлена к  списку готовых к  выполне- crDELAY( xHandle, 0 );
нию, в противном случае — код ошибки, Для того чтобы организовать многозадач- }
crEND();
определенный в файле ProjDefs.h (обычно ную среду на основе сопрограмм, прежде все- }
errCOULD_NOT_ALLOCATE_REQUIRED_ го необходимо соответствующим образом
/* Точка входа. С функции main() начинается выполнение про-
MEMORY). настроить ядро FreeRTOS: граммы. */
1. В исходный текст программы должен быть void main(void) {

API-функция  включен заголовочный файл croutine.h, со- /* До запуска планировщика создать Сопрограмму 1
vCoRoutineSchedule() держащий определения API-функций для и Сопрограмму 2.
Приоритеты сопрограмм одинаковы и равны 1.
работы с сопрограммами: Параметр, передаваемый при создании, не используется и ра-
вен 0. */
Выполнение сопрограмм должно быть xCoRoutineCreate(vCoRoutine1, 1, 0);
организовано при помощи циклического #include "croutine.h” xCoRoutineCreate(vCoRoutine2, 1, 0);

вызова API-функции vCoRoutineSchedule(). /* В программе не создается ни одной задачи.


Ее прототип: Однако задачи можно добавить, создавая их до запуска плани-
ровщика */
2. Конфигурационный файл FreeRTOSConfig.h
/* Запуск планировщика. Сопрограммы начнут выполняться.
void vCoRoutineSchedule( void ); должен содержать следующие макроопреде- */
ления, установленные в 1: configUSE_IDLE_ vTaskStartScheduler();
}
HOOK и configUSE_CO_ROUTINES.
Вызов vCoRoutineSchedule() рекомендуется 3. Следует также определить количе- /* Функция, реализующая задачу Бездействие, должна присутство-
вать в программе и содержать вызов vCoRoutineSchedule() */
располагать в задаче Бездействие: ство приоритетов сопрограмм. Файл

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011 www.kite.ru


100 компоненты микроконтроллеры

Рис. 4. Результат выполнения учебной программы № 1,


Рис. 2. Результат выполнения учебной программы № 1
когда Сопрограмма 1 не выполняет переключения на другую сопрограмму

// Глобальная переменная:
unsigned int uGlobalVar;

// Функция, реализующая сопрограмму


void vACoRoutineFunction( xCoRoutineHandle xHandle, unsigned
portBASE_TYPE uxIndex )
{
// Статическая переменная:
static unsigned int uStaticVar;

// Локальная переменная — В СТЕКЕ!!!


unsigned int uLocalVar = 10L;
Рис. 3. Ход выполнения сопрограмм в учебной программе № 1 crSTART( xHandle );
for(;; )
{
uGlobalVar = 1;
void vApplicationIdleHook(void) { чае результаты работы программы (рис. 4) uStaticVar = 10;
/* Так как задача Бездействие не выполняет других действий,
то вызов vCoRoutineSchedule() размещен внутри бесконечного будут свидетельствовать о том, что процес- uLocalVar = 100;
цикла.*/ сорное время получает только Сопрограмма
for (;;) { // Вызов блокирующей API-функции
vCoRoutineSchedule(); 1. Причиной этому является тот факт, что crDELAY( xHandle, 10 );
}
}
Сопрограмма 1 не выполняет принудительно- // После вызова блокирующей API-функции
го переключения на другую сопрограмму, что // значение глобальной и статической переменной
// uGlobalVar и uStaticVar гарантированно сохранится.
является необходимым условием корректной
Результаты работы учебной программы работы кооперативной многозадачной среды. // Значение же локальной переменной uLocalVar
// может оказаться не равным 100!!!
№ 1 приведены на рис. 2. На рис. 2 видно, что }
сообщения на дисплей выводят обе сопро- Ограничения crEND();
}
граммы, следовательно, каждая из них по- при использовании сопрограмм
лучает процессорное время. На рис. 3 пред-
ставлено разделение процессорного вре- Платой за уменьшение объема потребляе- Вызов блокирующих API-функций
мени между сопрограммами. мой оперативной памяти при использовании Еще одним последствием использования
Сопрограммы выполняются в  режиме сопрограмм вместо задач является то, что общего для всех сопрограмм стека является
кооперативной многозадачности, поэтому программирование сопрограмм сопряжено то, что вызов блокирующих API-функций
текущая сопрограмма выполняется до тех с рядом ограничений. В целом реализация допускается только непосредственно из тела
пор, пока не произойдет явное переключе- сопрограмм сложнее, чем реализация задач. сопрограммы, но не допускается из функций,
ние на другую сопрограмму. На протяжении которые вызываются из тела сопрограммы.
времени 0…t1 будет выполняться только Использование локальных переменных Рассмотрим пример:
Сопрограмма 1, а именно будет выполняться Особенность сопрограмм в том, что когда
// Функция, реализующая сопрограмму
продолжительный по времени пустой цикл сопрограмма переходит в блокированное со- void vACoRoutineFunction(xCoRoutineHandle xHandle, unsigned
(рис. 3). Как только пустой цикл Сопрограммы стояние, стек сопрограммы не сохраняется. portBASE_TYPE uxIndex)
{
1 будет завершен, в момент времени t1 прои- То есть если переменная находилась в стеке crSTART( xHandle );
зойдет явное переключение на другую сопро- в момент, когда сопрограмма перешла в бло-
for(;; )
грамму. В результате чего управление получит кированное состояние, то по выходу из него {
Сопрограмма 2 на такой же продолжительный значение переменной, вероятно, будет дру- // Непосредственно в сопрограмме
// блокирующие API-функции вызывать можно.
промежуток времени — t1…t2. гим. Эта особенность объясняется тем фак- crDELAY( xHandle, 10 );
Следует обратить внимание на обязатель- том, что все сопрограммы в программе ис-
// Однако внутри функции vACalledFunction() их НЕЛЬЗЯ
ный вызов API-функции crDELAY(xHandle, 0), пользуют один и тот же стек. вызывать!!!
благодаря которому происходит принуди- Чтобы избежать потери значения пере- vACalledFunction();
}
тельное переключение на другую сопрограм- менных, не следует размещать их в стеке, crEND();
му и, таким образом, реализуется принцип то есть нельзя использовать локальные пере- }
кооперативной многозадачности. менные в сопрограммах. Все переменные, void vACalledFunction(void) {
Продемонстрировать важность «ручного» используемые в сопрограмме, должны быть // Здесь нельзя вызывать блокирующие API-функции!!!
переключения на другую сопрограмму можно, глобальными либо объявлены статическими // ОШИБКА!
если исключить из функции Сопрограммы 1 (ключевое слово static). Рассмотрим пример crDELAY( xHandle, 10 );
}
вызов API-функции crDELAY(). В таком слу- функции, реализующей сопрограмму:

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011


микроконтроллеры компоненты 101

Внутренняя реализация сопрограмм не до- 2. xTicksToDelay — количество квантов вре- crQUEUE_SEND(


пускает вызова блокирующих API-функций мени, в течение которых сопрограмма xCoRoutineHandle xHandle,
xQueueHandle pxQueue,
внутри выражения switch. Рассмотрим пример: будет заблокирована. Если xTicksToDelay void *pvItemToQueue,
равен 0, то вместо блокировки сопрограм- portTickType xTicksToWait,
portBASE_TYPE *pxResult
// Функция, реализующая сопрограмму мы происходит переключение на другую )
void vACoRoutineFunction( xCoRoutineHandle xHandle, unsigned
portBASE_TYPE uxIndex ) готовую к выполнению сопрограмму.
{
crSTART( xHandle );
Отдельно следует обратить внимание на вы-
зов crDELAY(), когда аргумент xTicksToDelay Аргументы API-функции crQUEUE_SEND():
for(;; )
{ равен 0. В  этом случае вызывающая 1. xHandle — дескриптор вызывающей со-
// Непосредственно в сопрограмме crDELAY(xHandle, 0) сопрограмма переходит программы. Автоматически передается
// блокирующие API-функции вызывать можно.
crDELAY( xHandle, 10 ); в состояние готовности к выполнению, а в со- в функцию, реализующую сопрограмму,
switch( aVariable )
стояние выполнения переходит другая сопро- в виде первого ее аргумента.
{ грамма, приоритет которой выше или равен 2. pxQueue — дескриптор очереди, в которую
case 1 : // Здесь нельзя вызывать блокирующие API-функции.
break; приоритету вызывающей сопрограммы. будет записан элемент. Дескриптор очере-
default: // Здесь тоже нельзя. Посредством вызова crDELAY(xHandle, 0) ди может быть получен при ее создании
}
} происходит принудительное переключение API-функцией xQueueCreate().
crEND(); на другую сопрограмму, что было продемон- 3. pvItemToQueue — указатель на элемент,
}
стрировано в учебной программе № 1. который будет записан в очередь. Размер
Следует отметить, что применительно элемента зафиксирован при создании оче-
API-функции, предназначенные к сопрограммам не существует аналога API- реди. Именно это количество байт будет
для вызова из сопрограмм функции vTaskDelayUntil(), которая пред- скопировано с адреса, на который ссыла-
назначена для вызова из задач и позволя- ется указатель pvItemToQueue.
Текущая версия FreeRTOS v7.0.1 поддер- ет организовать циклическое выполнение 4. xTicksToWait — максимальное количество
живает следующие API-функции, предназна- какого-либо действия со строго заданным пери- квантов времени, в течение которого со-
ченные для вызова из сопрограмм: одом. Также отсутствует аналог API-функции программа может пребывать в блокиро-
• crDELAY(); xTaskGetTickCount(), которая позволяет полу- ванном состоянии, если очередь полна
• crQUEUE_SEND(); чить текущее значение счетчика квантов. и записать новый элемент нет возмож-
• crQUEUE_RECEIVE(). ности. Для представления времени в мил-
Кроме этого, существуют еще API-функции Использование очередей лисекундах следует использовать макро-
crQUEUE_SEND_FROM_ISR() и crQUEUE_ в сопрограммах определение portTICK_RATE_MS [1, № 4].
RECEIVE_FROM_ISR(), предназначенные для Задание xTicksToWait равным 0 приведет
вызова из обработчиков прерываний и вы- Как известно, очереди во FreeRTOS пред- к тому, что сопрограмма не перейдет в бло-
полняющие операции с очередью, которая ис- ставляют собой базовый механизм межзадач- кированное состояние, если очередь полна,
пользуется только в сопрограммах. ного взаимодействия, на механизме очередей и управление будет возвращено сразу же.
Все вышеперечисленные API-функции основываются такие объекты ядра, как сема- 5. pxResult — указатель на переменную типа
на самом деле представляют собой макросы форы и мьютексы. portBASE_TYPE, в которую будет поме-
языка Си, но для простоты будем называть FreeRTOS допускает использование очере- щен результат выполнения API-функции
их API-функциями. дей и в сопрограммах, но в этом случае су- crQUEUE_SEND(). Может принимать сле-
Стоит подчеркнуть, что API-функции, ществует одно серьезное ограничение: одну дующие значения:
предназначенные для вызова из сопрограмм, и ту же очередь нельзя использовать для пере- – pdPASS — означает, что данные успеш-
разрешено вызывать только непосредствен- дачи сообщений от очереди к сопрограмме но записаны в очередь. Если определено
но из тела сопрограммы. Вызов их из других и наоборот. Допускается лишь передача сооб- время тайм-аута (параметр xTicksToWait
функций запрещен. щений между сопрограммами и обработчика- не равен 0), то возврат значения pdPASS
Префикс всех вышеперечисленных API- ми прерываний. Когда очередь создана, ее сле- говорит о  том, что свободное место
функций указывает на заголовочный файл дует использовать только в задачах или только в очереди появилось до истечения вре-
croutine.h, в котором эти API-функции объ- в сопрограммах. Эта особенность существен- мени тайм-аута и элемент был помещен
явлены. но ограничивает возможности совместного в очередь.
использования задач и сопрограмм. – Код ошибки errQUEUE_FULL, опреде-
Реализация задержек Следует учитывать, что для сопрограмм набор ленный в файле ProjDefs.h.
в сопрограммах API-функций для работы с очередями гораздо Следует отметить, что при записи элемента
беднее набора API-функций для задач. Для со- в очередь из тела сопрограммы нет возмож-
Для корректной реализации временных за- программ нет аналогов следующих API-функций: ности задать время тайм-аута равным бес-
держек внутри сопрограмм следует приме- 1) uxQueueMessagesWaiting() — получение конечности, такая возможность есть, только
нять API-функцию crDELAY(), которая пере- количества элементов в очереди. если задача записывает элемент в очередь.
водит вызывающую сопрограмму в блоки- 2) xQueueSendToFront() — запись элемента Установка аргумента xTicksToWait равным
рованное состояние на заданное количество в начало очереди. константе portMAX_DELAY приведет к пере-
квантов времени. Ее прототип: 3) xQueuePeek() — чтение элемента из очере- ходу сопрограммы в блокированное состо-
ди без удаления его из очереди. яние на конечное время, равное portMAX_
void crDELAY( xCoRoutineHandle xHandle, portTickType 4) xQueueSendToFrontFromISR() — запись DELAY квантов времени. Это связано с тем,
xTicksToDelay );
элемента в начало очереди из обработчика что сопрограмма не может находиться в при-
прерывания. остановленном (suspended) состоянии.
Аргументы API-функции crDELAY():
1. xHandle — дескриптор вызывающей со- Запись элемента в очередь Чтение элемента из очереди
программы. Автоматически передается Для записи элемента в очередь из тела сопро- Для чтения элемента из очереди служит
в функцию, реализующую сопрограмму, граммы служит API-функция crQUEUE_SEND(). API-функция crQUEUE_RECEIVE(), кото-
в виде первого ее аргумента. Ее прототип: рую можно вызывать только из тела сопро-

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011 www.kite.ru


102 компоненты микроконтроллеры

граммы. Прототип 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
граммах (но не в задачах).

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011


микроконтроллеры компоненты 103

Текст учебной программы № 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 В этом случае низкоприоритетная служеб-
ная сопрограмма не вызывает блокирующих

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011 www.kite.ru


104 компоненты микроконтроллеры

Рис. 7. Работа модифицированной учебной программы № 2

API-функций. Результат выполнения модифицированной учебной


программы № 2 приведен на рис. 7.
На рис. 7 видно, что теперь выполняется только низкоприоритет-
ная служебная сопрограмма. Высокоприоритетная Сопрограмма № 1
не получает процессорного времени, даже несмотря на то, что она
вышла из блокированного состояния, когда Прерывание № 1 поме-
стило сообщение в Очередь № 1.
Рассмотрим реальную программу, в которой высокоприоритетная
сопрограмма отвечает за обработку события, ожидая, когда в очереди
появится сообщение. Сообщение в очередь помещает обработчик пре-
рывания, которое возникает при наступлении события.
Пусть в текущий момент выполняется низкоприоритетная со-
программа и происходит это прерывание. Обработчик прерывания
помещает сообщение в очередь. Однако высокоприоритетная со-
программа не получит управления сразу же после выполнения обра-
ботчика прерывания. Высокоприоритетная сопрограмма вынуждена
ожидать, пока низкоприоритетная сопрограмма отдаст управление,
вызвав блокирующую API-функцию.
Таким образом, время реакции системы на событие зависит от того,
насколько быстро выполняющаяся в данный момент сопрограмма вы-
полнит переключение на другую сопрограмму. Высокоприоритетная со-
программа вынуждена ожидать, пока выполняется низкоприоритетная.
С точки зрения времени реакции системы на внешние события
кооперативная многозадачность не позволяет гарантировать задан-
ное время реакции, что является одним из основных недостатков
кооперативной многозадачности.

Выводы

Подводя итог, можно выделить следующие тезисы относительно со-


программ во FreeRTOS:
• Выполняются в режиме кооперативной многозадачности.
• Значительно экономят оперативную память.
• Автоматически устраняют проблему реентерабельности функций.
• Не гарантируют заданного времени реакции системы на прерывание.
• При написании сопрограмм следует придерживаться строгих огра-
ничений.
• Бедный набор API-функций для работы с сопрограммами.
Таким образом, использование сопрограмм может быть оправда-
но лишь в том случае, если преследуется цель написания програм-
мы, работающей под управлением FreeRTOS, на микроконтроллере,
который не имеет достаточного объема оперативной памяти для
реализации программы с использованием задач. n

Литература

1. Курниц А. FreeRTOS — операционная система для микроконтроллеров //


Компоненты и технологии. 2011. № 2–8.
2. www.freertos.org
3. http://www.ee.ic.ac.uk/t.clarke/rtos/lectures/RTOSlec2x2bw.pdf

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 9 '2011


микроконтроллеры компоненты 93

FreeRTOS —
операционная система
для микроконтроллеров

Это очередная статья из цикла, посвященного FreeRTOS — операционной


системе для микроконтроллеров. Здесь читатель познакомится с ново-
Андрей Курниц введением последних версий FreeRTOS — встроенной реализацией про-
kurnits@stim.by граммных таймеров.

Что представляет собой времени, чем использование 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. Операции с таймером, состояния таймера и переходы между ними дению памяти, занимаемой служебной
структурой управления таймером.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011 www.kite.ru


94 компоненты микроконтроллеры

б Рис. 3. Влияние сброса таймера на отсчет времени

Типичный пример использования операции сброса таймера —


в устройстве, содержащем ЖКИ-дисплей с подсветкой. Подсветка
дисплея включается по нажатию любой клавиши, а выключается спу-
стя, например, 5 с после последнего нажатия. Если для отсчета 5 с ис-
пользовать интервальный таймер, то операция сброса этого таймера
должна выполняться при нажатии любой клавиши (подсветка в это
время включена). Функция таймера должна реализовывать выклю-
чение подсветки. В этом случае, пока пользователь нажимает на кла-
Рис. 2. Работа интервального и периодического таймера виши, таймер сбрасывается и начинает отсчет 5 с с начала. Как только
с момента последнего нажатия на клавишу прошло 5 с, выполнится
функция таймера, и подсветка будет выключена.
Режимы работы таймера Операция изменения периода работы таймера подобна операции
сброса. При изменении периода отсчет времени также начинается с на-
Таймеры во FreeRTOS различаются по режиму работы в зависимо- чала, отличие заключается лишь в том, что таймер начинает отсчиты-
сти от состояния, в которое переходит таймер после того, как произо- вать другой, новый период времени. Таким образом, время, прошедшее
шло его срабатывание. Программный таймер во FreeRTOS может от момента запуска до момента изменения периода, не учитывается: но-
работать в одном из двух режимов: вый период начинает отсчитываться с момента его изменения (рис. 4).
• режим интервального таймера (One-shot timer); На рис. 4б видно, что в результате изменения периода таймер
• режим периодического таймера (Auto-reload timer). не срабатывает, если на момент изменения периода таймер отсчитал
промежуток времени больше, чем новый период таймера.
Интервальный таймер
Характеризуется тем, что после срабатывания таймера он перехо-
дит в пассивное состояние. Таким образом, функция таймера будет
вызвана один раз — когда время, равное периоду таймера, истечет. а
Однако после этого интервальный таймер можно «вручную» запу-
стить заново, но автоматически этого не происходит (рис. 2а).
Интервальный таймер применяют, когда необходимо организовать
однократное выполнение какого-либо действия спустя заданный про-
межуток времени, который отсчитывается с момента запуска таймера.

Периодический таймер
Характеризуется тем, что после срабатывания таймера он остается
в активном состоянии и начинает отсчет временного интервала с на-
чала. Можно сказать, что после срабатывания периодический таймер
сам автоматически запускается заново. Таким образом, единожды
запущенный периодический таймер реализует циклическое выпол- б
нение функции таймера с заданным периодом (рис. 2б).
Периодический таймер применяют, когда необходимо организо-
вать циклическое, повторяющееся выполнение определенных дей-
ствий с точно заданным периодом.
Режим работы таймера задается в момент его создания и не может
быть изменен в процессе выполнения программы.

Сброс таймера и изменение периода

Во FreeRTOS есть возможность сбросить таймер после того, как он


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

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011


микроконтроллеры компоненты 95

Рис. 5. Передача команды вследствие вызова API-функции сброса таймера

Реализация программных зывать задачей обслуживания программных де, где программные таймеры играют роль
таймеров во 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. Для всех программных таймеров в про-
свои функции за счет того, что в программе Таким образом, очередь команд выступа- грамме используется одна-единственная
выполняется одна дополнительная сервисная ет средством безопасного управления про- задача обслуживания таймеров и одна-
задача, которую в дальнейшем мы будем на- граммными таймерами в многозадачной сре- единственная очередь команд.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011 www.kite.ru


96 компоненты микроконтроллеры

2. Функция таймера выполняется в контексте типа xTIMER_MESSAGE, размер которой выполнит. Размер очереди зависит от ко-
задачи обслуживания таймеров, а не в кон- равен 8 байт. Пусть используется очередь личества вызовов API-функций для рабо-
тексте обработчика прерывания микрокон- длиной 10 команд, тогда для размещения их ты с таймерами во время, когда функция
троллера. в памяти потребуется 810 = 80 байт. В сумме обслуживания таймеров не выполняется.
3. Процессорное время не расходуется зада- получаем 58+80 = 138 байт. А именно когда:
чей обслуживания таймеров, когда проис- Каждый таймер в программе обслужива- • Планировщик еще не запущен или при-
ходит отсчет времени. Задача обслужива- ется с помощью структуры управления тай- остановлен.
ния таймеров получает управление, лишь мером xTIMER, ее размер составляет 34 байт. • Происходит несколько вызовов API-
когда истекает время, равное периоду ра- Так как таймеров в программе 10, то памяти функций для работы с таймерами из об-
боты одного из таймеров. потребуется 3410 = 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 байт и памяти, в которой мальное число невыполненных команд, функции. Такое использование идентифи-
разместятся элементы очереди. Элемент оче- которые могут храниться в очереди, пре- катора будет продемонстрировано ниже
реди команд представляет собой структуру жде чем задача обслуживания таймеров их в учебной программе.

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011


микроконтроллеры компоненты 97

Создание/удаление таймера Аргументы и возвращаемое значение: 2. xBlockTime — определяет время тайм-аута —


Для того чтобы создать программный 1. xTimer — дескриптор таймера, полу- максимальное время нахождения вызываю-
таймер, следует вызвать API-функцию ченный при его создании API-функцией щей xTimerStop() задачи в блокированном
xTimerCreate(). Ее прототип: xTimerCreate(). состоянии, если очередь команд полностью
2. xBlockTime — определяет время тайм- заполнена и нет возможности поместить
xTimerHandle xTimerCreate( const signed char *pcTimerName, аута — максимальное время нахождения в нее команду об останове таймера.
portTickType xTimerPeriod, unsigned portBASE_TYPE uxAutoReload,
void * pvTimerID, tmrTIMER_CALLBACK pxCallbackFunction ); вызывающей xTimerDelete() задачи в бло- 3. Возвращаемое значение — может прини-
кированном состоянии, если очередь ко- мать два значения:
манд полностью заполнена и нет возмож- • pdFAIL — означает, что команда об оста-
Аргументы и возвращаемое значение: ности поместить в нее команду об уни- нове таймера так и не была помещена
1. pcTimerName — нультерминальная (закан- чтожении таймера. в очередь команд, а время тайм-аута ис-
чивающаяся нулем) cтрока, определяющая 3. Возвращаемое значение — может прини- текло.
имя таймера. Ядром не используется, а слу- мать два значения: • pdPASS — означает, что команда об оста-
жит лишь для наглядности и при отладке. • pdFAIL — означает, что команда об уда- нове успешно помещена в очередь ко-
2. xTimerPeriod — период работы таймера. лении так и не была помещена в очередь ко- манд.
Задается в системных квантах времени, манд, а время тайм-аута истекло. API-функции xTimerStart() и xTimerStop()
для задания в миллисекундах следует ис- • pdPASS — означает, что команда об уда- предназначены для вызова из задачи или
пользовать макроопределение portTICK_ лении успешно помещена в очередь команд. функции таймера. Существуют версии этих
RATE_MS. Например, для задания перио- Вызов xTimerDelete() приводит к осво- API-функций, предназначенные для вызова
да работы таймера равным 500 мс следует бождению памяти, занимаемой структурой из обработчиков прерываний, о них будет
присвоить аргументу xTimerPeriod значе- управления таймером xTIMER. сказано ниже.
ние выражения 500/portTICK_RATE_MS. API-функции  xTimerCreate()
Нулевое значение периода работы таймера и xTimerDelete() недопустимо вызывать Сброс таймера
не допускается. из обработчиков прерываний. Сброс таймера осуществляется с помощью
3. uxAutoReload — определяет тип создавае- API-функции xTimerReset(). Ее прототип:
мого таймера. Может принимать следую- Запуск/останов таймера
щие значения: Запуск таймера осуществляется с помощью portBASE_TYPE xTimerReset( xTimerHandle xTimer, portTickType
xBlockTime );
• pdTRUE — будет создан периодический API-функции xTimerStart(). Ее прототип:
таймер.
• pdFALSE — будет создан интервальный portBASE_TYPE xTimerStart( xTimerHandle xTimer, portTickType Аргументы и возвращаемое значение:
xBlockTime );
таймер. 1. xTimer — дескриптор таймера, полу-
4. pvTimerID — задает указатель на иден- ченный при его создании API-функцией
тификатор, который будет присвоен Аргументы и возвращаемое значение: xTimerCreate().
создаваемому экземпляру таймера. Этот 1. xTimer — дескриптор таймера, полу- 2. xBlockTime — определяет время тайм-
аргумент следует использовать при соз- ченный при его создании API-функцией аута — максимальное время нахождения
дании нескольких экземпляров таймеров, xTimerCreate(). вызывающей xTimerReset() задачи в блоки-
которым соответствует одна-единственная 2. xBlockTime — определяет время тайм-аута — рованном состоянии, если очередь команд
функция таймера. максимальное время нахождения вызываю- полностью заполнена и нет возможности
5. pxCallbackFunction — указатель на функ- щей xTimerStart() задачи в блокированном поместить в нее команду о сбросе таймера.
цию таймера, фактически — имя функции состоянии, если очередь команд полностью 3. Возвращаемое значение — может прини-
в программе. Функция таймера должна заполнена и нет возможности поместить мать два значения:
иметь следующий прототип: в нее команду о запуске таймера. • pdFAIL — означает, что команда о сбросе
3. Возвращаемое значение — может прини- таймера так и не была помещена в оче-
void vCallbackFunction( xTimerHandle xTimer ); мать два значения: редь команд, а время тайм-аута истекло.
• pdFAIL — означает, что команда о запуске • pdPASS — означает, что команда о сбро-
таймера так и не была помещена в очередь се таймера успешно помещена в очередь
Указатель на функцию таймера задан так- команд, а время тайм-аута истекло. команд.
же в виде макроопределения tmrTIMER_ • pdPASS — означает, что команда о запуске Операция сброса может применяться как
CALLBACK. успешно помещена в очередь команд. к активному таймеру, так и к находящему-
6. Возвращаемое значение. Если таймер успеш- Запуск таймера может быть произве- ся в пассивном состоянии. В случае если
но создан, возвращаемым значением бу- ден и  с  помощью вызова API-функции таймер находился в пассивном состоянии,
дет ненулевой дескриптор таймера. Если же xTimerReset(), подробно об этом — в описа- вызов xTimerReset() будет эквивалентен вы-
таймер не создан по причине нехватки опе- нии API-функции xTimerReset() ниже. зову xTimerStart(), то есть таймер будет за-
ративной памяти или при задании периода Таймер, который уже отсчитывает вре- пущен. Если таймер уже отсчитывал время
таймера равным нулю, то возвращаемым мя, находясь в активном состоянии, может в момент вызова xTimerReset() (то есть на-
значением будет 0. быть принудительно остановлен. Для этого ходился в активном состоянии), то вызов
Важно, что таймер после создания нахо- предназначена API-функция xTimerStop(). xTimerReset() приведет к тому, что таймер
дится в пассивном состоянии. API-функция Ее прототип: заново начнет отсчет времени с момента вы-
xTimerCreate() действует непосредственно зова xTimerReset().
и не использует очередь команд таймеров. portBASE_TYPE xTimerStop( xTimerHandle xTimer, portTickType Допускается вызов xTimerReset(), когда тай-
xBlockTime );
Ранее созданный таймер может быть уда- мер уже создан, но планировщик еще не за-
лен. Для этого предназначена API-функция пущен. В этом случае отсчет времени начнет-
xTimerDelete(). Ее прототип: Аргументы и возвращаемое значение: ся не с момента вызова xTimerReset(), а с мо-
1. xTimer — дескриптор таймера, полу- мента запуска планировщика.
portBASE_TYPE xTimerDelete( xTimerHandle xTimer, portTickType ченный при его создании API-функцией Л е г ко з а м е т и т ь , ч т о A P I - ф у н к ц и и
xBlockTime );
xTimerCreate(). xTimerReset() и xTimerStart() полностью экви-

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011 www.kite.ru


98 компоненты микроконтроллеры

валентны. Две различные 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() мо- если в данный момент выполняется за- * В программе не должно быть ее явных вызовов.
жет возвращать два значения: дача с приоритетом меньше, чем у задачи

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011


микроконтроллеры компоненты 99

* В функцию автоматически передается дескриптор таймера в виде аргумента xTimer. */


void vAutoReloadTimerFunction(xTimerHandle xTimer) {
/* Сигнализировать о выполнении.
* Вывести сообщение о текущем времени, прошедшем с момента запуска планировщика. */
printf(“AutoReload timer. Time = %d sec\n\r”, xTaskGetTickCount() / configTICK_RATE_HZ);
/* Увеличить период работы периодического таймера на 1 секунду */
uiAutoReloadTimerPeriod += 1000 / portTICK_RATE_MS;
/* Установить новый период работы периодического таймера.
* Время тайм-аута (3-й аргумент) обязательно должно быть 0!
* Так как внутри функции таймера нельзя вызывать блокирующие API-функции. */
xTimerChangePeriod(xTimer, uiAutoReloadTimerPeriod, 0);
}
/*-----------------------------------------------------------*/

/* Функция интервальных таймеров.


* Нескольким экземплярам интервальных таймеров соответствует одна-единственная функция.
* Эта функция автоматически вызывается при истечении времени любого из связанных с ней таймеров.
* Для того чтобы выяснить, время какого таймера истекло, используется идентификатор таймера. */
void vOneShotTimersFunction(xTimerHandle xTimer) {
/* Указатель на идентификатор таймера */
unsigned portBASE_TYPE *pxTimerID; Рис. 6. Выполнение учебной программы

/* Получить идентификатор таймера, который вызывал эту функцию таймера */


pxTimerID = pvTimerGetTimerID(xTimer); /* Выполнить сброс только первого интервального таймера.
* Именно он начнет отсчитывать время сразу после запуска планировщика.
/* Различные действия в зависимости от того, какой таймер вызывал функцию */ * Остальные 2 таймера после запуска планировщика останутся в пассивном состоянии. */
switch (*pxTimerID) { xTimerReset(xOneShotTimers[0], 0);
/* Сработал интервальный таймер 1 */
case ID_TIMER_1: /* Индицировать текущее время.
/* Индикация работы + текущее время */ * Оно будет равно 0, так как планировщик еще не запущен. */
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / configTICK_RATE_HZ); printf(“Timers start! Time = %d sec\n\r\n\r”, xTaskGetTickCount() / configTICK_RATE_HZ);
/* Запустить интервальный таймер 2 */
xTimerStart(xOneShotTimers[1], 0); /* Запуск планировщика.
break; * Автоматически будет создана задача обслуживания таймеров.
/* Сработал интервальный таймер 2 */ * Таймеры, которые были переведены в активное состояние (например, вызовом xTimerReset())
case ID_TIMER_2: * ДО этого момента, начнут отсчет времени. */
/* Индикация работы + текущее время */ vTaskStartScheduler();
printf(“\t\t\t\tOneShot timer ID = %d. Time = %d sec\n\r”, *pxTimerID, xTaskGetTickCount() / configTICK_RATE_HZ);
/* Запустить интервальный таймер 3 */ return 1;
xTimerStart(xOneShotTimers[2], 0); }
break; /*-----------------------------------------------------------*/
case ID_TIMER_3:
/* Индикация работы + текущее время */
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!”); онный файл FreeRTOSConfig.h должен содержать следующие строки:
fflush();
/* Удалить периодический таймер.
* После этого активных таймеров в программе не останется. */ #define configUSE_TIMERS 1
xTimerDelete(xAutoReloadTimer, 0); #define configTIMER_TASK_PRIORITY 1
break; #define configTIMER_QUEUE_LENGTH ( 10 )
} #define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE
}
/*-----------------------------------------------------------*/
Результат работы учебной программы приведен на рис. 6.
/* Точка входа в программу. */
short main( void ) В учебной программе демонстрируется прием, когда запуск (в дан-
{ ном случае сброс, как было сказано выше — не имеет значения) тай-
unsigned portBASE_TYPE i;
меров производится ДО запуска планировщика. В этом случае тайме-
/* Создать периодический таймер. ры начинают отсчет времени сразу после старта планировщика.
* Период работы таймера = 1 секунда.
* Идентификатор таймера не используется (0). */ В графическом виде работа учебной программы представлена
xAutoReloadTimer = xTimerCreate(“AutoReloadTimer”, uiAutoReloadTimerPeriod, pdTRUE, 0, на рис. 7.
vAutoReloadTimerFunction);
/* Выполнить сброс периодического таймера ДО запуска планировщика. Учебная программа демонстрирует также разницу между интер-
* Таким образом, он начнет отсчет времени одновременно с запуском планировщика. */
xTimerReset(xAutoReloadTimer, 0);
вальными и периодическими таймерами. Как видно на рис. 6 и 7, бу-
дучи единожды запущен, интервальный таймер вызовет свою функ-
/* Создать 3 экземпляра интервальных таймеров.
* Период работы таймеров = 12 секунд.
цию один раз. Периодический же таймер напротив — вызывает свою
* Каждому из них передать свой идентификатор. функцию до тех пор, пока не будет удален или остановлен.
* Функция для них всех одна — vOneShotTimersFunction(). */
for (i = 0; i < NUMBER_OF_TIMERS; i++) {
Справедливости ради следует отметить, что на практике можно
xOneShotTimers[i] = xTimerCreate(“OneShotTimer_n”, 12000 / portTICK_RATE_MS, pdFALSE, обойтись без использования идентификатора таймера для определе-
(void*) &uxOneShotTimersIDs[i], vOneShotTimersFunction);
}
ния, какой таймер вызвал функцию таймера, как это сделано в учеб-
ной программе.

Рис. 7. Отсчет временных промежутков в учебной программе

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011 www.kite.ru


100 компоненты микроконтроллеры

Для этой цели вполне достаточно использовать дескриптор тайме- сделать вывод о том, какой конкретно таймер инициировал вызов
ра. В таком случае функция интервальных таймеров в модифициро- этой функции таймера.
ванной учебной программе примет вид: Результат выполнения модифицированной учебной программы
ничем не будет отличаться от приведенного на рис. 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

КОМПОНЕНТЫ И ТЕХНОЛОГИИ • № 10 '2011

Вам также может понравиться