news 2026/9/12 16:57:47

ESP32蓝牙Beacon测距实战:RSSI转距离原理与校准滤波指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32蓝牙Beacon测距实战:RSSI转距离原理与校准滤波指南

搞 ESP32 开发的朋友应该都有这种感觉,从板子到手到真正让它按自己的想法跑起来,中间要翻过的山头真不少。之前几讲把环境搭建、GPIO、Wi-Fi 联网这些基础打完后,很多人在蓝牙这块又卡住了,尤其是想用蓝牙做点实际应用的时候,比如室内定位、防丢器、靠近检测这类。这篇第六讲,主题就是 ESP-IDF 加 VSCode 环境下,用 ESP32 做蓝牙 beacon 测距,也就是让设备通过低功耗蓝牙收发 beacon 广播,用 RSSI 信号强度把距离给估算出来。

这篇内容适合两类人看:一是已经把 ESP-IDF 环境跑通、想往蓝牙方向深入的同学;二是正在做室内定位、防丢、展馆导览、智能门禁靠近解锁这类需求,想评估蓝牙 beacon 方案是否可行的工程师。我会把方案选型、协议格式、发射端配置、接收端解析、RSSI 转距离的数学原理和校准方法、滤波处理,以及实际调试中踩过的坑和排查思路全部过一遍。整个过程基于 ESP32 常规开发板,用 ESP-IDF 的官方蓝牙栈 Bluedroid 实现,VSCode 作为编辑器,代码会给出关键部分,都是可以直接抄走改改就能用的。

1. 蓝牙 beacon 测距的方案选型与整体思路

1.1 为什么是 beacon,它能解决什么问题

先说结论:beacon 测距解决的是“你这设备大概离我多远”的问题,精度在米级,胜在成本低、部署简单、终端兼容性好。

Beacon 本质是一种单向广播设备,它不断向外发送固定格式的蓝牙广播包,自己不关心有没有人收到。接收端(比如手机、另一个 ESP32)通过监听这些广播包,获取到信号强度 RSSI,然后利用无线信号在空间中传播衰减的规律反推出距离。这种机制和 GPS 完全不是一回事,不需要卫星、不需要联网、不需要回传通道,非常适合室内场景。

实际项目里我用 beacon 测距做过几类东西:一个是展馆里的大屏互动,观众手机靠近展品 1 米以内自动弹介绍,靠的就是 beacon 广播加手机端测距;另一个是仓库物料防丢提醒,物料上挂一个 beacon,网关用 ESP32 扫描它,信号强度低于阈值就报警;还有朋友在做宠物防丢项圈,手机 App 扫到 beacon 后大致判断宠物在房间哪个区域。这些场景的共同特点是:不需要厘米级精度,但需要低功耗、低成本、能大规模部署。对比一下同类无线测距方案就很清楚了:

方案频段典型精度成本功耗适用场景
UWB(超宽带)3.1~10.6GHz厘米级汽车钥匙、人员高精度定位
Wi-Fi RTT5GHz1~2米中(需AP支持)商场、机场手机定位
蓝牙 beacon RSSI2.4GHz1~5米很低极低防丢、导览、室内粗略定位
ZigBee2.4GHz2~5米物联网传感网(很少用于测距)

所以如果你的需求是“知道个大概位置”就行,那蓝牙 beacon 就是性价比之王。但也要事先有心理准备:RSSI 测距天然有噪声,环境一变精度就飘,方案设计时必须把这一点考虑进去,后面第 5 章我会专门讲怎么校准和滤波。

1.2 硬件和软件栈选型:ESP32、ESP-IDF、VSCode

为什么选 ESP32?因为它一颗芯片就把双模蓝牙(经典蓝牙 BT 和低功耗蓝牙 BLE)、Wi-Fi、双核 CPU、丰富外设全集成在一起了,价格还便宜。做 beacon 测距时,ESP32 既可以当 beacon 发射端,也可以当扫描接收端,甚至同时作为网关把测距结果用 Wi-Fi 发到服务器,一颗芯片全搞定。我最早是用 Nordic 的 nRF52832 做低功耗 beacon,后来发现很多项目还需要联网上云,ESP32 的双模加 Wi-Fi 优势就出来了,整个系统的元件数量也少很多。

