分层架构:分层原则、层间通信、依赖方向

分层架构,说白了就是把代码按职责切分成几个堆叠的层次。每一层只关心自己的事,不越界。我刚开始做嵌入式项目时,经常把业务逻辑、硬件驱动、协议栈全揉在一个文件里。嗯,后来维护起来那叫一个痛苦——改一个GPIO引脚,结果把整个状态机搞崩了。

分层架构的核心思想就一句话:上层依赖下层,下层不依赖上层。听起来简单,但实际落地时,很多人会栽在「层间通信」和「依赖方向」这两个坑里。

分层原则:谁该在哪一层?

我个人习惯把嵌入式软件分成四层,从下往上分别是:

  • 硬件抽象层(HAL):直接操作寄存器、GPIO、SPI、UART 等。这一层只做一件事——把硬件细节封装成统一的接口。
  • 驱动层(Driver):管理具体外设,比如 LCD 驱动、传感器驱动。它调用 HAL 的接口,但不关心业务逻辑。
  • 服务层(Service):提供一些通用功能,比如日志、定时器管理、任务调度。这一层是「中间人」,上下都跟它打交道。
  • 应用层(Application):业务逻辑所在。比如一个温控系统,应用层决定「温度超过 30°C 就开风扇」。它只调用服务层和驱动层的接口,绝不直接碰寄存器。

我在项目中遇到过一种情况:有人把业务逻辑直接写在驱动里,结果换了一个传感器型号,整个应用层都得重写。这就是分层没分好。记住一个原则:每一层只解决一个抽象级别的问题

核心原则:

  • 上层可以调用下层,下层绝不能调用上层
  • 同层之间尽量不互相调用,非要调用就通过回调或消息
  • 每一层对外暴露的接口要稳定,内部实现随便改

层间通信:怎么传数据?

层间通信说白了就是「上层怎么把活儿派给下层,下层怎么把结果回给上层」。常见的方式有三种:

通信方式 适用场景 缺点
直接函数调用 简单、实时性要求高 耦合度高,改接口影响大
回调函数 异步事件驱动(如中断、定时器) 回调嵌套多了容易乱
消息队列 多任务、解耦要求高 增加内存开销和延迟

我个人最常用的是直接函数调用 + 回调的组合。比如应用层调用驱动层的 sensor_read(),驱动层读取完数据后,通过回调把结果传回应用层。这样既保持了调用链清晰,又不会让驱动层反过来依赖应用层。

你想想看,如果驱动层直接调用应用层的函数,那驱动层就得知道应用层有哪些函数——这不就反向依赖了吗?我曾经在一个项目里看到有人这么干,结果驱动层一改,应用层也得跟着改,最后两个层耦合得跟麻花似的。

我的建议:层间通信尽量用「接口」而不是「具体实现」。比如定义一个 sensor_ops_t 结构体,里面放函数指针。上层只跟这个结构体打交道,下层换驱动时,只要重新填一下函数指针就行。

依赖方向:箭头往哪指?

依赖方向是分层架构里最容易翻车的地方。很多人以为「上层调用下层」就是依赖方向,其实没那么简单。

依赖方向分两种:

  • 编译期依赖:A 的代码里 #include 了 B 的头文件,或者调用了 B 的函数。这是最直接的依赖。
  • 运行期依赖:A 在运行时需要 B 提供数据或服务。比如应用层调用驱动层的函数,运行期依赖就是「应用层 -> 驱动层」。

分层架构要求:编译期依赖和运行期依赖的方向必须一致,都是从上往下。如果出现「编译期依赖是 A -> B,但运行期依赖是 B -> A」,那就是循环依赖,早晚出问题。

举个例子:应用层想读取温度,它调用驱动层的 temp_read()。编译期依赖是应用层 #include 了驱动层的头文件,运行期依赖也是应用层调用驱动层。没问题。

但如果驱动层在读取完温度后,直接调用应用层的 handle_temp() 函数——这就反了。驱动层编译时需要知道应用层的函数签名,运行期也反过来调用了应用层。这就是典型的反向依赖

避坑指南:我曾经在一个智能家居项目里,因为赶工期,让驱动层直接调用了应用层的回调函数。结果后来要换一个通信协议,驱动层和应用层都得改,改了一个星期才搞定。从那以后,我强制要求所有层间通信必须通过「接口层」来中转,谁也别直接依赖谁。

用 SVG 画一张分层架构图

下面这张图展示了典型的分层架构、层间通信路径和依赖方向。箭头表示依赖方向,都是从下往上指——因为下层不依赖上层,所以箭头指向调用方。

应用层(Application) 业务逻辑:温控、报警、用户交互 服务层(Service) 日志、定时器、任务调度、协议解析 驱动层(Driver) LCD驱动、传感器驱动、Flash驱动 硬件抽象层(HAL) GPIO、SPI、UART、I2C 寄存器操作 依赖方向(下层不依赖上层) 依赖方向 依赖方向 函数调用 / 回调 函数调用 / 消息队列 寄存器读写 / HAL API

实际项目中的分层经验

我在做一款工业数据采集器时,遇到过一个问题:应用层需要同时读取温度、湿度、气压三个传感器。如果每个传感器都单独写一个驱动,应用层就得分别调用三个不同的接口——代码里全是 temp_read()hum_read()press_read(),乱得很。

后来我加了一个传感器服务层,它封装了所有传感器的驱动,对外只暴露一个接口:sensor_read(sensor_type_t type, float *value)。应用层只需要传一个枚举值,服务层自动选择对应的驱动去读。这样应用层完全不知道底层有几个传感器,也不知道它们是用 I2C 还是 SPI 通信的。

这就是分层的好处:每一层都只解决一个抽象级别的问题。应用层只关心「读什么数据」,不关心「怎么读」。驱动层只关心「怎么操作寄存器」,不关心「数据用来干什么」。

总结一下分层架构的三个要点:

  1. 分层原则:每一层只做一件事,接口要稳定,实现随便改。
  2. 层间通信:用函数调用、回调或消息队列,但别让下层反过来调用上层。
  3. 依赖方向:编译期和运行期的依赖方向必须一致,从上往下,不能有循环依赖。

嗯,分层架构看起来简单,但真正用好它,需要你在设计初期就想清楚每一层的边界。别等到代码写了两万行再回头分层——那时候改起来,比重新写还痛苦。

公众号:蓝海资料掘金营,微信deep3321