目录
最近在做有关低功耗蓝牙 BLE 的项目,还是有必要整理一下 BLE 的资料。在树莓派连接低功耗蓝牙 BLE 用 C 语言实现已经实现,前期的准备这个折磨了我好久,网上关于这个的资料很少,大部分都是用 Python 来实现的,但是作为一名嵌入式工程师,我们还是得必须使用 C 语言来实现,所以我就开启了阅读源码的道路。本人第一次从源码中提取自己想要的 API 刚开始确实有点难度,踩过很多的坑,但是天不负有心人,只要坚持最终还是有所成就的!请相信自己。
1、BLE 简介
蓝牙(Bluetooth)是一种短距离的无线通讯技术,运行在 2.4GHz 免费频段,可实现固定设备、移动设备之间的数据交换。一般蓝牙 3.0 之前的 BR/EDR 蓝牙称为经典蓝牙,而将蓝牙 4.0 规范下的 LE 蓝牙称为低功耗蓝牙(BLE, Bluetooth Low Energy)。BLE 主要用于医疗保健、运动健身、信标、安防、家庭娱乐等邻域的新兴应用。相较经典蓝牙,低功耗蓝牙旨在保持同等通信范围的同时显著降低低功耗和成本。
2、BLE 的主要特点
- 低功耗,使用纽扣电池就可以运行数月至数年。
- 快连接,毫秒级的连接速度,传统蓝牙甚至长达数分钟。
- 远距离,长达数百米的通信距离,而传统蓝牙通常10米左右。
3、BLE 协议架构
Bluetooth LE 协议栈从下至上分为几个层级:Physical Layer(PHY)、Link Layer(LL)、Host Controller Interface(HCI)、Logical Link Control and Adaption Protocol Layer(L2CAP)、Attribute Protocol(ATT)、Security Manager Protocol(SMP)、Generic Attribute Protocol(GATT)、Generic Access Protocol(GAP)。
- PHY:PHY 层主要负责在物理信道上发送和接收信息包。Bluetooth LE 使用 40个射频信道。频率范围:2402 MHz到2480MHz。
- LL:LL 层是整个 BLE 协议栈的核心,也是 BLE 协议栈的难点和重点。LL 层要做的事情非常多,比如具体选择哪个射频通道进行通信,怎么识别空中的数据包,具体在哪个时间把数据包发送出去,怎么保证数据的完整性,ACK 如何接收,如何进行重传,以及如何对链路进行管理和控制等待。LL 层只负责把数据发出去或者收回来,对数据进行怎么的解析则交给上面的CAP 或者 GATT。(LL 层主要负责创建、修改和释放逻辑链路(以及,如果需要,它们相关的逻辑传输)。以及与失败之间的物理链路相关的参数的更新。它控制链路层状态机处于准备、广播、监听/扫描、发起连接、已连接五种状态之一)
- HCI:HCI 层向主机和控制提供一个标准化的接口。该层可以由软件 API 实现或者使用硬件接口 UART、SPI、USB 来控制。
- L2CAP:L2CAP 层负责对主机和协议栈之间交换的数据进行协议复用能力、分段和重组操作。
- ATT:ATT 层实现了属性服务器和属性客户端之间的点对点协议。ATT 客户端向 ATT 服务端发送命令、请求和确认。ATT 服务端向客户端发送响应、通知和指示。简单来说,ATT 层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。
- SMP:SMP 层用于生成加密密钥和身份密钥。SMP 还管理加密密钥和身份密钥的存储,并负责生成随机地址并将随机地址解析为已知设备身份。
- GATT:GATT 层表示属性服务器和可选的属性客户端的功能。该配置文件描述了属性服务器中使用的服务、特征和属性的层次结构。该层提供用于发现、读取、写入和指示服务特性和属性的接口。
- GAP:GAP 层代表所有蓝牙设备通用的基本功能,例如传输。协议和应用程序配置文件使用的模式和访问程序。GAP 服务包括设备发现、连接模式、安全、身份验证、关联模型和服务发现。GAP 目前主要用来进行广播,扫描和发起连接等。
4、BLE 角色划分
在 Bluetooth LE 协议栈中不同的层级有不同的角色划分。这些角色划分互不影响。
- LL:设备可划分为主机(Master或Central)和从机(Periphere),从机广播,主机可以发起连接。
- GAP:定义了4种特定角色:广播者、观察者、外围设备和中心设备。
- GATT:设备可以分为服务端和客户端。
当主机和从机建立连接之后才能相互互发数据
- 主机,主机可以发起对从机的扫描连接。例如手机,通常作为BLE的主机设备
- 从机,从机只能广播并等待主机的连接。例如智能手环,是作为BLE的从机设备
另外还有观察者(Observer)和广播者(Broadcaster),这两种角色不常使用,但也十分有用,例如 iBeacon,就可以使用广播者角色来做,只需要广播特定内容即可。
- 观察者,观察者角色监听空中的广播事件,和主机唯一的区别是不能发起连接,只能持续扫描从机。
- 广播者,广播者可以持续广播信息,和从机的唯一区别是不能被主机连接,只能广播数据。
GATT 其实是一种属性传输协议,简单的讲可以认为是一种属性传输的应用协议。这个属性 的结构非常简单。它由服务组成,每个服务由不同数量的特征组成,每个特征又由很多其他的元素组成。
GATT 服务端 和 GATT 客户端这两种角色存在于 Bluetooth LE 连接建立之后。GATT 服务器存储通过属性协议传输的数据,并接受来自 GATT 客户端的属性协议请求、命令和确认。简而言之,通提供数据的一端称为 GATT 服务端,访问数据的一端称为 GATT 客户端。
5、BLE 数据收发
5.1 广播方式
上面整个数据包有以下问题:
- 没有对数据包进行分类组织,设备 B 无法找到自己想要的数据0x53。为此文明需要在 access address 之后加入两个字段:LLheader 和长度字节。LL header 用来表示数据包的 LL 类型,长度字节用来指明 payload 的长度。
- 设备 B 什么时候开启射频窗口以接收空中数据包?LL 层还必须定义通信时序。
- 当设备 B 拿到数据0x53后,该如何解析这个数据呢?这个就是 GAP 层要做的工作, GAP 层引入了 LTV(Length-Type-Value)结构来定义数据。广播包最大值为31字节。
有了 PHY,LL 和 GAP ,就可以发送广播包,但广播包携带的信息极其有限,而且还有如下几大限制:
- 无法进行一对一双向通信(广播是一对多通信,而且是单方向的通信)
- 由于不支持组包和拆包,因此无法传输大数据
- 通信不可靠及效率低下。广播信道不能太多,否则江导致扫描效率低下。为此,BLE 只使用37(2402MHz) /38(2426MHz) /39(2480MHz) 三个信道进行广播和扫描,因此广播不支持跳频。由于广播是一对多的,所以广播也无法支持 ACK ,这些都使广播通信变得不可靠。
- 扫描端功耗高。由于扫描端不知道设备端何时广播,也不知设备选用哪个频道进行广播,扫描端只能拉长扫描窗口时间,并同时对37/38/39 三个通道进行扫描,主要功耗就会比较高。
而连接则可以很好解决上述问题。
到底什么叫连接(connect)?像有线UART,很容易理解,就是用线(Rx和Tx等)把设备A和设备B相连,即为理解。用“线”把两个设备相连,实际是让2个设备有共同的通信媒介,并让两者时钟同步起来。
5.2 广播报文
一个完整的BLE 广播报文由四部分组成,分别是前导码、接入地址、协议数据单元(PDU)和CRC校验码。
- 前导码:用来同步时序,可以是0x55或者0xAA,由接入地址的第一个比特决定。如果接入地址的第一个比特是“0”,则前导码是0x55;如果接入地址的第一个比特是“1”,则前导码是0xAA。在广播报文里面,这一字节为0xAA。
- 接入地址:长度为4个字节,广播报文的接入地址为0x8E89BED6
- 协议数据单元(PDU):包含两个字节的报头和0~37字节的净荷
- CRC校验码:长度为3个字节
PDU 数据报头(2个字节)由下面几个字段组成:
- PDU Type(4bits):PDU类型,标识广播报文的类型
- RFU(2bits):Reserved For Future ,保留位
- TxAdd(1bit):发送地址类型,标识广播地址是公有地址还是随机地址
- RxAdd(1bit):接收地址类型,广播报文不使用这一比特
- Length(6bits):长度,标识净荷的长度(6~37字节)
- RFU(2bits):Reserved For Future,保留位
公有地址和私有地址的区别(为什么说这个呢?因为接下的项目中我们需要连接 BLE ,会涉及到这个地址的连接类型 dst_type 的选择,还是有必要了解一下的)
- 公有地址:类似 MAC 地址,由OUI 和一个唯一的数字组成
- 随机地址:为防止设备被跟踪,广播地址可以是随机的。随机地址又分为静态设备地址(Static Device Address)、私有地址(Private Device Address)和不可解释私有地址(Nonresolvable Private Address)。
6、树莓派连接低功耗蓝牙 BLE (命令实现)
6.1 BLE 扫描
为了连接到 BLE 设备,我们首先需要扫描发现设备。其中一种方法是使用 hcitool 实用程序,它基本上是所有 HCI 命令(经典蓝牙和 BLE)的“瑞士刀”。经典蓝牙的扫描使用 scan,而 BLE 设备的扫描则使用 lescan。
pi@raspberrypi:~ $ sudo hcitool -i hci0 lescan
LE Scan ...
A0:76:4E:59:12 ESP32
... ....
pi@raspberrypi:~ $ sudo hcitool leinfo A0:76:4E:59:12
Requesting information ...
Handle: 64 (0x0040)
Features: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
在上面的扫描过程中,我们会发现 ESP32 BLE 设备,这是因为我们在 ESP32 BLE 中设定的广播报文里含有设备名。
BLE 如何开启广播模式请参考官方文档,这里就不多说了。
6.2 BLE 连接与断开
hcitool 命令不是操作 GATT 的工具,通过它我们扫描发现 BLE 设备的地址后,就可以使用gatttool 命令来连接并操纵 BLE 设备的 GATT。该命令支持交互式和非交互式两种工作模式,在交互模式下,控制台提供了一个界面,使您可以发出命令并与设备进行交互,您将需要时再退出返回到终端。
使用下面命令可以用来连接、断开蓝牙 BLE。
pi@raspberrypi:~ $ gatttool -I -i hci0 -b A0:76:4E:59:12
-I 进入交互模式
-i 指定蓝牙的接口设备,如果不指定默认使用 hci0
-b 指定连接的蓝牙设备的MAC地址
[A0:76:4E:59:12][LE]> help //查看使用帮助信息
... ...
[A0:76:4E:59:12][LE]> connect //有时第1此可能connect 不成功,多connect几次就ok
Attempting to connect to A0:76:4E:59:12
Error: connect to A0:76:4E:59:12: Function not implemented (38)
[A0:76:4E:59:12][LE]> connect
Attempting to connect to A0:76:4E:59:12
Connection successful
[A0:76:4E:59:12][LE]> disconnect //断开BLE蓝牙设备的连接
(gatttool:2362): GLib-WARNING **: 10:07:24.803: Invalid file descriptor.
[A0:76:4E:59:12][LE]> exit //退出交互模式
pi@raspberrypi:~ $
6.3 查看所有 Services
使用 primary 命令可以查看当前蓝牙设备提供的所有的服务。
[A0:76:4E:59:12][LE]> connect
Attempting to connect to A0:76:4E:59:12
Connection successful
[A0:76:4E:59:12][LE]> primary //发现BLE的GATT服务,这里有3个服务,最后1个为自定义服务
attr handle: 0x0001, end grp handle: 0x0005 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0014, end grp handle: 0x001c uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0028, end grp handle: 0xffff uuid: 0000abf0-0000-1000-8000-00805f9b34fb
其中:
- UUID: 00001801-0000-1000-8000-00805f9b34fb 为 Generic Attribute 标准服务,它的特征值handle范围为 0x00014~0x001C
- UUID: 00001800-0000-1000-8000-00805f9b34fb 为 Generic Access 标准服务, 它的特征值handle范围为 0x0001~0x0005
- UUID: 0000abf0-0000-1000-8000-00805f9b34fb 为 自定义服务, 它的特征值handle范围为0x0028~0xffff
不同的设备可能会不太一样。
6.4 查找服务特征值
使用 characteristics 命令可以查看当前蓝牙设备提供的所有的characteristics。每个特征值都有一个独一无二的 UUID 值,同时也有一个相应的 handle 值,它是在使用命令或编程访问特征值时的句柄。每个特征值也有它的相应属性,如可读、可写等。
[A0:76:4E:59:12][LE]> characteristics
handle: 0x0002, char properties: 0x20, char value handle: 0x0003, uuid:00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0015, char properties: 0x02, char value handle: 0x0016, uuid:00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0017, char properties: 0x02, char value handle: 0x0018, uuid:00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0019, char properties: 0x02, char value handle: 0x001a, uuid:00002aa6-0000-1000-8000-00805f9b34fb
handle: 0x0029, char properties: 0x06, char value handle: 0x002a, uuid:0000abf1-0000-1000-8000-00805f9b34fb
handle: 0x002b, char properties: 0x12, char value handle: 0x002c, uuid:0000abf2-0000-1000-8000-00805f9b34fb
handle: 0x002e, char properties: 0x06, char value handle: 0x002f, uuid:0000abf3-0000-1000-8000-00805f9b34fb
handle: 0x0030, char properties: 0x12, char value handle: 0x0031, uuid:0000abf4-0000-1000-8000-00805f9b34fb
6.5 读服务特征值
使用 char-read-uuid 命令可以读取某个 characteristics 值。
[A0:76:4E:59:12][LE]> characteristics 0x0001 0x001c //查找标准服务里的所有特征值
handle: 0x0002, char properties: 0x20, char value handle: 0x0003, uuid:00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0015, char properties: 0x02, char value handle: 0x0016, uuid:00002a00-0000-1000-8000-00805f9b34fb
[A0:76:4E:59:12][LE]> char-read-uuid 00002a05-0000-1000-8000-00805f9b34fb //该特征值不具备可读属性,所以读取失败
Error: Read characteristics by UUID failed: Attribute can't be read
[A0:76:4E:59:12][LE]> char-read-uuid 00002a00-0000-1000-8000-00805f9b34fb //该特征值可读
handle: 0x0016 value: 45 53 50 33 32 //("ESP32")
[A0:76:4E:59:12][LE]> char-read-hnd 0x0016 //使用 handle 来读取该特征值的 handle(注意是0x0016,而不是0x0015)
Characteristic value/descriptor: 45 53 50 33 32
3.6 写服务特征值
我们可以使用 char-write-req 命令来写相应的数据。
[A0:76:4E:59:12][LE]> char-read-hnd 0x002f
Characteristic value/descriptor: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[A0:76:4E:59:12][LE]> char-write-req 0x002f 11223344
Characteristic value was written successfully
[A0:76:4E:59:12][LE]> char-read-hnd 0x002f
Characteristic value/descriptor: 11 22 33 44
6.7 读写 Notification
从上面的操作中我们可以了解到 BLE通信就是对相应的特征值进行读、写操作。但通常数据的读写都是由主机(如手机)发起。那如果蓝牙从机(如ESP32)有数据更新(如传感器重新采样了),那如何通知主机来读取呢?对于这种情形的话,蓝牙BLE支持 Notification 机制,如果主机选择使能该机制之后,从机的数据更新将会主动发送给主机。
gatttool 的交互模式下并不能读到这些数据。如果想实时获取 Notification 的数据 话,则需要使用 gatttool 非交互模式来监听。
pi@raspberrypi:~ $ gatttool -I -i hci0 -b A0:76:4E:59:12
... ...
[A0:76:4E:59:12][LE]> exit
(gatttool:2467): GLib-WARNING **: 14:52:01.698: Invalid file descriptor.
pi@raspberrypi:~ $ sudo gatttool -i hci0 -b A0:76:4E:59:12 --char-write-req -a 0x002d -n 0100
//此时在 BLE 控制终端随便输入一些数据,树莓派的 gatttool 命令下将会显示收到的数据。
7、树莓派连接低功耗蓝牙 BLE (C语言实现)
下面这个代码 BLE 的 MAC 地址和 uuid 都要改成自己设备的,使用到了 BlueZ 库 和 Gattlib 库,具体的后续在说(这阵子实在没有充足的时间来对这个项目进行总结,如果你有什么疑惑的地方可以私聊我,或者直接留言)。
这个 demo 可以自己再改进一下,锻炼一下自己的编程能力。
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <glib.h>
#include <gattlib.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#define BLE_SCAN_TIMEOUT 5
#define bleaddr "A0:76:4E:55:ED:B2" //target BLE device BT-MAC addr !!uppercase!! characters
const char * write_uuid = "c303";//any esp32 writable characteristic uuid
static uuid_t uuid;//be used to change type :wrtite_uuid(char) to uuid(uuid_t)
/*扫描回调函数*/
static void BLE_discovered_callback(void *adapter,const char* addr, const char* name, void* user_data) //regular scanning
{
if(name)
{
printf("BLE Discovered %s - '%s'\n", addr, name);
}
else
{
printf("BLE Discovered %s - 'unknown'\n", addr);
}
return ;
}
int main(int argc, char** argv)
{
int rv,i;
void * adapter ;
const char * adapter_name = NULL; //if NULL ,hci0 be choiced default in my situation
char uuid_str[MAX_LEN_UUID_STR + 1]; //MAX_LEN_UUID_STR =37
gatt_connection_t * conn = NULL;
rv = gattlib_adapter_open(adapter_name,&adapter); //open the Pi bt-device based on the dapter_name,if adapter_name is NULL,default Pi bt-device will be chose(hci0 in this situation)
if(rv)
{
printf("adapter open failed\n");
return 1;
}
rv = gattlib_adapter_scan_enable(adapter,BLE_discovered_callback,BLE_SCAN_TIMEOUT,NULL);
//gattlib_adapter_scan_enable_with_filter()
if (rv)
{
printf("Failed to scan.\n");
return -1;
}
if(gattlib_adapter_scan_disable(adapter) != GATTLIB_SUCCESS)
{
printf("Error: end scan failed,ready to find AD info\n\n");
return -1;
}
puts("Scan completed\n");
printf("start to connnect ...\n");
conn = gattlib_connect(NULL,bleaddr, GATTLIB_CONNECTION_OPTIONS_LEGACY_DEFAULT);
if (conn == NULL) {
printf("Fail to connect to the bluetooth device : %s\n",bleaddr);
return 1;
}
printf("coonect to BLE devices : %s sucess \n",bleaddr);
memset(&uuid_str,0,sizeof(uuid_str));
strncpy(uuid_str,write_uuid,sizeof(uuid_str));
if (gattlib_string_to_uuid(uuid_str, strlen(uuid_str) + 1, &uuid) < 0) {
printf("string to uuid_t failed \n");
return -1;
}
for(i=49;i<54;i++) //十进制的49 对应ascii码的31,即字符'1'
{
rv = gattlib_write_without_response_char_by_uuid(conn,&uuid,(uint8_t*)&i,1);
if (rv != GATTLIB_SUCCESS)
{
if(rv == GATTLIB_NOT_FOUND)
{
printf("Could not find GATT Characteristic with UUID %s");
return -1;
}
else
{
printf("Error while writing GATT Characteristic with UUID %s (ret:%d)",uuid_str, rv);
return -2;
}
}
sleep(1);
}
gattlib_disconnect(conn);
if(gattlib_adapter_close(adapter) != GATTLIB_SUCCESS)
{
printf("adapter close failed\n");
}
printf("\nnormally exit\n");
return 0;
}