STM Урок 106. FreeRTOS. Динамическое создание и уничтожение задач. Часть 1



Продолжаем тему по изучению операционной системы реального времени FreeRTOS.

После изучения бинарных (двоичных) семафоров я хотел создать урок по использованию NENCONN API в библиотеке стека протоколов LWIP, но потом, почитав ещё гору различной литературы, всё-таки пришёл к выводу, что мы к этому ещё не готовы, так как мы слишком поверхностно знаем программирование наших контроллеров при использовании FreeRTOS. Так как вполне возможно, что нам нужно будет работать с несколькими соединениями одновременно, а мы даже не знаем, как работает задача, в каком она состоянии может находиться, как работает планировщик, в каком случае он разрешит выполняться той или иной задаче. А также, думаю, целесообразным будет в некоторых случаях под конкретное соединение создавать задачу не заранее в Cube MX, а в тот момент, когда нам будет необходимо данное соединение. Поэтому у меня и родилась данная тема — умение создавать задачи в коде, а также их уничтожать.

Но прежде чем приступить к этому непосредственно, давайте немного в общих чертах познакомимся, как вообще организована многозадачность в FreeRTOS, какие бывают задачи, какие состояния у них бывают и когда именно они выполняются. Также у каждой задачи, существующей в FreeRTOS, есть свой приоритет, но этим мы пока озадачиваться не будем и будем предполагать, что все задачи, которые мы пока будем создавать, будут иметь один и тот же приоритет. Правда мы создавали одну задачу с более низким приоритетом, чем задача по умолчанию, но пока она у нас не будет выполнять никакой код, тем не менее мы её оставим.

Также существуют несколько режимов многозадачности. Мы условимся, что пока будем использовать режим по умолчанию — вытесняющая многозадачность.

Если мы посмотрим код функции main() из занятий, в которых мы использовали FreeRTOS, то мы увидим, что после того, как в данной функции были вызваны функции создания задач osThreadCreate, вызывается функция osKernelStart, которая в своём теле делает ничто иное, как запускает стандартную функцию FreeRTOS vTaskStartScheduler, которая и запускает планировщик.

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

Любая задача может иметь несколько состояний на конкретный момент:

1) Выполняется (Running);

2) Готова к выполнению (Ready);

3) Блокирована (Blocked);

4) Приостановлена (Suspended).

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

 

 

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

Пока давайте рассмотрим такую ситуацию. У нас есть, например, три задачи, которые имеют одинаковый приоритет и «хотят» одновременно выполняться. Но по условиям работы операционной системы FreeRTOS в состоянии «Выполняется» в конкретный момент времени может находиться только одна задача. Поэтому планировщик переводит в такое состояние одну из задач, находящихся в состоянии «Готова к выполнению» и задача начинает выполняться. И, так как у нас задач целых три, следовательно, у нас скорее всего появится в очереди ещё одна задача, находящаяся в состоянии «Готова к выполнению» (не забываем, что она имеет точно такой же приоритет, как и выполняемая задача). В этом случае выполняемая задача (находящаяся в состоянии «Выполняется») будет выполняться определённое количество времени, а не до окончания выполнения кода, который в ней имеется, но конечно при условии, что это время не превышает время, необходимое для выполнения всего кода задачи. И это время строго фиксированное и оно определено в макроопределении configTICK_RATE_HZ и также называется системный квант времени. Это время, которое проходит после того как планировщик выполнил какую-то команду или задачу и заново получит управление. Только в константе, правда, определено не время, а частота, которая находится в макроопределении в герцах и из которой спокойно можно получить и время. У нас данная константа задаётся в CubeMX в разделе FreeRTOS в параметре TICK_RATE_HZ

 

 

Мы видим, что частота равна 1000 герц. Мы можем её изменить по нашему усмотрению, но пока мы этого делать не будем. Получается, что системный квант времени в нашем случае равен 1 милисекунде. То есть если код, который содержится в задаче, которая в данный момент выполняется, требует время для своего выполнения больше, чем системный квант времени, то по прошествии времени, равному системному кванту, задача перейдёт в состояние «Приостановлена», а задача 2 начнёт выполняться. Задача 3 из состояния «Приостановлена» перейдёт в состояние «Готова к выполнению». Затем будет выполняться задача 2 также в течении системного кванта времени либо до окончания всего кода, если он меньше системного кванта времени и так по кругу. Круг будет бесконечным, если во всех задачах в теле функций данных задач будет бесконечный цикл. Это самый идеальный вариант для понимания процесса многозадачности. То есть все 3 задачи у нас постоянно будут «хотеть» выполняться. И также в бесконечном цикле не должно быть задержек, вызванных специальной функцией vTaskDelay или osDelay, при использовании которых процесс переключения состояний задач пойдёт по несколько другому алгоритму.

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

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

Напомню цель нашего занятия — динамическое создание и уничтожение задач.

Мы с вами создавали задачи только при помощи добавления их в проектогенераторе Cube MX в свойствах FREERTOS. А теперь мы должны научиться это делать в коде.

