hal接口必须用纯虚类定义,所有函数声明为virtual void func() = 0;禁止基类实现、硬件头文件包含及非pod成员;bsp需隔离封装,hal不管理资源与生命周期;编译期配置优先用constexpr模板,运行时行为保留虚函数。

HAL 接口必须用纯虚类定义,不能带实现
嵌入式 C++ 的 HAL 本质是解耦驱动与业务逻辑的契约,不是工具库。一旦在 HAL 基类里写具体实现(比如默认空函数体、std::cout 日志、或调用底层寄存器操作),就等于把硬件细节泄漏进抽象层,后续换芯片或仿真时必然崩。
常见错误现象:HAL_SPI::transfer() 默认实现里直接调了 HAL_SPI_TransmitReceive()(STM32 HAL 库函数),导致上层代码编译通过但无法跨平台;或者加了 virtual ~HAL_SPI() = default; 却忘了加 virtual 到所有接口函数,子类重写后多态失效。
- 所有接口函数必须声明为
virtual void init() = 0;形式,强制派生类实现 - 析构函数必须是
virtual且纯虚或带空实现(virtual ~HAL_SPI() = default;可接受) - 禁止在基类中包含硬件头文件(如
"stm32f4xx_hal.h"或"regmap.h"),只允许前向声明 - 成员变量只能是 POD 类型(
bool,uint8_t,void*),禁用std::string、std::vector等动态分配类型
HAL 实现类要隔离 BSP,避免 #include 硬件 SDK
很多人把 HAL 实现直接塞进 hal_stm32f4.cpp,里面大段 #include "stm32f4xx_hal.h" 和 __HAL_RCC_GPIOA_CLK_ENABLE(),结果导致整个应用层被绑死在某颗芯片上,连单元测试都写不了。
正确做法是让 HAL 实现只依赖「最小 BSP 接口」——比如一个独立的 bsp_gpio.hpp,它只暴露 bsp_gpio_init()、bsp_gpio_write() 这种无 SDK 依赖的 C 风格函数,由 BSP 层自己去对接 HAL 库或寄存器操作。
- BSP 层提供 C 函数(非类),不暴露任何芯片头文件给 HAL 实现
- HAL 实现文件(如
hal_spi_stm32.cpp)只 include 自己的hal_spi.hpp和bsp_spi.hpp - 如果要用 CMSIS 或厂商 HAL,必须封装在 BSP 层内部,HAL 实现里看不到
HAL_SPI_StateTypeDef这类类型 - 仿真时只需替换 BSP 实现(比如用
bsp_spi_mock.cpp返回固定数据),HAL 和业务层完全不动
别在 HAL 里做资源管理,交给上层或 RAII 封装
HAL_UART::open() 或 HAL_I2C::acquire_bus() 这类函数看似方便,实则埋雷:谁负责关?什么时候关?并发访问怎么同步?HAL 不该承担设备生命周期职责,它只管“怎么发”“怎么读”,不管“谁来用”“用多久”。
典型踩坑:多个线程同时调 hal_i2c->transfer(),HAL 内部用静态 mutex 锁总线,结果性能卡死;或者 HAL_ADC::start_conversion() 启动 DMA 后没提供 cancel 接口,超时处理只能硬复位。
- HAL 接口应设计为无状态、无副作用的“操作函数”,如
read(uint8_t* buf, size_t len),不隐式初始化/释放硬件 - 资源获取(如 GPIO 引脚占用、I2C 总线仲裁)放在单独的
Driver或Device类里,用 RAII 管理(构造即 acquire,析构即 release) - 中断回调必须是纯函数指针或
std::function参数,HAL 基类不持有回调对象,避免循环引用或生命周期错配 - 对 DMA、中断等异步能力,HAL 只提供注册接口(
set_rx_callback(void (*cb)(const uint8_t*, size_t))),不参与调度逻辑
constexpr + 模板参数可替代部分运行时 HAL 多态
不是所有抽象都需要虚函数。像 SPI 模式(CPOL/CPHA)、UART 波特率、GPIO 上拉配置这些编译期已知的参数,硬用虚函数+运行时分支,既增加 ROM 占用,又削弱内联机会。
例如 template<uint32_t br> struct UART_HAL : HAL_UART</uint32_t>,波特率作为模板参数,生成的 write() 可直接展开寄存器配置指令,比 virtual write(const uint8_t*, size_t, uint32_t br) 快且确定。
- 适合固定配置的外设(如板载温感 I2C 地址固定为 0x40),用模板特化实现
TempSensorHAL - 运行时可变参数(如用户通过串口输入改变 ADC 采样率)仍走虚函数路径,二者混用不冲突
- 注意模板实例膨胀:别把整个 HAL 接口全模板化,只对真正影响代码生成的参数模板化
- Clang/GCC 下
-flto能帮优化掉未使用的模板实例,但裸机环境慎用,链接脚本可能不兼容
HAL 的难点从来不在“怎么写虚函数”,而在于划清三道线:HAL 和 BSP 的头文件边界、HAL 和 Driver 的责任边界、编译期配置和运行时行为的表达边界。越早用静态分析(比如 cppcheck --suppress=unusedFunction 扫虚函数实现)和链接脚本检查符号依赖,越不容易在量产前夜发现 HAL_I2C 间接拖进了 libstdc++.a。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











