嵌入式项目架构设计指南
项目架构设计
选自:【单片机嵌入式程序四层架构设计思路】 https://www.bilibili.com/video/BV19xFfzzEWL/?share_source=copy_web&vd_source=2c56c6a2645587b49d62e5b12b253dca
一、分层

第一层:BSP驱动层


嵌入式软件架构中的 第一层:BSP 驱动层(Board Support Package)
屏蔽芯片差异,让“换芯片就像换零件”
什么是 BSP?
BSP = Board Support Package(板级支持包)
它是连接 硬件 和 上层软件 的桥梁,主要职责是:
- 封装不同芯片的寄存器操作
- 实现标准接口(如 I2C_Init, SPI_Write)
- 处理时序、中断、引脚配置等底层细节
🎯 目标:让上层代码不需要知道用的是 STM32 还是 GD32
- STM32 和 GD32 是两种不同的 MCU 芯片
- 它们的 I2C 寄存器地址、位定义、时钟树配置都不一样
- 但通过 BSP 封装层,对外都提供统一的接口:
I2C_Init(); I2C_Write(addr, data);
为什么需要这一层?举个例子
❌ 错误写法(直接操作寄存器)
// 写给 STM32 的代码
STM32_I2C1->CR1 |= I2C_CR1_PE; // 启用 I2C
while(!(STM32_I2C1->SR1 & 0x01)); // 等待发送完成
👉 问题:如果换成 GD32,寄存器名字和位定义完全不同!必须重写所有代码!
✅ 正确写法(BSP 封装)
// 上层调用的标准接口
BSP_I2C_Write(I2C_PORT_1, addr, &data, 1);
而在 bsp_i2c.c 文件中处理差异:
// bsp_i2c.c
void BSP_I2C_Write(uint8_t port, uint8_t addr, uint8_t *data, uint8_t len) {
#if defined(USE_STM32)
STM32_I2C1->CR1 |= I2C_CR1_PE;
// ... STM32 特有的时序控制
#elif defined(USE_GD32)
GD32_I2C1_CTL |= I2C_CTL_EN;
// ... GD32 特有的时序控制
#endif
}
✅ 结果:换芯片只需改一个宏定义,上层代码一行不用改!
第二层:OSAL系统抽象层


OSAL = Operating System Abstraction Layer
是一层 统一接口封装层,屏蔽了底层操作系统的差异。
解耦与跨平台兼容性
目标:让上层业务代码 不依赖具体操作系统,实现“一次开发,多平台运行”。
1. FreeRTOS
- 是一个 轻量级实时操作系统(RTOS),广泛用于微控制器(MCU)。
- 提供任务调度、消息队列、信号量、定时器等功能。
- 特点:开源、免费、资源占用小、适合 IoT 设备。
2. RT-Thread
- 国产的 实时操作系统(RTOS),功能比 FreeRTOS 更丰富。
- 支持文件系统、网络协议栈、设备驱动框架等。
- 特点:中文文档好、社区活跃、适合国产化项目。
3. BareMetal(裸机)
- 指 没有操作系统 的运行环境。
- 程序直接在硬件上运行,由开发者手动管理中断、时钟、内存等。
- 优点:极致性能、最小体积;缺点:开发复杂、难以维护。
为什么需要这一层?举个例子
假设你要做一个 LED 控制任务:
❌ 错误做法:直接调用 FreeRTOS API
// 绑死 FreeRTOS
xTaskCreate(Task1, "LED", 128, NULL, 1, NULL);
👉 问题:如果以后想换成 RT-Thread,必须改所有代码!
✅ 正确做法:通过 OSAL 抽象层
// 统一接口,不管底层是什么
OSAL_TaskCreate(Task1, "LED", 128, 1);
然后在 OSAL 层内部根据配置决定调用哪个系统的 API:
// OSAL 实现层
#if defined(USE_FREERTOS)
xTaskCreate(...);
#elif defined(USE_RT_THREAD)
rt_thread_create(...);
#else // 裸机
// 手动调度或不启用任务
#endif
✅ 结果:上层业务代码一行都不用改!
第三层:Service业务组件层


