Embedded HandbookПриложения
Все разделы › IV. FreeRTOS
STM32F103, STM32F407 · уровень средний · 13 мин чтения
Открыть в интерактивном справочнике

31. События и уведомления

Очередь передаёт данные, мьютекс охраняет ресурс. А что, если передать нужно только факт — «отсчёт готов», «кнопка нажата», «все трое закончили»? Для этого есть два механизма: группа событий, когда фактов много и ждать нужно нескольких сразу, и уведомление задачи, когда получатель один и хочется обойтись без объекта ядра вовсе.

Как это называется в документации

Оригинал Перевод
event group группа событий
event bit / flag бит события, флаг
wait for all / any ждать все / любой
clear on exit сбросить на выходе
rendezvous / synchronisation point точка синхронизации
task notification уведомление задачи
notification value значение уведомления
direct to task адресовано конкретной задаче
lightweight облегчённый, без объекта ядра
input mask маска входов
POSIX port порт ядра для компьютера: задача — поток, тик — сигнал

31.1 Группа событий

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

#define EV_ADC_DONE   (1u << 0)
#define EV_UART_RX    (1u << 1)
#define EV_BUTTON     (1u << 2)

static EventGroupHandle_t eg;

/* в main, до vTaskStartScheduler */
eg = xEventGroupCreate();

Флагов не 32, а 24. Старший байт слова занят ядром под служебные признаки. Биты 0…23 — ваши; попытка использовать бит 24 и выше ловится проверкой.

Цена — 32 байта за группу. Семафор стоит 80, и группа заменяет до двадцати четырёх таких: экономия почти два килобайта там, где флагов много.

Ожидание

EventBits_t bits = xEventGroupWaitBits(
    eg,
    EV_ADC_DONE | EV_UART_RX | EV_BUTTON,   /* какие биты интересуют      */
    pdTRUE,                                 /* сбросить их на выходе      */
    pdFALSE,                                /* ждать ЛЮБОЙ, не все        */
    portMAX_DELAY);

if (bits & EV_ADC_DONE) { /* ... */ }
if (bits & EV_UART_RX)  { /* ... */ }

Два флага-аргумента решают всё:

Аргумент pdFALSE pdTRUE
сбросить на выходе флаги остаются стоять дождавшиеся сбрасываются
ждать все достаточно любого нужны все перечисленные

Функция возвращает значение группы в момент пробуждения — и разбирать нужно его, а не читать группу заново: к моменту чтения другая задача могла флаги уже сбросить.

⚠️ Без сброса на выходе задача проснётся снова тут же. Флаг стоит, условие выполнено — ожидание завершится немедленно, и так по кругу. Сброс на выходе — обычный режим; оставлять флаги имеет смысл, только если на них ждёт ещё кто-то.

Ждать все — то, чего не умеет ни очередь, ни семафор:

xEventGroupWaitBits(eg, EV_ADC_DONE | EV_UART_RX, pdTRUE, pdTRUE, portMAX_DELAY);
/* оба датчика отчитались — можно совмещать */

Задача спит, пока не соберутся оба флага, и просыпается один раз.

Установка

xEventGroupSetBits(eg, EV_BUTTON);                   /* из задачи */
xEventGroupSetBitsFromISR(eg, EV_ADC_DONE, &woken);  /* из прерывания */

⚠️ Установка из прерывания идёт через задачу-демон таймеров. Один флаг может разбудить много задач, и делать это в обработчике слишком долго. Поэтому SetBitsFromISR кладёт запрос демону (раздел 29), а тот ставит флаг, когда получит процессор. Следствия: должны быть включены программные таймеры (configUSE_TIMERS 1) и передача вызова демону (INCLUDE_xTimerPendFunctionCall 1, по умолчанию 0) — без второго компоновщик не найдёт xTimerPendFunctionCallFromISR, которую вы не вызывали (с трассировкой — саму xEventGroupSetBitsFromISR); а флаг встаёт с задержкой на переключение к демону — и после выхода из прерывания, а не внутри.

Сбросить флаг вручную — xEventGroupClearBits; прочитать, не ожидая, — xEventGroupGetBits.


31.2 Точка синхронизации

Задача, которую группа решает лучше всего: несколько задач должны дождаться друг друга и продолжить одновременно.

#define SYNC_A   (1u << 8)
#define SYNC_B   (1u << 9)
#define SYNC_C   (1u << 10)
#define SYNC_ALL (SYNC_A | SYNC_B | SYNC_C)

/* в каждой из трёх задач, со своим битом */
xEventGroupSync(eg, SYNC_A, SYNC_ALL, portMAX_DELAY);
/* здесь все трое одновременно */

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

Неразрывность важна. Сделать то же двумя вызовами — поставить, потом ждать — нельзя: между ними другая задача могла увидеть все флаги, сбросить их и уйти, а эта осталась ждать навсегда.

Где применяют: этапы обработки, которые нельзя смешивать (все собрали данные — все считают — все выводят); запуск, где задачи должны стартовать одновременно после инициализации; тесты с несколькими участниками.

