欢迎光临
我们一直在努力

嵌入式软件架构设计:分层思想、设计规范与开源实践

📌 本文目标:给出一套通用的嵌入式软件分层架构模板,讲清楚每一层的职责边界、设计原则和模块间通信方式。

📌 参考项目:RT-Thread — 国内最优秀的嵌入式开源项目之一,其设备驱动框架和分层设计堪称教科书级别。

📌 适用范围:MCU 裸机 / RTOS / 嵌入式 Linux 应用层均可参考此思想。


一、为什么需要架构设计?

大多数嵌入式项目的死法不是"做不出来",而是改不动。

没有架构的项目有架构的项目
换一颗芯片,整个项目重写 只需重写 HAL 层,上层不动
加一个功能,到处改代码 新增一个模块,注册进框架即可
只有写的人能看懂 新人看目录结构就能理解系统
一个 Bug 引发连锁崩溃 层间隔离,故障不扩散

架构的本质是"分离关注点":让每一层只关心自己该做的事,通过约定好的接口与其他层交互。


二、通用分层架构总览

┌──────────────────────────────────────────────────┐
│ Application Layer │ ← 业务逻辑(状态机、策略、流程)
├──────────────────────────────────────────────────┤
│ Service Layer │ ← 功能组件(日志、配置、OTA、通信协议)
├──────────────────────────────────────────────────┤
│ OS Abstraction Layer (OSAL) │ ← 操作系统抽象(裸机/RTOS 可切换)
├──────────────────────────────────────────────────┤
│ Driver Layer │ ← 设备驱动(面向功能的抽象接口)
├──────────────────────────────────────────────────┤
│ Hardware Abstraction Layer (HAL) │ ← 寄存器操作(面向硬件的直接封装)
├──────────────────────────────────────────────────┤
│ Hardware (PCB) │ ← 物理硬件
└──────────────────────────────────────────────────┘

核心原则:上层可以调用下层,下层绝不依赖上层。


三、各层详解

3.1 Hardware Abstraction Layer(HAL 层)

职责: 屏蔽芯片差异,封装寄存器操作为统一接口。

设计要求:

  • 一个外设对应一组接口(hal_uart.h、hal_spi.h、hal_gpio.h)
  • 接口只暴露"做什么",不暴露"怎么做"
  • 换芯片时,只需重新实现这一层

示例接口定义:

/* hal_uart.h — 所有平台共用这套接口声明 */
typedef struct {
uint32_t baudrate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
} hal_uart_config_t;

int hal_uart_init(uint8_t port, const hal_uart_config_t *config);
int hal_uart_send(uint8_t port, const uint8_t *data, uint16_t len, uint32_t timeout_ms);
int hal_uart_recv(uint8_t port, uint8_t *buf, uint16_t len, uint32_t timeout_ms);
int hal_uart_deinit(uint8_t port);

📖 RT-Thread 中的对应设计: rt-thread/components/drivers/ 下的 serial 框架,通过 struct rt_uart_ops 将底层操作抽象为函数指针表,上层只调用 rt_device_read/write,完全不感知硬件差异。


3.2 Driver Layer(驱动层)

职责: 基于 HAL,封装具体外设设备的完整驱动逻辑。

与 HAL 的区别:

HAL 层Driver 层
“往 SPI 发一帧数据” “读取 W25Q128 Flash 的第 N 页”
“拉高 GPIO” “点亮 LED3”
“I2C 写寄存器” “设置 MPU6050 采样率为 1KHz”

