4、UWB基础测距:TWR(双边双向测距)原理、DWM1000驱动移植与初始化

这一章我们聊聊UWB测距最核心的东西——TWR。说白了,就是两个模块怎么通过来回发信号,算出它们之间的距离。我当年第一次接触UWB时,觉得这东西挺玄乎,后来把TWR的时序图画出来一看,其实没那么复杂。

4.1 TWR测距原理:为什么是“双边双向”?

先看单边双向测距(SS-TWR)。设备A发一个数据包,设备B收到后立刻回复。A记录下从发出到收到回复的时间差,再减去B的处理时间,就能算出信号在空中飞了多久。但这里有个坑——B的处理时间必须精确已知,否则误差会很大。

双边双向测距(DS-TWR)就聪明多了。它让A和B各发一次,各收一次,总共四个时间戳。这样即使B的处理时间有偏差,也能通过数学运算抵消掉。嗯,这里要注意,DS-TWR不是简单地把SS-TWR做两次,而是有专门的时序设计。

核心公式:

T_prop = (T_round1 × T_round2 - T_reply1 × T_reply2) / (T_round1 + T_round2 + T_reply1 + T_reply2)

这个公式能自动消除时钟漂移带来的误差。我在项目中实测过,用DS-TWR比SS-TWR的测距精度提升了至少40%。

DS-TWR 时序流程图 设备A (发起方) 设备B (响应方) Poll (发起) T_poll T_reply1 Response (响应) T_response T_reply2 Final (最终) T_final T_round1 T_round2 关键时间戳: T_round1 = T_poll + T_prop + T_reply1 + T_prop T_round2 = T_response + T_prop + T_reply2 + T_prop

4.2 DWM1000模块简介

DWM1000是Decawave(现Qorvo)推出的集成UWB模块。它把天线、射频前端、基带处理器全封装在一个小模块里。你想想看,以前做UWB测距得自己搭射频电路,现在一个模块全搞定。

参数 DWM1000 说明
工作频段 3.5GHz - 6.5GHz 支持6个信道
数据速率 110kbps / 850kbps / 6.8Mbps 速率越低,灵敏度越高
测距精度 ±10cm(理想环境) 实际项目中一般±30cm
接口 SPI 最高20MHz时钟
功耗 发射约31mA,接收约23mA 睡眠模式可到1μA

选型建议:如果项目对功耗敏感,可以考虑DWM1000的睡眠模式。我有个做室内定位手环的朋友,用DWM1000的睡眠+定时唤醒方案,把平均功耗压到了50μA以下。

4.3 DWM1000驱动移植:从官方库到你的MCU

官方提供了DW1000的驱动库,但那是给STM32写的。如果你用其他MCU,就得自己移植。我个人习惯先把底层SPI读写搞定,再一层层往上封装。

4.3.1 底层SPI接口

DWM1000的SPI时序有点特殊。它要求先发一个字节的命令头,再跟数据。命令头的高两位决定操作类型:读、写、或读寄存器。

// 底层SPI读写函数
static int dw1000_spi_read(uint16_t reg_addr, uint8_t *buf, uint16_t len)
{
    uint8_t header[2];
    
    // 命令头:读操作
    header[0] = (uint8_t)((reg_addr & 0x7F) | 0x80);  // 最高位置1表示读
    header[1] = (uint8_t)((reg_addr & 0x3F80) >> 7);
    
    // 片选拉低
    dw1000_cs_low();
    
    // 发送命令头
    spi_transmit(header, 2);
    
    // 读取数据
    spi_receive(buf, len);
    
    // 片选拉高
    dw1000_cs_high();
    
    return 0;
}

static int dw1000_spi_write(uint16_t reg_addr, uint8_t *buf, uint16_t len)
{
    uint8_t header[2];
    
    // 命令头:写操作
    header[0] = (uint8_t)(reg_addr & 0x7F);  // 最高位置0表示写
    header[1] = (uint8_t)((reg_addr & 0x3F80) >> 7);
    
    dw1000_cs_low();
    spi_transmit(header, 2);
    spi_transmit(buf, len);
    dw1000_cs_high();
    
    return 0;
}

