< Все темы
Печать

Задачи и межзадачное взаимодействие в ОСРВ MULTEX-ARM

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

Описание задачи в общем виде

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

int taskExample (int size) {

    // Инициализация
    void *p = malloc (size);

    // Циклическое тело
    while (!stop) {
        someAction (p);
        taskDelay (1);
    }

    // Завершение
    free (p);
    return 0;
}

Демонстрационный проект

Для исследования взаимодействия задач в статье использован проект “Пример использования задач“, размещённый в разделе примеров на сайте set-code.ru. Ссылка на раздел сайта:

/multex/#demo

Данный пример можно скачать, скомпилировать и запустить на целевой машине. Проект сконфигурирован для процессора Allwinner A20, но его легко можно пересобрать для любого поддерживаемого процессора, изменив параметр ARCH_PROC в файле config.h. О том как собрать и запустить проект под управлением MULTEX-ARM подробно рассказано в обучающем видео, приведённом ниже:

https://rutube.ru/video/593551a1f3491dfb9e556855ef617399/?r=wd&t=40

По ходу описания взаимодействия задач будут приведены примеры кода из этого проекта. Результаты работы программы можно наблюдать на логическом анализаторе. Для подключения к анализатору в проекте используются 3 пина именуемые A, B и C настроенные на выводы процессора PI14, PI16 и PI18, соответственно. Такие значения заданы с помощью макросов в коде проекта и их можно изменить при необходимости. Скриншоты экрана логического анализатора, демонстрирующие наиболее характерные моменты работы программы будут приведены в качестве иллюстраций.

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


Взаимодействие задач без синхронизации

Рассмотрим простой пример параллельного выполнения двух задач, назовём их A и B. Пусть они управляют двумя выходными линиями процессора. Это позволит подключить к этим выходам логический анализатор и увидеть взаимодействие задач в реальном времени. Каждая из задач описывается одной и той же функцией taskSimple(). В секции инициализации задачи выполняется всего одно действие — настройка заданного выхода процессора. В теле задачи выполняется некая функция pulse() — она создаёт импульс, хорошо видимый на экране логического анализатора, после чего задача встаёт на ожидание. Ждать в данном случае задача будет тика системного килогерцового таймера. По каждому событию таймера управление получают все задачи, ожидающие этого события в порядке убывания приоритетов. Задачи с одинаковыми приоритетами получат управление в порядке их регистрации в системе. Каждая задача отрабатывает реакцию на событие — в нашем случае создаёт импульс и снова встаёт на ожидание. И так до установки флага stop, по которому задача завершит выполнение цикла и перейдёт к завершающей части. В данном случае никаких ресурсов освобождать не нужно. После выхода из функции задача будет удалена системой.

int taskSimple (int pin) {
    gpio_direction_output (pin, 0);

    while (!stop) {
        pulse (pin);
        taskDelay (1);
    }

    return 0;
}

Запуск двух задач, управляющих разными выходными линиями в нашем примере осуществляется из функции testSimple(). Запуск каждой из задач осуществляется с помощью метода taskSpawn(). Задачи будут запускаются с одинаковыми приоритетами равными 10.

void testSimple () {
    stop = false;
    task_A = taskSpawn ("task A", 10, 0, 0, taskSimple, pin_A);
    task_B = taskSpawn ("task B", 10, 0, 0, taskSimple, pin_B);
}

После запуска testSimple() на верхнем графике логического анализатора мы увидим импульсы соответствующие работе задачи A, на нижнем графике — импульсы задачи B. Каждую миллисекунду, по событию таймера, сначала выполняется задача A, затем она встаёт на ожидание и управление получает задача B. Далее обе задачи ожидают следующего события таймера.

После вызова функции testSimple() в консоли можно посмотреть список всех задач и их приоритеты по команде i. Наименьшее значение соответствует более высокому приоритету. В списке запущенные нами задачи отображаются как task A и task B. Обе задачи имеют одинаковый приоритет равный десяти.

В процессе работы задач можно изменить приоритет их выполнения с помощью функции taskPrioritySet(). Для удобства вызова из консоли в проекте создана простая функция AP(), позволяющая изменять приоритет задачи A.

void AP (int priority) {
    taskPrioritySet (task_A, priority);
}

Например, если понизить приоритет задачи A до 11, то на логическом анализаторе видно, что порядок следования импульсов изменился. Теперь первой получает управление более приоритетная задача B.

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


Взаимодействие задач с помощью Семафоров