设计要点:

  • 一个设备一个模块(drv_w25qxx.c、drv_mpu6050.c)
  • 对上提供功能接口,而非硬件接口
  • 内部维护设备状态(是否初始化、是否忙碌)
  • /* drv_w25qxx.h — 对上层暴露的是"Flash 操作"而不是"SPI 操作" */
    int w25qxx_init(void);
    int w25qxx_read(uint32_t addr, uint8_t *buf, uint32_t len);
    int w25qxx_write(uint32_t addr, const uint8_t *data, uint32_t len);
    int w25qxx_erase_sector(uint32_t sector_addr);


    3.3 OS Abstraction Layer(OSAL 系统抽象层)

    职责: 隔离 RTOS 差异。今天用 FreeRTOS,明天换 RT-Thread,上层代码不用动。

    为什么需要这一层?

    很多项目直接在业务代码里写 xSemaphoreGive()、osDelay(),一旦换 OS 就要全局替换。OSAL 解决这个问题。

    /* osal.h — 统一接口 */
    osal_mutex_t osal_mutex_create(void);
    int osal_mutex_lock(osal_mutex_t mutex, uint32_t timeout_ms);
    int osal_mutex_unlock(osal_mutex_t mutex);

    osal_thread_t osal_thread_create(const char *name, void (*entry)(void*),
    void *arg, uint32_t stack_size, uint8_t priority);

    void osal_delay_ms(uint32_t ms);

    💡 裸机环境下,osal_delay_ms 实现为阻塞延时,osal_mutex_lock 实现为关中断;RTOS 环境下切换为对应 API。同一套业务代码,不改一行就能在裸机和 RTOS 间切换。


    3.4 Service Layer(服务/中间件层)

    职责: 提供通用功能组件,为应用层提供"开箱即用"的能力。

    常见组件:

    ┌─────────────────────────────────────────────────────┐
    │ Service Layer │
    │ │
    │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
    │ │ Log │ │ Config │ │ OTA │ │ CLI │ │
    │ │ 日志 │ │ 参数管理│ │ 远程升级│ │ 命令行 │ │
    │ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
    │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
    │ │ MQTT │ │ Modbus │ │ CJSON │ │ FOTA │ │
    │ │ 客户端 │ │ 协议栈 │ │ 解析器 │ │ 差分升级│ │
    │ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
    └─────────────────────────────────────────────────────┘

    设计要点:

    • 每个组件独立编译、独立测试
    • 组件之间不直接调用,通过事件/回调通信
    • 可裁剪:不需要的组件删掉,不影响其他部分

    📖 RT-Thread 中的对应设计: rt-thread/components/ 目录,每个组件(finsh、dfs、net、at)都是独立 package,通过 menuconfig 按需开启。


    3.5 Application Layer(应用层)

    职责: 纯业务逻辑,不关心硬件、不关心 OS、不关心通信细节。

    设计要点:

    • 以状态机或事件驱动方式组织流程
    • 只调用 Service 层和 Driver 层的接口
    • 所有硬件相关的"脏活"已被下层屏蔽

    /* app_main.c — 应用层只关心"做什么",不关心"怎么做" */
    void app_task_entry(void *arg)
    {
    sensor_data_t data;

    while (1) {
    /* 采集 → 处理 → 上报,全是语义化调用 */
    sensor_read(&data);
    data_process(&data);

    if (alarm_check(&data)) {
    alarm_trigger(ALARM_LEVEL_HIGH);
    }

    cloud_report(&data);
    osal_delay_ms(1000);
    }
    }


    四、层间通信机制

    各层之间如何协作?三种核心模式:

    4.1 回调函数(Callback)

    下层通知上层的标准方式。 下层不依赖上层头文件,只持有一个函数指针。

    /* 驱动层定义回调类型 */
    typedef void (*uart_rx_callback_t)(uint8_t *data, uint16_t len);

    /* 驱动层提供注册接口 */
    void uart_register_rx_callback(uart_rx_callback_t cb);

    /* 应用层注册自己的处理函数 */
    uart_register_rx_callback(my_data_handler);

    📖 RT-Thread 中大量使用此模式:设备驱动的 rx_indicate 回调、软件定时器的 timeout 回调等。

    4.2 事件/消息队列

    任务间解耦的最佳方式。 发送方不需要知道谁在接收。

    生产者(中断/驱动) → [消息队列] → 消费者(应用任务)

    4.3 发布-订阅(Pub/Sub)

    一对多通知。 一个事件发生,多个模块响应。

    [传感器模块] — publish("temp_high") –> [报警模块]
    –> [日志模块]
    –> [云端上报模块]


    五、目录结构规范

    一个规范的嵌入式项目,打开目录就应该能看懂架构:

    project/
    ├── app/ # 应用层
    │ ├── app_main.c
    │ ├── app_sensor.c
    │ └── app_cloud.c

    ├── service/ # 服务层(中间件)
    │ ├── mqtt/
    │ ├── logger/
    │ ├── config/
    │ └── ota/

    ├── driver/ # 驱动层
    │ ├── drv_led.c
    │ ├── drv_w25qxx.c
    │ ├── drv_esp8266.c
    │ └── drv_mpu6050.c

    ├── osal/ # OS 抽象层
    │ ├── osal.h # 统一接口
    │ ├── osal_freertos.c # FreeRTOS 实现
    │ └── osal_bare.c # 裸机实现

    ├── hal/ # 硬件抽象层
    │ ├── stm32f4/ # 具体芯片实现
    │ │ ├── hal_uart.c
    │ │ ├── hal_spi.c
    │ │ └── hal_gpio.c
    │ └── hal_def.h # 统一类型定义

    ├── lib/ # 第三方库(纯算法、不依赖硬件)
    │ ├── cjson/
    │ ├── lwrb/ # 轻量级环形缓冲区
    │ └── mbedtls/

    ├── config/ # 项目配置
    │ ├── board_config.h # 引脚定义、时钟配置
    │ └── proj_config.h # 功能裁剪开关

    ├── docs/ # 文档
    │ ├── architecture.md
    │ └── api_reference.md

    └── tools/ # 构建脚本、烧录工具
    ├── CMakeLists.txt
    └── flash.sh


    六、核心设计原则(速查表)

    原则说明反例
    单向依赖 上层调用下层,下层不 include 上层头文件 hal_uart.c 里 include app_main.h
    接口与实现分离 .h 只暴露接口,.c 隐藏实现细节 把 static 函数写进 .h
    模块内聚 一个模块只做一件事 drv_sensor.c 里面还包含数据上报逻辑
    面向接口编程 用函数指针表实现多态 用 if (chip == STM32) … else if (chip == GD32)
    防御式编程 所有外部输入都做合法性检查 直接解引用传入的指针,不判空
    零全局变量 用结构体封装模块状态,通过句柄访问 满屏 uint8_t flag1, flag2, count;

    七、推荐参考的开源项目

    🏆 首推:RT-Thread

    项目RT-Thread/rt-thread
    ⭐ Stars 10K+
    语言 C
    架构亮点 完整的分层设计:内核 → 设备框架 → 组件 → 应用
    学习重点 components/drivers/ 下的 I/O Device 框架

    为什么推荐 RT-Thread 作为架构学习范本?

  • 设备驱动框架是分层设计的极致体现:
  • 应用层: rt_device_read(dev, …)

    设备框架: struct rt_device → ops → read()

    驱动层: stm32_uart_read() ← 具体芯片实现

    HAL 层: STM32 HAL_UART_Receive()

    无论底层是 STM32、GD32 还是 RISC-V,应用层调用的都是 rt_device_read——这就是分层的威力。

  • 组件化设计,按需裁剪:

    • 通过 Kconfig 机制,每个组件可独立开关
    • 不需要网络?关掉 NET 组件,编译体积直接缩小
    • 这种"可裁剪"的思想值得所有项目学习
  • 代码规范堪称教材:

    • 统一的命名前缀(rt_)
    • 完整的 Doxygen 注释
    • 每个函数都有参数校验(RT_ASSERT)
  • 其他值得参考的项目

    项目地址学什么
    EasyLogger armink/EasyLogger 如何设计一个可裁剪、可扩展的嵌入式组件
    FlashDB armink/FlashDB KV 数据库设计、Flash 磨损均衡
    letter-shell NevermindZZT/letter-shell 命令注册框架、函数指针表的工程应用
    MultiButton 0x1abin/MultiButton 事件驱动 + 状态机的经典小项目(仅 200 行)
    lwrb MaJerle/lwrb 环形缓冲区的工业级实现

    💡 学习建议: 先看 MultiButton(200行代码,2小时读完),理解事件驱动和状态机;再看 EasyLogger(中等规模),理解组件化设计;最后看 RT-Thread 设备框架(大型项目),理解完整分层架构。


    八、从"能用"到"好用":一个演进的例子

    以"串口接收数据"这件小事为例,展示架构思维如何一步步升级:

    Level 0:初学者写法

    所有逻辑堆在中断里,全局变量满天飞,无法维护。

    Level 1:中断 + 标志位

    中断里收数据存缓冲区、置标志位,主循环里判断标志位处理。耦合度高,但至少中断里不做耗时操作了。

    Level 2:环形缓冲区 + 回调

    中断里只往 Ring Buffer 塞数据,驱动层提供回调注册接口,应用层注册自己的处理函数。模块间解耦。

    Level 3:设备框架 + 消息队列

    驱动注册为"设备",应用层通过统一的 device_read 接口获取数据。中间通过消息队列异步传递,完全解耦。

    Level 3 就是 RT-Thread 的做法,也是工业级嵌入式软件的标准做法。


    九、总结:架构设计 Checklist

    开始一个新项目前,对照这张表检查:

    □ 是否划分了清晰的层次?每层职责是否单一?
    □ 换一颗芯片,需要改动的代码是否局限在 HAL 层?
    □ 加一个新功能,是否能在不改动其他模块的情况下完成?
    □ 每个模块是否可以独立编译、独立测试?
    □ 中断中是否只做"最少的事"(存数据 + 发通知)?
    □ 全局变量是否已经消灭(或封装进模块结构体)?
    □ 是否有统一的错误码定义和错误处理策略?
    □ 目录结构是否能让新人"看一眼就懂"?


    🎯 一句话总结:好的嵌入式架构 = 分层隔离 + 接口抽象 + 事件驱动 + 可裁剪。

    看一遍 RT-Thread 的 components/drivers/ 目录,胜读十篇架构文章。


    推荐阅读顺序:MultiButton(入门) → EasyLogger(进阶) → RT-Thread Device Framework(精通)

    赞(0)
    未经允许不得转载:171主机测评 » 嵌入式软件架构设计:分层思想、设计规范与开源实践
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址