把产品功能拆解成模块,后续增删模块就会简单很多。
✅ 把产品功能拆成“独立、高内聚”的模块,像搭积木一样组合业务逻辑
什么是 Service 层?
Service = 业务组件层(Business Service Layer)
它是上层应用逻辑的“积木块”,每个模块负责一个明确的功能,比如:
- 日志记录
- 传感器数据处理
- 低功耗管理
- FOTA 升级
🎯 目标:让功能可以自由插拔,互不干扰,快速复用
🧩 二、图解说明
图中四个模块:
| 模块 | 功能 |
|---|---|
| LOG | 记录系统运行日志,支持文件/串口输出 |
| SENSOR | 采集温度、湿度等传感器数据,并做滤波处理 |
| POWER | 管理休眠唤醒、电源模式切换 |
| FOTA | 实现远程固件升级(Over-The-Air) |
这些模块都通过 标准接口通信,彼此之间没有直接依赖。
为什么需要这一层?举个例子
❌ 差的写法(功能耦合在一起)
void app_main() {
// 所有功能混在一起
read_sensor();
log_data("sensor: %d", value);
if (value > 30) {
enter_low_power_mode();
}
check_fota_update();
}
👉 问题:如果要删掉日志功能,就得改所有地方;加新功能也容易出错。
✅ 正确写法(Service 模块化)
void app_main() {
log_init(); // 初始化日志模块
sensor_get_data(); // 获取传感器数据
if (need_update) fota_start(); // 启动升级
}
每个函数对应一个服务模块,调用简单清晰。
💡 四、Service 层的核心价值
| 优势 | 说明 |
|---|---|
| ✅ 自由插拔模块 | 想加就加,想删就删,不影响其他功能 |
| ✅ 拒绝“牵一发而动全身” | 修改一个模块不会影响整个系统 |
| ✅ 快速复用到新项目 | 只需复制 log_service.c 就能用在下一个产品 |
| ✅ 便于单元测试 | 每个模块可以单独测试,不依赖硬件 |
第四层:APP应用逻辑层

✅ 只写业务逻辑,不碰底层细节。像指挥官一样,调用服务模块完成任务。
什么是 APP 层?
APP = Application Layer(应用层)
它是整个系统的“大脑”,负责:
- 定义产品的核心业务流程
- 调用 Service 层提供的标准接口
- 不关心硬件、驱动、操作系统如何实现
🎯 目标:让代码清爽、易读、聚焦于“做什么”,而不是“怎么做”
🧩 二、图解说明
图中三个步骤:
- 获取传感器数据 → 调用
Sensor_Service_Read() - 业务逻辑处理 → 判断温度是否过高
- 执行组件动作 → 触发蜂鸣器报警 + 记录日志
💡 所有操作都通过 Service 层的 API 完成,完全屏蔽了底层复杂性。
⚙️ 三、为什么需要这一层?举个例子
❌ 差的写法(业务和驱动混在一起)
void app_main() {
// 直接操作硬件寄存器!
uint8_t temp = I2C_Read(0x48, 0x00);
if (temp > 45) {
GPIO_SetBit(BUZZER_PIN); // 直接控制引脚
UART_Send("Critical Temp!"); // 直接串口发送
}
}
👉 问题:如果换芯片,所有代码都要改;功能扩展困难。
✅ 正确写法(APP 层调用 Service)
void Main_App_Handler() {
int temp = Sensor_Service_Read(); // 获取传感器数据
if (temp > 45) {
Buzzer_Service_Alarm(ON); // 调用蜂鸣器服务
Log_Service_Save("Critical Temp!"); // 调用日志服务
}
}
✅ 优点:
- 代码清晰简洁
- 只关注“温度太高就报警”这个业务逻辑
- 模块间松耦合,易于维护和测试
💡 四、APP 层的核心价值
| 优势 | 说明 |
|---|---|
| ✅ 不碰底层细节 | 不需要知道 I2C 是怎么通信的,也不用管中断、定时器 |
| ✅ 像组装乐高一样写代码 | 几行代码拼出完整业务流程 |
| ✅ 只写业务逻辑 | 真正回归产品本质:“我要实现什么功能?” |
二、设计数据流和状态机
第一原则:干掉全局变量 → 引入消息总线(Message Bus)


