本文目标给出一套通用的嵌入式软件分层架构模板讲清楚每一层的职责边界、设计原则和模块间通信方式。 参考项目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 LayerHAL 层职责屏蔽芯片差异封装寄存器操作为统一接口。设计要求一个外设对应一组接口hal_uart.h、hal_spi.h、hal_gpio.h接口只暴露做什么不暴露怎么做换芯片时只需重新实现这一层示例接口定义/* hal_uart.h — 所有平台共用这套接口声明 */typedefstruct{uint32_tbaudrate;uint8_tdata_bits;uint8_tstop_bits;uint8_tparity;}hal_uart_config_t;inthal_uart_init(uint8_tport,consthal_uart_config_t*config);inthal_uart_send(uint8_tport,constuint8_t*data,uint16_tlen,uint32_ttimeout_ms);inthal_uart_recv(uint8_tport,uint8_t*buf,uint16_tlen,uint32_ttimeout_ms);inthal_uart_deinit(uint8_tport);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 操作 */intw25qxx_init(void);intw25qxx_read(uint32_taddr,uint8_t*buf,uint32_tlen);intw25qxx_write(uint32_taddr,constuint8_t*data,uint32_tlen);intw25qxx_erase_sector(uint32_tsector_addr);3.3 OS Abstraction LayerOSAL 系统抽象层职责隔离 RTOS 差异。今天用 FreeRTOS明天换 RT-Thread上层代码不用动。为什么需要这一层很多项目直接在业务代码里写xSemaphoreGive()、osDelay()一旦换 OS 就要全局替换。OSAL 解决这个问题。/* osal.h — 统一接口 */osal_mutex_tosal_mutex_create(void);intosal_mutex_lock(osal_mutex_tmutex,uint32_ttimeout_ms);intosal_mutex_unlock(osal_mutex_tmutex);osal_thread_tosal_thread_create(constchar*name,void(*entry)(void*),void*arg,uint32_tstack_size,uint8_tpriority);voidosal_delay_ms(uint32_tms); 裸机环境下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 — 应用层只关心做什么不关心怎么做 */voidapp_task_entry(void*arg){sensor_data_tdata;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下层通知上层的标准方式。下层不依赖上层头文件只持有一个函数指针。/* 驱动层定义回调类型 */typedefvoid(*uart_rx_callback_t)(uint8_t*data,uint16_tlen);/* 驱动层提供注册接口 */voiduart_register_rx_callback(uart_rx_callback_tcb);/* 应用层注册自己的处理函数 */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里 includeapp_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⭐ Stars10K语言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其他值得参考的项目项目地址学什么EasyLoggerarmink/EasyLogger如何设计一个可裁剪、可扩展的嵌入式组件FlashDBarmink/FlashDBKV 数据库设计、Flash 磨损均衡letter-shellNevermindZZT/letter-shell命令注册框架、函数指针表的工程应用MultiButton0x1abin/MultiButton事件驱动 状态机的经典小项目仅 200 行lwrbMaJerle/lwrb环形缓冲区的工业级实现学习建议先看 MultiButton200行代码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精通