嵌入式软件分层
软件分层的核心目的是降低开发者的心智负担。当我们面对一个复杂系统时,不需要同时思考从硬件寄存器到业务逻辑的所有细节,而是聚焦于当前层的职责,信任下层提供的抽象。
分层带来的价值:
- 可替换性:更换硬件平台时,只需修改驱动层
- 可复用性:协议层代码可以在不同项目间复用
- 可测试性:每层可以独立进行单元测试
- 协作开发:不同开发者可以并行开发不同层
常见的软件分层
AUTOSAR
AUTOSAR(汽车开放系统架构)是汽车电子领域的工业标准,提供了完整的分层规范:
┌────────────────────────────────────┐
│ Application Layer (SWC) │ 应用软件组件
├────────────────────────────────────┤
│ Runtime Environment (RTE) │ 运行时环境
├────────────────────────────────────┤
│ Services Layer (BSW) │ 基础软件服务层
├────────────────────────────────────┤
│ ECU Abstraction Layer │ ECU 抽象层
├────────────────────────────────────┤
│ MCAL │ 微控制器抽象层
├────────────────────────────────────┤
│ Hardware │ 硬件
└────────────────────────────────────┘
AUTOSAR 的优势在于标准化程度高,有完整的规范文档和工具链支持。但对于中小型嵌入式项目来说过于复杂。
OSI 七层模型
OSI 模型是网络通信的经典参照,专注于数据在网络中的传输:
┌──────────────────┐
│ 应用层 │ HTTP, FTP, MQTT
├──────────────────┤
│ 表示层 │ 数据格式转换、加密
├──────────────────┤
│ 会话层 │ 连接管理
├──────────────────┤
│ 传输层 │ TCP, UDP
├──────────────────┤
│ 网络层 │ IP 路由
├──────────────────┤
│ 数据链路层 │ 帧封装、MAC
├──────────────────┤
│ 物理层 │ 比特流传输
└──────────────────┘
OSI 模型的价值在于清晰定义了每层的职责边界,但它只覆盖"通信"这一个维度,不涉及业务逻辑和系统服务。
面向业务数据流的六层模型
结合 AUTOSAR 的分层思想和 OSI 的通信模型,针对中小型嵌入式项目提出简化的六层架构。核心关注点是业务数据如何从硬件一层层流向应用。
分层
┌─────────────────────────────────────────────────────────────────┐
│ L6 应用层 │ 状态机 │ 业务逻辑 │ UI 交互 │
├─────────────────────────────────────────────────────────────────┤
│ L5 服务层 │ 日志 │ 配置存储 │ 定时任务 │ Shell │
├─────────────────────────────────────────────────────────────────┤
│ L4 协议层 │ Modbus │ 私有协议 │ Protobuf │ JSON │
├─────────────────────────────────────────────────────────────────┤
│ L3 传输层 │ TCP/UDP │ 可靠传输 │ 分包重组 │
├─────────────────────────────────────────────────────────────────┤
│ L2 链路层 │ UART帧 │ CAN帧 │ 以太网帧 │
├─────────────────────────────────────────────────────────────────┤
│ L1 驱动层 │ HAL │ 外设驱动 │ DMA │ 中断 │
└─────────────────────────────────────────────────────────────────┘
各层详解
L1 驱动层 (Driver/HAL)
职责:屏蔽硬件差异,提供统一的外设操作接口
向上提供:字节级别的读写能力
| 经典参照: | 项目 | 特点 |
|---|---|---|
| STM32 HAL | ST 官方库,接口统一,易于移植 | |
| CMSIS-Driver | ARM 标准驱动接口规范 | |
| RT-Thread Device | 统一设备框架 rt_device_* |
|
| ESP-IDF Driver | 乐鑫官方驱动层 |
L2 链路层 (Link)
职责:在字节流上建立帧结构,处理帧定界、校验、转义
从下层获取:连续的字节流 向上提供:完整的、经过校验的数据帧
| 经典参照: | 协议 | 帧格式 | 适用场景 |
|---|---|---|---|
| HDLC | 标志位 + 转义 + CRC | 通用串口通信 | |
| CAN 帧 | 硬件级帧结构 | CAN 总线通信 |
L3 传输层 (Transport)
职责:提供可靠传输、分包重组、流量控制
从下层获取:单个数据帧(可能丢失、乱序) 向上提供:可靠的、有序的数据流
| 经典参照: | 协议 | 特点 | 适用场景 |
|---|---|---|---|
| TCP | 完整可靠传输 | 网络通信 | |
| Xmodem | 停等协议 | 串口文件传输 | |
| CAN-TP (ISO 15765) | CAN 分包传输 | 汽车诊断 | |
| 自定义 ACK 机制 | 简单重传 | 资源受限场景 | |
| Modbus | 寄存器读写 | 工业控制 |
L4 协议层 (Protocol)
职责:定义数据语义、命令格式、业务报文结构
从下层获取:可靠的字节流 向上提供:结构化的命令和数据
| 经典参照: | 协议 | 模式 | 适用场景 |
|---|---|---|---|
| Protobuf | 二进制序列化 | 高效数据交换 | |
| JSON | 文本格式 | 调试友好 | |
| 私有 TLV | Tag-Length-Value | 自定义协议 |
L5 服务层 (Service)
职责:提供系统级公共服务,与具体业务逻辑解耦
特点:服务层是"横向"的,为所有其他层提供支撑
| 典型服务: | 服务 | 经典实现 | 说明 |
|---|---|---|---|
| 日志 | EasyLogger, RTT | 调试和追踪 | |
| 配置存储 | NVS, LittleFS | 参数持久化 | |
| 定时任务 | 软件定时器 | 周期性任务调度 | |
| Shell | letter-shell, finsh | 命令行交互 | |
| OTA | MCUboot | 固件升级 |
L6 应用层 (Application)
职责:实现具体的业务逻辑
从下层获取:结构化的命令和数据 输出:业务动作、状态变更、响应数据
| 经典模式: | 模式 | 说明 | 适用场景 |
|---|---|---|---|
| 状态机 | 有限状态自动机 | 设备控制流程 | |
| 事件驱动 | 消息队列 + 事件处理 | 异步响应场景 | |
| 命令模式 | 命令表 + 处理函数 | 请求-响应场景 |
各层软件设计复用
实现层间灵活配置的关键在于选择合适的工具和框架。以下按配置灵活度分类整理。
L1 驱动层配置工具
| 工具 | 类型 | 说明 |
|---|---|---|
| STM32CubeMX | 图形化配置 | 生成 HAL 初始化代码,引脚/时钟/外设可视化配置 |
| MCUXpresso Config | 图形化配置 | NXP 官方,类似 CubeMX |
L2-L3 通信栈
| 工具/库 | 覆盖层级 | 配置方式 | 说明 |
|---|---|---|---|
| lwIP | L2-L4 | lwipopts.h 宏配置 | 轻量 TCP/IP 栈,可深度裁剪 |
| ringbuffer | L2 | 代码抽象 | 环形缓冲区,驱动与链路层之间的数据缓存 |
| kfifo (Linux) | L2 | 代码抽象 | 内核无锁环形队列,可移植到裸机 |
| lwrb | L2 | 代码抽象 | 轻量级 ring buffer,支持 DMA |
L4 协议层工具
| 工具 | 类型 | 配置方式 | 说明 |
|---|---|---|---|
| nanopb | Protobuf | .proto → C 代码 | 嵌入式 Protobuf,编译时生成 |
| cJSON / jsmn | JSON | 代码抽象 | 轻量 JSON 解析器 |
| MessagePack | 二进制序列化 | 代码抽象 | 比 JSON 更紧凑 |
| FreeModbus | Modbus | 编译时配置 | 嵌入式 Modbus 从站 |
L5 服务层组件
| 组件 | 服务类型 | 配置方式 | 说明 |
|---|---|---|---|
| EasyLogger | 日志 | 宏配置 | 轻量,支持多后端 |
| Segger RTT | 日志 | 编译时 | 高速调试通道 |
| LittleFS | 文件系统 | 编译时配置 | 掉电安全,磨损均衡 |
| SPIFFS | 文件系统 | 编译时配置 | SPI Flash 专用 |
| NVS (ESP-IDF) | 键值存储 | 运行时 | 参数持久化 |
| letter-shell | Shell | 宏配置 | 轻量命令行 |
| finsh (RT-Thread) | Shell | 运行时 | 支持 C 表达式 |
L6 应用层框架
| 框架 | 类型 | 配置方式 | 说明 |
|---|---|---|---|
| QP/C | 状态机 | 代码生成 (QM) | 层次状态机,活动对象模式 |
| stateMachine | 状态机 | 代码抽象 | 轻量有限状态机,表驱动 |
| libevent | 事件管理 | 运行时 | 事件循环,回调驱动 |
| FreeRTOS 事件组 | 事件管理 | 运行时 | RTOS 内置事件标志 |
| 命令分发表 | 命令模式 | 代码抽象 | cmd_id → handler 映射表 |
灵活度等级
每层的实现可以在不同灵活度之间选择,根据项目需求做取舍:
最灵活 ◄──────────────────────────────────────────► 最精简
动态配置 静态可配置 代码抽象 硬编码
(运行时切换) (编译时选择) (回调/函数指针) (直接调用)
│ │ │ │
▼ ▼ ▼ ▼
外部指令配置 menuconfig 接口结构体 内联代码
选择依据:
- 资源受限 → 倾向硬编码,减少间接调用开销
- 需要灵活 → 倾向动态配置,支持运行时调整
- 团队协作 → 倾向代码抽象,接口清晰便于分工