3、硬件抽象层(HAL)移植:GPIO、UART、SPI、Timer底层驱动接口实现
好,咱们进入正题。硬件抽象层,简称HAL,说白了就是给协议栈和芯片硬件之间加一层「翻译官」。协议栈不关心你用的是STM32还是NXP,它只认HAL接口。你把这层写好了,换芯片就像换手机壳一样简单。
我个人习惯,移植HAL的第一步不是写代码,而是先看芯片手册。嗯,这一步很多人会跳过去,直接抄例程。我吃过这个亏——有一次项目赶工期,我直接拿现成的GPIO驱动往上怼,结果蓝牙连上就断,查了三天才发现是某个引脚的中断触发方式没配对。所以,先看手册,再看代码。
3.1 GPIO接口实现
蓝牙协议栈里,GPIO主要干三件事:控制芯片复位、控制电源使能、以及中断唤醒。你想想看,如果复位引脚拉高拉低的时序不对,芯片可能压根不工作。
我一般会封装成这样的结构:
// hal_gpio.h
typedef struct {
void (*init)(uint8_t pin, uint8_t mode);
void (*write)(uint8_t pin, uint8_t level);
uint8_t (*read)(uint8_t pin);
void (*irq_enable)(uint8_t pin, uint8_t trigger);
void (*irq_disable)(uint8_t pin);
} hal_gpio_t;
这里要注意的是 mode 参数。不同芯片的GPIO模式定义不一样,有的叫 GPIO_MODE_OUTPUT,有的叫 GPIO_MODE_OUT_PP。我建议你在HAL层统一用枚举,比如:
typedef enum {
HAL_GPIO_MODE_INPUT,
HAL_GPIO_MODE_OUTPUT,
HAL_GPIO_MODE_AF_PP, // 复用推挽
HAL_GPIO_MODE_AF_OD // 复用开漏
} hal_gpio_mode_t;
然后在具体芯片的驱动里做映射。这样换芯片时,你只需要改底层映射表,上层代码纹丝不动。
3.2 UART接口实现
UART是蓝牙协议栈的「生命线」。HCI命令、ACL数据、事件通知,全走这条通道。所以UART驱动必须稳定,不能丢字节,不能乱序。
我建议你实现以下接口:
// hal_uart.h
typedef struct {
void (*init)(uint32_t baudrate, uint8_t parity, uint8_t stopbits);
void (*send)(uint8_t *data, uint16_t len);
void (*send_byte)(uint8_t byte);
uint8_t (*receive_byte)(void);
uint16_t (*available)(void);
void (*irq_handler)(void);
} hal_uart_t;
这里有个坑——波特率。蓝牙协议栈对时序要求很严,波特率偏差超过2%就可能丢包。我曾经在一个项目里,芯片的UART时钟源是内部RC振荡器,精度只有±3%,结果蓝牙配对总是超时。后来我换成外部晶振,问题才解决。
另外,接收中断一定要用FIFO或者环形缓冲区。为什么?因为蓝牙协议栈的数据包是变长的,你没法预判什么时候收完。用环形缓冲区,中断只管往里塞数据,主循环或者协议栈任务再从缓冲区取,这样就不会丢数据。
3.3 SPI接口实现
SPI在蓝牙芯片里通常用来接外部Flash、传感器,或者某些高速数据通道。SPI的难点在于时钟极性和相位配置,搞错了数据就全乱套。
我一般这样封装:
// hal_spi.h
typedef struct {
void (*init)(uint32_t freq, uint8_t mode); // mode: 0-3
void (*cs_enable)(void);
void (*cs_disable)(void);
uint8_t (*transfer)(uint8_t tx_data);
void (*transfer_dma)(uint8_t *tx, uint8_t *rx, uint16_t len);
} hal_spi_t;
这里 mode 参数对应SPI的四种模式(CPOL和CPHA的组合)。我建议你在注释里写清楚每种模式的适用场景,比如模式0(CPOL=0, CPHA=0)最常用,大多数Flash芯片都支持。
DMA传输这块,我个人建议能上DMA就上DMA。蓝牙协议栈的数据量不大,但SPI传输如果占用CPU太久,会影响蓝牙的实时性。我记得有一次,我用轮询方式读Flash里的蓝牙配置,结果导致蓝牙广播间隔抖动,手机连接不稳定。换成DMA后,问题立刻消失。
3.4 Timer接口实现
Timer是蓝牙协议栈的「心跳」。协议栈内部有很多超时管理、定时任务、以及微秒级的延迟。比如蓝牙广播间隔、连接间隔、扫描窗口,全依赖Timer。
我建议提供以下接口:
// hal_timer.h
typedef struct {
void (*init)(uint32_t freq); // 初始化定时器频率
void (*start)(void);
void (*stop)(void);
uint32_t (*get_us)(void); // 获取微秒级时间戳
void (*delay_us)(uint32_t us); // 微秒级阻塞延时
void (*delay_ms)(uint32_t ms); // 毫秒级阻塞延时
void (*set_callback)(void (*cb)(void)); // 设置定时中断回调
} hal_timer_t;
这里最关键的接口是 get_us()。蓝牙协议栈需要高精度的时间戳来计算超时和调度任务。我建议用硬件定时器的计数器直接读,不要用软件累加。软件累加会有中断延迟,精度不够。
另外,delay_us() 这个接口,很多人会用循环等待来实现。但循环等待在低功耗模式下会出问题——CPU休眠了,循环就不跑了。我建议用定时器的捕获比较功能来实现精确延时,这样CPU可以继续休眠,定时器到时间了再唤醒。
3.5 移植验证 checklist
接口写完了,怎么验证对不对?我列一个自检清单,你照着测一遍:
| 接口 | 验证方法 | 常见问题 |
|---|---|---|
| GPIO | 写一个LED闪烁程序,观察波形 | 引脚复用配置错误,输出电平反相 |
| UART | 回环测试:发送0x55 0xAA,接收对比 | 波特率偏差、停止位配置错误 |
| SPI | 读写Flash ID,对比数据手册 | 时钟极性/相位错误、CS时序不对 |
| Timer | 测量1ms延时,用逻辑分析仪看误差 | 时钟源选择错误、预分频系数算错 |
嗯,这个清单看起来简单,但每一条背后都有血泪史。我记得有一次SPI验证通过了,但蓝牙就是连不上。后来发现是Flash的写保护引脚没拉高,导致配置写不进去。所以,验证的时候不仅要测功能,还要测边界条件——比如UART在最大波特率下连续收发10000个字节,看有没有丢包。
最后说一句,HAL移植完成后,不要急着跑协议栈。先写一个简单的测试程序,把每个接口单独测一遍。这一步省不了,省了后面debug的时间会翻倍。我当年就是太自信,结果在协议栈里查了一个星期的bug,最后发现是GPIO中断触发方式配错了。你说冤不冤?