软件栈为什么用 ESP-IDF 而不是 Arduino?Arduino 生态写个简单 beacon 确实快,但到了要精细调 BLE 广播参数、扫描窗口、做低功耗管理、跟 Wi-Fi 共存这些环节,Arduino 的库封装把底层细节藏得太深,反而不顺。ESP-IDF 是乐鑫官方给的完整开发框架,BLE 这块用的是 Bluedroid 蓝牙协议栈,虽然接口风格偏底层,但控制粒度很细,广播间隔、扫描窗口、发射功率、厂商数据内容全部可以自己定义。对于 beacon 测距这种需要精确控制协议字段的应用,用 ESP-IDF 是更踏实的路子。

开发环境选 VSCode 加 Espressif IDF 插件,纯粹是效率问题。VSCode 的代码补全、Git 集成、终端体验,加上乐鑫官方插件把构建、烧录、串口监视、内存分析都集成到图形界面里,对新手友好,对老手也顺手。整个开发调试在 VSCode 里闭环,不用反复切换工具。这一讲的示例我都按 VSCode 下的操作来讲,但底层命令就是 idf.py 那几个,在别的环境也没差别。

2. 开发环境搭建与第一个蓝牙工程

2.1 VSCode 下配置 ESP-IDF 开发环境

如果你是从零开始装环境,建议直接在 VSCode 的扩展商店里搜“Espressif IDF”,安装乐鑫官方的插件。插件首次启动时会把 ESP-IDF 工具链、Python 虚拟环境、OpenOCD、QEMU 等全部下载下来,路径默认放在用户目录的 esp 文件夹下。有几个细节值得注意:

  • 安装 ESP-IDF 时版本选择:建议直接选最新 stable 版本。我之前在一个老项目上用了 v4.4,新项目想迁移到 v5.x,发现 API 变动不小,尤其是 BLE 相关的头文件和结构体都有调整。从 v5.0 开始,ESP-IDF 启用了新的组件依赖管理,构建系统更严格,虽然学习曲线略微陡一点,但长远看值得。
  • 选择目标芯片:ESP32 和 ESP32-C3 这类芯片在框架里是不同的 target,工程创建后要执行一次 set-target。在 VSCode 插件里,命令面板(Ctrl+Shift+P)输入 ESP-IDF: Set Espressif Device Target,选择自己的芯片型号。
  • 最常遇到的环境报错:插件提示 “The path for ESP-IDF is not valid: /tools/idf.py not found”。这个错误九成是 ESP-IDF 没有正确安装或者路径配置错误。如果手动下载过 ESP-IDF,必须在 VSCode 设置的 “Idf: Espressif Idf Path” 中指向包含 idf.py 的那个目录的父级目录,比如你下载在 D:\esp-idf,那这里填 D:\esp-idf 而不是 D:\esp-idf\tools。

配完之后建工程有两条路:一是用 VSCode 命令面板的 “ESP-IDF: Show Examples Projects”,从官方示例库里找模板;二是用 idf.py create-project 自己建一个干净工程。对于讲蓝牙这种需要改官方示例的情况,我更推荐直接从示例工程派生,省得在 CMakeLists 里手动链接一堆组件包。另外建议在 VSCode 里把 C/C++ 插件也装好,并让它根据 ESP-IDF 生成的头文件路径自动配置智能提示,这需要执行一次 “ESP-IDF: Add vscode Configuration Folder” 命令,会在工程目录下生成 .vscode/c_cpp_properties.json,这样跳转定义、补全才会好使。

2.2 官方 iBeacon 示例工程编译与烧录

