ESP32 BLE Beacon测距实战:从RSSI到距离估算的完整指南
2026/9/11 5:09:16 网站建设 项目流程

一直做 ESP32 的联网项目,Wi-Fi 相关的应用是熟门熟路了,直到有个室内展馆的需求把我按在地上摩擦:参观者走近某个展品到一定距离时,要触发语音讲解,精度不需要厘米级,但要稳定且延迟低。GPS 进室内直接废掉,Wi-Fi 指纹定位又太重,最后我盯上了蓝牙 beacon 方案。于是重新把 VSCode + ESP-IDF 这套环境翻出来,用 ESP32 扫描 BLE beacon 做 RSSI 距离估算,从原理到代码再到实际标定完整跑了一遍。这篇文章就是把这段经历整理出来,包括 iBeacon 帧格式、扫描回调处理、距离换算公式、参数标定方法,还有一堆只有真机实测才会踩到的坑,给做室内定位、防丢提醒、近场交互项目的朋友省点时间。

1. Beacon 测距的底层逻辑:为什么 RSSI 能换算成距离

1.1 室内定位方案对比:为什么偏偏是蓝牙 beacon

做室内距离触发,可选的方案其实有好几个,但每个都有明显的短板。

GPS 在室内的信号衰减非常严重,尤其是混凝土结构的建筑里,定位误差可以到几十米,精确定位基本不用想。UWB(超宽带)方案精度确实高,能做到厘米级,但成本也高,beacon 标签几百块一个,接收端设备也不便宜,对大部分应用来说属于杀鸡用牛刀。Wi-Fi 指纹定位需要先在场地里做密集的指纹采集,现场环境一变(比如移动了货架、关了扇门)指纹数据就失效,维护成本太高。

蓝牙 beacon 在这个场景下刚好卡在中间:beacon 标签几块钱到十几块钱一个,广播功耗极低(一块纽扣电池能用一年以上),手机、嵌入式设备几乎都支持 BLE 扫描。更重要的是,ESP32 本身就带 BLE 硬件,等于接收端零额外成本。所以最终方案很明确:在展品旁边放一个或者多个 BLE beacon,ESP32 作为扫描接收端持续监听,通过接收到的信号强度估算距离,达到阈值就触发动作。

1.2 信号衰减模型:RSSI 和距离不是线性关系

蓝牙 beacon 测距的原理说白了就是一件事:信号在空中传播时会衰减,距离越远,接收端的 RSSI 值越低。但这里面有个关键点,RSSI 和距离之间不是线性关系,而是对数关系。

信号在自由空间里衰减的物理模型中,接收功率和距离的关系大致是:

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

其中:

  • A 表示距离发射端 1 米时测到的 RSSI 值(dBm)
  • n 是路径损耗指数,和环境密切相关。自由空间里 n 约等于 2,室内因为有墙壁反射、人体吸收等干扰,n 通常在 2.5 到 4 之间
  • d 是距离(米)

为什么要用对数而不是线性?因为信号能量在传播中是按指数规律衰减的。就好比声音,你在 1 米外听到的音量和 2 米外不是差一半,而是差了大概 6dB。蓝牙信号每远离一倍距离,RSSI 大约下降 6dBn 个 dB,这个关系用对数画出来是一条接近直线的曲线,用线性去拟合反而误差更大。

这个公式是后面所有距离换算的基础,理解这一点,后面标定参数时就不会懵。

1.3 iBeacon 帧格式到底长什么样

市面上 BLE beacon 协议有两类主流:iBeacon(苹果提出)和 Eddystone(谷歌提出)。我这次用的是 iBeacon,ESP-IDF 官方示例也是以它做演示的,理解一个,另一个也就顺带通了。

iBeacon 的广播数据包结构是这样的:

字段字节数内容
Company ID2厂商标识,Apple 是 0x004C
iBeacon 标识2固定为 0x02 0x15,用于识别这是 iBeacon 帧
UUID16区分不同应用或项目
Major2区分同一应用下的分组(比如楼层)
Minor2区分同一分组下的具体设备(比如具体展位)
TX Power1距发射端 1 米处的 RSSI 参考值(有符号数)

