2. 实时系统分层架构基础:经典三层架构

做嵌入式开发这么多年,我见过太多“一锅粥”式的代码。所有功能揉在一起,中断里调驱动,驱动里跑算法,应用层直接操作寄存器。这种代码,别说测试了,连看懂都费劲。

后来我慢慢明白,分层不是学院派的花架子。它是用来保命的。今天我们就聊聊实时系统里最经典的“三层架构”——驱动层、OS层、应用层。说白了,就是把硬件、系统、业务这三件事拆开,各管各的。

2.1 为什么非要分层?

你想想看,一个嵌入式产品从原型到量产,代码量可能翻十倍。如果所有代码都混在一起,改一个LED闪烁频率,都可能把电机控制搞崩。我早期做的一个项目,就因为驱动里顺手改了个GPIO方向,结果整个调度器挂了。查了三天。

分层的好处很直接:

  • 隔离变化:换芯片、换外设,只动驱动层,应用层不用改。
  • 方便测试:每一层都可以单独测。驱动层用硬件环回,OS层用任务模拟,应用层用桩函数。
  • 提高复用:驱动库、OS内核、业务逻辑,都能在不同项目间搬。

核心原则:每一层只依赖它的下一层,绝不跨层调用。应用层不能直接写寄存器,驱动层不能自己创建任务。

2.2 经典三层架构的职责划分

这三层,每层都有自己的“地盘”。我习惯用一张图来理解它们的关系:

实时系统经典三层架构 应用层(Application Layer) 业务逻辑、控制算法、人机交互、通信协议 任务:Task_A, Task_B, Task_C ... 可测试性:单元测试、桩函数、模拟输入 OS层(Operating System Layer) 任务调度、中断管理、同步互斥、时钟管理 内核对象:信号量、消息队列、事件标志组 可测试性:调度器测试、资源竞争测试、超时测试 驱动层(Driver Layer) 硬件抽象、寄存器操作、外设初始化、中断服务 接口:GPIO、UART、SPI、I2C、ADC、PWM 可测试性:硬件环回、寄存器模拟、中断注入 API调用 驱动接口 中断通知 数据流方向:应用层 → OS层 → 驱动层 → 硬件;中断/事件反向通知

这张图我每次做架构评审都会拿出来。它清楚地告诉我们:数据流是单向的,但事件流可以反向(比如中断)。

2.3 各层详细职责

2.3.1 驱动层(Driver Layer)

驱动层是离硬件最近的。它的任务就是把芯片手册里的寄存器操作,封装成干净的C函数接口。我见过有人把驱动写成“寄存器大全”,一个函数里塞了十几个寄存器操作。这种代码,换一个芯片版本就得重写。

驱动层应该提供什么?

  • 硬件初始化:时钟、GPIO模式、外设参数配置。
  • 数据收发:UART发送/接收、SPI读写、I2C传输。
  • 中断服务:ISR里只做最必要的事,比如清标志、读数据、发信号量。
  • 状态查询:外设忙不忙、有没有错误。

我的习惯:驱动层函数名统一用 drv_<外设>_<操作> 格式。比如 drv_uart_send()、drv_spi_read()。这样OS层调用时一目了然。

举个例子,一个简单的UART驱动接口:

/* 驱动层头文件:drv_uart.h */
#ifndef DRV_UART_H
#define DRV_UART_H

#include <stdint.h>

/* 初始化UART,波特率、数据位、停止位、校验位 */
int drv_uart_init(uint32_t baud, uint8_t data_bits, uint8_t stop_bits, uint8_t parity);

/* 发送一帧数据,阻塞直到发送完成 */
int drv_uart_send(const uint8_t *data, uint16_t len);

/* 接收一帧数据,非阻塞,返回实际接收字节数 */
int drv_uart_recv(uint8_t *buf, uint16_t max_len);

/* 注册接收中断回调,由OS层的中断处理调用 */
void drv_uart_set_rx_callback(void (*callback)(uint8_t byte));

#endif /* DRV_UART_H */

注意看,这个驱动接口里没有任务、没有信号量、没有调度。它只关心硬件。OS层拿到这个接口后,再封装成带超时、带缓冲、带任务同步的版本。

2.3.2 OS层(Operating System Layer)

OS层是承上启下的关键。它管理着任务、中断、资源。我早期做的一个项目,OS层和驱动层耦合太紧,驱动里直接调OS的延时函数。后来换RTOS,驱动层全得改。教训深刻。

OS层的核心职责:

  • 任务管理:创建、删除、挂起、恢复任务。设置优先级。
  • 调度器:决定哪个任务运行。实时系统常用优先级抢占式调度。
  • 同步与互斥:信号量、互斥锁、事件标志组、消息队列。
  • 时钟与定时器:系统滴答、软件定时器、超时管理。
  • 中断管理:中断嵌套、中断延迟、中断与任务的通信。

