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 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.
- 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.
#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.
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.