ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ESP32 BLE Beacon RSSI测距实战:从原理到滤波算法

ESP32 BLE Beacon RSSI测距实战:从原理到滤波算法 这个系列写到了第六讲前面把 WiFi、MQTT、OTA 这些联网基本功都过了一遍这一篇加一点“空间感知”的能力让 ESP32 通过蓝牙 beacon 的 RSSI 估算距离。实际做项目时很多朋友一提“室内定位”就想到 UWB。UWB 确实是厘米级精度但成本也摆在那边一套基站加标签动辄几百上千。而在仓库物资分区、展馆导览、养老院区域告警这类“知道你在某个房间、某个展位附近”的场景BLE beacon 测距是完全够用的一颗 ESP32 加几块钱的 beacon 就能把系统跑起来。这篇我按自己实际开发的顺序来写环境是基于 VSCode ESP-IDF核心是蓝牙 beacon 的广播与扫描、RSSI 转距离的数学模型、标定方法和滤波算法。内容包括完整的工程思路和关键代码适合已经能编译 ESP32 工程、但对蓝牙协议栈还不太熟的朋友。踩过的坑我也会一并列出来免得你重复交学费。1. 先说清楚BLE beacon 测距的精度边界别拿它当 UWB 用1.1 RSSI 测距的物理本质BLE beacon 测距的原理说白了就是“看信号强度猜距离”。beacon 设备定时广播数据包接收端拿到每个包的 RSSIReceived Signal Strength Indicator接收信号强度指示根据信号在空间中的衰减规律反推距离。理想情况下无线信号在自由空间传播时接收功率和距离的平方成反比转换成 dBm 表示就是一条直线距离每增加 10 倍RSSI 下降约 20dB这对应路径损耗指数 n2。但室内环境远没有这么理想墙壁、金属货架、人体都会反射和吸收信号实际路径损耗指数 n 通常在 2 到 4 之间走廊这种狭长空间甚至可能出现“波导效应”信号衰减比空旷场地还慢。一次扫描拿到的 RSSI 值波动也非常大同一台 beacon 放在同一个位置1 秒内扫到的信号强度可能差 8 到 15dB。换算成距离可能一下子从 1 米跳到 5 米。这一点新手特别容易忽略也是很多人一测就说“这玩意儿不靠谱”的根本原因。可以用一个生活类比帮助理解你在房间里喊一句话让另一个人猜你离他多远。空旷广场上音量大致能反映距离但如果中间隔了几道墙、几件家具对方只能凭感觉猜一个很粗略的范围。RSSI 测距就是这种“凭音量猜距离”的思路不是精密测量。1.2 和其他测距方案的横向对比很多需求方上来就要求“误差 20 厘米以内”这时候做技术选型的人得先分清楚预算和场景。我把几个常见方案放在一起对比方案精度硬件成本功耗适用场景UWB 测距厘米级高需要专用芯片较高机器人对接、AGV 避障、室内定位基站WiFi RTT米级中需要支持 FTM 的 AP中手机室内定位依赖网络基建BLE RSSI3~5 米误差优化后 1~2 米极低普通 BLE 芯片即可极低区域级定位、资产管理、考勤打卡蓝牙 AoA/AoD亚米级中高需要阵列天线中室内导航、物品追踪但生态复杂从左边的表能清楚看到BLE RSSI 的优势从来不是精度而是成本和普适性。beacon 本身可以做得非常小一颗 CR2032 电池撑几个月甚至一年接收端用 ESP32 这种通用 MCU 就能做不需要额外专用芯片。1.3 哪些项目适合用哪些直接被劝退以我的经验以下几类项目用 beacon 测距是划算的展馆、博物馆导览参观者走到某个展位附近就推送对应介绍仓库物资分区管理把仓库按货架划成多个区域判断货物在哪个区域养老院、幼儿园区域告警佩戴 beacon 标签进入危险区域或离开指定范围时告警会议签到、工位打卡识别“人到了这个房间”不需要精确坐标。反过来这几类项目建议直接放弃 beacon 测距机械臂抓取、自动充电桩对接需要厘米级甚至毫米级精度地下车库车辆定位金属车身对信号屏蔽严重RSSI 会非常不稳定长距离稀疏节点定位两个 beacon 相距超过 15 米后RSSI 衰减进入“平台期”距离变化对信号强度的影响越来越不敏感。选型时把精度预期定在“区域级”而不是“坐标级”beacon 测距就能发挥价值否则后面一定会被数据打脸。2. VSCode 里把 ESP-IDF 工程跑起来少踩几个环境的坑2.1 工具链版本组合和下载渠道在做 beacon 测距之前先把 ESP-IDF 开发环境准备好。目前官方推荐用 VSCode 加 Espressif IDF 插件整体体验比命令行直接敲 idf.py 要友好不少尤其是代码补全、烧录和串口监视器集成。工具链要注意两点第一VSCode 和插件都走官网下载VSCode 直接到官方站点拿安装包Windows 下安装时建议选择“加入 PATH”选项后面终端操作省事。ESP-IDF 插件安装完成后第一次启动会引导下载工具链这一步网络不好时容易失败可以在插件设置里设置idf.espIdfPath和idf.toolsPath指向已经解压好的 ESP-IDF 目录比如C:\esp_544\.espressif\v5.4.4\esp-idf这样的路径。第二ESP-IDF 版本我建议直接用当前的 v5.x 长期支持版本不要用特别旧的 v4.x。V5 以后蓝牙协议栈和驱动接口有调整但网上教程大多还停留在 v4.x代码抄过来会出现esp_ble_gap_config_adv_data()这类函数名对不上或者头文件路径找不到的问题。2.2 创建工程不要从零开始手写开发蓝牙应用最忌讳从空工程手写初始化流程。ESP-IDF 官方仓库里带了大量 example直接用它们做起点能省掉很多查手册的时间。打开 VSCode 的 ESP-IDF 插件执行 “Create New Project”选择示例模板时可以找bluetooth/bluedroid/ble/ble_ibeacon和bluetooth/nimble相关例子。其中ble_ibeacon直接就是一个 iBeacon 广播设备例程扫描端可以参考ble_gattc或者自己写基础扫描逻辑但核心回调函数是通用的。我自己习惯在同一个工程里放两份 main 源码通过menuconfig编译选项切换角色。目录结构大致是beacon_demo/ ├── main/ │ ├── CMakeLists.txt │ ├── beacon_main.c // 广播端 │ ├── scanner_main.c // 扫描端 │ └── rssi_utils.c // RSSI 滤波与测距算法 ├── components/ │ └── ... ├── sdkconfig └── CMakeLists.txt在CMakeLists.txt里通过idf_build_set_property或者简单的#ifdef宏来做编译切换比如烧录 A 板卡时启用广播端烧录 B 板卡时启用扫描端。这样维护一份工程就够了。2.3 编译烧录的命令行和常见报错虽然 VSCode 插件提供了图形化按钮但命令行这几个命令还是建议记一下排查问题的时候效率高idf.py set-target esp32 idf.py menuconfig idf.py build idf.py -p COM3 flash monitor实际开发中常碰到的报错有这么几个插件提示the path for esp-idf is not valid: /tools/idf.py not found这基本是idf.espIdfPath配置错了检查路径要精确到esp-idf目录而不是上一级编译时报找不到esp_log_color.h通常是因为环境变量IDF_PATH没有激活在插件终端里执行$IDF_PATH/export.ps1Windows或者重新打开 VSCode 窗口烧录时报Failed to connect to ESP32先检查板子驱动和串口号ESP32-S3 这类板子如果使用板载 USB 转串口芯片可能需要按住 BOOT 键再点烧录。3. 广播端和扫描端的分工哪个做 Beacon哪个负责测距3.1 beacon 广播端实现用 ESP-IDF 发 iBeacon 格式数据包Beacon 本质上就是一个不断重复发送广播包的 BLE 设备只是广播数据里塞了特定格式的信息。常见的格式有苹果的 iBeacon 和谷歌的 Eddystone两者都是把自定义数据放进广播包的 Manufacturer Specific Data 字段。用 ESP-IDF 做 beacon 广播端推荐用 NimBLE 协议栈它在 ESP32 上内存占用更小、API 更简洁。核心逻辑是构造一条广播数据然后周期性广播。关键代码如下static uint8_t beacon_data[26] { 0x02, 0x01, 0x06, // FlagsLE General Discoverable 0x1A, 0xFF, 0x4C, 0x00, // Manufacturer Specific0x004C 是 Apple ID 0x02, 0x15, // iBeacon 类型和长度 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // UUID 0x00, 0x01, // Major 0x00, 0x0A, // Minor 0xC5 // TX Power1米处参考值需实测标定 };初始化时设置广播参数ble_gap_adv_set_fields(BLE_GAP_ADV_FIELD_FLAGS | BLE_GAP_ADV_FIELD_MFG_DATA, beacon_data, sizeof(beacon_data)); struct ble_gap_adv_params adv_params { .conn_mode BLE_GAP_CONN_MODE_NON, .disc_mode BLE_GAP_DISC_MODE_GEN, .itvl_min BLE_GAP_ADV_ITVL_MS(200), .itvl_max BLE_GAP_ADV_ITVL_MS(300), }; ble_gap_adv_start(adv_params);广播间隔我习惯设在 200ms 到 500ms。间隔越短接收端越容易快速捕捉到信号但功耗也越高。做静态资产标签可以用 500ms做人员走动轨迹追踪建议 200ms。3.2 扫描端实现捕获 beacon 包并提取 RSSI扫描端是测距的主角它负责监听空中的广播包过滤出我们关心的 beacon然后从回调里取出 RSSI。在 NimBLE 协议栈中扫描结果通过ble_gap_event回调返回。初始化扫描的代码如下static void on_scan(struct ble_gap_event *event, void *arg) { switch (event-type) { case BLE_GAP_EVENT_DISC: if (event-disc.event_type BLE_HCI_ADV_RPT_EVTYPE_ADV_IND) { int rssi event-disc.rssi; // 在这里解析 event-disc.data过滤 UUID process_rssi(rssi); } ble_gap_disc_restart(); break; } } // 启动扫描 struct ble_gap_disc_params disc_params { .passive 1, .itvl BLE_GAP_DISC_ITVL_MS(200), .window BLE_GAP_DISC_WINDOW_MS(200), .filter_policy BLE_GAP_DISC_FILT_POLICY_NO_FILTER, }; ble_gap_disc(BLE_OWN_ADDR_PUBLIC, disc_params, on_scan, NULL);注意passive 1被动扫描模式不主动发扫描请求比较省电对普通 beacon 广播也够用了。itvl和window都设成 200ms相当于持续监听RSSI 采样率会比较高。解析广播数据时需要遍历 AD Structure把0xFFManufacturer Specific类型的数据抠出来再判断前两个字节是不是 Apple 的0x4C, 0x00然后比对 UUID。这段代码比较机械但一定要写对否则会把旁边手机、耳机发出的蓝牙广播也当成 beacon 处理。3.3 一个工程里做双角色还是分开部署很多初学者会问能不能让同一个 ESP32 既当 beacon 广播又当扫描器测距从协议栈角度说NimBLE 和 Bluedroid 都支持同时做广播和扫描但实际项目里我不太建议这么干。原因是同一块芯片的两个角色共用同一个射频前端广播和扫描会互相抢占时隙扫描的空窗期变长RSSI 采样的连续性也会受影响。更合理的架构是标签端小体积、低功耗只广播 iBeacon。一般用 ESP32-C3、nRF52832 这类低功耗芯片或者直接买现成的 beacon 模块。接收端ESP32 固定在某个位置持续扫描周围的 beacon把 RSSI 换算成距离然后通过 WiFi、串口或 MQTT 上报给上位机。如果手头只有一块 ESP32 开发板又想在桌面上自测可以先用手机装一个 beacon 模拟 App比如 nRF Connect 或 iBeacon 模拟器让手机扮演 beaconESP32 只做扫描端。等整套测距逻辑调通了再去烧一版广播端固件到另一块板子上。这样调试时变量少出问题容易定位。4. RSSI 换算成米路径损耗模型不是拿来就用的4.1 模型公式对数距离路径损耗模型RSSI 转距离最常用的公式是对数距离路径损耗模型RSSI A - 10 * n * log10(d)A距离 beacon 1米处测到的 RSSI 平均值单位 dBmn路径损耗指数室内一般 2~4d距离单位米。反解出距离d 10 ^ ((A - RSSI) / (10 * n))看起来很简单但直接套公式的人十有八九会翻车。翻车原因很简单A 和 n 根本不是固定的芯片参数而是和环境强相关的经验值。同一台 beacon 放在会议室角落和空旷大厅里1 米处的 RSSI 可能差 10dB 以上n 值更是随环境变化。所以做项目时A 和 n 必须实测标定不能从某篇博客里抄一组参数就上线。4.2 最小二乘拟合标定 A 和 n标定的方法其实不复杂。拿到一个 beacon在空旷无人的场地里让它和接收端分别放在 0.5m、1m、2m、3m、5m、8m 这几个距离上每个距离采集 50 到 100 个 RSSI 样本取平均作为该距离的 RSSI 值。把公式做一次线性化变换令 y RSSIx log10(d) 则 y A - 10n * x这是一条直线x 是已知量y 是测量值用最小二乘法拟合就能得到斜率k和截距b其中A bn -k/10。如果手边有 Python用几行代码就能算import numpy as np # 实测数据距离米和对应的平均RSSIdBm distances [0.5, 1.0, 2.0, 3.0, 5.0, 8.0] rssi_avg [-55, -58, -65, -69, -76, -82] x np.log10(distances) y np.array(rssi_avg) k, b np.polyfit(x, y, 1) A b n -k / 10 print(fA {A:.2f} dBm, n {n:.2f})没有 Python 环境的话用 Excel 插入散点图添加一元线性趋势线也能得到同样的斜率和截距。关键是采样的时候要控制变量beacon 和接收端天线朝向保持固定记录 RSSI 时人等远离测试区域走动避免人体吸收信号造成额外衰减。4.3 我的标定结果与参数参考值下面是一组我在普通办公室环境实测的数据beacon 用某 CR2032 供电的小型标签ESP32-S3 开发板做接收端距离(m)平均 RSSI(dBm)标准差(dBm)0.5-542.31.0-582.82.0-653.53.0-694.15.0-765.28.0-826.3拟合出来A ≈ -58.4dBmn ≈ 2.2。在 1 米以内误差大概 ±0.3 米3 米左右误差到 ±1.2 米8 米处已经不太能看了误差可能到 ±3 米。这里要特别提醒不同批次、不同天线的模组1 米处 RSSI 可能差 2 到 5dB。自己打样玩玩无所谓如果做量产产品每条产线最好都能做一次简单标定至少抽检几台设备求平均参数。5. 距离值疯狂跳变怎么办先滤波再说人话5.1 RSSI 原始数据的噪声特征就算标定做得再准直接拿单次扫描的 RSSI 去算距离输出依然会抖到没法用。我在固定位置放了一台 beacon同一个距离下连续采集 50 个 RSSI最大和最小值差了 14dB。换算成距离短时波动能到 2 倍以上。所以做 beacon 测距滤波不是可选项而是必选项。滤波的目标不是消除噪声而是让距离输出“看起来像一个正常的物理量”不会从 1 米突然蹦到 8 米再跳回来。5.2 三种常用滤波方案我实际项目中用得最多的是这三种各有侧重滑动平均维护一个长度为 N 的环形缓冲区每次取平均值。平滑效果取决于 NN5 时响应快噪声仍有残留N20 时很平滑但距离变化时的响应明显迟钝。适合 beacon 静止、接收端也静止的资产管理场景。指数加权移动平均EMA只保存一个旧值每次新数据进来更新float ema_rssi 0; float alpha 0.3; // 0~1越大越跟随新数据 float rssi_filtered alpha * rssi_raw (1.0f - alpha) * ema_rssi; ema_rssi rssi_filtered;alpha 选 0.2~0.4 比较合适。这个方案响应速度快但偶尔会跟着尖峰一起抖一下。中值滤波保存最近 5~7 个样本取中间值。它对突然出现的离群点压制效果极好适合周围蓝牙设备多、经常出现瞬时干扰的场景。实际项目我常用“中值滤波 滑动平均”的组合先去掉尖峰再做一次平滑既稳又不易被单次异常值带偏。下面是一段简单的滑动平均环形缓冲代码#define RSSI_BUF_LEN 10 static int rssi_buf[RSSI_BUF_LEN]; static int rssi_idx 0; static int rssi_cnt 0; float rssi_smooth(int raw) { rssi_buf[rssi_idx] raw; rssi_idx (rssi_idx 1) % RSSI_BUF_LEN; if (rssi_cnt RSSI_BUF_LEN) rssi_cnt; int sum 0; for (int i 0; i rssi_cnt; i) { sum rssi_buf[i]; } return (float)sum / rssi_cnt; }如果项目要求更高可以用一维卡尔曼滤波。它的思路是根据上一时刻的估计值和当前测量值动态调整权重。比如 RSSI 噪声大时滤波器自动更相信历史估计噪声小时更相信当前测量。一维卡尔曼实现代码量不大但想要调好需要理解测量噪声协方差和过程噪声协方差两个参数的物理意义新手先不用一上来就上卡尔曼用 EMA 很多时候已经够用。5.3 距离跳变的业务层处理迟滞和置信度滤波只解决了“显示值乱跳”的问题业务逻辑上还有一个容易踩的坑用距离做区域判断时阈值附近的抖动会导致频繁触发和退出。比如设定“进入房间”阈值是 1.5 米人站在临界点附近滤波后的距离在 1.4 到 1.6 米之间波动系统就会反复上报进入/离开事件。解法是加迟滞进入阈值用 1.5 米离开阈值用 2.5 米。也就是说只有距离从大于 2.5 米连续减小到 1.5 米以内才判定进入从小于 1.5 米连续增大到 2.5 米以上才判定离开。中间地带保持上一次状态不变。另一个做法是“连续 N 次扫描有效才触发”例如连续 5 次解析到 beacon 且距离小于阈值才认为目标在区域内。这样可以过滤掉瞬时遮挡造成的信号丢失。还有一点建议距离换算的公式里当 RSSI 大于 A 时比如 1 米内测到 -50dBm算出来的距离小于 1 米这是正常的但当 RSSI 特别大或者特别小时要分别做上下限裁剪避免输出 0 米或者 999 米这种离谱值。我一般把有效范围限制在 0.3 米到 15 米之间超过就直接饱和。6. 实测数据复盘同一个房间不同方向能差出 3 米6.1 我的测试环境和方法我在一个约 20 平米的办公室做了几组实测。环境是一张靠墙办公桌两台显示器和一些线缆旁边有 WiFi 路由器和 USB3.0 硬盘盒。接收端 ESP32-S3 固定在墙角桌面beacon 放在一台小塑料支架上分别放在 0.5m、1m、2m、4m、6m 五个位置。每个位置连续记录 2 分钟一共约 120 个 RSSI 样本经过“中值 滑动平均”滤波后再换算距离。结果如下真实距离(m)滤波后估算距离(m)绝对误差(m)0.50.60.11.01.10.12.02.30.34.03.20.86.08.52.5可以看出4米以内还算可用6米以上误差就会明显拉大。这也印证了前面的观点BLE RSSI 只适合做近距离区域判断不适合做长距离测距。我还做了一个对比实验把 beacon 的天线朝向从扫描端方向转 90 度同样在 2 米处RSSI 立刻下降了 6dB 左右估算距离从 2.2 米变成 3.5 米。所以如果 beacon 会挂在门禁、货架上一定要评估安装方向并且尽量让接收端能看到 beacon 天线的主辐射方向。6.2 干扰和硬件细节的坑这轮测试里我额外发现几个容易忽略的问题一是 USB3.0 设备对 2.4G 频段的干扰。测试时 USB3.0 硬盘盒一开始插在旁边扫描结果标准差明显偏大拔掉之后 RSSI 曲线平稳了很多。USB3.0 的频谱泄漏刚好会踩到 2.4G这在办公环境里非常常见。二是 PC 蓝牙驱动的干扰。如果做原型验证时用 PC 的蓝牙适配器扫描不同蓝牙驱动比如那些 CSR 芯片的老驱动对 RSSI 的处理方式不一样扫出来的值和 ESP32 扫出来的可能差好几 dB。所以项目里一定要统一接收端硬件不要今天用 PC 明天用 ESP32否则前面的标定全部白做。三是 ESP32 板上的天线区域不能遮挡。有些开发板的 PCB 天线在板子边缘调试时如果用杜邦线从附近飞线或者把板子压在金属桌面上RSSI 会明显下降。测试时建议把开发板架空天线区域保持净空。四是电池电压的影响。CR2032 供电的 beacon 在电池电压从 3.3V 降到 2.8V 时实际发射功率可能下降 3~5dB导致接收端觉得 beacon “变远了”。做长期部署时要考虑定期更换电池或者选择支持电压补偿的 beacon。6.3 如果要做产品还要考虑什么把单个 beacon 的距离做稳定只是第一步。实际产品里通常有多个 beacon 同时广播接收端需要按 MAC 地址或者 UUID Major Minor 区分不同设备并为每个设备维护独立的 RSSI 滤波状态。可以在代码里建立一个以 MAC 地址为 key 的表每扫到一个 beacon 就更新对应表项的滤波器和距离估计。如果一个区域需要更准的定位一个接收端是不够的。可以放三个固定接收端用三路测距结果做三边定位把目标坐标估计出来。但因为 RSSI 误差不是高斯分布直接最小二乘解出来的位置常常漂到房间外面一般还要配粒子滤波或卡尔曼滤波做位置跟踪工作量会明显上一个台阶。另外距离数据最终要发挥作用通常要和后台联动。我在这套系统里就是把 ESP32 计算出的距离通过 WiFi 上报到 MQTT Broker后台根据距离变化触发告警或记录轨迹。这样就和前面几讲的联网能力串起来了beacon 测距从“一个 Demo”变成了“一个完整系统”。最后再分享一个调试习惯做 RSSI 标定时不要只记录 ESP32 读取到的数值最好同时用手机上的 nRF Connect 在旁边扫一遍两边数据对照看。很多时候你以为是自己代码写错了其实只是手机蓝牙和开发板的位置、天线方向不一样。保持测试环境的一致性比精确拟合参数更重要。
返回列表