ESP32配DWM3000自制UWB定位:3基站精度10厘米

2026-10-30 19:43:08 95阅读

轮式小车在室内最大的麻烦不是控制,是"我在哪"。GPS 进了屋就废,靠里程计跑十分钟能飘出半米,加雷达又要多花一笔钱多调一套 SLAM。这篇要复现的方案走了另一条路:用 UWB 测距配三个固定基站,直接把位置算出来,标定后精度能到 10 厘米级。整套东西是四块 ESP32 加四个 Qorvo DWM3000 模块,一个当标签贴在车上,三个当基站固定在墙角。

ESP32配DWM3000的UWB室内实时定位系统,右侧为房间平面图上的标签轨迹

原作者是 Dharagesh,六月中旬发的完整教程,代码给得很足——二十多个代码块外加一份九万多字符的完整工程。我把接线、固件、标定、Python 解算这条链路完整走了一遍,顺手把几个会卡住人的地方标出来。

先看硬件:SPI 一根线都不能少

DWM3000 是 Qorvo 的 UWB 模组,里面是 DW3110 芯片,符合 IEEE 802.15.4z 和 FiRa,支持通道 5(6.5 GHz)和通道 9(8 GHz),速率可以跑 850 kbps 或者 6.8 Mbps,SPI 时钟最高 38 MHz,板载 38.4 MHz 晶振,模块本身大概 24×16 毫米。原作者给的系统精度是标定后 10 厘米或更好,量程 200 米——对比一下,蓝牙大概 1 到 3 米,Wi-Fi 5 到 15 米,GPS 是 5 到 10 米且只在室外。

这四个模组和四块 ESP32-WROOM 一对一配对,走 SPI。引脚是死的,接线的时候一条一条对:

VCC 接 3.3V,GND 接 GND,SCK 接 GPIO18,MOSI 接 GPIO23,MISO 接 GPIO19,CS 接 GPIO4,RST 接 GPIO27,IRQ 接 GPIO34(这条可选)。

DWM3000模块引脚定义图,标出3V3、GND、SPI时钟与数据脚位置

这里有个必须先说的坑:DWM3000 只吃 3.3V。手上如果没有 3.3V 输出,直接用 5V 接上去就是烧模块。另外这颗芯片对电源噪声敏感,供电最好干净一点,别跟电机共用一路。

ESP32-WROOM与Qorvo DWM3000的完整SPI接线原理图

四套接法完全一样,Anchor 之间靠固件里的宏区分身份,不用改线。

基站摆哪,决定了你后面所有数据的质量

原作者用的是三个基站,坐标单位厘米,左下 (15, 5)、右下 (290, 5)、上中 (165, 625),房间按 480×650 厘米算。这个摆法不是随便画的:三个点不能共线,否则三边解算会退化成两个答案。三角形越高越"胖",位置解算越稳,所以尽量把基站铺开到房间的角和边上,别全挤在一面墙上。

办公室内三个UWB基站的实拍布置位置,分别固定在两面墙与后方墙面上

按官方这个布置,标签在中间区域活动时精度最好。贴墙走或者被桌子挡住的时候会飘,这是 UWB 的通病——多径和非视距遮挡(NLOS)绕不过去,只能靠滤波缓解。原作者用了 50 点的中值滤波,这个数在代码里的宏是 FILTER_SIZE。

固件:标签和基站烧的不是同一份

这是最容易搞混的一点。四块 ESP32 里的固件分两种,标签一块、基站三块。

先看标签这边的关键定义,`RST_PIN` 和 `CHIP_SELECT_PIN` 就是上表的 GPIO27 和 GPIO4:

// SPI Setup
#define RST_PIN 27
#define CHIP_SELECT_PIN 4
// Scalable Anchor Configuration
#define NUM_ANCHORS 3 // Change this to scale the system
#define TAG_ID 10
#define FIRST_ANCHOR_ID 1 // Starting ID for anchors (1, 2, 3, ...)

注意 NUM_ANCHORS 这个宏。想扩站就改它,加第四个基站不用重写逻辑。后面解算的时候 Python 端也要跟着改成四个点。

测距有效范围也在宏里卡着:

// Ranging Configuration
#define FILTER_SIZE 50 // For median filter
#define MIN_DISTANCE 0
#define MAX_DISTANCE 10000.0

MAX_DISTANCE 给的是 10000,单位是毫米,等于 10 米。房间大过这个尺寸的话得自己调。射频配置那块用的是 CHANNEL_5、前导码 128、前导码序列 9、PAC8、DATARATE_6_8MB、PHR 标准模式、850 kbps 的 PHR 速率——这些参数四块板必须完全一致,改一块就会配不上。

基站那边的固件要简单些,开串口 921600,硬复位,然后设天线延迟 setTXAntennaDelay(16350),再用 setSenderID 把自己的 ID 写进去。每个基站只改这一个 ID。

测距怎么算出来的:ADS-TWR 四步握手

UWB 测距不是量信号强弱,是量时间。这个方案用的是双边双向测距(ADS-TWR),一次完整测量要走四帧:Poll、Response、Final、Report。

ADS-TWR四次握手时序图,标出T_round1、T_reply1、T_round2、T_reply2与ToF

标签在 T1 发 Poll,基站在 T2 收到、T3 回 Response,标签在 T4 收到后于 T5 发 Final,基站在 T6 收到。中间算出两个往返时间 T_round1 和 T_reply1,基站再补一轮 T_round2 和 T_reply2,用这两个往返加时钟偏移就能解出飞行时间 ToF,再乘光速得距离。

