|
UWB 精准定位实战:DW3000 室内厘米级追踪系统

UWB 精准定位实战:DW3000 室内厘米级追踪系统

GPS 在室外很好用,但到了室内——仓库、工厂、地下停车场——GPS 信号基本就歇菜了。怎么办?UWB(Ultra Wideband,超宽带)技术来救场。

UWB 的定位精度可以做到 10-30 厘米,远超 WiFi、蓝牙和 Zigbee 的米级精度。加上 DW3000 芯片模块价格已经下探到百元以内,个人 DIYer 也能玩起厘米级定位系统了。今天我们就从零开始,用 ESP32 + DW3000 搭建一套室内厘米级追踪系统。


一、为什么 UWB 能这么准?

1.1 超宽带信号的核心原理

传统窄带通信(比如 WiFi、蓝牙)用的是很窄的频段,信号像一根细激光。而 UWB 在 3.1GHz ~ 10.6GHz 超宽频带上发送 纳秒级脉冲信号,信号像一束散开的光。

这个特性带来两个直接好处:

  • 时间分辨率极高:纳秒级脉冲 = 光速 × 1ns ≈ 30cm 的距离分辨率
  • 多径抗干扰强:短脉冲不容易和反射信号叠加,穿透力强

简单说,WiFi 测距靠信号强度(RSSI),受环境影响大;UWB 靠信号飞行时间(ToF),物理上就更精确。

1.2 测距方式对比

测距方式原理精度延迟
RSSI信号强度衰减估算3-5 米
AOA/AOD信号到达角度1-2 米
TWR(双向测距)往返飞行时间10-30 厘米
TDoA到达时间差10-50 厘米

双向测距(TWR, Two-Way Ranging) 是我们今天的主角。

1.3 DS-TWR 双向测距详解

DW3000 支持 DS-TWR(Double-Sided Two-Way Ranging,双边双向测距),流程如下:

设备 A                        设备 B
  |                              |
  | ---- Poll (t1 发送) -------> |
  |                              | t2 (接收)
  |                              |
  | <--- Response (t3 发送) ---- |
  | t4 (接收)                    |
  |                              |
  | ---- Final (t5 发送) ------> |
  |                              | t6 (接收)
  |                              |

距离计算公式:

Round Trip Time A = t4 - t1
Round Trip Time B = t6 - t3

Reply Time A = t5 - t4
Reply Time B = t3 - t2

飞行时间 ToF = (RT_A × RT_B - Reply_A × Reply_B) / (RT_A + RT_B + Reply_A + Reply_B)

距离 = ToF × 光速 / 2

DS-TWR 的精妙之处在于做了两次往返测量,抵消了设备间时钟偏差,所以精度比单边 TWR 高一个量级。


二、硬件清单

配件型号数量参考价
开发板ESP32-S3 DevKit4 块¥25/块
UWB 模块DWM3000(DW3000 芯片)4 块¥80-120/块
天线UWB PCB 天线(模块自带)-模块集成
连接线SPI 杜邦线 / 排针若干¥5
电源5V 2A 适配器 × 33 个¥10/个
固定架3D 打印支架3 套自制

为什么选 4 块? 3 块做锚点(Anchor),1 块做标签(Tag),就能实现三边定位。多出来一块备用——调试时当测试锚点用。

2.1 DW3000 模块引脚

DW3000 模块通常有 12 个引脚,我们核心用到的:

引脚功能说明
VCC电源3.3V(注意!不是 5V)
GND接地共地必须可靠
SCKSPI 时钟最高 38MHz
MOSISPI 主机输出
MISOSPI 主机输入
CSSPI 片选低电平有效
IRQ中断输出收发完成通知
WAKEUP唤醒低功耗唤醒引脚
RESET复位低电平复位

三、接线与硬件搭建

3.1 ESP32-S3 连接 DW3000

ESP32-S3 和 DW3000 通过 SPI 连接,接线如下:

ESP32-S3          DWM3000
────────          ───────
3V3       ───→    VCC
GND       ───→    GND
GPIO10    ───→    CS   (片选)
GPIO11    ───→    SCK  (时钟)
GPIO12    ───→    MOSI (主机输出)
GPIO13    ───→    MISO (主机输入)
GPIO14    ───→    IRQ  (中断)
GPIO15    ───→    RST  (复位)

3.2 锚点与标签角色分配

          Anchor 1 (0, 0)

             / \
            /   \
           /     \
          /       \
   Anchor 2 ────── Anchor 3
   (5, 0)  ●────────● (2.5, 4.33)
             \
              \
               \
                ● Tag(移动目标)

