PureBasic - форум

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.



STM32F103C8T6 - FreeRTOS

Сообщений 1 страница 2 из 2

1

Думаю каждый при разработке более или менее сложной программы для МК сталкивался с ситуацией когда скорость выполнения программы меньше чем нужно. И дело даже не в производительности МК, а в алгоритме программы. Скажем при опросе кнопок для защиты от дребезга использованы функции задержки выполняя которые МК зря расходует энергию. А ведь вместо этой задержки можно выполнить другой код, что позволит более рационально использовать ресурсы МК. Но такой подход может значительно усложнить код сделав его запутанным. Видимо это обстоятельство привело к появлению RTOS - операционных систем реального времени для МК. Они представляют из себя планировщик задач, т. е. позволяют реализовать многозадачность, которая может быть нескольких видов, основные из которых это кооперативная и вытесняющая. В первом случае, активная задача сама решает когда она ей больше не нужно процессорное время и она может отдать его другим задачам, а во втором случае, задачу "никто не спрашивает" выполнила она все или нет, ее работу "наглым образом" :D прерывает планировщик и отдает процессорное время другой задаче. По такому принципу реализована многозадачность в винде. :) Такой подход хорош тем что если зависнет одна задача, другие будут нормально работать и более или менее гарантируется периодичность выполнения задач, а не как в корпоративной многозадачности, в которой одна задача может выполняться сколько угодно долго в то время как другие будут ждать пока она отдаст им процессорное время.
ОСь FreeRTOS позволяет реализовать как корпоративную, так и вытесняющую многозадачность и она поддерживает многие модели МК среди которые есть AVR и STM32.
В сети можно найти много статей о использовании FreeRTOS с STM32.

Итак, для того чтобы добавить FreeRTOS в проект, ее нужно скачать с официального сайта на котором сейчас доступная версия 9.0.0. В архиве много файлов и папок, но нам понадобится всего несколько.
Нужно скопировать все *.c файлы и папку include из папки "FreeRTOS\Source".
Также нужно скопировать папку "FreeRTOS\Source\portable\GCC\ARM_CM3".
Нужно из папки "FreeRTOS\Source\portable\MemMang" скопировать один из *.c файлов в зависимости от ситуации. Обычно хватает файла heap_1.c.

Отличия этих файлов

heap_1.c — собственный хитрый Malloc без Free. Т.е. мы можем создавать задачи (семафоры, очереди и т.д.), но не можем удалять их, чтобы освободить память. В принципе, для не слишком замороченных программ его хватает чуть более чем всегда. Задачи/очереди/семафоры всякие обычно создаются единожды и редко уничтожаются.
heap_2.c — динамичная фрагментированная память. Т.е. память динамически выделяется, освобождается, но при этом участки памяти получаются фрагментированными, с дырками на месте освободившейся памяти. И мы не сможем выделить большой кусок памяти, пусть даже у нас будет куча маленьких свободных секторов суммарно нужного обьема. А если новая задача укладывается в какой-либо из этих просветов, то будет выбран наиболее подходящий по размеру свободный кусок и задача развернется там.
heap_3.c — использован классический для Си Malloc/Free механизм с небольшой допилкой для невозожности запуска его из двух разных потоков.
heap_4.c — Динамическое выделение памяти, может дефрагментировать память, чтобы получить нужный кусок. Разумеется за все приходится платить. Временем в первую очередь.
heap_5.c — Тоже что и heap_4.c, но добавлена поддержка нескольких куч.[/b]

Также понадобится файл конфигурации ОС FreeRTOSConfig.h который можно взять скажем из папки "FreeRTOS\Demo\CORTEX_STM32F103_Primer_GCC".

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

Код
Код:
#include "stm32f10x_conf.h"

#include "FreeRTOS.h"
#include "task.h"

TaskHandle_t Task_1_Handle;
TaskHandle_t Task_2_Handle;
TaskHandle_t Task_3_Handle;
TaskHandle_t Task_4_Handle;

