You are here

Решение №1: Зачем использовать ядро ОСРВ?

Перевод может содержать ошибки. Читайте первоисточник: Solution #1 - Why Use an RTOS Kernel?

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

 

Решение №1: Зачем использовать ядро ОСРВ?

Также смотрите пункт ЧаВо: "Why use an RTOS?".

 

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

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

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

 

Реализация

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

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

 

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

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

 

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

Планировщик ОСРВ не используется.

 

Оценка

Маленький размер кода.
Нет зависимостей от исходного кода сторонних компаний.
Нет накладных расходов ОЗУ и ПЗУ на работу ОСРВ.
Сложно реализовать сложные временные последовательности.
Масштабирование сопряжено со значительным увеличением сложности.
Хронометраж трудно оценить и поддерживать из-за взаимозависимости между различными функциями.

 

Заключение

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

 

Пример

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

 

Функция управления агрегатом

Функция управления может быть представлена следующим псевдокодом:

/*----------------------------------------------------------------------------*/
void PlantControlCycle( void )
{
    TransmitRequest();
    WaitForFirstSensorResponse();

    if( Получены данные от первого датчика )
    { 
        WaitForSecondSensorResponse();
        
        if( Получены данные от второго датчика )
        {
            PerformControlAlgorithm();
            TransmitResults();
        }
    }
}

 

Функции интерфейса пользователя

Это функции обслуживания клавиатуры, ЖК-дисплея, интерфейса RS232 и встроенного веб-сервера.

Следующий псевдокод представляет простую структуру с бесконечным циклом для управления этими интерфейсами.

/*----------------------------------------------------------------------------*/
int main( void )
{
    Initialise();
    
    for( ;; )
    {
        ScanKeypad();
        UpdateLCD();
        ProcessRS232Characters();
        ProcessHTTPRequests();   
    }

    // Здесь никогда не должны оказаться.
    return 0;
}

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

 

Планирование функции управления агрегатом

Длина функции управления означает, что её нельзя просто вызывать внутри обработчика прерывания таймера, настроенного на период 10 мс.

Добавление её в бесконечный цикл потребовало бы введения некоторого временнОго контроля. Например ... :

/*----------------------------------------------------------------------------*/
/* Флаг, используемый для обозначения времени, в которое должен начаться */
/* цикл управления (проблема взаимного исключения */
/* игнорируется в этом примере). */
int TimerExpired;

/*----------------------------------------------------------------------------*/
/* Обработчик прерывания таймера. Конфигурируется для вызова каждые 10 мс. */
void TimerInterrupt( void )
{    
    TimerExpired = true;
}

/*----------------------------------------------------------------------------*/
/* Функция main() содержит бесконечный цикл, в который был добавлен */
/* вызов функции управления агрегатом. */
int main( void )
{
    Initialise();
    
    for( ;; )
    {
        /* Крутимся, пока не настанет время для следующего цикла управления. */
        if( TimerExpired )
        {
            PlantControlCycle();
            TimerExpired = false;

            ScanKeypad();
            UpdateLCD();

            /* Светодиоды могут использовать счётчик количества прерываний */
            /* таймера, либо другой таймер. */
            ProcessLEDs();

            /* Буферы должны быть достаточно большими, чтобы вместить данные, */
            /* передаваемые в течение 10 мс. */
            ProcessRS232Characters();
            ProcessHTTPRequests();   
        }

        /* Процессор теперь можно перевести в спящий режим, если он */
        /* будет разбужен любым прерыванием. */
    }

    /* Здесь никогда не должны оказаться. */
    return 0;
}

... но это не является приемлемым решением:

  • Задержка или неисправнось в шине управления приводит к увеличению времени выполнения функции управления агрегатом. Требования к хронометражу функций интерфейса пользователя скорее всего будут нарушены.
  • Выполнение всех функций в каждом цикле может приводить к нарушению хронометража цикла управления.
  • Джиттер (дрожание) во время выполнения может привести к пропуску циклов. Например, время выполнения ProcessHTTPRequests() может быть незначительным, когда нет HTTP-запросов, но становится довольно продолжительным, когда обслуживается веб-страница.
  • Оно не очень удобно в обслуживании, т.к. зависит от каждой функции, выполняемой в течение максимального времени.
  • Буферы связи обслуживаются только один раз за проход цикла, поэтому их длина должна быть больше, чем могла бы быть в иных случаях.

 

Альтернативные структуры

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

  1. Продолжительность каждого вызова функции

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

    /*----------------------------------------------------------------------------*/
    /* Определяем состояния для функции цикла управления. */
    typdef enum eCONTROL_STATES
    {
        eStart, /* Старт нового цикла. */
        eWait1, /* Ожидание ответа от первого датчика. */
        eWait2  /* Ожидание ответа от второго датчика. */
    } eControlStates;
    
    void PlantControlCycle( void )
    {
    static eControlState eState = eStart;
    
        switch( eState )
        {
            case eStart :
                TransmitRequest();
                eState = eWait1;
                break;
                
            case eWait1;
                if( Получили данные от первого датчика )
                {
                    eState = eWait2;
                }
                /* Как обрабатывать таймауты? */
                break;
                
            case eWait2;
                if( Получили данные от второго датчика )
                {
                    PerformControlAlgorithm();
                    TransmitResults();
                    
                    eState = eStart;
                }
                /* Как обрабатывать таймауты? */
                break;           
        }
    }
    
    

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

  2. Разрешение таймера (The granularity of the timer).

    Более короткий интервал таймера даст больше гибкости.

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

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

    /*----------------------------------------------------------------------------*/
    int main( void )
    {
    int Counter = -1;
    
        Initialise();
        
        /* Каждая функция реализована как конечный автомат, что гарантирует
        /* быстрое выполнение, но при этом вызывать их нужно чаще. */
        
        /* Обратите внимание, что частота таймера была увеличена. */
        
        for( ;; )
        {
            if( TimerExpired )
            {
                Counter++;
                
                switch( Counter )
                {
                    case 0  : ControlCycle();
                              ScanKeypad();
                              break;
                              
                    case 1  : UpdateLCD();
                              break;
    
                    case 2  : ControlCycle();
                              ProcessRS232Characters();
                              break;
    
                    case 3  : ProcessHTTPRequests();
                              
                              // Go back to start
                              Counter = -1;                          
                              break;
                              
                }
                
                TimerExpired = false;
            }
        }
    
        // Здесь никогда не должны оказаться. */
        return 0;
    }
    
    

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

    /*----------------------------------------------------------------------------*/
        for( ;; )
        {
            if( TimerExpired )
            {
                Counter++;
                
                /* Функция управления вызывается каждый второй цикл. */
                switch( Counter )
                {
                    case 0  : ControlCycle();
                              break;
                              
                    case 1  : Counter = -1;
                              break;
                }
    
                /* Выполнение только одной из остальных функций. */
                /* Причём обработка запускается только по необходимости. */
                /* Функция EventStatus() проверяет события с последней итерации. */
                switch( EventStatus() )
                {
                    case EVENT_KEY  :   ScanKeypad();
                                        UpdateLCD();
                                        break;
                               
                    case EVENT_232  :   ProcessRS232Characters();
                                        break;
                                
                    case EVENT_TCP  :   ProcessHTTPRequests();
                                        break;
                }
                
                TimerExpired = false;
            }
        }
    
    

    Обработка событий таким способом уменьшает количество циклов процессора, потраченных впустую, но проект в целом всё равно будет демонстрировать дрожание (jitter) на частоте, с которой выполняется цикл управления.

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

Hobby's category: