项目架构设计

选自:【单片机嵌入式程序四层架构设计思路】 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 层提供的标准接口
  • 不关心硬件、驱动、操作系统如何实现

🎯 目标:让代码清爽、易读、聚焦于“做什么”,而不是“怎么做”


🧩 二、图解说明

图中三个步骤:
  1. 获取传感器数据 → 调用 Sensor_Service_Read()
  2. 业务逻辑处理 → 判断温度是否过高
  3. 执行组件动作 → 触发蜂鸣器报警 + 记录日志

💡 所有操作都通过 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.cconfig.hmodule.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接口都要有空检查和范围判断,要把错误堵在接口处。

整体要点

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