三边定位需要至少 3 个锚点知道精确坐标,标签与每个锚点分别测距,通过几何计算解出标签坐标。


四、软件搭建

4.1 环境准备

使用 PlatformIO + Arduino 框架:

# 安装 PlatformIO
pip install platformio

# 创建项目
pio init --board esp32-s3-devkitc-1 --project-option "framework=arduino"

platformio.ini 中添加 DW3000 依赖:

[env:esp32-s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
lib_deps =
    thotlabs/DWM3000 @ ^1.0.0
    bblanchon/ArduinoJson @ ^6.21.0
monitor_speed = 115200
upload_speed = 921600

4.2 锚点固件代码

#include <Arduino.h>
#include <SPI.h>
#include <DWM3000.h>

// ============ SPI 引脚定义 ============
#define DW3000_CS   10
#define DW3000_IRQ  14
#define DW3000_RST  15

// ============ 锚点坐标(米)============
#define ANCHOR_X  0.0
#define ANCHOR_Y  0.0
#define ANCHOR_Z  1.2  // 安装高度 1.2m

// ============ 锚点地址 ============
// 每个锚点需要唯一短地址
#define THIS_ADDR   0x0001
#define ANCHOR_2    0x0002
#define ANCHOR_3    0x0003

DWM3000 dwm;
uint8_t this_address[] = {0x01, 0x00};
bool is_anchor = true;

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("=== DWM3000 Anchor Boot ===");
  Serial.printf("Anchor ID: 0x%04X, Position: (%.2f, %.2f, %.2fm)\n",
                THIS_ADDR, ANCHOR_X, ANCHOR_Y, ANCHOR_Z);

  // SPI 初始化
  SPI.begin(11, 13, 10);  // SCK, MISO, CS
  dwm.begin(DW3000_CS, DW3000_IRQ);
  dwm.reset();
  dwm.setDeviceAddress(this_address, 2);

  // 配置 UWB 通道(Channel 5, PRF 64MHz)
  dwm.setChannel(5);
  dwm.setPreambleCode(10);
  dwm.setPreambleLength(DW_PREAMBLE_LEN_128);
  dwm.setDataRate(DW_DATARATE_6M8);

  Serial.println("DWM3000 initialized, ready for ranging...");
}

void loop() {
  // 监听 Tag 发来的 Poll 消息
  if (dwm.available()) {
    uint8_t source_addr[2];
    float distance;

    // 接收并处理
    dwm.read(source_addr, 2);
    distance = dwm.getDistance();

    Serial.printf("[%.3f] Distance to Tag: %.3f m\n",
                  millis() / 1000.0, distance);

    // 通过 Serial 或 WiFi 上报距离数据
    Serial.printf("RANGE:%d:%.3f:%.3f:%.3f\n",
                  THIS_ADDR, ANCHOR_X, ANCHOR_Y, distance);

    dwm.clearInterrupt();
  }
}

4.3 标签(Tag)固件代码

#include <Arduino.h>
#include <SPI.h>
#include <DWM3000.h>
#include <ArduinoJson.h>

#define DW3000_CS   10
#define DW3000_IRQ  14
#define DW3000_RST  15

// 标签地址
uint8_t tag_address[] = {0x10, 0x00};

// 三个锚点短地址
uint16_t anchors[] = {0x0001, 0x0002, 0x0003};
float distances[3] = {0, 0, 0};

// 锚点坐标
float anchor_x[] = {0.0, 5.0, 2.5};
float anchor_y[] = {0.0, 0.0, 4.33};

DWM3000 dwm;
unsigned long last_ranging = 0;
const unsigned long RANGING_INTERVAL = 200;  // 200ms 测距一次

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("=== DWM3000 Tag Boot ===");

  SPI.begin(11, 13, 10);
  dwm.begin(DW3000_CS, DW3000_IRQ);
  dwm.reset();
  dwm.setDeviceAddress(tag_address, 2);

  dwm.setChannel(5);
  dwm.setPreambleCode(10);
  dwm.setPreambleLength(DW_PREAMBLE_LEN_128);
  dwm.setDataRate(DW_DATARATE_6M8);

  Serial.println("Tag ready, starting ranging...");
}

/**
 * 三边定位算法(最小二乘法简化版)
 * 通过三个锚点的距离解算二维坐标
 */