Поэтому давайте создадим проект из проекта урока 104 BIN_SEMAPHORES, назовём его CREATE_TASKS и запустим в Cube MX. Также для мониторинга некоторой отладочной информации нам потребуется USART, поэтому давайте его включим

 

 

Также переопределим ножку PB6 в ножку PA9 ибо именно так у нашей платы F746-Discovery подключен USART через ST-Link

 

 

 

Перейдём в Configuration и настроим там наш USART

 

 

Зайдём в настройки FREERTOS и включим там возможность использования функции получения информации о задачах для отладки проекта

 

 

Также перейдём на закладку «Timers and Semaphores» и удалим наш семафор, пока он нам не потребуется

 

 

Сгенерируем код проекта и откроем его в System Workbench.

В настройках проекта изменим уровень оптимизации на 1, а также удалим все конфигурации отладчика. Откроем файл main.c и закомментируем там строки с ошибкой в функции конфигурации видеоускорителя DMA2D.

Также удалим функцию PrintCounter.

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

 

volatile uint32_t TIM1_Count=0, TIM1_Count_Sec=0;

char str1[60];

volatile uint8_t tasks_started=0;

uint32_t ncount1=0, ncount2=0;

 

Объявления подключенных хедеров перенесём в правильное место, так как они находились в месте, предназначенном для глобальных переменных

 

/* USER CODE BEGIN Includes */

#include "stdint.h"

#include "string.h"

#include "ltdc.h"

#include "MT48LC4M32B2.h"

#include "fonts.h"

/* USER CODE END Includes */

 

А глобальные переменные также перенесём в нужное место

 

/* Private variables ---------------------------------------------------------*/

#define LCD_FRAME_BUFFER SDRAM_DEVICE_ADDR

char str1[60];

/* USER CODE END PV */

 

 

Из функций задач удалим инкрементирование переменной

 

tasks_started++;

 

Также удалим код из бесконечных циклов, оставив там только задержки

 

for(;;)

{

  ncount1++;

  TFT_SetTextColor(LCD_COLOR_MAGENTA);

  sprintf(str1,"%lu ",ncount1);

  TFT_DisplayString(220, 100, (uint8_t *)str1, LEFT_MODE);

  PrintCounter(1);

  osDelay(1);

}

 

for(;;)

{

  ncount2++;

  TFT_SetTextColor(LCD_COLOR_CYAN);

  sprintf(str1,"%lu ",ncount2);

  TFT_DisplayString(220, 160, (uint8_t *)str1, LEFT_MODE);

  PrintCounter(2);

  osDelay(1);

}

 

Также удалим инициализацию дисплея и памяти, а также вывод шапки из функции main, а добавим инициализацию и вывод обновлённой шапки в функцию задачи по умолчанию StartDefaultTask

 

/* USER CODE BEGIN 5 */

MT48LC4M32B2_init(&hsdram1);

HAL_LTDC_SetAddress(&hltdc,LCD_FRAME_BUFFER,0);

TFT_FillScreen(LCD_COLOR_BLACK);

TFT_SetFont(&Font24);

TFT_SetTextColor(LCD_COLOR_LIGHTGREEN);

TFT_DisplayString(0, 10, (uint8_t *)"Create tasks", CENTER_MODE);

TFT_SetTextColor(LCD_COLOR_MAGENTA);

TFT_DisplayString(14, 60, (uint8_t *)"Task1:", LEFT_MODE);

TFT_DisplayString(14, 110, (uint8_t *)"Task2:", LEFT_MODE);

TFT_DisplayString(14, 160, (uint8_t *)"Task3:", LEFT_MODE);

/* Infinite loop */

 

Попробуем собрать наш проект. У нас появится шапка проекта на экране дисплея

 

 

Теперь добавим три функции для задач, которые мы будем динамически создавать и уничтожать

 

/* USER CODE BEGIN 4 */

//---------------------------------------------------------------

void Task01(void const * argument)

{

  TFT_SetTextColor(LCD_COLOR_BLUE);

  sprintf(str1,"%lu ", osKernelSysTick());

  TFT_DisplayString(280, 60, (uint8_t *)str1, RIGHT_MODE);

  osThreadTerminate(NULL);

}

//---------------------------------------------------------------

void Task02(void const * argument)

{

  TFT_SetTextColor(LCD_COLOR_BLUE);

  sprintf(str1,"%lu ", osKernelSysTick());

  TFT_DisplayString(280, 110, (uint8_t *)str1, RIGHT_MODE);

  osThreadTerminate(NULL);

}

//---------------------------------------------------------------

void Task03(void const * argument)

{

  TFT_SetTextColor(LCD_COLOR_BLUE);

  sprintf(str1,"%lu ", osKernelSysTick());

  TFT_DisplayString(280, 160, (uint8_t *)str1, RIGHT_MODE);

  osThreadTerminate(NULL);

}

//---------------------------------------------------------------

/* USER CODE END 4 */

 

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

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

