CH58x 主机获取从机服务特征句柄说明 ...... 矜辰所致
前言
前面我们分析过主机从机示例,讲过 GATT 应用框架,也讲过从机作为 GATT 服务器的一些服务特征值添加,当然GATT 部分最重要的还是主机从机之间的数据交互流程,在说明从机示例的时候,我们常用手机作为 GATT 客户端进行测试说明。我们还没有在主机示例上对此部分进行针对性的说明。
主机要与从机进行数据交互,连接上以后首先是要能够知道自己要操作的特征值是哪一个。
所以本文的内容就是说明主机在数据交互的过程中如何定位要操作的服务和特征值。
相关博文:
沁恒微蓝牙 GATT 应用框架说明
CH585 蓝牙 示例工程 Central 全解析
CH58x/CH59x 系列芯片从机示例解析
沁恒微蓝牙从机添加服务和特征示例.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- 一、基础说明
-
- [1.1 主机如何区分不同的特征值](#1.1 主机如何区分不同的特征值)
- [1.2 句柄顺序说明](#1.2 句柄顺序说明)
-
- [1.2.1 BLE 调试工具句柄说明](#1.2.1 BLE 调试工具句柄说明)
- 二、主机示例获取句柄流程
-
- [2.1 官方示例流程](#2.1 官方示例流程)
- [2.2 TMOS + 蓝牙协议栈的交互规则](#2.2 TMOS + 蓝牙协议栈的交互规则)
- [2.3 几个状态标志](#2.3 几个状态标志)
- [2.4 示例测试效果](#2.4 示例测试效果)
- 三、库函数提供的获取句柄函数
-
- [3.1 库函数说明](#3.1 库函数说明)
- [3.1 用法示例](#3.1 用法示例)
-
- [3.1.1 获取所有特征值的句柄](#3.1.1 获取所有特征值的句柄)
- 结语
一、基础说明
1.1 主机如何区分不同的特征值
首先必须要知道的一个问题:
蓝牙主机作为 GATT 客户端在连接上 作为 GATT 服务端的 从机时,面对那么多不同服务,不同的特征值,他是如何区分定位目标的 ?
答案是: 句柄!!!
蓝牙主机 和 从机建立连接后,从机的 GATT 表中,每个服务、特征值声明项、特征值数值项、描述符(包括 CCCD / 用户描述项) 都会被分配一个唯一的 16 位句柄(Handle),主机所有对 GATT 层的操作(读 / 写特征值、控制 CCCD、接收 Notify/Indicate)都是通过句柄来进行的。
哪怕是 "只读 / 只写 / 不可操作" 的属性,也有句柄。
1.2 句柄顺序说明
下图是一个典型的 GATT 句柄对应示例:

从最开始的 服务 0x1800 开始,最开始句柄就为 0x0001,依次递增 1。
每一个申明项都会占用一个句柄,从机 gattprofile.c 文件中每一个 simpleProfileAttrTb[] 成员都会占用一个句柄。
这个理解起来可以结合文章《沁恒微蓝牙从机添加服务和特征示例》服务和特征值的定义说明
如下图所示:

对于句柄顺序,有几点需要说明:
1、句柄位置取决于 ATT 数据库的初始化顺序 ,对于开发者我们上层来看,句柄的顺序就等于我们 "初始化 GATT 属性的顺序", 蓝牙协议规范不强求这个顺序。只是通常情况,我们有一套惯例,比如服务 0x1800 (GAP Service) 是所有蓝牙从机必备的标准服务,通常情况下放在最前面。
但是我们也可以任意放置,比如我把自定义的服务先注册,如下图:

通过 PC 端的 BLE 调试工具可以看到句柄:

1.2.1 BLE 调试工具句柄说明
插入一个软件说明:上图是使用沁恒微 PC 端的软件 BLEDebug 连接后读取到的句柄说明(在沁恒微官网搜索 BLEDebug 可以下载)。
我们用一张图来说明里面对句柄的描述:

回到我们要说的句柄顺序继续说明。
2、上面我们知道了服务的排列顺序:无强制要求,0x2800(服务声明)可以放在自定义服务后面,完全由从机代码决定;
对于特征值来说:声明 + 数值项强制连续,0x2803 后面必须紧跟数值项,这是蓝牙规范的硬性要求;
所以如果一个特征值的声明项句柄为 a ,那么句柄为 a+1 的必定为此特征值的数据项。
那我们还知道特征值可以带描述符项,也可以不带描述符,描述符还可以有多个描述符,描述符的顺序也没有要求,只要它们都归属在当前特征值的范围内,可以任意顺序排列,所以我们的从某个特征值开始的句柄可以有下面这些情况。
不带描述符:
| 属性类型 | 句柄 | UUID | 说明 |
|---|---|---|---|
| 特征值声明 | 句柄 a | 0x2803 | Characteristic Declaration |
| 数值项(特征值本身) | 句柄 a+1 | 0xFFE1(自定义) | 实际的特征值数据 |
带一个描述符:
| 属性类型 | 句柄 | UUID | 说明 |
|---|---|---|---|
| 特征值声明 | 句柄 a | 0x2803 | Characteristic Declaration |
| 数值项(特征值本身) | 句柄 a+1 | 0xFFE1(自定义) | 实际的特征值数据 |
| 用户描述符 | 句柄 a+2 | 0x2901 | User Description |
带两个描述符:
其中一个为 CCCD,CCCD在后面
| 属性类型 | 句柄 | UUID | 说明 |
|---|---|---|---|
| 特征值声明 | 句柄 a | 0x2803 | Characteristic Declaration |
| 数值项(特征值本身) | 句柄 a+1 | 0xFFE1(自定义) | 实际的特征值数据 |
| 用户描述符 | 句柄 a+2 | 0x2901 | User Description |
| CCCD | 句柄 a+3 | 0x2902 | Client Characteristic Configuration |
带两个描述符:
其中一个为 CCCD,CCCD在前面
| 属性类型 | 句柄 | UUID | 说明 |
|---|---|---|---|
| 特征值声明 | 句柄 a | 0x2803 | Characteristic Declaration |
| 数值项(特征值本身) | 句柄 a+1 | 0xFFE1(自定义) | 实际的特征值数据 |
| CCCD | 句柄 a+2 | 0x2902 | Client Characteristic Configuration |
| 用户描述符 | 句柄 a+3 | 0x2901 | User Description |
好了,讲了这么多,都是为了让大家理解这个句柄会怎么排列,除了 特征值声明 + 数值项 强制连续,其他的顺序都没有强制要求,这也告诉我们 在开发的时候,不要硬编码句柄,记得动态获取句柄才是唯一可靠的方式。 当然,调式时候可以使用硬编码测试。
二、主机示例获取句柄流程
2.1 官方示例流程
主机整体流程在之前 《 CH585 蓝牙 示例工程 Central 全解析 》 博文中有过初步的讲解,这里我们再过一遍本文关注的部分。
按照官方主机例程 Central 的代码来说明:
-
在 GAP 事件回调函数
centralEventCB(gapRoleEvent_t *pEvent); -
当建立连接后进入
case GAP_LINK_ESTABLISHED_EVENT:事件分支; -
开启了一个 TMOS 任务
tmos_start_task(centralTaskId, START_SVC_DISCOVERY_EVT, DEFAULT_SVC_DISCOVERY_DELAY); -
在这个 TMOS 任务中调用了
centralStartDiscovery();函数。 -
这个函数就是应用层的服务发现处理函数,在此函数中,调用了库函数
GATT_DiscPrimaryServiceByUUID用来发现 UUID 为 0xFFE0 的服务的句柄。 -
上面调用了发现函数以后,后续从机返回的响应,会由协议栈自动推送 GATT_MSG_EVENT 到 TMOS 任务的。
-
所以接下来就等待从机返回响应通过 TMOS 任务中的消息处理事件处理:
Central_ProcessEvent--->
if(events & SYS_EVENT_MSG)--->
central_ProcessTMOSMsg((tmos_event_hdr_t *)pMsg);--->
centralProcessGATTMsg((gattMsgEvent_t *)pMsg); -
函数
centralProcessGATTMsg就是主机 GATT 消息核心处理函数,里面处理 MTU/读/写/Notify 等GATT 的操作,在此函数最后,当处于发现服务状态的时候,会进入centralGATTDiscoveryEvent(pMsg);函数。 -
最后在
centralGATTDiscoveryEvent(pMsg);函数中,根据不同的状态,会先进行找服务,如果服务找到了,会找特征值,如果特征值找到了,会找 CCCD。所以说如果是使用官方主机从机示例测试,这个
centralGATTDiscoveryEvent(pMsg);会进入3次,依次执行找到服务,找特征值(GATT_ReadUsingCharUUID),找CCCD(GATT_ReadUsingCharUUID)。全部找完才会把状态切换回
BLE_DISC_STATE_IDLE。 -
最后又通过 TMOS 任务消息传递回到函数
centralProcessGATTMsg进行数据交互。
以上就是示例中主机找句柄的流程。如果想要完全熟悉这个流程,可能是需要多看几遍代码,对于新手来说难点可能在于交互逻辑 和 一些状态的区分,为了让大家更容易理解这个流程,下面有几个点特别说明一下。
2.2 TMOS + 蓝牙协议栈的交互规则
首先要明确知道一点:
TMOS + 蓝牙协议栈的交互本质是 "异步事件驱动"。TMOS 是协议栈和应用层之间的唯一 "消息中转站" 。
TMOS + 蓝牙协议栈的核心规则如下表格:
| 规则 | 解释 | 典型操作 | 返回值/通知方式 |
|---|---|---|---|
| 规则1 | GATT 层所有需从机响应的操作→ 走TMOS消息异步返回 | • 发现服务/特征值 • 读/写特征值 • 写CCCD使能Notify • 交换MTU | bStatus_t = SUCCESS(请求已发送) 结果通过 GATT_MSG_EVENT 回调 |
| 规则2 | 所有纯本地操作(不涉及从机)→ 直接同步返回 | • 获取连接状态 • 获取本地地址 • 注册GATT服务(从机端) • 设置扫描参数 | bStatus_t = 实际结果 无TMOS消息 |
所有需要和从机交互的 GATT 消息(请求 / 响应)都是通过 TMOS 传递的;
对于我们应用来说,不用关心 TMOS 底层怎么传消息,只需要知道 "哪些操作会触发 TMOS 事件 ",协议栈已封装好所有 TMOS 消息逻辑 ,我们只需要 "收消息、处理消息" 。
有个简单的判断方法,CH585 蓝牙库中,需要走 TMOS 的 GATT 函数,都会带有 taskId 参数。
TMOS + 协议栈的交互流程示意图如下:

既然说了 GATT 层,再把容易混淆的 GAP 层额外再提一下,这个在我之前的博文:《CH58x/CH59x 系列芯片从机示例解析》 中的第四节 ---消息传递 有过一些说明:

对于主机来说,也是一样的:
GAP 负责 "蓝牙连接层面" 的操作 扫描、连接、断开、广播、配对、连接参数更新 ;
GATT 负责 "连接后的数据交互" 找服务 / 特征值、读 / 写数据、Notify/Indication、MTU 协商;
在主机示例中,GAP 消息不走 centralProcessGATTMsg,而是直接进入 centralEventCB 回调函数 ,这是协议栈预设的 GAP 事件处理入口:

OK,提一下,回到我们主机找服务的话题上来 。
2.3 几个状态标志
了解了 TMOS 的交互流程,我们应该更容易的理解了主机获取句柄流程,在上面流程的函数中,有 2 个状态标志:
c
// Application state 蓝牙连接状态
static uint8_t centralState = BLE_STATE_IDLE;
// Discovery state GATT 发现状态
static uint8_t centralDiscState = BLE_DISC_STATE_IDLE;
这两个状态时应用层自己定义,用来管理自己代码逻辑的,不难理解,但是也需要搞清楚,不要和协议栈返回的状态什么搞混淆了。这里我直接通过代码注释说明一下即可:
c
// Application states 管理主机连接状态
enum
{
BLE_STATE_IDLE, //空闲态,上电无连接,断开连接后,
BLE_STATE_CONNECTING, // 主机发起连接请求后,用来防止重复发起连接或者其他操作干扰连接
BLE_STATE_CONNECTED, // 连接状态,centralEventCB 收到 GAP_LINK_ESTABLISHED_EVENT 且为 SUCCESS,GATT操作前置条件,允许发现流程,读写操作
BLE_STATE_DISCONNECTING // 正在断开种,主机发起断开请求后。
};
// Discovery states 连接后的服务发现流程
enum
{
BLE_DISC_STATE_IDLE, // 发现空闲态,初始状态吗,发现流程完成后,断开连接后。
BLE_DISC_STATE_SVC, // 正在找服务,调用 centralStartDiscovery() 时设置,centralGATTDiscoveryEvent 中走 "解析服务句柄" 分支;
BLE_DISC_STATE_CHAR, // 正在找特征值,解析完服务句柄后设置。centralGATTDiscoveryEvent中走 "解析特征值句柄" 分支;
BLE_DISC_STATE_CCCD // 找CCCD,解析完特征值句柄后设置,centralGATTDiscoveryEvent中走 "CCCD" 分支;
};
/*********************************************************************
除了2个状态标志,还有一个主机连接句柄变量centralConnHandle , 它是蓝牙协议栈为 "当前主机与从机的连接" 分配的唯一数字标识,协议栈给这条 "蓝牙连接链路" 发的一个身份证号。
代码中定义初始值为:
GAP_CONNHANDLE_INIT (0xFFFE):
是协议栈定义的 "无效连接句柄",表示当前主机没有和任何从机建立连接;
还有一个宏定义
GAP_CONNHANDLE_ALL (0xFFFF):
比如
表示 "所有连接"(比如断开所有连接时用);
有效连接句柄范围:
0x0000 ~ 0xFFFD。
centralConnHandle 在主机建立连接以后会赋值:

然后在连接期间,这个连接上的通信(所有和从机进行数据交互的功能) 函数都能见到这个参数,比如:
c
GATT_ExchangeMTU(centralConnHandle, &req, centralTaskId);
// Discovery simple BLE service
GATT_DiscPrimaryServiceByUUID(centralConnHandle,
uuid,
ATT_BT_UUID_SIZE,
centralTaskId);
GATT_ReadUsingCharUUID(centralConnHandle, &req, centralTaskId);
if(GATT_WriteCharValue(centralConnHandle, &req, centralTaskId) == SUCCESS)
if(GATT_ReadCharValue(centralConnHandle, &req, centralTaskId) == SUCCESS)
一件断开所有连接示例:
c
// 一键断开所有连接
if(centralState == BLE_STATE_CONNECTED || centralState == BLE_STATE_CONNECTING) {
bStatus_t status = GAPRole_TerminateLink(GAP_CONNHANDLE_ALL);
if(status == SUCCESS) {
PRINT("发起断开所有连接请求...\n");
} else {
PRINT("断开所有连接请求失败:%d\n", status);
}
}
如果是多连接主机的示例,那么这个地方肯定会以数组的形式保存这个连接句柄。
2.4 示例测试效果
示例测试效果如下:

只要是从上面往下看的,那么这个结果分析也不是难事,这里需要说明,最后的范围到 0xFFFF 表示这是最后一个服务,是蓝牙协议正常的规范 。
比如,我们可以换一个前面的服务读取测试一下:

三、库函数提供的获取句柄函数
3.1 库函数说明
官方示例流程是一种标准的方式,在官方提供的蓝牙库中,有好几个获取服务的函数,这里给个表格说明一下:
| 函数 | 效果 | 最终能得到的结果 |
|---|---|---|
GATT_DiscPrimaryServiceByUUID |
已知服务 UUID,快速定位其 Handle 范围。 | 服务起始 Handle + 结束 Handle |
GATT_DiscAllPrimaryServices |
找从机所有主服务 | 所有服务的 Handle 范围 + UUID |
GATT_FindIncludedServices |
找服务中的嵌套子服务(极少用) | 子服务的 UUID + 句柄 |
GATT_DiscAllChars |
找指定服务内的所有特征值 | 服务内每个特征值的: 1.特征值声明句柄 2.特征值数值项句柄(核心,用于读写) 3.特征值属性(读 / 写 / Notify) |
GATT_DiscCharsByUUID |
找指定服务内指定 UUID 的特征值 只能找特征值,不能找描述符 | 目标特征值的数值项句柄(charHdl) 和属性(读写 / Notify ) |
GATT_ReadUsingCharUUID |
按 UUID 读取特征值或描述符, 顺带获取其 Handle 既能找特征值,也能找 CCCD | 目标属性的 Handle + 当前数据 |
GATT_DiscAllCharDescs |
找特征值的所有描述符 | 每个描述符的句柄 + 描述符类型 (如 0x2902=CCCD) |
一般来说,做产品建议就是按照官方示例的框架进行设计:
GATT_DiscPrimaryServiceByUUID(找服务)→ GATT_ReadUsingCharUUID(找特征值)→ GATT_ReadUsingCharUUID(找 CCCD);
3.1 用法示例
本小节当作补充章节,随时更新,实际中使用到的时候会来更新。
3.1.1 获取所有特征值的句柄
此小节代码来自博客:
https://www.cnblogs.com/gscw/p/17846232.html
代码如下(为了方便查阅还是放了一下,是来自大佬的原创,只有结尾处针对官方示例进行了细微修改,替换原来主机示例中的 2个函数即可):
c
/*参考博客
https://www.cnblogs.com/gscw/p/17846232.html
*/
static void centralStartDiscovery(void)
{
centralSvcStartHdl = centralSvcEndHdl = centralCharHdl = 0;
centralDiscState = BLE_DISC_STATE_SVC;
uint8_t ret = 0;
ret = GATT_DiscAllPrimaryServices(centralConnHandle, centralTaskId);
printf("GATT_DiscAllPrimaryServices ret = %x\n", ret); //这是返回的0 表示 SUCCESS,上面的请求成功发送
}
static void centralGATTDiscoveryEvent(gattMsgEvent_t *pMsg)
{
attReadByTypeReq_t req;
if(centralDiscState == BLE_DISC_STATE_SVC)
{
if((pMsg->method == ATT_READ_BY_GRP_TYPE_RSP &&
pMsg->hdr.status == bleProcedureComplete) ||
(pMsg->method == ATT_ERROR_RSP))
{
// Discover characteristic
centralDiscState = BLE_DISC_STATE_CHAR;
uint8_t ret = GATT_DiscAllChars(centralConnHandle,0x01,0xFFFF,centralTaskId);
PRINT("GATT_DiscAllChars:%02x\r\n",ret); //这是返回的0 表示 SUCCESS,上面的请求成功发送
}
}
else if(centralDiscState == BLE_DISC_STATE_CHAR)
{
// Characteristic found, store handle
if(pMsg->method == ATT_READ_BY_TYPE_RSP &&
pMsg->msg.readByTypeRsp.numPairs > 0)
{
for(unsigned char i = 0; i < pMsg->msg.readByTypeRsp.numPairs ; i++) {
//characteristic properties
uint8_t char_properties = pMsg->msg.readByTypeRsp.pDataList[pMsg->msg.readByTypeRsp.len * i + 2];
uint16_t char_value_handle = BUILD_UINT16(pMsg->msg.readByTypeRsp.pDataList[pMsg->msg.readByTypeRsp.len * i+3], \
pMsg->msg.readByTypeRsp.pDataList[pMsg->msg.readByTypeRsp.len * i + 4]);
//characteristic uuid length
uint8_t char_uuid_length = pMsg->msg.readByGrpTypeRsp.len - 5;
//uuid
uint8_t *char_uuid = &(pMsg->msg.readByGrpTypeRsp.pDataList[pMsg->msg.readByGrpTypeRsp.len * i + 5]);
PRINT("______________________________\n");
PRINT("char_uuid :");
for(uint8_t i = 0; i < char_uuid_length; i++ ){
printf("%02x ", char_uuid[i]);
}printf("\n");
// PRINT("char_properties :%02x,%s\r\n",char_properties,(char_properties&(GATT_PROP_WRITE|GATT_PROP_WRITE_NO_RSP))?"wite handle":"");
PRINT("char_value_handle:%04x\r\n",char_value_handle);
PRINT("char_uuid :%02d bit\r\n",char_uuid_length);
if(char_properties&(GATT_PROP_WRITE|GATT_PROP_WRITE_NO_RSP)) {
PRINT("Write handle:%d\r\n",char_value_handle);
// tmos_start_task(centralTaskId, START_READ_OR_WRITE_EVT, 1600);
}
if(char_properties&(GATT_PROP_READ)) {
PRINT("read handle:%d\r\n",char_value_handle);
}
if(char_properties&GATT_PROP_NOTIFY) {
// centralCCCDHdl = char_value_handle+1; //通过GATT_DiscAllChars或者handle,noti/indi的handle值需要+1
PRINT("Notify handle:%d\r\n",char_value_handle+1);
// tmos_start_task(centralTaskId, START_WRITE_CCCD_EVT, 1600);
}
if(char_properties&GATT_PROP_INDICATE) {
// centralCCCDHdl = char_value_handle+1; //通过GATT_DiscAllChars或者handle,noti/indi的handle值需要+1
PRINT("Indicate handle:%d\r\n",char_value_handle+1);
// tmos_start_task(centralTaskId, START_WRITE_CCCD_EVT, 800);
}
/*
这里测试只是为了满足正常的主机流程能够正常走下去,要不然上面获取所有的服务会出现写句柄的问题
这里是为了测试的硬编码,知道从机哪几个UUID 具备什么特征值
仅供测试,实际需要优化处理方式,本示例可以在测试时候打印所有 UUID
*/
if(char_uuid[0] == LO_UINT16(SIMPLEPROFILE_CHAR1_UUID) && char_uuid[1] == HI_UINT16(SIMPLEPROFILE_CHAR1_UUID)){
centralCharHdl = char_value_handle;
PRINT("设置要写的句柄为: %d\r\n",centralCharVal);
tmos_start_task(centralTaskId, START_READ_OR_WRITE_EVT, DEFAULT_READ_OR_WRITE_DELAY);
}
if(char_uuid[0] == LO_UINT16(SIMPLEPROFILE_CHAR4_UUID) && char_uuid[1] == HI_UINT16(SIMPLEPROFILE_CHAR4_UUID)){
centralCCCDHdl = char_value_handle+1;
PRINT("设置要写的CCCD为: %d\r\n",centralCCCDHdl);
tmos_start_task(centralTaskId, START_WRITE_CCCD_EVT, DEFAULT_WRITE_CCCD_DELAY);
}
}
}
}
}
上面代码中 CCCD 的句柄为 char_value_handle+1, 这个大家通过上面的学习应该知道为什么了,这里结尾再通过文章开头的图说明一下 :

使用上面这种方式, 128 bit 的 UUID 的特征值的句柄也能正常获取,使用上面方式连接我们上一篇博文 《沁恒微蓝牙从机添加服务和特征示例》 添加的特征值的效果如下:

结语
本文我们介绍了主机与从机连接后的句柄为何物,详细的介绍了 CH58x 主机获取从机服务特征值句柄的流程和方法,也说明了如何查看区分的句柄,希望通过本文的学习,大家以后遇见句柄处理时得心应手 。
另外,在实际开发过程中,建议大家还是使用官方示例的流程进行获取服务和特征值句柄,然后再进行正常通信,这是最简便规范的一种方式。
好了,本文就到这里。谢谢大家!