void calculatePosition() {
  // 构建方程组并求解
  // 简化:假设所有锚点在同一平面(z=0)
  float A_mat[3][3] = {
    {2*(anchor_x[1]-anchor_x[0]), 2*(anchor_y[1]-anchor_y[0]), 0},
    {2*(anchor_x[2]-anchor_x[0]), 2*(anchor_y[2]-anchor_y[0]), 0},
    {1, 1, 0}
  };

  float B_vec[3] = {
    pow(distances[0], 2) - pow(distances[1], 2)
    - pow(anchor_x[0], 2) + pow(anchor_x[1], 2)
    - pow(anchor_y[0], 2) + pow(anchor_y[1], 2),

    pow(distances[0], 2) - pow(distances[2], 2)
    - pow(anchor_x[0], 2) + pow(anchor_x[2], 2)
    - pow(anchor_y[0], 2) + pow(anchor_y[2], 2),

    1  // 权重
  };

  // 高斯消元求解(2x2 系统)
  float det = A_mat[0][0] * A_mat[1][1] - A_mat[0][1] * A_mat[1][0];
  if (abs(det) < 0.001) {
    Serial.println("⚠️  三边定位退化(锚点共线)!");
    return;
  }

  float x = (B_vec[0] * A_mat[1][1] - B_vec[1] * A_mat[0][1]) / det;
  float y = (A_mat[0][0] * B_vec[1] - A_mat[1][0] * B_vec[0]) / det;

  // 输出 JSON 格式结果
  StaticJsonDocument<256> doc;
  doc["x"] = x;
  doc["y"] = y;
  doc["dist_a1"] = distances[0];
  doc["dist_a2"] = distances[1];
  doc["dist_a3"] = distances[2];

  char buf[256];
  serializeJson(doc, buf);
  Serial.printf("POSITION: %s\n", buf);
}

void loop() {
  unsigned long now = millis();
  if (now - last_ranging < RANGING_INTERVAL) return;
  last_ranging = now;

  // 依次与三个锚点测距
  for (int i = 0; i < 3; i++) {
    dwm.startRanging(anchors[i]);

    // 等待测距完成(最多 100ms)
    unsigned long start = millis();
    while (!dwm.rangingComplete() && (millis() - start < 100)) {
      delay(1);
    }

    if (dwm.rangingComplete()) {
      distances[i] = dwm.getDistance();
      Serial.printf("Anchor %d: %.3f m\n", i + 1, distances[i]);
    } else {
      Serial.printf("Anchor %d: TIMEOUT\n", i + 1);
      distances[i] = -1;  // 标记失败
    }
  }

  // 三个距离都有效才计算位置
  bool all_valid = true;
  for (int i = 0; i < 3; i++) {
    if (distances[i] < 0) all_valid = false;
  }

  if (all_valid) {
    calculatePosition();
  } else {
    Serial.println("⚠️  距离数据不完整,跳过定位计算");
  }
}

五、测距精度优化技巧

DW3000 标称精度 10cm,但实测时你可能发现抖动到了 50cm 甚至 1m。别急,按下面步骤排查优化:

5.1 校准天线延迟(关键!)

DW3000 的天线内部走线会产生固定延迟,必须校准,否则测距会有系统性偏移。

// 已知距离为 1.0m 时,校准 TX 天线延迟
// 出厂默认值通常是 16626(Channel 5)
// 需要根据实测调整
dwm.setTxAntennaDelay(16626);
dwm.setRxAntennaDelay(16626);

// 校准方法:
// 1. 把两个模块精确放在 1.0m 距离处
// 2. 读取测量值,比如测得 1.15m
// 3. 调整 delay 值直到读数接近 1.0m
// 经验公式:新delay = 旧delay × (实际距离 / 测量距离)

5.2 多径环境处理

// 启用首路径功率检测(First Path Power)
// 帮助区分直达信号和反射信号
dwm.setRXMode(DW_RX_MODE);
dwm.enableFirstPathDetection(true);

// 设置接收灵敏度阈值
// 值越小越灵敏,但误报也越多
dwm.setRXThreshold(10);

5.3 卡尔曼滤波平滑

原始测距数据有抖动,上卡尔曼滤波可以显著平滑:

// 简化的卡尔曼滤波
float kalman_filter(float measurement, float &estimate,
                    float &error, float processNoise,
                    float measurementNoise) {
  // 预测
  float prediction = estimate;
  float predError = error + processNoise;

  // 更新
  float gain = predError / (predError + measurementNoise);
  estimate = prediction + gain * (measurement - prediction);
  error = (1 - gain) * predError;

  return estimate;
}