void Task_1(void const *argument) // Задача 1.
{
  while(1)
  {
    GPIO_SetBits(GPIOA, GPIO_Pin_4);
    vTaskDelay(200);
    GPIO_ResetBits(GPIOA, GPIO_Pin_4);
    vTaskDelay(100);
  }
}

void Task_2(void const *argument) // Задача 2.
{
  while(1)
  {
    GPIO_SetBits(GPIOA, GPIO_Pin_5);
    vTaskDelay(50);
    GPIO_ResetBits(GPIOA, GPIO_Pin_5);
    vTaskDelay(400);
  }
}

void Task_3(void const *argument) // Задача 3.
{
  while(1)
  {
    GPIO_SetBits(GPIOA, GPIO_Pin_6);
    vTaskDelay(100);
    GPIO_ResetBits(GPIOA, GPIO_Pin_6);
    vTaskDelay(500);
  }
}

void Task_4(void const *argument) // Задача 4.
{
  while(1)
  {
    GPIO_SetBits(GPIOA, GPIO_Pin_7);
    vTaskDelay(200);
    GPIO_ResetBits(GPIOA, GPIO_Pin_7);
    vTaskDelay(200);
  }
}


void vApplicationIdleHook(void) // Функция вызывается когда МК делать нечего, т. е. все задачи неактивны.
{
   __WFI();  // Отправляем МК спать до ближайшего прерывания.
}

int main(void)
{

    {
        GPIO_InitTypeDef GPIO_InitStructure; // Настройка портов A4, A5, A6 и A7 на выход

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;

        GPIO_Init(GPIOA, &GPIO_InitStructure);
    }

    // Создание задач.

    xTaskCreate((TaskFunction_t) Task_1, "Blinker 1",
                configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, Task_1_Handle);

    xTaskCreate((TaskFunction_t) Task_2, "Blinker 2",
                configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, Task_2_Handle);

    xTaskCreate((TaskFunction_t) Task_3, "Blinker 3",
                configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, Task_3_Handle);

    xTaskCreate((TaskFunction_t) Task_4, "Blinker 4",
                configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, Task_4_Handle);


    // Запуск планировщика задач.
    vTaskStartScheduler();

    while(1)
    {
    }
}

Сами по себе задачи (потоки) это обычные функции код которых регулярно выполняется по мере переключения задач и в функции должен быть бесконечный цикл.
Задача создается функцией xTaskCreate.

Описание аргументов функции

Task_1 - это имя функции которая будет задачей.
"Blinker 1" - текстовая строка для отладки. Может быть любой, но длина ограничена в конфиге ОС.
configMINIMAL_STACK_SIZE - размер стека для задачи. Определяется опытным путем. В данном случае стоит системный минимум, т. к. задачи используют очень мало памяти.
NULL - передаваемый argument в задачу. В данном случае не нужен, потому NULL.
tskIDLE_PRIORITY + 1 - приоритет задачи. Выбран минимально возможный. На 1 пункт выше чем IDLE.
Task_1_Handle - адрес переменной типа xTaskHandle в которую запишется дескриптор задачи. Зная этот дескриптор мы можем ею управлять, например приостановить или завершить ее работу.

Функция vTaskStartScheduler() запускает планировщик задач и с этого момента на ОСи начинают крутиться задачи. :D

Скриншот

Среда EmBitz при отладке обнаружила использование FreeRTOS в проекте и показала все задачи и некоторую информацию о них. :)

http://s3.uploads.ru/aHzTi.png

Теперь о том что наверное интересует многих - сколько ресурсов требует ОСь. Как оказалось, сравнительно не много. Нужно примерно 1.5 КБ flash и по 602 байта ОЗУ на каждую задачу. Учитывая что в МК 20 КБ ОЗУ, это позволит запустить до 30 задач, что на мой взгляд больше чем достаточно. :) А ведь есть МК с гораздо большим объемом ОЗУ... :) Например STM32F03RET6 содержит 512 КБ flash и 64 КБ ОЗУ. :D

Файлы примера. http://pure-basic.narod.ru/forum_files/ … askLed.zip
Светодиоды нужно подключить к выводам A4, A5, A6 и A7.

