You are here

Решение №2: Полностью вытесняющая система

Перевод может содержать ошибки. Читайте первоисточник: Solution #2 - A Fully Preemptive System

Назад: [Как работает FreeRTOS] Вверх: [Как работает FreeRTOS] Вперёд: [Как работает FreeRTOS]

 

Краткий обзор

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

 

Реализация

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

Решение №2: задачи по функциям и их приоритеты

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

Приоритеты распределяются задачам в соответствие с их требованиями к хронометражу. Чем жёстче требования, тем выше приоритет. Но следует учитывать, что распределение приоритетов - не всегда простая задача.

 

Концепция работы

Задача с наивысшим приоритетом, которая готова к запуску (не заблокирована), это задача, которой ОСРВ гарантирует получение процессорного времени. Ядро немедленно приостановит выполнение задачи, если станет доступна задача с более высоким приоритетом.

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

Когда нет задач, готовыхк запуску, выполняется задача простоя. Задача простоя может переводить процессор в режим пониженного энергопотребления.

 

Конфигурация планировщика

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

 

Оценка

Простая, сегментированная, гибкая, обслуживаемая структура проекта с небольшим количеством взаимозависимостей.
Использование процессора автоматически переключается от задачи к задаче в самых неотложных случаях без каких-либо явных действий в исходном коде приложения.
Управляемая событиями структура гарантирует, что процессорное время не будет потрачено впустую для опроса событий, которые не произошли. Обработка выполняется только тогда, когда необходимо что-то обработать.
Энергопотребление может быть снижено, если задача простоя переводит процессор в режим энергосбережения (sleep mode), но часть энергии может быть потрачена впустую, т.к. тиков ОС иногда приводит к ненужному пробуждению процессора.
Функциональность ядра будет использовать вычислительные ресурсы. Степень использования будет зависеть от выбранной частоты тиков ядра.
Это решение требует некоторого, довольно большого, количества задач, каждая из которых требует своего собственного стека. Кроме того, многим задачам нужны очереди, через которые задачи будут получать события. Поэтому это решение использует довольно много ОЗУ.
Частое переключение контекста между задачами с одинаковым приоритетом тоже приводит к расходованию циклов процессора впустую.

 

Заключение

Это может быть хорошим решением при наличии достаточного объёма ОЗУ и вычислительной мощности. Разделение приложения на задачи и назначение приоритетов каждой задаче требует тщательного рассмотрения.

 

Пример

Этот пример является частичной реализацией гипотетического приложения, представленного ранее. Используется API FreeRTOS.

 

Задача управления агрегатом

Эта задача реализует всю функциональность управления. Она имеет критические требования к хронометражу, и поэтому ей назначается наивысший приоритет в системе:

/*----------------------------------------------------------------------------*/
#define CYCLE_RATE_MS       10
#define MAX_COMMS_DELAY     2

void PlantControlTask( void *pvParameters )
{
TickType_t xLastWakeTime;
DataType Data1, Data2;

    InitialiseTheQueue();

    // A
    xLastWakeTime = xTaskGetTickCount();

    // B
    for( ;; )
    {
        // C
        vTaskDelayUntil( &xLastWakeTime, CYCLE_RATE_MS );
        
        /* Запрос данных с датчика. */
        TransmitRequest();
        
        // D
        if( xQueueReceive( xFieldBusQueue, &Data1, MAX_COMMS_DELAY ) )
        {
            // E
            if( xQueueReceive( xFieldBusQueue, &Data2, MAX_COMMS_DELAY ) )
            {
                PerformControlAlgorithm();
                TransmitResults();                
            }
        } 
    }
    
    /* Здесь никогда не должны оказываться. */
}