Основным механизмом синхронизации задач в реальном времени и организации взаимоисключающего доступа задач к общим ресурсам в MULTEX-ARM являются семафоры. Без синхронизации задачи, использующие общий ресурс, могут нарушить целостность используемых данных. Для предотвращения такой ситуации создаётся бинарный семафор, который задача должна захватить, перед началом использования общего ресурса. Задача, пытающаяся захватить уже занятый семафор будет приостановлена средствами операционной системы до освобождения общего ресурса. Захватив освободившийся семафор задача продолжит своё выполнение.

Рассмотрим синхронизацию задач на простом примере. С помощью функции testSem() можно запустить две задачи с одинаковым приоритетом 10. В отличие от предыдущего примера эти задачи, описанные с помощью функции taskSem(), сразу же пытаются захватить общий семафор sem с помощью метода semTake(). Так как семафор создан уже закрытым, то обе задачи остановятся. Соответственно на логическом анализаторе импульсы видны не будут.

int taskSem (int pin) {

    gpio_direction_output (pin, 0);

    while (!stop) {
        int state = semTake (sem, WAIT_FOREVER);
        if (state == OK)
            pulse (pin);
        else {
            // Остановка при первой же неудаче
            halfPulse (pin);
            printf ("semTake error: %d\n", state);
            stop = true;
        }
    }

    if (sem != NULL) {
        semDelete (sem);
        sem = NULL;
    }

    return 0;
}

void testSem () {
    stop = false;
    if (sem == NULL)
        sem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);
    task_A = taskSpawn ("task A", 10, 0, 0, taskSem, pin_A);
    task_B = taskSpawn ("task B", 10, 0, 0, taskSem, pin_B);
}

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

int taskRelease (int val) {
    while (!stop) {
        if (sem != NULL) {
            semGive (sem);
            semGive (sem);
            semGive (sem);
            semGive (sem);
        }
        taskDelay (1);
    }
    return 0;
}

void testRelease () {
    taskSpawn ("task R", 15, 0, 0, taskRelease, 0);
}

После вызова функции testRelease() на логическом анализаторе появятся импульсы, иллюстрирующие взаимодействие всех трёх запущенных процессов. Так как приоритет открывающей задачи ниже чем у ожидающих, то при каждом открытии семафора произойдёт переключение на одну из ожидающих задач. Из всех ожидающих будет выбрана самая приоритетная. Если приоритеты ожидающих задач одинаковы, а в нашем случае это именно так, то будет выбрана первая стоящая в очереди. Первой в очереди стоит задача A, она отработает — создаст импульс и снова встанет на ожидание, теперь уже в конец очереди. Управление снова переключится на открывающую задачу, которая опять откроет семафор. Теперь управление переключится на задачу B. И так далее. На экране логического анализатора можно увидеть чередующуюся последовательность из четырёх импульсов.

Если поднять приоритет задачи A с помощью функции AP() до 9, то картина изменится — теперь доступ к ресурсам получит только задача A, как наиболее приоритетная. На логическом анализаторе в этом случае будут видны импульсы только на одном графике (как показано на рисунке ниже). Если понизить приоритет задачи A до 11 то все ресурсы достанутся задаче B. Так что если нужно распределить работу с семафором на две задачи их приоритеты должны быть одинаковыми.

Задачи в MULTEX-ARM могут не только синхронизироваться между собой, но и обмениваться при этом данными. Механизм обмена данными между задачами описан в следующем разделе.


Обмен данными между задачами с помощью очередей сообщений

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

Рассмотрим пример обмена данными между задачами через две очереди сообщений queue_A и queue_B. Очереди создаются в функции testMsg() с помощью метода msgQCreate(). Размер каждой очереди в проекте задан равным двум сообщениям, чтобы наглядно показать принцип их работы. В качестве сообщения используется одно целое значение типа int.

void testMsg () {
    stop = false;
    queue_A = msgQCreate (2, sizeof (int), MSG_Q_FIFO);
    queue_B = msgQCreate (2, sizeof (int), MSG_Q_FIFO);
    setMsgDesc (&msg_A, pin_A, queue_A, queue_B);
    setMsgDesc (&msg_B, pin_B, queue_B, queue_A);
    task_A = taskSpawn ("task A", 10, 0, 0, taskMsg, (int) &msg_A);
    task_B = taskSpawn ("task B", 10, 0, 0, taskMsg, (int) &msg_B);
}

Для описания обеих задач используется одна и та же функция taskMsg(), содержащая как получение сообщений из очереди, так и запись сообщений.