这么绕的原因是时钟不同步。标签和基站各自用各自的晶振,频率差一点点,单边测距就废了。ADS-TWR 靠双向往返把时钟偏差消掉,代价是多一帧。原作者的处理是把 Final 帧之后的 T_reply1、T_round2 由基站补发回标签,标签端再用 t_roundA、t_replyA 加 clock_offset 算距离,最后拿 isValidDistance 卡一下上下限。

组装完成的ESP32与DWM3000组合模块正面,用于基站或标签

天线延迟这一项必须标定。板子内部从芯片到天线的路径是有延迟的,不标的话距离会恒定偏几厘米。原作者的参考值是 16350 个 device tick,但这是他那批板子的数,你换板子、换线长、换摆放都可能要重标。标定的做法是拿两个模块放在一个已知距离上(比如正好 1 米),看测出来偏多少,把差值换算成 tick 填回去。

PC 端:三边定位 + 实时画图

标签算完距离之后不自己解位置,而是打包成 JSON 走 Wi-Fi 发出去,PC 开一个 TCP server 监听 7007 端口接。解算用 scipy 的最小二乘,核心就这段:

def trilaterate(distances, anchor_positions):
  """Calculate tag position using trilateration with 3 anchors."""
  def equations(p):
      x, y = p
      return [
          np.sqrt(
              (x - anchor_positions[0][0]) ** 2 + (y - anchor_positions[0][1]) ** 2
          )
          - distances[0],
          np.sqrt(
              (x - anchor_positions[1][0]) ** 2 + (y - anchor_positions[1][1]) ** 2
          )
          - distances[1],
          np.sqrt(
              (x - anchor_positions[2][0]) ** 2 + (y - anchor_positions[2][1]) ** 2
          )
          - distances[2],
      ]
  initial_guess = np.mean(anchor_positions, axis=0)
  try:
      result = least_squares(equations, initial_guess, method="lm")
      return result.x if result.success else None
  except Exception as e:
      print(f"Trilateration error: {e}")
      return None

这段逻辑很直白:拿三个距离各自列一个圆方程,求交。用最小二乘而不是解析解,是因为三边在有噪声的情况下通常不严格交于一点,强行解会出复数。initial_guess 取三个基站坐标的均值,也就是三角形的重心,这个初值对 lm 方法够用了。

画图那部分是把实时坐标叠在房间平面图上,用 matplotlib 的动画,interval 设 100 毫秒。基站坐标在 Python 里是这么写的:

ANCHOR_POSITIONS = np.array([[15, 5], [290, 5], [165, 625]])

这里有个原作者自己写岔了的地方要提醒:正文和这张表都写 A2 是 (290, 5),但某一段示例 JSON 里出现了 "A2": (300.0, 5.0)。以 290 为准,写 Python 的时候别照抄那段 JSON,否则画出来的轨迹会系统性向右偏。

踩坑清单,按我遇到的顺序排

第一,3.3V 只接 3.3V。这条说多少遍都不算多,模组烧了没有替代品。

第二,四块板的射频参数必须逐字一致。通道、前导码长度、前导码序列号、PAC 大小、数据率,改一个就跑不通,症状是能收到帧但算不出距离。

第三,天线延迟不标就是白干。哪怕用原作者的 16350 先跑起来,也要拿卷尺量一段已知距离核对一遍,偏了就重标。

第四,基站别共线,也别全装在一个平面高度上如果房间有高低差的话。几何退化的时候最小二乘会给出一个看着像模像样但完全是错的解,比直接报错更麻烦。

第五,滤波窗口别设太小。FILTER_SIZE 50 意味着前 50 个样本要用完才开始出数,急着看结果容易误判成"不动"。想快速验证的话先临时改小,跑通了再改回来。

这东西对移动机器人到底有多大用

说点实话。UWB 定位不是万能的,它给的是绝对位置,粒度和成本摆在那里:四个模组加四块 ESP32,成本比一套激光雷达低得多,但精度比雷达建图差、刷新率也不如轮速里程计。它的正确用法是跟里程计做融合,用 UWB 兜住长期漂移,用里程计补高频运动,这样两边短板互相补上。

真正省事的地方在于它不需要你的车先"认识"环境。SLAM 得先跑一圈建图,环境一变可能还要重建;UWB 只要基站不动,坐标就是绝对的,车开进来就能用。对于固定场地里跑巡检、送物、做演示的小车,这个特性比精度更重要。

最后一句,这套方案最值得学的地方不在 UWB 本身,在于它把"传感器 → 固件 → 上位机解算 → 可视化"整条链路摊开给你看了。你要是只想给小车加个位置源,把这套东西封成一个串口模块挂到自己的导航栈里也行,没必要照抄整套 Python。


作者:固件烧录仔
链接:https://www.aiddithome.com/p/c5cf01e284b23.html
本作版权归作者所有,未经允许请勿转载。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
6条评论
电烙铁爱好者
1楼 · 2026-10-30 19:43:21

这方案的 Python 可视化做得挺省事,画在平面图上比看数字直观多了。

入门没多久的新手
2楼 · 2026-10-30 19:43:21

作者那个 290 和 300 写岔的地方被你抓出来了,这种细节最容易让人怀疑人生。

锡点
3楼 · 2026-10-30 19:43:20

DWM3000 只吃 3.3V 这条真得画红圈,我朋友第一次上电就闻到味了。

参数党小马
4楼 · 2026-10-30 19:43:20

天线延迟不标真的白干,一米测出一米零五,调半天以为是算法问题。

刷视频进来围观
5楼 · 2026-10-30 19:43:20

三个基站别共线,这个几何退化我吃过亏——解出来的点看着挺顺滑,其实一直在飘。

1 2