Пояснения к меткам в представленном выше фрагменте кода:

  1. Начальная инициализация переменной xLastWakeTime. Эта переменная используется с функцией API vTaskDelayUntil() для управления частотой выполнения функции управления.
  2. Эта функция, PlantControlTask(), запускается как самостоятельная задача, поэтому никогда не должна завершаться.
  3. Функция vTaskDelayUntil() сообщает ядру, что задача должна запускаться строго через 10 мс после времени, сохранённого в переменной xLastWakeTime. Пока это время не вышло, задача будет заблокирована. Поскольку эта задача имеет наивысший приоритет в системе, она гарантированно возобновит выполнение в строго нужный момент. Она вытеснит любую выполняющуюся задачу с более низким приоритетом.
  4. Имеется конечное время между запросом данных с сетевых датчиков и получения данных. Данные, поступающие через управляющую шину, помещаются в очередь xFieldBusQueue с помощью функции обработчика прерывания, поэтому функция управления может сделать блокирующий вызов на ожидании поступления данных. Как и перед этим, поскольку задаче управления назначен наивысший приоритет в системе, это гарантирует немедленное продолжение выполнения задачи сразу же по получению данных.
  5. Как и 'D', ожидаем данные от датчика, но уже от второго.

Возвращаемое функцией xQueueReceive() значение 0 показывает, что данные не были получены в течение отведённого времени блокировки. Это ошибочное состояние, которое должно быть обработано. Обработка этой и других ошибок были опущены для простоты.

 

Задача встроенного веб-сервера

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

/*----------------------------------------------------------------------------*/
void WebServerTask( void *pvParameters )
{
DataTypeA Data;

    for( ;; )
    {
        /* Блокируем задачу, пока не получим данные. В очередь xEthernetQueue */
        /* данные будут помещаться обработчиком прерываний Ethernet. */
        if( xQueueReceive( xEthernetQueue, &Data, MAX_DELAY ) )
        {
            ProcessHTTPData( Data );
        }        
    }
}

 

Интерфейс RS232

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

/*----------------------------------------------------------------------------*/
void RS232Task( void *pvParameters )
{
DataTypeB Data;

    for( ;; )
    {
        /* Блокируем до получения данных. В очередь xRS232Queue данные будут */
        /* помещаться в обработчике прерывания от RS232. */
        if( xQueueReceive( xRS232Queue, &Data, MAX_DELAY ) )
        {
            ProcessSerialCharacters( Data );
        }        
    }
}

 

Задача сканирования клавиатуры

Это простая циклическая задача. Ей назначается средний приоритет, т.к. требования к хронометражу такие же, как и у задачи обслуживания порта RS232.

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

/*----------------------------------------------------------------------------*/
#define DELAY_PERIOD 4

void KeyScanTask( void *pvParmeters )
{
char Key;
TickType_t xLastWakeTime;

    xLastWakeTime = xTaskGetTickCount();

    for( ;; )
    {
        /* Ждём момента начала следующего цикла. */
        vTaskDelayUntil( &xLastWakeTime, DELAY_PERIOD );
        
        /* Сканируем клавиатуру. */
        if( KeyPressed( &Key ) )
        {
            UpdateDisplay( Key );
        }
    }
}

Если общая система хронометража была бы такой, что этой задаче можно было бы назначить наименьший приоритет, тогда вызов vTaskDelayUntil() можно было бы вообще убрать. Функция сканирования клавиатуры тогда выполнялась бы непрерывно в цикле, пока все задачи с более высоким приоритетом находятся в заблокированном состоянии. Это эффективно расположить в задаче простоя.

 

Задача управления светодиодами

Это самая простая из всех задач.

/*----------------------------------------------------------------------------*/
#define DELAY_PERIOD 1000

void LEDTask( void *pvParmeters )
{
TickType_t xLastWakeTime;

    xLastWakeTime = xTaskGetTickCount();

    for( ;; )
    {
        /* Ждём момента начала следующего цикла. */
        vTaskDelayUntil( &xLastWakeTime, DELAY_PERIOD );

        /* Мигаем нужным светодиодом. */
        if( SystemIsHealthy() )
        {
            FlashLED( GREEN );
        }
        else
        {
            FlashLED( RED );
        }        
    }
}


Далее: Решение №3: Сокращение используемого объёма ОЗУ

 

Hobby's category: