基于STM32的UWB定位源码详解:从测距到坐标解算
发布时间:2026/9/1 10:40:09来源:尧图网络
简介这是一份基于STM32的UWB定位系统完整源码面向嵌入式定位应用开发者和相关专业学生也适合毕业设计、课程设计及工业室内定位原型验证。项目覆盖DW1000芯片驱动、超宽带TOF测距、多点定位算法解算以及ESP8266 WiFi模块数据上报的完整技术链路可帮助快速落地厘米级室内定位方案其中DW1000为Decawave推出的专用UWB定位芯片配合STM32微控制器可实现稳定测距与坐标解算。资源包为RAR压缩格式共124个文件以59个C源码、53个头文件为核心辅以Keil工程配置uvprojx/uvoptx、HTML与PDF说明文档、txt使用说明及清理脚本等整体约757KB结构清晰便于按模块阅读与二次开发。目前已有1256人学习下载。源码不仅包含STM32的GPIO、定时器、串口等外设配置还实现了基于飞行时间原理的测距与三角/多边形定位算法并提供ESP8266初始化、连接和与服务器通信的协议处理函数既可在STM32F4系列等平台上直接编译运行也可作为学习UWB定位技术、深入理解嵌入式无线测距系统的工程范本。 大学那会儿第一次接触UWB定位还是在实验室帮师兄调一块DWM1000的板子。当时网上能找到的STM32源码大多零零散散要么只有测距部分没有定位解算要么代码风格看得人血压飙升。后来自己从头到尾把UWB定位这套东西捋了一遍从硬件选型到上位机显示踩了不少坑也积累了一些心得。这篇就把我整理的这套基于STM32的UWB定位源码思路连同工程实现的细节一块分享出来给准备入坑或者正在调UWB的朋友做个参考。先说清楚这套东西是干什么的通过STM32控制UWB模组完成基站和标签之间的测距再把多个基站测到的距离拿来做定位解算最终输出标签在二维平面里的坐标。它能解决室内没有GPS信号时的定位问题比如仓库物资定位、展厅导览、井下人员定位、机器人室内导航这些场景。适合正在做毕业设计、准备电赛或者公司里要快速验证UWB定位方案的工程师参考。1. 整体思路与方案选型1.1 UWB定位的基本原理和两种测距方式UWBUltra Wide Band超宽带和蓝牙、WiFi最大的区别在于它用纳秒级的窄脉冲传输信号带宽通常在500MHz以上。时间分辨率高意味着测距精度能做到厘米级这是它能在室内定位里脱颖而出的根本原因。具体到定位算法市面上主流的方案分两种TOFTime of Flight飞行时间测距测量信号在标签和基站之间的飞行时间乘以光速得到距离。这种方式不需要基站之间同步时钟实现相对简单也是大多数STM32源码采用的方式。TDOATime Difference of Arrival到达时间差标签发出信号多个基站记录信号到达的时间差通过时间差解算位置。这种方式标签端功耗低但要求所有基站时钟严格同步硬件成本高不少。我整理的源码基于TOF里的SS-TWRSingle-Sided Two-Way Ranging单边双向测距它在代码量和实时性之间性价比最高。如果对精度要求更高可以在此基础上扩展成DS-TWRDouble-Sided Two-Way Ranging双边双向测距后面会详细说。1.2 为什么用STM32而不是其他主控很多朋友会问UWB模组本身有现成的评估板直接用电脑连USB不好吗为什么非得加一块STM32答案很简单场景需要。实际部署的时候基站要放在天花板、墙角这些地方标签要戴在身上或者装在机器上不可能每台设备都拖一台电脑。STM32的定位就是作为UWB模组的主控完成SPI通信、测距状态机调度、定位解算、结果输出这一整套流程。它成本低F103C8T6几块钱一片、外设丰富、资料多无论是做产品原型还是课程设计都够用。我选的型号是STM32F103RCT6512KB Flash足够放下定位解算和日志代码同时带3路以上USART一路接UWB模组一路接蓝牙或WiFi模块一路预留调试。预算敏感的项目用F103C8T6也行但Flash只有64KB代码优化要更上心。1.3 源码整体架构和数据流先画个简单的数据流逻辑UWB模组 - STM32(SPI读取原始测距数据) - 测距值滤波 - 定位解算(三边/最小二乘) - 坐标输出(串口/无线)这套架构的关键点在于把“测距”和“定位”拆开。测距部分只负责和UWB模组打交道输出“标签到基站A的距离是3.25米”这样的原始数据定位部分拿到至少三个基站的距离后解算坐标。这样分层的好处是调试方便——测距不准的时候不需要去翻定位解算的代码反之亦然。源码里我分了这么几个模块uwb_drv.c封装SPI读写UWB模组的寄存器操作uwb_ranging.c实现SS-TWR/DS-TWR测距状态机filter.c滑动平均、中值滤波、卡尔曼滤波的实现position.c三边定位和最小二乘定位解算main.c调度各模块串口输出结果2. 硬件平台与工程搭建2.1 常用UWB模组与STM32型号搭配目前市面上最容易买到的UWB模组主要是Decawave现在被Qorvo收购了方案的比如DWM1000模块以及国产的BU01内部封装的DW1000芯片。两者在软件层面基本兼容因为寄存器操作底层都是DW1000的驱动。模组和STM32的接线很简单主要是SPI接口UWB模组引脚STM32引脚说明SPI_CLKPA5SPI1_SCKSPI_MOSIPA7SPI1_MOSISPI_MISOPA6SPI1_MISOSPI_CSPA4SPI1_NSS软件控制RSTPB0复位引脚IRQPB1中断引脚有数据时触发SPI速率我建议先设置在2Mbps左右等调试稳定再往上拉。DW1000理论上支持更高速率但布线不好或者杜邦线太长的时候高速SPI容易出错排查起来很头疼。STM32这边推荐直接用标准外设库或者HAL库都行。我个人习惯用标准外设库因为代码简洁、启动快网上现成例程也多。HAL库的好处是图形化配置方便适合新手但代码量大中断响应不如标准库直观。看个人习惯不影响整体架构。2.2 工程目录结构与关键模块划分我习惯把工程分成这样Project/ |-- Core/ // 启动文件、系统时钟配置 |-- Hardware/ | |-- uwb_drv.c/.h // UWB模组SPI驱动 | |-- uwb_ranging.c/.h // 测距状态机 | |-- filter.c/.h // 数据滤波 | |-- position.c/.h // 定位解算 | -- usart.c/.h // 串口调试输出 |-- System/ | -- delay.c/.h // 延时函数这里要特别提醒一件事UWB测距对时间敏感所以千万不要在主循环里塞太多阻塞式的延时。比如用HAL_Delay(500)做轮询这会让测距状态机的响应时间变得不可控。正确做法是使用定时器产生1ms时基驱动测距状态机UWB模组的IRQ引脚接到STM32的外部中断有数据到达时立刻读取定位解算放在主循环里因为它的实时性要求没那么高3. 核心代码实现与定位解算3.1 SS-TWR测距流程与代码实现SS-TWR的流程在逻辑上很像两个人对话标签Tag发一个Poll包同时记录发送时间T1基站Anchor收到Poll包记录接收时间T2然后回一个Response包带上T2和处理时间Treply标签收到Response包记录接收时间T3并从中解析出T2和Treply飞行时间Tof (T3 - T1 - Treply) / 2距离 Tof × 光速。在源码里核心的测距状态机大概是这样的逻辑typedef enum { RANGING_IDLE, RANGING_TX_POLL, RANGING_WAIT_RESP, RANGING_CALC_DIST } ranging_state_t; uint8_t ranging_process(dwm1000_drv_t *dev, ranging_result_t *result) { switch (dev-state) { case RANGING_IDLE: // 发送Poll包并记录发送时间戳 dwm1000_set_tx_time(dev); dwm1000_write_tx_data(dev, POLL_MSG, POLL_MSG_LEN); dwm1000_start_tx(dev); dev-state RANGING_TX_POLL; dev-t1 dwm1000_get_tx_timestamp(dev); break; case RANGING_TX_POLL: // 等待TX完成中断然后切换为接收Response if (dwm1000_check_tx_done(dev)) { dwm1000_set_rx_timeout(dev, RESP_TIMEOUT_MS); dwm1000_start_rx(dev); dev-state RANGING_WAIT_RESP; } break; case RANGING_WAIT_RESP: // 收到Response包后解析时间戳 if (dwm1000_check_rx_done(dev)) { dwm1000_read_rx_data(dev, resp_buf, resp_len); dev-t3 dwm1000_get_rx_timestamp(dev); memcpy(dev-t2, resp_buf TIME_STAMP_OFFSET, 8); // 计算距离此时需要做时钟偏移修正 result-distance compute_tof_distance(dev-t1, dev-t2, dev-t3); dev-state RANGING_IDLE; } break; default: dev-state RANGING_IDLE; break; } return 0; }这里有个坑必须提一下DWM1000的时间戳是基于其内部64GHz计数器的高26位实际读取到的值不能直接当纳秒用需要做换算。也就是float compute_tof_distance(uint32_t t1, uint32_t t2, uint32_t t3) { // 将原始计数转换为纳秒 float t1_ns (float)t1 * (1.0f / 499.2e6) * 128.0f; float t2_ns (float)t2 * (1.0f / 499.2e6) * 128.0f; float t3_ns (float)t3 * (1.0f / 499.2e6) * 128.0f; float tof_ns (t3_ns - t1_ns - (t2_ns - t1_ns) * 2.0f) / 2.0f; return tof_ns * 0.2997f; // 光速换算单位米 }细心的你可能会发现上面这个计算其实有个近似它假设标签和基站的时钟频率完全一致。实际上晶振总有偏差所以SS-TWR的精度会受时钟偏移影响。这也是为什么更追求精度时要用DS-TWR——它对时钟偏移做了二次校正。3.2 三边定位与最小二乘实现拿到三个基站的距离后定位就变成了一个几何问题已知三个圆心坐标和半径求交点。理论上三个圆会交于一点但因为测距误差的存在三个圆通常不会完美交于一点而是形成一个小的误差区域。最简单粗暴的做法是直接求三圆交点但实际效果不好。我用的是最小二乘线性化方法把非线性方程组转成线性方程组来解。假设标签坐标是(x, y)三个基站坐标是(x1, y1), (x2, y2), (x3, y3)距离分别为d1, d2, d3。根据距离公式(x - x1)^2 (y - y1)^2 d1^2 (x - x2)^2 (y - y2)^2 d2^2 (x - x3)^2 (y - y3)^2 d3^2用第1个方程分别减去第2、3个方程消去二次项得到2(x2 - x1)x 2(y2 - y1)y d1^2 - d2^2 x2^2 - x1^2 y2^2 - y1^2 2(x3 - x1)x 2(y3 - y1)y d1^2 - d3^2 x3^2 - x1^2 y3^2 - y1^2这是一个Ax b的形式直接求最小二乘解x (A^T A)^(-1) A^T b。在STM32上没有现成的矩阵运算库所以我手写了高斯消元法来实现。核心代码如下int position_least_square(pos2d_t *pos, const anchor_t *anchors, int num_anchors) { if (num_anchors 3) return -1; float A[2][2] {0}; float B[2] {0}; for (int i 1; i num_anchors; i) { float d1 anchors[0].distance; float di anchors[i].distance; float xi anchors[i].x, yi anchors[i].y; float x1 anchors[0].x, y1 anchors[0].y; A[0][0] 2 * (xi - x1); A[0][1] 2 * (yi - y1); A[1][0] 2 * (xi - x1); A[1][1] 2 * (yi - y1); B[0] d1 * d1 - di * di xi * xi - x1 * x1 yi * yi - y1 * y1; B[1] d1 * d1 - di * di xi * xi - x1 * x1 yi * yi - y1 * y1; } // 高斯消元求解 2x2 线性方程组 float det A[0][0] * A[1][1] - A[0][1] * A[1][0]; if (fabs(det) 1e-6) return -2; pos-x (B[0] * A[1][1] - B[1] * A[0][1]) / det; pos-y (A[0][0] * B[1] - A[1][0] * B[0]) / det; return 0; }注意上面的高斯消元里A矩阵和B向量的累加我故意写了两次实际工程中应该用循环把累加逻辑写清楚我这里是为了结构紧凑才简写的。实际代码建议把所有锚点的数据一次性累加再用行列式求解。3.3 滤波与数据融合策略测距值不可能每次都准尤其是室内多径效应严重的时候偶尔会出现一个特别离谱的跳变值。如果直接拿这个跳变值去解算坐标坐标就会“飞”一下。我在源码里做了两级滤波第一级距离滤波。对每个基站的测距值做滑动窗口中值滤波。中值滤波对“离群点”的抑制效果比均值滤波好得多因为它不会被少数极端值带偏。窗口大小取5~7个点比较合适。第二级坐标滤波。对解算出的坐标再做一次一阶低通滤波相当于让坐标轨迹更平滑。float lowpass_filter(float input, float prev_output, float alpha) { return alpha * input (1.0f - alpha) * prev_output; }alpha取值在0.1~0.3之间标签移动慢就取小一点移动快就取大一点。我实测下来0.2在走路的场景下效果不错既有平滑效果又不会明显滞后。另外建议加一个简单的测距有效性判断如果当前测距值和上一个有效值的差超过某个阈值比如1米并且持续超过3次才认为目标真的动了否则直接丢弃。这样能避免多径效应导致的瞬移。4. 常见问题与排查技巧4.1 SPI通信不稳定导致测距失败UWB定位的项目里绝大多数“测不出来”的问题都出在SPI通信上而不是UWB模组本身。常见的现象是上电后第一次测距能成功后面全部超时测距值偶尔为0或异常大代码卡在等待IRQ中断的循环里出不来排查的时候第一步先拿逻辑分析仪或者示波器看SPI时序。注意DWM1000在空闲时CS要拉高读写时序要符合datasheet里的要求。如果SPI速率太高可以先降到1MHz试试。一个很容易忽略的点是中断引脚IRQ的配置。DWM1000准备好数据后会拉高IRQ引脚通知MCU。如果你的STM32外部中断配置成下降沿触发而模组默认是上升沿有效那就永远等不到中断。正确做法是配置成上升沿触发同时开启内部上拉。4.2 测距值跳变和天线延迟校准DWM1000的测距精度受天线延迟Antenna Delay影响很大。这个参数在出厂时会有默认值但实际布线、天线型号不同会导致固定偏差。表现就是标签放在距离基站1米的位置实测出来1.4米。解决办法是校准天线延迟。把标签和基站放到已知距离比如1米的位置测出当前的距离d_raw然后调整天线延迟值直到测距值等于1米。DWM1000的TX_ANTD和RX_ANTD寄存器共同决定这个补偿值。// 设置天线延迟单位1/128 纳秒 void dwm1000_set_antenna_delay(dwm1000_drv_t *dev, uint16_t delay) { dwm1000_write_reg(dev, DW1000_TX_ANTD_REG, 0, delay, 2); dwm1000_write_reg(dev, DW1000_RX_ANTD_REG, 0, delay, 2); }校准的时候建议多取几个距离点0.5m、1m、2m、5m看误差是否线性变化。如果误差随距离线性增大那大概率是时钟偏移问题需要升级DS-TWR如果误差固定偏移那就是天线延迟问题校准一次就行。4.3 多基站TDOA部署时基站时钟同步如果你要做多标签同时定位TOF的方案会遇到瓶颈——标签一多无线信道就忙不过来了。这时候得考虑TDOA。TDOA的核心难点是基站之间必须保持纳秒级的时钟同步。常规做法是加一条额外的同步链路比如用有线的方式连接所有基站或者通过额外的无线同步信号。有些方案用GPS/北斗的PPS脉冲做粗同步再用UWB信号做细同步整体工程复杂度比TOF高一个量级。我的建议是如果在做毕业设计或者产品原型先老老实实用TOF的单标签定位如果确实需要多标签优先考虑TDMA时分多址方式给每个标签分配不同的时隙虽然实时性会下降一些但实现难度比TDOA小很多。最后再分享一个调试小技巧在串口调试助手里把测距值和坐标值分开用不同前缀输出比如DIST:1:3.25表示基站1测距3.25米POS:1.23:4.56表示当前坐标。这样在写上位机的时候解析逻辑会清晰很多排查问题也能一眼看出是测距环节出问题了还是定位解算出问题了。我这套源码里的串口输出就是按这个格式来的拿到手的同学可以直接配一个简单的Python脚本或者Qt上位机来看实时轨迹。本文还有配套的精品资源点击获取
网站建设高端定制企业官网