4、基础通信协议:SPI通信配置、DW1000寄存器读写、中断处理机制
好,咱们进入第四章。这一章是硬骨头,也是所有后续功能的基础。
你想想看,UWB模块DW1000虽然功能强大,但它自己不会说话。主控芯片(比如STM32)要跟它沟通,靠的就是SPI总线。SPI搞不定,后面什么测距、定位全是空中楼阁。
我个人习惯,拿到一个新模块,第一件事就是先把它的通信链路打通。就像盖房子先打地基,这一步稳了,后面才踏实。
4.1 SPI通信配置:主控与DW1000的“对话通道”
DW1000的SPI接口支持模式0和模式3。我一般用模式0,也就是CPOL=0、CPHA=0。时钟极性空闲时为低,数据在第一个时钟沿采样。
这里有个关键点:DW1000的SPI时钟频率最高能到20MHz。但实际项目中,我建议你先从低一点的速度开始调,比如2MHz或4MHz。为什么?
因为PCB走线、排针连接、线缆长度都会引入干扰。我在一个项目里遇到过,SPI跑10MHz时读写偶尔出错,降到8MHz就稳如老狗。所以,先求稳,再求快。
下面是一个典型的SPI初始化代码片段(基于STM32 HAL库):
// SPI句柄定义
SPI_HandleTypeDef hspi1;
// SPI初始化函数
void MX_SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER; // 主模式
hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工
hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8位数据
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件管理片选
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 分频系数
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // MSB先行
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
}
嗯,这里要注意:DW1000的片选引脚必须由GPIO控制,不能用SPI硬件的NSS。因为我们需要在读写前后精确控制CS的时序。
4.2 DW1000寄存器读写:从“敲门”到“对话”
SPI通道打通了,接下来就是怎么读写DW1000的寄存器。DW1000的寄存器地址是16位的,但SPI传输时,地址和数据是分步走的。
读写流程其实很简单:
- 拉低CS,告诉DW1000:“我要跟你说话了”。
- 发送第一个字节:高4位是命令(读0x00,写0x80),低4位是地址的高4位。
- 发送第二个字节:地址的低8位。
- 如果是写操作:继续发送数据字节(一个或多个)。
- 如果是读操作:先发送一个哑字节(0x00),然后接收数据字节。
- 拉高CS,结束本次传输。
我刚开始做的时候,总搞不清楚读操作为什么要发一个哑字节。后来看了DW1000的数据手册才明白:DW1000在收到地址后,需要至少一个时钟周期来准备数据。这个哑字节就是用来“等”的。
下面是一个读写寄存器的函数示例:
// 写寄存器
void DW1000_WriteReg(uint16_t regAddr, uint8_t *data, uint16_t len)
{
uint8_t header[2];
header[0] = 0x80 | ((regAddr >> 8) & 0x0F); // 写命令 + 地址高4位
header[1] = regAddr & 0xFF; // 地址低8位
CS_LOW(); // 拉低片选
HAL_SPI_Transmit(&hspi1, header, 2, 100); // 发送地址
HAL_SPI_Transmit(&hspi1, data, len, 100); // 发送数据
CS_HIGH(); // 拉高片选
}
// 读寄存器
void DW1000_ReadReg(uint16_t regAddr, uint8_t *data, uint16_t len)
{
uint8_t header[2];
header[0] = 0x00 | ((regAddr >> 8) & 0x0F); // 读命令 + 地址高4位
header[1] = regAddr & 0xFF; // 地址低8位
CS_LOW(); // 拉低片选
HAL_SPI_Transmit(&hspi1, header, 2, 100); // 发送地址
HAL_SPI_Receive(&hspi1, data, len, 100); // 接收数据
CS_HIGH(); // 拉高片选
}
4.3 中断处理机制:让DW1000主动“喊你”
轮询寄存器状态虽然简单,但效率太低。你总不能一直盯着DW1000问:“你收到数据了吗?你发完了吗?”
DW1000提供了丰富的中断源,比如:
- RXFCG:接收完成且CRC校验通过
- RXFCE:接收完成但CRC校验失败
- TXFRS:发送完成
- RXPTO:接收超时
- LDEERR:LDE算法错误
我的做法是:在初始化时,把需要的中断使能位写进系统状态寄存器(SYS_MASK_0x0D)。然后,DW1000的IRQ引脚会拉低,通知主控有事件发生。
中断处理流程大致如下:
- DW1000拉低IRQ引脚。
- 主控进入中断服务函数。
- 读取系统事件状态寄存器(SYS_STATUS_0x0F),判断是哪个事件触发了中断。
- 处理对应事件(比如读取接收数据、准备下一次发送)。
- 关键一步:写1到对应的状态位,清除中断标志。
- 退出中断服务函数。
下面是一个中断处理的代码骨架:
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
{
uint32_t status;
// 读取系统事件状态
DW1000_ReadReg(SYS_STATUS_ID, (uint8_t *)&status, 4);
// 判断接收完成事件
if(status & SYS_STATUS_RXFCG_MASK)
{
rx_flag = 1; // 设置接收标志
// 清除中断标志(写1清零)
DW1000_WriteReg(SYS_STATUS_ID, (uint8_t *)&SYS_STATUS_RXFCG_MASK, 4);
}
// 判断发送完成事件
if(status & SYS_STATUS_TXFRS_MASK)
{
tx_flag = 1; // 设置发送标志
DW1000_WriteReg(SYS_STATUS_ID, (uint8_t *)&SYS_STATUS_TXFRS_MASK, 4);
}
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}
}
你可能会问:为什么清除中断标志是写1而不是写0?
这是DW1000的设计特点。它的状态寄存器是“写1清零”的。你写1到某个位,该位就会被清零。如果你写0,则不会有任何变化。我第一次用的时候也踩过这个坑,写0清不掉中断,IRQ引脚一直拉低,程序直接卡死在中断里。
4.4 知识体系总览
说了这么多,咱们用一张图把这一章的核心逻辑串起来:
这张图把本章的三个核心模块串在了一起。SPI是底层通道,寄存器读写是具体操作,中断处理是高效机制。三者配合,才能让DW1000乖乖听话。