Точка синхронизации: три задачи ставят свои биты в xEventGroupSync и продолжают вместе


31.3 Уведомление задачи

Самый дешёвый механизм в ядре, и часто лучший.

У каждой задачи в блоке управления есть слово-уведомление и байт состояния. Их не нужно создавать, они не занимают дополнительной памяти — пять байт уже внутри тех 92, что стоит задача. Отправить уведомление — значит записать в это слово и разбудить задачу.

Как семафор

/* прерывание */
vTaskNotifyGiveFromISR(h_task, &woken);
portYIELD_FROM_ISR(woken);

/* задача */
count = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);

Полная замена связке «семафор — отложенная обработка» из раздела 28, без единого объекта. Первый аргумент Take выбирает поведение:

Аргумент На выходе Ведёт себя как
pdTRUE значение обнуляется двоичный семафор
pdFALSE значение уменьшается на единицу считающий семафор

Во втором случае функция возвращает, сколько событий накопилось, — и ни одно не теряется.

Двоичный семафор Уведомление
объект ядра 80 байт нет
создание нужно не нужно
скорость обычная заметно выше
получателей сколько угодно ждущих ровно один, известный заранее
из прерывания да да

Последняя строка — единственное ограничение, и оно принципиальное: уведомление адресуется конкретной задаче по описателю. Разбудить «кого-нибудь, кто ждёт», оно не может. Если ждущих может быть несколько или получатель не известен отправителю — семафор или очередь.

Два пути от прерывания к задаче: через двоичный семафор — отдельный объект ядра на 80 байт, список ждущих и любой отдающий; через уведомление — слово и байт состояния, 5 байт уже внутри блока управления задачи на 92 байта, без объекта, но получатель ровно один и известен заранее

Как очередь на одно значение

Уведомление несёт 32 бита, и это можно использовать:

/* отправитель */
xTaskNotify(h_task, raw_value, eSetValueWithOverwrite);

/* получатель */
xTaskNotifyWait(0, 0xFFFFFFFFu, &value, portMAX_DELAY);

Пять способов изменить значение при отправке:

Действие Что происходит Похоже на
eNoAction только разбудить сигнал
eSetBits установить биты группа событий на одну задачу
eIncrement прибавить единицу считающий семафор
eSetValueWithOverwrite записать, затерев старое очередь на одно место с перезаписью
eSetValueWithoutOverwrite записать, если старое прочитано очередь на одно место

Отсчёт АЦП, код команды, номер события — всё, что умещается в слово, передаётся уведомлением без очереди, без копирования, без 80 байт заголовка.

⚠️ Одно место, не очередь. С перезаписью второе значение затирает первое, если задача не успела прочитать; без перезаписи отправка вернёт отказ. Для потока значений нужна очередь — уведомление хранит ровно одно.

⚠️ Ячейка одна, и она не только ваша. Потоковые буферы и буферы сообщений (раздел 32) будят ждущую задачу тем же уведомлением. Задача, которая ждёт данных буфера, проснётся от вашего xTaskNotifyGive раньше срока и без данных; а ожидание буфера съедает признак уведомления, пришедшего до него, и xTaskNotifyWait потом ждёт его впустую. Для своих сигналов заводят отдельную ячейку: configTASK_NOTIFICATION_ARRAY_ENTRIES 2 и функции с окончанием Indexed — xTaskNotifyGiveIndexed(h, 1), ulTaskNotifyTakeIndexed(1, pdTRUE, portMAX_DELAY).


31.4 Что выбрать

Задача Средство
разбудить одну известную задачу уведомление
передать ей одно слово уведомление со значением
один факт, ждущих может быть несколько двоичный семафор
много фактов, ждать любого или всех группа событий
несколько задач ждут друг друга xEventGroupSync
поток значений очередь

Порядок предпочтения — сверху вниз: уведомление, пока его достаточно; дальше по мере надобности. В типичном приборе бо́льшая часть связок «прерывание → задача» укладывается в первую строку.


31.5 🔧 Входы и выходы по маске

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

Выход Условие
1 входы 1 и 4
2 вход 1
3 входы 3 и 4
4 любой вход

Маску нужно уметь менять на ходу.

Кажется, что это ровно xEventGroupWaitBits: по задаче на выход, каждая ждёт свою маску в режиме «все» или «любой». Но выход должен и гаснуть, когда условие перестало выполняться, а сброс бита группа не сообщает — ждущая задача об этом не узнает. Поэтому группа несёт состояние входов и один служебный признак «изменилось». Задача выходов ждёт этот признак, а режимы «все» и «любой» считает сама — так же, как их понимает ядро.

#define IN(n)    (1u << ((n) - 1))          /* вход n = 1…4 — биты 0–3 */
#define IN_ALL   0x0Fu
#define EV_CHG   (1u << 8)                  /* входы или правила изменились */

typedef struct {
    EventBits_t mask;                       /* какие входы участвуют */
    BaseType_t  all;                        /* pdTRUE — нужны все, pdFALSE — любой */
} rule_t;

