+7 (812) 670-9095
Обратная связьEnglish
Главная → Статьи → Системное ПО → Статья #25. Каналы передачи данных: введение и базовые службы
Версия для печати

Статья #25. Каналы передачи данных: введение и базовые службы

17 апреля 2019

После небольшого перерыва мы продолжаем публиковать переводы статей «Вся правда об ОСРВ» Колина Уоллса. В этот раз поговорим о каналах передачи данных (далее – каналы). Каналы, по сравнению с почтовыми ящиками или очередями, предоставляют более гибкий способ передачи простых сообщений между задачами.




Статья #25. Каналы передачи данных: введение и базовые службы


Использование каналов


В Nucleus SE каналы определяются на этапе сборки. В каждом приложении может быть до 16 каналов. Если в приложении не сконфигурирован ни один канал, ни структуры данных, ни код служебных вызовов, относящиеся к каналам, не будут включены в приложение.

Канал передачи данных – набор хранилищ, размер каждого из которых позволяет размещать один элемент данных заданной пользователем длины в байтах. Доступ к данным контролируется таким образом, чтобы ими могли безопасно пользоваться несколько задач. Задачи могут записывать данные в канал, пока не заполнятся все области. Задачи могут читать данные из канала, причем данные поступают по принципу FIFO. Попытка записи в переполненный канал или чтения из пустого канала может привести к ошибке или приостановке задачи, в зависимости от выбранных настроек вызовов API и конфигурации Nucleus SE.


Каналы и очереди


