Embedded HandbookApps
All sections › IV. FreeRTOS
STM32F103, STM32F407 · level intermediate · 13 min read
Open in the interactive handbook

31. Events and notifications

A queue passes data, a mutex guards a resource. But what if all you need to pass is a fact — "the sample is ready", "the button is pressed", "all three have finished"? There are two mechanisms for this: an event group, when there are many facts and you need to wait for several at once, and a task notification, when there is one receiver and you would like to do without a kernel object at all.

Terms used in the documentation

Term Meaning
event group a word of event flags
event bit / flag one bit of an event group
wait for all / any wait until all / any of the bits are set
clear on exit clear the bits when the wait ends
rendezvous / synchronisation point the place where tasks wait for each other
task notification a signal sent straight to a task
notification value the word a notification carries
direct to task addressed to one specific task
lightweight cheap, with no kernel object
input mask which inputs take part in a condition
POSIX port the kernel port for a computer: a task is a thread, the tick is a signal

31.1 The event group

One word in which each bit is a separate flag. Tasks and interrupts set the flags, tasks wait on them — on one or on several at once.

#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();

There are 24 flags, not 32. The top byte of the word is taken by the kernel for its internal flags. Bits 0…23 are yours; an attempt to use bit 24 and above is caught by a check.

The cost is 32 bytes per group. A semaphore costs 80, and a group replaces up to twenty-four of them: a saving of almost two kilobytes where there are many flags.

Waiting

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)  { /* ... */ }

Two flag arguments decide everything:

Argument pdFALSE pdTRUE
clear on exit the flags stay set the bits waited for are cleared
wait for all any one is enough all those listed are needed

The function returns the value of the group at the moment of wake-up — and that is what you must parse, rather than reading the group again: by the time you read it, another task may already have cleared the flags.

⚠️ Without clear on exit the task wakes up again immediately. The flag is set, the condition is met — the wait ends at once, and so on round and round. Clear on exit is the normal mode; leaving the flags set makes sense only if someone else is also waiting on them.

Waiting for all is something neither a queue nor a semaphore can do:

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

The task sleeps until both flags are collected and wakes up once.

Setting

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

⚠️ Setting from an interrupt goes through the timer daemon task. One flag can wake many tasks, and doing that in a handler takes too long. So SetBitsFromISR puts a request to the daemon (section 29), and the daemon sets the flag when it gets the CPU. Consequences: software timers must be enabled (configUSE_TIMERS 1) and so must passing a call to the daemon (INCLUDE_xTimerPendFunctionCall 1, 0 by default) — without the second, the linker will not find xTimerPendFunctionCallFromISR, which you never called (with tracing — xEventGroupSetBitsFromISR itself); and the flag is set with a delay for the switch to the daemon — and after the interrupt exits, not inside it.

To clear a flag by hand — xEventGroupClearBits; to read without waiting — xEventGroupGetBits.


31.2 The synchronization point

The problem a group solves best: several tasks must wait for each other and continue at the same time.

#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 does two actions indivisibly: it sets its own flag and waits for everyone. When the last one is set, all waiting tasks wake up at once, and the synchronization flags are cleared automatically — unlike an ordinary wait.

The indivisibility matters. You cannot do the same with two calls — set, then wait: between them another task could see all the flags, clear them and leave, while this one is left waiting forever.

Where it is used: processing stages that must not be mixed (everyone has collected data — everyone computes — everyone outputs); a start-up where tasks must begin at the same time after initialization; tests with several participants.

The synchronization point: three tasks set their bits in xEventGroupSync and continue together


31.3 The task notification

The cheapest mechanism in the kernel, and often the best.

Every task has, in its control block, a notification word and a state byte. They do not need to be created and take no additional memory — five bytes already inside the 92 that a task costs. Sending a notification means writing to that word and waking the task.

As a semaphore

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

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

A complete replacement for the "semaphore — deferred processing" pairing from section 28, without a single object. The first argument of Take selects the behaviour:

Argument On exit Behaves like
pdTRUE the value is reset to zero a binary semaphore
pdFALSE the value is decremented by one a counting semaphore

In the second case the function returns how many events have accumulated — and none is lost.

Binary semaphore Notification
kernel object 80 bytes none
creation needed not needed
speed ordinary noticeably higher
receivers any number of waiting tasks exactly one, known in advance
from an interrupt yes yes

The last row is the only limitation, and it is fundamental: a notification is addressed to a specific task by its handle. It cannot wake "whoever is waiting". If there may be several waiting tasks, or the receiver is not known to the sender — a semaphore or a queue.

Two paths from an interrupt to a task: through a binary semaphore — a separate 80-byte kernel object, a list of waiters and any giver; through a notification — a word and a state byte, 5 bytes already inside the 92-byte task control block, no object, but exactly one receiver known in advance

As a queue for one value

A notification carries 32 bits, and you can use them:

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

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

Five ways to change the value when sending:

Action What happens Similar to
eNoAction just wake a signal
eSetBits set bits an event group for one task
eIncrement add one a counting semaphore
eSetValueWithOverwrite write, overwriting the old value a one-slot queue with overwrite
eSetValueWithoutOverwrite write if the old value has been read a one-slot queue