注意这里的字节序很关键,Major 和 Minor 是大端模式,也就是高字节在前。我在第一次解析时就直接按小端读了,结果数字完全对不上,查了半天才发现是这个问题。

TX Power 字段值得多说两句。它代表的是距离发射端 1 米处应该测到的 RSSI 理论值,由 beacon 标签厂家出厂时标定写入。正常来说这个值在 -59dBm 到 -65dBm 之间。有些文章里的公式用 TX Power 代替 A 值直接用,但实际上,厂家标定的 TX Power 是基于他们自己的测试环境,你的使用环境墙面地面材质、天线摆放方向、接收端不同,实际 1 米处的 RSSI 都会不一样。所以做项目时,最好还是自己实测标定 A 值,不要盲目信任 beacon 里写死的那个 TX Power。

2. VSCode + ESP-IDF 环境准备:BLE 扫描器工程搭建

2.1 工程创建前必须确认的 ESP-IDF 版本

如果你已经装好了 VSCode 和 ESP-IDF 插件,可以直接跳过这段。但如果你像我一样是从零开始,或者准备新开一个项目,有几个版本相关的问题值得先确认。

我这边用的是 ESP-IDF v5.4.4,VSCode 装的是 Espressif IDF 官方插件。在创建工程之前,先把插件安装好,然后在 VSCode 的命令面板里输入ESP-IDF: Configure ESP-IDF Extension,插件会自动检测或者帮你下载指定版本的 ESP-IDF 工具链。这里有个经验:别贪新直接上最新版本,ESP-IDF 的 API 在不同版本间是有变动的,我就遇到过 v5.2 和 v5.4 在蓝牙回调函数参数类型上做微调的情况。如果项目赶时间,建议用官方长期支持的版本,比如 v5.3.x 或 v5.4.x,配套的示例代码也最稳定。

另外要注意,ESP-IDF 插件默认会创建一个工作区,工程路径里尽量不要包含中文和空格,否则编译时可能出现一些莫名其妙的路径问题。

2.2 menuconfig 蓝牙协议栈配置

创建工程时,VSCode 命令面板输入ESP-IDF: Create Project from Template,在模板列表里找bluetooth/bluedroid/ble/ble_ibeacon,这个示例虽然名字是 iBeacon,但它同时包含了广播端和扫描端两种角色,做扫描端可以少写不少初始化代码。

工程创建完成后,第一件事不是写代码,而是用ESP-IDF: Menuconfig打开配置界面,把蓝牙栈搭好。

要修改的配置项:

Component config → Bluetooth → Bluetooth → Enabled Component config → Bluetooth → Bluetooth host → Bluedroid Component config → Bluetooth → Bluedroid Options → Max Classic Bluetooth Connections → 0

如果扫描端只用 BLE,不涉及经典蓝牙,把经典蓝牙连接数设为 0 可以省下不少内存。同理,Max BLE Connections也改成 1,因为我们只是扫描端,不建立连接。这样编译出来的固件体积更小,运行时也更稳定。

这里补充一个选择点:ESP-IDF 支持两套蓝牙协议栈,Bluedroid 和 NimBLE。Bluedroid 功能全、兼容性好,官方示例多,但内存占用大;NimBLE 轻量、代码简洁,适合纯 BLE 场景。我这次用的是 Bluedroid,因为要参考官方 ble_ibeacon 示例,用 esp_ble_gap 系列 API 写起来直接。如果你的项目只有扫描这一个功能,NimBLE 也是个不错的选择,但 API 体系和 Bluedroid 不同,代码没法直接照搬。

2.3 扫描模式选择:主动扫描是关键

配置完协议栈,还要在代码里碰到一个容易被忽略的概念:扫描模式。

