Целью работы является освоение процесса запуска многопоточного резидентного программного обеспечения для микросхем на основе RISC-V под управлением FreeRTOS.
Для достижения поставленной цели требуется решить следующие задачи:
Презентация к блоку "Операционные системы реального времени RISC-V"
В настоящее время к оборудованию, для которого разрабатывается встроенное ПО, предъявляются новые требования по надежности, времени реакции на событие и объему выполняемых функций. Это приводит к усложнению логики работы программ. Другими словами, эра тривиальных задач подходит к концу или может быть уже завершена, а для решения нетривиальных задач разработчику необходим своего рода помощник. Операционная система реального времени (ОСРВ) прекрасно выполняет роль помощника разработчика. ОСРВ с легкостью берет на себя часть рутинной работы, а главное - реализует механизм псевдопараллельного исполнения кода задач.
На сегодняшний день на рынке ОСРВ появляется все больше решений с открытым исходным кодом, которые могут составить конкуренцию коммерческим аналогам. Примером является FreeRTOS, которая портирована более чем на 20 платформ (микроконтроллеров) и потребует от аппаратного обеспечения от 32 Кбайт флэш-памяти и от 16 Кбайт ОЗУ. FreeRTOS предоставляется с открытым исходным кодом программ и лицензирована в соответствии с GNU General Public License (GPL).
Использование FreeRTOS позволит нам организовать сразу несколько потоков (задач, нитей - threads) и в любой момент уничтожить, приостановить или запустить любой из них.
Любая задача может иметь несколько состояний на конкретный момент (Рис. 10.1):
(рис 10.1) Возможные состояния задач во FreeRTOS
Отличием 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 приведет к размещению созданного значения в постоянной памяти микросхемы).Основным назначением 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), который выводит строку приветствия и периодически выводит данные, передаваемые между двумя задачами с помощью очереди.
(рис 10.2) Запущенный проект hello_blinky
Для выполнения работы требуется:
В лабораторной работе необходимо разработать два набора задач для программы. Каждая задача осуществляет мигание светодиодом. В первом наборе задач светодиод и период мигания задаются в теле задачи. Во втором наборе требуется реализовать одну задачу, которая вызывается необходимое количество раз с передачей параметров. Оба набора задач должны выполнять одинаковый функционал. Для выбора реализуемого набора задач предлагается использовать условную компиляцию. Порядок работы со светодиодами:
n = max(k, 2 + (a % 4));T[0] = a * t;T[i+1] = T[i] + b * t;где а = номер варианта, b = количество букв в фамилии студента, t - базовый период, принимаемый как 200 тиков процессора, k - количество доступных для работы светодиодов. Для эмуляции в эмуляторе предлагается выводить строку, обозначающую мигание определенного светодиода. Максимальное количество светодиодов k предлагается принять равным 5 при эмуляции. Для остановки выполнения программы используется стандартное в ОС Linux сочетание клавиш для остановки выполнения программы Ctrl+C.
Для выполнения задания необходимо:
//#define USE_PARAMETERS_IN_TASKS
#ifndef USE_PARAMETERS_IN_TASKS
void parameter_task_function_1(void *pvParameters)
{
while(1)
{
vSendString("Toggle 1");
vTaskDelay(25000);
}
}
void parameter_task_function_2(void *pvParameters)
{
while(1)
{
vSendString("Toggle 2");
vTaskDelay(50000);
}
}
void parameter_task_function_3(void *pvParameters)
{
while(1)
{
vSendString("Toggle 3");
vTaskDelay(100000);
}
}
int main_tasks(){
xTaskCreate((TaskFunction_t )parameter_task_function_1,
(const char* )"LED1_Task",
(uint16_t )512,
(void* )NULL,
(UBaseType_t )2,
(TaskHandle_t* )NULL);
xTaskCreate((TaskFunction_t )parameter_task_function_2,
(const char* )"LED2_Task",
(uint16_t )512,
(void* )NULL,
(UBaseType_t )2,
(TaskHandle_t* )NULL);
xTaskCreate((TaskFunction_t )parameter_task_function_3,
(const char* )"LED3_Task",
(uint16_t )512,
(void* )NULL,
(UBaseType_t )2,
(TaskHandle_t* )NULL);
vTaskStartScheduler();
return 0;
}
#else
typedef struct {
int num;
char* text;
} lab_task_data;
char* names[] = {"Toggle 1", "Toggle 2", "Toggle 3"};
lab_task_data data[3];
void parameter_task_function(void *pvParameters)
{
lab_task_data* config = (lab_task_data*)pvParameters;
while(1)
{
vSendString(config->text);
vTaskDelay(config->num);
}
}
int main_tasks(){
for(int i=0;i<3;i++){
data[i].num = 100*(250*<<i);
data[i].text = names[i];
xTaskCreate((TaskFunction_t )parameter_task_function,
(const char* )names[i],
(uint16_t )512,
(void* )data[i],
(UBaseType_t )2,
(TaskHandle_t* )NULL);
}
vTaskStartScheduler();
return 0;
}
#endif
int main(){
main_tasks();
while(1);
}
Каждая из этих программ выполняет мигание светодиодами. Каждый светодиод мигает с определенной частотой, заданной параметром задержки.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.