Лабораторная работа №10. Настройка и запуск задач FreeRTOS
10.1 Цели и задачи
Целью работы является освоение процесса запуска многопоточного резидентного программного обеспечения для микросхем на основе RISC-V под управлением FreeRTOS.
Для достижения поставленной цели требуется решить следующие задачи:
- Изучить принцип настройки FreeRTOS.
- Ознакомиться с процессом запуска ядра FreeRTOS.
- Разработать задачи, выполняемые во FreeRTOS.
Презентация к блоку "Операционные системы реального времени RISC-V"
10.2 Основные теоретические сведения
В настоящее время к оборудованию, для которого разрабатывается встроенное ПО, предъявляются новые требования по надежности, времени реакции на событие и объему выполняемых функций. Это приводит к усложнению логики работы программ. Другими словами, эра тривиальных задач подходит к концу или может быть уже завершена, а для решения нетривиальных задач разработчику необходим своего рода помощник. Операционная система реального времени (ОСРВ) прекрасно выполняет роль помощника разработчика. ОСРВ с легкостью берет на себя часть рутинной работы, а главное - реализует механизм псевдопараллельного исполнения кода задач.
На сегодняшний день на рынке ОСРВ появляется все больше решений с открытым исходным кодом, которые могут составить конкуренцию коммерческим аналогам. Примером является FreeRTOS, которая портирована более чем на 20 платформ (микроконтроллеров) и потребует от аппаратного обеспечения от 32 Кбайт флэш-памяти и от 16 Кбайт ОЗУ. FreeRTOS предоставляется с открытым исходным кодом программ и лицензирована в соответствии с GNU General Public License (GPL).
Использование FreeRTOS позволит нам организовать сразу несколько потоков (задач, нитей - threads) и в любой момент уничтожить, приостановить или запустить любой из них.
Любая задача может иметь несколько состояний на конкретный момент (Рис. 10.1):
- Выполняется (Running);
- Готова к выполнению (Ready);
- Блокирована (Blocked);
- Приостановлена (Suspended).
Отличием FreeRTOS от ОС общего назначения (Windows, Linux, MacOS) заключается в том, что программы под управлением ОСРВ организуют ограниченный набор действий, но эти действие требуют максимально быстрой обработки. ОС общего назначения могут реализовывать больший набор функций, но время отклика для них не является критичным параметром.
Если рассмотреть типичное приложение с использованием FreeRTOS, то можно выделить три слоя поверх аппаратного обеспечения: пользовательский код, платформонезависимый код и платформозависимый код:
-
Платформонезависимый код
- Задачи. Основное назначение ядра - работа с задачами.
- Связь. Задачам необходимо обмениваться данными, за это отвечает механизм очередей в ядре ОС.
- Аппаратное сопряжение - содержит код, абстрагирующий пользовательские программы от особенностей конкретного аппаратного обеспечения.
Для запуска созданных задач требуется запустить планировщик операционной системы. Для этого требуется вызвать функцию vTaskStartScheduler(). Место вызова этой функции в коде не принципиально, однако обычно её используют в функции main.
В общем случае структура программы выглядит следующим образом:
#include "clock_config.h" #include "bsp.h" #include "FreeRTOS.h" int main() { // config CPU/MPU clock init_clock(); // init CPU/MPU periphery init_bsp(); // create tasks, mutexes, etc create_freertos_elements(); // start OS vTaskStartScheduler(); // This line will never be reached while(1){} }
Точкой входа в программу является функция main, однако задачи запускаются планировщиком ОС. Допускается выносить создание задач и запуск планировщика в отдельную функцию.
Задачи создаются с помощью функции xTaskCreate.
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, const char * const pcName, configSTACK_DEPTH_TYPE usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pxCreatedTask );
Функция принимает в качестве параметров указатель на исполняемую функцию, название задачи, размер выделяемой для задачи памяти на стеке, указатель на структуру параметров задачи, приоритет и обработчик (Handler) задачи для доступа из других задач. В качестве параметра для задач может передаваться (void*)NULL, если параметра нет.
Требуется проверять успешность выполнения запуска задачи, так как из-за нехватки памяти этого может не произойти. В таком случае предлагается уменьшать размер задач и размер стека, выделяемый для задачи.
Удаление задачи выполняется с помощью функции void vTaskDelete( TaskHandle_t xTask ), которая принимает в качестве параметра обработчик задачи.
Для управления задачами существует набор функций:
- vTaskDelay(const TickType_t xTicksToDelay)
- vTaskDelayUntil(TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement)
- xTaskDelayUntil(TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement)
- uxTaskPriorityGet(const TaskHandle_t xTask)
- uxTaskPriorityGetFromISR(const TaskHandle_t xTask)
- uxTaskBasePriorityGet(const TaskHandle_t xTask)
- uxTaskBasePriorityGetFromISR(const TaskHandle_t xTask)
- vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority)
- vTaskSuspend(TaskHandle_t xTask)
- vTaskResume(TaskHandle_t xTask)
- xTaskResumeFromISR(TaskHandle_t xTask)
- xTaskAbortDelay(TaskHandle_t xTask)
Эти функции осуществляют задержку, смену приоритета, приостановку и возобновление задач. Они вызываются в коде задач. Это требуется для того, чтобы управлять выполнением задач: прервать одну задачу для упрощения выполнения других, повышение приоритета для выполнения более важных функций, и тд.
Основной функцией задержки является vTaskDelay( const TickType_t xTicksToDelay ). Она приостанавливает задачу на указанное число системных тиков. Длительность тика определяется в FreeRTOSConfig.h. Для их использования в этом файле нужно определить макрос INCLUDE_vTaskDelay, равный 1.
Для получения приоритета задачи используется функция uxTaskPriorityGet(), а для установки - vTaskPrioritySet(). Для использования функции получения приоритета задачи нужно создать макросы INCLUDE_uxTaskPriorityGet и configUSE_MUTEXES, равные 1. Для использования функции изменения приоритета нужно создать макрос INCLUDE_vTaskPrioritySet, равный 1.
Для приостановки задачи используется функция vTaskSuspend(TaskHandle_t xTaskToSuspend), а для возобновления - vTaskResume(TaskHandle_t xTaskToResume). Для их использования в этом файле нужно определить макрос INCLUDE_vTaskDelay, равный 1.
Во многих функциях в названии присутствует FromISR. Это означает, что данные функции предназначены для использования в аппаратных прерываниях.
Пример запуска задачи с параметром:
/* Task to be created. */ void vTaskCode( void * pvParameters ) { /* The parameter value is expected to be 1 as 1 is passed in the pvParameters value in the call to xTaskCreate() below. configASSERT( ( ( uint32_t ) pvParameters ) == 1 ); for( ;; ) { /* Task code goes here. */ } } /* Function that creates a task. */ void vOtherFunction( void ) { BaseType_t xReturned; TaskHandle_t xHandle = NULL; /* Create the task, storing the handle. */ xReturned = xTaskCreate( vTaskCode,/* Function that implements the task. */ "NAME", /* Text name for the task. */ STACK_SIZE, /* Stack size in words, not bytes. */ ( void * ) 1,/* Parameter passed into the task. */ tskIDLE_PRIORITY,/* Priority of created task. */ &xHandle ); /* Pass out the created handle.*/ if( xReturned == pdPASS ) { /* The task was created. */ /* Use the task's handle to delete the task. */ vTaskDelete( xHandle ); } }
с большим количеством параметров может потребовать больше кода, чем разработка специализированных задач.
Помимо штатных инструментов FreeRTOS одним из важнейших приемов работы для разработки встраиваемого ПО выступит условная компиляция.Для того, чтобы в рамках одного проекта сформировать разный набор задач предлагается использовать условную компиляцию. Для её использования применяются директивы препроцессора для создания макросов. Напомним, как работает данный принцип. Основной директивой является #define, а выбор нужных действий осуществляется с помощью следующих директив:
- #ifdef - выполнить блок команд, если макрос определен,
- #ifndef - выполнить блок команд если макрос не определен,
- #if - выполнить блок команд если макрос соответствует условию,
- #else - завершить блок команд и выполнить другой блок, противоположный по назначению одной из команд выше,
- #endif - завершить блок команд.
Пример использования условной компиляции:
#define TASKS_SET_1 //#define TASKS_SET_2 void create_tasks() { #ifndef TASKS_SET_1 create_task_1(); create_task_2(); #endif #ifdef TASKS SET_2 create_task_3(); create_task 4(); #else create_task_5(); create_task_6(); #endif }
В ходе выполнения этой функции будут созданы задачи 5 и 6.
Использование условной компиляции позволяет достигнуть следующих преимуществ для встраиваемых систем:
- Возможность выбора конфигурации задач на основе созданных наборов.
- Возможность добавления и быстрого отключения отладочных функций.
- Создание числовых или строковых констант без увеличения объема исполняемого файла (использование для этих целей ключевого слова const приведет к размещению созданного значения в постоянной памяти микросхемы).
10.2.1 Сборка и запуск программ FreeRTOS
Основным назначением FreeRTOS является разработка многозадачных программ для встраиваемых систем на основе микроконтроллеров и микропроцессоров. Для программирования этих устройств используются отладочные платы. Однако, работа с многозадачностью на реальных аппаратных устройствах чревата дополнительными сложностями, которые выходят за рамки задач данного практикума. Поэтому, для упрощения выполнения лабораторных работ вместо отладочных плат будет использоваться эмулятор на базе QEMU. Так, вместо мигания светодиодом требуется выводить сообщение об изменении его состояния.
Для разработки своих программ на базе FreeRTOS требуется использовать файлы исходного кода ОС. Для этого нужно загрузить архив по ссылке. В архиве есть исходный код и демонстрационные проекты для разных архитектур. Для выполнения лабораторных работ №13-15 предлагается использовать демонстрационный проект /FreeRTOS/Demo/RISC-V-Qemu-virt_GCC в качестве базового шаблона. Данный проект уже включает в себя необходимые файлы для сборки приложений под архитектуру RISC-V.
Выполним сборку и запуск проекта с помощью инструментов Syntacore Kit. Для этого необходимо открыть в терминале гостевой ОС каталог проекта и выполнить команду
$ make
При ее выполнении возможно возникновение два вида ошибок:
- Если пример не скомпилируется из-за отсутствующих инструкций csrc и csrv (ошибки вида "Error: unrecognized opcode …"), то необходимо в файле Makefile изменить все вхождения -march=rv32imac на -march=rv32imac_zicsr_zifencei для поддержки необходимого расширения.
- В случае невозможности компиляции из-за не определенной константы configCLINT_BASE_ADDRESS, необходимо добавить её ручное определение в FreeRTOSConfig.h. В этом файле используется константа CLINT_ADDR. В таком случае необходимо добавить в начало файла FreeRTOSConfig.h после директив #include строку
#define configCLINT_BASE_ADDRESS CLINT_ADDR
После успешной компиляции, в каталоге проекта появится бинарный файл ./build/RTOSDemo.axf. Запустим его выполнение в QEMU с помощью команды
$ qemu-system-riscv32 -nographic -machine virt -net none \ -chardev stdio,id=con,mux=on -serial chardev:con \ -mon chardev=con,mode=readline -bios none \ -smp 4 -kernel ./build/RTOSDemo.axf
Если всё выполнено корректно, то будет запущен набор задач main_blinky (Рис. 10.2), который выводит строку приветствия и периодически выводит данные, передаваемые между двумя задачами с помощью очереди.