简介:面向Android开发者的BLE4.0通信示例工程,完整演示低功耗蓝牙从设备扫描、连接、服务发现到数据读写与通知订阅的闭环流程。代码基于Android 4.3+官方API编写,覆盖BluetoothLeScanner、BluetoothGatt、BluetoothGattCharacteristic等核心类,并针对扫描参数配置、连接状态回调、特征值读写等关键环节给出可直接落地的写法;资源描述对设备扫描、连接、服务发现、数据交互、通知订阅、断开连接六大流程均有细致拆解,适合需要快速集成蓝牙通信或入门IoT设备互联的开发者。压缩包共56个文件,包含Java源码、XML布局与配置、PNG图片资源以及可直接安装的APK包,整体仅208KB,结构紧凑,便于逐文件对照学习和二次开发。已有303人学习下载。通过这个Demo可直观理解BLE通信状态机与常见异常处理,同时结合源码梳理服务发现、通知订阅等完整流程,为可穿戴设备、传感器数据采集等场景提供可复用的参考实现。 做嵌入式这些年,陆陆续续点过不少灯、调过不少协议栈。坦白说,真正让我觉得“模块间能说话”这事变得有实用价值的,不是串口也不是CAN,而是BLE。BLE 4.0这个版本的Demo,到今天依然是很多物联网产品验证原型的第一站。这篇就围绕一个典型的“BLE4.0Demo”,把从方案选型、GATT结构设计、连接参数配置到实际调试踩坑的完整路径捋一遍。适合刚接触蓝牙低功耗的硬件工程师、嵌入式软件开发者,以及被WiFi功耗折磨得没脾气、想转BLE做产品原型的朋友。
1. 项目概述与方案选型
1.1 为什么到现在还在聊BLE 4.0
每次提到BLE,总有人第一时间想到BLE 5.0甚至5.4,觉得4.0是过时产物。但很多实际产品设计里,BLE 4.0仍然占据一席之地。原因并不复杂:BLE 4.0定义了低功耗蓝牙的基础框架,广播、扫描、连接、GATT服务、配对绑定这一套核心机制,从4.0到现在几乎没有颠覆性变化。而BLE 5.0增加的2Mbps物理层速率、Coded PHY长距离模式、扩展广播,都是在4.0的地基上新增的可选项。
从需求出发,如果一个Demo只需要传输温湿度、计步数据、开关状态这类小包数据,BLE 4.0的1Mbps有效吞吐量已经足够。而且4.0的协议栈更精简,对MCU的Flash和RAM占用更小,在很多成本敏感的芯片上(比如CC2541、nRF51822时期的老平台),跑4.0的协议栈比跑5.x更轻松。更重要的是,BLE 4.0对Android 4.3以上、iOS 5以上系统有良好的兼容性,做产品验证时不需要担心老设备的兼容性问题。
1.2 硬件平台与开发板选型
做BLE4.0Demo,硬件选型几乎决定了后面开发的顺畅程度。我列几个常用的平台,方便你对着自己的需求挑:
- nRF52832:虽然是BLE 5.0芯片,但完全兼容4.0协议,SDK成熟,文档丰富,适合做原型验证。缺点是管脚较少,外设扩展需要细心安排。
- CC2541:TI的经典BLE 4.0 SoC,51内核,跑协议栈后剩余资源有限,但胜在成本低、资料多,适合量产的简单场景。
- ESP32:乐鑫的芯片,支持BLE 4.2,编程模型简单,用ESP-IDF或Arduino都能快速上手。我遇到过不少开发者用它做Demo,再根据需求换到更专用芯片。
- AT32 / AP6256 等模块方案:直接用透传模块,发送AT指令控制,省去协议栈开发,适合硬件工程师快速出原型。
如果你是第一次做BLE项目,我的建议是:别一上来就啃协议栈源码,先用成熟的SDK和开发板把一个最简单的“从机广播、主机扫描连接、收发数据”跑通,再慢慢往里加东西。这个思路和学单片机先点灯、学Linux先跑hello world是一回事。选型时重点看三点:① 是否有现成的SDK和示例工程;② 芯片是否满足功耗目标(睡眠电流、峰值电流);③ 是否有足够的Flash(一个带OTA的BLE 4.0工程,通常需要128KB以上Flash,RAM至少8-16KB)。
2. GATT结构与连接参数设计
2.1 GATT服务结构怎么定
BLE通信绕不开GATT。简单说,GATT定义了一个“属性(Attribute)”的表格,服务(Service)是一组属性的集合,特征(Characteristic)是服务里的具体数据项,属性(Property)决定这个特征可读、可写还是可通知。设计Demo时,很多人上来就随便起UUID,结果真机调试时被自己坑了——有些手机系统对标准服务有特殊处理,自定义服务建议使用自定义的128位UUID,避免和Bluetooth SIG定义的16位UUID(比如电池服务0x180F、设备信息服务0x180A)冲突。
我常用的做法是:用一个自定义服务(比如0xFFF0)承载业务数据,把数据分成两个特征:
- Write特征(0xFFF1):手机端给设备下发命令。
- Notify特征(0xFFF2):设备主动上报数据(比如传感器采集结果、状态变化)。
为什么这样拆?因为BLE的规范中,Write和Notify分别对应下行控制与上行数据上报,拆开后,主从两端逻辑清晰,调试时也能通过属性权限快速判断是哪一步出了问题。另外,如果后续要支持“读”操作,可以再加一个Read特征,但注意一个特征不建议同时挂太多属性,否则部分手机在枚举服务时会表现异常。
2.2 连接参数规范与功耗的权衡
连接参数是BLE4.0Demo中最容易被忽略、但又最影响体验和功耗的部分。连接参数主要由四个值决定:
- Connection Interval:两个连接事件之间的时间间隔,单位是1.25ms的整数倍。范围7.5ms到4s。
- Slave Latency:从机可以跳过多少个连接事件不监听。跳过的次数越多,从机越省电,但数据延迟也会变大。
- Supervision Timeout:超过这个时间没收到连接事件,链路就断开。范围100ms到32s。
- 实际发送数据时,有效吞吐量 = 每个连接事件可发送的数据包数 / (Connection Interval × (1 + Slave Latency))。
这里有一个关键矛盾:间隔越短,数据收发越及时,但主机和从机都要频繁醒来,功耗越高。间隔越长,整体越省电,但发一个包可能要等几百毫秒。iOS系统对连接参数有一整套审核逻辑,如果App请求的Connection Interval不在系统允许的范围内(通常是15ms到30ms),系统会忽略请求并使用默认参数。如果你在做iOS配套App,务必在请求连接参数之前查一下当前系统版本对参数的限制。
我在实际项目里一般这样配:需要低延迟控制时,Connection Interval设为15ms,Slave Latency设为0;如果只是周期性上报传感器数据,则把Connection Interval设为100ms左右,Slave Latency设为4,这样从机大部分时间可以睡大觉。实测下来,前者单次通信延迟大约在10-20ms,后者能把平均电流从几十毫安降到几毫安(视具体外设而定)。
提示:连接参数的修改时机有讲究。主机可以在连接稳定后发起参数更新请求,从机也可以通过L2CAP层主动请求更新。但不要一连接上就立刻改参数,部分协议栈会报错。稳妥的做法是建立连接后等1秒左右,再发起参数更新。
3. 实操:从零搭一个可用的BLE4.0Demo
3.1 广播包设计与ADV配置
广播是BLE设备的身份名片。设计广播包时,核心问题不是“我要广播什么”,而是“我要让对端在扫描时一眼认出我,且不被系统过滤掉”。
广播包的结构是多个AD Structure的组合,每个Structure由Length、Type、Data组成。常用字段包括:
- Flags(0x01):声明设备是LE Limited Discoverable Mode还是General Discoverable Mode,通常设为0x06(同时支持BR/EDR和LE)。
- Complete Local Name(0x09):设备名称,比如“MyBLEDemo”或产品的品牌名。
- Service UUID(0x02/0x03/0x06/0x07):服务UUID,按128位还是16位、是否完整列表选择对应的Type值。
- Manufacturer Specific Data(0xFF):厂商自定义数据,可以塞一些简单状态标志,比如固件版本号、设备ID,方便App扫描时直接识别。
广播间隔建议设置在100ms到500ms之间。间隔越短,被发现越迅速,但广播期间的电流消耗也会增加。做低功耗产品时,广播间隔拉长到1s以上也常见,代价是连接体验变差,用户拿手机扫半天扫不到设备就会想卸载你的App。
这里分享一个调试技巧:用nRF Connect的“Scanner”页面扫到设备后,不要只看名字,点进去看广播包的具体字节。你会发现有些手机系统会对广播包做缓存或过滤,比如Android系统在部分版本上会缓存广播包,导致扫描不到新增的Service UUID。真机调试时,只要发现广播内容和你配置的不一致,优先怀疑缓存问题,而不是协议栈配置。
3.2 主从通信流程与数据收发
一个完整的BLE4.0Demo,通信流程可以抽象成这个链路:
设备上电 => 初始化协议栈和GATT服务 => 开始广播 => 手机(主机)扫描到设备 => 发起连接 => 连接成功后双方交换MTU大小 => 手机写入命令(Write)=> 设备处理并返回结果(Notify)=> 断开连接或进入睡眠。
MTU交换是个值得留意的细节。BLE 4.0默认MTU是23字节,其中包含3字节的L2CAP头,意味着单包用户数据只有20字节。如果你的业务字段稍长,比如一次要传50字节的日志或传感器批量数据,就必须在连接成功后主动请求MTU交换,把MTU提到247字节(常见值,部分协议栈支持更高)。这个操作一旦漏掉,你会发现明明协议栈支持长包,但数据就是发不出去,或者被系统自动分包,接收端拼接顺序错乱。
我在Demo里实现数据帧格式时,习惯用最简的“帧头+长度+命令字+数据+CRC”:
0xAA 0x55 | Length(2B) | Cmd(1B) | Data(N) | CRC16(2B)为什么加CRC?BLE底层虽然有CRC校验,但GATT层的传输是“每包确认”的,底层丢包重传并不代表上层业务封装完整。尤其当App和固件不是同一拨人开发时,一个清晰的帧格式能减少大量“数据对不上”的扯皮。CRC算法不用自造,选CRC-16/CCITT或CRC-32都行,我习惯用CRC32,虽然多两个字节,但碰撞概率更低,调试时内心更踏实。
3.3 与GPIO联动的外设控制场景
很多Demo的价值不仅在于“能收发字符串”,更在于“收到命令后真的能控制硬件”。我在做BLE4.0Demo时,最喜欢加的一个示例就是通过BLE控制LED灯或继电器的通断,这个看似简单的功能,把BLE从“数据通道”变成了“控制通道”,正好衔接到不少热词里提到的“ble主从模块gpio”场景。
实现思路很简单:在Notify/Write特征的回调里解析命令,比如收到{0xAA 0x55, 0x00 0x03, 0x01, 0x01, CRC},就把GPIO1置高;收到0x00就把GPIO1置低。但这里有个坑:BLE回调函数的执行上下文往往是在协议栈的任务/线程中,如果直接在回调里做delay或复杂运算,会把协议栈卡死,甚至触发看门狗。正确做法是:回调里只做消息记录和标志位设置,真正的GPIO操作放到主循环或单独的任务里去执行。
另外要注意GPIO的电平匹配和驱动能力。BLE开发板的GPIO通常只支持几毫安的驱动电流,直接驱动继电器线圈或功率LED很容易烧管脚。我一般会在中间加一个三极管或MOS管做开关,或者用ULN2003这类达林顿驱动芯片。这不是BLE特有的问题,但实际做Demo时,十个里总有两个人会在这上面烧掉几个IO口。
4. 调试、踩坑与常见问题速查
4.1 Linux下用bluetoothctl调试的实用姿势
做BLE调试,手机端有nRF Connect,电脑端我推荐Linux下的bluetoothctl(配合bluez协议栈使用)。很多人用它只执行scan on和connect,其实它能做的事远不止这些。
常用调试流程:
打开一个终端,运行bluetoothctl,然后按顺序执行:
power on agent on default-agent scan on # 等几秒,看到目标设备MAC地址,记下来 scan off connect <MAC地址>连接成功后,不需要退出bluetoothctl,直接敲menu gatt进入GATT子菜单,然后list-attributes查看服务列表,select-attribute <UUID>选到目标特征后,用read或write操作特征值。这些操作能帮你在不写一行代码的情况下,验证从机端GATT结构和读写属性是否正确。
这里还有个小技巧:如果你只想开BLE、不想让系统在扫描时同时处理传统的BR/EDR(即蓝牙经典模式),可以通过bluetoothctl或配置文件把BR/EDR关掉。这在调试时会减少很多干扰。具体命令为:
power off adapter set-privacy on adapter set-adv-data ...需要注意不同bluez版本命令格式有差异,实际操作时先执行help看当前版本支持的命令再操作。调试时如果发现设备扫描不到,先检查广播是否真的在发(用另一台设备或逻辑分析仪抓包),再看systemctl status bluetooth确认服务状态,不要一开始就怀疑射频参数。
4.2 跨平台调试的典型差异
BLE4.0Demo验收的时候,至少要在iOS和Android两个平台上各跑一遍,因为两边的行为差异很大。
在iOS端,主要是连接参数的审核问题。App通过CoreBluetooth发起连接后,调setDesiredConnectionInterval请求期望参数,但最终生效的参数由系统决定。如果你的设备需要低延迟,但系统给了你100ms的间隔,延迟体验会明显变差。这种情况要么调低设备的通信频率,要么检查自己请求的参数是否在系统允许的合理范围内。
在Android端,最大的坑是扫描过滤和动态权限。从Android 6.0开始,应用扫描BLE设备需要定位权限,而Android 12及以上进一步收紧了对附近设备(NEARBY_WIFI_DEVICES)的权限要求。不少人写完App在手机上跑,扫不到设备,第一反应是模块坏了,实际上是没授权或者权限策略把扫描结果拦了。另一个Android的老毛病是,扫描回调在某些手机上会被系统批量延迟触发,处理时不要把每次扫描回调都当成实时事件去刷新UI,对结果做去重和过滤会稳妥很多。
如果你在Windows上用WinForms(.NET Framework 4.7.2)做上位机,想和BLE 4.0设备通信,可用的第三方库不算太多。我目前用下来比较顺的是:
- 32feet.NET:老牌蓝牙库,对经典蓝牙(RFCOMM)支持好,但BLE支持一般,某些场景需要扩展。
- Windows.Devices.Bluetooth(WinRT API):这是系统自带的API,在.NET Framework 4.7.2中通过包引用也能调用,功能完整,但UWP/WinRT的异步API和WinForms的同步模型有冲突,需要用
AsTask()等机制桥接。 - InTheHand.Net.Bluetooth:这是32feet.NET的新版本,支持UWP API的调用方式,同时兼容.NET Framework。
我的经验是,在.NET Framework 4.7.2项目里优先用Windows.Devices.Bluetooth那套API,虽然写起来繁琐,但功能最完整,也别想着省事,老老实实处理好async/await的线程切换,否则UI会卡到怀疑人生。
4.3 常见问题速查表
把我在Debug过程中遇到的高频问题汇总成一个表格,方便你开发时随时翻查:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 手机扫描不到设备 | 广播没开启或广播包被缓存 | 确认advertising已启动,清蓝牙缓存或用新MAC测试 |
| 能扫描到但连接不上 | 设备已与其他主机连接 | 检查从机是否只有单连接能力,断开旧连接或开启多连接支持 |
| 连接后发送无响应 | MTU未交换或特征权限不对 | 确认特征属性是Write/Notify,检查MTU协商结果 |
| 数据收发乱码/截断 | 单包数据超长或封包错误 | 启用长包支持,使用自定义帧格式+长度字段 |
| iOS下延迟明显高 | 连接参数被系统覆盖 | 调整请求参数的优先级,优先适配iOS系统推荐区间 |
| Android上首次连接后收到重复数据 | 系统通知回调机制 | 在固件侧处理去重,或App端记录并过滤重复seq/包号 |
| 电流异常偏高 | 广播间隔太短或GPIO拉高功耗 | 加大广播间隔,检查外设供电和GPIO状态 |
这些问题的共同点是:初期看起来像“硬件坏了”或“协议栈崩了”,最终排查下来,80%都是配置和时序问题。遇到问题别急着换硬件,先把日志打开,看协议栈事件流转,确认连接是否建立、特征是否枚举成功、数据通路是否闭合,再动手改代码。
5. 从Demo走向产品:几个实用扩展建议
如果这个BLE4.0Demo只是为了学习,跑到“能收发数据”就可以收工了。但如果你想往产品方向走,还有几个点值得提前考虑。
第一个是安全性与配对绑定。BLE 4.0的“Just Works”配对方式虽然能加密链路,但无法防中间人攻击。如果产品涉及门锁、支付、医疗数据等场景,必须升级到MITM保护模式(Passkey Entry或Numeric Comparison),并实现长期密钥存储(LTK)。很多人在Demo阶段图省事不绑定,量产时发现安全问题再来改,成本会成倍增加。
第二个是OTA升级。做过一次你就知道,没有OTA的BLE设备就是一块砖。Demo阶段可以把固件升级接口预留出来,比如用额外的Service和Characteristic承载升级数据,或者选择支持bootloader的芯片。哪怕先不做升级逻辑,也建议在协议栈里预留好Flash分区,别等产品卖出去了才想着改。
第三个是功耗的精细优化。BLE4.0Demo跑通了之后,用万用表量一下整机电流:广播状态 vs 连接状态 vs 深睡眠状态,三者的电流差别可能是一百倍。这时候再去优化广播间隔、连接参数、外设供电,才是真正的低功耗设计。我见过不少项目,Demo阶段没关注功耗,后面发现电池续航远低于预期,最后只能换个两倍大的电池,纯属给自己挖坑。
我在实际调这些项目的时候,一个比较深的体会是:BLE本身只是个管道,真正决定产品好坏的,是管道两端的数据设计和状态管理。你把GATT服务定义清楚、连接参数配合理、状态机梳理明白,哪怕用的是老掉牙的BLE 4.0芯片,体验也不会差。而一旦这些细节没想清楚,换再新的蓝牙版本也一样会翻车。以后做新的Demo,也不妨拿这个项目当模板,先通后精,再逐步往BLE 5.x的扩展广播、Coded PHY、Mesh这些方向迁移。
本文还有配套的精品资源,点击获取