BLE 扫描分为主动扫描(Active Scan)和被动扫描(Passive Scan)。被动扫描只是干听,只接收广播者周期性发的 ADV_IND 广播包;主动扫描会在收到广播后,额外发送一个 SCAN_REQ 给广播者,请求对方再发一个包含更多数据的扫描响应包(SCAN_RSP)。

那 beacon 数据到底在哪?大部分 iBeacon 直接把广播数据放在 ADV_IND 的 adv data 里,被动扫描也能拿到。但有些广播设备厂商会把一部分数据塞进 SCAN_RSP,这时候如果用被动扫描,就永远拿不到完整数据。所以我的建议是直接用主动扫描,多花一点点功耗,换来的数据完整度是值得的。

扫描参数结构体里相关配置项:

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 = 0x100, .scan_window = 0x50, .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE };

scan_intervalscan_window的单位是 0.625 毫秒,所以 0x100(256)约等于 160ms 的扫描间隔,0x50(80)约等于 50ms 的扫描窗口。窗口越大,抓到广播包的概率越高,功耗也越高。对 beacon 这种一般 100ms 广播一次的场景,这个比例覆盖得比较稳。

scan_duplicate这个参数更关键。如果设成 ENABLE,硬件会自动过滤掉重复地址的广播,beacon 是周期性广播,MAC 地址不变,很容易被过滤掉,导致只触发一次回调就再也不更新了。所以扫描 beacon 时务必设成 DISABLE。

3. 扫描与解析:从裸广告数据到完整 iBeacon 信息

3.1 初始化 BLE 主机并设置扫描参数

用 Bluedroid 协议栈,初始化流程比想象中要短,核心就三步:注册回调、设置扫描参数、开始扫描。

esp_err_t ret; esp_bluedroid_init(); esp_bluedroid_enable(); esp_ble_gap_register_callback(gap_event_handler); esp_ble_gap_set_scan_params(&ble_scan_params); ret = esp_ble_gap_start_scanning(30); ESP_LOGI(TAG, "start scanning, ret=%d", ret);

esp_ble_gap_start_scanning的参数是扫描持续秒数,传 0 表示持续扫描直到主动调用esp_ble_gap_stop_scanning()。我习惯传 0,靠应用逻辑控制扫描生命周期。

这套流程几乎不需要修改,直接搬进工程就行。要注意的是,esp_bluedroid_enable()之后不能立刻调用esp_ble_gap_*系列的 API,需要等蓝牙控制器真正 ready。实际开发中如果你发现前几步没问题但扫描从来没开始,多半就是这里时序问题。稳妥的做法是在ESP_BT_STATUS相关的事件确认后再触发扫描,不过 ESP-IDF 内部对大部分初始化调用做了队列缓冲,直接链式调用也基本能跑通,只是不推荐。

3.2 扫描回调事件处理流程

核心逻辑全在gap_event_handler里。扫描到广播帧时,系统会触发ESP_GAP_BLE_SCAN_RESULT_EVT事件,代码要在这个事件里辨别当前是哪种搜索结构:

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { // 处理单次扫描结果 handle_ibeacon_adv(&param->scan_rst); } else if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { ESP_LOGI(TAG, "scan completed"); } break; default: break; } }

这里容易犯的错是把ESP_GAP_BLE_SCAN_RESULT_EVTESP_GAP_SEARCH_INQ_RES_EVT混为一谈,实际上前者是事件类型,后者是当前扫描结果的具体状态。事件里的scan_rst结构体包含了好几个关键字段:

  • bda:广播者 MAC 地址
  • rssi:本次测得信号强度
  • ble_adv:广告数据缓冲区
  • adv_data_len:adv data 的长度
  • scan_rsp_len:扫描响应数据的长度

注意,ble_adv缓冲区里既有 adv data 又有 scan rsp,需要根据两个长度字段区分区间。我当时就是想当然地认为整个缓冲区都是 adv data,结果解析出了谁都看不懂的乱码。

3.3 解析厂商特定数据段

拿到广告数据后,下一步是从里面把 iBeacon 厂商数据提取出来。BLE 广播数据是按 AD Structure 组织的,每个结构是[长度][类型][数据]三元组,长度字段包含类型字节但不包含长度字节本身。

iBeacon 属于厂商特定数据,AD type 是 0xFF。ESP-IDF 提供了现成接口:

uint8_t *adv_data = param->scan_rst.ble_adv; uint8_t len = param->scan_rst.adv_data_len; uint8_t manu_len = 0; uint8_t *manu_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_MANU_FACTURER, &manu_len);

这个函数会在广告数据里自动遍历查找指定类型的 AD Structure,返回数据段指针。找到厂商数据后,再按 iBeacon 格式进一步解析:

typedef struct { uint8_t company_id[2]; // 厂商ID uint8_t ibeacon_type[2]; // iBeacon 固定标识 0x02 0x15 uint8_t uuid[16]; // UUID uint16_t major; // 大端序 uint16_t minor; // 大端序 int8_t tx_power; // 1米处RSSI参考值 } __attribute__((packed)) ibeacon_info_t; if (manu_data != NULL && manu_len >= 25) { ibeacon_info_t *info = (ibeacon_info_t *)manu_data; if (info->company_id[0] == 0x4C && info->company_id[1] == 0x00) { if (info->ibeacon_type[0] == 0x02 && info->ibeacon_type[1] == 0x15) { uint16_t major = (info->major >> 8) | (info->major << 8); uint16_t minor = (info->minor >> 8) | (info->minor << 8); int8_t tx_power = info->tx_power; // 到这里就是一个完整有效的 iBeacon 帧 } } }

manu_len >= 25这个判断是必须的,因为一个完整的 iBeacon 厂商数据段最少有 25 字节(2 字节厂商 ID + 2 字节 iBeacon 标识 + 16 字节 UUID + 2 字节 Major + 2 字节 Minor + 1 字节 TX Power)。我之前写的是>= 23,虽然也能通过判断,但容易让后面结构体强制转换时发生越界访问,建议直接写成 25。

3.4 多 beacon 管理:MAC 与 UUID 的双重区分

实际项目里不可能只有一个 beacon,展馆里几十个展位,每个展位放一个,扫描端必须能区分谁是谁。区分逻辑有两层:

第一层是靠 UUID 区分归属。同一个项目的所有 beacon 共享同一个 UUID,解析出来以后先和预置的 UUID 比对,不一致的直接丢弃,这样外来的干扰 beacon 就不会混进来。

第二层是靠 Major/Minor 或者 MAC 地址区分具体设备。这里我建议优先用 MAC 地址做设备 ID,因为 MAC 在扫描结果里本来就带着,不需要额外解析。但要注意,beacon 广播是可以改 MAC 地址的(随机地址类型),如果厂商固件里开了 MAC 随机化,MAC 就会周期性变化,这时候就不能用它做稳定标识,只能靠 Major/Minor 组合来识别。

在代码里,我维护了一个简单的结构体数组:

#define MAX_BEACON_NUM 16 typedef struct { uint8_t mac[6]; uint8_t major[2]; uint8_t minor[2]; int8_t rssi; float distance; uint32_t last_seen; } beacon_node_t; static beacon_node_t s_beacons[MAX_BEACON_NUM];

每次扫描到有效 iBeacon 帧,就按 MAC 或者 Minor 值在数组里查找,找到就更新 RSSI 和last_seen,没找到就找空槽位插入。定期检查last_seen,超过 3 秒没更新的节点判定为离线,移除。这个环形缓冲的思路对几十个 beacon 的场景完全够用,不需要上复杂的哈希表。

4. RSSI 到距离:标定、滤波与换算公式实操

4.1 路径损耗公式与参数的物理含义

有了 RSSI 值,下一步就是换算成距离。这里我再强调一遍公式:

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

其中 A 是 1 米处测到的 RSSI 参考值,n 是路径损耗指数。

这个公式是从对数衰减模型反推出来的。为什么 RSSI 和距离的关系能用这个公式?因为电磁波在传播中,接收功率与距离的 n 次方成反比(自由空间里 n=2,也就是平方反比关系),用 dB 表示,接收功率就是距离对数的线性函数。反过来,知道 RSSI 就能反推距离。

这里有个新手最容易犯的错:直接把 iBeacon 帧里的 TX Power 当 A 用。TX Power 确实是厂家在 1 米处测出来的 RSSI,但那是针对厂家测试环境、厂家接收设备和无遮挡环境来说的。你的场地地面、墙面、金属装饰、无线干扰源,都会让实际 1 米处的 RSSI 偏离这个标称值。我实测过同一个 beacon,手机在 1 米处测到 -58dBm,ESP32 在同一位置测到 -61dBm,差了 3dB。3dB 看着不大,代入公式算距离误差可能超过三成。所以必须实测标定。

4.2 实测标定 A 值和 n 值的完整过程

标定这事说难不难,但要做对。我用的方法分三步:

第一步,标定 A 值。找一个相对空旷的场地,把 beacon 和 ESP32 都固定在高度 1 米左右的位置(比如用三脚架或者堆箱子),距离正好 1 米(用卷尺量,别目测)。连续采集 50 次 RSSI,取平均值作为 A。为什么要 50 次?因为 RSSI 单次测量波动很大,同一位置连续采可能相差 5 到 8dB,只有取平均才能把随机起伏消掉。

第二步,标定 n 值。把 beacon 移到 3 米处,同样固定高度,连续采 50 次 RSSI 取平均。然后代入公式反求 n:

n = (A - RSSI_3m) / (10 * log10(3))

比如实测 A = -61dBm,3 米处平均 RSSI = -74dBm,那 n = (61 - 74) / (10 * 0.477) ≈ 2.72。这个 2.72 在室内场景算正常区间,如果算出来超过 4,说明场地遮挡太厉害,或者有强反射,beacon 测距的可靠性会明显下降。

第三步,验证。把 beacon 放到 2 米处,用标定的参数算距离,看误差在多少。我这边实测下来,2 米处算出来 1.8 到 2.3 之间波动,这个精度对触发类应用已经够用。如果误差超过 0.5 米,建议重新确认场地环境是否过于复杂,或者多增加几个采样点做分段拟合,把场地划分成"近距离段"和"远距离段",用不同的 n 值。

4.3 滑动平均与异常值剔除的组合滤波

RSSI 波动大是测距误差的主要来源,尤其在有人走动、有金属架的室内环境,单次测量值能跳 10dB 以上。如果不滤波直接代入公式,算出来的距离会像心跳一样来回蹦。我试过把原始 RSSI 直接换算距离,1 秒内的输出能从 0.8 米跳到 2.7 米,完全没法用。

最简单的滤波是滑动平均,开一个环形缓冲区,存最近 N 次 RSSI 值,每次取平均:

#define RSSI_FILTER_SIZE 10 static int8_t s_rssi_buf[RSSI_FILTER_SIZE]; static uint8_t s_rssi_idx = 0; static int8_t rssi_filter(int8_t new_rssi) { s_rssi_buf[s_rssi_idx] = new_rssi; s_rssi_idx = (s_rssi_idx + 1) % RSSI_FILTER_SIZE; int32_t sum = 0; for (int i = 0; i < RSSI_FILTER_SIZE; i++) { sum += s_rssi_buf[i]; } return (int8_t)(sum / RSSI_FILTER_SIZE); }

但单纯滑动平均有个问题:如果某次测量出现极端值(比如人体正好挡住天线,RSSI 瞬间掉到 -95dBm),平均结果会被拉偏。所以在平均之前,我加了一步异常值剔除:把当前采样值和上一次滤波输出比较,偏差超过 8dB 的就丢弃,不进入滑动窗口。这里 8dB 的阈值不能设得太小,否则正常波动也会被滤掉导致更新迟滞;也不能太大,否则起不到剔除作用。实测下来 8 到 10dB 比较合适。

这一套组合滤波代码量不大,但效果非常明显。加上滤波后,距离输出从"乱跳"变成了"平滑爬升",对触发类应用来说体验完全不同。

5. 数据落地:串口可视化与 OLED 实时显示

5.1 JSON 格式输出串口,方便上位机解析

测距结果最终要给业务逻辑用。我这里分了两条路:一条是开发调试时用串口输出,方便在电脑上实时看数据;另一条是接 OLED 屏幕,现场直接显示。

串口输出我用的是 JSON 格式,一行一条记录:

ESP_LOGI(TAG, "{\"mac\":\"%02X:%02X:%02X:%02X:%02X:%02X\",\"major\":%d,\"minor\":%d,\"rssi\":%d,\"distance\":%.2f}", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], major, minor, filtered_rssi, distance_m);

这里要强调一个坑:ESP-IDF 的ESP_LOGI默认会在日志前面加时间戳和 TAG 前缀,如果上位机要直接按 JSON 解析,得在 menuconfig 里把日志输出格式改成无前缀的,或者上位机解析时跳过前缀部分。我开发时直接用 VSCode 串口监视器看文本,倒是无所谓,但如果你要对着 Python 脚本解析,就得注意这个。

Python 端解析脚本很简单,用json.loads()逐行读取即可,如果日志带了前缀,就先用正则把{...}部分抓出来再解析。

5.2 OLED 实时显示距离

现场演示时不能总拖着电脑,我接了一块 0.96 寸 SSD1306 OLED,走 I2C 接口。这里顺带一提,如果项目里同时挂了其他 I2C 设备(比如温湿度传感器),ESP-IDF v5.x 支持多个 I2C 控制器,可以在 menuconfig 里分别配置两组 SDA/SCL 引脚,互不干扰。我就是把显示和其他传感器分到了两个控制器,省去了地址冲突和总线占用的问题。

OLED 驱动代码用现成的 ssd1306 驱动即可,显示逻辑很简单:每 1 秒刷新一次,第一行显示 Minor 值标识哪个 beacon,第二行显示 RSSI 和换算后的距离:

char line1[32], line2[32]; snprintf(line1, sizeof(line1), "beacon:%d", minor); snprintf(line2, sizeof(line2), "%.1fm rssi:%d", distance_m, filtered_rssi); ssd1306_draw_string(&dev, line1, 0, 0); ssd1306_draw_string(&dev, line2, 0, 16);

刷新频率不要太高,OLED 本身刷新带宽有限,而且 100ms 刷一次肉眼也看不出区别。1 秒一次既能直观看到距离变化趋势,又不给主循环增加负担。

5.3 扩展思路:三点定位与防丢提醒

beacon 测距最直接的扩展就是三点定位。原理很简单:已知三个 beacon 的物理坐标(比如在展厅平面图上是固定的),手机或移动端分别测到到三个 beacon 的距离,就能用三边测量法解出自身位置。前提是三个 beacon 位置别摆成一条直线,不然方程组解不出来。ESP32 做接收端时也是一样,相当于移动端,只需要周期性汇总三个方向的距离即可。

另一个我做过的扩展是防丢提醒。给贵重物品贴一个 beacon,人身上带一个 ESP32 手表或者手环,当距离超过预设阈值(比如 5 米)就触发振动或者蜂鸣。距离阈值不要设太死,因为 RSSI 波动在临界点附近会导致频繁触发,建议加迟滞逻辑:超过 6 米才报警,回到 4 米以内才解除,这样不会在边界反复横跳。

6. 实测中的坑与调参经验

6.1 扫描不到 beacon 的排查链路

第一次跑代码时,串口啥都没有,一个 beacon 都扫不到。我当时按这个顺序排查,没走弯路,分享出来:

第一步,确认 beacon 本身在广播。用手机装一个 nRF Connect 或者 Beacon Scanner 应用,看能不能搜到这个 beacon,能搜到就说明硬件没问题,搜不到先检查 beacon 电池和开关。

