本帖最后由 起个名字好难啊 于 2024-5-14 16:49 编辑
(饭)前(撒)yan
前两个星期,园长就问大家有没有需要学的,然后:
行行行,那就满足你们,
这次这个BLE帖子的篇幅有点长,因为我是边学边写,相当于一个手记了,顺便也贴一下我在看的教学视频:
https://www.bilibili.com/video/BV1ad4y1d7AM?p=1&vd_source=02a465997504a99b4366d967ab71e479
学习本贴需要体现下载BLE 调试工具,App 推荐下载NRF Connecet:点击下载安装包
也可以下载鹏老师自己写的电脑端BLE调试助手:https://gitee.com/ospanic/BlueCom
什么是BLE?
看到这里,你就应该知道,BLE就是低功耗蓝牙的简称,莫工对BLE也不是很了解👀️ 。我只知道,现在的手机不能在系统设置当中直接搜索到BLE设备,只能使用APP或者小程序搜。
一、BLE 广播数据
BLE广播数据是什么呢?由什么作用呢?
广播数据是BLE从机发送一段信息,数据上包含了BLE设备的MAC地址,设备名称等内容,手机作为主机搜索到广播数据之后进行识别显示。
根据教学视频的讲义,BLE广播数据的字节最高只能到37个(字节),而且BLE MAC地址默认占用了6个,只剩下31个字节给我们使用。这31个字节全部由"AD Structure" 结构体组成,但是不会只有一个AD Structure,实际上会有好几个这样的结构体。
Ai-M6x发送广播数据的实践
在使用教程中鹏老师给我们演示了如何使用MicroPython来实现了广播数据的发送,但是Ai-M61不支持MicroPython,所以我们直接使用C语言来进行实践。根据教程,我们的逻辑应该是:
st=>start: 开始
op=>operation: 初始化协议栈
op1=>operation: 启动蓝牙
cond=>condition: 启动成功
op2=>operation: 发送广播
e=>end: 结束
st->op->op1->cond
cond(no)->e
cond(yes)->op2->e
Ai-M61 启用蓝牙组件
在工程的 proj.conf
文件中,添加以下代码启用BLE组件:
set(CONFIG_BLUETOOTH 1)
set(CONFIG_BTBLECONTROLLER_LIB ble1m0s1bredr0)
set(CONFIG_BLE_USE_MAC2 0)
set(CONFIG_BLE_TP_SERVER 1)
set(CONFIG_BTBLECONTROLLER_LIB ble1m10s1bredr0)
需要引用的头文件
#include "bluetooth.h"
#include "hci_driver.h"
#include "hci_core.h"
#include "conn.h"
#include "conn_internal.h"
#include "gatt.h"
#include "btble_lib_api.h"
创建广播数据
最简单的广播数据就是MAC+设备名称,但是因为广播数据需要定义TYPE(第一个AD Structure),所以设备名称在第二个AD Structure里定义:
#define ble_slave_name "Ai-M61-BLE"
/* 定义广播数据 总子节数不得超过31 byte */
static const struct bt_data salve_adv[] = {
BT_DATA_BYTES(BT_DATA_FLAGS,(BT_LE_AD_GENERAL| BT_LE_AD_NO_BREDR)), //数据头占用 2 byte
BT_DATA(BT_DATA_NAME_COMPLETE,ble_slave_name,sizeof(ble_slave_name)-1),//第二个数据,定义名称
};
启动BLE协议栈和蓝牙
Ai-M61自动蓝牙的函数需要一个回调函数,当蓝牙启动成功之后,会通过触发回调,我们可以在回调函数当中打印蓝牙MAC地址信息和开始广播:
**
* @brief 蓝牙启动回调
*
* @param err
*/
static void bt_enable_cb(int err)
{
if (!err)
{
bt_addr_le_t bt_addr;
bt_get_local_public_address(&bt_addr);
LOG_I("BD_ADDR:(MSB)%02x:%02x:%02x:%02x:%02x:%02x(LSB)",
bt_addr.a.val[5], bt_addr.a.val[4], bt_addr.a.val[3], bt_addr.a.val[2], bt_addr.a.val[1], bt_addr.a.val[0]);
//蓝牙启动完成之后发送广播包
int err = -1;
err = bt_le_adv_start(BT_LE_ADV_CONN, salve_adv, ARRAY_SIZE(salve_adv), NULL, 0);
if (err)
LOG_E("[BLE] adv fail(err %d)", err);
}
}
/**
* @brief 启动BLE 协议栈
*
*/
static void ble_stack_start(void)
{
// Initialize BLE controller
btble_controller_init(configMAX_PRIORITIES - 1);
// Initialize BLE Host stack
hci_driver_init();
bt_enable(bt_enable_cb);
}
所以我们只需要在main 当中调用 ble_stack_start()
函数就能让Ai-M61的BLE发送广播,使用电脑的BLE调试助手的搜索结果:
二、扫描响应
什么是扫描响应
看完教程之后,大家可能有个疑虑?什么是扫描响应?
说到,扫描响应分可分为:
- 可连接非定向(常用)
- 可连接定向 (快连)
- 不可连接非定向(信标)
- 可扫描非定向 (能看不能连)
对于我的理解,扫描响应得数据应该是区别于广播数据,但是也能被广播出去,这就以为着,当我们的广播数据太长的时候,可以使用扫描响应数据。通常情况下就是体现在BLE 蓝牙名字的设置上。
创建扫描响应
扫描响应数据的创建和广播数据一样,用的结构体也一样,在这里,我把BLE的名称放在扫描响应数据里面:
//定义蓝牙名称
#define ble_slave_name "Ai-M61-BLE"
/* 定义广播数据 总子节数不得超过31 byte */
static const struct bt_data salve_adv[] = {
BT_DATA_BYTES(BT_DATA_FLAGS,(BT_LE_AD_GENERAL| BT_LE_AD_NO_BREDR)), //数据头占用 2 byte
// BT_DATA(BT_DATA_NAME_COMPLETE,ble_slave_name,sizeof(ble_slave_name)-1),//第二个数据,定义名称
};
/* 定义扫描响应数据 总字节不得超过31byte*/
static const struct bt_data salve_rsp[] = {
BT_DATA(BT_DATA_NAME_COMPLETE,ble_slave_name,sizeof(ble_slave_name)-1),//把设备名称放在扫描响应当中
};
在Ai-M6x的BLE 组件中,扫描响应数据的添加和广播数据用的函数是同一个bt_le_adv_start
,它的第四个第五个参数就是扫描响应的数据和数据长度, 所以,只需要给bt_le_adv_start
函数的第四和第五个参数传递扫描响应数据和它的数据长度就行了。
bt_le_adv_start(BT_LE_ADV_CONN, salve_adv, ARRAY_SIZE(salve_adv), salve_rsp, ARRAY_SIZE(salve_rsp));
实践结果
三、状态切换
教程中,BLE 状态一共有五种:
- 就绪态: BLE启动之后的状态
- 广播态: BLE从机发送广播包时的状态
- 连接态: BLE连接成功之后
- 扫描态(BLE主机模式): BLE主机扫描从机时的状态
- 发起态(BLE主机模式): BLE主机对从机发起连接时的状态
本教程只关注从机模式的状态,从机模式合理的状态切换的逻辑:
st=>start: 开始
op=>operation: 启动蓝牙(就绪态)
op1=>operation: 发起广播(广播态)
cond=>condition: 是否已经连接
op2=>operation: 连接态
cond1=>condition: 是否断开连接
e=>end: 结束
st->op->op1->cond
cond(no)->op1
cond(yes)->op2->e
同样的,我们也只对连接和断开连接两个事件进行处理,连接对应的是连接态,而断开连接会变为就绪态,我们应该发起广播,把状态切换成广播态,让手机等BLE主机能够扫描到它。
连接回调和断开回调的创建
连接回调函数
我们可以在连接回调函数当中直接打印“connected:现在是连接态”信息,并且主动关闭广播,这样可以节省功耗,就像这样:
static void _connected(struct bt_conn *conn, u8_t err)
{
if (err) {
LOG_E("connected err:%d", err);
bt_conn_unref(conn);
return ;
}
LOG_I("connected:现在是连接态");
bt_le_adv_stop();
return ;
}
断开连接回调
断开连接的回调函数里,除了打印之外,还需要主动打开广播:
static void _disconnected(struct bt_conn *conn, u8_t reason)
{
LOG_W("disconnected, 此时是就绪态reason:%d", reason);
LOG_I("切换到 广播态");
int err = bt_le_adv_start(BT_LE_ADV_CONN, salve_adv, ARRAY_SIZE(salve_adv), salve_rsp, ARRAY_SIZE(salve_rsp));
if (err)
LOG_E("[BLE] adv fail(err %d)", err);
}
注册回调函数
回调函数不能直接运行,它必须有接口调用它们,在Ai-M6x的BLE协议库中,使用 struct bt_conn_cb
声明回调函数接口,并以下面这个函数来注册:
bt_conn_cb_register
用法示例:
/* 定义struct bt_conn_cb 结构体来指定相应的回调函数 */
static struct bt_conn_cb conn_callbacks = {
.connected = _connected,
.disconnected = _disconnected,
};
/* 在初始化函数中注册回调函数*/
/**
* @brief 启动BLE 协议栈
*
*/
static void ble_stack_start(void)
{
// Initialize BLE controller
#ifdef CONFIG_Ai_M6x
btble_controller_init(configMAX_PRIORITIES - 1);
#endif
#ifdef CONFIG_Ai_WB2
ble_controller_init(configMAX_PRIORITIES - 1);
#endif
// Initialize BLE Host stack
hci_driver_init();
bt_enable(bt_enable_cb);
bt_conn_cb_register(&conn_callbacks);//注册回调
}
运行结果,根据代码去试试吧
四、服务与特征(特性)
BLE 的服务和特性,是BLE数据互交的基础,前面所讲的内容全部都是让BLE 设备具备可连接功能而且。所以服务和特性直接决定了BLE设备是否可以接收和发送数据的能力。
服务决定了BLE设备的BLE设备类型,比如BLE HID是鼠标设备还是键盘设备。都需要通过定义服务来确定,在这里,不会深究BLE具备的服务类型。一个服务下面有一个或者多个特征,这些特征决定了服务的功能,一个服务到底是用来传数据的还是接收数据的,都需要特征来决定。
在实际编程当中,服务通常以service作为标识,而特征作为service的成员来定义,GATT的库当中,给了相关的宏定义让我们方便地创建特征。
UUID介绍
UUID是每个服务和特征的标识符,最长是128Bit(位),UUID有三种长度:
- 16Bit UUID
- 32Bit UUID
- 128Bit UUID
特征的属性和权限
属性
特征除了拥有UUID之外,还需要属性来声明这个特征的功能,比如这个特征是用来做通知的,还是用来读写的等,GATT一共给了以下几个属性:
BT_GATT_CHRC_BROADCAST
:广播类型的特征(非标准,不常用)
BT_GATT_CHRC_READ
:可读属性(常用)
BT_GATT_CHRC_WRITE_WITHOUT_RESP
:特征写入回复
BT_GATT_CHRC_WRITE
:可写属性
BT_GATT_CHRC_NOTIFY
:可通知属性(notify)
BT_GATT_CHRC_INDICATE
:需要从机对数据确认的属性
BT_GATT_CHRC_AUTH
:连接认证
BT_GATT_CHRC_EXT_PROP
:扩展的属性
权限
特征定义好了属性之后,还需要给权限,读或者写?还是其他的权限,GATT库也给我们声明好了:
BT_GATT_PERM_NONE
:无权限,只有notify 支持
BT_GATT_PERM_READ
: 可读权限
BT_GATT_PERM_WRITE
:可写权限
BT_GATT_PERM_READ_ENCRYPT
:加密连接的可读权限
BT_GATT_PERM_WRITE_ENCRYPT
:加密连接的可写权限
BT_GATT_PERM_READ_AUTHEN
:需要验证的可读权限
BT_GATT_PERM_WRITE_AUTHEN
:需要验证的可写权限
BT_GATT_PERM_PREPARE_WRITE
:需要准备的可写权限
在这里,我们可以得知服务与特征的一些关系:
服务创建的实践
开发流程
从图中我们可以看到,UUID 作为标识符,用来识别了服务和特征,在编程当中,一个服务和特征的创建都要先从UUID开始,教程也是一样,先定义了三个UUID:9011(服务的UUID),9012(特征1的UUID),9013(特征2)的UUID,流程就这么出来了:
st=>start: 开始
op=>operation: 创建UUID
op1=>operation: 声明服务的UUID
op2=>operation: 声明特征1的UUID,属性,特征
op3=>operation: 声明特征2的UUID,属性,特征
op4=>operation: 创建服务接口
op5=>operation: 注册服务
e=>end: 结束
st->op->op1->op2->op3->op4->op5->e
创建UUID
创建UUID可以使用宏定义来创建:
#define SERVER_UUID_16BIT BT_UUID_DECLARE_16(0X9011) //服务器UUID
#define NOTIFY_UUID_16BIT BT_UUID_DECLARE_16(0X9012) //特征1 UUID
#define WRITER_UUID_16BIT BT_UUID_DECLARE_16(0X9013) //特征2 UUID
声明服务和特征
声明服务和特征有一个专门的结构体,struct bt_gatt_attr
,结构体里只能创建一个服务,但是能过声明多个特征,所以当创建一个服务及其特征的时候,通常以创建 struct bt_gatt_attr
结构数组来实现,例如:
/* 定义一个服务,这个服务有两个特性 */
static struct bt_gatt_attr salve_uuid_server[] = {
/* 服务 UUID */
BT_GATT_PRIMARY_SERVICE(SERVER_UUID_16BIT),
/* 可读可通知的特征 */
BT_GATT_CHARACTERISTIC(NOTIFY_UUID_16BIT,BT_GATT_CHRC_NOTIFY,BT_GATT_PERM_READ,NULL,NULL,NULL),
/* 可读可写属性的特征 */
BT_GATT_CHARACTERISTIC(WRITER_UUID_16BIT,BT_GATT_CHRC_WRITE,BT_GATT_PERM_READ|BT_GATT_PERM_WRITE,NULL,NULL,NULL),
};
注册服务接口
struct bt_gatt_attr
结构体不能直接进行服务注册,需要创建 bt_gatt_service ble_uuid_server
结构体,并指向 struct bt_gatt_attr
之后才能使用注册,例如:
/* 创建 BLE 服务接口*/
static struct bt_gatt_service ble_uuid_server = BT_GATT_SERVICE(salve_uuid_server);
然后使用 bt_gatt_service_register
函数进行一个服务的注册,这个注册函数放在广播之前调用,我们在蓝牙启动之后,没进入广播态之后调用:
static void ble_stack_start(void)
{
// Initialize BLE controller
btble_controller_init(configMAX_PRIORITIES - 1);
// Initialize BLE Host stack
hci_driver_init();
bt_enable(bt_enable_cb);
bt_conn_cb_register(&conn_callbacks);
conn_callbacks._next = NULL;
/* 注册BLE 服务*/
int ret = bt_gatt_service_register(&ble_uuid_server);
}
实践结果
五、数据收发
接收数据
在视频教程的第6节,鹏老师提到BLE设备想要收到手机的数据,需要写一个“被写”的回调函数,也就是接收回调,在回调函数里打印手机发送来的数据。这个函数只需要在具有可写属性的特征里声明即可,根据上一节的定义,特则2(UUID:9013)就是一个可写特征,这个接收回调函数是放在特征2里:
static ssize_t ble_uuid_write_val(struct bt_conn* conn, const struct bt_gatt_attr* attr, const void* buf, u16_t len, u16_t offset, u8_t flags)
{
uint8_t* recv_buffer;
recv_buffer = pvPortMalloc(sizeof(uint8_t) * len);
memcpy(recv_buffer, buf, len);
LOG_RD("recv ble data len= %d:%s -----", len, recv_buffer);
for (size_t i = 0; i < len; i++)
{
LOG_RI("0x%x ", recv_buffer[i]);
}
printf("\r\n");
vPortFree(recv_buffer);
return len;
}
/* 定义一个服务,这个服务有两个特性 */
static struct bt_gatt_attr salve_uuid_server[] = {
/* 服务 UUID */
BT_GATT_PRIMARY_SERVICE(SERVER_UUID_16BIT),
/* 可读可通知的特征 */
BT_GATT_CHARACTERISTIC(NOTIFY_UUID_16BIT,BT_GATT_CHRC_NOTIFY,BT_GATT_PERM_READ,NULL,NULL,NULL),
/* 可读可写属性的特征 并声明接收回调函数*/
BT_GATT_CHARACTERISTIC(WRITER_UUID_16BIT,BT_GATT_CHRC_WRITE_WITHOUT_RESP,BT_GATT_PERM_WRITE,NULL,ble_uuid_write_val,NULL),
};
实践结果
发送数据
在视频当中,BLE设备给手机发送数据是通过Notify通知手机读取,但是在BLE GATT当中,BLE 设备Notify 需要通过GATT_CCC 来报告Notify的状态,所以需要改一下 struct bt_gatt_attr
结构体,需要在具有“Notify”属性的特征之下增加 BT_GATT_CCC
:
/* 定义一个服务,这个服务有两个特性 */
static struct bt_gatt_attr salve_uuid_server[] = {
/* 服务 UUID */
BT_GATT_PRIMARY_SERVICE(SERVER_UUID_16BIT),
/* 可读可通知的特征 */
BT_GATT_CHARACTERISTIC(NOTIFY_UUID_16BIT,BT_GATT_CHRC_NOTIFY,BT_GATT_PERM_READ,NULL,NULL,NULL),
/*添加GATT_CCC*/
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
/* 可读可写属性的特征 并声明接收回调函数*/
BT_GATT_CHARACTERISTIC(WRITER_UUID_16BIT,BT_GATT_CHRC_WRITE,BT_GATT_PERM_WRITE,NULL,ble_uuid_write_val,NULL),
};
这时候,特征1 才能被监听,在NRF Connect App中可以看出区别:
调用Notify发送数据
在接收回调当中使用 bt_gatt_notify
函数给手机发送数据,当手机发送数据下来的时候就回复"Hello Master":
static ssize_t ble_uuid_write_val(struct bt_conn* conn, const struct bt_gatt_attr* attr, const void* buf, u16_t len, u16_t offset, u8_t flags)
{
uint8_t* recv_buffer;
recv_buffer = pvPortMalloc(sizeof(uint8_t) * len);
memcpy(recv_buffer, buf, len);
LOG_D("recv ble data len= %d:%s", len, recv_buffer);
for (size_t i = 0; i < len; i++)
{
LOG_RI("0x%x ", recv_buffer[i]);
}
printf("\r\n");
vPortFree(recv_buffer);
/* 给手机发送数据 */
bt_gatt_notify(conn, &salve_uuid_server[2], "Hello Master", strlen("Hello Master"));
return len;
}
实践结果
六、声明
我是跟着鹏老师说的原理及流程来开发Ai-M6x的BLE,再次之前对一些概念并不是很清楚,所以难免会有一些理解不到位的情况。
上面的五个小节在例程当中都分别由相应的源码,请运行在AiPi-Open-Kits的AiPi-aiThinkerClud分支,并且在 examples
使用git 指令获取demo 源码:
git clone https://gitee.com/seahi007/Ai-BLE_exmple.git
必看
如果觉得鹏老师讲得好,三连加关注吧