必须定义自有接口契约而非直接封装厂商函数,用纯虚基类抽象能力(如UartDriver),按芯片型号特化实现,解耦初始化并校准波特率误差。

不能直接封装厂商库的函数名,必须定义你自己的接口契约,再让各厂商实现去适配它。 否则所谓“统一”只是把不同命名的函数用宏简单替换一遍,换芯片时照样要改逻辑、修时序、调波特率误差——这不是抽象,是贴膏药。
为什么不能用宏#define统一函数名
常见错误是写这样的代码:#define hal_gpio_write pin, level 然后在不同平台下用 #ifdef STM32F4xx 展开成 HAL_GPIO_WritePin(...),另一处展开成 NXP_GPIO_SetPin(...)。问题在于:
- 参数顺序、语义不一致:STM32 的
HAL_GPIO_WritePin第二个参数是GPIO_PIN_x,而 NXP SDK 可能要求先传端口号再传引脚号 - 返回值含义不同:有的返回
void,有的返回status_t,上层没法做错误处理 - 初始化流程割裂:STM32 要先
__HAL_RCC_GPIOA_CLK_ENABLE(),NXP 可能要调CLOCK_EnableClock(kCLOCK_Gpio1),宏包不住这种差异 - 编译期无法校验接口一致性:宏展开后 IDE 不识别,重构、跳转、自动补全全部失效
用纯虚基类定义硬件契约(C++11+)
核心是把“能力”而非“实现”作为接口。比如串口发送,不是暴露 HAL_UART_Transmit,而是定义:
class UartDriver {
public:
virtual ~UartDriver() = default;
virtual bool init(uint32_t baudrate, uint8_t data_bits = 8) = 0;
virtual size_t send(const uint8_t* data, size_t len, uint32_t timeout_ms = 100) = 0;
virtual size_t recv(uint8_t* buf, size_t max_len, uint32_t timeout_ms = 100) = 0;
};
注意点:
- 返回值统一用
size_t表示实际传输字节数,失败时返回0—— 避免跨平台对HAL_OK/kStatus_Success的依赖 - 超时单位强制为
ms,屏蔽底层是滴答计数器还是 HAL_GetTick() 的差异 - 不暴露句柄(
UART_HandleTypeDef或uart_handle_t),由具体实现内部管理
平台适配层必须按芯片型号特化,不能按厂商粗粒度划分
同一厂商不同系列芯片,寄存器布局、时钟树、DMA 触发条件都可能不同。比如 STM32F1 和 STM32H7 的 UART 波特率计算公式就不同,硬塞进一个 STM32Driver 类里会埋雷。
推荐命名与组织方式:
- 目录结构:
/pal/stm32f429/uart.cpp、/pal/gd32f303/uart.cpp、/pal/lpc55s69/uart.cpp - 类名严格对应芯片型号:
Stm32f429UartDriver、Gd32f303UartDriver - 构造函数接受芯片专属配置结构体,例如:
Stm32f429UartDriver(UART_HandleTypeDef& huart),而不是泛泛的void*
这样做的好处是:移植到新芯片时,新增一个目录 + 一个类即可,老代码完全不动,CI 也能针对单个平台跑单元测试。
初始化入口必须解耦,禁止全局单例隐式依赖
很多项目写 UartDriver::getInstance(),结果发现 ESP32 上要用 uart_driver_install() 先注册中断,而 STM32 是裸机轮询——这种单例模式会让初始化顺序变成玄学。
正确做法是把驱动实例交给上层容器管理:
- 应用层(main.cpp)显式创建:
auto uart = std::make_unique<stm32f429uartdriver>(huart1);</stm32f429uartdriver> - 通过依赖注入传入业务模块:
Logger logger(std::move(uart)); - 若需运行时切换(如调试口从 UART1 切到 USB CDC),用
std::unique_ptr<uartdriver></uartdriver>指针赋值即可,不牵扯内存泄漏或析构顺序
真正容易被忽略的是:波特率误差必须在适配层内校准。比如 GD32F303 在 115200 波特率下误差达 3.2%,而 STM32F429 是 0.1%。这个数值不能靠文档估算,得实测示波器抓波形,然后在 init() 里动态修正 USARTDIV 值——这部分逻辑,必须锁死在 Gd32f303UartDriver::init() 内部,绝不暴露给上层。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











