首页 / 资讯中心 / 文章详情

BLE通信从入门到实战:扫描、连接与数据收发全解析

BLE通信从入门到实战:扫描、连接与数据收发全解析 ★ FEATURED ARTICLE
1. 从一个灯控Demo说起BLE通信到底难在哪很多人第一次接触低功耗蓝牙都是被低功耗三个字吸引进来的。做IoT设备电池要撑半年甚至一年WiFi功耗太高Zigbee要网关最后绕一圈还是回到BLE。但真正上手写代码的时候你会发现事情没那么简单——扫描能扫到设备连接却总是超时连接上了数据发出去对方收不到好不容易收发通了换个手机又不行了。我最初做BLE相关项目时踩的坑比写的代码还多。印象最深的一次是做一个简单的灯控Demo手机扫描到一个BLE模块连上去发一条开灯指令模块上的LED亮起来。听起来是不是特别简单结果我在这上面耗了整整两天。第一天卡在扫描不到设备第二天卡在连接后立刻断开。后来才发现扫描不到是因为广播间隔设得太长连接断开是因为MTU协商没处理好。所以这篇内容我想把BLE基础通信这件事从头到尾讲透。扫描、连接、收发数据这三个环节每一个都有它的门道。我会用一个IoT简易Demo作为主线——手机端作为Central中心设备一个BLE模块作为Peripheral外围设备双方建立连接后互相收发数据最终实现一个可以通过手机控制设备状态的最小系统。这篇文章适合谁看如果你刚接触BLE被各种GATT、Service、Characteristic、UUID搞得头晕那这篇就是写给你的。如果你已经能跑通Demo但遇到连接不稳定、数据丢包的问题这篇里的排查思路和参数调优经验也能帮到你。我不会只给你一堆API调用而是会解释每个步骤背后的逻辑——为什么广播要这样设、为什么MTU要协商、为什么写数据要区分有无响应。整个Demo的硬件成本很低一块支持BLE的开发板市面上几十块钱的模块就够用一部手机加上你熟悉的开发环境。软件层面手机端可以用Android原生API也可以用跨平台框架设备端可以用厂商提供的SDK。核心逻辑是通用的换平台只是API名字不同而已。在开始之前先明确一个概念BLE通信的本质是基于GATT协议的数据交互。Central和Peripheral建立连接后所有的数据收发都是通过读写Characteristic来完成的。扫描是为了找到Peripheral的广播信息连接是为了建立数据通道收发数据则是读写具体的Characteristic值。把这三件事串起来就是一个完整的BLE通信链路。2. 扫描环节为什么你的设备扫不到扫到了又连不上2.1 广播包里的信息到底怎么看BLE设备在未连接状态下会周期性地发送广播包。这个广播包里包含了设备名称、MAC地址、发射功率、服务UUID等关键信息。Central端扫描的本质就是监听这些广播包并解析出自己需要的内容。很多人扫描不到设备第一个要检查的就是广播间隔。BLE协议规定广播间隔范围是20ms到10.24s但实际使用中如果你把间隔设成1s以上手机端扫描窗口没覆盖到就会漏掉。我一般建议把广播间隔设在100ms到500ms之间既能保证被快速发现功耗也不会太高。第二个要检查的是广播类型。BLE有四种广播类型可连接非定向广播、可连接定向广播、不可连接非定向广播、可扫描非定向广播。如果你希望设备能被连接必须使用可连接广播类型。有些模块默认配置的是不可连接广播这时候你扫描能看到设备但点击连接就会失败。第三个容易被忽略的是广播数据长度限制。传统BLE广播包最多31字节其中还要扣除设备名称、标志位等固定字段。如果你塞了太多自定义数据可能导致关键信息被截断。比如设备名称太长把Service UUID挤掉了Central端就匹配不到目标服务。下面这张表是我在实际项目中总结的扫描参数配置参考参数推荐值说明广播间隔100-500ms太短功耗高太长扫描慢广播类型可连接非定向最通用的类型扫描窗口与广播间隔匹配建议至少覆盖一个完整广播周期扫描模式主动扫描可获取扫描响应数据过滤条件按Service UUID避免扫到一堆无关设备2.2 扫描回调里的坑重复设备与信号强度扫描回调触发时你会收到一个扫描结果对象。这里有两个细节需要注意。第一同一个设备可能会回调多次。因为设备在持续广播每次收到广播包都会触发回调。如果你在回调里直接做UI更新列表会疯狂刷新。正确的做法是用MAC地址做去重只保留最新的一条记录。我一般会用一个Map来缓存扫描结果key是设备地址value是设备信息每次回调更新对应条目。第二信号强度RSSI是波动的。同一个设备在不同距离、不同角度下RSSI值可能相差十几dBm。如果你要根据RSSI做距离判断不要用单次值而是取一段时间内的平均值。我在做室内定位相关Demo时通常会维护一个滑动窗口取最近5次RSSI的中位数这样判断会稳定很多。还有一个实际经验扫描不要一直开着。Android从6.0开始对BLE扫描做了限制长时间扫描会消耗大量电量系统可能会限制你的扫描频率。正确的做法是扫描一段时间比如10秒找到目标设备后就停止扫描然后发起连接。如果没找到等几秒再重新扫描。2.3 连接前的最后一步解析广播数据扫描到设备后你需要从广播数据中提取关键信息。广播数据是一串字节数组按照AD Structure格式排列。每个AD Structure包含长度、类型和数据三部分。常见的类型有0x01Flags表示设备能力0x02/0x0316位Service UUID0x06/0x07128位Service UUID0x08/0x09设备名称0x0A发射功率解析的时候要注意字节序问题。BLE协议规定多字节数据采用小端序但不同平台API返回的数据格式可能不一样。有些平台已经帮你解析好了直接返回设备名称和UUID列表有些平台返回原始字节数组需要你自己解析。我建议在Demo阶段先用平台封装好的API等跑通了再研究底层解析。连接失败最常见的原因有三个一是设备已经连了其他CentralBLE Peripheral通常只能被一个Central连接二是广播已经停止有些设备在广播一段时间后就关闭广播以省电三是连接参数不匹配比如Central要求的连接间隔超出了Peripheral的支持范围。3. 连接建立从发起请求到服务发现中间发生了什么3.1 连接参数协商为什么连接总是超时发起连接时Central会发送一个连接请求其中包含连接参数。这些参数决定了后续通信的节奏连接间隔两次通信之间的时间间隔范围7.5ms到4s从机延迟Peripheral可以跳过的连接事件数监督超时超过这个时间没收到对方响应就认为连接断开连接超时通常是因为参数设置不合理。比如你把连接间隔设成7.5ms但Peripheral的处理器性能跟不上每次连接事件都来不及响应几次之后监督超时就会触发断开。我一般建议Demo阶段把连接间隔设在30ms到50ms之间这个范围兼容性好功耗也可接受。还有一个隐藏问题不同手机厂商对连接参数的处理策略不同。有些手机会强制修改你请求的连接参数以平衡功耗和性能。如果你发现连接后通信延迟很大可以尝试在连接成功后主动请求更新连接参数。3.2 服务发现GATT层级结构拆解连接建立后第一件事是服务发现。BLE的GATT结构是树形的Profile规范一个完整的应用场景定义比如心率监测Service服务一组相关功能的集合比如心率服务Characteristic特征具体的数据点比如心率测量值Descriptor描述符特征的附加信息比如单位、格式每个Service和Characteristic都有一个UUID。标准UUID是16位的比如心率服务是0x180D。自定义UUID是128位的通常用工具生成。在Demo中我建议先用自定义UUID避免和标准服务冲突。服务发现的过程就是遍历Peripheral上的所有Service和Characteristic找到你需要的那个。这个过程可能比较耗时尤其是设备上服务很多的时候。我一般会在连接成功后异步做服务发现不要阻塞主线程。3.3 连接状态维护断开重连与自动回连BLE连接不是一劳永逸的。信号干扰、距离过远、设备休眠都可能导致断开。一个健壮的Demo应该处理断开事件并尝试自动重连。自动重连的策略有很多种。简单的是断开后立即重连但如果设备已经关机这样会无限重试。我通常用指数退避策略第一次断开后等1秒重连失败等2秒再失败等4秒最多重试5次。这样既能快速恢复又不会在设备不可用时疯狂消耗资源。还有一个细节Android的自动重连。Android提供autoConnect参数设为true时系统会在设备重新广播时自动连接。但这个功能在不同版本上表现不一致有些版本会一直保持连接尝试导致功耗偏高。我的建议是Demo阶段先手动重连等逻辑稳定了再考虑用系统自动重连。4. 数据收发写特征、读特征、通知订阅的实操细节4.1 写数据的两种方式有响应与无响应向Characteristic写数据有两种方式有响应写Central发送数据后等待Peripheral返回确认。可靠但速度慢。无响应写Central发送数据后不等确认直接发下一条。速度快但可能丢包。选择哪种方式取决于你的数据特性。如果是控制指令比如开关灯必须用有响应写确保指令到达。如果是传感器数据流可以用无响应写偶尔丢一包不影响整体。写数据时还要注意数据长度限制。默认情况下BLE的ATT MTU是23字节扣除3字节的ATT头实际可写数据是20字节。如果你要发更长的数据需要先协商MTU。4.2 MTU协商突破20字节限制MTU协商是BLE开发中容易被忽略但很重要的环节。默认MTU是23意味着你一次最多发20字节。对于大多数控制指令够用但如果你要传JSON、图片片段或者长字符串就不够了。协商MTU的流程是Central发起MTU交换请求Peripheral返回它支持的MTU值双方取较小值作为最终MTU。Android上可以调用requestMtu(247)来请求更大的MTU。但要注意不是所有设备都支持大MTU有些模块最大只支持128。我实测下来MTU设为247是一个比较稳妥的选择。这个值在大多数Android手机上都能协商成功实际可写数据达到244字节足够传一些简单的结构化数据。协商成功后后续的写操作就可以用这个长度了。4.3 通知与指示Peripheral主动上报数据前面说的都是Central主动读写但很多场景下需要Peripheral主动上报数据比如传感器定时上报温度。这时候就要用到通知或指示。通知Peripheral发送数据不需要Central确认。速度快可能丢包。指示Peripheral发送数据等待Central确认。可靠但速度慢。使用通知的步骤是Central先向Characteristic的客户端特征配置描述符写入0x0001开启通知然后Peripheral就会在数据变化时主动推送。Central收到通知后触发回调在回调里处理数据。这里有个常见坑忘记开启通知。很多人连接后直接等数据结果一直收不到。必须先在Central端订阅对应的CharacteristicPeripheral才会开始推送。4.4 数据收发的完整代码逻辑下面用伪代码展示一个完整的收发流程平台无关重点看逻辑// 连接成功后 onConnected(device): discoverServices() // 发现服务 onServicesDiscovered(): targetService findService(SERVICE_UUID) writeChar targetService.findCharacteristic(WRITE_UUID) notifyChar targetService.findCharacteristic(NOTIFY_UUID) // 开启通知 setCharacteristicNotification(notifyChar, true) writeDescriptor(notifyChar.CCCD, ENABLE_NOTIFICATION) // 协商MTU requestMtu(247) // 发送数据 function sendData(data): writeCharacteristic(writeChar, data, WRITE_TYPE_DEFAULT) // 接收数据 onCharacteristicChanged(characteristic, data): if characteristic.uuid NOTIFY_UUID: processData(data)这个流程里顺序很重要。必须先发现服务再获取Characteristic再开启通知。如果顺序错了比如在服务发现完成前就写数据会直接失败。5. Demo实战从零搭建一个BLE灯控系统5.1 设备端配置广播、服务与特征定义设备端我用的是一块常见的BLE模块通过厂商提供的工具配置GATT服务。配置步骤如下第一步定义服务UUID。我用的是自定义128位UUID避免和标准服务冲突。服务UUID设为0000FFF0-0000-1000-8000-00805F9B34FB。第二步添加两个Characteristic。一个用于接收控制指令UUID为0000FFF1-...属性设为可写另一个用于上报设备状态UUID为0000FFF2-...属性设为通知。第三步配置广播。设备名称设为BLE_LED_DEMO广播间隔200ms广播类型设为可连接非定向。广播数据里包含服务UUID方便Central过滤。第四步烧录固件并上电。设备开始广播后用手机上的BLE调试工具应该能扫到。5.2 手机端扫描与连接代码实现与参数设置手机端我用Android原生API来写核心代码如下// 扫描 BluetoothLeScanner scanner bluetoothAdapter.getBluetoothLeScanner(); ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build(); ScanFilter filter new ScanFilter.Builder() .setServiceUuid(new ParcelUuid(SERVICE_UUID)) .build(); scanner.startScan(Arrays.asList(filter), settings, scanCallback); // 连接 BluetoothGatt gatt device.connectGatt(context, false, gattCallback);扫描回调里我做了去重处理只保留最新的设备信息。连接时autoConnect设为false手动控制重连逻辑。连接成功后在onConnectionStateChange回调里调用gatt.discoverServices()。服务发现完成后在onServicesDiscovered回调里获取Characteristic并开启通知。5.3 收发联调指令下发与状态回传联调阶段我定义了一个简单的应用层协议开灯指令0x01关灯指令0x02状态上报0x11表示灯亮0x12表示灯灭手机端发送0x01设备收到后点亮LED然后通过通知Characteristic上报0x11。手机端收到通知后更新UI。联调时遇到一个问题发送指令后设备没反应。排查发现是写Characteristic时用了无响应写而设备端只处理有响应写。改成WRITE_TYPE_DEFAULT后正常。另一个问题是通知收不到。检查后发现是CCCD描述符写入的值不对。开启通知应该写0x0001我误写成了0x0002那是开启指示的值。改正后通知正常。5.4 实测中的意外情况与解决实测中遇到最诡异的问题是连接后几秒钟自动断开。日志显示监督超时。排查后发现是连接间隔设得太短7.5ms设备端处理不过来。改成30ms后稳定了。还有一个问题是不同手机表现不一致。同一套代码在A手机上正常在B手机上扫描不到设备。后来发现B手机对后台扫描有限制需要申请定位权限并开启定位服务。这是Android系统的限制不是代码问题。最后一个经验调试时打开日志。Android的BLE API提供了gatt.getConnectionState()、gatt.getServices()等方法配合日志输出可以快速定位问题。我习惯在关键回调里都加上日志出问题时看日志比猜快得多。6. 踩坑复盘那些文档里不会写的BLE经验6.1 权限与定位服务的隐藏关联Android 6.0以后BLE扫描需要定位权限。Android 10以后还需要开启定位服务GPS开关。这个设计让很多人困惑BLE和定位有什么关系原因是BLE扫描可以用于室内定位系统为了保护隐私把BLE扫描归类为定位相关操作。实际开发中你需要在Manifest里声明BLUETOOTH、BLUETOOTH_ADMIN、ACCESS_FINE_LOCATION权限运行时动态申请定位权限检查定位服务是否开启未开启时引导用户去设置如果漏了任何一步扫描都会返回空结果而且不会有明显报错。这是新手最容易踩的坑。6.2 连接间隔与功耗的平衡术连接间隔直接决定功耗。间隔越短通信越快功耗越高。我做过一个粗略测试连接间隔7.5ms时设备平均电流约5mA间隔100ms时平均电流降到0.5mA左右。对于电池供电的设备我一般建议需要快速响应的场景30-50ms普通控制场景100-200ms低频上报场景500ms-1s但要注意连接间隔不是你想设多少就设多少。Central和Peripheral会协商最终取双方都支持的值。有些手机最低只支持15ms你请求7.5ms也会被改成15ms。6.3 数据丢包与重传策略BLE虽然比经典蓝牙稳定但在2.4GHz频段拥挤的环境下丢包仍然会发生。尤其是使用无响应写或通知时丢包不会报错数据直接丢失。我的应对策略是在应用层加序号和确认。每条数据带一个递增的序号接收方收到后检查序号是否连续。如果发现跳号说明中间有丢包可以请求重传。对于关键指令直接用有响应写或指示牺牲速度换可靠性。还有一个技巧避免在连接刚建立时发大量数据。连接初期连接参数可能还没稳定这时候发数据容易丢。我一般会等连接后100-200ms再开始通信。6.4 多设备连接与资源释放一个Central可以同时连接多个Peripheral但数量有限。Android上一般最多连4-7个设备具体取决于芯片。连接数越多每个连接的稳定性越差。资源释放也很重要。不用Gatt时一定要调用gatt.close()否则会泄漏连接资源。我遇到过忘记close导致后续连接失败的情况排查了很久才发现是资源没释放。另外BluetoothGatt的所有操作都应该串行执行。比如你调用了discoverServices()要等回调完成后再做下一步操作。并发调用多个GATT操作会导致不可预期的结果。7. 从Demo到产品下一步可以扩展的方向跑通这个Demo后你已经有了一套可用的BLE通信框架。接下来可以根据实际需求扩展。如果要做OTA固件升级需要增加一个用于传输固件数据的Characteristic把固件分包发送每包带校验。这个过程中MTU协商和重传策略尤其重要。如果要做多设备组网可以考虑让一个Central连接多个Peripheral或者用BLE Mesh方案。Mesh适合节点多的场景但协议复杂度高Demo阶段不建议直接上。如果要做低功耗优化可以从连接参数、广播间隔、数据上报频率三个维度入手。我一般会先用功耗分析仪测出各状态的电流再针对性调整。还有一个方向是跨平台。Android和iOS的BLE API差异较大如果要做双端建议用跨平台框架封装一层把平台相关的代码隔离出来。这样核心逻辑只写一遍两端都能用。最后分享一个我在实际项目中总结的原则BLE通信的稳定性三分靠代码七分靠参数。同样的代码参数调好了稳如磐石参数不对各种奇怪问题。所以遇到问题时先检查广播间隔、连接间隔、MTU、超时时间这几个参数往往比翻代码更快找到原因。
阅读完成 · 觉得有帮助?
咨询建站