Bluetooth LE 介绍以及树莓派 (RPi) 如何连接低功耗蓝牙 BLE——分别用命令和C语言实现

目录

1、简介

2、BLE 的主要特点

3、BLE 协议架构

4、BLE 角色划分

5、BLE 数据收发

5.1 广播方式

5.2 广播报文


最近在做有关低功耗蓝牙 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 MHz2480MHz
  • 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 广播方式

 上面整个数据包有以下问题:

  1. 没有对数据包进行分类组织,设备 B 无法找到自己想要的数据0x53。为此文明需要在 access address 之后加入两个字段:LLheader 和长度字节。LL header 用来表示数据包的 LL 类型,长度字节用来指明 payload 的长度。
  2. 设备 B 什么时候开启射频窗口以接收空中数据包?LL 层还必须定义通信时序。
  3. 当设备 B 拿到数据0x53后,该如何解析这个数据呢?这个就是 GAP 层要做的工作, GAP 层引入了 LTV(Length-Type-Value)结构来定义数据。广播包最大值为31字节

有了 PHY,LL 和 GAP ,就可以发送广播包,但广播包携带的信息极其有限,而且还有如下几大限制:

  1. 无法进行一对一双向通信(广播是一对多通信,而且是单方向的通信)
  2. 由于不支持组包和拆包,因此无法传输大数据
  3. 通信不可靠及效率低下。广播信道不能太多,否则江导致扫描效率低下。为此,BLE 只使用37(2402MHz) /38(2426MHz) /39(2480MHz) 三个信道进行广播和扫描,因此广播不支持跳频。由于广播是一对多的,所以广播也无法支持 ACK ,这些都使广播通信变得不可靠。
  4. 扫描端功耗高。由于扫描端不知道设备端何时广播,也不知设备选用哪个频道进行广播,扫描端只能拉长扫描窗口时间,并同时对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;
 

    
}

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值