尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ESP32 BLE Beacon测距实战:从RSSI到距离估算的完整方案

ESP32 BLE Beacon测距实战:从RSSI到距离估算的完整方案 在嵌入式开发里待久了你会发现“测距”这件事永远是刚需。无论是室内定位、设备防丢、还是人靠近自动亮屏本质上都是先解决“距离估算”的问题。而ESP32自带的蓝牙BLE功能配合ESP-IDF这一套官方开发框架算是目前性价比最高、上手最快的方案之一。这篇我专门梳理一下基于ESP-IDF和VSCode开发ESP32蓝牙beacon测距的完整思路从环境搭建、协议原理到代码实现、实测标定一步一步把坑都填上给你一套能直接拿去用的流程。整篇会围绕一个核心思路ESP32 A作为beacon广播自己的RSSI信号ESP32 B负责扫描并解析这个信号再通过接收信号强度与距离之间的衰减关系估算出两台设备之间的距离。这个方案不需要额外硬件只用ESP32自带的蓝牙模块开发成本很低适合做室内定位、访客统计、智能联动这类场景。如果你手里正好有一块ESP32开发板而且C语言基础够用那么这篇内容你完全可以跟着做出来。即便你之前只玩过Arduino没碰过ESP-IDF我也尽量把环境配置、代码结构解释清楚让你少走弯路。1. 整体思路为什么选beacon测距而不是别的方式1.1 测距方案的取舍先把话放在前头——用蓝牙beacon测距不是所有场景的最优解。但如果你想快速搭一个低功耗、低成本的测距原型它确实是最省事的路径。我们来对比一下常见的几种方案方案成本精度功耗开发难度适用场景BLE beacon RSSI低1~5米低低室内分区、防丢提醒UWB高10~30厘米高高高精度定位Wi-Fi RTT中1~2米中中室内导航超声波低厘米级中中短距避障所以如果你只是想让设备在2~10米范围内感知“有人靠近了”或者“距离大致多远”BLE beacon这套方案完全够用。它的核心原理其实很简单发射端周期性广播数据包接收端拿到这个包的同时会附带一个RSSI值Received Signal Strength Indicator接收信号强度指示再根据信号强度随距离衰减的数学模型反推出距离。1.2 为什么选ESP-IDF VSCode这一套组合很多刚接触ESP32的人会直接用Arduino IDE因为它简单装好库就能点上传。但一旦你想认真做点产品化的东西比如自定义广播数据、精细控制蓝牙参数、优化功耗Arduino那层封装反而容易碍事。ESP-IDF是乐鑫官方的开发框架代码更底层、控制力更强、资料也最全。VSCode则是目前做ESP-IDF开发最顺手的编辑器。配合乐鑫官方的ESP-IDF Extension你能直接在编辑器里完成工程创建、编译、烧录、串口监视还能自动补全API、跳转定义。比起命令行折腾idf.py build这套工具链的体验确实提升了一个数量级。我个人的建议是环境一次性配好之后后面所有ESP32相关开发都在这套流程里跑不会有“换个项目又要重配环境”的问题。2. 环境准备VSCode ESP-IDF从零搭到能编译2.1 安装VSCode和ESP-IDF插件先说VSCode。直接去官网下载对应你操作系统的版本Windows、macOS、Linux都支持。安装过程没什么好说的一路“下一步”就行但有两个小细节值得注意安装路径不要带中文和空格避免后续工具链解析出奇怪的错误。插件建议只装必要的C/C微软出品、ESP-IDF Extension乐鑫官方、串口监视器ESP-IDF插件自带、可能再加个Bracket Pair Colorizer之类的辅助插件。ESP-IDF插件装完之后第一次启动会引导你配置工具链。它有两种方式一种是从头下载ESP-IDF和工具链另一种是指向你本机已有的ESP-IDF。如果你之前没装过建议直接选“通过插件下载ESP-IDF”插件会帮你把工具链、Python环境、OpenOCD这些一并装好整个过程大概要下载几百MB视网络情况需要10到30分钟。2.2 配置过程常见的大坑这里我要重点说一下配置环节的坑。很多人在第一次配置工具链时会卡在“Downloading ESP-IDF”这一步进度条半天不动。原因多半是网络问题——乐鑫的工具链托管在GitHub上国内访问有时候不稳定。解决办法有三种配置代理让插件下载走代理。手动下载ESP-IDF源码包解压到本地然后在插件设置里指定路径。配置环境变量IDF_GITHUB_ASSETS指定下载源为乐鑫的国内镜像。第三种方式我实测最省心。在VSCode的settings.json里加一条idf.gitHubAssetsUrl: https://dl.espressif.cn/github_assets重新配置一次下载速度就正常了。另一个高频报错是The path for ESP-IDF is not valid: /tools/idf.py not found.这个多半是因为没有正确选择ESP-IDF的安装目录或者目录里缺少idf.py这个脚本。如果你是手动下载的源码包一定要确保路径选到了包含idf.py的根目录例如C:\Espressif\frameworks\esp-idf-v5.1.1而不是选到了里面的examples子目录。2.3 创建第一个Beacon工程环境搭好之后我们要新建工程。在VSCode里按CtrlShiftP输入ESP-IDF: Create Project选择一个模板。但这里我们不需要从头写直接在工程里用bluetooth/bluedroid/ble下的例子改造会更快。我更推荐的方式是直接从零开始建一个空白工程然后加入Beacon广播和扫描的代码。这样你清楚每一句代码在做什么而不是复制了一堆不知道干嘛的模板代码。新建工程后工程目录大概是这个样子beacon_demo/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── main.c └── sdkconfig其实整个工程核心就两个文件顶层CMakeLists.txt和main/main.c。main.c里写的就是我们要实现的beacon广播和扫描逻辑。3. Beacon测距原理把RSSI换算成距离3.1 苹果iBeacon协议怎么定义广播包Beacon广播最有代表性的就是苹果的iBeacon协议虽然现在也可以用Eddystone或者自定义格式但iBeacon的字段定义值得先搞懂。一个标准iBeacon广播包的数据位大概是这样的字段长度含义Advertisement Type1字节0x02表示BLE广播Length1字节0x15后续数据长度Company ID2字节0x004C代表AppleiBeacon Type1字节0x02iBeacon Length1字节0x15UUID16字节应用标识Major2字节区域标识Minor2字节设备标识TX Power1字节1米处的RSSI参考值前三个字段加起来是广播类型和长度后面才是实际数据。UUID、Major、Minor用于标识你是哪个beacon、哪台设备。TX Power是接收者在1米处测到的RSSI值这个字段我们在距离换算时非常关键。3.2 RSSI转距离的衰减模型蓝牙信号在空气中传播时强度会随着距离增加而衰减。这种衰减关系在工程上常用对数距离路径损耗模型Log-distance Path Loss Model来拟合RSSI TXPower - 10 * n * log10(d)反过来如果已知RSSI和TXPower就能估算距离d 10 ^ ((TXPower - RSSI) / (10 * n))这里面有两个关键参数TXPower1米处测得的信号强度参考值一般通过校准实测得到。n路径衰减指数和环境息息相关。开阔空间大概2.0左右室内有遮挡大概是2.5到3.5隔着墙体可能到4以上。n越大信号衰减越快距离越短时RSSI掉得越厉害。这个公式就是你做测距时的核心引擎。很多开源项目直接把这个公式写死n2.0结果在室内测出来惨不忍睹。正确做法是先做一组实测标定把TXPower和n都标出来。3.3 为什么RSSI测距误差大以及怎么缓解RSSI的抖动是物理特性决定的没办法完全消除。多径反射、人体遮挡、Wi-Fi共频干扰、天线朝向都会让RSSI上下波动3~10dBm。dBm这个东西看着数值小但对距离结果影响巨大——比如TXPower-59dBmn2.0RSSI从-60变成-65算出来的距离就从1.12米变成2米近翻了一倍。缓解误差的办法有几个多次采样取平均比如连续取10个样本去掉最大值最小值再求均值。滑动窗口滤波每来一个新值就修改一次窗口平滑过渡。加一个简单的中值滤波把突变毛刺削掉。实测下来中值滤波滑动平均的组合最实用代码量小、实时性好。后面我会在代码里直接写出来。4. 代码实现Beacon广播端4.1 初始化BLE并设置广播参数广播端要做的就是把ESP32切换到广播模式周期性发送iBeacon数据包。整个流程分三步初始化BLE协议栈、配置广播数据、启动广播。先看初始化这块。在ESP-IDF里BLE相关的头文件是esp_gap_ble_api.h和esp_bt_main.h底层用的控制器接口是esp_bt_controller_mem_release和esp_bt_controller_init。我直接给一段能跑的代码#include stdio.h #include string.h #include esp_bt.h #include esp_bt_main.h #include esp_gap_ble_api.h #include esp_log.h #define TAG BEACON_ADV // 这里用你自己的UUID区分你自己的应用 static uint8_t adv_config_done 0; static uint8_t uuid[16] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; static esp_ble_adv_params_t adv_params { .adv_int_min 0x100, // 最小广播间隔 100ms .adv_int_max 0x100, // 最大广播间隔 100ms .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, };广播间隔这里我特意把min和max都设成了0x100也就是100ms。这个值不是随便拍的它决定了beacon的功耗和响应速度。广播间隔越短接收端发现beacon越快但耗电也越高。如果做防丢器可以把间隔拉到300~500ms省电优先如果做人靠近联动100ms能明显感觉响应更快。4.2 填充iBeacon广播数据接下来要构造iBeacon的广播数据。这里有个很容易踩的坑iBeacon的广播数据格式是固定的一个字节位置不对广播端虽然能跑但手机或接收端不认。static esp_ble_adv_data_t adv_data { .set_scan_respond false, .include_name false, .include_txpower false, .min_interval 0x100, .max_interval 0x100, .appearance 0x00, .manufacturer_len 25, .p_manufacturer_data (uint8_t *)iBeacon_adv_data, };注意上面这个结构体里的manufacturer_len设为25对应iBeacon数据的长度Company ID 2字节 iBeacon Type 1字节 iBeacon Length 1字节 UUID 16字节 Major 2字节 Minor 2字节 TX Power 1字节加起来正好25。构造这个数组也要仔细static uint8_t iBeacon_adv_data[25] { // 厂商字段头 0x02, 0x15, // 长度和类型 0x4C, 0x00, // Company IDApple 0x02, // iBeacon Type 0x15, // iBeacon Length // UUID 16字节 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, // Major 2字节 0x00, 0x01, // Minor 2字节 0x00, 0x02, // TXPower 0xC5 };最后那个TXPower0xC5是十进制197但实际值要用补码表示也就是197 - 256 -59dBm。也就是说这块板子在1米处测到的信号强度大约是-59dBm。这个值后期需要根据你自己的开发板实测校准不同板子的天线设计和布局会导致这个值差很多。4.3 启动广播并处理事件数据填充好之后调用esp_ble_gap_set_device_name设置蓝牙名字再调用esp_ble_gap_start_advertising启动广播。我这里把完整的GAP事件回调也写上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_ADV_DATA_SET_COMPLETE_EVT: adv_config_done (~ADV_CONFIG_FLAG); if (adv_config_done 0) { esp_ble_gap_start_advertising(adv_params); } break; case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: if (param-adv_start_cmpl.status ! ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG, adv start failed); } else { ESP_LOGI(TAG, adv start success); } break; default: break; } }这里有个使用ESP-IDF时的通用经验所有BLE的事件都是通过回调机制异步通知你的不像Arduino那样顺序执行。所以你的逻辑一定要分成“发起一步”和“收到事件后再做下一步”不然会出现数据还没设置好就启动广播导致广播内容为空的情况。5. 代码实现Beacon扫描端与距离解算5.1 扫描参数配置接收端的任务更重一些它要开启BLE扫描收到广播包后取出RSSI值再套用距离公式。扫描端的初始化逻辑和广播端一样但GAP事件分支不同主要处理的是ESP_GAP_BLE_SCAN_RESULT_EVT。扫描参数的配置static esp_ble_scan_params_t 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, // 扫描间隔 .scan_window 0x30, // 扫描窗口 .scan_duplicate BLE_SCAN_DUPLICATE_DISABLE };两个参数值得解释一下scan_interval表示多久扫描一次scan_window表示每次扫描持续多长时间。窗口越大发现设备越快但功耗也越高。0x50和0x30的比例大概就是窗口占间隔的一多半扫描比较积极适合实时测距如果做低功耗场景可以把window调小。注意.scan_duplicate设为BLE_SCAN_DUPLICATE_DISABLE也就是关闭过滤重复包这样每次收到广播都能触发回调方便连续采样。5.2 从广播数据中解析iBeacon并提取RSSI当扫描事件触发时可以这样拿到广播地址、RSSI和广播数据case ESP_GAP_BLE_SCAN_RESULT_EVT: { esp_ble_gap_cb_param_t *scan_result param-scan_rst; if (scan_result-search_evt ESP_GAP_SEARCH_INQ_RES_EVT) { uint8_t *adv_data scan_result-ble_adv.data; uint8_t adv_data_len scan_result-ble_adv.len; int rssi scan_result-rssi; // 这里你就可以把adv_data拿去解析iBeacon字段 // 然后和广播端的UUID做匹配 process_beacon_data(scan_result-bda, rssi, adv_data, adv_data_len); } break; }解析iBeacon数据时要格外小心数组越界。BLE广播包的长度是可变的你不能假设一定包含完整iBeacon数据。我建议先判断adv_data_len是否大于等于25再做解析否则直接丢弃。否则代码一跑起来隔三差五就会遇到意外崩溃。解析的核心逻辑就是跳过前面的广播头找到厂商数据段再核对Company ID是不是0x4C00确认是iBeacon之后提取UUID、Major、Minor、TXPower。解析完如果UUID匹配就把当前这个RSSI喂给滤波模块。5.3 RSSI滤波与距离计算核心函数这一节就是整个测距逻辑的重头戏。我直接给一个实测可用的滤波测距函数组合。先定义全局变量#define RSSI_SAMPLE_NUM 11 #define DEFAULT_TXPOWER (-59) #define PATH_LOSS_EXP 2.5 static int rssi_samples[RSSI_SAMPLE_NUM]; static int rssi_index 0; static int rssi_sample_count 0;每次拿到一个新的RSSI我们把它填入环形缓冲区void rssi_add_sample(int rssi) { rssi_samples[rssi_index] rssi; rssi_index (rssi_index 1) % RSSI_SAMPLE_NUM; if (rssi_sample_count RSSI_SAMPLE_NUM) { rssi_sample_count; } }滤波时先拷贝样本到临时数组做一个简单的选择排序去掉最大的两个值和最小的两个值剩下的求平均int rssi_filtered(void) { if (rssi_sample_count 0) { return DEFAULT_TXPOWER; } int temp[RSSI_SAMPLE_NUM]; memcpy(temp, rssi_samples, sizeof(int) * rssi_sample_count); // 简单选择排序 for (int i 0; i rssi_sample_count - 1; i) { for (int j i 1; j rssi_sample_count; j) { if (temp[j] temp[i]) { int t temp[i]; temp[i] temp[j]; temp[j] t; } } } int valid_num rssi_sample_count; int start 0; if (valid_num 5) { start 2; valid_num - 4; } int sum 0; for (int i start; i start valid_num; i) { sum temp[i]; } return sum / valid_num; }距离计算公式float rssi_to_distance(int rssi) { if (rssi 0) { return 0.1; } float ratio (float)(DEFAULT_TXPOWER - rssi) / (10.0 * PATH_LOSS_EXP); return powf(10.0, ratio); }我用的DEFAULT_TXPOWER是-59PATH_LOSS_EXP是2.5。这套参数在办公室室内环境实测下来1到5米之间误差大概在1到2米5到10米误差会明显变大这是物理规律换什么滤波都救不了。想提高精度必须做环境标定尤其是针对你自己部署场景的n值标定。6. 实测标定怎么确定你自己的TXPower和n值6.1 为什么不能照搬别人的参数很多教程给出的默认参数是-59dBm和n2.0看起来挺合理但实际跑起来你会发现问题很多。不同ESP32开发板的天线设计、外壳材质、摆放朝向都会影响信号传播。我之前用某款带金属外壳的开发板同样距离下RSSI比裸板差了8个dBm算出来的距离直接翻倍。所以不要偷懒参数一定要自己标。标定的场景要和你实际使用场景尽量一致。比如你是装在墙角那就固定在那个位置标定你是手持移动就模拟手持状态标定。6.2 标定步骤实操标定过程不需要什么高级仪器一把卷尺就够了。具体步骤如下让广播端和扫描端保持1米距离不要有遮挡记录扫描端输出的RSSI平均值记为TXPower。让两端分别处于2米、3米、5米、8米、10米每个距离点采集50个RSSI样本取平均值。把这组数据用对数模型拟合求n值。不会拟合算法的用Excel就行把距离和RSSI分别取对数做线性回归斜率就是-10n。我标过的一组数据供你参考距离(m)实测RSSI均值(dBm)1-592-663-715-788-8410-88把这组数据代入公式反推n你会发现n不是固定值远距离时n会变大。这也是为什么单一n值的模型在远距离误差大。更精细的做法是分段标定比如1~3米用n2.03~7米用n2.87米以上用n3.5。代码里做成查找表就行。6.3 标定过程中要注意的问题标定时最容易被忽略的点是人站在哪。人体含水量高对2.4GHz信号吸收非常明显如果你站在两个设备中间RSSI会突然掉5到10个dBm。所以记录数据时人一定要站在设备侧面不要挡在中间。另外开发板的天线朝向也很重要。ESP32的PCB天线是有方向性的天线正面朝接收端和侧面朝接收端测出来的RSSI能差好几dBm。标定和实际使用时的设备朝向要保持一致不然你标定得再准确实际部署时朝向不同误差照样大。7. 常见问题与排查技巧实录7.1 广播端启动失败或扫描不到beacon最典型的现象是代码烧进去了另一个设备就是收不到广播。排查顺序我建议从这几个地方入手先看串口日志有没有打印ESP_GAP_BLE_SCAN_RESULT_EVT。如果扫描端一个包都收不到大概率是扫描参数没配对比如广播在37、38、39三个信道上发扫描端channel_map却没开全就会漏。另外不少例程默认关闭了蓝牙双模的共存如果你配置了Wi-Fi同时开启要和蓝牙共存的情况需要在menuconfig里打开Bluetooth controller下的ESP_BT_CONTROLLER_WIFI_COEXIST_ENABLE。再一个容易被忽略的坑是广播数据长度。ESP-IDF默认的广播数据长度配置如果是传统广播模式最多只能放31字节。iBeacon那25字节加前面3字节广播头刚好够。但是如果你还想在广播里带上设备名字节就不够了广播数据会被截断接收端解析时就会错乱。解决办法是把设备名放到扫描应答数据里而不是广播数据里。7.2 RSSI波动异常大如果你发现滤波之后RSSI还是在-50到-80之间来回跳多半不是代码问题而是环境问题。我碰到过好几个情况最后发现都是同一个原因开发板直接放在电脑旁边电脑的USB 3.0接口和显示器会发出很强的电磁干扰把蓝牙信号压得死死的。遇到这种情况把设备放到支架上离电脑远一点测试波动幅度立竿见影就降下来了。7.3 距离计算结果负数或NAN这个基本是公式参数的问题。当RSSI大于TXPower时ratio就是负数10的负次方算出来会小于1但还不至于NAN。真正NAN多半是powf传入了一个非常不合理的大数或者参数类型不对。检查一下你是不是把TXPower写成了正数。我看到不少新手会把-59写成59算出来全是负数距离。另外把接收强度大于0的情况直接判为0.1米省心很多因为RSSI大于0在半径1米内基本不可能除非你把两台设备贴到一起并且开了高增益天线。7.4 工程编译报错找不到蓝牙头文件这个问题在新手里出现的频率极高。运行idf.py build时提示找不到esp_gap_ble_api.h但在VSCode里看代码明明有引用啊。原因是编译配置里没有启用蓝牙组件。ESP-IDF里BLE协议栈是可选组件默认有些工程模板是关闭的。解决办法有两种一种是在menuconfig里打开Component config - Bluetooth - Bluetooth - [*] Bluetooth同时勾选BluedroidClassic Bluetooth可以不勾BLE足够用。另一种更干净的方式是在main/CMakeLists.txt里手动引入组件依赖然后在源码前加上#include esp_gap_ble_api.h时确保头文件目录正确。实际上只要menuconfig打开蓝牙头文件问题就自动解决了。7.5 两个ESP32同时广播和扫描资源冲突如果你打算让一块板子同时扫描和广播比如做中继节点这时要注意ESP32的BLE控制器在同一时刻只能以一种角色为主。你需要用非连接模式或者合理调度时间片否则会角色切换失败。实测中直接同时开启广播和扫描大概率会出现ESP_ERR_INVALID_STATE转换成“交替广播/扫描”的方式各跑一小段时间大部分场景都能解决。8. 部署细节与项目扩展方向到这一步你已经能跑通从beacon广播、扫描、滤波到距离估算的全链路了。接下来真正投入实用之前还有两个点值得打磨一是供电和功耗优化二是把测距结果接到你自己的业务逻辑里。供电方面如果用手机充电宝给ESP32供电RSSI值会产生微小但持续的变化因为电源纹波会干扰射频前端。想要测距稳定尽量用高质量的线性稳压电源或者用锂电池供电效果都还行。广播功率也可以从默认的9dBm调低到6dBm甚至3dBm这样近距离的RSSI分辨率更高远距离的误报也能减少。扩展方面最常见的两个方向一个是把广播端做成低功耗的标签用一块CR2032纽扣电池供电在esp_bt_sleep_enable和esp_ble_gap_update_whitelist上下功夫待机电流可以做到几十微安级别另一个是把扫描端的数据通过MQTT上报给服务器这样就能在后台看到设备历史轨迹和在线状态。这两个方向都是在现有代码基础上加模块不需要推翻重来。如果你是刚跑通第一个demo我个人建议先别急着优化功耗先把测距精度以你的部署环境为准调好再把业务逻辑接进来。因为精度问题会在你调试业务逻辑时反复出现晚调不如早调。说到底BLE beacon测距不是一个“装上就能用”的方案它是需要标定、滤波、场景适配三件事都做对才能稳定工作的技术活。但正因为这些坑都需要填你把这一整套跑通之后再去看其他无线测距方案比如UWB或者Wi-Fi RTT思路会清晰很多——毕竟核心都是先理解信号传播再做数学建模最后用工程手段降低误差。
返回列表