Events and Notifications in Pico-Framework
Pico-Framework provides multiple mechanisms for inter-task communication and asynchronous signaling. These mechanisms are designed to meet the needs of embedded systems developers who require a mix of speed, flexibility, and reliability.
There are three core signaling mechanisms available in the framework:
- Task Notifications: Fast, lightweight signals delivered directly to tasks using FreeRTOS primitives.
- EventManager Events: Structured messages that support routing, payloads, and pub-sub communication between components.
- Direct GPIO Listeners: Low-latency callbacks used for fast response to GPIO changes, especially in interrupt service routines.
Choosing the right mechanism for a given use case depends on performance requirements, data complexity, and system architecture.
Overview and Feature Comparison
Feature | Task Notifications | EventManager Events | Direct GPIO Listeners |
---|---|---|---|
Payload support | ✗ | ✓ | ✓ |
One-to-one delivery | ✓ | ✓ | ✓ |
Broadcast support | ✗ | ✓ | ✗ |
ISR-safe | ✓ | ⚠ (with care) | ✓ |
Suitable for ultra-low latency | ✓ | ✗ | ✓ |
Strong type safety | Moderate | High | Moderate |
Works in task context | ✓ | ✓ | ✓ |
Works in ISR context | ✓ | ⚠ (postEventFromISR only) | ✓ |
Best for system-level signals | ✓ | ✓ | ✓ |
Best for structured application logic | ✗ | ✓ | ✗ |
⚠ Note 1: EventManager::postEventFromISR() is safe to call from ISRs, but care must be taken: the function must not allocate memory or block. Events posted this way are queued and processed later in task context.
⚠ Note 2: While the EventManager supports event injection from ISR, it is not suitable for ultra-low latency or time-critical response paths. Use direct task notifications or GPIO listeners for those cases.
Task Notifications
Task notifications are ideal for simple, low-overhead signaling between tasks. They use FreeRTOS's xTaskNotifyIndexed
function and deliver a single integer value to a specific task. Notifications are highly efficient and ISR-safe.
When to Use Task Notifications
- Notify a specific task of a state change
- No payload required (or fits in a small integer)
- Require minimal overhead
- Need ISR-safe signaling with deterministic latency
Example: Sending a Task Notification
xTaskNotifyIndexed(taskHandle, 0,
Notification::NetworkReady.code(),
eSetValueWithOverwrite);
Example: Waiting for a Task Notification
void NetworkController::run() {
waitFor(Notification::NetworkReady);
printf("Network is ready\n");
}
EventManager Events
The EventManager
enables structured communication using Event
objects. Events support routing, typed notifications, optional payloads, and source/target fields. They are delivered through task-safe queues.
This is the preferred system for decoupled component communication and for carrying structured data between producers and consumers.
Event Structure
Notification
field with kind and enum (system or user)- Optional source and target pointers
- Optional payload (pointer + size)
When to Use EventManager
- You need structured data delivery
- Multiple subscribers or targeted delivery
- Clean separation between modules
- Want to implement pub-sub or FSM-style logic
Example: Posting an Event
Event evt;
evt.source = this;
evt.target = AppContext::get<ConfigController>();
evt.notification = Notification::makeUser(UserNotification::ConfigUpdated);
evt.data = &updatedConfig;
evt.size = sizeof(updatedConfig);
EventManager::postEvent(evt);
Note: Use
postEventFromISR()
when sending events from interrupt context, and avoid dynamic memory allocations.
Example: Receiving the Event
void ConfigController::onEvent(const Event& event) {
if (event.notification.kind == NotificationKind::User &&
event.notification.user == UserNotification::ConfigUpdated) {
auto* cfg = static_cast<ConfigData*>(event.data);
applyNewConfig(*cfg);
}
}
Direct GPIO Listeners
GPIO listeners offer the fastest response to pin changes, using static callbacks that are safe to invoke from ISRs. They are ideal for simple signal processing or state changes that must be detected immediately.
Configuration
In framework_config.h
, you can enable GPIO events, notifications, or both:
#define GPIO_NOTIFICATIONS 1
#define GPIO_EVENTS 2
#define GPIO_EVENTS_AND_NOTIFICATIONS (GPIO_NOTIFICATIONS | GPIO_EVENTS)
#define GPIO_EVENT_HANDLING GPIO_EVENTS_AND_NOTIFICATIONS
Internal Handler Example
void GpioEventManager::gpio_event_handler(uint gpio, uint32_t events) {
GpioEvent gpioEvent = { static_cast<uint16_t>(gpio), static_cast<uint16_t>(events) };
#if GPIO_EVENT_HANDLING & GPIO_NOTIFICATIONS
auto it = listeners.find(gpio);
if (it != listeners.end()) {
for (auto& cb : it->second) {
cb(gpioEvent);
}
}
#endif
#if GPIO_EVENT_HANDLING & GPIO_EVENTS
Event evt(SystemNotification::GpioChange, gpioEvent, sizeof(GpioEvent));
AppContext::get<EventManager>()->postEvent(evt);
#endif
}
When to Use Direct GPIO Listeners
- Require sub-millisecond response to GPIO changes
- Working in an ISR context
- No routing or payload processing is needed
Example: Adding a Listener
gpioEventManager.addListener(pin, [](const GpioEvent& evt) {
// React immediately to pin edge
});
Notification Typing and Safety
The framework enforces clear notification domains using NotificationKind
. This protects against enum overlap and makes routing logic safe and expressive.
enum class SystemNotification {
NetworkReady = 1,
StorageMounted = 2
};
enum class UserNotification {
ProgramStarted = 1,
ProgramStopped = 2
};
Use the helper functions to construct correctly typed notifications:
Notification n1 = Notification::makeSystem(SystemNotification::NetworkReady);
Notification n2 = Notification::makeUser(UserNotification::ProgramStarted);
Internally, each notification object includes:
NotificationKind
(System/User)- Enum value (cast to uint8_t)
Summary: Choosing the Right Tool
When to Use Task Notifications
- Ultra-fast, one-to-one signals
- ISR or task context
- No payload required
- Minimal overhead
When to Use EventManager
- Structured messages with payloads
- Targeted or broadcast delivery
- Clean pub-sub or FSM patterns
- More complex application logic
When to Use Direct GPIO Listeners
- Fastest GPIO response (ISR-safe)
- No routing or structured metadata needed
- Static callback registration
Each mechanism is designed for a specific category of signaling. You can combine them cleanly within the same app. All systems are integrated with FreeRTOS and respect task-safety constraints.
Pico-Framework encourages flexibility while maintaining discipline — ensuring real-time signaling stays reliable, understandable, and well-type