第三章 软件架构与驱动开发:UWB驱动框架、SPI通信优化、中断处理与DMA传输
做UWB车钥匙,硬件选型只是第一步。真正让芯片“听话”的,是底层的软件架构。这一章,我重点聊聊驱动框架怎么搭、SPI怎么跑得快、中断和DMA怎么配合。这些是实战中的硬骨头,啃下来,测距精度才能稳得住。
3.1 UWB驱动框架设计
驱动框架,说白了就是给上层应用一个干净的接口。我个人习惯把驱动分成三层:硬件抽象层(HAL)、协议层、应用接口层。
- 硬件抽象层:直接操作寄存器,封装SPI读写、GPIO控制、中断注册。这一层跟芯片绑定,换芯片就得重写。
- 协议层:处理UWB的帧格式、时间戳解析、测距算法(如TWR、TDoA)。这一层尽量通用,不依赖具体硬件。
- 应用接口层:提供简单的API,比如
uwb_start_ranging()、uwb_get_distance()。上层应用只管调用,不用管底层细节。
关键点:驱动框架的核心是“解耦”。我在项目中遇到过,因为协议层和硬件层耦合太紧,换了一颗UWB芯片,几乎重写了整个驱动。后来我强制要求:协议层只通过HAL的接口访问硬件,任何寄存器操作都不允许出现在协议层代码里。
举个例子,HAL层的SPI读写函数,我通常会这样设计:
// hal_uwb.h
typedef struct {
int (*spi_write_read)(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len);
void (*gpio_set_cs)(uint8_t level);
void (*gpio_set_rst)(uint8_t level);
void (*irq_register)(void (*callback)(void));
} hal_uwb_t;
// 初始化时传入具体实现
hal_uwb_t hal = {
.spi_write_read = spi_dma_transfer,
.gpio_set_cs = gpio_write_cs,
.gpio_set_rst = gpio_write_rst,
.irq_register = nvic_enable_irq
};
这样,协议层只需要调用hal->spi_write_read(),完全不用关心底层是SPI轮询还是DMA。嗯,这里要注意:函数指针的结构体,初始化时一定要检查每个成员是否为空,否则跑起来直接HardFault。
3.2 SPI通信优化
UWB芯片和主控之间,最常用的通信接口就是SPI。测距过程中,数据量不大,但实时性要求高。SPI通信的瓶颈往往不在速率,而在“等待”。
我建议从三个方面优化:
- 时钟频率:UWB芯片的SPI时钟上限通常是20MHz~40MHz。别一上来就拉满,先看信号完整性。我记得有一次,SPI时钟调到30MHz,通信偶尔出错,示波器一看,时钟沿上有毛刺。降到20MHz,问题消失。
- 数据包大小:UWB的帧数据通常不大(几十字节),但频繁的小包传输会拉高CPU开销。我习惯把多个小包合并成一个大包,一次SPI传输搞定。比如,读取测距结果时,把距离、质量、时间戳一次性读完。
- 片选管理:每次SPI传输前,都要拉低片选(CS),传输完再拉高。这个操作看似简单,但时序不对,芯片就不响应。我踩过坑:CS拉低后,没有等足够的时间(通常几微秒)就发时钟,导致第一个字节丢失。
实战技巧:SPI通信的“黄金法则”——先看波形,再调代码。用逻辑分析仪抓一下CS、CLK、MOSI、MISO的时序,一眼就能看出问题。我曾经花了两天调试一个SPI通信失败的问题,最后发现是CS拉低后延时不够。示波器一看,真相大白。
3.3 中断处理与DMA传输
UWB测距对实时性要求很高。主控需要及时响应UWB芯片的中断,读取测距结果。如果CPU被其他任务占着,中断响应慢了,测距精度就会受影响。
我常用的方案是:中断触发 + DMA传输。
- 中断处理:UWB芯片完成一次测距后,会拉高一个GPIO引脚(IRQ)。主控检测到中断,立即进入中断服务函数。中断服务函数里,只做最轻量级的工作——比如设置一个标志位,或者启动DMA传输。绝对不要在中断里做复杂计算或打印日志。
- DMA传输:读取测距结果时,用DMA代替CPU轮询。CPU只需要配置好DMA的源地址、目的地址、传输长度,然后启动传输。DMA完成后,触发一个完成中断,CPU再处理数据。
举个例子,DMA读取UWB测距结果的流程:
// 中断服务函数(轻量级)
void UWB_IRQHandler(void) {
// 清除中断标志
uwb_clear_irq();
// 启动DMA读取测距结果
DMA_Start(DMA_CHANNEL, (uint32_t)&UWB_SPI_DR, (uint32_t)rx_buffer, 32);
}
// DMA完成中断
void DMA_Complete_IRQHandler(void) {
// 解析测距结果
uwb_parse_result(rx_buffer);
// 通知上层应用
uwb_result_ready = 1;
}
注意:DMA传输时,要确保SPI的时钟和DMA的时钟同步。有些MCU的DMA和SPI不在同一个时钟域,需要配置同步逻辑。我曾经遇到过,DMA传输的数据全是0xFF,查了半天,发现是DMA时钟比SPI时钟慢,导致数据采样错误。
另外,中断优先级也要合理设置。UWB的中断优先级,我通常设为最高(比定时器、串口都高)。为什么?因为测距结果如果没及时读取,下一次测距就会覆盖上一次的数据,精度直接崩掉。你想想看,车钥匙在关键时刻测距不准,后果是什么?
3.4 避坑指南
做UWB驱动开发,有几个坑我反复踩过,分享出来,大家少走弯路。
- SPI模式不匹配:UWB芯片通常支持SPI模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1)。配置前,一定要看数据手册。我曾经因为模式配错,通信一直超时,浪费了半天。
- 中断丢失:如果UWB芯片的中断信号是脉冲(短电平),而主控的中断响应不够快,就可能漏掉中断。我建议把UWB芯片的中断配置为电平触发,主控检测到电平变化后,再确认中断源。
- DMA缓冲区对齐:有些MCU的DMA要求缓冲区地址按4字节对齐。如果不对齐,DMA传输会异常。我习惯用
__attribute__((aligned(4)))来声明缓冲区。
总结一下:驱动框架要解耦,SPI通信要优化时序,中断和DMA要配合好。这些基础打牢了,UWB测距精度才能有保障。下一章,我会讲测距算法的实现,包括双边测距(TWR)和到达时间差(TDoA)的实战技巧。