CodeRigor

FREE LESSON

Software timers, event flags & daemon lifetime

Use FreeRTOS software timers and event groups: daemon callbacks, ANY/ALL waits, coalescing, timeout snapshots, static storage and acknowledgement of asynchronous deletion.

A hardware timer can interrupt at a chosen count. A FreeRTOS software timer asks a shared service task to run a callback after a number of RTOS ticks. That callback may wake useful work; it should not become the work itself. Begin after tasks and queues; the ISR handoff lesson explains the hardware-timer path.

One daemon serves every software timer

configUSE_TIMERS enables the timer daemon and its command queue. This course uses a 1 kHz tick and daemon priority 7. Timer resolution and dispatch depend on ticks, priority and other work. A software timer is not a promise of precise hardware timing. The emulator's repeatable virtual ticks cannot establish a board's worst-case latency.

The timer daemon is shared task context
Commands and callbacks share the daemon. Post a condition quickly, then let the worker perform blocking or expensive work.
The timer daemon is shared task context
Commands and callbacks share the daemon. Post a condition quickly, then let the worker perform blocking or expensive work.

The callback runs in ordinary task context, so it uses xEventGroupSetBits, not its FromISR variant. However, it must not enter the Blocked state: no vTaskDelay, no positive-timeout queue waits, and no waiting for a worker to finish. Blocking the daemon stalls other timers and command processing. The ISR-safe event-group setter defers work to the daemon too; its successful return means the deferral was accepted, not that every waiter has run.

Creating a timer leaves it dormant. xTimerStart and xTimerChangePeriod submit commands; their block-time argument bounds waiting for queue space, not the time before a callback finishes. A one-shot timer expires once per start; an auto-reload timer becomes eligible again every period until stopped. A late daemon may process overdue expirations together: keep callbacks bounded and choose deliberately whether the application needs every occurrence or only the latest condition.

Flags are conditions, not messages

An event group lets several tasks wait on bit combinations. In this target, EventBits_t has 32 bits but FreeRTOS reserves the upper eight: application masks use the lower 24. Reject a zero mask and reserved bits before calling an API whose contract asserts on them. Other tick-width configurations need their own valid mask; this exercise targets the configured Cortex-M4 port.

Repeated sets coalesce
A set bit remembers a condition. Setting it twice does not remember two jobs; a counter or queue is needed for that.
Repeated sets coalesce
A set bit remembers a condition. Setting it twice does not remember two jobs; a counter or queue is needed for that.
  • ANY is satisfied by at least one requested bit.
  • ALL requires every requested bit. Other set bits cannot satisfy it.
  • Clear-on-exit clears the requested mask when the condition is satisfied. A timed-out wait leaves its partial flags set.
  • The returned snapshot describes the flags before automatic clearing. It can contain unrelated flags and can be nonzero on an ALL timeout.

This example observes a partial ALL result, then completes its condition. It runs in a real task; every test wait is bounded.

C++
#include "FreeRTOS.h"
#include "event_groups.h"
#include "task.h"
#include <cstdio>
#include <cstdlib>
static void demo(void*) {
    StaticEventGroup_t storage;
    auto group = xEventGroupCreateStatic(&storage);
    configASSERT(group != nullptr);
    xEventGroupSetBits(group, 1);
    auto partial = xEventGroupWaitBits(group, 3, pdTRUE, pdTRUE, 2);
    auto left = xEventGroupGetBits(group);
    xEventGroupSetBits(group, 2);
    auto complete = xEventGroupWaitBits(group, 3, pdTRUE, pdTRUE, 0);
    std::printf("%lu %lu %lu %lu\n", (unsigned long)partial,
                (unsigned long)left, (unsigned long)complete,
                (unsigned long)xEventGroupGetBits(group));
    vEventGroupDelete(group);
    std::exit(0);
}
int main() {
    configASSERT(xTaskCreate(demo, "flags", 1024, nullptr, 1, nullptr) == pdPASS);
    vTaskStartScheduler();
}

The output is 1 1 3 0. Returning only a boolean would discard the useful partial observation. In the build, return {observed, ready} and mask the snapshot to the caller's requested flags.

Static storage still needs a lifetime protocol

xTimerCreateStatic uses the supplied StaticTimer_t. The timer's ID can be a pointer to callback context, such as an event-group handle and bit mask. Both objects must remain alive and at stable addresses while the daemon can use them. A stack buffer is valid only if its scope lasts long enough. Static allocation removes timer heap allocation; it does not turn queued work into synchronous work.

Deletion needs acknowledgement before storage expires
With one owner and no later restart commands, a FIFO daemon fence after DELETE proves that earlier daemon references have been retired.
Deletion needs acknowledgement before storage expires
With one owner and no later restart commands, a FIFO daemon fence after DELETE proves that earlier daemon references have been retired.

A successful xTimerDelete means the command was accepted. Under the fixture's single-owner policy, it queues an xTimerPendFunctionCall acknowledgement after DELETE and waits for that acknowledgement before leaving scope. FIFO command processing provides the ordering. New commands must be excluded during teardown; the fence cannot protect against another task restarting a deleted handle. The fence callback only signals its waiter and does not touch its borrowed context afterwards. Never perform this blocking teardown from a daemon callback.

Build and inspect

Implement the bounded flag wait, short pulse callback, and dormant static timer factory. The fixture owns teardown so you can focus on the kernel semantics. Tests use actual one-shot and auto-reload timers, independent timer IDs, partial ALL timeouts, persistent flags, coalescing and invalid masks. Trace these three questions separately: what is stored, what satisfies the waiter, and which objects a pending daemon command still borrows.

Use a queue or counting notification when every sample matters. Use event bits for conditions such as “configuration loaded” and “link ready”. For periodic work that must block, prefer a worker with xTaskDelayUntil, and use hardware capture/interrupts when the requirement depends on peripheral timing.

The checks and build establish functional behavior under emulation. They do not measure hardware response time, interrupt jitter, electrical behavior or worst-case execution time.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Software timers share a daemon task and command queue. The examples use ESP32/Arduino; this lesson implements the underlying FreeRTOS APIs on the emulated Cortex-M4.