OTA项目回顾(三)-BLE
BLE技术揭秘
BLE(低功耗蓝牙)是蓝牙 4.0 引入的轻量级无线协议,专为低功耗 IoT 场景设计。它和经典蓝牙共用 2.4GHz 频段但协议栈完全不同,核心分为两层:GAP 负责广播和连接建立(设备怎么被发现),GATT 负责数据传输(连上后怎么读写)。数据以 Service → Characteristic → Descriptor 的树形结构组织,手机作为 Client 发读写请求,设备作为 Server 响应或主动 Notify 推送。默认每包只能传 20 字节可用数据,通过 MTU 协商可以扩展到更大。
参考:谷雨BLE
1.蓝牙简介
蓝牙跑在 2.4GHz 免费频段,是一种短距离无线通信技术,现在手机、IoT 设备、医疗健康、智能家居到处都是。
蓝牙 4.0 以后分两种模式:
- 单模(BLE):只支持低功耗蓝牙,功耗极低、连接快、传输距离远,但不支持音频,数据速率也有限,适合小数据量的场景。
- 双模(BR/EDR + BLE):同时兼容经典蓝牙和低功耗蓝牙。经典蓝牙适合音频和大数据量传输(比如 A2DP 听歌、SPP 串口透传),BLE 适合低功耗数据交互。
经典蓝牙在音频和传统数传领域曾经是主力,但苹果对经典蓝牙的 SPP 等数据接口有 MFI 认证要求,加上功耗偏高,在移动互联网时代逐渐被边缘化。BLE 从 iPhone 4S 和 Android 4.3 开始原生支持,没有认证门槛,功耗又极低,所以现在移动互联设备大量采用。不过它不能传音频,速率也受限。双模则是两头通吃——既能传音频、也能低功耗传数据,兼容性最好,功耗不太敏感的场景下是比较理想的选择。
2.BLE特点
BLE 瞄准的市场主要是移动智能终端、智能家居和 IoT 互联设备,几个核心特点:
低功耗:一颗纽扣电池跑几个月甚至几年,因为 BLE 不通信时深度休眠,只在需要时快速唤醒、传完即睡。
快速连接:毫秒级完成连接建立,经典蓝牙某些场景下甚至要几十秒甚至数分钟。
远距离:BLE 5.0 后可达数百米,而经典蓝牙通常就 10 米左右。
除了物理层的优势,BLE 另一个关键能力在协议层——标准 Profile 和自定义 Profile。
蓝牙联盟为 BLE 定义了一系列标准 Profile(可以理解为"数据规范"):只要大家都遵守同一个 Profile,不同厂商的设备就能互通。比如无线键鼠用的 HID,不管是连 Windows、Mac 还是 iOS,都是即插即用——这就是标准化的力量。常见的标准 Profile 还有 Battery Service(电池电量)、HRP(心率采集)等。
而自定义 Profile 才是 BLE 真正被广泛应用的关键。手机普及之后,开发者可以为自己的设备定义任意格式的数据交互规范——比如用一个字节 01 表示开灯、00 表示关灯,手机 APP 写过去就实现了智能灯控。能收发任意格式数据、能和任意自定义设备连通,这让 BLE 的应用场景从键鼠心率带一路扩展到智能插座、车载诊断、工业传感器等几乎所有小数据场景。
3.BLE工作流程
3.1角色
BLE设备主要分为两种角色:主机(Master)和从机(Peripheral),只有主机和从机建立链接才可以发送数据
- 主机,主机可以发起对从机的扫描连接。例如手机,通常作为BLE的主机设备
- 从机,从机只能广播并等待主机的连接。例如智能手环,是作为BLE的从机设备
蓝牙协议栈没有限制设备的角色范围,同一个BLE设备,可以作为主机,也可以作为从机,我们称之为主从一体,主从一体的好处是,每个BLE设备都是对等的,可以发起连接,也可以被别人连接,更加实用。
3.2 广播
在 BLE 中,从机设备如果希望被主机发现,通常需要先进入广播状态。设备进入广播状态后,会按照设定的时间间隔周期性地发送广播数据包。这个周期性的发送过程称为广播事件(Advertising Event),两个广播事件之间的基础时间间隔称为广播间隔(Advertising Interval)。
这里描述的是传统广播(Legacy Advertising)流程,也是 ESP32 BLE 开发中最常见的广播方式之一。
在每个广播事件中,广播包会分别在37,38和39三个信道上依次广播,如下图所示。
广播时间间隔的范围是从20ms到10.24s,广播间隔影响建立连接的时间。广播间隔越大,连接的时间越长。
另外BLE链路层会在两个广播事件之间添加一个010ms的随机延时,保证多个设备广播时,不会一直碰撞广播。也就是说,设置100ms的广播间隔,实际上两次广播事件的时间间隔可能是100110ms之间的任意时间。
广播数据包最多能携带31个字节的数据,一般包含可读的设备名称,设备是否可连接等信息。
当主机收到从机广播的数据包后,它可以再发送获取更多数据包的请求,这个时候从机将广播扫描回应数据包,扫描回应数据包和广播包一样,可以携带31个字节的数据。
3.3扫描
扫描是主机监听广播信道、接收从机广播数据的过程。主机通过扫描可以发现周围正在广播的从机设备,获取广播数据包;如果从机支持扫描响应,主机还可以发送扫描请求,进一步获取扫描响应数据包。之后,主机可以根据扫描到的设备信息,选择是否向某个从机发起连接请求,从而建立连接并进行通信。
扫描过程中有两个比较重要的时间参数:扫描窗口 和 扫描间隔。
扫描窗口:主机真正打开接收窗口、监听广播信道的时间。
扫描间隔:两个连续扫描窗口开始时刻之间的时间差。
可以简单理解为:
|<----------- 扫描间隔 ----------->|+---------------+ +---------------+|扫描窗口|休息时间|扫描窗口|+---------------+ +---------------+扫描窗口和扫描间隔需要满足以下关系:
- 扫描窗口和扫描间隔的设置值都不能大于 10.24 s。
- 扫描窗口的值不能大于扫描间隔。
- 如果扫描窗口等于扫描间隔,表示主机一直处于扫描状态,中间没有休息时间。
根据是否发送扫描请求,BLE 扫描可以分为 被动扫描 和 主动扫描。
被动扫描
主机只监听广播信道上的数据。当接收到从机发送的广播包后,协议栈会把广播数据上报给上层应用。被动扫描不会向从机发送额外请求,因此只能获取广播包中携带的数据。
主动扫描
主动扫描除了接收从机的广播包之外,还会在条件允许时向从机发送扫描请求。如果从机的广播类型支持扫描响应,那么从机会返回一个 扫描响应数据包。这样主机除了能拿到广播数据外,还可以额外获取扫描响应数据。
因此,主动扫描通常比被动扫描能获取更多信息,例如完整设备名称、额外服务 UUID 或厂商自定义数据等。
3.4连接
BLE 设备在建立连接之后,主机和从机就不再依赖广播信道进行普通数据通信,而是会切换到数据信道上进行收发。连接建立后,双方会按照一定的时间周期进行通信,这个周期称为 连接事件。
在每个连接事件中,主机和从机可以相互发送数据。如果当前没有业务数据需要传输,链路层也会通过空包维持连接状态。
可以简单理解为:
连接建立|v+-----------+ +-----------+ +-----------+|连接事件||连接事件||连接事件|+-----------+ +-----------+ +-----------+||||<-- 连接间隔 -->||<-- 连接间隔 -->|连接中比较重要的参数主要有三个:连接间隔、从机延迟 和 监督超时。
连接间隔(Connection Interval)
连接间隔表示两次连接事件之间的时间间隔,单位是 1.25 ms。它的取值范围通常是 7.5 ms ~ 4 s。连接间隔越短,通信响应越快,但功耗越高;连接间隔越长,功耗越低,但数据交互的延迟也会增加。
从机延迟(Slave Latency)
从机延迟表示从机在没有数据需要发送时,可以跳过若干个连接事件,从而减少唤醒次数、降低功耗。如果从机有数据需要发送,则需要在连接事件中进行响应。
监督超时(Supervision Timeout)
监督超时用于判断连接是否已经失效。如果在这个时间内没有成功完成连接事件,设备就会认为连接断开,并回到未连接状态。监督超时的单位是 10 ms,取值范围通常是 100 ms ~ 32 s。
连接参数之间有一定约束关系。比如监督超时必须大于有效连接间隔,否则设备可能还没来得及进行下一次通信,就被判断为连接超时。
有效连接间隔可以简单理解为从机实际参与通信的最大时间间隔:
有效连接间隔 = 连接间隔 × (1 + 从机延迟)
例如:
连接间隔=100ms 从机延迟=4有效连接间隔 = 100 ms × (1 + 4) = 500 ms
这表示在没有数据需要发送时,从机最多可以每隔 500 ms 参与一次连接事件。
实际开发中,连接参数通常需要根据应用场景进行权衡:
如果希望通信响应更快,可以适当减小连接间隔,但功耗会增加。
如果希望降低功耗,可以适当增大连接间隔,或者设置合理的从机延迟。
如果设备需要兼容手机等平台,还需要注意不同系统对连接参数可能有额外要求。
总的来说,BLE 连接阶段的核心就是:主机和从机按照连接间隔周期性进入连接事件,在连接事件中完成数据收发或维持链路;通过连接间隔、从机延迟和监督超时这几个参数,可以在通信实时性和功耗之间做平衡。
4.GAP和GATT
4.1 GAP和GATT的区别
蓝牙协议栈分为两类结构:控制器(Controller)和主机(Host)。每个类别都有子类别,这些子类别执行特定的角色。我们将要研究的两个子类别是 通用访问配置文件 (GAP)和 通用属性配置文件 (GATT)。
- GAP是Generic Access Profile的缩写,中文含义是:通用访问配置文件,定义了 BLE网络堆栈的一般拓扑。
- GATT是Generic Attribute Profile的缩写,中文含义是:通用属性配置文件,详细描述了一旦设备建立连接后如何传输属性(数据)。
GATT特别关注如何根据其描述的规则格式化打包和发送数据。在BLE网络堆栈中,属性协议(ATT)与GATT紧密对齐,GATT直接位于ATT的顶部。GATT实际上使用ATT来描述如何从两个连接的设备交换数据。
4.2 GAP通用访问配置文件
BLE设备可以使用两种机制与外界通信:广播或连接。这些机制受通用访问配置文件(GAP)准则的约束。GAP定义了启用BLE的设备如何使其自身可用,以及两个设备如何直接相互通信。
4.2.1 建立连接
设备可以通过采用GAP中指定的以下角色来加入BLE网络:
A、广播(Broadcasting):这些角色不必显式地相互连接即可传输数据。
广播者(Broadcaster):广播公共数据包的设备,例如可以广播按下按钮的时间。
观察者(Observer):侦听广播者发送的广告包中数据的设备。广播者和观察者之间没有任何连接。/2、/2
B、连接(Connecting):这些角色必须显式连接和握手才能传输数据。这些角色比广播角色更常用。
从机设备(Peripheral): 通过广播,告知其他设备自己的存在,以便主机设备可以建立连接。连接后,从机设备不再向其他主机设备广播数据,而是保持与主机设备的连接。
从机设备功耗低,因为它们只需要定期发送信标即可。主机设备负责开始与从机设备的通信。
手环是BLE外设的一个示例。
主机设备(Central):一种通过侦听广播包来启动与从机设备的连接的设备。主机设备可以连接到许多其他从机设备。
当主机设备要连接时,它将请求连接数据包发送到从机设备。如果从机设备接受来自主机设备的请求,则建立连接。
当您的手机连接到手环时,就是BLE Central设备的一个示例。
4.2.2 建立连接后
主机设备可以更新连接参数: 主机设备通常在设备与其自身之间建立连接参数。只有主机设备能修改连接参数。但是,从机设备可以要求主机设备更改连接参数,及从机发送更新参数请求。
从机设备或主机设备可以终止连接:连接可能由于多种原因而终止,例如设备的电池可能耗尽或网络干扰可能导致连接失败。设备还可以主动与对等设备断开连接。
4.3 通用属性配置文件
4.3.1模型角色
GATT分为两种类型,注意与从机或主机无关。
客户端(Client):客户端可以发送请求给GATT服务端,客户端可以读(Read)/写(Write)服务端的属性(Attributes ),通过属性可以通信数据。
服务端(Server):服务端是用来存储属性(Attributes )的,每当客户端发送请求时,服务端会相应这些请求。
4.3.2客户端与服务端的关系
一个示例如下:手环采集了心跳信息,希望计算机读取该信息。手环充当服务端并提供信息。手机充当客户端,读取该信息。
GAP和GATT模型角色基本上彼此独立从机设备或主机设备都可以充当服务端或客户端,这取决于数据的流动方式。
在一般的主从机通信时,主机可以通过读写从机的属性,实现接收和发送数据给从机,从机可以通过发送通知的方式实现与主机的通信。因此,一般从机是作为GATT的服务端,主机作为GATT的客户端。
5.协议栈分层协作
下面以如何发送一个无线数据包的例子来简单阐述协议栈中各分层的作用和必要性。实际上,协议栈的实现可能更加负责,它需要考虑方方面面的因素。
5.1 发送数据包
假设有设备A和设备B,设备A要把自己的电量状态83%(十六进制表示为0x53)发给设备B,该怎么做呢?作为一个开发者,他希望越简单越好,对他而言,他希望调用一个简单的API就能完成这件事,比如send(0x53),实际上我们的BLE协议栈就是这样设计的,开发者只需调用send(0x53)就可以把数据发送出去了,其余的事情BLE协议栈帮你搞定。很多人会想,BLE协议栈是不是直接在物理层就把0x53发出去,就如下图所示:
这种方式初看起来挺美的,但由于很多细节没有考虑到,实际是不可行的。首先,它没有考虑用哪一个射频信道来进行传输,在不更改API的情况下,我们只能对协议栈进行分层,为此引入LL层,开发者还是调用send(0x53),send(0x53)再调用send_LL(0x53,2402M)(注:2402M为信道频率)。这里还有一个问题,设备B怎么知道这个数据包是发给自己的还是其他人的,为此BLE引入access address概念,用来指明接收者身份,其中,0x8E89BED6这个access address比较特殊,它表示要发给周边所有设备,即广播。如果你要一对一的进行通信(BLE协议将其称为连接),即设备A的数据包只能设备B接收,同样设备B的数据包只能设备A接收,那么就必须生成一个独特的随机access address以标识设备A和设备B两者之间的连接。
5.2 广播方式
我们先来看一下简单的广播情况,这种情况下,我们把设备A叫advertiser(广播者),设备B叫scanner或者observer(扫描者)。广播状态下设备A的LL层API将变成send_LL(0x53,2402M, 0x8E89BED6)。由于设备B可以同时接收到很多设备的广播,因此数据包还必须包含设备A的device address(0xE1022AAB753B)以确认该广播包来自设备A,为此send_LL参数需要变成send_LL(0x53,2402M, 0x8E89BED6, 0xE1022AAB753B)。LL层还要检查数据的完整性,即数据在传输过程中有没有发生窜改,为此引入CRC24对数据包进行检验 (假设为0xB2C78E) 。同时为了调制解调电路工作更高效,每一个数据包的最前面会加上1个字节的preamble(前导帧),preamble一般为0x55或者0xAA。这样,整个空中包就变成(注:空中包用小端模式表示!):
上面这个数据包还有如下问题:
没有对数据包进行分类组织,设备B无法找到自己想要的数据0x53。为此我们需要在access address之后加入两个字段:LL header和长度字节。LL header用来表示数据包的LL类型,长度字节用来指明payload的长度
设备B什么时候开启射频窗口以接收空中数据包?如上图case1所示,当设备A的数据包在空中传输的时候,设备B把接收窗口关闭,此时通信将失败;同样对case2来说,当设备A没有在空中发送数据包时,设备B把接收窗口打开,此时通信也将失败。只有case3的情况,通信才能成功,即设备A的数据包在空中传输时,设备B正好打开射频接收窗口,此时通信才能成功,换句话说,LL层还必须定义通信时序。
当设备B拿到数据0x53后,该如何解析这个数据呢?它到底表示湿度还是电量,还是别的意思?这个就是GAP层要做的工作,GAP层引入了LTV(Length-Type-Value)结构来定义数据,比如020105,02-长度,01-类型(强制字段,表示广播flag,广播包必须包含该字段),05-值。由于广播包最大只能为31个字节,它能定义的数据类型极其有限,像这里说的电量,GAP就没有定义,因此要通过广播方式把电量数据发出去,只能使用供应商自定义数据类型0xFF,即04FF590053,其中04表示长度,FF表示数据类型(自定义数据),0x0059是供应商ID(自定义数据中的强制字段),0x53就是我们的数据(设备双方约定0x53就是表示电量,而不是其他意思)。
最终空中传输的数据包将变成:
AAD6BE898E600E3B75AB2A02E102010504FF5900538EC7B2- AA – 前导帧(preamble)
- D6BE898E – 访问地址(access address)
- 60 – LL帧头字段(LL header)
- 0E – 有效数据包长度(payload length)
- 3B75AB2A02E1 – 广播者设备地址(advertiser address)
- 02010504FF590053 – 广播数据
- 8EC7B2 – CRC24值
有了PHY,LL和GAP,就可以发送广播包了,但广播包携带的信息极其有限,而且还有如下几大限制:
- 无法进行一对一双向通信 (广播是一对多通信,而且是单方向的通信)
- 由于不支持组包和拆包,因此无法传输大数据
- 通信不可靠及效率低下。广播信道不能太多,否则将导致扫描端效率低下。为此,BLE只使用37(2402MHz) /38(2426MHz) /39(2480MHz)三个信道进行广播和扫描,因此广播不支持跳频。由于广播是一对多的,所以广播也无法支持ACK。这些都使广播通信变得不可靠。
- 扫描端功耗高。由于扫描端不知道设备端何时广播,也不知道设备端选用哪个频道进行广播,扫描端只能拉长扫描窗口时间,并同时对37/38/39三个通道进行扫描,这样功耗就会比较高。
而连接则可以很好解决上述问题,下面我们就来看看连接是如何将0x53发送出去的。
5.3 连接方式
到底什么叫连接(connection)?像有线UART,很容易理解,就是用线(Rx和Tx等)把设备A和设备B相连,即为连接。用“线”把两个设备相连,实际是让2个设备有共同的通信媒介,并让两者时钟同步起来。蓝牙连接有何尝不是这个道理,所谓设备A和设备B建立蓝牙连接,就是指设备A和设备B两者一对一“同步”成功,其具体包含以下几方面:
- 设备A和设备B对接下来要使用的物理信道达成一致
- 设备A和设备B双方建立一个共同的时间锚点,也就是说,把双方的时间原点变成同一个点
- 设备A和设备B两者时钟同步成功,即双方都知道对方什么时候发送数据包什么时候接收数据包
- 连接成功后,设备A和设备B通信流程如下所示:
如上图所示,一旦设备A和设备B连接成功(此种情况下,我们把设备A称为Master或者Central,把设备B称为Slave或者Peripheral),设备A将周期性以CI(connection interval)为间隔向设备B发送数据包,而设备B也周期性地以CI为间隔打开射频接收窗口以接收设备A的数据包。同时按照蓝牙spec要求,设备B收到设备A数据包150us后,设备B切换到发送状态,把自己的数据发给设备A;设备A则切换到接收状态,接收设备B发过来的数据。由此可见,连接状态下,设备A和设备B的射频发送和接收窗口都是周期性地有计划地开和关,而且开的时间非常短,从而大大降低系统功耗并大大提高系统效率。
现在我们看看连接状态下是如何把数据0x53发送出去的,从中大家可以体会到蓝牙协议栈分层的妙处。
- 对开发者来说,很简单,他只需要调用send(0x53)
- GATT层定义数据的类型和分组,方便起见,我们用0x0013表示电量这种数据类型,这样GATT层把数据打包成130053(小端模式!)
- ATT层用来选择具体的通信命令,比如读/写/notify/indicate等,这里选择notify命令0x1B,这样数据包变成了:1B130053
- L2CAP用来指定connection interval(连接间隔),比如每10ms同步一次(CI不体现在数据包中),同时指定逻辑通道编号0004(表示ATT命令),最后把ATT数据长度0x0004加在包头,这样数据就变为:040004001B130053
- LL层要做的工作很多,首先LL层需要指定用哪个物理信道进行传输(物理信道不体现在数据包中),然后再给此连接分配一个Access address(0x50655DAB)以标识此连接只为设备A和设备B直连服务,然后加上LL header和payload length字段,LL header标识此packet为数据packet,而不是control packet等,payload length为整个L2CAP字段的长度,最后加上CRC24字段,以保证整个packet的数据完整性,所以数据包最后变成:
- AAAB5D65501E08040004001B130053D550F6
- AA – 前导帧(preamble)
- 0x50655DAB – 访问地址(access address)
- 1E – LL帧头字段(LL header)
- 08 – 有效数据包长度(payload length)
- 04000400 – ATT数据长度,以及L2CAP通道编号
- 1B – notify command
- 0x0013 – 电量数据handle
- 0x53 – 真正要发送的电量数据
- 0xF650D5 – CRC24值