Перевод может содержать ошибки. Читайте первоисточник: Solution #1 - Why Use an RTOS Kernel?
Назад: [Как работает FreeRTOS] | Вверх: [Как работает FreeRTOS] | Вперёд: [Как работает FreeRTOS] |
Также смотрите пункт ЧаВо: "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;
}
... но это не является приемлемым решением:
Можно выделить два фактора, ограничивающие применение простой структуры главного цикла, описанной к этому моменту.
Разрешение каждой функции выполняться полностью занимает слишком много времени. Этого можно избежать, разделив каждую функцию на несколько состояний. За один вызов обрабатывается только одно состояние. Используем функцию управления в качестве примера:
/*----------------------------------------------------------------------------*/
/* Определяем состояния для функции цикла управления. */
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;
}
}
Теперь функция стала структурно более сложной, и создаёт дополнительные проблемы с планированием. Сам код становится труднее в понимании, когда добавляются дополнительные состояния, например для обработки времени ожидания и обработки ошибок.
Более короткий интервал таймера даст больше гибкости.
Реализация функции управления как конечного автомата (при этом каждый вызов сокращается) может сделать возможной вызов из обработчика прерывания таймера. Интервал таймера должен быть достаточно коротким, чтобы функция вызывалась с частотой соответствующей требованиям хронометража. Эта опция чревата проблемами с хронометражом и обслуживанием.
В качестве альтернативы решение с бесконечным циклом может быть модифицировано таким образом, чтобы на каждом заходе вызывались различные функции, и так, чтобы высокоприоритетная функция управления вызывалась более часто:
/*----------------------------------------------------------------------------*/
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) на частоте, с которой выполняется цикл управления.