int taskMsg (int addr) {
    tMsgDesc *desc = (tMsgDesc *) addr;
    gpio_direction_output (desc->pin, 0);

    while (!stop) {
        int data = value;

        while (msgQReceive (desc->in, &data, sizeof (int), 
            NO_WAIT) == OK);

        while (msgQSend (desc->out, &data, sizeof (int),
                      NO_WAIT, MSG_PRI_NORMAL) == OK) {
            pulse (desc->pin);
        }
        taskDelay (1);
    }
    return 0;
}

Чтобы не запутаться в направлениях передачи данных используется структура с описанием очередей tMsgDesc, которая передаётся в задачу в качестве параметра при запуске. Функция setMsgDesc() создана просто для удобства заполнения полей структуры.

typedef struct {
    int pin;
    MSG_Q_ID in;
    MSG_Q_ID out;
} tMsgDesc;

tMsgDesc msg_A, msg_B;

void setMsgDesc (tMsgDesc *desc, int pin, MSG_Q_ID in, MSG_Q_ID out) {
    desc->pin = pin;
    desc->in = in;
    desc->out = out;
}

После вызова из консоли функции testMsg() задачи A и B запустятся с одинаковым приоритетом 10. Первой получит управление задача A. Очистив входящую очередь задача переходит к записи в исходящую очередь. Запись ведётся до полного заполнения очереди — на логическом анализаторе можно наблюдать два импульса, соответствующие отправке двух сообщений. После этого задача встаёт на ожидание тика системного таймера и управление переходит задаче B. Она получает все пришедшие сообщения и тоже отправляет 2 сообщения. После чего так же встаёт на ожидание. Со следующим тиком таймера весь процесс повторяется.

Если понизить приоритет задачи A с помощью функции AP() до 11 то произойдёт изменение порядка срабатывания задач. Теперь после тика таймера первой получит доступ к очередям задача B.


Синхронизация задач с обработчиками прерываний

В ОСРВ MULTEX-ARM начиная с версии 5.9 для синхронизации задач с прерываниями можно использовать стандартные механизмы межзадачного взаимодействия — семафоры и очереди сообщений. При этом нужно учесть, что обработчик прерывания не является задачей и не может быть остановлен для переключения на другую задачу. Логика работы семафоров и очередей сообщений в обработчиках прерываний отличается от обычной, а именно:

  • При освобождении семафора в обработчике прерывания переключение на ожидающую задачу происходит не мгновенно, а после завершения обработки имеющихся прерываний;
  • Захват семафора в обработчике прерывания происходит, только если семафор открыт до возникновения прерывания. Значение таймаута для метода semTake() в обработчике игнорируется и фактически всегда равно NO_WAIT;
  • Запись в очередь и чтение из очереди сообщений в обработчике прерывания возможно только без ожидания. То есть, очереди обмена сообщениями с обработчиком прерывания должны быть готовы до возникновения прерывания. Передающая очередь уже должна содержать нужные данные, а принимающая должна быть своевременно очищена и иметь свободные слоты.

Далее в статье будут приведены примеры, иллюстрирующие эти особенности.


Освобождение семафора в прерывании

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

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

int t4SemGiveHandler (int dummy) {
    for (int i=0; i<2; i++) {
        pulse (pin_A);
        semGive (sem);
    }
    return 0;
}

int t2SemGiveHandler (int dummy) {
    for (int i=0; i<4; i++) {
        pulse (pin_B);
        semGive (sem);
    }
    return 0;
}

Настройка и запуск таймеров, а также запуск задачи taskSem(), ожидающей освобождения семафора, осуществляется с помощью функции testIntSemGive(). При запуске таймеров их периоды заданы близкими по значению и подобраны таким образом, чтобы обработчики иногда перекрывались. Листинг запускающей функции приведён ниже. Код задачи taskSem() был приведён ранее в этой статье.

void testIntSemGive (bool semCounter) {
    stop = false;
    if (sem == NULL) {
        if (semCounter) {
            // Целочисленный семафор
            sem = semCCreate (SEM_Q_FIFO, 0);
        } else {
            // Бинарный семафор
            sem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);
        }
    }

    taskSpawn ("task C",  9, 0, 0, taskSem, pin_C);

    gpio_direction_output (pin_A, 0);
    gpio_direction_output (pin_B, 0);

    taSetInterrupt ( TIMER_2,
        INTERRUPT_GROUP_MAJOR, INTERRUPT_PRIORITY_BASE,
        t2SemGiveHandler);

    taSetInterrupt ( TIMER_4,
        INTERRUPT_GROUP_TIME_CRITICAL, INTERRUPT_PRIORITY_BASE,
        t4SemGiveHandler);

    taStart (TIMER_4, 3000000, true);
    taStart (TIMER_2, 2990000, true);
}

