第4章 测距算法实现:双边双向测距(DS-TWR)

好,咱们进入正题。DS-TWR,全称是Double-Sided Two-Way Ranging。名字挺长,说白了就是「两边各发一次,来回测两次」。为什么要这么折腾?因为单边测距(SS-TWR)有个硬伤——时钟误差会直接吃掉你的精度。

我记得刚入行那会儿,第一次用UWB做车钥匙定位,图省事用了单边测距。结果呢?车停在跟前,钥匙显示距离5米。我当时就懵了。后来才明白,晶振那点漂移,在单边测距里会被放大得不成样子。

核心观点:DS-TWR不是炫技,是刚需。它通过两次往返测量,把时钟误差的影响降到最低。

4.1 原理:为什么非要「双边」?

先看单边测距的问题。设备A发一个包,设备B回一个包。A记录下发送和接收的时间戳,算出往返时间。听起来没问题,对吧?但问题在于——A和B的时钟不是同步的。哪怕晶振标称20ppm,实际跑起来偏差可能更大。

你想想看,如果A的时钟比B快0.01%,那测出来的时间差就会跟着偏。在短距离(比如10米以内)可能还好,但车钥匙场景下,我们要的是厘米级精度。这点偏差就受不了。

DS-TWR怎么解决?它让A和B各发起一次测距。A发一次,B回一次;然后B发一次,A回一次。这样我们得到两个往返时间,再做个加权平均。时钟误差在两次测量中方向相反,一平均就抵消了大半。

公式其实不复杂:

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

嗯,看着有点唬人。但你仔细看,分子是两个往返时间乘积减去两个回复时间乘积,分母是四个时间之和。这个公式的妙处在于——它几乎不依赖时钟的绝对精度。

我的经验:实际项目中,用DS-TWR配合好的晶振(±10ppm以内),测距误差可以控制在±5cm以内。这在车钥匙场景里已经非常够用了。

4.2 代码实现:从原理到落地

理论说完了,咱们看代码。我用的是Decawave的DW1000芯片,这是目前UWB车钥匙方案里的主流选择。代码基于C语言,跑在STM32上。

先定义几个关键结构体:

typedef struct {
    uint64_t tx_time;      // 发送时间戳
    uint64_t rx_time;      // 接收时间戳
    uint32_t sequence;     // 帧序号
} uwb_timestamp_t;

typedef struct {
    uwb_timestamp_t poll;      // A发起的轮询
    uwb_timestamp_t response;  // B的响应
    uwb_timestamp_t final;     // A的最终消息
} ds_twr_session_t;

这里要注意,时间戳是64位的,单位是UWB的时钟周期(约15.65皮秒)。别想着用微秒,精度不够。

接下来是核心的测距函数:

float ds_twr_calculate_distance(ds_twr_session_t *session) {
    uint64_t t_round1, t_reply1, t_round2, t_reply2;
    uint64_t t_prop_raw;
    float distance;
    
    // 计算第一次往返
    t_round1 = session->response.rx_time - session->poll.tx_time;
    t_reply1 = session->response.tx_time - session->response.rx_time;
    
    // 计算第二次往返
    t_round2 = session->final.rx_time - session->response.tx_time;
    t_reply2 = session->final.tx_time - session->final.rx_time;
    
    // DS-TWR核心公式
    t_prop_raw = (t_round1 * t_round2 - t_reply1 * t_reply2) 
               / (t_round1 + t_round2 + t_reply1 + t_reply2);
    
    // 转换为米(光速约3e8 m/s,时间单位是15.65e-12 s)
    distance = (float)t_prop_raw * 15.65e-12 * 3e8;
    
    return distance;
}

这段代码我调试了整整两天。你猜问题出在哪?溢出。t_round1 * t_round2这个乘法,两个64位数相乘,结果可能超过128位。我一开始没注意,结果算出来的距离忽大忽小。

避坑指南:我曾经在乘法运算上栽过跟头。建议用128位中间变量,或者分段计算。DW1000的SDK里其实有现成的64位乘法函数,别自己手写。

4.3 误差分析:哪些因素在偷你的精度?

代码写好了,跑起来也能出数。但你真的信任这个数吗?我建议你做个误差分析。下面这个表是我在实际测试中总结的:

误差来源 典型影响 我的应对方法
时钟漂移 ±2~5cm 用DS-TWR + 温补晶振
多径效应 ±10~30cm 首径检测 + 信道滤波
天线延迟 固定偏差(5~15cm) 出厂校准 + 查表补偿
帧处理延迟 ±1~3cm 硬件时间戳,避免软件延迟

你看,多径效应是最大的坑。车钥匙在车里使用时,信号会在金属车身、座椅、玻璃之间来回反射。接收到的第一个信号可能不是直射路径,而是反射路径。这会导致测出来的距离偏大。

怎么解决?DW1000有个功能叫「首径检测」(First Path Detection)。它会分析接收信号的脉冲形状,找到最早到达的那个路径。我建议你把这个功能打开,并且配合信道冲激响应(CIR)做二次确认。

还有一个容易被忽略的点——天线延迟。每根天线的物理特性不同,信号从芯片到天线辐射出去需要时间。这个延迟是固定的,但不同天线差异可能达到几纳秒。换算成距离就是几十厘米。

我的习惯:每块板子在出厂前都做一次天线延迟校准。方法很简单——把两块板子放在已知距离(比如1米),测出来的距离减去真实距离,就是天线延迟的等效距离。把这个值写到Flash里,每次上电读出来补偿。

4.4 实战中的几个小技巧

最后分享几个我在项目中积累的经验:

  • 帧间隔别太短。我见过有人把轮询间隔设到1ms,结果信道冲突严重。建议至少10ms以上。
  • 做多次测量取中位数。单次测量可能有野值,连续测5次取中位数,稳定性好很多。
  • 注意温度变化。晶振的频率会随温度漂移。车钥匙在夏天车内可能到70°C,冬天零下20°C。我建议做温度补偿,或者至少在不同温度下标定一次。
  • 别迷信理论精度。芯片手册上写的±10cm,那是在实验室理想环境下。实际车钥匙场景,能做到±15cm已经很不错了。

嗯,DS-TWR这块就讲这么多。说白了,原理不复杂,但落地时细节很多。你只要把时钟误差、多径效应、天线延迟这三个大头管住了,精度基本就稳了。下一章咱们聊怎么把测距结果用起来——也就是定位算法。