После небольшого перерыва мы продолжаем публиковать переводы статей «Вся правда об ОСРВ» Колина Уоллса. В этот раз поговорим о каналах передачи данных (далее – каналы). Каналы, по сравнению с почтовыми ящиками или очередями, предоставляют более гибкий способ передачи простых сообщений между задачами.
В 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 (служебный вызов) в Nucleus SE имеет активирующую директиву #define в nuse_config.h. Для каналов такими символами являются:
По умолчанию им присваивается значения 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 поддерживает десять служебных вызовов, связанных с каналами, которые предоставляют следующий функционал:
Базовыми операциями, которые выполняются с каналами, являются запись (которую также называют отправкой) и чтение (также известна как прием сообщений). Кроме того, возможна запись данных в начало канала (jamming). Nucleus RTOS и Nucleus SE предоставляют три основных вызова API для этих операций, которые будут рассмотрены ниже.
Служебный вызов Nucleus RTOS API для записи в канал очень гибкий, что позволяет приостанавливать задачи неявным образом либо с таймаутом, если операцию нельзя завершить немедленно (например, при попытке записи в переполненный канал). Nucleus SE имеет аналогичный вызов, но приостановка задач опциональна, а таймаут не реализован.
Nucleus RTOS также предоставляет службу для трансляции (broadcast) на канал, но в Nucleus SE она не поддерживается. Она будет описана в разделе «Нереализованные вызовы API» в следующей статье.
pipe – указатель на предоставленный пользователем блок управления каналом;
message – указатель на отправляемое сообщение;
size – количество байт в сообщении. Если канал поддерживает сообщения переменной длины, этот параметр должен быть равен или меньше длины сообщения, поддерживаемой каналом. Если канал поддерживает сообщения фиксированной длины, этот параметр должен быть равен размеру поддерживаемого каналом сообщения;
suspend – спецификация приостановки задачи, может принимать значения NU_NO_SUSPEND, NU_SUSPEND или значение таймаута.
NUSE_SUCCESS – вызов был успешно завершен;
NUSE_INVALID_PIPE – некорректный индекс канала;
NUSE_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NUSE_INVALID_SUSPEND – попытка приостановки из не связанного с задачей потока или при отключенной блокировке задач;
NUSE_PIPE_FULL – канал переполнен, и тип приостановки задач не был указан;
NUSE_PIPE_WAS_RESET – канал был сброшен, пока задача была приостановлена.
Вариант кода функции 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 имеет аналогичный служебный вызов, но приостановка задач опциональна, а таймаут не реализован.
STATUS NU_Receive_From_Pipe(NU_PIPE *pipe, VOID *message, UNSIGNED size, UNSIGNED *actual_size, UNSIGNED suspend);
NUSE_SUCCESS – вызов был успешно завершен;
NUSE_INVALID_PIPE – некорректный индекс канала;
NUSE_INVALID_POINTER – нулевой указатель на сообщение (NULL);
NUSE_INVALID_SUSPEND – попытка приостановки задачи из не связанного с задачей потока или при отключенной поддержке приостановки задач;
NUSE_PIPE_EMPTY – канал пуст, и тип приостановки задач не был указан;
NUSE_PIPE_WAS_RESET – канал был сброшен, пока задача была приостановлена.
Вариант кода функции 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 имеет аналогичный служебный вызов, но приостановка задач опциональна, а таймаут не реализован.
pipe – указатель на предоставленный пользователем блок управления каналом;
message – указатель на отправляемое сообщение;
size – количество байт в сообщении. Если канал поддерживает сообщения переменной длины, этот параметр должен быть равен или меньше размера сообщения, поддерживаемого каналом. Если канал поддерживает сообщения фиксированной длины, этот параметр должен совпадать с размером сообщения, поддерживаемым каналом;
suspend – спецификация приостановки задач, может принимать значения NU_NO_SUSPEND, NU_SUSPEND или значение таймаута.
pipe – индекс (ID) используемого канала;
message – указатель на отправляемое сообщение, которое является последовательностью байтов, равную сконфигурированному размеру сообщения в канале;
suspend – спецификация приостановки задач, может принимать значения NUSE_NO_SUSPEND или NUSE_SUSPEND.
Вариант кода функции 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