Для начала следует запустить из консоли функцию testIntSemGive() без параметров, при этом семафор sem будет создан как бинарный. После запуска программы можно наблюдать результат её работы на логическом анализаторе. На верхних двух графиках видны импульсы, полученные в результате работы обработчиков прерываний таймеров. На нижнем графике видны импульсы, возникающие в тот момент, когда задача получает управление после освобождения семафора. В случае, если прерывания не перекрываются мы можем наблюдать, что задача получает управление один раз после завершения работы каждого из обработчиков. И поскольку мы используем бинарный семафор, то не важно сколько раз был отпущен семафор в прерывании — задача получит управление только один раз.

Если же прерывания выполняются подряд или с перекрытием то задача получает управление и вовсе один раз — после завершения работы последнего прерывания.

Обратите внимание работу прерываний. Прерывание от таймера 2 (график A) имеет более высокий приоритет, чем прерывание от таймера 4 (график B), поэтому обработчик прерывания таймера 4 прерывается в момент возникновения прерывания от более приоритетного таймера 2. Такое прерывание называется вложенным.

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

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


Захват семафора в прерывании

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

Значение таймаута для метода semTake() в обработчике игнорируется и фактически всегда равно NO_WAIT. В случае, если семафор был освобождён до возникновения прерывания, метод semTake() в обработчике прерывания вернёт OK. Если же семафор уже захвачен в момент возникновения прерывания, метод semTake() вернёт ERROR, вне зависимости от выбранного значения таймаута.

В следующем примере изменено направление передачи управления. Теперь семафор освобождается в задаче и захватывается в прерываниях от таймеров. Задача, освобождающая семафор, описана в функции taskSemGive().

int taskSemGive (int pin) {
    gpio_direction_output (pin, 0);

    while (!stop) {
        for (int i=0; i<6; i++)
            if (semGive (sem) == OK)
                pulse (pin);
            else
                halfPulse (pin);
        taskDelay (5);
    }

    if (sem != NULL) {
        semDelete (sem);
        sem = NULL;
    }
    return 0;
}

В прерываниях от таймеров, возникающих независимо от хода выполнения задачи, производится попытка захвата этого семафора. Как уже было отмечено, в обработчиках прерываний невозможно ожидать освобождения семафора, поэтому функция semTake() здесь используется с параметром NO_WAIT. В случае успешного захвата семафора обработчик прерывания создаёт обычный импульс, а в случае неудачи — импульс половинной длины.

int t4SemTakeHandler (int dummy) {
    for (int i=0; i<2; i++) {
        if (semTake (sem, NO_WAIT) == OK)
            pulse (pin_A);
        else
            halfPulse (pin_A);
    }
    return 0;
}

int t2SemTakeHandler (int dummy) {
    for (int i=0; i<4; i++) {
        if (semTake (sem, NO_WAIT) == OK)
            pulse (pin_B);
        else
            halfPulse (pin_B);
    }
    return 0;
}

Функция testIntSemTake(), запускающая задачу и обработчики прерываний, ни чем не отличается от аналогичной функции из предыдущего примера.

void testIntSemTake (bool semCounter) {
    stop = false;
    if (sem == NULL) {
        if (semCounter)
            // Целочисленный семафор
            sem = semCCreate (SEM_Q_FIFO, 0);
        else
            // Бинарный семафор
            sem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);
    }

    taskSpawn ("task C", 10, 0, 0, taskSemGive, pin_C);

    gpio_direction_output (pin_A, 0);
    gpio_direction_output (pin_B, 0);

    taSetInterrupt (
        TIMER_2,
        INTERRUPT_GROUP_MAJOR,
        INTERRUPT_PRIORITY_BASE,
        t2SemTakeHandler);

    taSetInterrupt (
        TIMER_4,
        INTERRUPT_GROUP_TIME_CRITICAL,
        INTERRUPT_PRIORITY_BASE,
        t4SemTakeHandler);

    taStart (TIMER_4, 3000000, true);
    taStart (TIMER_2, 2990000, true);
}    

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

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

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

Этот же пример можно запустить с использованием семафора-счётчика. Для этого следует перезапустить целевую машину и вызвать из консоли функцию testIntSemTake() с параметром 1. Результат работы программы приведён на следующем графике.

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


Обмен данными между задачами и обработчиками прерываний

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

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