Добавим прототипы для данных функций в соответствующем месте файла main.c

 

/* Private function prototypes -----------------------------------------------*/

void Task01(void const * argument);

void Task02(void const * argument);

void Task03(void const * argument);

/* USER CODE END PFP */

 

Также добавим глобальные переменные для хендлов наших задач

 

char str1[60];

osThreadId Task01Handle,Task02Handle,Task03Handle;

 

Теперь перейдём в функцию задачи по умолчанию StartDefaultTask и в её бесконечном цикле добавим код создания одной из задач, а также увеличим задержку

 

for(;;)

{

  osThreadDef(tsk01, Task01, osPriorityNormal, 0, 128);

  Task01Handle = osThreadCreate(osThread(tsk01), NULL);

  osDelay(500);

}

 

Мы создали задачу таким же образом, как они создаются в функции main() автоматически. Сначала мы объявили функцию задачи. Мы также присвоили задаче имя, которое находится в первом параметре. Следующим параметром идёт имя функции задачи, затем уровень приоритета. Мы присвоили такой же приоритет, как и у задачи по умолчанию. Дальше мы передаём максимальное количество экземпляров функции задачи, так как мы можем использовать одну и ту же функцию для нескольких задач. Если мы передаём 0, то количество экземпляров неограничено. Затем мы передаём значение стека под задачу в словах. Затем мы вызываем функцию, которая непосредственно создаёт экземпляр задачи, передавая в качестве первого параметра структуру, созданную в вызове предыдущей функции, а в качестве второго параметра — указатель на параметры, передаваемые в задачу. Мы пока ими не пользуемся и у нас будет NULL. В качестве возвращаемого значения мы получаем хендл (идентификатор) задачи. Затем мы вызываем задержку на 500 милисекунд, в течении которой наша задача по умолчанию будет пребывать в состоянии «Блокирована», тем самым давая возможность спокойно выполняться коду задачи, которую мы только что создали. Также мы надеемся, что за это время задача выполнится, хотя это не обязательно, нам так просто удобнее, а также задача успеет себя уничтожить, в чём мы убедимся немного позже, выведя определённую информацию в USART.

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

 

 

 

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

 

 

Предыдущий урок Программирование МК STM32 Следующая часть

 

 

Отладочную плату можно приобрести здесь 32F746G-DISCOVERY

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM FreeRTOS. Динамическое создание и уничтожение задач

3 комментария на “STM Урок 106. FreeRTOS. Динамическое создание и уничтожение задач. Часть 1
  1. Владимир:

    ссылку на видеоурок поправьте пожалуйста

  2. Евгений:

    Добрый день.
    Подскажите, пожалуйста, почему системный квант времени равен 1 мс при 1000 Гц?
    Частота работы микроконтроллера 100 МГц. 100М/1000 = 100 000, выходит 10^-5 с.

  3. Marek:

    Добрый день.

    Можете подсказать, почему блокирует udp thread i Ethif? (STM32F407VG STMcubeIDE)

    defaultTask X 3 2380 1
    IDLE R 0 117 3
    myTask02 R 0 117 2
    udp_thread1 B 4 397 5
    EthIf B 6 265 4


    static void udp_thread(void *arg) {
    printf("udp_thread run\r\n");
    struct_out *qstruct;
    err_t err, recv_err;
    struct netconn *conn;
    struct netbuf *buf;
    ip_addr_t *addr;
    unsigned short port;
    struct_sock *arg_sock;
    arg_sock = (struct_sock*) arg;
    conn = netconn_new(NETCONN_UDP);
    if (conn != NULL) {
    printf("Conected ! \r\n");

    err = netconn_bind(conn, IP_ADDR_ANY, arg_sock->port);
    if (err == ERR_OK) {
    for (;;) {
    recv_err = netconn_recv(conn, &buf);
    if (recv_err == ERR_OK) {
    addr = netbuf_fromaddr(buf);
    port = netbuf_fromport(buf);
    printf("Port %d \r\n", port);
    netconn_connect(conn, addr, port);
    qstruct = osMailAlloc(strout_Queue, osWaitForever);
    osMailPut(strout_Queue, qstruct);
    osMailFree(strout_Queue, qstruct);
    netconn_send(conn, buf);
    netbuf_delete(buf);
    }
    }
    } else {
    netconn_delete(conn);
    }
    }
    for (;;) {
    osDelay(1000);
    printf("Not conected ! \r\n");
    }
    }

    void StartDefaultTask(void const *argument) {
    /* init code for LWIP */
    MX_LWIP_Init();
    /* USER CODE BEGIN 5 */
    printf("MX_LWIP_Init \r\n");
    sys_thread_new("udp_thread1", udp_thread, NULL, 1024, osPriorityNormal);
    osThreadList((unsigned char*) str_buf);
    osDelay(10);
    printf("\r\n%s\r\n", str_buf);
    /* Infinite loop */
    for (;;) {
    osDelay(1);
    }
    /* USER CODE END 5 */
    }

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*