0

2

В продолжение темы. Сколько процессорного времени использует ОСь FreeRTOS, и вообще, насколько сильно загружен процессор выполняя текущие задачи?
Вычислить процент загрузки процессора относительно не сложно. Необходимо посчитать сколько тактов процессор выполнял полезную работу, а сколько спал ничего не делая. Достаточно вычислить последнее, а первое не обязательно, ведь нам известна частота тактирования ядра и если ее поделить на число тактов "сна" процессора, то получим соотношение "полезная работа"/"сон" из которого можно получить загрузку в процентах.
Теперь осталось определится чем считать такты ядра. Конечно для этого можно использовать один из таймеров, но это не всегда возможно, т. к. они могут быть заняты. Нужен какой-то счетчик (желательно большой разрядности) который не жалко использовать для таких целей и который тактируется непосредственно от ядра. Оказывается есть такой. Как известно в ядре ARM Cortex-M3 присутствует модуль отладки (точнее это не отдельный модуль, а часть ядра). Нужный нам счетчик находится в блоке DWT и называется CYCCNT. Он 32-ух разрядный и тактируется непосредственно от ядра - то что нам нужно. Осталось дело за малым - программно запустить модуль отладки и активировать счетчик. Для этой цели в начало кода нужно добавить строки

Код:
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // Включаем TRACE
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;            // Разрешаем DWT счетчик CYCCNT

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

Структура с регистрами блока DWT
Код:
typedef struct
{
  __IO uint32_t CTRL;                    /*!< Offset: 0x000 (R/W)  Control Register                          */
  __IO uint32_t CYCCNT;                  /*!< Offset: 0x004 (R/W)  Cycle Count Register                      */
  __IO uint32_t CPICNT;                  /*!< Offset: 0x008 (R/W)  CPI Count Register                        */
  __IO uint32_t EXCCNT;                  /*!< Offset: 0x00C (R/W)  Exception Overhead Count Register         */
  __IO uint32_t SLEEPCNT;                /*!< Offset: 0x010 (R/W)  Sleep Count Register                      */
  __IO uint32_t LSUCNT;                  /*!< Offset: 0x014 (R/W)  LSU Count Register                        */
  __IO uint32_t FOLDCNT;                 /*!< Offset: 0x018 (R/W)  Folded-instruction Count Register         */
  __I  uint32_t PCSR;                    /*!< Offset: 0x01C (R/ )  Program Counter Sample Register           */
  __IO uint32_t COMP0;                   /*!< Offset: 0x020 (R/W)  Comparator Register 0                     */
  __IO uint32_t MASK0;                   /*!< Offset: 0x024 (R/W)  Mask Register 0                           */
  __IO uint32_t FUNCTION0;               /*!< Offset: 0x028 (R/W)  Function Register 0                       */
       uint32_t RESERVED0[1];
  __IO uint32_t COMP1;                   /*!< Offset: 0x030 (R/W)  Comparator Register 1                     */
  __IO uint32_t MASK1;                   /*!< Offset: 0x034 (R/W)  Mask Register 1                           */
  __IO uint32_t FUNCTION1;               /*!< Offset: 0x038 (R/W)  Function Register 1                       */
       uint32_t RESERVED1[1];
  __IO uint32_t COMP2;                   /*!< Offset: 0x040 (R/W)  Comparator Register 2                     */
  __IO uint32_t MASK2;                   /*!< Offset: 0x044 (R/W)  Mask Register 2                           */
  __IO uint32_t FUNCTION2;               /*!< Offset: 0x048 (R/W)  Function Register 2                       */
       uint32_t RESERVED2[1];
  __IO uint32_t COMP3;                   /*!< Offset: 0x050 (R/W)  Comparator Register 3                     */
  __IO uint32_t MASK3;                   /*!< Offset: 0x054 (R/W)  Mask Register 3                           */
  __IO uint32_t FUNCTION3;               /*!< Offset: 0x058 (R/W)  Function Register 3                       */
} DWT_Type;

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