避坑指南:我曾经在一个项目中,让应用层直接调用OS层的任务延时函数 os_task_delay()。结果测试时发现,某个任务因为延时被挂起,但驱动层的中断服务却因为等待这个任务而卡死。后来我强制规定:应用层只能通过OS层提供的API与驱动层交互,不能直接调用驱动函数。

OS层提供的典型API:

/* OS层头文件:os_api.h */
#ifndef OS_API_H
#define OS_API_H

#include "drv_uart.h"  /* 依赖驱动层接口 */

/* 任务管理 */
int os_task_create(void (*entry)(void *), void *param, uint8_t priority);
int os_task_delete(uint8_t task_id);
void os_task_delay(uint32_t ms);

/* 同步互斥 */
int os_sem_create(uint8_t *sem_id, uint32_t initial_count);
int os_sem_take(uint8_t sem_id, uint32_t timeout_ms);
int os_sem_give(uint8_t sem_id);

/* 消息队列 */
int os_mq_create(uint8_t *mq_id, uint32_t msg_size, uint32_t queue_len);
int os_mq_send(uint8_t mq_id, const void *msg, uint32_t timeout_ms);
int os_mq_recv(uint8_t mq_id, void *msg, uint32_t timeout_ms);

/* 驱动封装 - 带超时的UART发送 */
int os_uart_send(uint8_t port, const uint8_t *data, uint16_t len, uint32_t timeout_ms);

#endif /* OS_API_H */

看到没?OS层把驱动层的 drv_uart_send() 包装成了带超时的 os_uart_send()。应用层不用关心底层是轮询还是中断,也不用管超时怎么实现。

2.3.3 应用层(Application Layer)

应用层是写业务逻辑的地方。控制算法、通信协议、人机交互、数据采集,都在这里。应用层只调用OS层的API,不碰驱动,不碰寄存器。

应用层的可测试性设计,我特别看重。因为业务逻辑最复杂,也最容易出bug。如果应用层代码里混了OS调用和驱动操作,单元测试根本没法写。

一个典型的应用层任务:

/* 应用层任务:温度采集与控制 */
void task_temperature_control(void *param)
{
    uint8_t mq_id = (uint8_t)(uint32_t)param;
    uint8_t temp_data[4];
    uint8_t cmd;

    while (1) {
        /* 等待控制命令 */
        if (os_mq_recv(mq_id, &cmd, 100) == OS_OK) {
            if (cmd == CMD_READ_TEMP) {
                /* 通过OS层读取温度传感器 */
                os_uart_send(UART_SENSOR, "READ", 4, 50);
                os_task_delay(10);
                /* 处理数据... */
            }
        }
    }
}

这个任务里,所有操作都是通过OS层API完成的。测试时,我可以把OS层替换成测试桩,模拟消息队列和UART的行为,单独测这个任务的逻辑。

2.4 数据流与接口规范

三层之间的数据流,我总结了一个表格:

数据流方向 发起方 接收方 典型内容 传输方式
应用层 → OS层 应用任务 OS内核 API调用参数、消息数据 函数调用、消息队列
OS层 → 驱动层 OS内核/任务 驱动函数 读写命令、配置参数 函数调用
驱动层 → OS层 中断服务 OS内核/任务 中断事件、数据就绪 信号量、消息队列
OS层 → 应用层 OS内核 应用任务 事件通知、数据返回 消息队列、事件标志

这里有个关键点:驱动层不能主动调用OS层。驱动层只提供函数接口,由OS层来调用。驱动层的中断服务里,可以通过OS层提供的API(比如给信号量)来通知上层,但这不是“调用”,而是“触发”。

接口规范铁律:

  • 应用层只能包含 #include "os_api.h",不能包含 #include "drv_xxx.h"。
  • OS层只能包含 #include "drv_xxx.h",不能包含应用层的头文件。
  • 驱动层不能包含任何OS层或应用层的头文件。

2.5 分层架构对可测试性的意义

分层之后,测试变得清晰了:

  • 驱动层测试:用硬件环回或寄存器模拟器,验证每个外设接口的正确性。
  • OS层测试:在模拟环境中,验证调度、同步、超时行为。不需要真实硬件。
  • 应用层测试:用桩函数替换OS层API,验证业务逻辑。跑在PC上就行。

我记得有一次,应用层的一个状态机出了问题。因为分层清晰,我直接在PC上编译了应用层代码,用桩函数模拟了所有OS调用,半小时就定位到了bug。如果代码是混在一起的,光搭测试环境就得一天。