BSP分层设计与驱动架构
这是星球课程体系的核心议题,相关问答数量最多。本专题把散落的讨论合并成一张完整的分层地图。
一、完整分层:从 HAL 到 APP
星主给出的整体链路是:HAL 是硬件抽象层,Driver 把 HAL 组合起来完成基本外设控制,Handler 控制 Driver 去满足运行逻辑,Adapter 把 Handler 挂载到 wrapper 的抽象接口上,APP 调用 platform 里的 wrapper 接口完成业务(hal是硬件抽象层,driver将hal组合起来完成基本的外设控制,Jack)。
两对最容易混淆的概念:
- Core vs BSP:Core 是芯片底层驱动(如 IIC 外设本身的初始化),BSP 是面向板级设备的驱动(如挂 IIC 的 MPU6050),BSP 是 Core 的上层,依赖 Core 提供的接口(同学提问: Core和BSP,Jack)。
- Driver vs Handler:BSP 层由 Driver 与 Handler 组成。Driver 直接操作硬件、保持简单通用;Handler 向上提供统一北向接口,内部携带独立线程,负责资源管理、数据缓存、优先级与错误处理(BSP层的设计逻辑与优势总结,Jack)。
Handler 存在的六大理由:独立线程便于时间管理;避免 App 与 Driver 直接耦合;把硬件阻塞隔离在 BSP 内以保证 App 实时性;统一协调多任务访问同一外设;对变化慢的数据做"数据新鲜度"缓存;硬件更换时只需改 Driver(同上,Jack)。课程用的 RT-Thread 设备框架,对应的正是这套架构里的 Handler 层(问: 我之前用过 RT Thr,Jack)。
二、BSP 的纯粹性标准
BSP 文件里只应放 MCU 与板上外设器件的交互逻辑。以 MPU6050 为例:无论硬件 IIC、软件 IIC 还是 IIC+DMA,都由 Core 提供接口,引脚初始化也不属于 BSP——BSP 只是"描述 MCU 与片上器件通讯"的代码,这样写出的驱动才能在 STM32、GD32 甚至 Linux 上复用,跳槽时逻辑才带得走(对于bsp层,它的作用是什么?,Jack;判断标准同 什么类型的函数适合放到BSP驱,Jack)。
对"硬件管理与业务逻辑"的边界,用按键说清楚:按键按下被检测到(中断触发)是硬件管理,归 BSP 的 Driver 层;把按下时刻、类型告知 App 属于 Handler 层的职责;而"按下后开继电器"是纯业务逻辑,归 App。长按/短按/三连按的判定属于业务——BSP 不可能穷尽所有用法,不同产品对长按时长的理解也不同(问:bsp层的目的之一是完全将硬件管理和业务逻辑分开吗?如果是的话,Jack;双击超时的实现见 如果在按键中,我需要检测双击,也就是某段时间内再次按下,判定双击、多击等等,Jack)。
三、分层的代价与收益量化
分层引入指针嵌套确实增加寻址次数,但量级极小:72MHz 的 STM32F1 单条指令约 10~100ns,4 层指针跳转平均约 200ns,对运行速度影响甚微;且后续可通过 sct 分散加载把常用 IO 固定链接地址,用立即数直接访问,比函数嵌套还快。真正的代价是对开发人员水平要求更高(问: 我们把bsp层抽出来,,Jack)。
架构手法上,LED 驱动采用桥接模式的目的不是"给人看",而是通过文件管理链接的最小单元,实现高内聚低耦合,进而支撑数据归类、差分升级、飞秒启动,在 hex 层面优化系统——具体的模式可以换(工厂、桥接都行),要掌握的是设计与架构能力本身(老师,写这个LED驱动为啥要用设计模式中的桥接模式,优点在哪,Jack)。
四、inst/init 与对象管理
- inst 函数的职责是"解包+自挂载":每层不直接操作上层传来的接口"包裹",而是把接口挂载到自身;init 再完成建队列、初始化芯片、自检(如读 ID)等前提,之后模块才能安心执行业务逻辑(inst函数在整个模块中的地位,Jack)。
- 一个 Handler 构造一个线程:通用驱动能管理的对象数量有上限,超限必须再开线程(老师,调用handler inst的时候会调用外部传入的线程创建接口,Jack)。
- 私有数据保护:APP 读取设备 ID 必须通过 BSP 提供的函数,先检查初始化完成再返回,防止 APP 先于 BSP 启动读到脏数据引发 IO 异常(BSP驱动中如何处理私有数据?比如设备的ID,APP想知道设备的ID:,Jack;行业惯例图解见 行业常识:驱动中私有变量的访问方法(防止脏数据系统紊乱,Jack)。
- 模块间不共享队列:LED 和按键各用各的队列,"写 LED 驱动的人并不知道 key 的存在"——驱动之间不能互相传递私有信息(老师我这里不是很理解为什么要为led也创建一个队列,按道理两个线程,Jack)。
五、可扩展性与兼容设计
- 兼容多种芯片:当 Handler 要同时兼容 AHT21/DHT11 时,文件命名就不应再含具体型号,而应体现"温湿度 Handler"(问: 老师,我们现在的这里的教,Jack)。
- 多通信接口:设备同时支持 IIC/SPI(如 MPU6050)时,可用结构体封装接口类型,由 Handler 传入后在 inst 中选择;同一 IIC 总线挂两个 MPU6050 则靠地址区分,调用写寄存器接口时要能从调用者反推出具体对象(看了b站的aht21驱动视频,,爱健身的机器人;老师,如果我有两个MPU6050,都挂接在同一个IIC总线上,它们通过地址来区分,mx)。
- 软硬件外设统一封装:软件 SPI 与硬件 SPI 对外提供相同的读/写/初始化/去初始化接口、内部配置不同——这正是函数指针接口层的典型用法(星主,请教一个问题,我想把软件 spi 和硬件 spi 用一个对象封装起来,这是一个好听的id 提问)。
六、理念层:解耦的边界
- 去平台化与平台化是一对互逆操作:驱动与 App 解耦叫"去平台化",项目运行时把抽象接口具体化叫"平台化",平台化的本质就是"将模块内部的抽象接口具体化"(关于“平台化”和“去平台化”的,Lili_iliLX / Jack)。
- Core 以下不必过度抽象:CubeMX 生成的代码解耦性一般,但 Core 及以下是原厂 SDK,换芯片就整体换掉,复用度太低,花太多心思抽象意义不大(老师,做解耦时,core 层和 driver层感觉cubemx生成的代码解耦性不强,Jack)。
- 事件驱动是 BSP 与 App 衔接的默认姿势:按键检测线程平时阻塞不占 CPU,按下时经队列唤醒线程处理,比轮询扫描省掉大量空扫、有利于低功耗(什么是事件驱动? 事件驱动简,Jack;问:按键模块是怎么进行的 答:,Jack)。
来源笔记
- hal是硬件抽象层,driver将hal组合起来完成基本的外设控制 | Jack
- 同学提问: Core和BSP | Jack
- BSP层的设计逻辑与优势总结 | Jack
- 问: 我之前用过 RT Thr | Jack
- 对于bsp层,它的作用是什么? | Jack
- 什么类型的函数适合放到BSP驱 | Jack
- 问:bsp层的目的之一是完全将硬件管理和业务逻辑分开吗?如果是的话 | Jack
- 如果在按键中,我需要检测双击,也就是某段时间内再次按下,判定双击、多击等等 | Jack
- 问: 我们把bsp层抽出来, | Jack
- 老师,写这个LED驱动为啥要用设计模式中的桥接模式,优点在哪 | Jack
- inst函数在整个模块中的地位 | Jack
- 老师,调用handler inst的时候会调用外部传入的线程创建接口 | Jack
- BSP驱动中如何处理私有数据?比如设备的ID,APP想知道设备的ID: | Jack
- 行业常识:驱动中私有变量的访问方法(防止脏数据系统紊乱 | Jack
- 老师我这里不是很理解为什么要为led也创建一个队列,按道理两个线程 | Jack
- 问: 老师,我们现在的这里的教 | Jack
- 看了b站的aht21驱动视频, | 爱健身的机器人
- 老师,如果我有两个MPU6050,都挂接在同一个IIC总线上,它们通过地址来区分 | mx
- 星主,请教一个问题,我想把软件 spi 和硬件 spi 用一个对象封装起来 | 这是一个好听的id
- 关于“平台化”和“去平台化”的 | Lili_iliLX
- 老师,做解耦时,core 层和 driver层感觉cubemx生成的代码解耦性不强 | Jack
- 什么是事件驱动? 事件驱动简 | Jack
- 问:按键模块是怎么进行的 答: | Jack