// 在 loop() 中使用
float raw_distance = dwm.getDistance();
float smoothed_distance = kalman_filter(
    raw_distance, estimate, error, 0.01, 0.1);

5.4 锚点布局要点

❌ 错误布局(共线):

  A1 ●────● A2 ────● A3
     三锚点在同一直线上 → 定位结果严重偏移

✅ 正确布局(三角形):

          A1 ●
            / \
           /   \
          /     \
   A2 ● ──────── ● A3
   三角形布局 → 定位精度最高

黄金法则

  • 锚点间距 ≥ 3m,越大越好
  • 锚点高度一致(建议 1.2-1.5m)
  • 避免金属物体靠近天线(至少 50cm)
  • 标签和锚点之间尽量无遮挡

六、常见问题排查

Q1:测距值全是 0 或者不返回?

可能原因

  • SPI 接线错误或 CS 引脚冲突
  • DW3000 模块供电不足(需要稳定 3.3V)
  • 片选引脚没有拉低

排查步骤

// 添加 SPI 通信诊断
void diag_spi() {
  uint8_t dev_id[2];
  dwm.readDeviceId(dev_id, 2);
  Serial.printf("Device ID: 0x%02X%02X\n", dev_id[0], dev_id[1]);
  // DW3000 的 Device ID 应为 0xDECA03xx
}

Q2:测距值漂移严重(误差 > 50cm)

  1. 检查天线延迟校准(最常见的坑)
  2. 确认两个模块通道号一致(Channel 5)
  3. 检查周围是否有 2.4GHz WiFi 路由器(干扰)
  4. 降低数据速率到 DW_DATARATE_850K 增加可靠性
  5. 确保模块间天线朝向一致

Q3:三边定位计算结果乱跳

  1. 检查锚点是否共线(用 det 判断,接近 0 说明共线)
  2. 给每个测距值加权重(距离越近权重越大)
  3. 引入第四个锚点做冗余校验
  4. 用 EKF(扩展卡尔曼滤波)代替简单三边定位

Q4:功耗太高,电池撑不住?

// 启用 DW3000 深度睡眠
dwm.enterSleep();  // 睡眠电流 ~32μA
delay(5000);        // 5 秒后唤醒
dwm.wakeup();

// 标签端:按需唤醒测距
// 通过外部中断(按键/传感器)触发测距
// 或者用超低功耗定时器(LPUART)定时唤醒

七、进阶应用方向

7.1 PDOA(到达角 + 距离)

DW3000 搭配双天线可测量信号到达角(Phase Difference of Arrival):

// 启用 PDOA 模式
dwm.setPDMode(DW_PDOA_MODE_3);  // 模式 3 = 同时返回距离+角度
float angle = dwm.getPhaseDiff();  // 单位:度

有了角度信息,理论上两个锚点就能定位,比三边定位少一个锚点。

7.2 结合蓝牙/WiFi 做混合定位

室外 → GPS
过渡区 → 蓝牙 AOA
室内 → UWB DW3000

通过 Kalman 融合多源传感器数据,实现室内外无缝定位。

7.3 实际应用场景

  • 仓库资产追踪:叉车、托盘实时位置监控
  • 工厂安全:人员靠近危险区域自动报警
  • 智能家居:人在哪个房间,空调灯光自动切换
  • RoboMaster:机器人精准位置同步(大疆官方方案)
  • 数字钥匙:UWB 车钥匙(BMW/iPhone 已在用)

八、成本核算

项目单价数量小计
ESP32-S3 开发板¥254¥100
DWM3000 模块¥904¥360
电源线/杜邦线-若干¥20
3D 打印外壳-3 套¥15
合计¥495

一套 4 节点的厘米级定位系统,不到 500 块,性价比拉满。


总结

UWB 是目前消费级方案中定位精度最高的技术,DW3000 把门槛拉到了个人 DIY 能触及的范围。核心要点回顾:

  1. 测距选 DS-TWR:精度高、抗时钟偏差
  2. 天线延迟必须校准:这是精度第一坑
  3. 锚点三角形布局:别共线!
  4. 卡尔曼滤波平滑:原始数据有抖动很正常
  5. 供电要稳:3.3V 纹波过大会导致测距飘

下一步可以试试把定位数据推送到 MQTT Broker,在 Grafana 上做实时轨迹大屏,或者接入 Home Assistant 做人在感应。

动手试试吧,厘米级定位没有想象中那么高门槛!