乐鑫在 ESP-IDF 里放了一个很关键的参考工程:examples/bluetooth/bluedroid/ble/ble_ibeacon。这个工程就是用 ESP32 广播一个 iBeacon 格式的 beacon 包,非常适合作为我们这一讲的起点。在 VSCode 里通过 “ESP-IDF: Show Examples Projects” 找到它,选择一个工作目录复制一份出来。

打开工程后,目录结构核心就三个文件:

  • main/ibeacon_demo.c:beacon 广播主逻辑,定义并配置广播数据。
  • main/ibeacon_demo.h:iBeacon 数据结构的定义和广播包组装的辅助函数。
  • main/CMakeLists.txt:组件构建配置,一般不用改。

在 VSCode 底部状态栏选好串口和目标芯片,点击火焰图标开始编译。编译过程中如果看到类似于 “idf.py build 失败” 的情况,九成是环境变量、Python 包版本或者工具链路径的问题,可以打开终端手动跑一下idf.py build看完整报错日志,信息往往比 IDE 里截断的提示更明确。

烧录跑起来后,用手机下载一个通用的 BLE 扫描工具(比如 nRF Connect),扫描能看到一个名字为空、厂商数据为 Apple 0x004C 的广播包,那大概率就是 ESP32 发出的 iBeacon 了。到这一步说明蓝牙广播已经通,接下来就是改协议内容和加扫描测距逻辑。

3. beacon 广播数据格式与发射端实现

3.1 iBeacon 协议格式拆解

iBeacon 是苹果定义的 beacon 广播格式,它不基于标准 GATT 服务,而是把数据塞在广播包的厂商特定字段里。一个完整的 iBeacon 广播包结构分几层:

数据段长度(字节)内容说明
PDU 头10x02表示后续广播数据长度
Flags1 + 10x01 0x06广播类型标志,LE General Discoverable
AD Length10x1A厂商数据段总长度(26 字节)
AD Type10xFF厂商特定数据
Company ID20x004CApple 公司标识符
iBeacon Type20x02 0x15标识这是一个 iBeacon 广播
UUID16自定义区分不同应用或厂商
Major2自定义通常表示区域编号
Minor2自定义通常表示具体设备编号
TX Power1自定义1 米处的参考 RSSI 值(有符号)

这里的 TX Power 是整个测距功能的关键。它表示接收者在距离 1 米处理论上测到的 RSSI 数值,单位是 dBm,因为有符号所以用 int8_t 表示。广播端把这个值写进包里,接收端拿这个作为参考基准。也就是说,如果广播端把发射功率调高了,TX Power 值也应该同步调大,否则接收端算出来的距离会产生系统性偏差。

顺带提一句,iBeacon 是苹果家的格式,如果做 Android 端或者更通用的场景,也可以用 Google 推出的 Eddystone 格式。Eddystone 除了有类似 iBeacon 的 UID 帧,还支持 URL 帧和 TLM(遥测)帧,比如广播温湿度数据。我自己实际项目里两种格式都遇到过,但从测距角度看,核心都是读取参考 RSSI 和实测 RSSI 做衰减计算,协议帧不同不影响本讲思路,我就以 iBeacon 为例展开,掌握了之后换 Eddystone 也就是改解析逻辑的事。

3.2 在 ESP32 上配置自定义 iBeacon 广播

官方示例工程里已经把广播数据组织的部分写好了,我们只需要改几个参数。核心代码逻辑在ibeacon_demo.c里,示例会调用esp_ble_gap_config_adv_data配置广播数据。

关键修改点:

  • UUID:改成自己的应用标识,16 字节。
  • Major:通常用于表示楼层或区域。
  • Minor:表示同一区域里的具体设备编号。
  • TX Power:需要和实际发射功率匹配,多数 ESP32 默认配置下参考值可以写 -59 dBm(iOS 设备实测经验值),后续校准后再调整。

