从 USB 理解嵌入式软件分层
USB 只是一个例子。真正需要掌握的,不是把某一种通信协议孤立地背下来,而是学会面对一个复杂系统时,如何划分软件层次、确定每一层负责什么,以及判断问题应该到哪一层排查。
先把同一个问题拆成不同层次
以“设备无法正常通信”为例,可以从三个层面观察:
- 物理层:信号是否真的能够在硬件之间传输,例如电源、连线、引脚复用、电平和信号质量。
- 协议层:双方是否按照同一套规则交换数据,例如数据如何打包、设备如何寻址、谁先发起通信,以及如何校验和处理错误。
- 接口层:设备之间通过什么标准连接,例如连接器、供电能力和对外暴露的接口形式。
这三个层面相互关联,但不能混为一谈。Type-C 是接口形式,不等于某一个具体协议;看到总线上有电平变化,也不等于协议解析一定正确;应用层收到错误数据,也不一定是应用代码本身造成的。
从 USB 抽象出软件开发的通用分层
在嵌入式开发中,可以把硬件和软件关系简化为下面三层:
| 软件层次 | 主要职责 | 不应承担的职责 |
|---|---|---|
| 应用层 | 处理字节、数据结构、帧头、长度、校验和业务逻辑 | 直接操作寄存器,关心具体引脚来自哪一种芯片 |
| 驱动层 | 配置外设、处理中断和 DMA,收发数据,并向上提供稳定接口 | 解释业务含义,决定一帧数据代表什么业务动作 |
| 硬件层 | 提供供电、时钟、引脚连接和实际信号通路 | 代替软件完成协议解析和业务决策 |
这种分层的核心不是把代码机械地分成几个文件,而是让每一层只依赖自己应该知道的内容:
- 应用层只关心“收到了哪些数据、数据是否有效、业务该如何处理”;
- 驱动层只关心“如何让控制器可靠地收发数据”;
- 硬件层只关心“信号是否有正确的物理通路”。
因此,同一套应用层的数据解析逻辑,理论上可以复用在 USB、UART、RS232、RS485 甚至蓝牙之上。底层传输方式不同,但上层仍然可以统一为“接收数据、缓存数据、解析帧、校验、执行业务”的流程。
为什么要做这种抽象
1. 降低认知负担
学习一个新外设时,不必同时理解芯片模拟电路、控制器寄存器、协议格式和业务逻辑。先明确当前问题属于哪一层,再只研究这一层需要的知识。
2. 方便替换底层实现
如果应用代码只依赖“发送数据”和“接收数据”这类抽象接口,那么把 UART 换成 USB CDC 时,通常只需要替换驱动适配部分,不需要重写整个业务模块。
3. 缩小故障范围
排查“没有收到正确数据”时,可以按顺序验证:
- 硬件是否连通、供电是否正常、引脚配置是否正确;
- 驱动是否初始化成功、是否收到中断或数据;
- 缓冲区和协议解析是否正确;
- 业务逻辑是否正确处理了解析结果。
每一层都通过可观察证据验证,而不是看到现象后直接修改应用代码。
USB 作为案例
USB 的通信由主机发起,设备响应。设备需要经过枚举,主机才能识别设备并使用相应的端点和传输方式。对于嵌入式开发者来说,可以把 USB 看成一条由底层驱动提供的数据通道:
- USB 控制器和 HAL 负责初始化、端点配置、传输和中断;
- USB 协议栈负责枚举、描述符和传输规则;
- 应用代码负责处理收到的有效数据及其业务含义。
这样理解 USB 的目的,不是忽略底层细节,而是知道什么时候需要进入底层。只有当上层证据表明问题来自枚举、端点、传输或硬件信号时,才继续深入对应层次。
本文是个人博客的第一篇文章,作为测试