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

资讯详情

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

ESP32+蓝牙Beacon RSSI测距实战:从环境搭建到标定滤波

ESP32+蓝牙Beacon RSSI测距实战:从环境搭建到标定滤波 做室内定位或者接近检测的时候我一开始也纠结过方案。UWB精度高但成本直接把项目预算打穿Wi-Fi RTT又依赖路由器和终端硬件支持挑来挑去最后老老实实用回了蓝牙beacon。一个ESP32开发板十几块搭配手机或另一个ESP32做接收端就能覆盖几十个点位偏偏这还是对开发者最友好的一条路。所以这一讲我把蓝牙beacon测距单独拎出来讲透从ESP-IDFvscode环境搭建开始到手写beacon广播和扫描接收代码再到用RSSI信号强度反推距离最后把标定和滤波这些“书上不写但工程必须做”的坑也一并填上。不管你是刚把LED点灯玩明白还是已经在用ESP-IDF做产品原型这讲内容都能直接抄作业。为什么是beacon而不是其他方案说到底是个性价比问题。ESP32这东西特别的地方在于BLE和经典蓝牙都支持既能当beacon发射端也能当扫描接收端这意味着你用一片芯片就能模拟整套测距链路调试成本低到离谱。唯一要认清的现实是RSSI测距不是激光测距它做到1到3米的精度已经算优秀适合做区域判定、存在感知、设备接近提醒不适合拿来做厘米级定位。认清这一点后面很多调参就不会钻牛角尖。1. 方案选型为什么ESP32蓝牙beacon做测距是个务实选择1.1 几种无线测距方案的血泪对比我不是说所有场景都该无脑选beacon先看一张实际对比表你能更清楚知道自己在为什么买单。方案成本精度功耗实现复杂度典型场景UWB高10~30cm中高无感门禁、车钥匙蓝牙5.1 AoA/AoD高亚米级中高商场导航、资产定位蓝牙beacon RSSI极低1~3m低低区域感知、设备接近提醒Wi-Fi RTT中1~2m高中室内位置服务从开发角度讲UWB需要专用芯片协议栈开发成本不低蓝牙5.1测向需要天线阵列硬件设计也不友好。而beacon方案本质就是“接收信号强度指示”只要BLE协议栈能上报RSSI剩下全是数学和调参的活。ESP32上跑ESP-IDFBLE广播和扫描API都是现成的半天就能把链路调通这个启动速度是其他方案给不了的。1.2 Beacon测距链路里ESP32能扮演什么角色一条完整的RSSI测距链路只有两个角色广播端和接收端。广播端周期性发送一个包含设备标识和校准发射功率的beacon包接收端在扫描到广播包时能拿到RSSI信号强度再套用路径损耗模型估算距离。有意思的是ESP32两个角色都能干。你可以让一个ESP32作为beacon放房间角落手机或另一个ESP32扫描也可以让两个ESP32互相扫描完全脱离手机做设备间测距。这就给开发者省去了外部测试工具我在调试时就常用“两块开发板互测”的方式验证代码比老跑手机优雅多了。1.3 关键概念iBeacon数据包长什么样谈beacon绕不开iBeacon格式它是苹果定义的BLE广播数据规范也是目前最主流的beacon协议。一个完整的iBeacon包在BLE广播帧里长这样02 01 1A 1A FF 4C 00 02 15 UUID 16字节 Major 2字节 Minor 2字节 Measured Power 1字节02 01 1A表示广播类型为“LE General Discoverable Mode”长度2字节。1A FF表示厂商自定义数据长度26字节。4C 00苹果公司标识符。02 15iBeacon类型标识。UUID标识beacon所属的“区域”可以理解为beacon的命名空间。Major/Minor进一步细分子组和具体编号。Measured Power距离1米处测到的RSSI参考值通常用补码表示设备发射时写死。后面测距时我们主要靠的就是最后的这个Measured Power和扫描到的RSSI差值所以广播端这个参数不能随便填一定要用实测值标定。2. 开发环境搭建ESP-IDFvscode安装与工程创建的隐藏坑2.1 从vscode安装到ESP-IDF扩展初始化先说环境。vscode官网下载安装包然后打开扩展面板搜索“Espressif IDF”安装由乐鑫官方提供的扩展。安装完之后扩展会让你选择ESP-IDF的安装方式推荐选“Express”在线安装它会自动下载完整的工具链。如果你网络条件不好也可以选“Existing ESP-IDF installation”指向本地已经安装好的ESP-IDF。很多人卡在“the path for esp-idf is not valid: /tools/idf.py not found.”这个报错上。这个问题的原因很简单ESP-IDF扩展配置里面的IDF_PATH指向的目录里根本没有idf.py脚本。常见情况是在Windows上手动设置了IDF_PATH但路径多了一级或少了一级。正确的IDF_PATH应该是找到export.bat和idf.py所在的根目录比如C:\Espressif\frameworks\esp-idf-v5.2.2而不是再往上一级或往里一级。另一个高频坑是本地离线安装ESP-IDF时就算你在扩展配置里选了非C盘的安装路径工具链还是会被安装到C盘。这个问题的根子在环境变量IDF_TOOLS_PATH。扩展虽然让你设置了框架路径但工具链的默认安装路径如果还是C盘那一定是环境变量没生效。务必要在系统环境变量里新建IDF_TOOLS_PATH指向你期望的工具目录比如D:\Espressif\tools同时把ESP-IDF扩展的“IDF Tools Path”配置成同一个目录。设置完成后重启vscode再执行一次“ESP-IDF: Configure ESP-IDF Extension”让配置生效。2.2 创建工程并完成首次烧录打开命令面板CtrlShiftP输入“ESP-IDF: Create Project”选择模板“hello_world”芯片目标选择ESP32。工程生成后在底部状态栏点齿轮图标选择串口端口Windows下一般是COM3或COM4然后按F1执行“ESP-IDF: Build, Flash, Monitor”。如果一切正常你会看到串口输出“Hello world!”。这里要补充一个细节主菜单“ESP-IDF: Build, Flash, Monitor”其实是三步动作的串联先编译再烧录最后打开串口监视器。如果你改了代码只想编译用“Build”就行烧录时可以单独用“Flash”。我用这扩展两年最稳定的是这三个动作分开做连续一键反而偶尔会丢串口。2.3 配置蓝牙前必须处理的内存分配问题新建的工程默认没有启用蓝牙控制器。ESP32的蓝牙和Wi-Fi共用一套射频硬件编译时你需要打开蓝牙开关。在menuconfig配置界面里路径是“Component config → Bluetooth → Bluedroid Options”或者“Controller Options”把“Enable Bluetooth”打开。一个比较隐蔽的坑是BLE初始化只使用了蓝牙控制器的一部分内存如果你没有在代码里调用esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)释放掉经典蓝牙占用的内存就会导致后面启动BLE扫描时BLE内存不足直接初始化失败。这个函数必须在蓝牙控制器初始化之前调用。我见过很多新手复制官方示例却因为漏了这行代码反复重启复位所以这里特意先提一嘴后面写代码时我会再放一遍完整初始化流程。3. Beacon广播与扫描的核心API拆解3.1 广播端构造并发送iBeacon广播包先看广播端代码。以ESP-IDF 5.x的API为例初始化部分包括释放经典蓝牙内存、初始化蓝牙控制器为BLE模式、注册蓝牙GAP回调、配置广播数据最后启动广播。#include stdio.h #include string.h #include esp_bt.h #include esp_bt_main.h #include esp_gap_ble_api.h #include esp_bt_device.h // 这里写自己的UUID16字节 static const uint8_t ibeacon_uuid[16] { 0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25 }; static const int major 100; static const int minor 1; static const int8_t measured_power -59; static esp_ble_adv_params_t adv_params { .adv_int_min 0x100, // 广播间隔最小值 .adv_int_max 0x100, // 广播间隔最大值 .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, }; static esp_err_t build_ibeacon_data(uint8_t *buf, size_t buflen) { if (buflen 30) return ESP_ERR_INVALID_ARG; uint8_t pos 0; buf[pos] 0x02; // 广播数据段长度 buf[pos] 0x01; // 类型Flags buf[pos] 0x1A; // LE General Discoverable BR/EDR不支持 buf[pos] 0x1A; // 厂商数据长度 buf[pos] 0xFF; // 类型Manufacturer Specific buf[pos] 0x4C; // Company ID 低字节Apple buf[pos] 0x00; // Company ID 高字节 buf[pos] 0x02; // iBeacon指示 buf[pos] 0x15; // 剩余长度 21字节 memcpy(buf pos, ibeacon_uuid, 16); pos 16; buf[pos] (major 8) 0xFF; buf[pos] major 0xFF; buf[pos] (minor 8) 0xFF; buf[pos] minor 0xFF; buf[pos] (uint8_t)measured_power; return ESP_OK; } static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { if (event ESP_GAP_BLE_SET_ADV_DATA_COMPLETE_EVT) { esp_ble_gap_start_advertising(adv_params); printf(advertising started\n); } } void app_main(void) { ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); esp_bt_controller_init(bt_cfg); esp_bt_controller_enable(ESP_BT_MODE_BLE); esp_bluedroid_init(); esp_bluedroid_enable(); esp_ble_gap_register_callback(gap_event_handler); uint8_t adv_data[30]; ESP_ERROR_CHECK(build_ibeacon_data(adv_data, sizeof(adv_data))); esp_ble_gap_config_adv_data(adv_data, sizeof(adv_data)); }代码里有几个点需要解释。广播间隔adv_int_min和adv_int_max单位是0.625ms0x100也就是256 × 0.625ms 160ms。这个间隔决定了beacon的广播频率间隔越短接收端刷新距离值越快但功耗也越高。测试场景用160ms正式产品一般用500ms到1s更省电。build_ibeacon_data里那段字节拼接看起来繁琐但这就是BLE广播包的底层逻辑一切数据都是“长度类型值”的TLV结构。你最好自己手写一遍而不是直接抄库至少能理解为什么广播包最大31字节而iBeacon正好吃掉30字节。3.2 扫描端配置扫描参数并解析beacon数据扫描端代码稍微复杂一点但逻辑很清晰初始化蓝牙和协议栈注册GAP回调设置扫描参数开始扫描。在回调中事件类型是ESP_GAP_BLE_SCAN_RESULT_EVT再根据search_evt区分是不是扫描到了广播包。#include stdio.h #include esp_bt.h #include esp_bt_main.h #include esp_gap_ble_api.h 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 0x100, .scan_window 0x80, .scan_duplicate BLE_SCAN_DUPLICATE_DISABLE }; static void parse_beacon(uint8_t *adv_data, uint8_t len, int8_t rssi) { // 这里简化处理直接遍历广播数据找厂商数据 uint8_t pos 0; while (pos len) { uint8_t field_len adv_data[pos]; if (field_len 0) break; uint8_t field_type adv_data[pos 1]; if (field_type 0xFF field_len 26) { if (adv_data[pos 2] 0x4C adv_data[pos 3] 0x00) { printf(found iBeacon, rssi%d dBm\n, rssi); // 这里可以提取UUID和Measured Power } } pos field_len 1; } } static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { if (event ESP_GAP_BLE_SCAN_RESULT_EVT) { esp_ble_gap_cb_param_t *p param; if (p-scan_rst.search_evt ESP_GAP_SEARCH_INQ_RES_EVT) { parse_beacon(p-scan_rst.ble_adv, p-scan_rst.adv_data_len, p-scan_rst.rssi); } } } void app_main(void) { ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); esp_bt_controller_init(bt_cfg); esp_bt_controller_enable(ESP_BT_MODE_BLE); esp_bluedroid_init(); esp_bluedroid_enable(); esp_ble_gap_register_callback(gap_event_handler); esp_ble_gap_set_scan_params(scan_params); esp_ble_gap_start_scanning(0); }代码中的scan_interval和scan_window是扫描功耗的关键。scan_interval是扫描周期scan_window是每个周期里真正打开射频接收的时间单位也是0.625ms。上面配置的0x100约160ms一轮0x80约80ms也就是一轮里有近一半时间在听广播。这个配置在测试时很稳但要是跑电池供电的传感器建议把scan_interval拉大到1sscan_window降到20ms左右扫描功耗可以明显下降。3.3 广播信道和扫描窗口的配合BLE广播会同时在37/38/39三个信道上发送扫描端默认也会扫这三个信道所以理论上不会漏。但如果你发现扫描端一直收不到广播不要急着怀疑硬件先看广播间隔和扫描窗口的配置。如果广播间隔是160ms扫描窗口只有10ms那每个广播周期内接收端只有6%的时间在听有很大概率漏几包。调试阶段应把scan_window调大到至少30ms甚至用持续扫描模式等调通了再优化功耗。4. RSSI测距模型从dBm到米的关键换算4.1 路径损耗公式是怎么来的拿到RSSI之后测距就是一个数学问题。最常用的模型是“对数距离路径损耗模型”公式如下d 10 ^ ((P0 - RSSI) / (10 * n))d估算距离单位米。P0距离发射端1米处接收到的RSSI参考值也叫Measured Power单位dBm。RSSI当前扫描到的信号强度单位dBm。n路径损耗指数是个环境相关参数。空旷室外接近2室内有墙体遮挡时在2.5~4之间。这个公式的本质是信号在自由空间里传播距离每增加10倍信号强度衰减约20dB但室内存在反射、衍射衰减会更剧烈所以用n来补偿。4.2 一个算例让你心里有数假设标定得到P0 -59 dBmn 2.5当前RSSI是-70 dBm那么d 10 ^ ((-59 - (-70)) / (10 * 2.5)) 10 ^ (11 / 25) 10 ^ 0.44 ≈ 2.75米如果RSSI变成-80 dBm距离就是d 10 ^ ((-59 - (-80)) / 25) 10 ^ 0.84 ≈ 6.92米同一个环境下RSSI从-70掉到-80距离翻了一倍多。这也解释了为什么RSSI测距在远距离时误差很大——信号强度稍微抖动距离估算就会大幅变化。4.3 为什么别直接信任广播包里的Measured Power很多beacon设备广播里的Measured Power是拿苹果参考机型在标准环境下测的但你的接收设备不一定是手机有可能是另一块ESP32天线方向、外壳、电路布局都不一样。更别说ESP32与手机灵敏度本身就存在5~10dBm的差异这在距离估算上可能是2到3倍误差。所以工程上一定要用自己手头的发送端和接收端组合做实测标定拿到专属的P0和n再套公式。5. 实测标定与滤波让测距结果稳下来的经验5.1 标定P0和n的可落地步骤标定时不需要复杂仪器一个卷尺、两块开发板就够。具体做法把广播端固定好高度约1米远离墙壁和金属物体。扫描端从0.5米开始每隔0.5米或1米取一个点每个点采集至少50个RSSI样本取平均。记录距离和平均RSSI的对应表。利用最小二乘法或Excel拟合得到P0和n。拟合的方式很简单把公式两边取对数得到RSSI P0 - 10 * n * log10(d)这就是一条以log10(d)为自变量、RSSI为因变量的直线斜率是-10n截距是P0。在Excel里插入散点图横轴改成log10(距离)再加趋势线就能直接读出斜率和截距。5.2 滑动平均滤波最简单也最有效的招单次RSSI的波动经常像个神经病-65dBm下一秒变-72dBm。我在工程里最常用的就是滑动平均只保存最近N个RSSI值取平均后作为有效值。N取5到10比较合适太小滤波效果不好太大响应变慢。#define RSSI_BUFFER_SIZE 8 static int32_t rssi_buffer[RSSI_BUFFER_SIZE]; static int rssi_index 0; static int rssi_count 0; int32_t rssi_filtered(int32_t raw_rssi) { rssi_buffer[rssi_index] raw_rssi; rssi_index (rssi_index 1) % RSSI_BUFFER_SIZE; if (rssi_count RSSI_BUFFER_SIZE) rssi_count; int64_t sum 0; for (int i 0; i rssi_count; i) { sum rssi_buffer[i]; } return (int32_t)(sum / rssi_count); }这个滤波器最大的好处是不占额外内存、响应可控、实时性好。很多正式产品里也用类似思路够用了。5.3 实测误差表现和物理环境的“不讲道理”我做过一组室内实测5米距离纯滑动平均后的估算距离多数在4到6.5米之间波动1米距离误差基本能控制在0.5米以内。但一旦有人从中间走过、或者门被打开误差能瞬间翻倍。这是因为人体含水量高对2.4GHz信号衰减非常严重。所以如果你做的是人员接近检测建议把目标区域划分成“近、中、远”三档而不是纠结具体米数。距离值连续变化反而会让业务逻辑难以处理。6. 踩坑记录开发过程中最折磨人的几个问题6.1 蓝牙初始化失败罪魁祸首是Wi-Fi共存和内存分配我在用ESP32同时跑Wi-Fi和BLE时踩过一个很经典的坑Wi-Fi正常连接BLE广播一启动就重启日志提示ESP_ERR_NO_MEM。原因前面提过ESP32的蓝牙控制器和Wi-Fi控制器共享内存池默认配置下内存预留给BR/EDR了BLE能用的堆空间不够。解决办法就是在初始化蓝牙控制器前释放经典蓝牙资源esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);这行代码非常重要。如果你用官方BLE示例却完全跑不通先检查是不是少了它。6.2 vscode离线安装ESP-IDF工具链的路径问题这个坑我见得太多了。vscode离线安装ESP-IDF扩展后即使界面里选了D盘工具链还是悄悄装到C盘。根因是扩展的路径配置和系统环境变量IDF_TOOLS_PATH不一致。我的建议是安装前就先把系统环境变量设好新建变量IDF_TOOLS_PATH值设为D:\Espressif\tools。在vscode里按F1输入“ESP-IDF: Configure ESP-IDF Extension”把“IDF Tools Path”改成同一个目录。重启vscode再确认一次配置。另外IDF_PATH要指向ESP-IDF框架源码目录比如D:\Espressif\frameworks\esp-idf-v5.2.2这个目录下必须有idf.py。如果扩展里反复报“the path for esp-idf is not valid”大概率是指到了子目录或者多套了一层目录。6.3 扫描回调里做耗时操作导致丢包扫描回调是运行在蓝牙协议栈线程里的如果你在其中做大量打印、写Flash、解析复杂JSON轻则丢包重则蓝牙任务阻塞复位。我习惯的做法是回调里只做拷贝——把RSSI和beacon信息放到队列或全局环形缓冲区然后用独立任务去处理。打印调试可以临时做但正式逻辑一定要异步化。6.4 防踩坑清单问题典型原因解决办法蓝牙初始化失败/反复重启未释放经典蓝牙内存初始化前调用esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)vscode报idf.py路径无效IDF_PATH指错目录确保IDF_PATH指向含idf.py的根目录工具链强制装C盘环境变量IDF_TOOLS_PATH未生效修改系统环境变量并重启vscode扫描不到beacon扫描窗口过小或广播间隔过长调试时把scan_window调大距离值跳变严重单次RSSI噪声大采用滑动平均或卡尔曼滤波发射端与接收端灵敏度差异大使用了苹果参考Measured Power用自己的板子实测标定P0和n6.5 一个提高调试效率的小设计调试测距时我把广播端和扫描端程序烧进两块板子用USB线同时带两个串口监视器。广播端打印收到的扫描命令帧这里不展开但思路是把两块板子做成互发beacon扫描端打印RSSI和距离。这样不需要手机和第三方app调参效率高很多。如果你手里只有一块板子也可以用手机上的BLE扫描工具把RSSI显示出来但ESP32接收灵敏度和手机不完全一样标定数据还是建议用最终硬件组合来测。最后再分享一个小技巧我后来做项目时喜欢在接收端保存最近30秒的RSSI序列用来做“动态阈值判断”——比如连续5次RSSI大于-60dBm才判定设备进入近距离范围这比单次距离计算可靠得多。RSSI测距不会像UWB那样给你一个漂亮精确的坐标但只要把物理模型摸清楚、把标定做扎实它完全能撑起一整个室内感知项目。这一讲到这里链路已经通了下一步你可以尝试把多个beacon的RSSI结合起来做三边定位那个就等“联网篇”后续再聊。
返回列表