蓝牙BLE从机Peripheral讲解二(句柄指示\确认(Handle Value Indication\Confirmation))
前言:
CH583 是集成 BLE 无线通讯的 RISC-V MCU 微控制器。一般在使用BLE协议进行数据传输,会优先考虑Peripheral(外设从机角色例程)。在CH582的SDK中,自定义包含五种不同属性的服务,包含可读、可写、通知、可读可写、安全可读,唯独没有indication属性的特征值。本篇博客针对Indication使用进行讲解。
一、indication属性原理
当Server想要Client发送快速的属性状态更新时,它可以发送一条句柄值通知(Notification)。这是Server能够发给Client的两种消息中的一种,并且是不要求响应的。Server可以在任何时刻发送该Notification,同时该Notification也是不可靠的。
句柄值指示(Indication)类似于Notification,它有着相同的属性句柄字段和数值,不同的是Client收到Indication以后需要回复。Server一次只能发送一条Indication,并且只有在收到确认响应(Confirmation)后才能发起下一条Indication。这些行为都是在GATT层完成。由于有自动确认机制,Indication在很多应用层级的协议制定中有比较广泛应用。
二、CH58x芯片Indication的实现
由于Peripheral例程中已包含了Notification的功能,通道4进行Noti数据传输。本篇博客基于该功能进行修改,对以前只接触过Notification的小白来说更加通俗易懂。
1、特征值4属性修改,给Characteristic 的属性添加indication 属性
static uint8_t simpleProfileChar4Props = GATT_PROP_INDICATE;
按照如上修改配置文件属性后,对应的属性表也配置成功了,不需要再进行修改,默认的Noti已经配置好属性表
// Characteristic Value 4 { {ATT_BT_UUID_SIZE, simpleProfilechar4UUID}, 0, 0, simpleProfileChar4},
2、配置文件修改,在peripheral.c文件夹中基于Notification进行修改
static void peripheralChar4Notify(uint8_t *pValue, uint16_t len) { #if 0 //原始程序是Noti attHandleValueNoti_t noti; if(len > (peripheralMTU - 3)) { PRINT("Too large noti\n"); return; } noti.len = len; noti.pValue = GATT_bm_alloc(peripheralConnList.connHandle, ATT_HANDLE_VALUE_NOTI, noti.len, NULL, 0); if(noti.pValue) { tmos_memcpy(noti.pValue, pValue, noti.len); if(simpleProfile_Notify(peripheralConnList.connHandle, ¬i) != SUCCESS) { GATT_bm_free((gattMsg_t *)¬i, ATT_HANDLE_VALUE_NOTI); } } #else //修改为Indication bStatus_t rs; attHandleValueInd_t indi; indi.len = len; indi.pValue = GATT_bm_alloc( peripheralConnList.connHandle, ATT_HANDLE_VALUE_IND, indi.len, NULL,0); if(indi.pValue) { tmos_memcpy(indi.pValue, pValue, indi.len); if(simpleProfile_Indi(peripheralConnList.connHandle, &indi, Peripheral_TaskID) != SUCCESS) { GATT_bm_free((gattMsg_t *)&indi, ATT_HANDLE_VALUE_IND); } } #endif }
同时,函数调用simpleProfile_Indi需要重写
bStatus_t simpleProfile_Indi(uint16_t connHandle, attHandleValueInd_t *pInd, uint8_t taskId) { uint16_t value = GATTServApp_ReadCharCfg(connHandle, simpleProfileChar4Config); // If notifications enabled if(value & GATT_CLIENT_CFG_INDICATE) { // Set the handle pInd->handle = simpleProfileAttrTbl[SIMPLEPROFILE_CHAR4_VALUE_POS].handle; // Send the notification return GATT_Indication(connHandle, pInd, FALSE, taskId); } return bleIncorrectMode; }
3、写入操作之前验证属性数据
在simpleProfile_WriteAttrCB函数中做如下修改
case GATT_CLIENT_CHAR_CFG_UUID: status = GATTServApp_ProcessCCCWriteReq(connHandle, pAttr, pValue, len, offset, GATT_CLIENT_CFG_INDICATE);
4、特征值4的indication数据发送
在CH58x的peripheral例程中,发送indication或者notification是通过TMOS任务管理,1s调用1次performPeriodicTask函数进行发送,然后在函数中进行peripheralChar4Notify或者peripheralChar4Indcation的发送,最终就实现了Indication数据1s发送1次。
static void peripheralChar4Notify(uint8_t *pValue, uint16_t len) { bStatus_t rs; attHandleValueInd_t indi; indi.len = len; indi.pValue = GATT_bm_alloc( peripheralConnList.connHandle, ATT_HANDLE_VALUE_IND, indi.len, NULL,0); if(indi.pValue) { tmos_memcpy(indi.pValue, pValue, indi.len); if(simpleProfile_Indi(peripheralConnList.connHandle, &indi,, Peripheral_TaskID) != SUCCESS) { GATT_bm_free((gattMsg_t *)&indi, ATT_HANDLE_VALUE_IND); } } }
这里将原本属于Noti的功能修改为Indication。在进行参数传递的时候多传递了一个taskId,这也是Indication和Notification的区别之一。蓝牙协议规定Indication发送后是需要对方回复confirm确认的,这个确认必须要有一个对应的实体也就是task去接收并且处理(也可以不处理,只是当作一个Indication成功的通知,接收这个动作是必须的)。所以Indication发送的时候必须要提前制定确认消息的接收task ID。这里就会通过simpleProfile_Indi() 被应用层调用来指定发送的Indication的值和接收确认消息的task ID,一般就由应用层task本身接收,这里taskId的含义是即将被通知响应的任务。
以上,Indication的数据发送已基本完成,接下来进行验证。
三、手机连接CH58x芯片验证Indication功能
1、通过Indication发送数据,1s调用1次该函数:
static void performPeriodicTask(void) { uint8_t notiData[SIMPLEPROFILE_CHAR4_LEN] = {0x77, 0x88, 0x99}; peripheralChar4Notify(notiData, SIMPLEPROFILE_CHAR4_LEN); }
2、烧录程序至芯片,使用NRF_CONNECT工具连接,连接之后发现特征值4已经具有Indication属性,枚举成功,同时1s接收1次该数据。
四、Central连接Peripheral获取Indication
1、在CH58x默认例程中Central连接peripheral成功后,从机已经具备Noit的属性,同时主机会进行CCCD的使能,进而接收来自从机的Noti数据。
上述已经提供了基于Noti修改为Indication的讲解,接下来我们使用Central连接Peripheral并且接收Indication的数据,基于Central的例程进行修改。
2、例程是通过UUID获取对应handle值进行打开通道,最终还会将handle值的数据打印出来,具体可以参见这个函数(无需修改):
centralGATTDiscoveryEvent
在该函数的最后是开启了一个TMOS任务
tmos_start_task(centralTaskId, START_WRITE_CCCD_EVT, DEFAULT_WRITE_CCCD_DELAY);
if(events & START_WRITE_CCCD_EVT) { if(centralProcedureInProgress == FALSE) { / Do a write attWriteReq_t req; req.cmd = FALSE; req.sig = FALSE; req.handle = centralCCCDHdl; req.len = 2; req.pValue = GATT_bm_alloc(centralConnHandle, ATT_WRITE_REQ, req.len, NULL, 0); if(req.pValue != NULL) { req.pValue[0] = 2; //Indication req.pValue[1] = 0; if(GATT_WriteCharValue(centralConnHandle, &req, centralTaskId) == SUCCESS) { centralProcedureInProgress = TRUE; } else { GATT_bm_free((gattMsg_t *)&req, ATT_WRITE_REQ); } } } return (events ^ START_WRITE_CCCD_EVT); }
根据协议规范要求,req.pValue的值应为2、0。
3、上面是类似使能了Indication的属性,接下来需要接收到来自其数据。
在centralProcessGATTMsg函数中做修改
if(pMsg->method == ATT_HANDLE_VALUE_IND) { printf("Receive indicate = "); for(i = 0; i < pMsg->msg.handleValueInd.len; i++){ PRINT("%02x ", pMsg->msg.handleValueInd.pValue[i]); }printf("\n"); Con_status = ATT_HandleValueCfm(pMsg->connHandle); //读取IND数据后返回Confirmation。 这里设置进行返回后会停止indication,为正常。返回0为成功。 PRINT("Con_status = %x\r\n", Con_status); }
Indication与Noti最重要的一点区别就是Indication具有Confirmation,因此这里我们也进行回复确认,即上面函数中的ATT_HandleValueCfm,发送成功后,则为0。
如上是接收数据的记录和返回值的确认,显示已成功。
注意:如果不进行返回值的确认,则无法发送下一次的Indication。因此在Indi后,对方一定要进行Confirmation。
4、上面的操作完成代表着从机发送Indication给主机成功,主机给与回复应答,抓包验证如下:
但是主机给从机回包(Confirmation)是底层处理的,应用层在使用的时候不可能每次通过抓包去判断从机是否成功发送数据。
在应用层可以通过查看TMOS任务的消息传递进行判断,需要在从机添加任务消息:
static void Peripheral_ProcessTMOSMsg(tmos_event_hdr_t *pMsg) { switch(pMsg->event) { case GAP_MSG_EVENT: { Peripheral_ProcessGAPMsg((gapRoleEvent_t *)pMsg); break; } case GATT_MSG_EVENT: { gattMsgEvent_t *pMsgEvent; pMsgEvent = (gattMsgEvent_t *)pMsg; if(pMsgEvent->method == ATT_MTU_UPDATED_EVENT) { peripheralMTU = pMsgEvent->msg.exchangeMTUReq.clientRxMTU; PRINT("mtu exchange: %d\n", pMsgEvent->msg.exchangeMTUReq.clientRxMTU); } peripheralProcessGATTMsg((gattMsgEvent_t *)pMsg); //添加传递任务消息,主机Confirm后,从机会进入该函数 break; } default: break; } } static void peripheralProcessGATTMsg(gattMsgEvent_t *pMsg) { if(pMsg->method == ATT_HANDLE_VALUE_CFM) { PRINT("Indication_Over\r\n"); //从机应用层查看主机Confirm成功 } }
打印验证:
从机可以接收主机收到消息的任务回应,应用层可以在这里判断主从一包的收发已经完成,开启下一包的处理。
不同的芯片查看方式不同,但是一定会留出应用层可查看的方式,一般传递taskId正确,则在从机的回调函数会提供相应回应。
五、总结
主机通过Indication进行数据收发已完成,参考上面的程序逻辑进行添加即可。一般用户在使用蓝牙发送数据时主要是通过Noti完成,使用Indi处理比较少,主要是因为Indi需要进行回包,相比Noti会多花费一些时间。同时带来的优点是数据传输的可靠,因此用户可以根据具体需求选择使用Noti或是Indi进行数据处理。
用户需要数据传输可靠,可以通过Indi处理完成,也可以直接通过应用层进行处理:从机发送数据给主机后,主机返回校验给从机,从机查看校验是否正确进而判断是否可是下一包的发送。应用层的可靠传输与Indi的可靠传输没有太大的区别,对于用户而言应用层传输可以实现自定义,Indi直接通过底层处理即可。
附录
提供Peripheral和Central的程序,提供参考使用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!