Малые замечания.

Версия 1.1.0

© Copyright 2006-2008
Грибов Игорь,
e-mail: gribov@depni.sinp.msu.ru

Оглавление.

Изменения в версиях.
Характеристика реального времени.
Счетчики.
Кольцевой буфер (FIFO).
Полное чтение данных.
Против микро-оптимизации.

Изменения в версиях.

Полная версия раздела задается в виде: версия.подверсия.выпуск.

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

Версия 1.1.0. Добавлен пример в главе счетчики.


Характеристика реального времени.

Динамические свойства программ реального времени принято характеризовать тремя определениями: программы жесткого, мягкого и интерактивного реального времени.

Жесткое реальное время. Предусматривает наличие гарантированного времени отклика системы на конкретное событие, например, аппаратное прерывание, выдачу команды управления и т.п. Абсолютная величина времени отклика большого значения не имеет. Так, если необходимо, чтобы программа отработала некоторую команду за 1 миллисекунду, но она справляется с этим заданием лишь в 95% случаев, а в 5% не укладывается в норматив, такую систему нельзя охарактеризовать как работающую в жестком реальном времени. Если же команду нужно отработать в течение часа, что и происходит в 100% случаев – налицо жесткое реальное время.

Мягкое реальное время. В этом случае ожидающееся время отклика системы является величиной скорее индикативной, нежели директивной. Конечно, предполагается что в большинстве случаев (процентов 80 - 90) отклик уложится в заданные пределы. Однако и остальные варианты – в том числе полное отсутствие реакции системы – не должны приводить к плачевным результатам. Обычно считается, что если временной норматив превышен на один порядок, то это еще терпимо .

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


Счетчики.

При использовании счетчиков всегда проверяйте граничные условия (достижение предела счета).

typedef unsigned short  unsigned16;

#define TIMEXPIRED   21

unsigned16 counter;

void timer_function(void)
{
   if (counter > 0) counter--;
}

void monitor(void)
{
   counter = TIMEXPIRED;
   while (TRUE) {   
      if (counter == 0) {
         counter = TIMEXPIRED;
         time_expired();
      }
      do_something_1();
      do_something_2();
//    ...
      do_something_n();
   }
}

В этом примере некоторая функция time_expired() должна вызываться периодически по истечении TIMEXPIRED тактов таймера. Причем вызов не может производиться непосредственно из функции обработки таймерного сигнала timer_function(), а осуществляется в мониторе monitor(). Если в обработчике таймера счет не будет останавливаться по достижении нулевого предела, возможно неконтролируемое переполнение счетчика counter. Ведь общая программная нагрузка на систему вполне может оказаться такой, что в мониторе не произойдет захвата нулевого значения и переустанова счетчика. Тогда придется долго (65535 тактов для unsigned16 счетчика) ждать очередного вызова time_expired().

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

typedef int             int32;

int32 counter;

void counter_function(void)    // сf_0
{
   if (counter > 0) {          // сf_2
      counter--;               // сf_3
      if (counter == 0) {      // сf_4
         do_something();       // сf_5
      }
   }
}

void stop_counter(void)
{
   counter = -1;
}

Периодически вызываемая функция counter_function() обслуживает декрементный счетчик counter, по истечении которого должна быть выполнена некоторая подпрограмма [cf_5]. А функция stop_counter(), которая останавливает счетчик, может вызываться асинхронно относительно counter_function(), например из аппаратного прерывания. Если в качестве counter использовать беззнаковую переменную, останов счета возможен лишь при задании нулевого значения счетчика. Это может привести к нежелательным эффектам. Если функция останова счета будет вызвана после выполнения сравнения [cf_2], но до декремента счетчика [cf_3], значение переменной counter станет максимальным положительным числом соответствующего беззнакового формата. Таким образом, счетчик не будет отключен и по истечении соответствующего числа вызовов counter_function() однократно активируется функция do_something(). Когда же останов беззнакового счетчика осуществляется после выполнения декремента [cf_3], условие [cf_4] станет истинным, даже если значение счетчика было заметно отличным от нуля. Такое поведение является не вполне ожидаемым, поскольку после останова счетчика подпрограмма do_something() - если она еще не выполняется - запускаться не должна. Переменная со знаком позволяет использовать отрицательную область значений для управления счетчиком, что дает возможность избежать отмеченных неприятностей.


Кольцевой буфер (FIFO).

Рассмотрим базовый алгоритм организации кольцевого буфера.

#define FIFO_SIZE   111   // Размер кольцевого буфера (FIFO)
#define ERROR       -1    // Код ошибки
#define OK          1     // Код нормального завершения

typedef char            int8;
typedef unsigned char   unsigned8;
typedef short           int16;
typedef unsigned short  unsigned16;
typedef int             int32;
typedef unsigned int    unsigned32;

typedef struct {
   unsigned8 data_1[16];
   int16 data_2;
   unsigned32 data_3;
} fifodata;                  // Структура данных FIFO

