C++ 事件总线(EventBus)实现嵌入式系统解耦
摘要:在嵌入式软件开发中,随着功能模块的增加,模块间的'相互调用'往往会导致代码耦合度指数级上升。修改 A 模块的代码,B、C、D 模块都要跟着改,牵一发而动全身。本文将抛弃传统的直接调用方式,基于 C++11 实现一个轻量级的发布 - 订阅(Publish-Subscribe)系统,彻底斩断模块间的依赖链。
一、架构之殇:为什么你的代码越来越难改?
假设我们要开发一个智能环境控制器,它有四个独立模块:
- 采集模块:读取温度传感器。
对嵌入式系统中模块耦合度高的问题,提出基于 C++11 的事件总线(EventBus)解决方案。通过发布 - 订阅模式,将采集、显示、网络等模块解耦,降低依赖。核心实现利用 std::function 和 std::map 构建通用总线,支持动态注册回调。重构后新增功能无需修改原有模块代码,显著提升了系统的可维护性和扩展性。同时文中指出了空指针风险、生命周期管理及中断上下文使用时的注意事项。
摘要:在嵌入式软件开发中,随着功能模块的增加,模块间的'相互调用'往往会导致代码耦合度指数级上升。修改 A 模块的代码,B、C、D 模块都要跟着改,牵一发而动全身。本文将抛弃传统的直接调用方式,基于 C++11 实现一个轻量级的发布 - 订阅(Publish-Subscribe)系统,彻底斩断模块间的依赖链。
假设我们要开发一个智能环境控制器,它有四个独立模块:
在传统的面向过程编程中,采集模块的代码通常长这样:
// SensorTask.cpp
#include "LcdDriver.h" // 依赖 LCD
#include "WifiModule.h" // 依赖 WiFi
#include "FanControl.h" // 依赖 风扇
void Sensor_Read_Loop() {
float temp = HAL_ADC_Read(); // 噩梦的开始:采集者必须知道所有消费者的存在
LCD_ShowTemp(temp); // 直接调用
WiFi_Upload(temp); // 直接调用
if (temp > 30) Fan_On(); // 直接调用
}
SensorTask.cpp,重新编译采集模块。这违背了'开闭原则'(对扩展开放,对修改关闭)。我们要引入一个中间人(Broker)。
结果:采集者根本不知道显示器、WiFi 的存在。大家老死不相往来,却配合得天衣无缝。
为了适配嵌入式资源,我们不使用复杂的模板元编程,而是利用 C++11 的 std::function 和 std::map 实现一个通用版本。
EventBus.h)#pragma once
#include <vector>
#include <map>
#include <functional>
// 1. 定义事件类型(这是唯一的公共依赖)
enum class EventID {
TEMP_UPDATED, // 温度更新
WIFI_CONNECTED, // WiFi 连上
BUTTON_PRESSED, // 按键按下
ERROR_OCCURED // 系统故障
};
// 2. 定义回调函数原型:接收一个 void* 指针,允许传输任意数据
using EventCallback = std::function<void(void* payload)>;
class EventBus {
public:
// 单例模式:全局只有一个总线
static EventBus& Get() {
static EventBus instance;
return instance;
}
// [订阅]:谁关心某个事件,就来登记
void Subscribe(EventID id, EventCallback cb) {
subscribers[id].push_back(cb);
}
// [发布]:发生什么事了,通知大家
void Publish(EventID id, void* payload = nullptr) {
auto it = subscribers.find(id);
if (it != subscribers.end()) {
for (auto& callback : it->second) {
if (callback) callback(payload);
}
}
}
private:
// 存储结构:EventID -> 回调函数列表
std::map<EventID, std::vector<EventCallback>> subscribers;
EventBus() = default; // 私有构造
};
让我们看看重构后的代码是如何工作的。
为了安全传输,我们定义传输的数据结构。
// AppData.h
struct TempData {
float temperature;
float humidity;
};
注意: 这里没有任何 include "Lcd.h" 或 include "Wifi.h"。它只依赖 EventBus。
#include "EventBus.h"
#include "AppData.h"
void Sensor_Loop() {
// 1. 获取硬件数据
TempData currentData;
currentData.temperature = 35.5f;
currentData.humidity = 60.0f;
// 2. 广播事件:我任务完成了,剩下的你们随意
// 就像发朋友圈一样,不指定谁看
EventBus::Get().Publish(EventID::TEMP_UPDATED, ¤tData);
}
#include "EventBus.h"
#include "AppData.h"
class LcdModule {
public:
void Init() {
// 注册监听:当温度更新时,自动调用 UpdateScreen
EventBus::Get().Subscribe(EventID::TEMP_UPDATED, [this](void* data) {
this->UpdateScreen(data);
});
}
private:
void UpdateScreen(void* rawData) {
// 1. 数据还原(Type Casting)
TempData* data = static_cast<TempData*>(rawData);
// 2. 业务逻辑
printf("LCD Display: Temp = %.1f\n", data->temperature);
}
};
完全独立的另一个文件,逻辑互不干扰。
void InitFanControl() {
EventBus::Get().Subscribe(EventID::TEMP_UPDATED, [](void* rawData) {
TempData* data = static_cast<TempData*>(rawData);
if (data->temperature > 30.0f) {
HAL_GPIO_WritePin(FAN_PORT, FAN_PIN, 1); // 开风扇
}
});
}
需求:老板突然要求,温度过高时,蜂鸣器也要响。
旧架构做法:打开 SensorTask.cpp,添加蜂鸣器头文件,修改采集逻辑,重新测试采集功能(容易引入 Bug)。
新架构做法:
BuzzerTask.cpp。TEMP_UPDATED 事件。SensorTask.cpp 一个字都不用改! 这就是架构设计的魅力。
在嵌入式环境使用 EventBus,有三个关键点必须注意:
void* payload 提供了灵活性,但也带来了风险。
TEMP_UPDATED 事件携带的数据类型必须是 struct TempData。std::any (C++17) 或自定义的 Variant 类来增强类型安全。Publish 传递的是指针。
Publish 函数返回前,所有订阅者都已经处理完数据(同步调用模式,本文采用的就是这种)。std::map 和 std::vector 的内存分配不是重入安全的。Subscribe。Publish,需确保总线实现不涉及动态内存分配(Pre-allocated),或者仅在 ISR 中置标志位,在主循环中分发事件。代码的耦合度是衡量架构质量的核心指标。
通过引入 EventBus,我们将 N 对 N 的网状依赖,转化为了 1 对 N 的星型依赖。这不仅让代码逻辑更加清晰,更极大地提升了系统的可测试性和可维护性。
对于 STM32 这样的单片机项目,只要合理控制内存,C++11 的这一特性绝对是提升代码逼格和质量的神器。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online