第二步,确认 ESP32 的配置。menuconfig 里 Bluetooth 有没有 Enable,Bluetooth host 是不是选对了,这些都是最基础但最容易漏的。

第三步,确认扫描参数。这里最隐蔽的是scan_duplicate设置,一旦设成 ENABLE,beacon 的周期广播会被过滤掉,表现就是回调事件只触发一次后就不再响应。

第四步,确认扫描窗口大小。如果scan_window太短,比如小于 10ms,beacon 广播可能正好落在窗口之外。尤其是 Wi-Fi 也在跑的情况下,窗口会被压缩,更不容易抓到。我最后用的是 interval 0x100、window 0x50,实测覆盖效果不错。

6.2 距离跳变严重:从硬件到软件的完整排查

扫到了 beacon,也换算出了距离,但距离值像股票走势图一样上蹿下跳,这是做测距项目百分之百会遇到的问题。我踩完一圈,总结出三个层面的原因:

硬件层面,ESP32 的 PCB 天线是有方向性的。我一开始把 ESP32 板子竖着放在桌面上,beacon 放在正前方,信号倒是稳定。后来为了现场布线方便把板子平放,同样的距离,RSSI 差了 6 到 8dB,距离直接从 1.5 米跳到 2.5 米。所以做标定和部署时,接收天线的朝向和高度必须固定,不要换来换去。

环境层面,人体遮挡和金属反射是两大元凶。展厅现场有人路过和没人路过,同一位置的 RSSI 能差 10dB 以上。金属展架对信号的反射会让某些位置信号增强(多径效应),有些位置信号反而相消变弱。这就是为什么我没法靠一套参数打天下,必须实地标定的原因。

软件层面,滤波参数没有适配到当前环境的波动幅度。前面说的异常值剔除阈值如果设得太小,会把正常的波动也滤掉,导致输出值一直卡在旧值不动。我建议拿到一组实测数据后先看一下 RSSI 的标准差,如果波动在 5dB 以内,窗口 10 次就够;如果波动超过 8dB,窗口拉大到 20 次,同时把剔除阈值放宽到 12dB。

6.3 Wi-Fi 与 BLE 共存干扰

ESP32 一个射频前端要同时服务 Wi-Fi 和 BLE,二者共用天线,同一时刻只能跑一种协议。Wi-Fi 是抢占式的,当 Wi-Fi 在大量收发数据时,BLE 的扫描窗口会被压缩甚至完全挤掉,表现就是 RSSI 采集变得稀疏,距离更新卡顿。

如果项目里 Wi-Fi 和 BLE 同时跑(比如测距同时还要把数据上传服务器),有几个可操作的优化方向:

  • 降低 Wi-Fi 工作强度,比如减少 MQTT 上报频率,别一直满速传文件
  • BLE 扫描参数调得更激进,窗口加大,弥补被抢占的部分
  • 实测下来,把 Wi-Fi 的广播间隔调大、关闭 WiFi 省电模式下的一些主动扫描行为,对 BLE 扫描的干扰会明显减少

我做过一组对照实验:Wi-Fi 空闲时,BLE 扫描到 beacon 的间隔稳定在 120ms 左右;Wi-Fi 高速下载时,这个间隔抖动到 500ms 以上。所以如果你发现距离刷新卡顿了,先别急着怀疑代码,看看是不是 Wi-Fi 占了射频。

另外,ESP32 的 2.4G 射频同时开 Wi-Fi 和 BLE 时,建议把 ESP32 和 beacon 的距离控制在 10 米以内,超过这个距离,接收灵敏度下降,RSSI 波动会更大,测距结果基本不可用。

调参这事,我给个土办法:先在空旷地标一组 A 和 n,然后拿实际场地跑一遍,看距离误差往哪个方向偏。如果算出来的距离普遍偏大,说明实际环境衰减比标定时厉害,就把 n 调大一点;反之调小。不用纠结非要一次性标定完美,beacon 测距本质上是个"够用就好"的东西,对触发类应用来说,阈值附近留够迟滞区间,波动就不至于影响功能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询