Nucleus SE также поддерживает очереди, которые были подробно рассмотрены в предыдущих статьях (#23 и #24). Основное различие между каналами и очередями заключается в размере сообщения. Очереди содержат сообщения, состоящие из одной переменной типа ADDR, обычно это указатели. Канал содержит сообщения произвольного размера, индивидуального для каждого канала в приложении и назначаемого во время настройки параметров.


Настройка каналов

Количество каналов


Как и для большинства объектов Nucleus SE, настройка каналов управляется директивами #define в nuse_config.h. Основным параметром является NUSE_PIPE_NUMBER, который определяет количество сконфигурированных в приложении каналов. По умолчанию это значение равно нулю (т.е. в приложении нет каналов) и может принимать значения вплоть до 16. Некорректное значение приведет к ошибке компиляции, которая будет сгенерирована проверкой в файле nuse_config_check.h (этот файл входит в nuse_config.c и компилируется вместе с ним), что приведет к срабатыванию директивы #error.

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


Активация вызовов API


Каждая функция API (служебный вызов) в Nucleus SE имеет активирующую директиву #define в nuse_config.h. Для каналов такими символами являются:


NUSE_PIPE_SEND
NUSE_PIPE_RECEIVE
NUSE_PIPE_JAM
NUSE_PIPE_RESET
NUSE_PIPE_INFORMATION
NUSE_PIPE_COUNT


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

Ниже приведен фрагмент кода из файла nuse_config.h по умолчанию.


#define NUSE_PIPE_NUMBER    0  /* Number of pipes in the
                                   system - 0-16 */
                                /* Service call enablers */
#define NUSE_PIPE_SEND         FALSE 
#define NUSE_PIPE_RECEIVE      FALSE
#define NUSE_PIPE_JAM          FALSE
#define NUSE_PIPE_RESET        FALSE
#define NUSE_PIPE_INFORMATION  FALSE
#define NUSE_PIPE_COUNT        FALSE

Если функции API активированы, но в приложении нет каналов (кроме NUSE_Pipe_Count(), который разрешен всегда), произойдет ошибка компиляции. Если ваш код использует вызов API, который не был активирован, произойдет ошибка компоновки, так как код реализации не был включен в приложение.


Служебные вызовы каналов

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


  • Отправка сообщения в канал. В Nucleus SE реализовано в функции NUSE_Pipe_Send().
  • Прием сообщения из канала. В Nucleus SE реализовано в функции NUSE_Pipe_Receive().
  • Отправка сообщения в начало канала. В Nucleus SE реализовано в NUSE_Pipe_Jam().
  • Восстановление канала в неиспользуемое состояние с освобождением всех приостановленных задач (сброс). В Nucleus SE реализовано в NUSE_Pipe_Reset().
  • Получение информации о конкретном канале. В Nucleus SE реализовано при помощи NUSE_Pipe_Information().
  • Возврат счетчика сконфигурированных на данный момент каналов в приложении. В Nucleus SE реализовано при помощи NUSE_Pipe_Count().
  • Добавление нового канала в приложение (создание). Не реализовано в Nucleus SE.
  • Удаление канала из приложения. Не реализовано в Nucleus SE.
  • Возврат указателей на все каналы в приложении. Не реализовано в Nucleus SE.
  • Отправка сообщения всем задачам, приостановленным на канале (broadcast). Не реализовано в Nucleus SE.

Рассмотрим реализацию каждого из этих служебных вызовов подробнее.

Служебные вызовы записи и чтения из каналов


Базовыми операциями, которые выполняются с каналами, являются запись (которую также называют отправкой) и чтение (также известна как прием сообщений). Кроме того, возможна запись данных в начало канала (jamming). Nucleus RTOS и Nucleus SE предоставляют три основных вызова API для этих операций, которые будут рассмотрены ниже.


Запись в канал


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

Nucleus RTOS также предоставляет службу для трансляции (broadcast) на канал, но в Nucleus SE она не поддерживается. Она будет описана в разделе «Нереализованные вызовы API» в следующей статье.


Вызов для отправки сообщений в канал в Nucleus RTOS

Прототип служебного вызова:

STATUS NU_Send_To_Pipe(NU_PIPE *pipe, VOID *message, UNSIGNED size, UNSIGNED suspend);

Параметры:


pipe – указатель на предоставленный пользователем блок управления каналом;
message – указатель на отправляемое сообщение;
size – количество байт в сообщении. Если канал поддерживает сообщения переменной длины, этот параметр должен быть равен или меньше длины сообщения, поддерживаемой каналом. Если канал поддерживает сообщения фиксированной длины, этот параметр должен быть равен размеру поддерживаемого каналом сообщения;
suspend – спецификация приостановки задачи, может принимать значения NU_NO_SUSPEND, NU_SUSPEND или значение таймаута.


Возвращаемое значение:

NUSE_SUCCESS – вызов был успешно завершен;
NU_INVALID_PIPE – некорректный указатель на канал;
NU_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NU_INVALID_SIZE – размер сообщения не совместим с размером сообщения, поддерживаемым каналом;
NU_INVALID_SUSPEND – попытка приостановки из не связанного с задачей потока;
NU_PIPE_FULL – канал переполнен, и тип приостановки задачи не был указан;
NU_TIMEOUT – канал переполнен даже после приостановки задачи на указанный период времени;
NU_PIPE_DELETED – канал был удален, пока задача была приостановлена;
NU_PIPE_RESET – канал был сброшен, пока задача была приостановлена.

Вызов для отправки сообщений в канал в Nucleus SE

Этот служебный вызов API поддерживает основной функционал Nucleus RTOS API.

Прототип служебного вызова:

STATUS NUSE_Pipe_Send(NUSE_PIPE pipe, U8 *message, U8 suspend);

Параметры:

pipe – индекс (ID) используемого канала;
message – указатель на отправляемое сообщение (последовательность байтов допустимой для данного канала длины);
suspend – спецификация приостановки задач, может принимать значения NUSE_NO_SUSPEND и NUSE_SUSPEND.

Возвращаемое значение:


NUSE_SUCCESS – вызов был успешно завершен;
NUSE_INVALID_PIPE – некорректный индекс канала;
NUSE_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NUSE_INVALID_SUSPEND – попытка приостановки из не связанного с задачей потока или при отключенной блокировке задач;
NUSE_PIPE_FULL – канал переполнен, и тип приостановки задач не был указан;
NUSE_PIPE_WAS_RESET – канал был сброшен, пока задача была приостановлена.


Реализация отправки сообщения в канал в Nucleus SE


Вариант кода функции API NUSE_Pipe_Send() (после проверки параметров) выбирается при помощи условной компиляции в зависимости от того, активирована ли поддержка вызовов API для блокировки (приостановки) задач или нет. Ниже мы рассмотрим оба варианта.

Если блокировка отключена, код этого вызова API довольно прост:


if (NUSE_Pipe_Items[pipe] == NUSE_Pipe_Size[pipe])  /* pipe
                                                       full */
{
   return_value = NUSE_PIPE_FULL;
}
else                              /* pipe element available */
{
   data = &NUSE_Pipe_Data[pipe][NUSE_Pipe_Head[pipe]];
   for (i=0; i<msgsize; i++)
   {
      *data++ = *message++;
   }
   NUSE_Pipe_Head[pipe] += msgsize;
   if (NUSE_Pipe_Head[pipe] == (NUSE_Pipe_Size[pipe] * msgsize))
   {
      NUSE_Pipe_Head[pipe] = 0;
   }
   NUSE_Pipe_Items[pipe]++;
   return_value = NUSE_SUCCESS;
}


Функция проверяет, есть ли свободное пространство в канале, и использует индекс NUSE_Pipe_Head[] для помещения сообщения в область данных канала.


Если блокировка задач активирована, код становится более сложным:

do
{
   if (NUSE_Pipe_Items[pipe] == NUSE_Pipe_Size[pipe]) /* pipe
                                                         full */
   {
      if (suspend == NUSE_NO_SUSPEND)
      {
         return_value = NUSE_PIPE_FULL;
      }
      else
      {                                           /* block task */
         NUSE_Pipe_Blocking_Count[pipe]++;
         NUSE_Suspend_Task(NUSE_Task_Active, (pipe << 4) |
                           NUSE_PIPE_SUSPEND);
         return_value =
            NUSE_Task_Blocking_Return[NUSE_Task_Active];
         if (return_value != NUSE_SUCCESS)
         {
            suspend = NUSE_NO_SUSPEND;
         }
      }
   }
   else                             /* pipe element available */
   {  
      data = &NUSE_Pipe_Data[pipe][NUSE_Pipe_Head[pipe]];
      for (i=0; i<msgsize; i++)
      {
         *data++ = *message++;
      }
      NUSE_Pipe_Head[pipe] += msgsize;
      if (NUSE_Pipe_Head[pipe] == (NUSE_Pipe_Size[pipe] *
                                   msgsize))
      {
         NUSE_Pipe_Head[pipe] = 0;
      }
      NUSE_Pipe_Items[pipe]++;
      if (NUSE_Pipe_Blocking_Count[pipe] != 0)
      {
         U8 index;         /* check whether a task is blocked
                              on this pipe */
         NUSE_Pipe_Blocking_Count[pipe]--;
         for (index=0; index<NUSE_TASK_NUMBER; index++)
         {
            if ((LONIB(NUSE_Task_Status[index]) ==
                  NUSE_PIPE_SUSPEND)
                  && (HINIB(NUSE_Task_Status[index]) == pipe))
            {
               NUSE_Task_Blocking_Return[index] = NUSE_SUCCESS;
               NUSE_Wake_Task(index);
               break;
            }
         }
      }
      return_value = NUSE_SUCCESS;
      suspend = NUSE_NO_SUSPEND;
   }
} while (suspend == NUSE_SUSPEND);


Некоторые пояснения могут быть полезными.

Код заключен в цикл do…while, который выполняется, пока параметр приостановки задач имеет значение NUSE_SUSPEND.

Если канал заполнен и параметр suspend имеет значение NUSE_NO_SUSPEND, вызов API завершается со значением NUSE_PIPE_FULL. Если параметр suspend имеет значение NUSE_SUSPEND, задача приостанавливается. При завершении (то есть когда задача возобновляется), если возвращаемое значение равно NUSE_SUCCESS, то есть задача была возобновлена, потому что сообщение было прочитано (а не потому что канал был сброшен), код возвращается в начало цикла.

Если канал не заполнен, предоставляемое сообщение сохраняется, используя индекс NUSE_Pipe_Head[], в области данных канала. Выполняется проверка, есть ли в канале приостановленные задачи (ожидающие сообщений). Если такие задачи есть, первая из них возобновляется. Переменной suspend присваивается значение NUSE_NO_SUSPEND, а вызов API завершается со значением NUSE_SUCCESS.


Чтение из канала


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


Вызов для чтения из канала в Nucleus RTOS

Прототип служебного вызова:

STATUS NU_Receive_From_Pipe(NU_PIPE *pipe, VOID *message, UNSIGNED size, UNSIGNED *actual_size, UNSIGNED suspend);


Параметры:

pipe – указатель на предоставленный пользователем блок управления каналом;
message – указатель на хранилище для принимаемого сообщения;
size – количество байт в сообщении. Должно совпадать с размером сообщения, указанным при создании канала;
suspend – спецификация приостановки задач, может принимать значения NU_NO_SUSPEND, NU_SUSPEND или значение таймаута.

Возвращаемое значение:

NU_SUCCESS – вызов был успешно завершен;
NU_INVALID_PIPE – некорректный указатель на канал;
NU_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NU_INVALID_SUSPEND – попытка приостановить задачу из не связанного с задачей потока;
NU_PIPE_EMPTY – канал пуст, и тип приостановки задач не был указан;
NU_TIMEOUT – канал пуст даже после приостановки задачи на указанное значение таймаута;
NU_PIPE_DELETED – канал был удален, пока задача была приостановлена;
NU_PIPE_RESET – канал был удален, пока задача была приостановлена.

Вызов для чтения из канала в Nucleus SE

Этот служебный вызов API поддерживает основной функционал Nucleus RTOS API.

Прототип служебного вызова:

STATUS NUSE_Pipe_Receive(NUSE_PIPE pipe, U8 *message, U8 suspend);

Параметры:

pipe – индекс (ID) используемого канала;
message – указатель на хранилище для принимаемого сообщение (последовательность байтов, длина которой совпадает с размером сообщения канала);
suspend – спецификация приостановки задач, может принимать значения NUSE_NO_SUSPEND и NU_SUSPEND.

Возвращаемое значение:


NUSE_SUCCESS – вызов был успешно завершен;
NUSE_INVALID_PIPE – некорректный индекс канала;
NUSE_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NUSE_INVALID_SUSPEND – попытка приостановки задачи из не связанного с задачей потока или при отключенной поддержке приостановки задач;
NUSE_PIPE_EMPTY – канал пуст, и тип приостановки задач не был указан;
NUSE_PIPE_WAS_RESET – канал был сброшен, пока задача была приостановлена.


Реализация чтения каналов в Nucleus SE


Вариант кода функции API NUSE_Pipe_Receive() (после проверки параметров) выбирается условной компиляцией в зависимости от того, активирована ли поддержка API вызовов блокировки (приостановки задач) или нет. Мы рассмотрим оба варианта ниже.

Если блокировка отключена, код этого вызова API довольно прост:


if (NUSE_Pipe_Items[pipe] == 0)            /* pipe empty */
{
   return_value = NUSE_PIPE_EMPTY;
}
else
{                                          /* message available */
   data = &NUSE_Pipe_Data[pipe][NUSE_Pipe_Tail[pipe]];
   for (i=0; i<msgsize; i++)
   {
      *message++ = *data++;
   }
   NUSE_Pipe_Tail[pipe] += msgsize;
   if (NUSE_Pipe_Tail[pipe] == (NUSE_Pipe_Size[pipe] * msgsize))
   {
      NUSE_Pipe_Tail[pipe] = 0;
   }
   NUSE_Pipe_Items[pipe]--;
   *actual_size = msgsize;
   return_value = NUSE_SUCCESS;
}


Функция проверяет наличие сообщения в канале и использует индекс NUSE_Pipe_Tail[], чтобы получить сообщение из области данных канала, и возвращает данные через указатель сообщения.

Если блокировка задач активирована, код становится более сложным:


do
{
   if (NUSE_Pipe_Items[pipe] == 0)             /* pipe empty */
   {
      if (suspend == NUSE_NO_SUSPEND)
      {
         return_value = NUSE_PIPE_EMPTY;
      }
      else
      {                                        /* block task */
         NUSE_Pipe_Blocking_Count[pipe]++;
         NUSE_Suspend_Task(NUSE_Task_Active, (pipe << 4) |
                           NUSE_PIPE_SUSPEND);
         return_value =
               NUSE_Task_Blocking_Return[NUSE_Task_Active];
         if (return_value != NUSE_SUCCESS)
         {
            suspend = NUSE_NO_SUSPEND;
         }
      }
   }
   else
   {                                     /* message available */
      data = &NUSE_Pipe_Data[pipe][NUSE_Pipe_Tail[pipe]];
      for (i=0; i<msgsize; i++)
      {
         *message++ = *data++;
      }
      NUSE_Pipe_Tail[pipe] += msgsize;
      if (NUSE_Pipe_Tail[pipe] == (NUSE_Pipe_Size[pipe] *
                                    msgsize))
      {
         NUSE_Pipe_Tail[pipe] = 0;
      }
      NUSE_Pipe_Items[pipe]--;
      if (NUSE_Pipe_Blocking_Count[pipe] != 0)
      {
         U8 index;         /* check whether a task is blocked */
                           /* on this pipe */
         NUSE_Pipe_Blocking_Count[pipe]--;
         for (index=0; index<NUSE_TASK_NUMBER; index++)
         {
            if ((LONIB(NUSE_Task_Status[index]) ==
                  NUSE_PIPE_SUSPEND)
                  && (HINIB(NUSE_Task_Status[index]) == pipe))
            {
               NUSE_Task_Blocking_Return[index] = NUSE_SUCCESS;
               NUSE_Wake_Task(index);
               break;
            }
         }
      }
      *actual_size = msgsize;
      return_value = NUSE_SUCCESS;
      suspend = NUSE_NO_SUSPEND;
   }
} while (suspend == NUSE_SUSPEND);


Некоторые пояснения могут быть полезны.

Код заключен в цикл do…while, который выполняется, пока параметр приостановки задач имеет значение NUSE_SUSPEND.

Если канал пуст и параметр suspend имеет значение NUSE_NO_SUSPEND, вызов API завершается со значением NUSE_PIPE_EMPTY. Если параметр suspend имеет значение NUSE_SUSPEND, задача приостанавливается. При завершении (то есть когда задача возобновляется), если возвращаемое значение равно NUSE_SUCCESS, то есть задача была возобновлена, потому что сообщение было отправлено (а не потому что канал был сброшен), код возвращается в начало цикла.

Если канал содержит в себе сообщения, хранимое сообщение возвращается, используя индекс NUSE_Pipe_Tail[]. Выполняется проверка, есть ли приостановленные (ожидающие отправки) задачи на этом канале. Если такие задачи есть, первая из них возобновляется. Переменной suspend присваивается значение NUSE_NO_SUSPEND, и вызов API завершается с кодом NUSE_SUCCESS.


Запись в начало канала


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


Вызов для записи в начало канала в Nucleus RTOS

Прототип служебного вызова:

STATUS NU_Send_To_Front_Of_Pipe(NU_PIPE *pipe, VOID *message, UNSIGNED size, UNSIGINED suspend);

Параметры:


pipe – указатель на предоставленный пользователем блок управления каналом;
message – указатель на отправляемое сообщение;
size – количество байт в сообщении. Если канал поддерживает сообщения переменной длины, этот параметр должен быть равен или меньше размера сообщения, поддерживаемого каналом. Если канал поддерживает сообщения фиксированной длины, этот параметр должен совпадать с размером сообщения, поддерживаемым каналом;
suspend – спецификация приостановки задач, может принимать значения NU_NO_SUSPEND, NU_SUSPEND или значение таймаута.


Возвращаемое значение:

NU_SUCCESS – вызов был успешно завершен;
NU_INVALID_PIPE – некорректный указатель на канал;
NU_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NU_INVALID_SIZE – размер сообщения несовместим с размером сообщения, поддерживаемым каналом;
NU_INVALID_SUSPEND – попытка приостановки из несвязанного с задачей потока;
NU_PIPE_FULL – канал полон, и тип приостановки задач не был указан;
NU_TIMEOUT – канал полон даже после приостановки задачи на указанный период времени;
NU_PIPE_DELETED – канал был удален, пока задача была приостановлена;
NU_PIPE_RESET – канал был сброшен, пока задача была приостановлена.

Вызов для записи в начало канала в Nucleus SE

Этот служебный вызов поддерживает основной функционал Nucleus RTOS API.

Прототип служебного вызова:

STATUS NUSE_Pipe_Jam(NUSE_PIPE pipe, ADDR *message, U8 suspend);

Параметры:


pipe – индекс (ID) используемого канала;
message – указатель на отправляемое сообщение, которое является последовательностью байтов, равную сконфигурированному размеру сообщения в канале;
suspend – спецификация приостановки задач, может принимать значения NUSE_NO_SUSPEND или NUSE_SUSPEND.


Возвращаемое значение:

NUSE_SUCCESS – вызов был успешно завершен;
NUSE_INVALID_PIPE – некорректный индекс канала;
NUSE_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NUSE_INVALID_SUSPEND – попытка приостановки задачи из не связанного с задачей потока или при отключенной блокировке задач;
NUSE_PIPE_FULL – канал полон, и тип приостановки задач не был указан;
NUSE_PIPE_WAS_RESET – канал был сброшен, пока задача была приостановлена.

Реализация записи в начало канала в Nucleus SE


Вариант кода функции NUSE_Pipe_Jam() очень похож на NUSE_Pipe_Send(), кроме того, что для хранения в нем данных используется индекс NUSE_Pipe_Tail[], следовательно:


if (NUSE_Pipe_Items[pipe] == NUSE_Pipe_Size[pipe])  /* pipe
                                                       full */
{
   return_value = NUSE_PIPE_FULL;
}
else                                 /* pipe element available */
{
   if (NUSE_Pipe_Tail[pipe] == 0)
   {
      NUSE_Pipe_Tail[pipe] = (NUSE_Pipe_Size[pipe] - 1) * msgsize;
   }
   else
   {
      NUSE_Pipe_Tail[pipe] -= msgsize;
   }
   data = &NUSE_Pipe_Data[pipe][NUSE_Pipe_Tail[pipe]];
   for (i=0; i<msgsize; i++)
   {
      *data++ = *message++;
   }
   NUSE_Pipe_Items[pipe]++;
   return_value = NUSE_SUCCESS;
}


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


Об авторе: Колин Уоллс уже более тридцати лет работает в сфере электронной промышленности, значительную часть времени уделяя встроенному ПО. Сейчас он – инженер в области встроенного ПО в Mentor Embedded ( подразделение Mentor Graphics). Колин Уоллс часто выступает на конференциях и семинарах, автор многочисленных технических статей и двух книг по встроенному ПО. Живет в Великобритании. Профессиональный блог Колина: https://blogs.mentor.com/colinwalls/, e-mail: colin_walls@mentor.com


Источник: https://www.embedded.com/design/operating-systems/4461205/Pipes--introduction-and-basic-services


Теги: ОСРВ, RTOS, каналы, взаимодействие задач, микроконтроллеры