Hello everyone, I am the Mixed Content Master.
This time we share a commonly used concept/programming model in embedded systems—the Observer Pattern.
Overview of the Observer Pattern
The Observer Pattern is a behavioral design pattern that focuses on establishing a dynamic subscription-notification mechanism between objects.
It defines a one-to-many dependency between objects, so that when the state of one object (the subject, also known as the Subject) changes, all objects that depend on it (the observers) are notified and updated automatically.
In embedded systems, the Observer Pattern is widely used to decouple event publishers from subscribers, making it particularly suitable for the following application scenarios:
- Handling sensor data updates
- Changes in hardware status
- Collaboration among multiple modules
Embedded Application Scenarios
1. Sensor Data Distribution
Multiple modules (such as display, storage, alarm) need to receive real-time updates of sensor data changes. The Observer Pattern can treat the sensor as the subject (Subject) and each module as an observer (Observer), achieving automatic notifications when data updates occur.
Class Diagram:

Subject Class (SensorSubject):
-
Contains a list of observers (observers array)
-
Maintains the current sensor value (sensor_value)
-
Provides two key methods: attach and set_value
Observer Interface (ObserverCallback):
- Defines a unified update interface
- Corresponds to function pointer types in the code
Concrete Observer Classes:
- DisplayObserver: Handles display updates
- LoggerObserver: Handles logging
- AlarmObserver: Handles threshold alarms
Code:
#include<stdio.h>
#define OBSERVER_MAX_NUM 5
// Observer callback function type
typedef void(*ObserverCallback)(int value);
// Subject (Observed)
typedef struct
{
ObserverCallback observers[OBSERVER_MAX_NUM];
int count;
int sensor_value;
} SensorSubject;
// Attach observer to subject
void sensor_attach(SensorSubject* subject, ObserverCallback callback)
{
if (!subject || !callback)
{
printf("Invalid parameters!\n");
return;
}
if (subject->count >= OBSERVER_MAX_NUM)
{
printf("Observers full!\n");
return;
}
subject->observers[subject->count++] = callback;
}
// Update sensor value and notify observers
void sensor_set_value(SensorSubject* subject, int value)
{
if (!subject)
{
printf("Invalid parameters!\n");
return;
}
subject->sensor_value = value;
// Notify all observers
for (int i = 0; i < subject->count; ++i)
{
if (subject->observers[i])
{
subject->observers[i](subject->sensor_value);
}
}
}
// Observer 1: Display module
void display_update(int value)
{
printf("[Display] Value: %d\n", value);
}
// Observer 2: Logger module
void logger_update(int value)
{
printf("[Logger] Value: %d\n", value);
}
// Observer 3: Alarm module
void alarm_update(int value)
{
if (value > 100)
{
printf("[Alarm] Value %d exceeds limit!\n", value);
}
}
int main(void)
{
// Initialize sensor subject
SensorSubject sensor =
{
.observers = {0},
.count = 0,
.sensor_value = 0
};
// Register observers
sensor_attach(&sensor, display_update);
sensor_attach(&sensor, logger_update);
sensor_attach(&sensor, alarm_update);
// Simulate sensor data updates
sensor_set_value(&sensor, 25);
sensor_set_value(&sensor, 120);
return 0;
}

This example allows objects (display, logging, alarm modules) to subscribe to another object (sensor), and when the subject’s state changes, all observers are automatically notified.
Note: This example is only to illustrate the basic idea of the Observer Pattern. It implements the core functionality of the Observer Pattern in a single-threaded environment. For practical applications, additional thread safety mechanisms, dynamic memory management, and more comprehensive error handling are needed.
2. Zephyr Sensor Subsystem
In Zephyr, the sensor subsystem uses a mechanism similar to the Observer Pattern. The sensor driver acts as the subject, and when sensor data is updated, it triggers the corresponding events.
Applications can register as observers to listen for these events and process them when data updates occur.
#include<zephyr.h>
#include<device.h>
#include<devicetree.h>
#include<drivers/sensor.h>
// Sensor event handling function, as the observer's update method
static void sensor_callback(const struct device *dev, struct sensor_trigger *trig)
{
struct sensor_value temp;
if (sensor_sample_fetch(dev) < 0) {
return;
}
if (sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp) < 0) {
return;
}
// Process sensor data
printk("Temperature: %d.%06d\n", temp.val1, temp.val2);
}
void main(void)
{
const struct device *dev = device_get_binding(DT_LABEL(DT_INST(0, st_stts751)));
if (dev == NULL) {
return;
}
struct sensor_trigger trig = {
.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_AMBIENT_TEMP
};
// Register sensor event callback, equivalent to registering an observer
if (sensor_trigger_set(dev, &trig, sensor_callback) < 0) {
return;
}
while (1) {
k_sleep(K_MSEC(100));
}
}
Related articles: Will Zephyr Become the Leader of RTOS in the IoT Era?
3. Inter-task Communication and Synchronization Mechanisms
In RTOS, the communication and synchronization mechanisms between tasks can be likened to the Observer Pattern.
EventGroupHandle_t xEventGroup;
// Create event group
xEventGroup = xEventGroupCreate();
// Task 1 as the subject sets the event
void vTask1(void *pvParameters)
{
while(1)
{
// Set event bits
xEventGroupSetBits(xEventGroup, 0x01);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Task 2 as the observer waits for the event
void vTask2(void *pvParameters)
{
EventBits_t uxBits;
while(1)
{
// Wait for event bits
uxBits = xEventGroupWaitBits(
xEventGroup, // Event group handle
0x01, // Event bits to wait for
pdTRUE, // Clear event bits on exit
pdFALSE, // Not all bits need to be set
portMAX_DELAY // Wait indefinitely
);
// Process event
if ((uxBits & 0x01) != 0)
{
// Perform corresponding operations
}
}
}
The event group can be seen as a subject, while the tasks waiting for these events can be seen as observers.
When an event in the event group is set (state changes), the tasks waiting for that event will be awakened and execute corresponding operations, just like observers receiving notifications from the subject and updating accordingly.
Related articles: Embedded Event Flag Groups
4. MQTT
MQTT is a lightweight messaging protocol primarily used for communication between IoT devices, and it applies the principles of the Observer Pattern in its design and usage.
Its core concepts include:
- Publisher: Generates messages and publishes them to specific topics.
- Topic: A classification label for messages, used to distinguish different types of messages.
- Broker: Responsible for receiving messages from publishers and forwarding them to subscribers who have subscribed to the corresponding topics.
- Subscriber: Subscribes to one or more topics and receives messages forwarded by the broker when new messages are published to those topics.
Related articles: MQTT Applied to Inter-process Communication
That concludes this sharing session. If you find the article helpful, please share it. Thank you!
You might also like:
Embedded Programming Model | MVC Model
Step-by-step Guide to Building an Embedded Containerized Development Environment!
An Elegant Multi-functional Debugger for Embedded Systems!
A Very Lightweight Embedded Logging Library!
A Very Lightweight Embedded Thread Pool Library!
Popular C Language Projects on GitHub!
Practical | Teach You to Turn on Lights via Webpage in 10 Minutes
Essential Skills for Embedded Development | Git Submodules