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 делает два действия неразрывно: ставит свой флаг
и ждёт всех. Когда встал последний, все ждущие просыпаются разом,
а флаги синхронизации сбрасываются автоматически — в отличие
от обычного ожидания.
Неразрывность важна. Сделать то же двумя вызовами — поставить, потом ждать — нельзя: между ними другая задача могла увидеть все флаги, сбросить их и уйти, а эта осталась ждать навсегда.
Где применяют: этапы обработки, которые нельзя смешивать (все собрали данные — все считают — все выводят); запуск, где задачи должны стартовать одновременно после инициализации; тесты с несколькими участниками.
31.3 Уведомление задачи
Самый дешёвый механизм в ядре, и часто лучший.
У каждой задачи в блоке управления есть слово-уведомление и байт состояния. Их не нужно создавать, они не занимают дополнительной памяти — пять байт уже внутри тех 92, что стоит задача. Отправить уведомление — значит записать в это слово и разбудить задачу.
Как семафор
/* прерывание */
vTaskNotifyGiveFromISR(h_task, &woken);
portYIELD_FROM_ISR(woken);
/* задача */
count = ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
Полная замена связке «семафор — отложенная обработка» из раздела 28,
без единого объекта. Первый аргумент Take выбирает поведение:
| Аргумент | На выходе | Ведёт себя как |
|---|---|---|
pdTRUE |
значение обнуляется | двоичный семафор |
pdFALSE |
значение уменьшается на единицу | считающий семафор |
Во втором случае функция возвращает, сколько событий накопилось, — и ни одно не теряется.
| Двоичный семафор | Уведомление | |
|---|---|---|
| объект ядра | 80 байт | нет |
| создание | нужно | не нужно |
| скорость | обычная | заметно выше |
| получателей | сколько угодно ждущих | ровно один, известный заранее |
| из прерывания | да | да |
Последняя строка — единственное ограничение, и оно принципиальное: уведомление адресуется конкретной задаче по описателю. Разбудить «кого-нибудь, кто ждёт», оно не может. Если ждущих может быть несколько или получатель не известен отправителю — семафор или очередь.
Как очередь на одно значение
Уведомление несёт 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 Итоги
- Группа событий — 24 флага в одном слове за 32 байта; старший байт занят ядром.
- Ждать можно любого из флагов или всех сразу; функция возвращает значение в момент пробуждения, и разбирать нужно его.
- Сброс на выходе — обычный режим; без него задача просыпается по кругу.
- Установка из прерывания идёт через демона таймеров: таймеры должны быть включены, а флаг встаёт после выхода из обработчика.
xEventGroupSyncставит флаг и ждёт всех неразрывно; так делают точку синхронизации, двумя вызовами — нельзя.- Уведомление живёт в блоке управления задачей: объекта нет, создавать нечего, работает быстрее семафора.
- Адресовано ровно одной задаче — это его единственное ограничение.
- Ячейку 0 делят с потоковыми буферами; для своих сигналов — ячейка 1
и функции
...Indexed. - Пять способов изменить значение позволяют заменить семафор, группу на одну задачу и очередь на одно место.
- Группа будит на установку битов, но не на сброс. Для «выхода по маске входов» ждут служебный признак изменения, а режимы «все» и «любой» считают сами (31.5).
Дальше: раздел 32 «Буферы потоков и сообщений» — когда данные не укладываются в сообщения фиксированного размера: байтовый поток с UART и кадры переменной длины.