观察者(发布 - 订阅)模式

单片机中观察者模式及按键事件实现

一、观察者(发布 - 订阅)模式全解析

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 资源;

  • 对比「直接调用」:模块间无硬编码依赖,新增 / 移除响应逻辑只需注册 / 注销观察者,符合「开闭原则」;

  • 典型适配场景:中断事件分发、传感器数据上报、系统状态同步,是嵌入式模块化开发的核心模式之一。

posted @ 2025-12-10 13:07  Afangdong  阅读(73)  评论(0)    收藏  举报