广播参数上,示例里用的是系统默认,但实际项目中建议自己设置。重点调两个参数:

  1. 广播间隔(adv_int_min / adv_int_max):单位是 0.625 毫秒。间隔越短,接收端发现越快、测距刷新率越高,但功耗越大。我在做防丢器时广播间隔设在 100~200ms,兼顾发现速度和续航;做静态定位点名时甚至可以拉到 1 秒以上。
  2. 发射功率(tx_power):ESP32 的 BLE 发射功率可以通过esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, level)设置,level 范围从 -12dBm 到 +9dBm 左右。功率调高,覆盖距离远,但功耗也上去,而且 TX Power 字段要同步改。默认 +9dBm 在室内空旷场景大概能覆盖 30 到 50 米。

下面给出一个自定义广播参数和数据的核心代码段(基于 v5.x API):

#include "esp_gap_ble_api.h" #include "esp_bt.h" #include "esp_bt_main.h" #include "esp_gap_bt_api.h" // 在 ble_app_init() 中做配置 esp_ble_adv_params_t adv_params = { .adv_int_min = 0x20, // 32 * 0.625ms = 20ms .adv_int_max = 0x20, // 保持和 min 一致,方便观察 .adv_type = ADV_TYPE_IND, .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .channel_map = ADV_CHNL_ALL, .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, }; esp_ble_gap_config_adv_params(&adv_params); // 设置发射功率为 +9dBm esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9);

注意:广播包中所有数据必须一次性打包成一个连续 buffer 供esp_ble_gap_config_adv_data使用,官方示例在ibeacon_demo.c里有一个set_ibeacon的辅助函数,把esp_ble_ibeacon_t结构体转换成匹配广播协议栈的字节序列。如果你要自己改结构,务必记得把结构体的字节序和广播包的排列对齐,Intel 小端模式下 Major、Minor 这些字段要手动做字节序转换,否则手机上扫出来数值是反的。

4. ESP32 扫描 beacon 与 RSSI 测距实现

4.1 BLE 扫描器配置与广播包解析

测距的核心是作为接收端的 ESP32 要能扫描到蓝牙广播包,并读出每个包的 RSSI 值和厂商数据内容。ESP-IDF 里 BLE 扫描的启动非常简单,但有几个参数会影响测距效果,必须认真设置。

初始化 BLE 后,注册 GAP 回调,然后设置扫描参数并开始扫描:

