CH58x 主机对不同属性从机特征值的读写 ...... 矜辰所致
前言
按照前面博文讲解的流程,讲完了主机从机通信框架(GATT 应用框架),讲完了从机自定义服务,讲完了主机获取从设备服务特征值句柄, 获取到了句柄当然就是要数据读写了。
所以本文的内容就是说明一下主机如何进行数据读写的。
相关博文:
CH58x 主机获取从设备服务特征值句柄
沁恒微蓝牙 GATT 应用框架说明
CH585 蓝牙 示例工程 Central 全解析
CH58x/CH59x 系列芯片从机示例解析
沁恒微蓝牙从机添加服务和特征示例.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 基础说明](#一、 基础说明)
-
- [1.1 特征值的属性](#1.1 特征值的属性)
- [1.2 示例流程说明](#1.2 示例流程说明)
- [1.3 库函数说明](#1.3 库函数说明)
-
- [1.3.1 长数据和短数据区分](#1.3.1 长数据和短数据区分)
- [二、 新增不同属性读写测试](#二、 新增不同属性读写测试)
-
- [2.1 GATT_PROP_WRITE_NO_RSP](#2.1 GATT_PROP_WRITE_NO_RSP)
- [2.2 GATT_PROP_INDICATE](#2.2 GATT_PROP_INDICATE)
- [三、 补充说明](#三、 补充说明)
- 结语
一、 基础说明
我们之前说过,主机从机的数据交互,实际上就是对从机特征值的读写。而我们从机的特征值,具备不同的属性,主机针对不同属性的特征值,操作也会有不同。
1.1 特征值的属性
我们在从机示例,在文件 gattprofile.c 中特征值定义的时候通过 simpleProfileChar1Props 设置特征数的属性,我们通过跳转可以看到库中所有关于特征值属性的宏定义 ,我们这里直接用注释解释一下:
// GATT Characteristic Properties Bit Fields
//广播特征值(极少用)从机可广播该特征值的数值(无需主机连接)
#define GATT_PROP_BCAST 0x01 //!< Permits broadcasts of the Characteristic Value
//可读特征值(最常用) 主机可主动读取该特征值的当前数据
#define GATT_PROP_READ 0x02 //!< Permits reads of the Characteristic Value
//无响应写 主机向从机写数据,从机接收后不返回响应 速度快,功耗低
#define GATT_PROP_WRITE_NO_RSP 0x04 //!< Permits writes of the Characteristic Value without response
//带响应写 主机向从机写数据,从机接收后必须返回响应 可靠
#define GATT_PROP_WRITE 0x08 //!< Permits writes of the Characteristic Value with response
//通知(主动推送,无确认) 从机主动向主机推送特征值数据,主机接收后无需返回确认 ,需要主机写CCCD,0x0001
#define GATT_PROP_NOTIFY 0x10 //!< Permits notifications of a Characteristic Value without acknowledgement
//指示(主动推送,有确认) 从机主动向主机推送特征值数据,主机接收后必须返回确认 ,需要主机写CCCD,0x0002
#define GATT_PROP_INDICATE 0x20 //!< Permits indications of a Characteristic Value with acknowledgement
//认证写(需加密) 主机必须先和从机建立加密连接(绑定 / 配对),才能写该特征值
#define GATT_PROP_AUTHEN 0x40 //!< Permits signed writes to the Characteristic Value
// 扩展属性 该特征值有 "扩展属性"(如支持可靠写、广播等扩展功能) 需读取 "特征值扩展属性描述符(0x2900)" 才能知道具体扩展功能。
#define GATT_PROP_EXTENDED 0x80 //!< Additional characteristic properties are defined in the Characteristic Extended Properties Descriptor
1.2 示例流程说明
经过前面的几篇文章,大家应该对主机读写数据这个流程已经很熟悉了,本文再再再次简要说明一下。
主机读写数据请求 :
主机写数据是在获取到了句柄之后新建了两个任务事件:
-
if(events & START_READ_OR_WRITE_EVT)用来「写特征值」
使用
GATT_WriteCharValue函数用来「读特征值」
使用
GATT_ReadCharValue函数 -
if(events & START_WRITE_CCCD_EVT)用来「写CCCD」
也使用
GATT_WriteCharValue函数
主机接收的数据处理 :
数据是通过 TMOS 消息传递,最终在 centralProcessGATTMsg 函数中通过不同分分支处理:
-
pMsg->method == ATT_READ_RSP处理「读特征值」的结果
-
pMsg->method == ATT_WRITE_RSP处理「写特征值」的结果
-
pMsg->method == ATT_HANDLE_VALUE_NOTI接收「从机主动推送的 Notify(通知) 数据」
-
示例中没有处理「从机主动推送的 Indicate (通知) 数据」分支
也就是
pMsg->method == ATT_HANDLE_VALUE_NOTI分支,我们本文会添加测试一下
我们看一下官方示例测试的效果,我在读写请求的 TMOS 事件里面加了个打印方便测试 :

1.3 库函数说明
库函数中提供了多种不同的读写的函数,我们来看一下。
读操作(Read):
| 函数 | 场景 | 说明 |
|---|---|---|
GATT_ReadCharValue |
数据短(< MTU-1) | 知道特征值句柄即可,会返回ATT_READ_RSP响应 |
GATT_ReadLongCharValue |
数据很长 | 读取长特征值,分多次读取(Read Blob), 返回多个ATT_READ_BLOB_RSP响应 |
GATT_ReadMultiCharValues |
一次性读取多个特征值(均为短数据) | 一次请求,批量读取, 返回ATT_READ_MULTI_RSP响应 |
GATT_ReadCharDesc |
读描述符(短数据) | 读取特征的通知 / 指示配置状态 |
GATT_ReadLongCharDesc |
读描述符(长数据) | 读取超长的描述符内容(极少用) |
写操作(Write)
| 函数 | 场景 | 说明 |
|---|---|---|
GATT_WriteNoRsp |
不需要回应的写 | 无响应,最快,但不知道服务器是否收到 |
GATT_WriteCharValue |
需要确认服务器收到 | 有响应,适合关键数据 |
GATT_WriteLongCharValue |
数据超过 MTU-3 | 分批次写长特征值(Prepare+Execute Write),返回多个响应 |
GATT_ReliableWrites |
批量配置多个参数(如设备多模式参数) | 先批量准备多个写操作,再统一执行(原子操作), 确保所有写要么都成要么都败 |
GATT_SignedWriteNoRsp |
带签名的无确认写(仅未加密连接可用)) | 需安全但无需确认的控制操作 |
GATT_WriteCharDesc |
写描述符(短数据) | 配置特征的通知 / 指示功能 |
GATT_WriteLongCharDesc |
写描述符(长数据) | 超长描述符写入(极少用) |
1.3.1 长数据和短数据区分
上面函数分为长数据和短数据的读写,对于长数据和短数据是多少,它们并不是固定的,长 / 短的分界由当前协商好的 ATT_MTU 决定 :
| 类型 | 字节数 | 判定标准 |
|---|---|---|
| 短数据 | ≤ ATT_MTU - 3(写)/ ≤ ATT_MTU - 1(读) | 单次 ATT 请求能装下 |
| 长数据 | > ATT_MTU - 3(写)/ > ATT_MTU - 1(读) | 必须分片传输 |
比如:
| 场景 | 短数据定义 | 长数据定义 | 备注 |
|---|---|---|---|
| 默认 ATT_MTU=23 | ≤ 20 字节(写)/ ≤ 22 字节(读) | > 20 字节(写)/ > 22 字节(读) | 兼容 BLE 4.0 |
| 协商 ATT_MTU=158 | ≤ 155 字节(写)/ ≤ 157 字节(读) | > 155 字节(写)/ > 157 字节(读) | 常用优化值 |
| CH585 最大 ATT_MTU=517 | ≤ 512 字节(写)/ ≤ 512 字节(读) | > 512 字节 | ATT 硬上限优先 512是个分界线 |
默认 MTU = 23 时:
读:> 22 字节 = 长数据
写:> 20 字节 = 长数据
MTU = 517 时:
读:> 512 字节 = 长数据(受 ATT 硬上限限制,不是 516)
写:> 512 字节 = 长数据(受 ATT 硬上限限制,不是 514)
通用规则:
无论 MTU 多大,> 512 字节必须用 Long 函数
这里几个数据要搞清楚:
1、单次 ATT 包传输的属性值最大 512 字节;
2、GATT 长读写函数(Read Long/Write Long)通过分块偏移,可访问理论上限 65536 字节的特征值(工程中常用 ≤ 512 字节,长数据函数就是为了突破 512 字节存在的);
3、BLE 4.2+ /5.0 ATT MTU 最大上限是 517 字节;
4、BLE 4.0/4.1 ATT_MTU 最大23 字节;
5、一般工程中为了兼容性,默认 ATT_MTU 都为 23字节;
6、CH585 一次传输 最大支持247 字节 MTU(有效数据 244 字节);
二、 新增不同属性读写测试
官方示例是读写包含了3个特征是属性:
GATT_PROP_READ 、GATT_PROP_WRITE 和 GATT_PROP_NOTIFY 。
我们本文再测试 一下 GATT_PROP_WRITE_NO_RSP 和 GATT_PROP_INDICATE 。
2.1 GATT_PROP_WRITE_NO_RSP
我们把在从机示例改一个特征值 属性为 GATT_PROP_WRITE_NO_RSP ,然后接收数据回调函数改一下方便查看:

我们在主机示例中,也修改一下给这个特征值写数据,这里因为我们能够算出来 CHAR3 的句柄,我们就不再去额外写 获取 CHAR 3 的句柄的程序了,我们直接在示例获取完 CCCD 后面增加一个自己的测试任务,给CHAR3 写数据:

然后在事件中写特征值,使用GATT_WriteNoRsp 函数,不需要响应参数就不需要任务ID,这里直接放代码:
c
static uint8_t mytestCharVal = 0x11;
...
if(events & START_MY_RWTEST_EVT)
{
// Do a write
attWriteReq_t req;
req.cmd = FALSE;
req.sig = FALSE;
req.handle = centralCharHdl + 5; //这里是因为根据char1 算的 char3 ,测试知道自己要写那个特征值,仅供测试
req.len = 1;
req.pValue = GATT_bm_alloc(centralConnHandle, ATT_WRITE_REQ, req.len, NULL, 0);
if(req.pValue != NULL)
{
*req.pValue = mytestCharVal++;
PRINT("准备写的句柄:%d\n", req.handle);
if(GATT_WriteNoRsp(centralConnHandle, &req) == SUCCESS)
{
tmos_start_task(centralTaskId, START_MY_RWTEST_EVT, 1600);
}
else
{
GATT_bm_free((gattMsg_t *)&req, ATT_WRITE_REQ);
}
}
return (events ^ START_MY_RWTEST_EVT);
}
测试效果如下:

2.2 GATT_PROP_INDICATE
再来看一下带 indicate 属性,这里因为测试,并没有去设置标志位防止一些请求冲突,我直接把例程中写 CCCD 的地方,改成写带有 indicate 属性特征值的 CCCD,使用的从机示例就是之前博文《沁恒微蓝牙从机添加服务和特征示例》 我们自己添加服务和特征值的示例 。
这里主要需要注意的地方:一个是写对 CCCD 的句柄,第二个是 notify写 0x0001 ,indicate写 0x0002 。
然后我们使用 GATT_WriteCharDesc 写入,代码如下:
c
if(events & START_WRITE_CCCD_EVT)
{
if(centralProcedureInProgress == FALSE)
{
// Do a write
attWriteReq_t req;
req.cmd = FALSE;
req.sig = FALSE;
req.handle = centralCCCDHdl + 7; //这里也是硬算的,仅供测试
req.len = 2;
req.pValue = GATT_bm_alloc(centralConnHandle, ATT_WRITE_REQ, req.len, NULL, 0);
if(req.pValue != NULL)
{
req.pValue[0] = 2; // notify写1 ,indicate写2
req.pValue[1] = 0;
PRINT("写CCCD的句柄:%d\n", centralCCCDHdl + 7);
if(GATT_WriteCharDesc(centralConnHandle, &req, centralTaskId) == SUCCESS)
{
centralProcedureInProgress = TRUE;
}
else
{
GATT_bm_free((gattMsg_t *)&req, ATT_WRITE_REQ);
}
}
}
return (events ^ START_WRITE_CCCD_EVT);
}
写完以后和 notify 一样需要到 centralProcessGATTMsg 里面处理,我们需要添加一个分支,而且我们需要注意一下要自己确认响应,看代码:
c
else if(pMsg->method == ATT_HANDLE_VALUE_NOTI)
{
PRINT("Receive noti: %x\n", *pMsg->msg.handleValueNoti.pValue);
}
else if(pMsg->method == ATT_HANDLE_VALUE_IND)
{
PRINT("my test Receive ind: ");
for(uint8_t i=0;i < pMsg->msg.handleValueInd.len;i++){
PRINT("%02x",pMsg->msg.handleValueInd.pValue[i]);
}
PRINT("\n");
//还要调用函数发送确认响应
bStatus_t Ind_test_state = ATT_HandleValueCfm(pMsg->connHandle);
PRINT("Ind_test_state:= %d \r\n ",Ind_test_state);
}
最后我们看看测试效果:

三、 补充说明
示例中写 CCCD 使用的是 GATT_WriteCharValue 而不是 GATT_WriteCharDesc ,实际上在 CH585 的库里面,这两个函数是一样的,不管是写 CCCD ,还是写特征值,两个函数效果一样。想想也应该一样,本质上是一样的,都是传入的需要写入的句柄,数值,和传递消息的任务 ID 。
结语
本文说明测试了一下主机读写不同属性从机特征值的操作,相对来说本文还是比较简单的。
官方示例和本文测试的读写,基本足够满足正常应用的需求了,对于文中介绍的库函数中的长数据读写函数示例,有机会使用到了再来说明吧。
好了,本文就到这里。谢谢大家!