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时,把数据格式设为8位,MSB先行。DW1000的SPI时序要求片选(CS)在传输期间保持低电平,传输结束后拉高。别小看这个细节,我见过有人忘了在每次传输后释放CS,导致模块死锁。

下面是一个典型的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传输时,地址和数据是分步走的。

读写流程其实很简单:

  1. 拉低CS,告诉DW1000:“我要跟你说话了”。
  2. 发送第一个字节:高4位是命令(读0x00,写0x80),低4位是地址的高4位。
  3. 发送第二个字节:地址的低8位。
  4. 如果是写操作:继续发送数据字节(一个或多个)。
  5. 如果是读操作:先发送一个哑字节(0x00),然后接收数据字节。
  6. 拉高CS,结束本次传输。

我刚开始做的时候,总搞不清楚读操作为什么要发一个哑字节。后来看了DW1000的数据手册才明白:DW1000在收到地址后,需要至少一个时钟周期来准备数据。这个哑字节就是用来“等”的。

核心要点: DW1000支持突发读写(Burst Read/Write)。如果你要连续读取多个寄存器的值,可以在发送地址后连续接收数据,地址会自动递增。这在读取时间戳或配置大量参数时非常有用,能大幅提升效率。

下面是一个读写寄存器的函数示例:

// 写寄存器
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();                   // 拉高片选
}
避坑指南: 我曾经在读取系统状态寄存器时,发现读回来的值总是0xFF。排查了半天,原来是CS拉高后没有加一个微秒级的延时。DW1000在CS上升沿后需要一点时间“消化”本次传输,紧接着再次拉低CS会导致通信错乱。解决方案很简单:在CS_HIGH()后加一个1us的延时。

4.3 中断处理机制:让DW1000主动“喊你”

轮询寄存器状态虽然简单,但效率太低。你总不能一直盯着DW1000问:“你收到数据了吗?你发完了吗?”

DW1000提供了丰富的中断源,比如:

  • RXFCG:接收完成且CRC校验通过
  • RXFCE:接收完成但CRC校验失败
  • TXFRS:发送完成
  • RXPTO:接收超时
  • LDEERR:LDE算法错误

我的做法是:在初始化时,把需要的中断使能位写进系统状态寄存器(SYS_MASK_0x0D)。然后,DW1000的IRQ引脚会拉低,通知主控有事件发生。

中断处理流程大致如下:

  1. DW1000拉低IRQ引脚。
  2. 主控进入中断服务函数。
  3. 读取系统事件状态寄存器(SYS_STATUS_0x0F),判断是哪个事件触发了中断。
  4. 处理对应事件(比如读取接收数据、准备下一次发送)。
  5. 关键一步:写1到对应的状态位,清除中断标志。
  6. 退出中断服务函数。
我的习惯: 在中断服务函数里只做最轻量级的工作——读取状态、设置标志位、清除中断。真正的数据处理(比如解析数据帧、计算距离)放到主循环或任务中去处理。这样可以避免中断嵌套导致系统卡死。

下面是一个中断处理的代码骨架:

// 中断服务函数
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寄存器读写 中断处理机制 寄存器读写细节 • 16位地址 + 8位数据 • 读操作需发送哑字节 • 支持突发读写模式 中断处理细节 • 多种中断源可配置 • 写1清零中断标志 • ISR内只做轻量工作 核心原则 SPI是物理通道,寄存器读写是操作方式,中断是效率保障 三者缺一不可,共同构成DW1000的通信基础

这张图把本章的三个核心模块串在了一起。SPI是底层通道,寄存器读写是具体操作,中断处理是高效机制。三者配合,才能让DW1000乖乖听话。