嵌入式架构分层设计低耦合高内聚项目落地实战工程 - 第2节
2. 嵌入式系统基础回顾
做嵌入式开发这些年,我见过太多项目从一个人写死循环开始,最后变成一团乱麻。说白了,架构就是给代码画个边界,让每个人知道自己该待在哪。
今天咱们把嵌入式软件架构的三种主流形态——裸机、RTOS、Linux——放在一起看看。你想想看,这三种架构本质上都是在解决同一个问题:怎么把硬件操作和业务逻辑分开。
裸机架构:最原始,也最直接
裸机架构就是没有操作系统,一个while(1)跑到底。我刚开始做项目时就是这么干的,一个main函数里塞了传感器读取、数据处理、显示刷新……后来加需求加到自己都看不懂。
裸机架构的核心特征:
- 所有代码在同一个循环中顺序执行
- 中断服务程序处理紧急事件
- 没有任务调度,全靠开发者手动安排时序
- 资源占用极低,适合MCU资源受限的场景
裸机架构的分层其实很薄,通常就两层:硬件抽象层(HAL)和应用层。HAL封装寄存器操作,应用层直接调用HAL接口。嗯,这里要注意,裸机架构最怕的就是中断嵌套和临界区保护,我曾经在一个电机控制项目里因为中断优先级没配好,导致电机在高速运行时突然抖动——排查了整整两天。
RTOS架构:给任务装上调度器
当项目里同时要处理按键、显示、通信、控制时,裸机就有点力不从心了。RTOS(实时操作系统)引入了任务、信号量、消息队列这些概念,说白了就是把CPU时间切成片,分给不同的任务。
| 特性 | 裸机 | RTOS |
|---|---|---|
| 任务管理 | 手动轮询 | 优先级调度 |
| 资源隔离 | 弱 | 强(通过互斥量) |
| 实时性 | 依赖中断 | 可预测 |
| 代码复杂度 | 低 | 中 |
| 典型场景 | 简单传感器 | 工业控制、无人机 |
我个人习惯在RTOS项目里至少分三层:硬件抽象层(HAL)、操作系统抽象层(OSAL)、应用层(APP)。OSAL这层特别重要,它把RTOS的API再包一层,这样以后换RTOS时,应用层代码基本不用动。
避坑指南:我曾经在一个项目里直接用了FreeRTOS的原生API,后来客户要求换成RT-Thread,结果应用层代码改了一大半。从那以后,我所有RTOS项目都加一层OSAL。
Linux架构:复杂系统的终极选择
当硬件资源足够(通常需要MMU、大容量RAM/Flash),且需要网络、文件系统、多进程时,Linux就登场了。Linux的分层更清晰:内核空间和用户空间完全隔离,驱动在内核态,应用在用户态。
Linux嵌入式架构通常分四层:
- 硬件层:芯片、外设、总线
- 内核层:驱动、文件系统、网络协议栈
- 系统服务层:udev、dbus、systemd
- 应用层:业务逻辑、UI、通信协议
你想想看,Linux的优势在于生态——驱动不用自己写,协议栈现成的,调试工具链成熟。但代价是启动时间、实时性、资源占用都比RTOS大。
注意:Linux不适合硬实时场景。如果要求响应时间在微秒级,还是得用RTOS或者裸机+中断。我见过有人硬要在Linux上做电机控制,结果因为调度延迟导致电机过冲——这不是Linux的错,是选型没想清楚。
经典三层架构:HAL、OSAL、APP
不管用裸机、RTOS还是Linux,我个人认为最实用的分层方案就是三层架构。它不复杂,但足够应对大多数嵌入式项目。
HAL层:把硬件差异关进笼子里
HAL(Hardware Abstraction Layer)是离硬件最近的一层。它的职责就一个:把芯片厂商提供的寄存器操作封装成统一的接口。
举个例子,同样是点亮一个LED,STM32和GD32的GPIO寄存器地址不一样。但有了HAL,应用层调用的都是HAL_GPIO_WritePin(),底层差异被屏蔽了。
// HAL层接口示例
typedef struct {
void (*init)(void);
void (*write)(uint8_t pin, uint8_t level);
uint8_t (*read)(uint8_t pin);
} GPIO_Driver_t;
// 具体芯片实现
static void stm32_gpio_init(void) {
// STM32特有的寄存器配置
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER |= (1 << 10); // PA5输出
}
static void gd32_gpio_init(void) {
// GD32特有的寄存器配置
RCU->AHBEN |= RCU_AHBEN_GPIOAEN;
GPIOA->CTL |= (1 << 10); // PA5输出
}
// HAL层统一接口
const GPIO_Driver_t GPIO_Driver = {
.init = stm32_gpio_init, // 换芯片时改这里
.write = gpio_write_pin,
.read = gpio_read_pin
};
我的经验:HAL层不要做任何业务判断,只做纯粹的硬件操作。我曾经见过有人在HAL层里加延时、做状态判断,结果换芯片时全乱套了。
OSAL层:让应用不依赖具体OS
OSAL(Operating System Abstraction Layer)是三层架构里最容易被忽略、但最值钱的一层。它的作用是把RTOS或Linux的系统调用封装成一套通用接口。
你想想看,如果应用层直接调用xTaskCreate()(FreeRTOS)或pthread_create()(Linux),那换OS时得改多少代码?
| 功能 | FreeRTOS | RT-Thread | OSAL统一接口 |
|---|---|---|---|
| 创建任务 | xTaskCreate | rt_thread_create | OSAL_TaskCreate |
| 发送信号量 | xSemaphoreGive | rt_sem_release | OSAL_SemGive |
| 获取消息 | xQueueReceive | rt_mq_recv | OSAL_MsgRecv |
| 延时 | vTaskDelay | rt_thread_mdelay | OSAL_DelayMs |
// OSAL层接口示例
// 应用层只调用OSAL接口,不直接调用RTOS API
void app_task(void *param) {
while(1) {
// 等待传感器数据
OSAL_SemTake(sensor_sem, OSAL_WAIT_FOREVER);
// 处理数据
process_sensor_data();
// 发送结果
OSAL_MsgSend(result_queue, &data, sizeof(data));
// 周期性执行
OSAL_DelayMs(100);
}
}
注意:OSAL层不要做得太厚。只封装常用的系统服务(任务、信号量、消息队列、定时器、内存管理),不要试图把RTOS的所有特性都包一层。我曾经见过一个OSAL层封装了200多个接口,结果维护成本比直接调RTOS还高。
APP层:专注业务逻辑
APP层是离用户最近的一层。它的职责就是实现业务逻辑——按键怎么响应、数据怎么处理、界面怎么显示。APP层不应该知道底层用的是STM32还是GD32,也不应该知道跑的是FreeRTOS还是Linux。
说白了,APP层只调用OSAL和HAL的接口,不直接操作寄存器,不直接调用RTOS API。这样做的最大好处是:
- 可移植性:换芯片、换OS,APP层代码不用改
- 可测试性:可以写单元测试,Mock掉底层接口
- 可维护性:新人接手时,只看APP层就能理解业务
三层架构的核心原则:
- 上层只能调用相邻下层,不能跨层调用
- 同层之间通过接口通信,不直接依赖
- 每一层只做自己的事,不越界