static rule_t rules[4] = {
    { IN(1) | IN(4), pdTRUE  },             /* выход 1: входы 1 и 4 */
    { IN(1),         pdTRUE  },             /* выход 2: вход 1 */
    { IN(3) | IN(4), pdTRUE  },             /* выход 3: входы 3 и 4 */
    { IN_ALL,        pdFALSE },             /* выход 4: любой вход */
};

static int rule_on(rule_t r, EventBits_t bits)
{
    EventBits_t hit = bits & r.mask;
    return r.all ? hit == r.mask : hit != 0;
}

/* сменить правило на ходу — из любой задачи */
void rules_set(int k, EventBits_t mask, BaseType_t all)
{
    taskENTER_CRITICAL();
    rules[k].mask = mask;
    rules[k].all = all;
    taskEXIT_CRITICAL();
    xEventGroupSetBits(eg, EV_CHG);         /* пересчитать выходы */
}

static void inputs_task(void *arg)
{
    EventBits_t last = 0;
    (void)arg;
    for (;;) {
        EventBits_t now = buttons_read() & IN_ALL;
        if (now != last) {
            xEventGroupClearBits(eg, IN_ALL & ~now);
            xEventGroupSetBits(eg, now | EV_CHG);   /* новые входы и признак — одним вызовом */
            last = now;
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

static void outputs_task(void *arg)
{
    (void)arg;
    for (;;) {
        /* ждать изменения; разбирать значение группы на момент пробуждения */
        EventBits_t bits = xEventGroupWaitBits(eg, EV_CHG, pdTRUE, pdFALSE, portMAX_DELAY);
        rule_t r[4];
        uint32_t out = 0;
        taskENTER_CRITICAL();
        memcpy(r, rules, sizeof r);
        taskEXIT_CRITICAL();
        for (int k = 0; k < 4; k++)
            if (rule_on(r[k], bits))
                out |= 1u << k;
        leds_write(out);
    }
}

Три места стоит разобрать.

Входы и признак — одним вызовом. xEventGroupSetBits(eg, now | EV_CHG) ставит новые входы и признак изменения разом: задача выходов проснётся и увидит уже новые входы. Отпущенные кнопки сбрасываются отдельным ClearBits перед этим, но признака он не ставит, так что промежуточного состояния никто не увидит.

Разбирать значение на момент пробуждения. xEventGroupWaitBits возвращает группу в тот момент, когда условие выполнилось (31.1), и правила считаются по нему. Перечитать группу после пробуждения — значит смешать два разных момента.

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

Кнопки здесь опрашиваются раз в 10 мс без подавления дребезга — оно разобрано в 8.5, и его ставят в buttons_read.

Проверено на настоящем ядре. Этот файл собран не только под Cortex-M3, но и с портом FreeRTOS для POSIX: ядро 10.5.1 работает на компьютере, каждая задача — поток, тик — сигнал таймера. Задача-«нажиматель» каждые 50 мс меняла состояние кнопок и записывала, что горит; посреди сценария выход 4 перевели на правило «только вход 2».

Нажато (входы 4…1) 0000 0001 1001 1000 1100 0100 0010 1111 0000
Горят (выходы 4…1) 0000 1010 1011 1000 1100 1000 1000 1111 0000

После смены правила:

Нажато 0001 0010 0011 0000
Горят 0010 1000 1010 0000

31.6 Грабли

Задача просыпается в цикле, не дожидаясь события. Флаг не сброшен на выходе — стоит и удовлетворяет условию снова и снова.

Флаг из прерывания ставится с задержкой или не ставится. SetBitsFromISR идёт через демона таймеров: таймеры выключены либо демон занят или имеет низкий приоритет.

Не компонуется xEventGroupSetBitsFromISR. Нет INCLUDE_xTimerPendFunctionCall 1: компоновщик не находит xTimerPendFunctionCallFromISR или саму функцию.

Бит 24 и выше не работает. Старший байт занят ядром; флагов 24.

Синхронизация зависает. Ставили флаг и ждали двумя вызовами вместо xEventGroupSync; кто-то сбросил флаги между ними.

Уведомление никого не будит. Описатель не той задачи либо ждущих несколько — уведомление адресное.

Потоковый буфер вернул 0 байт раньше срока. Задаче, которая его ждёт, послали уведомление в ту же ячейку 0; своим сигналам — ячейка 1.

Теряются значения. Уведомление хранит одно; для потока — очередь.

ulTaskNotifyTake возвращает ноль после ожидания. Истекло время — это отказ, а не событие; при portMAX_DELAY и INCLUDE_vTaskSuspend 1 такого не бывает.

Выход не гаснет, когда условие пропало. Ждали нужные биты xEventGroupWaitBits, а сброс бита группа не сообщает. Ждать признак изменения и считать правила самим (31.5).

Выходы считаются по «старым» входам. Группу перечитали после пробуждения, вместо того чтобы разобрать значение, которое вернула xEventGroupWaitBits.


31.7 Итоги

Дальше: раздел 32 «Буферы потоков и сообщений» — когда данные не укладываются в сообщения фиксированного размера: байтовый поток с UART и кадры переменной длины.

Открыть в интерактивном справочнике