static esp_ble_scan_params_t ble_scan_params = { .scan_type = BLE_SCAN_TYPE_ACTIVE, // 主动扫描,可拿到额外响应数据 .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, .scan_interval = 0x50, // 80 * 0.625ms = 50ms .scan_window = 0x30, // 30 * 0.625ms = 18.75ms .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE // 关闭去重,每个广播都回调 }; esp_ble_gap_set_scan_params(&ble_scan_params);

重点说scan_duplicate这个参数。系统默认会开启去重,同一个设备的广播包在去重周期内只上报一次,这对普通扫描场景是好事,但对测距是灾难,因为 RSSI 需要连续多次采样才能滤波。实测中我通常把它设为BLE_SCAN_DUPLICATE_DISABLE,这样每个广播包都会触发回调,RSSI 采样密度足够,滤波效果才上得来。代价是回调频率高,CPU 占用上升,这在后面低功耗优化里再讨论。

在 GAP 回调中,扫描结果的 event 是ESP_GAP_BLE_SCAN_RESULT_EVT,拿到scan_rst里包含bda(设备地址)、rssiadv_dataadv_data_len。iBeacon 的过滤逻辑就是查找adv_data中是否包含 0x02 0x15 这个标志序列,以及厂商 ID 是否是 0x004C。找到之后,从偏移位置取出 UUID、Major、Minor 和 TX Power。

代码思路如下:

case ESP_GAP_BLE_SCAN_RESULT_EVT: { esp_ble_gap_cb_param_t *param = (esp_ble_gap_cb_param_t *)arg; if (param->scan_rst.search_evt == ESP_GAP_BLE_SCAN_RESULT_EVT) { uint8_t *adv = param->scan_rst.adv_data; uint8_t len = param->scan_rst.adv_data_len; int rssi = param->scan_rst.rssi; // 自行实现查找 0x02 0x15 的逻辑,并提取字段 if (is_ibeacon(adv, len)) { // 记录到滑动窗口缓冲区,交给滤波和距离换算 } } break; }

这里特别提醒一个坑:adv_data指针指向的数据在回调返回后就失效了,如果要把广播数据缓存到自己的结构体里做后续处理,必须用memcpy复制一份,不能直接保存指针。否则回调返回后你拿到的可能是被覆盖或随机的内容。

4.2 RSSI 转距离的原理与参数校准方法

RSSI 换算距离的经典模型是数理统计里的对数距离路径损耗模型,公式是:

RSSI = A - 10 * n * lg(d)

反解出距离:

d = 10 ^ ((A - RSSI) / (10 * n))

其中:

  • A:距离 1 米时测得的 RSSI 绝对值(负值,比如 -59),相当于 iBeacon 里的 TX Power。
  • n:路径损耗指数,自由空间约 2,室内有遮挡时 2.5 到 4 不等。
  • d:估算距离,单位米。

为什么距离公式里必须预先知道 A 和 n?因为无线信号在现实环境里不是理想自由传播,墙壁反射、人体吸收、多径衰落都会让同样的距离呈现出完全不同的 RSSI 值。A 回答了“刚开始有多强”,n 回答了“衰减有多快”。这两个参数如果不经过实测校准直接拍脑袋填,测距结果可能偏差五米以上。

我在室内走廊做的一组实测数据很能说明问题(ESP32 做接收端,iPhone 做发射端):

实际距离(米)第一次 RSSI第二次第三次平均值
1-57-60-58-58.3
2-65-68-63-65.3
3-71-76-69-72.0
5-83-85-80-82.7
8-93-90-97-93.3

如果是理想环境(n=2,A=-59),5 米处理论 RSSI 应该是约 -73,但实测是 -82 到 -85,差了快 10 个 dB,按公式一算直接成了 9 到 12 米。所以强调一句:任何抛开现场校准的 RSSI 测距都是玩具。初版实现可以先用 A=-59、n=2 跑通流程,但要认真用的话,必须做一次校准。校准方法很简单:在目标环境里拉一条卷尺,从 1 米开始每隔 0.5 米或 1 米采集几十个 RSSI 样本求均值,然后用最小二乘把 A 和 n 拟合出来,再把参数回填。具体拟合方法我在第 5 章展开。

4.3 滤波处理:滑动平均和简易卡尔曼

就算校准做好,单次 RSSI 采样依然跳得厉害,直接算距离就会看到数值上下乱蹦。所以实际工程里必须加滤波。最常用的两个:

第一个是滑动平均。维护一个 N 个采样的环形缓冲区,每来一个新值,取平均值作为当前 RSSI 观测量。N 太小滤波效果差,N 太大会导致距离变化响应迟钝。我的经验是:小范围静态测距 N 取 5 到 8;移动目标跟踪,N 取 3 到 5,不然目标都走过去了显示的距离还没跟上。

第二个是简易一维卡尔曼滤波。如果嫌滑动平均太“钝”,可以试试这个思路。卡尔曼本质上是在“预测值”和“观测值”之间做一个带权重的折中,权重由系统噪声和测量噪声共同决定。对于 RSSI 这种随机噪声占主导的信号,效果通常优于滑动平均。

一个极简的卡尔曼实现如下:

typedef struct { float state; // 当前估计值 float cov; // 估计误差协方差 float q; // 过程噪声 float r; // 测量噪声 } kalman_1d_t; float kalman_filter(kalman_1d_t *kf, float measurement) { // 预测 float pred_cov = kf->cov + kf->q; // 更新 float gain = pred_cov / (pred_cov + kf->r); kf->state = kf->state + gain * (measurement - kf->state); kf->cov = (1.0f - gain) * pred_cov; return kf->state; }

参数调节技巧:如果发现滤波结果跟着原始数据抖得很厉害,说明测量噪声 r 设得太小;如果距离真实移动时反应慢半拍,说明过程噪声 q 设太小。默认可以取 q=0.05、r=2.0 作为起点,再根据波形微调。

我实际项目的做法是滑动平均加卡尔曼串起来:先用滑动平均去掉高频毛刺,再把平滑结果喂给卡尔曼,整体曲线既平滑又不至于反应太慢。代价是多一点 CPU,但 ESP32 双核 240MHz 完全跑得动。

5. 实测调试记录与误差标定

5.1 实测环境与数据采集过程

为了让大家对 beacon 测距有直观认识,我特意搭了一套实测环境:ESP32 开发板放在走廊一端固定位置,用 USB 供电,另一端用另一块 ESP32 作为可移动 beacon 发射端,沿走廊每隔 1 米采集 100 个 RSSI 样本,分别记录均值和标准差。走廊宽度约 2 米,两侧有金属门和墙面,属于典型的室内非理想环境。

采集程序里我开了 4.1 节说的扫描参数,不做滤波,原始 RSSI 直接通过串口打印出来,用 Python 脚本收集整理。这里要提醒的是,如果串口波特率太低,大量广播回调会导致打印数据丢失,建议串口波特率设为 115200 或 2000000,并且适当降低广播频率(500ms 间隔)来缓解数据洪峰。

100 个样本统计下来,几个关键位置的数据是这样的:

距离(米)RSSI 均值标准差最小最大
1-58.22.1-54-65
2-66.42.5-61-72
3-72.83.2-67-80
5-83.54.1-76-95
8-92.75.7-85-104

能看到一个规律:距离越远,标准差越大,这正好对应着 log 模型里距离和 RSSI 的非线性关系。0~3 米内的测距区分度很好,超过 5 米后 RSSI 波动加剧,距离分辨率明显下降。这个问题不是 ESP32 特有的,所有 RSSI 测距方案都逃不掉。

5.2 标定流程:把实测数据拟合成可用参数

拿到一组 (RSSI, d) 数据之后,需要反推出 A 和 n。手工算比较麻烦,我一般用 Python 的 numpy polyfit 或 scipy.optimize.curve_fit 来做最小二乘拟合。原理就是让模型预测值和实测值的误差平方和最小。

上面那组数据拟合的结果大致是 A ≈ -58.5,n ≈ 2.35。把它回代到代码里,做个效果验证:

实际距离(米)用默认参数估算用校准参数估算
11.11.0
22.32.1
33.93.2
57.85.4
813.58.7

校准后误差从动不动几米缩到多数时候一米以内,近距离误差基本在半米内。这个精度做防丢提醒、区域判定足够了,做高精度定位就得叠加多个锚点做三边测量。

三边测量的思路是:用至少三个已知坐标的接收端(或 beacon),各自测出到目标的距离,然后以各自坐标为中心画三个圆圈,交点就是目标位置。考虑到 RSSI 有噪声,实际用最小二乘求解,而不是纯几何求交。我把代码逻辑放在一个结构体里,思路就是用一个目标函数表示“目标点假设坐标与三个距离的吻合度”,然后用梯度下降或高斯牛顿迭代优化。这部分如果你项目需要可以继续深耕,这一讲先把单点测距做扎实。

5.3 提升精度的实用小技巧

实测做得多了,总结出几个能明显改善精度的土办法:

第一,多天线、多接收点冗余。一个接收点 RSSI 跳得厉害,就把接收点布置成阵列,取多个点的加权平均。我在一个房间里放过 4 个 ESP32 接收端,对同一个 beacon 的测量结果做加权融合,误差能再缩小 30% 左右。

第二,对采样数据做样本剔除。RSSI 偶尔会冒出一个极端值,比如瞬间低 20 个 dB,这是广播碰撞或遮挡造成的。用滑动窗口内的中位数替代平均值,或者把偏离均值超过 2 倍标准差的样本丢掉,都能减少偶发毛刺。

第三,记录环境变化因素。人站在中间、金属门开关、雨水天气,都会让 RSSI 产生几 dB 的偏移。项目上线之后如果要长期稳定,需要在代码里做一个自适应背景校准的机制:比如定期测量一个已知距离上的 RSSI 均值,发现整体漂移就修正 A 参数。

6. 实战常见问题与排查技巧实录

6.1 扫不到 beacon:先把排查顺序理顺

实际开发中问得最多的就是“我的 ESP32 怎么扫不到设备”,这个问题在 beacon 场景下尤其常见。我建议按照下面这个顺序排查:

第一,确认广播端在正常工作。用手机上的 nRF Connect 先扫一下,如果能扫到,说明广播没问题,问题在接收端;如果手机也扫不到,回头看广播端的esp_ble_gap_start_advertising是否被调用,广播间隔和发射功率设置是否合理。

第二,检查接收端扫描参数。扫描窗口和扫描间隔如果配得太小,比如窗口只有几毫秒,很可能刚好错过广播包,尤其是广播间隔在 100ms 以上的情况。直接把scan_window调到接近或大于scan_interval的一半,能显著提高扫描命中率。代价是功耗上升,看项目取舍。

第三,检查回调里的过滤逻辑。很多同学扫不到是过滤条件写错了,比如判断adv_data里某个偏移位置的字节值,结果偏移算错;或者用大小写敏感的方式比较厂商 ID。调试时最简单的办法是先把所有扫描结果连同原始adv_data以十六进制格式打印出来,肉眼看广播数据再反推解析逻辑。

6.2 RSSI 跳变严重、测距结果忽远忽近

这是 RSSI 测距最让人头疼的问题,但大部分原因是没做滤波,或者滤波参数不合适。我的经验是按这个优先级排查:

  • 是否已经关掉系统去重?如果没关,一秒只能采到一两个样本,滤波窗口形同虚设。
  • 滑动窗口长度是否合适?窗口太短,滤波效果不明显;太长,距离变化时响应太慢。建议先从 N=5 起步,观察串口输出曲线再调整。
  • 广播端是否在移动?如果 beacon 本身在移动,任何滤波都只能延迟响应,需要从算法上做外观测量(比如速度估计),但这属于另外一个复杂度层级,一般不轻易上。

另外一个非常容易被忽略的问题:回调里的处理耗时过长。BLE 扫描回调是在蓝牙协议栈的任务上下文中执行的,如果在里面做了大量日志打印、字符串格式化、甚至动态内存分配,会直接拖垮协议栈,导致广播包丢失或 RSSI 采样率严重下降。正确做法是回调里只做数据拷贝和标志位更新,解析、滤波、算距离全部放到应用任务里做。

6.3 蓝牙和 Wi-Fi 共存时的互相干扰

ESP32 上 Wi-Fi 和 BLE 共用 2.4GHz 频段和同一天线,同时工作会互相抢占资源。实测中开 Wi-Fi 传大文件时,BLE 扫描的 RSSI 会明显跳动,甚至出现广播包丢失。乐鑫的方案是两者分时复用,框架底层会自动做仲裁,但优先级分配得不好就会导致 BLE 响应变慢。

我的实践建议:测距场景尽量避免高吞吐 Wi-Fi 传输同时进行。如果架构上避不开,可以把 Wi-Fi 的调制方式和带宽调低,比如用 20MHz 频宽,减少占用的空口时间;或者降低 Wi-Fi 数据上行频率,让 BLE 有更多机会跑。另一个方案是换用 ESP32-C3 或 ESP32-C6,它们虽然也是单天线,但共存调度上比老 ESP32 要顺滑一些。如果项目要求测距和联网并发,这个因素在选型阶段就要考虑到。

6.4 低功耗优化:beacon 模式下把电流压到最低

beacon 测距的典型部署场景是电池供电,功耗是绕不开的话题。先说广播端:beacon 广播端功耗主要由广播间隔决定。间隔 100ms 时,ESP32 的 BLE 广播平均电流大概在几十微安到几百微安之间,间隔拉到 500ms 以上可以有效降低功耗。若不需要实时响应,甚至可以在广播之间进 light sleep,定时唤醒发一个包再睡过去,平均功耗能做到极低。

再说接收扫描端:如果接收端不需要 7x24 小时监听,可以设置一个占空比方案,比如扫描 1 秒、深度睡眠 10 秒,这样平均电流能大幅下降。如果要求持续性监测,把扫描窗口和间隔同时拉长也有帮助,但要注意过长的扫描间隔会导致发现延迟增大。我做过一个低功耗传感器节点,beacon 扫描占空比 10%(1s 扫描 + 9s 睡眠),平均电流压到了 1mA 以下,还能保证 1 到 2 秒内响应目标靠近事件。

6.5 几个容易踩的坑速查表

把这段时间踩过的坑整理成一张表,方便大家排查:

现象可能原因解决办法
编译报错找不到蓝牙头文件CMake 里没加 bluedroid 组件在 CMakeLists.txt 的 REQUIRES 里加上 bt 和 bluedroid
手机扫描设备名显示乱码广播包数据排列错误检查字节序和 adv_data 长度,逐字节对照协议
接收端 RSSI 始终固定不变扫描去重没关闭设置BLE_SCAN_DUPLICATE_DISABLE
测距结果整体偏大路径损耗指数 n 设太小按现场校准,室内一般 2.2~3.5
测距结果整体偏小参考 A 值偏小重新标定 1 米处 RSSI 均值
打日志后系统变卡回调里做耗时操作回调内只复制数据,任务里处理
同时开 Wi-Fi 后 BLE 丢包射频共存调度分时使用,或降低 Wi-Fi 空口占用

这些坑里最隐蔽的是第一个编译组件问题,ESP-IDF 的组件依赖是显式声明的,用例程派生工程没关系,但自己建工程时漏掉REQUIRES bt bluedroid,就会得到一堆找不到头文件的报错。另外提醒一下,ESP32 的 NVS flash 分区必须存在,蓝牙初始化时协议栈会读取 NVS 里保存的蓝牙配置,如果分区表里没配 NVS,初始化会直接失败。默认分区表没问题,但如果你为了省 flash 改了分区表,这一步很容易漏掉。

这个系列玩到这,我个人最大的体会是:beacon 测距看起来只是读几个 RSSI 值套个公式,但真要落地到一个产品、一个具体场景,核心工作量全在“把不确定的东西变得尽量确定”上——环境标定、参数拟合、滤波调参、共存避让,一个都不能省。跳过了这些,你拿到的就是一个数字玩具;把它们做好了,这套测距方案才算是真正从 demo 变成了可用的功能。下一步如果你想接着深入,可以试着在这套单点测距基础上做多 beacon 的三边定位,再把结果通过上一讲的 MQTT 通道发到云端做位置展示,那就是一个完整的室内定位应用原型了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 16:57:37

雷达Simulink仿真中的RF前端行为级建模与参数化实践

简介:这份资源面向雷达系统设计工程师与Simulink建模学习者,围绕射频前端行为在雷达系统级仿真中的整合展开,提供单站脉冲雷达目标探测与FMCW雷达距离/速度估计两套完整可运行模型。资源共9个文件,其中5个m脚本用于参数配置与仿真…

作者头像 李华
网站建设 2026/9/12 16:57:25

ESP32蓝牙Beacon测距实战:RSSI原理、代码实现与参数标定

1. 蓝牙beacon测距的总体设计思路开讲之前先说个事:这个系列的每一讲,我都在尽量控制篇幅,结果每讲写出来都能赶上小论文。倒不是因为废话多,而是ESP-IDF这套框架里,很多看起来“一行搞定”的功能,真要讲清…

作者头像 李华
网站建设 2026/9/12 16:55:30

FingerprintJS 如何关闭监控请求避免发送使用统计?

FingerprintJS 如何关闭监控请求避免发送使用统计? 【免费下载链接】fingerprintjs The most advanced free and open-source browser fingerprinting library 项目地址: https://gitcode.com/GitHub_Trending/fi/fingerprintjs 如果你用 NPM 或 Yarn 把 Fin…

作者头像 李华