CH585蓝牙Notify功能实战:手把手教你从零配置到数据上报(附完整代码)
CH585蓝牙Notify功能实战:从零构建数据上报系统的完整指南
刚拿到CH585开发板时,面对官方例程里密密麻麻的蓝牙协议栈代码,我盯着那个神秘的"Notify"功能研究了整整三天。作为嵌入式开发者,我们总需要让设备主动向手机推送数据——比如传感器的实时读数。但第一次尝试在CH585上实现Notify功能时,我遇到了Handle错位、配置遗漏、回调失效等一系列问题。本文将带你完整走通Notify特征的添加流程,避开那些让我熬夜调试的"坑"。
1. 环境准备与基础概念
在开始修改代码前,我们需要明确几个核心概念。Notify是蓝牙GATT协议中的一种通信机制,允许外围设备(Peripheral)在没有中心设备(Central)主动请求的情况下发送数据。想象一下智能手环持续向手机发送心率数据的场景——这就是Notify的典型应用。
必备工具清单:
- CH582/CH583/CH585开发板(本文以CH585为例)
- 沁恒官方SDK(版本建议≥V2.0)
- WCH-Link调试器
- 手机端蓝牙调试APP(推荐nRF Connect或LightBlue)
关键术语快速理解:
- GATT:通用属性协议,定义蓝牙设备间的数据交换格式
- Characteristic:特征值,蓝牙通信的基本数据单元
- CCCD:客户端特征配置描述符(Client Characteristic Configuration Descriptor),控制Notify/Indicate的开关
- Handle:属性表中每个特征的唯一标识符
// 典型属性表结构示例(简化版) static const gattAttribute_t simpleProfileAttrTbl[] = { // 服务声明 {ATT_BT_UUID_SIZE, primaryServiceUUID, GATT_PERMIT_READ, 0, &simpleProfileService}, // 特征1声明 {ATT_BT_UUID_SIZE, characterUUID, GATT_PERMIT_READ, 0, &char1Props}, {ATT_BT_UUID_SIZE, char1UUID, GATT_PERMIT_READ|GATT_PERMIT_WRITE, 0, char1Value}, // 特征5声明(我们将添加Notify) {ATT_BT_UUID_SIZE, characterUUID, GATT_PERMIT_READ, 0, &char5Props}, {ATT_BT_UUID_SIZE, char5UUID, GATT_PERMIT_READ, 0, char5Value}, // CCCD描述符将添加在这里 };2. 属性表深度解析与Notify配置
属性表(Attribute Table)是蓝牙协议栈的核心数据结构,它像一张路由表决定了数据如何被访问。很多开发者在这里犯错是因为没有理解其层次结构:
- 服务声明(Primary Service Declaration)
- 特征声明(Characteristic Declaration)
- 特征值(Characteristic Value)
- 描述符(Descriptor,包括CCCD)
关键修改步骤:
2.1 修改特征属性
在特征声明中启用Notify属性,这是第一步也是最容易遗漏的:
// 原代码(只有READ属性) static uint8_t simpleProfileChar5Props = GATT_PROP_READ; // 修改后(添加NOTIFY) static uint8_t simpleProfileChar5Props = GATT_PROP_READ | GATT_PROP_NOTIFY;2.2 添加CCCD配置数组
每个连接都需要独立的CCCD配置存储空间:
// 在文件头部添加全局变量 static gattCharCfg_t simpleProfileChar5Config[PERIPHERAL_MAX_CONNECTION];2.3 插入CCCD描述符
在属性表中正确位置添加CCCD描述符条目:
// 在特征5的特征值声明后添加 {ATT_BT_UUID_SIZE, clientCharCfgUUID, GATT_PERMIT_READ | GATT_PERMIT_WRITE, 0, (uint8_t *)simpleProfileChar5Config},常见陷阱:
- 描述符位置错误(必须在特征值之后)
- 权限设置不当(需要同时允许读写)
- 忘记初始化配置数组
3. 连接管理与Notify初始化
蓝牙连接建立时,需要初始化CCCD配置。很多开发者在这里遇到的问题是回调函数未正确绑定:
3.1 全局初始化
在协议栈初始化阶段注册特征配置:
void SimpleProfile_Init() { GATTServApp_InitCharCfg(INVALID_CONNHANDLE, simpleProfileChar5Config); }3.2 连接事件处理
在连接建立回调中重置配置:
static void peripheralConnStateCB(uint16_t connHandle, uint8_t state) { if(state == PEER_CONNECTED) { GATTServApp_InitCharCfg(connHandle, simpleProfileChar5Config); } }关键点验证:
- 确保
peripheralConnStateCB已注册到协议栈 - 检查
PERIPHERAL_MAX_CONNECTION是否足够 - 连接后使用蓝牙调试工具查看CCCD描述符是否存在
4. 数据发送接口实现与调试
Notify功能的最后一步是实现数据发送接口,这里最容易出现Handle错位问题:
4.1 底层Notify函数
bStatus_t simpleProfile5_Notify(uint16_t connHandle, attHandleValueNoti_t *pNoti) { uint16_t value = GATTServApp_ReadCharCfg(connHandle, simpleProfileChar5Config); if(value & GATT_CLIENT_CFG_NOTIFY) { pNoti->handle = simpleProfileAttrTbl[SIMPLEPROFILE_CHAR5_VALUE_POS].handle; return GATT_Notification(connHandle, pNoti, FALSE); } return bleIncorrectMode; }4.2 应用层封装
void peripheralChar5Notify(uint8_t *pValue, uint16_t len) { attHandleValueNoti_t noti; 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(simpleProfile5_Notify(peripheralConnList.connHandle, ¬i) != SUCCESS) { GATT_bm_free((gattMsg_t *)¬i, ATT_HANDLE_VALUE_NOTI); } } }调试技巧:
- 使用
PRINT输出关键Handle值进行验证 - 在手机端先写入CCCD(通常发送0x0001启用Notify)
- 逐步增加数据长度测试MTU限制
5. 实战案例:构建温度上报系统
让我们通过一个完整案例巩固所学。假设需要每2秒上报一次温度数据:
5.1 定时器设置
void TempReport_Init() { tmos_start_task(peripheralTaskId, TEMP_REPORT_EVT, 2000); } static void peripheralProcessTMOSMsg(tmos_event_t events) { if(events & TEMP_REPORT_EVT) { uint8_t tempData[2]; tempData[0] = readTemperature() >> 8; tempData[1] = readTemperature() & 0xFF; peripheralChar5Notify(tempData, 2); tmos_start_task(peripheralTaskId, TEMP_REPORT_EVT, 2000); } }5.2 特征UUID定义
建议自定义128位UUID替代默认的16位UUID:
// 在simple_profile.h中定义 #define SIMPLEPROFILE_CHAR5_UUID {0xFB,0x34,0x9B,0x5F,0x80,0x00,0x00,0x80,0x00,0x10,0x00,0x00,0xE2,0xAA,0x00,0x00}5.3 属性表位置验证
通过以下方法确认Handle位置:
void printCharHandles() { PRINT("Char5 Value Handle: 0x%04X\n", simpleProfileAttrTbl[SIMPLEPROFILE_CHAR5_VALUE_POS].handle); PRINT("Char5 CCCD Handle: 0x%04X\n", simpleProfileAttrTbl[SIMPLEPROFILE_CHAR5_VALUE_POS+1].handle); }6. 高级技巧与性能优化
当系统需要高频发送Notify数据时,需要考虑以下优化:
内存管理策略:
// 预分配通知缓冲区 #define NOTI_POOL_SIZE 5 static attHandleValueNoti_t notiPool[NOTI_POOL_SIZE]; attHandleValueNoti_t* allocNotiBuffer() { for(int i=0; i<NOTI_POOL_SIZE; i++) { if(notiPool[i].pValue == NULL) { return ¬iPool[i]; } } return NULL; }流量控制方案:
// 在发送前检查队列状态 if(GATT_GetNumPendingNotifications(connHandle) > 3) { PRINT("Notification queue full\n"); return bleMemAllocError; }错误处理增强:
bStatus_t safeNotifySend(uint16_t connHandle, uint8_t *data, uint16_t len) { attHandleValueNoti_t noti; noti.len = len; noti.pValue = data; if(simpleProfile5_Notify(connHandle, ¬i) != SUCCESS) { PRINT("Notify failed, status: %d\n", status); if(status == bleInvalidRange) { // Handle值可能错误 verifyHandles(); } return status; } return SUCCESS; }在最近的一个工业传感器项目中,我们实现了500Hz的加速度数据Notify传输。关键是在协议栈外做了数据缓冲,并使用DMA搬运数据到蓝牙发送缓冲区。当手机端延迟较高时,动态调整了Notify间隔避免队列溢出。
