A callback is a function registered with another function or system component to be invoked at a defined point — typically when an event occurs, an operation completes, or a condition is met. In C this registration is most commonly done via a function pointer, though the broader concept applies to any mechanism by which a framework or subsystem calls back into user-provided code. The caller does not need to know the callback's implementation; it only needs to know the agreed-upon signature.
In practice
In embedded C, callbacks are commonly implemented as function pointers, though some APIs use index-based dispatch, message passing, or other indirect mechanisms that achieve the same effect. A driver or middleware component typically accepts a function pointer and stores it; when the triggering condition occurs (DMA transfer complete, UART byte received, timer expiry), the stored pointer is called. This decouples the driver from the application logic without requiring dynamic dispatch. Many HAL layers — including STM32 HAL and nRF5 SDK event handlers — are structured entirely around registered callbacks. Note that registration does not always happen at initialization time; some APIs accept callbacks per request, per operation, or via configuration tables.
In an RTOS context, callbacks appear in timer expiry handlers (e.g., FreeRTOS software timer callbacks run in the timer daemon task, though other FreeRTOS callback types can run in different contexts), event notification hooks, and middleware completion routines. The execution context matters: a callback invoked from an ISR must follow ISR-safe rules — no blocking calls, no non-reentrant functions, and on FreeRTOS, only `FromISR` API variants. Mixing ISR-context and task-context callbacks is a common source of hard-to-reproduce bugs.
A recurring pitfall is assuming a callback runs in a predictable context or at a predictable priority. On many RTOS configurations, a "completion callback" may run in a high-priority system task or directly in an interrupt, not in the caller's task context. Shared state touched by a callback must be protected with the appropriate mechanism (critical section, mutex, or atomic operation) for that context. The blog post "Scorchers, Part 3: Bare-Metal Concurrency With Double-Buffering and the Revolving Fireplace" illustrates how subtle the concurrency issues around event-driven, callback-style designs can be even without an RTOS.
On bare-metal targets, the callback pattern also underlies cooperative state machine architectures and superloop designs. Rather than blocking and polling, a component registers a callback and returns immediately, allowing other work to proceed. The blog post "You Don't Need an RTOS (Part 3)" discusses this kind of event-driven bare-metal architecture, where callbacks serve a similar scheduling role to RTOS notifications.
Frequently asked
What is the difference between a callback and an interrupt service routine?
An
ISR is invoked directly by hardware via the interrupt controller and has strict constraints (no blocking, limited
stack,
latency-sensitive). A callback is a software-level concept: a function pointer called by other code. A callback can be invoked from an ISR, from an RTOS task, or from a bare-metal main loop — the term describes the invocation pattern, not the execution context.
Can a callback block or call RTOS APIs?
It depends entirely on the execution context. A callback invoked from an
ISR must not block and must use ISR-safe API variants (e.g.,
FreeRTOS `xSemaphoreGiveFromISR`). A callback invoked from an RTOS task context can generally use blocking calls, subject to the usual
deadlock and priority-inversion concerns. Always check the documentation of the API registering the callback to understand what context it fires in.
How do you pass state to a callback in C when there is no closure support?
The common pattern is to pair the function pointer with a `void *user_data` (or `void *context`) parameter. The registering code passes a pointer to its state struct as `user_data`; the framework passes it back as an argument when invoking the callback. The callback casts it back to the known type. This is the pattern used in
FreeRTOS timer callbacks (`pvTimerGetTimerID`), lwIP callbacks, and most embedded middleware.
What is a deferred callback or deferred interrupt processing, and why is it used?
Rather than executing significant work inside an
ISR-context callback, a common pattern is to post a message or signal from the ISR and handle the real work in a task-context callback. In
FreeRTOS this is often done with `xTimerPendFunctionCallFromISR`, which queues a function to be called from the timer daemon task. This keeps ISR
latency short and allows the deferred handler to use blocking APIs.
Are callbacks and event-driven programming the same thing?
Callbacks are the mechanism; event-driven programming is an architectural style. Event-driven designs rely heavily on callbacks — components register handlers for events and return control immediately rather than polling or blocking. However, you can use callbacks in non-event-driven code (e.g., a sort function that takes a comparator callback), and event-driven systems can use other dispatch mechanisms such as
message queues or function pointer tables.
Differentiators vs similar concepts
Callbacks are sometimes conflated with hooks or event handlers, which are largely synonyms in embedded middleware. A common convention distinguishes a callback (invoked once per triggering event, often registered per-operation) from a hook (typically a persistent, global override point registered once at system init), but many frameworks do not maintain this distinction and use the terms interchangeably — it is a convention, not a hard rule. Callbacks also differ from polling: polling has the caller repeatedly check for a condition, while a callback inverts control so the callee notifies the caller.