Код:
void vApplicationIdleHook(void) // Функция вызывается когда МК делать нечего, т. е. все задачи неактивны.
{
   __WFI();  // Отправляем МК спать до ближайшего прерывания.
}

Из комментариев думаю понятно для чего она нужна. Ее код выполняется когда другие задачи находятся в режиме ожидания и их код не выполняется. Функция __WFI() приостанавливает работу ядра до ближайшего прерывания. Чтобы определить загрузку процессора нужно перед вызовом __WFI() сохранить в 32-ух битной переменой число из счетчика CYCCNT, а после того как ядро "проснется" по прерыванию, нужно еще раз сохранить число из CYCCNT и из него вычесть ранее сохраненное и таким образом узнаем сколько тактов ядро "спало", т. е. не выполняло полезной работы. Чем больше это число тем меньше загрузка процессора.
Для этого код функции vApplicationIdleHook нужно немного доработать.

Код:
void vApplicationIdleHook(void) // Функция вызывается когда МК делать нечего, т. е. все задачи неактивны.
{
    if (Start_SleepTime == 0)
        Start_SleepTime = DWT->CYCCNT;  // Запоминаем время перехода в спячку.

    __WFI();  // Отправляем МК спать до ближайшего прерывания.
}

Перед вызовом __WFI() и при условии что в Start_SleepTime будет 0, в переменной сохраняется число тактов из счетчика CYCCNT. На первый взгляд логично было бы после вызова __WFI() еще раз прочитать число тактов из CYCCNT и вычислить сколько ядро было в "спячке", но это не так. Считывать нужно в прерывании, которое "разбудило" ядро, иначе результат получится совсем не тот что ожидаем. Во всех возможных обработчиках прерываний это делать слишком накладно. Допуская небольшую ошибку вычислений (такты потраченные на прерывания будут засчитаны за сон) это можно сделать только в обработчике системного таймера.

Код:
void vApplicationTickHook(void) // Вызывается при каждом прерывании системного таймера.
{
    static uint32_t Count, SleepTime;

    if (Start_SleepTime)
    {
        SleepTime += DWT->CYCCNT - Start_SleepTime;
        Start_SleepTime = 0;
    }

    if (++Count >= configTICK_RATE_HZ) // Прошла секунда.
    {
        Count = 0;

        if (SleepTime != 0)
        {
            CPU_Load = 100 - 100.0f / (SystemCoreClock / (float)SleepTime); // Загрузка CPU в процентах.
        }
        else CPU_Load = 100;

        SleepTime = 0;
    }
}

В начале функции проверяется чтобы Start_SleepTime был не 0, что означает что в vApplicationIdleHook было сохранено число из CYCCNT. Существует низкая вероятность (один шанс из больше чем 4 миллиарда) того что в момент чтения из CYCCNT в нем окажется 0 (счетчик 32-ух битный и при тактовой частоте ядра 72 МГц, он переполняется каждую минуту) и это приведет к ошибке вычисления, но из-за низкой вероятности этим можно пренебречь.
Дальше по коду, используя переменную-счетчик Count отсчитываются секундные интервалы и кадую секунду вычисляется загрузка процессора в процентах, путем деления частоты ядра на количество тактов в течение которого ядро "спало".
В таком виде задач как в первом сообщении, загрузка процессора ими и ОСью составляет 0.2% что относительно не много.
Если код одной из задач изменить

Код:
void Task_4(void const *argument) // Задача 4.
{
  while(1)
  {
      static volatile uint32_t i;
      for (i=0; i<100; i++);
      vTaskDelay(1);
  }
}

то нагрузка на процессор возрастет и составит почти 3%. При текущей конфигурации ОСи переключающей задачи 1000 раз в секунду, этот цикл выполнится также 1000 раз, а значит это эквивалентно циклу со счетом до 100 тысяч.
Увеличение числа в цикле до 1000 (что эквивалентно миллиону) создаст нагрузку 19.2%.
Если же увеличить до 5800 (эквивалентно 5800000) это почти полностью загрузит процессор - 97.8%.

Файлы http://pure-basic.narod.ru/forum_files/ … U_Load.zip

0