fifodata fdata[FIFO_SIZE];   // FIFO: массив структур данных
unsigned16 tail=0, head=0;   // Голова и хвост кольцевого буфера: определение и инициализация 

int16 read_fifo(fifodata *fd)        // rf_0
{
   unsigned16 t;                     // rf_2

   if (tail == head) return ERROR;   // rf_4
   t = tail+1;                       // rf_5
   if (t == FIFO_SIZE) t = 0;        // rf_6
   *fd = fdata[t];                   // rf_7
   tail = t;                         // rf_8
   return OK;
}

Функция чтения кольцевого буфера read_fifo(fd). Проверяет наличие записей в FIFO и возвращает код ошибки ERROR для пустого буфера [rf_4]. Если в буфере имеются не прочитанные записи, самая старая из них извлекается их хвоста FIFO, заносится в параметр fd и функция завершается с кодом OK. При этом манипуляции c хвостом буфера - закольцовывание [rf_6] и вывод очередного значения [rf_7] - осуществляются в локальной переменной [rf_2]. Именно такая локализация обеспечивает взаимную независимость функций чтения и записи кольцевого буфера. Новое значение адреса хвоста tail устанавливается [rf_8] лишь после фактического освобождения соответствующего элемента FIFO [rf_7]. Собственно функция чтения буфера не является безусловно сигналобезопасной, поскольку содержит разделяемый ресурс tail. Поэтому допустимы лишь транзакционные вызовы read_fifo(fd) (см. Критические ресурсы).

int16 write_fifo(fifodata *fd)   // wf_0
{
   unsigned16 h;                 // wf_2

   h = head+1;                   // wf_4
   if (h == FIFO_SIZE) h = 0;    // wf_5
   if (h == tail) return ERROR;  // wf_6
   fdata[h] = *fd;               // wf_7
   head = h;                     // wf_8
   return OK;
}

Функция записи кольцевого буфера write_fifo(fd). Проверяет наличие свободного места в FIFO и возвращает код ошибки ERROR когда буфер полон [wf_6]. Если в буфере есть место, в его голову заносится параметр fd и функция завершается с кодом OK. При этом манипуляции c головой буфера - закольцовывание [wf_5] и запись очередного значения [wf_7] – осуществляются в локальной переменной [wf_2]. Именно такая локализация обеспечивает взаимную независимость функций записи и чтения кольцевого буфера. Новое значение адреса головы head устанавливается [wf_8] лишь после фактического занесения данных в буфер [wf_7]. Собственно функция записи в буфер не является безусловно сигналобезопасной, поскольку содержит разделяемый ресурс head. Поэтому допустимы лишь транзакционные вызовы write_fifo(fd) (см. Критические ресурсы). Например, если готовность некоторых данных сигнализируется аппаратным прерыванием, следует запрещать повторные прерывания от того же источника на время занесения данных в кольцевой буфер.

unsigned16 nof_fifobusy(void)   // nf_0
{
   int32 ht;

   ht = head - tail;             // nf_4
   if (ht < 0) ht += FIFO_SIZE;  // nf_5
   return ht;
}

Функция nof_fifobusy() подсчитывает число занятых элементов в кольцевом буфере. Для обеспечения сигналобезопасности относительно записи и чтения FIFO разность головы и хвоста буфера присваивается локальной переменной со знаком [nf_4]. Если же использовать в условном операторе [nf_5] переменные head и tail непосредственно, можно получить совершенно неверный результат, когда запись или чтение буфера происходят после проверки условия в [nf_5] и совпадают с закольцовыванием FIFO.

При выборе размера буфера FIFO_SIZE следует учитывать, что максимальное число хранимых данных равно FIFO_SIZE-1. Отметим также, что мы сознательно не используем чуть более эффективный алгоритм закольцовывания FIFO: h=(head+1)&FIFO_ZIZE. Он предполагает, что размер буфера задается битовой маской: 0x1F, 0x7F и подобными значениями. Однако, если при внесении изменений в программу забыть об этом факте, FIFO потеряет работоспособность. См.также против микро-оптимизации.


Полное чтение данных.

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

#define FIFO_SIZE   10    // Размер кольцевого буфера (FIFO)
#define ERROR       -1    // Код ошибки
#define OK          1     // Код нормального завершения

typedef char            int8;
typedef unsigned char   unsigned8;
typedef short           int16;
typedef unsigned short  unsigned16;
typedef int             int32;
typedef unsigned int    unsigned32;
typedef struct {
   unsigned8 data_1[8];
   int16 data_2;
   unsigned32 data_3;
} appldata;           // Структура данных приложения

appldata apdata[FIFO_SIZE];    // FIFO: массив структур данных
unsigned16 tail, head;         // Голова и хвост кольцевого буфера
int16 sem_sigio, flag_sigio;   // Семафор доступа к приложению и флаг новых данных

