观察者(发布 - 订阅)模式
单片机中观察者模式及按键事件实现
一、观察者(发布 - 订阅)模式全解析
1. 核心定义
观察者模式是一种行为型设计模式,核心是定义「发布者(主题)」与「观察者」之间的一对多依赖关系:当发布者的状态发生改变时,会自动通知所有已订阅的观察者,观察者接收到通知后执行自身的更新逻辑。
这种模式的核心价值是解耦:发布者无需知道观察者的具体实现,只需依赖观察者的统一接口;观察者可动态注册 / 注销,不影响发布者的核心逻辑,模块间通过「事件通知」通信,而非直接调用。
2. 核心角色(通用模型)
| 角色 | 职责 |
|---|---|
| 发布者(主题) | 维护观察者列表,提供「注册 / 注销观察者」「通知观察者」的接口;状态变化时触发通知。 |
| 观察者 | 定义统一的「更新接口」(如update()),供发布者调用。 |
| 具体发布者 | 发布者的具体实现,维护业务状态,状态变化时调用通知逻辑。 |
| 具体观察者 | 实现观察者的更新接口,根据发布者的通知执行具体业务(如 LED 闪烁、串口打印)。 |
3. 关键特点
-
松耦合:发布者与观察者仅通过接口交互,无直接依赖;
-
动态性:观察者可在运行时注册 / 注销,灵活扩展;
-
一对多:一个发布者可对应多个观察者,状态变化时全员通知;
-
触发式:观察者被动接收通知,无需轮询发布者状态(对比「轮询模式」更高效)。
4. 典型使用场景(尤其是单片机 / 嵌入式领域)
| 场景 | 说明 |
|---|---|
| 硬件事件通知 | 按键按下 / 松开、传感器数据超限、定时器溢出时,通知多个模块响应(如 LED、蜂鸣器、串口)。 |
| 系统状态同步 | 单片机切换工作模式(休眠 / 工作 / 低功耗)时,通知显示屏、通信模块、传感器同步状态。 |
| 中断事件分发 | 外部中断(如 GPIO 中断、串口中断)触发后,通知多个业务模块处理(报警、数据记录)。 |
| 数据采集上报 | 传感器采集到有效数据后,通知数据处理模块、存储模块、无线传输模块分别处理。 |
| 故障告警扩散 | 检测到硬件故障(如电压过低)时,通知蜂鸣器报警、显示屏显示故障码、上位机上报。 |
二、单片机 C 语言实现:按键事件的发布 - 订阅
场景选择
以「按键按下事件」为例:
-
发布者:按键驱动模块(检测 KEY1 的状态,状态变化时通知观察者);
-
观察者:LED 模块(闪烁)、蜂鸣器模块(短鸣)、串口模块(打印事件日志);
-
核心逻辑:按键按下时,发布者自动通知三个观察者执行各自的响应逻辑。
实现说明
C 语言无「类 / 接口」语法,因此用结构体 + 函数指针模拟观察者接口,这是嵌入式领域实现观察者模式的常用方式。
完整代码(基于 51 单片机,普适性强)
#include <reg51.h>
#include <stdio.h>
/************************ 硬件引脚定义 ************************/
sbit KEY1 = P1^0; // 按键引脚(低电平有效)
sbit LED = P1^1; // LED引脚(高电平点亮)
sbit BEEP = P1^2; // 蜂鸣器引脚(高电平响)
/************************ 观察者接口定义 ************************/
// 观察者的更新函数指针(统一接口:无参数、无返回值)
typedef void (*ObserverUpdate)(void);
/************************ 发布者(按键主题)定义 ************************/
#define MAX_OBSERVER 5 // 最大观察者数量
// 发布者结构体:维护观察者列表、当前观察者数
typedef struct {
ObserverUpdate observers[MAX_OBSERVER]; // 观察者列表(存储更新函数指针)
unsigned char obs_count; // 当前观察者数量
} KeySubject;
/************************ 发布者核心函数 ************************/
// 1. 注册观察者(添加到列表)
void key_subject_register(KeySubject *subject, ObserverUpdate update_func) {
if (subject == NULL || update_func == NULL) return;
if (subject->obs_count >= MAX_OBSERVER) return; // 列表满则忽略
subject->observers[subject->obs_count] = update_func;
subject->obs_count++;
}
// 2. 注销观察者(从列表移除)
void key_subject_unregister(KeySubject *subject, ObserverUpdate update_func) {
if (subject == NULL || update_func == NULL) return;
unsigned char i;
for (i = 0; i < subject->obs_count; i++) {
if (subject->observers[i] == update_func) {
// 后续观察者前移,覆盖当前位置
for (; i < subject->obs_count - 1; i++) {
subject->observers[i] = subject->observers[i+1];
}
subject->obs_count--;
break;
}
}
}
// 3. 通知所有观察者(遍历列表,调用更新函数)
void key_subject_notify(KeySubject *subject) {
if (subject == NULL) return;
unsigned char i;
for (i = 0; i < subject->obs_count; i++) {
if (subject->observers[i] != NULL) {
subject->observers[i](); // 调用观察者的更新逻辑
}
}
}
/************************ 具体观察者实现 ************************/
// 观察者1:LED闪烁(按下按键后LED翻转)
void led_observer_update(void) {
LED = !LED; // LED状态翻转
}
// 观察者2:蜂鸣器短鸣(按下按键后响10ms)
void beep_observer_update(void) {
BEEP = 1; // 蜂鸣器响
// 简单延时10ms(51单片机12MHz晶振,指令周期1us)
unsigned int i;
for (i = 0; i < 10000; i++);
BEEP = 0; // 蜂鸣器停
}
// 观察者3:串口打印按键事件(需初始化串口)
void uart_observer_update(void) {
printf("KEY1 pressed! rn"); // 打印事件日志
}
/************************ 硬件初始化函数 ************************/
// 串口初始化(9600波特率,12MHz晶振)
void uart_init(void) {
TMOD |= 0x20; // 定时器1工作在模式2(8位自动重装)
TH1 = 0xFD; // 9600波特率对应的初值
TL1 = 0xFD;
TR1 = 1; // 启动定时器1
SCON = 0x50; // 串口工作在模式1,允许接收
EA = 1; // 开总中断
ES = 1; // 开串口中断(可选,此处仅打印无需中断)
}
// 按键初始化(上拉输入)
void key_init(void) {
KEY1 = 1; // 按键引脚置高(51单片机内部上拉)
}
/************************ 主函数 ************************/
void main(void) {
// 1. 硬件初始化
uart_init();
key_init();
LED = 0; // 初始LED灭
BEEP = 0; // 初始蜂鸣器停
// 2. 初始化发布者(按键主题)
KeySubject key_subject = {0}; // 初始观察者数量为0
// 3. 注册观察者(订阅按键事件)
key_subject_register(&key_subject, led_observer_update);
key_subject_register(&key_subject, beep_observer_update);
key_subject_register(&key_subject, uart_observer_update);
// 4. 主循环:检测按键状态,触发通知
unsigned char key_state = 1; // 初始按键未按下(高电平)
while (1) {
if (KEY1 == 0) { // 按键按下(低电平)
// 消抖(延时20ms)
unsigned int i;
for (i = 0; i < 20000; i++);
if (KEY1 == 0 && key_state == 1) { // 确认按键按下且状态变化
key_state = 0; // 更新按键状态
key_subject_notify(&key_subject); // 通知所有观察者
}
} else { // 按键松开
key_state = 1;
}
}
}
// 串口中断服务函数(空函数,仅为兼容printf)
void uart_isr(void) interrupt 4 {
if (RI) {
RI = 0; // 清接收中断标志
}
if (TI) {
TI = 0; // 清发送中断标志
}
}
三、代码解析与关键点
1. 核心模拟逻辑
-
观察者接口:用
ObserverUpdate函数指针模拟「统一更新接口」,所有观察者必须实现该类型的函数; -
发布者:
KeySubject结构体维护观察者列表,key_subject_register/unregister实现观察者的注册 / 注销,key_subject_notify遍历列表触发所有观察者的更新逻辑; -
解耦体现:按键驱动(发布者)无需知道 LED、蜂鸣器、串口的具体实现,只需调用
update函数;若新增一个「日志存储观察者」,只需实现update函数并注册,无需修改发布者代码。
2. 单片机适配要点
-
硬件消抖:按键检测加入 20ms 延时消抖,避免误触发;
-
资源限制:
MAX_OBSERVER限制观察者数量,适配单片机的内存(51 单片机 RAM 有限); -
无动态内存:嵌入式中避免
malloc/free,用静态数组存储观察者列表,更稳定。
3. 扩展场景
若需支持「按键松开事件」,只需在发布者中增加状态区分(如key_event枚举),并修改观察者接口为void (*ObserverUpdate)(unsigned char event),传递事件类型即可。
四、总结
观察者模式在单片机开发中是解决「多模块响应同一事件」的最优方案之一:
-
对比「轮询模式」:观察者被动接收通知,无需各模块轮询按键状态,节省 CPU 资源;
-
对比「直接调用」:模块间无硬编码依赖,新增 / 移除响应逻辑只需注册 / 注销观察者,符合「开闭原则」;
-
典型适配场景:中断事件分发、传感器数据上报、系统状态同步,是嵌入式模块化开发的核心模式之一。

浙公网安备 33010602011771号