🔍 核心思想:
❌ 避免模块间通过全局变量直接通信(导致“上帝类”、“面条代码”)
✅ 改为通过 统一的消息总线(IPC) 进行数据交换
全局变量会被多个对象反复征用读写,很容易出错,采用消息总线的方式,将所有消息封装成包,类似于ROS中的节点通信,为每个通讯对象赋予subscriber publisher的身份,互相通过统一的消息总线交换信息。
这样才能实现模块间的完全解耦。
🧩 原理图解析:
- 左边:多个模块互相调用全局变量 → 网状耦合,难以调试
- 右边:所有模块通过中间的“消息总线”通信 → 星型结构,松耦合
💡 实现方式:
typedef struct {
uint16_t msg_id;
void* data;
} Message;
// 发布消息
msg_publish(MSG_SENSOR_DATA, &temperature);
// 订阅消息
msg_subscribe(MSG_SENSOR_DATA, sensor_handler);
✅ 效果:模块之间只关心“发生了什么”,不关心“谁发的”或“怎么来的
第二原则:告别 if-else 嵌套 → 使用状态机(State Machine)

🔍 核心思想:
❌ 深层
if-else是“代码之癌”,逻辑混乱、难扩展
✅ 用 状态机 管理复杂业务流程,让逻辑清晰可控
🧩 对比图解析:
- 左边:
Nested IFs→ 代码树像一团乱麻,分支爆炸 - 右边:
State Machine→ 四个状态(IDLE → PROCESSING → SEND → WAIT_ACK),流转明确
💡 实践示例:
enum State {
IDLE,
PROCESSING,
SEND,
WAIT_ACK
};
void state_machine_handler() {
switch (current_state) {
case IDLE:
if (data_ready) {
current_state = PROCESSING;
}
break;
case PROCESSING:
// 处理数据
current_state = SEND;
break;
// ...
}
}
✅ 优点:
- 易于理解和维护
- 支持异步事件驱动
- 扩展新状态只需加一个 case
不要用if-else判断执行,而是用RTOS的优先级和信号量或者事件驱动状态机切换业务流程。
第三原则:参数统一管理 → 参数中心(Parameter Center)

🔍 核心思想:
❌ 配置散落在
main.c、config.h、module.c各处,修改时容易遗漏
✅ 建立 参数中心,统一存储、读取、校验配置项
🧩 架构图解析:
- 所有模块都通过
param_set()/param_get()接口访问参数 - 配置文件(如
config.h)加载到中心,集中管理
💡 实现方式:
// 设置参数
param_set("sensor_threshold", 25);
param_set("wifi_ssid", "MyHome");
// 获取参数
int threshold = param_get("sensor_threshold");
✅ 效果:
- 修改配置只需改一处
- 支持运行时动态更新
- 可增加校验机制(如数值范围检查)
设计一个专用参数中心,统一存储统一校验
第四原则:高可用与鲁棒性设计
这是系统的“安全底线”,确保在异常情况下仍能稳定运行。
🎯 两大核心策略:

首先要建立全生命周期的异常捕获

1. 异常捕获:现场还原“黑匣子”
⚠️ 当系统崩溃(HardFault / Crash)时,第一时间保存关键信息到 Flash
// 在 HardFault Handler 中执行
Flash_Log_Write(ERROR_STACK_DUMP);
Flash_Log_Write(REGISTERS_DUMP);
Flash_Log_Write(LOG_BUFFER);
✅ 目标:即使重启也能还原故障现场,就像飞机“黑匣子”一样
💡 实现方式:
- 使用 NVIC 的 HardFault Handler
- 将寄存器、堆栈、日志写入 Flash 的专用区域
- 可配合 OTA 升级自动上传日志
出故障时第一时间把CPU现场、日志、堆栈存入Flash中。
其次要进行防呆设计

2. 接口防呆:把错误拒之门外
⚠️ 所有 API 入口必须进行参数校验,防止非法输入引发崩溃
void api_function(void* data) {
ASSERT(data != NULL); // 判空
ASSERT(param >= 0 && param < 100); // 范围检查
// 正常逻辑...
}
✅ 效果:
- 错误在入口就被拦截
- 防止“野指针”、“越界访问”等常见问题
- 提高系统稳定性
所有API接口都要有空检查和范围判断,要把错误堵在接口处。
整体要点

更多推荐


所有评论(0)