物联网 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 DevKit | 4 块 | ¥25/块 |
| UWB 模块 | DWM3000(DW3000 芯片) | 4 块 | ¥80-120/块 |
| 天线 | UWB PCB 天线(模块自带) | - | 模块集成 |
| 连接线 | SPI 杜邦线 / 排针 | 若干 | ¥5 |
| 电源 | 5V 2A 适配器 × 3 | 3 个 | ¥10/个 |
| 固定架 | 3D 打印支架 | 3 套 | 自制 |
为什么选 4 块? 3 块做锚点(Anchor),1 块做标签(Tag),就能实现三边定位。多出来一块备用——调试时当测试锚点用。
2.1 DW3000 模块引脚
DW3000 模块通常有 12 个引脚,我们核心用到的:
| 引脚 | 功能 | 说明 |
|---|---|---|
| VCC | 电源 | 3.3V(注意!不是 5V) |
| GND | 接地 | 共地必须可靠 |
| SCK | SPI 时钟 | 最高 38MHz |
| MOSI | SPI 主机输出 | |
| MISO | SPI 主机输入 | |
| CS | SPI 片选 | 低电平有效 |
| 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)
- 检查天线延迟校准(最常见的坑)
- 确认两个模块通道号一致(Channel 5)
- 检查周围是否有 2.4GHz WiFi 路由器(干扰)
- 降低数据速率到
DW_DATARATE_850K增加可靠性 - 确保模块间天线朝向一致
Q3:三边定位计算结果乱跳
- 检查锚点是否共线(用
det判断,接近 0 说明共线) - 给每个测距值加权重(距离越近权重越大)
- 引入第四个锚点做冗余校验
- 用 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 开发板 | ¥25 | 4 | ¥100 |
| DWM3000 模块 | ¥90 | 4 | ¥360 |
| 电源线/杜邦线 | - | 若干 | ¥20 |
| 3D 打印外壳 | - | 3 套 | ¥15 |
| 合计 | ¥495 |
一套 4 节点的厘米级定位系统,不到 500 块,性价比拉满。
总结
UWB 是目前消费级方案中定位精度最高的技术,DW3000 把门槛拉到了个人 DIY 能触及的范围。核心要点回顾:
- 测距选 DS-TWR:精度高、抗时钟偏差
- 天线延迟必须校准:这是精度第一坑
- 锚点三角形布局:别共线!
- 卡尔曼滤波平滑:原始数据有抖动很正常
- 供电要稳:3.3V 纹波过大会导致测距飘
下一步可以试试把定位数据推送到 MQTT Broker,在 Grafana 上做实时轨迹大屏,或者接入 Home Assistant 做人在感应。
动手试试吧,厘米级定位没有想象中那么高门槛!