注意:DWM1000的SPI命令头是2字节的,不是常见的1字节。我曾经有个同事移植时只发了1字节命令头,结果读出来的数据全是0xFF,排查了整整一天才发现问题。

4.3.2 初始化流程

初始化DWM1000有固定的步骤。我建议按这个顺序来,不要跳步。

  1. 硬件复位:拉低RSTn引脚至少10ms,再拉高。
  2. 等待芯片就绪:读取设备ID寄存器(0x00),确认值为0xDECA0130。
  3. 配置系统参数:设置信道、数据速率、脉冲重复频率等。
  4. 配置天线延迟:校准天线带来的信号延迟。
  5. 使能中断:根据需要使能接收完成、发送完成等中断。
  6. 进入空闲状态:调用dwt_idle()让芯片进入待命模式。
// DWM1000初始化示例
void dwm1000_init(void)
{
    uint32_t dev_id;
    
    // 1. 硬件复位
    dw1000_reset();
    delay_ms(20);
    
    // 2. 软件复位
    dwt_softreset();
    delay_ms(10);
    
    // 3. 检查设备ID
    dev_id = dwt_readdevid();
    if (dev_id != 0xDECA0130) {
        // 设备异常,打印错误
        printf("DWM1000 not found! ID: 0x%08X\n", dev_id);
        while(1);
    }
    
    // 4. 配置默认参数
    dwt_config_t config = {
        .channel = 5,           // 信道5:6489.6MHz
        .prf = DWT_PRF_64M,    // 64MHz脉冲重复频率
        .txPreambleLength = DWT_PLEN_128,
        .rxPAC = DWT_PAC8,
        .txCode = 3,
        .rxCode = 3,
        .nsSFD = 1,
        .dataRate = DWT_BR_6M8, // 6.8Mbps
        .phrMode = DWT_PHRMODE_STD,
        .sfdTO = 129
    };
    dwt_configure(&config);
    
    // 5. 配置天线延迟(需根据实际硬件校准)
    dwt_setantennadelay(0x4050);
    
    // 6. 使能中断
    dwt_setinterrupt(DWT_INT_RXFCG | DWT_INT_TXFRS, 1);
    
    // 7. 进入空闲
    dwt_idle();
    
    printf("DWM1000 initialized successfully!\n");
}

4.4 天线延迟校准:一个容易被忽略的细节

天线延迟校准是测距精度的关键。DWM1000内部有一个天线延迟寄存器,默认值是0x4050。但这个值只适用于官方评估板。你换了自己的天线和PCB布局,这个值就得重新校准。

校准方法其实很简单:把两个模块放在已知距离(比如1米),然后读取测距结果。如果测出来是1.2米,说明天线延迟偏大,需要减小寄存器值。反之亦然。我一般会取10次测量的平均值,再微调寄存器值,直到误差在±5cm以内。

经验值参考:

  • 官方评估板:0x4050
  • 陶瓷天线+50Ω微带线:0x3F80 - 0x4100
  • PCB天线+匹配网络:0x3E00 - 0x4080

注意:这些只是参考值,每块板子都要单独校准。

4.5 初始化后的自检清单

初始化完成后,我建议做一次自检,确认模块工作正常。这是我个人习惯,能省去后面很多调试时间。

检查项 方法 正常结果
设备ID 读取0x00寄存器 0xDECA0130
SPI通信 写一个值再读回来 读写一致
发送测试 发送一个数据包 发送完成中断触发
接收测试 用另一个模块发数据 接收完成中断触发
测距测试 两个模块相距1米 测距值0.95-1.05米

嗯,到这里DWM1000的驱动移植和初始化就完成了。你可能会问,为什么初始化要这么复杂?其实每个步骤都有它的道理。比如天线延迟校准,不做的话测距误差可能到几十厘米。我刚开始做UWB项目时也嫌麻烦,后来被现场测试数据教育了一回,从此老老实实按流程走。