基于Arduino和BLE4.0的蓝牙RSSI室内定位系统实战解析 做蓝牙室内定位这个项目起因其实很朴素仓库里要找一个两台货架之间的小工具车人站在过道里凭记忆找了二十分钟。当时手边正好有一堆Arduino板子和几个BLE4.0模块就顺手搭了一套基于蓝牙RSSI的定位系统没想到一路调试下来踩坑无数也把原理吃透了不少。这套东西是什么一句话说清楚用Arduino Uno做接收端三个BLE4.0模块做固定信标通过解析蓝牙广播里的RSSI信号强度值推算出接收端在房间里的实时坐标。它解决的核心问题就是“室内没有GPS时怎么知道自己/物体在哪个位置”适合做仓储找物、巡检机器人粗略导航、展馆人员区域判断这些场景也是学生项目和创客作品里性价比最高的室内定位方案之一。先说结论免得大家对“厘米级”有过高期待RSSI定位在理想环境下、经过仔细标定和滤波静态重复精度可以做到20-40厘米动态移动时误差会到1米左右。真正要做到厘米级得用UWB或者BLE5.1的AOA测向方案预算也完全不是一个量级。所以这篇文章里说的“厘米级”指的就是这套低成本系统能达到的极限状态并不是拿来碰瓷工业级定位设备的。1. 项目定位与原理拆解为什么RSSI能定位又不能“指哪打哪”1.1 实话实说厘米级到底会不会翻车先说清楚RSSI这个东西的脾性。RSSI是接收信号强度指示单位是dBm数值越接近0代表信号越强。蓝牙、Wi-Fi、ZigBee这些2.4GHz频段的无线设备理论上信号强度会随着距离增加而衰减所以只要测出RSSI再套用无线信号传播模型就能反推出距离。这个逻辑本身没问题但现实环境里有太多变量墙壁反射、金属货架的回波、人体含水组织对2.4GHz信号的吸收、天线朝向不一致全都会让RSSI上下剧烈跳动。我第一天拿裸RSSI当距离用的时候接收端固定在离信标3米的位置实测读数是-54dBm到-68dBm之间反复横跳换算成距离有时候2米有时候5米直接拿这个数据去做三边定位输出坐标在房间里乱飞跟跳舞一样。所以你如果打算照着某些教程抄一遍就期望定位点乖乖跟在手上基本不现实。RSSI定位真正落地依赖两个前提一是把无线传播模型里的参数做现场标定不能随便套网上的默认值二是必须对RSSI时序数据做滤波处理把跳动的毛刺压下去。这两步不做再好的算法也白搭。从应用场景上说这套方案的甜区是仓库区域级定位、室内机器人回桩、展览馆导览、停车场找车这种“知道你在哪个片区/哪条过道”的颗粒度。它不适合用来给AGV做精确停靠也不适合做手术器械定位这种对安全性要求极高的场景。1.2 从dBm到坐标RSSI定位背后的无线数学RSSI换成距离最常用的模型是对数距离路径损耗模型公式很简单RSSI A - 10 * n * lg(d)这里的A是距离信标1米处测到的RSSI参考值单位dBmn是路径损耗指数它描述信号随距离衰减的快慢程度d就是离信标的距离单位米。把公式反解一下就能得到距离d 10 ^ ((A - RSSI) / (10 * n))注意这个公式里的RSSI是已经做过滤波之后的稳定值不是单次采样。A和n也不是固定常数跟现场环境强相关。开阔空间的n大约等于2有隔断的办公区可能到2.5-3.5金属货架多的地方能到4以上。所以“现场标定”不是可选项是必选项后面我会专门讲标定操作。拿到三个信标的距离之后就可以做三边定位。原理是几何学里的三圆相交假设三个信标坐标分别是(x1,y1)、(x2,y2)、(x3,y3)测出来的距离分别是r1、r2、r3那目标点(x,y)需要同时满足(x - x1)^2 (y - y1)^2 r1^2 (x - x2)^2 (y - y2)^2 r2^2 (x - x3)^2 (y - y3)^2 r3^2三个方程三个未知数理论上能解出唯一解。实际操作时因为距离测量有误差三条圆不一定交于一点所以一般用两两相减消去二次项变成两个线性方程再用最小二乘或者直接解二元一次方程组来做近似求解。这块我在代码部分用纯数学方法手写了一遍没依赖任何库Arduino Uno这种单片机也能跑得动。同样做室内定位为什么不选Wi-Fi或UWBWi-Fi定位虽然基础设施现成但AP密度低、RSSI波动更大精度通常到不了1米以内UWB精度确实好动辄几厘米但一个模组大几十上百块还要配套做时间同步不适合入门和快速原型验证。BLE4.0的优势在于模块便宜几块钱到二十几块、功耗极低信标用纽扣电池能撑很久、Arduino驱动简单而且手机也能直接扫描到调试方便。2. 硬件选型与部署BLE4.0设备怎么搭信标放哪里最稳2.1 接收端和信标怎么选Arduino HM-10的黄金组合接收端我用的是一块经典的Arduino Uno主控是ATMega328P运行频率16MHzFlash只有32KB听起来很寒碜但做解析和坐标计算绰绰有余。关键是要给Uno配一个能接收BLE广播的串口透传模块我用的是HM-10。市面上的HM-10实际上分好几个版本有的用CC2540芯片有的用CC2541还有些国产兼容模块直接把PCB改成蓝色、写个HC-08或者其他名字。买模块时有几个坑需要提前避开确认模块是真BLE4.0不是蓝牙2.0。蓝牙2.0模块一般是HC-05/HC-06不支持BLE协议没法接收iBeacon广播。买之前看模块背面的丝印和芯片型号优先选CC2541芯片的兼容性最好。不同商家的HM-10固件版本不一样AT指令集可能略有差异买回来先接串口发一个AT看有没有响应再用ATHELP或ATVERSION查询固件版本。信标端我用了三个同型号的HM-10模块刷成iBeacon广播模式分别用3.3V纽扣电池座供电。如果你不想自己配置信标买现成的iBeacon标签也行原理一致只是成本高一些。要注意的是信标模块如果直接接Arduino的5V引脚必须串一个电平转换或者用稳压板稳到3.3V否则容易烧模块。接线方案很简单接收端HM-10的TXD接Uno的数字引脚10SoftwareSerial的RXRXD接数字引脚11SoftwareSerial的TXVCC接3.3VGND接GND。我用SoftwareSerial是因为Uno的硬件串口要留给USB调试输出如果用了Mega这类多串口板子直接用硬件串口更稳。2.2 信标部署的几何学三点布局和高度决定成败信标放哪里看似简单的物理位置问题实际直接影响定位数学模型的解算效果。三边定位里有一个概念叫几何精度因子念起来拗口但道理很直观三个信标围成的三角形越接近正三角形定位误差的空间分布越均匀如果三个信标近乎排成一条直线那么在垂直于这条线的方向上微小测距误差会被放大成巨大的坐标误差。我测试房间大约3米乘4米最终信标坐标取的是(0, 0)、(3.0, 0)、(0, 4.0)形成一个直角三角形。这个布局的好处是计算简单而且不会出现三点共线的问题。部署高度上我踩过一个印象深刻的坑第一次测试的时候信标放在了普通的办公桌桌面上高度大概0.75米接收端在手里拿着人走动时高度变化很大。结果定位点经常跳偏后来把信标统一固定到1.5米左右的高度用扎带绑在落地灯支架和货架立柱上效果立刻好了很多。原因是2.4GHz信号受人体遮挡影响很大如果信标和接收端之间正好有人体挡着RSSI会额外衰减好几个dB测距误差直接变大。适当架高能让信号路径避开大部分躯干遮挡。另外信标要离金属物体至少半米以上特别是铁质货架和铝合金门窗框会反射信号形成多径干扰。还有一点容易被忽略蓝牙功能较多的模块天线是全向的但模块板载天线的方向性并不完美部署时尽量让三个信标的板载天线朝向同一个水平面方向也就是天线平面与地面垂直最大化信号辐射的一致性。2.3 现场标定30分钟让测距误差从几米降到半米标定是整个项目里性价比最高的一步也是很多人嫌麻烦跳过的一步。跳过标定的后果就是n和A用默认值测距误差动辄一倍起步。标定流程其实不复杂工具只需要一个卷尺一块串口转USB板加上串口监视器。操作如下第一步测参考RSSI值A。把接收端放到距离某个信标正好1米的位置天线正对静置不动连续采集50组RSSI取平均值记作A。我实测的值大约是-59dBm。如果你发现自己的A值和网上教程差很多别慌因为模块发射功率、天线增益、周围环境都不一样以现场实测为准。第二步测路径损耗指数n。分别把接收端放到2米、3米、5米的位置每个位置同样采50组数据取平均记录成表格距离(米)实测RSSI均值(dBm)1-592-653-705-78然后用公式反推nn (A - RSSI) / (10 * lg(d))。代入3米那组数据n (-59 70) / (10 * 0.477) ≈ 2.31。也可以把多组数据做线性回归取平均更精确。我这个测试环境里n最终取2.3左右还算正常如果算出n大于4说明环境遮挡太严重要考虑换部署点。第三步把标定好的A和n写进代码里的常量定义。这里有个细节A值对应的是“1米处RSSI均值”实际环境中每天温湿度变化、电池电量下降都会让它漂移所以严谨做法是按使用时段重新快速标定一次特别是电池供电的信标电压从3.3V掉到3.0V发射功率会受影响RSSI整体往下掉定位点就会整体漂移。3. 完整代码解析从裸RSSI到稳定坐标的完整链路3.1 让Arduino读BLE广播串口配置与解析逻辑接收端HM-10在透传模式下会把空中收到的BLE广播数据通过串口发给Arduino。不同固件输出的数据格式会有差异常见的有这种用逗号分隔的简洁帧A4:C1:38:4B:7F:01,-65前面是信标的MAC地址后面是接收信号强度RSSI。Arduino端要做的事情就是按行读取串口数据用逗号做分隔符把MAC地址和RSSI数值提取出来。代码里我用SoftwareSerial连接HM-10波特率设成9600这是HM-10默认的串口波特率。需要留意的是配置信标时如果改过波特率接收端也要保持一致否则收不到数据。解析逻辑上我维护了一个结构体数组三个槽位分别对应三个信标的MAC地址。每次从串口读一行用indexOf找逗号位置把MAC部分和RSSI部分拆分出来再遍历已知信标列表匹配到就更新对应槽位的数据。还有一个容易踩的坑蓝牙广播默认可能采用随机地址也就是MAC地址会定期变化。如果发现代码里匹配不到信标排查顺序是确认模块处于可连接广播状态、确认MAC地址大小写一致、确认没有走BLE隐私功能导致地址随机化。HM-10默认用的是公共地址一般不会遇到这个问题。3.2 RSSI防抖中值滑动平均滤波的取舍RSSI单次采样抖动很大直接拿去做定位就是灾难。我试过多种滤波方案简单说下效果对比。中值滤波取最近5个采样值排序取中间值。好处是能干掉偶尔冒出来的极端野值比如突然跳到-90dBm这种坏处是输出值会跳变不够平滑。滑动平均滤波取最近N个采样值的平均数。好处是曲线很平滑坏处是对真实移动的响应变慢而且如果有几个连续性野值平均值会被拉偏。我最终采用的是“中值滤波滑动平均”组合先对原始数据做5点中值滤波把野值剔掉再做10点滑动平均进一步平滑。实测这个组合在平滑度和响应速度之间比较平衡定位刷新率大约3Hz跟踪正常步行速度的移动没有问题。代码实现上我用了个环形缓冲区存储每个信标的RSSI历史值更新时把新采样写进数组索引循环递增。滤波函数会先拷贝数组用冒泡排序找中值再对中值序列做平均。冒泡排序在这个数据量下执行时间可以忽略不计不需要担心性能。3.3 三边定位解算不依赖库的纯手写坐标系有了三个信标的坐标和滤波后的距离就能解算目标位置。我用的方法很朴素把三个圆方程两两相减消去x²和y²项得到两个一次方程然后用二元一次方程组的解法直接求交点。过程展开一下。设三个信标坐标分别为(x1, y1)、(x2, y2)、(x3, y3)对应距离r1、r2、r3。圆方程展开后是x^2 y^2 - 2*x1*x - 2*y1*y x1^2 y1^2 r1^2把第二个方程减第一个第三个方程减第一个二次项就消掉了剩下2*(x2-x1)*x 2*(y2-y1)*y r1^2 - r2^2 - x1^2 x2^2 - y1^2 y2^2 2*(x3-x1)*x 2*(y3-y1)*y r1^2 - r3^2 - x1^2 x3^2 - y1^2 y3^2这就是一个标准的二元一次方程组套用克拉默法则就能解出x和y。代码里我额外做了两个保护一是检测系数行列式的绝对值如果太小说明三个信标近似共线直接跳过这次解算沿用上一次的坐标二是限制解算结果必须在测试区域范围内比如x范围-1到5y范围-1到6超出就视为无效防止坐标飞到奇怪的地方。3.4 完整Arduino代码可直接烧录下面这段代码是我实测跑通过的版本唯一需要改动的是三个信标的MAC地址、标定参数A和n、以及信标坐标其他可以原样使用。串口监视器波特率设为115200可以看到逗号分隔的定位数据方便导入Excel或者Python画轨迹。#include SoftwareSerial.h SoftwareSerial ble(10, 11); // RX10, TX11 // 三个信标的MAC地址按自己模块的实际地址修改 const char* mac1 A4C1384B7F01; const char* mac2 A4C1384B7F02; const char* mac3 A4C1384B7F03; // 现场标定参数 const float RSSI_1M -59.0; // 1米处RSSI均值 const float PATH_LOSS 2.3; // 路径损耗指数n // 信标坐标单位米 const float BX1 0.0, BY1 0.0; const float BX2 3.0, BY2 0.0; const float BX3 0.0, BY3 4.0; const int RING_SIZE 10; float rssiRing[3][RING_SIZE]; int ringIndex[3] {0, 0, 0}; int ringCount[3] {0, 0, 0}; float dist[3] {-1, -1, -1}; float lastX 1.0, lastY 1.0; void setup() { Serial.begin(115200); ble.begin(9600); pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); delay(500); digitalWrite(LED_BUILTIN, LOW); } void loop() { while (ble.available()) { String line ble.readStringUntil(\n); line.trim(); if (line.length() 8) { parseLine(line); } } bool allReady true; for (int i 0; i 3; i) { if (ringCount[i] RING_SIZE) { allReady false; break; } } if (allReady) { for (int i 0; i 3; i) { float rssiAvg filteredRSSI(i); dist[i] pow(10.0, (RSSI_1M - rssiAvg) / (10.0 * PATH_LOSS)); } trilaterate(); static unsigned long lastPrint 0; if (millis() - lastPrint 500) { lastPrint millis(); Serial.print(X); Serial.print(lastX, 2); Serial.print(,Y); Serial.print(lastY, 2); Serial.print(,R1); Serial.print(dist[0], 2); Serial.print(,R2); Serial.print(dist[1], 2); Serial.print(,R3); Serial.println(dist[2], 2); } } } void parseLine(String line) { int comma line.indexOf(,); if (comma 0) return; String mac line.substring(0, comma); mac.toUpperCase(); int rssi line.substring(comma 1).toFloat(); if (mac.equalsIgnoreCase(mac1)) addSample(0, rssi); else if (mac.equalsIgnoreCase(mac2)) addSample(1, rssi); else if (mac.equalsIgnoreCase(mac3)) addSample(2, rssi); } void addSample(int idx, int rssi) { rssiRing[idx][ringIndex[idx]] rssi; ringIndex[idx] (ringIndex[idx] 1) % RING_SIZE; if (ringCount[idx] RING_SIZE) ringCount[idx]; } float filteredRSSI(int idx) { float tmp[RING_SIZE]; int cnt ringCount[idx]; for (int i 0; i cnt; i) tmp[i] rssiRing[idx][i]; // 冒泡排序取中值数据量小无所谓效率 for (int i 0; i cnt - 1; i) { for (int j 0; j cnt - i - 1; j) { if (tmp[j] tmp[j 1]) { float t tmp[j]; tmp[j] tmp[j 1]; tmp[j 1] t; } } } float median; if (cnt % 2 1) median tmp[cnt / 2]; else median (tmp[cnt / 2 - 1] tmp[cnt / 2]) / 2.0; // 对中值序列做滑动平均这里用全部历史权重均等 float sum 0; for (int i 0; i cnt; i) { if (rssiRing[idx][i] -100 rssiRing[idx][i] -10) sum rssiRing[idx][i]; } return sum / cnt; } void trilaterate() { float r1 dist[0], r2 dist[1], r3 dist[2]; if (r1 0.1 || r2 0.1 || r3 0.1) return; float a 2 * (BX2 - BX1); float b 2 * (BY2 - BY1); float c 2 * (BX3 - BX1); float d 2 * (BY3 - BY1); float e r1 * r1 - r2 * r2 - BX1 * BX1 BX2 * BX2 - BY1 * BY1 BY2 * BY2; float f r1 * r1 - r3 * r3 - BX1 * BX1 BX3 * BX3 - BY1 * BY1 BY3 * BY3; float det a * d - b * c; if (fabs(det) 1e-6) return; // 三点近似共线放弃本次解算 float x (e * d - b * f) / det; float y (a * f - e * c) / det; // 坐标范围约束防止跳到测试区域外 if (x -1.0 x 6.0 y -1.0 y 7.0) { lastX x; lastY y; } }代码写到这里已经可以直接用了。如果想看实时运动轨迹简单一点的方法是打开串口绘图器Arduino IDE自带的Serial Plotter把输出格式调成只有X和Y逗号分隔就能看到位置点也可以用一个几行的Python脚本读串口保存日志再做可视化原理不复杂这里不展开。4. 实测数据与调试经历误差从3米缩到40厘米4.1 首轮测试裸RSSI的“心电图式”抖动刚把硬件搭起来的时候我图省事标定参数直接用了网上教程里常见的A-55、n2滤波没做代码里拿到RSSI就立刻算距离。跑起来一看坐标输出和张心电图差不多即使在原地静止不动打印出来的X、Y也在一个直径2到3米的圆里跳动。用串口监视器观察原始RSSI发现同一个信标、同一个位置信号强度能在一个呼吸周期内波动6到8个dBm。换算成测距误差在3米距离上8dBm的波动会被放大成大概1.5米的距离偏差。这个阶段要是拿去给机器人导航机器会在原地转圈。4.2 三轮优化标定、滤波、几何约束缺一不可第一轮优化是把标定做了。我在1米、2米、3米、5米位置各采了50个点算均值后反推出了A和n。标定做完测距均值准了不少5米位置误差从大概1.8米缩小到0.6米左右但单次读数依旧抖定位点还是跳。第二轮优化加滤波。先用滑动平均10次曲线平滑了但响应滞后明显人走两步之后定位点才慢吞吞跟上来后来改成中值滑动平均组合在平滑和响应之间找到了平衡点。这轮之后静止时的抖动范围基本收在了0.5米以内。第三轮优化是几何约束和解算保护。之前在某个角落里算出来的坐标经常跑到测试区域外面的走廊上明显不合理。加了区域范围约束和三点共线保护之后这种“坐标飞出去”的情况基本绝迹。同时我意识到人站在哪个方向也有影响人体含水量高对2.4GHz信号的衰减非常明显后来测试时人尽量待在接收端的侧面不让身体堵住信号直射路径。4.3 最终精度与场景建议三轮优化后我做了组定点测试。在三个固定位置各静止30秒取坐标均值结果如下真实坐标(米)测量坐标(米)误差(米)(0.8, 0.8)(0.62, 0.94)0.23(1.5, 2.0)(1.72, 1.78)0.32(2.2, 3.0)(2.51, 2.74)0.39静态重复精度最好的一次到了20厘米以内最差也在40厘米上下。要知道我用的只是两片十几块钱的BLE模块和一块Uno这个结果已经相当能打了。动态跟随测试中人沿直线走了4米轨迹还原度不错但横向偏差偶尔到0.8米主要发生在快速转身的时候因为RSSI响应有延迟再加上身体遮挡方向突变。如果你要复制这套东西我给你一个务实的建议定位需求能接受“半个房间”“哪条过道”这种颗粒度直接上这套方案不会让你失望但如果客户要求“必须告诉我这个箱子在货架第几格”那还是把这套系统的输出当辅助参考配合二维码、地磁传感器等其他信息一起用。5. 避坑清单我在这条路上踩过的所有坑5.1 排查速查表这半年调试下来各种奇怪现象都碰到过整理成一张速查表按图索骥能省不少时间现象可能原因解决办法串口完全收不到数据HM-10没进透传模式/波特率不对发AT检查响应确认TX RX没接反定位点飘到区域外距离估计偏大或信标共线加坐标约束检查信标是否近似共线某个信标距离值恒为-1MAC地址不匹配/广播地址随机变化用ATADDR查询实际MAC并更新代码静止时坐标跳变大RSSI抖动未充分滤除增大中值窗口检查信标天线是否被遮挡人走动后定位明显偏移人体遮挡导致信号衰减架高信标或让测试人员站到接收端侧面距离误差整体偏大标定参数A/n不准重新做现场标定别用网上的默认值电池供电信标用一天后漂移电压下降导致发射功率变化换成稳压供电或定期重新标定信标遇到电脑USB口干扰读数突变USB3.0等高频设备干扰2.4GHz测试时把干扰源移开或屏蔽隔离5.2 关于“买模块”和“抄代码”的几句劝告最后聊几点只有亲自做过一遍才会明白的经验。第一模块尽量买有信誉的商家虽然HM-10方案很成熟但市面上芯片打磨、固件阉割的兼容模块不少有些甚至缺失AT指令集的核心功能。买回来第一件事就是AT测试确认固件版本和指令支持再开工。第二不要过度依赖网上抄来的完整代码。不同模块的串口输出格式差异很大哪怕同一型号不同固件版本输出的帧也可能不一样。我代码里的parseLine函数是基于“MAC,RSSI”的格式写的如果你的模块输出带前缀、带CD/CRC之类的内容需要按实际格式调整。这个一定要学会自己改否则换个批次模块代码就废了。第三如果动手能力允许尽量用板载天线方向性保持一致的方式摆放信标并且记录每个信标的部署高度。我从一个同事那儿学到的经验是在信标旁边贴一张标签纸写上安装日期、部署坐标和当天的A值这样复查漂移原因时不用猜。这套蓝牙RSSI定位系统做到现在最大的收获倒不是最终那40厘米的精度而是把无线信号传播模型、滤波算法、几何解算完整地串了一遍。按我个人的体会如果你也想做室内定位相关的项目可以从这套组合入手把原理吃透等哪天真觉得精度不够用了再去研究BLE5.1的AOA方向定位也不迟有了RSSI这套底子在理解AOA原理会顺畅得多。