An ADC sample, a command code, an event number — anything that fits in a word is passed by notification with no queue, no copying, no 80 bytes of header.

⚠️ One slot, not a queue. With overwrite, the second value overwrites the first if the task has not read it yet; without overwrite the send returns failure. For a stream of values you need a queue — a notification holds exactly one.

⚠️ There is one slot, and it is not only yours. Stream buffers and message buffers (section 32) wake the waiting task with the same notification. A task waiting for buffer data will wake up from your xTaskNotifyGive too early and with no data; and a wait on a buffer consumes the flag of a notification that arrived before it, so xTaskNotifyWait then waits for it in vain. For your own signals set up a separate slot: configTASK_NOTIFICATION_ARRAY_ENTRIES 2 and the functions ending in Indexed — xTaskNotifyGiveIndexed(h, 1), ulTaskNotifyTakeIndexed(1, pdTRUE, portMAX_DELAY).


31.4 What to choose

Task Tool
wake one known task notification
pass it one word notification with a value
one fact, there may be several waiters binary semaphore
many facts, wait for any or all event group
several tasks wait for each other xEventGroupSync
a stream of values queue

The order of preference is top to bottom: a notification while it is enough; further down as needed. In a typical device most of the "interrupt → task" pairings fit into the first row.


31.5 🔧 Inputs and outputs by mask

A typical automation task: an output turns on when the inputs match a given mask. For example, four buttons and four LEDs:

Output Condition
1 inputs 1 and 4
2 input 1
3 inputs 3 and 4
4 any input

You need to be able to change the mask on the fly.

It looks like exactly xEventGroupWaitBits: one task per output, each waiting for its mask in "all" or "any" mode. But an output must also turn off when the condition stops being met, and the group does not report a bit being cleared — the waiting task will never find out. So the group carries the state of the inputs and one internal "changed" flag. The outputs task waits for that flag, and computes the "all" and "any" modes itself — the same way the kernel understands them.

#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);
    }
}

Three places are worth going through.

Inputs and the flag in one call. xEventGroupSetBits(eg, now | EV_CHG) sets the new inputs and the change flag at once: the outputs task wakes up and already sees the new inputs. Released buttons are cleared by a separate ClearBits before that, but it does not set the flag, so no one sees an intermediate state.

Parse the value at the moment of wake-up. xEventGroupWaitBits returns the group at the moment the condition was met (31.1), and the rules are evaluated on that. Re-reading the group after wake-up means mixing two different moments.

The rules are changed under protection. A rule has two fields. rules_set changes them in a critical section, and the outputs task copies the table in one too. After the change, rules_set sets the flag, and the outputs are recalculated at once, without waiting for a press.

The buttons here are polled once every 10 ms with no debouncing — that is covered in 8.5, and it goes into buttons_read.

Tested on the real kernel. This file is built not only for Cortex-M3 but also with the FreeRTOS port for POSIX: kernel 10.5.1 runs on a computer, each task is a thread, the tick is a timer signal. A "presser" task changed the state of the buttons every 50 ms and recorded what was lit; in the middle of the scenario output 4 was switched to the rule "input 2 only".

Pressed (inputs 4…1) 0000 0001 1001 1000 1100 0100 0010 1111 0000
Lit (outputs 4…1) 0000 1010 1011 1000 1100 1000 1000 1111 0000

After the rule change:

Pressed 0001 0010 0011 0000
Lit 0010 1000 1010 0000

31.6 Pitfalls

A task wakes up in a loop without waiting for an event. The flag is not cleared on exit — it stays set and satisfies the condition again and again.

A flag from an interrupt is set late or not at all. SetBitsFromISR goes through the timer daemon: timers are disabled, or the daemon is busy or has a low priority.

xEventGroupSetBitsFromISR does not link. There is no INCLUDE_xTimerPendFunctionCall 1: the linker cannot find xTimerPendFunctionCallFromISR or the function itself.

Bit 24 and above does not work. The top byte is taken by the kernel; there are 24 flags.

Synchronization hangs. The flag was set and waited on with two calls instead of xEventGroupSync; someone cleared the flags in between.

A notification wakes no one. The handle is of the wrong task, or there are several waiters — a notification is addressed.

A stream buffer returned 0 bytes too early. The task waiting on it was sent a notification in the same slot 0; your own signals belong in slot 1.

Values are lost. A notification holds one; for a stream — a queue.

ulTaskNotifyTake returns zero after waiting. The time ran out — that is a failure, not an event; with portMAX_DELAY and INCLUDE_vTaskSuspend 1 it does not happen.

An output does not turn off when the condition goes away. The needed bits were waited for with xEventGroupWaitBits, but the group does not report a bit being cleared. Wait for the change flag and evaluate the rules yourself (31.5).

Outputs are computed from "old" inputs. The group was re-read after wake-up instead of parsing the value returned by xEventGroupWaitBits.


31.7 Summary

Next: section 32 "Stream and message buffers" — when the data does not fit into fixed-size messages: a byte stream from a UART and variable-length frames.

Open in the interactive handbook