int16 read_fifo(appldata *ap)
{
   unsigned16 t;

   if (tail == head) return ERROR;
   t = tail+1;
   if (t == FIFO_SIZE) t = 0;
   *ap = apdata[t];
   tail = t;
   return OK;
}

int16 write_fifo(appldata *ap)
{
   unsigned16 h;

   h = head+1;
   if (h == FIFO_SIZE) h = 0;
   if (h == tail) return ERROR;
   apdata[h] = *ap;
   head = h;
   return OK;
}

Функции чтения read_fifo(ap) и записи write_fifo(ap) кольцевого буфера полностью аналогичны таковым из раздела "Кольцевой буфер (FIFO)".

void process_application(appldata *ap)
{
}

Функция приложения process_application(ap), где производится собственно обработка данных, приведена в виде заглушки.

void read_handler(void)   // rh_0
{
   appldata ap;

   do {                                   // rh_4
      flag_sigio = 0;                     // rh_5
      sem_sigio++;                        // rh_6
      if (sem_sigio == 0) {               // rh_7
         while (read_fifo(&ap) == OK) {   // rh_8
            process_application(&ap);     // rh_9
         }
      }    
      sem_sigio--;                        // rh_12
   } while (flag_sigio != 0);             // rh_13
   flag_sigio = 1;                        // rh_14
}

Повторно-входимая функция read_handler() обеспечивает сигналобезопасное извлечение данных из кольцевого буфера [rh_8] с последующей их отправкой на обработку в приложение [rh_9]. Сигналобезопасность поддерживается семафором sem_sigio: закрытие [rh_6], проверка занятости [rh_7] и открытие [rh_12]. Сам по себе цикл извлечения данных из FIFO [rh_8] не может гарантировать полное считывание всех поступивших в буфер посылок. Так, если запись новых данных в FIFO произойдет после вызова функции read_fifo(ap) с результатом ERROR (нет данных в буфере), но до открытия семафора [rh_12], последние данные не будут отправлены приложению. Возможность их обработки предоставится лишь после приема хотя бы одной новой посылки. Для извлечения всех поступивших в кольцевой буфер данных и служит внешний цикл на основе флага новых данных flag_sigio. Этот флаг устанавливается [rh_14] после любого обращения к функции read_handler(), обеспечивая внешнее зацикливание [rh_4] критической секции извлечения данных из FIFO (от [rh_6] до [rh_12]). Таким образом, в случае прихода новых данных до открытия семафора [rh_12] сработает цикл [rh_13], обеспечивая дочитывание данных из буфера. Если же запись данных произойдет после открытия семафора [rh_12], но до проверки условия [rh_13], эти данные будут немедленно отправлены приложению, а затем выполнен один “холостой” внешний цикл [rh_4].

void enable_interrupt(void)
{
}

void disable_interrupt(void)
{
}

Функции разрешения прерывания или сигнала enable_interrupt() и его запрета disable_interrupt() приведены в виде заглушек.

void interrupt_handler(void)   // ih_0
{
   appldata ap;

   disable_interrupt();   // ih_4
   write_fifo(&ap);       // ih_5
   enable_interrupt();    // ih_6
   read_handler();        // ih_7
}

Обработчик прерывания или сигнала. Осуществляет извлечение данных из регистров и иных источников (как правило аппаратных) и записывает эти данные в кольцевой буфер [ih_5]. На время записи повторные прерывания запрещаются [ih_4] и разрешаются сразу после занесения данных в FIFO [ih_6]. Затем осуществляется вызов сигналобезопасной функции чтения данных из буфера с последующей их обработкой в приложении [ih_7]. Отметим, что во время обработки данных в функции read_handler() прерывания или сигналы могут продолжать поступать. Новые данные при этом будут заноситься в FIFO. Таким образом, повторно входимый вызов interrupt_handler() является штатной ситуацией.

void init_io(void)
{
   tail = 0;
   head = 0;
   sem_sigio = -1;
}

Функция инициализации задает начальные значения головы и хвоста кольцевого буфера и открывает семафор выполнения приложения. Флаг новых данных flag_sigio инициализации не требует, поскольку всегда сбрасывается до его использования [rh _5].


Против микро-оптимизации.

Перепишем функцию занятия ресурса в слегка “оптимизированном” виде, заменив семафорную инкрементную операцию на пре-инкрементный оператор, внедренный в проверку условия занятости ресурса (см. Семафоры):

void semaphore_example(void)
{
   if (++sem == 0) {
      ...
   } else {
      ...
   }
   sem--;
}

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

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

Ясность, прозрачность, надежность исходного кода – не в ущерб его продвинутости, хотя, возможно, с незначительной потерей эффективности – являются ключевыми чертами по-настоящему профессионально разработанных программ. А для “скрытого” программного обеспечения, эти черты становятся также и настоятельной необходимостью.