Для иллюстрации описанных особенностей рассмотрим взаимодействие двух прерываний от таймеров с одной задачей через очереди сообщений.

Обработчик таймера t4MsgHandler() с более высоким приоритетом заполняет очередь сообщений A. При каждой удачной отправке сообщения в очередь будет создан импульс полной длины, который будет выведен на график A логического анализатора. При неудаче, если очередь заполнена и не может принять новые данные, на графике будет выведен импульс половинной длины.

int t4MsgHandler (int dummy) {
    int data = 0;
    for (int i=0; i<3; i++) {
        int send = msgQSend (queue_A, &data, sizeof (int),
                     NO_WAIT, MSG_PRI_NORMAL);
        if (send == OK) {
            totalTx++;
            pulse (pin_A);
        } 
        else 
            halfPulse (pin_A);
    }
    return 0;
}

Задача taskIntMsg() ожидает появления сообщений в очереди A и перекладывает их в очередь B. При каждой удачной пересылке сообщения на графике C графического анализатора будет появляться импульс полной длины.

int taskIntMsg (int pin) {
    gpio_direction_output (pin, 0);

    while (!stop) {
        int data = 0;

        int recv = msgQReceive (queue_A, &data, sizeof (int), 
                    WAIT_FOREVER);
        int send = msgQSend (queue_B, &data, sizeof (int), 
                    WAIT_FOREVER, MSG_PRI_NORMAL);
        if ((recv == OK) && (send == OK))
            pulse (pin);

    }
    return 0;
}

Обработчик таймера t2MsgHandler() с меньшим приоритетом забирает сообщения из очереди B. При каждом удачном получении сообщения из очереди будет создан импульс полной длины, который будет выведен на график B логического анализатора. При неудаче, если очередь пуста, на графике будет выведен импульс половинной длины.

int t2MsgHandler (int dummy) {
    int data = 0;
    int recv = OK;

    while (recv == OK) {
        recv = msgQReceive (queue_B, &data, sizeof (int),
                      NO_WAIT);
        if (recv == OK) {
            pulse (pin_B);
            totalRx++;
        } 
        else 
            halfPulse (pin_B);
    }

    return 0;
}

Запуск программы осуществляется с помощью функции testIntMsg(), листинг которой не сильно отличается от аналогичных функций из предыдущих разделов. Единственное отличие в параметре функции. Теперь это размер создаваемых очередей сообщений. По умолчанию (при вызове функции без параметра) создаются очереди длинной ровно в три сообщения, что соответствует количеству передаваемых данных.

void testIntMsg (int size) {
    stop = false;
    totalTx = totalRx = 0;
    int n = (size > 0) ? size : 3;
    queue_A = msgQCreate (n, sizeof (int), MSG_Q_FIFO);
    queue_B = msgQCreate (n, sizeof (int), MSG_Q_FIFO);

    gpio_direction_output (pin_A, 0);
    gpio_direction_output (pin_B, 0);

    taskSpawn ("task C", 10, 0, 0, taskIntMsg, pin_C);

    taSetInterrupt ( TIMER_2,
        INTERRUPT_GROUP_MAJOR, INTERRUPT_PRIORITY_BASE,
        t2MsgHandler);

    taSetInterrupt ( TIMER_4,
        INTERRUPT_GROUP_TIME_CRITICAL, INTERRUPT_PRIORITY_BASE,
        t4MsgHandler);

    taStart (TIMER_4, 3000000, true);
    taStart (TIMER_2, 2990000, true);
}

Вывод работы программы на графический анализатор приведён ниже. На графике C видно, что задача ожидающая данные из очереди A начинает работать сразу после завершения серии прерываний, заполняющих очередь. Задача справляется со своей работой по перемещению данных и прерывание получающее сообщения всегда вовремя получает данные. На графике работы второго прерывания (график B) всегда можно видеть 3 обычных импульса и один короткий, говорящий, что данные в очереди закончились.

При уменьшении размера очередей программа перестанет справляться. Чтобы посмотреть, как будет выглядеть работа программы с уменьшенными очередями следует перезапустить целевую машину и вызвать функцию testIntMsg() с параметром 2. Вывод программы на графический анализатор примет следующий вид:

Теперь обработчик может разместить в очереди только два сообщения. Далее очередь заполнена и третье сообщение разместить не удаётся, о чём свидетельствует короткий третий импульс на графике A графического анализатора. Данные в этом случае просто теряются. Это нужно иметь в виду при работе с прерываниями и создавать очереди достаточной длины.


Общий подход к взаимодействию с прерываниями

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

Оглавление