BLE GATT服务开发实战:从特征值读写到内存管理与安全配置
2026/7/29 11:04:51 网站建设 项目流程

1. 项目概述:从零到一,构建一个健壮的BLE GATT服务

在物联网设备开发中,蓝牙低功耗(BLE)因其低功耗和广泛的设备支持,成为了短距离无线通信的首选。而通用属性配置文件(GATT)则是BLE设备之间进行数据交换的“通用语言”。很多开发者,尤其是刚接触嵌入式蓝牙开发的同行,常常觉得GATT协议栈是个黑盒:知道怎么调用API让设备跑起来,但一旦遇到数据吞吐量上不去、连接不稳定或者安全策略失效等问题,就感到无从下手。

我过去在基于TI CC26xx系列芯片开发可穿戴和智能家居设备时,也踩过不少坑。比如,一个简单的传感器数据通知,在实验室测试时一切正常,到了真实环境中,随着连接设备增多,设备会莫名其妙地重启;又或者,为了实现一个需要高安全级别的特征值读写,代码写了一大堆,却发现安全机制根本没生效。这些问题,根源往往不在于芯片性能,而在于对GATT协议栈内部工作机制的理解不够深入,特别是对特征值读写流程、安全权限控制以及动态内存管理这三个核心环节的把握不到位。

本文将以TI的BLE-Stack SDK为例,抛开官方文档中那些泛泛而谈的介绍,直接深入到simple_gatt_profile.cgattservapp_util.c这样的核心源码文件中,结合我实际项目中的调试记录,拆解GATT应用开发中的关键实践。我会重点分享:如何设计一个高效、可靠的特征值读写流程,避免阻塞协议栈;如何正确配置和使用认证与授权,构建真正的安全防线;以及如何管理协议栈动态申请的内存,防止内存泄漏导致系统崩溃。这些内容,都是你在数据手册和快速入门指南里看不到的“实战干货”。

2. GATT服务与特征值:数据模型的基石与设计哲学

在深入代码之前,我们必须统一对GATT数据模型的基本认知。GATT采用客户端-服务器(Client-Server)架构,服务器(通常是我们开发的嵌入式设备)维护一个属性表(Attribute Table),这个表就是设备所有功能和数据的蓝图。

2.1 属性表的层级结构与设计考量

这个属性表是一个有序的列表,每一项都是一个属性(Attribute),由四个核心部分组成:

  1. 句柄(Handle):属性的唯一地址,相当于数组索引。客户端通过句柄来寻址特定的属性。
  2. 类型(UUID):标识这个属性是什么。例如,0x2800代表“主要服务声明”,0x2A19代表“电池电量”。
  3. 权限(Permissions):定义了客户端如何访问此属性(读、写、通知等)以及所需的安全级别(无、未认证、已认证、已授权)。
  4. 值(Value):属性所承载的实际数据。

服务、特征值和描述符都是一种特定类型的属性,它们以层级结构组织:

  • 服务(Service):一个或多个特征值的逻辑集合,代表一个完整的功能单元(如电池服务、设备信息服务)。
  • 特征值(Characteristic):服务中的实际数据点,是数据交互的核心(如电池电量百分比)。
  • 描述符(Descriptor):提供关于特征值的额外元信息。其中,客户端特征值配置描述符(CCCD)至关重要,它用于使能或禁用该特征值的通知(Notify)或指示(Indication)。

在TI的协议栈中,这个属性表通常在一个.c文件(如simple_gatt_profile.c)中以一个gattAttribute_t类型的常量数组形式静态定义。设计时的第一个关键决策就在这里:如何规划句柄范围?虽然协议栈会自动分配句柄,但预先规划有助于调试。例如,你可以约定:句柄1-10用于通用访问配置文件(GAP),11-20用于通用属性配置文件(GATT),21-50用于你的第一个自定义服务,以此类推。当使用蓝牙嗅探器抓包时,你能快速定位到问题出在哪个服务上。

2.2 特征值属性:权限与属性的精妙配合

一个特征值在属性表中实际上由多个属性条目构成,通常至少包括:

  1. 特征值声明(Characteristic Declaration):属性类型为0x2803,其值字段包含了该特征值的属性(Properties,如读、写、通知)和该特征值值的句柄。
  2. 特征值值(Characteristic Value):属性类型为该特征值自定义的UUID,存放实际数据。这里的权限(Permissions)字段是安全控制的第一道闸门
  3. 客户端特征值配置描述符(CCCD,可选):如果特征值属性中包含GATT_PROP_NOTIFYGATT_PROP_INDICATE,则必须包含此描述符,供客户端配置订阅。

让我们看一个TI示例中的关键细节。在simple_gatt_profile.c中,特征值5的定义如下:

// Characteristic Value 5 { { ATT_BT_UUID_SIZE, simpleProfileChar5UUID }, GATT_PERMIT_AUTHEN_READ, // 权限:仅允许已认证的客户端读取 0, &simpleProfileChar5 },

这里的GATT_PERMIT_AUTHEN_READ就是权限标志。它意味着,如果一个客户端尝试读取这个特征值,但连接尚未经过认证(即未完成带MITM保护的配对),协议栈的GATT服务器层会直接拦截该请求,并回复错误码ATT_ERR_INSUFFICIENT_AUTHEN (0x41),而根本不会去调用你在profile中注册的读回调函数simpleProfile_ReadAttrCB

实操心得:很多开发者在这里会困惑,为什么自己写的读回调函数没有被触发?首先就应该检查该特征值值的权限标志。权限检查发生在协议栈内部,早于应用层回调。GATT_PERMIT_READ(开放读取)、GATT_PERMIT_AUTHEN_READ(需认证)、GATT_PERMIT_AUTHOR_READ(需授权)是三扇不同安全级别的大门,务必根据数据敏感性正确选用。

3. 特征值读写流程深度解析:应用层与协议栈的协同

理解了静态的数据模型,我们来看动态的数据流。特征值的读写是GATT交互的核心,这个过程涉及应用层、Profile层和协议栈GATT层的紧密协作。

3.1 写入流程:从空中包到应用处理

当一个远程客户端(如手机App)发起一个“写请求”时,完整的处理链条如下:

  1. 协议栈接收与解析:BLE控制器收到空中数据包,传递给主机协议栈。ATT层解析出写命令、目标句柄和要写入的数据。
  2. 权限检查:协议栈检查目标属性(特征值值)的写入权限(如GATT_PERMIT_WRITE)。如果权限不足,直接回复错误,流程终止。
  3. 调用写回调:权限通过后,协议栈调用该属性所在Profile注册的写回调函数(如simpleProfile_WriteAttrCB)。
  4. 应用层处理:在写回调函数中,我们通常做两件事:
    • 数据校验与存储:检查数据长度、范围是否合法,然后将数据存入应用层变量(如simpleProfileChar4)。
    • 触发后续动作:根据业务逻辑,可能需要触发一个动作,比如控制一个LED,或者准备一个回复。

这里有一个至关重要的设计原则:最小化协议栈上下文中的处理时间。协议栈回调函数(simpleProfile_WriteAttrCB)是在协议栈的任务(Task)上下文中执行的。如果在这里执行耗时操作(如复杂的计算、阻塞式传感器读取),会阻塞协议栈处理其他事件(如连接间隔事件、其他ATT请求),导致连接不稳定甚至断开。

TI的示例代码给出了最佳实践:

static void SimpleBLEPeripheral_processCharValueChangeEvt(uint8_t paramID) { switch (paramID) { case SIMPLEPROFILE_CHAR3: // 1. 从Profile获取新值 uint8_t newValue; SimpleProfile_GetParameter(SIMPLEPROFILE_CHAR3, &newValue); // 2. 在应用上下文中执行耗时操作(如更新LCD) LCD_WRITE_STRING_VALUE("Char 3:", (uint16_t)newValue, 10, LCD_PAGE4); break; } }

注意看,它不是在写回调里直接更新LCD,而是通过paramID触发了一个应用层的事件SIMPLEPROFILE_CHAR3。这个事件被放入应用层的消息队列,随后在应用任务的主循环(SimpleBLEPeripheral_processAppMsg)中被处理。这样,耗时的LCD操作就在应用任务的上下文中安全执行,不会阻塞协议栈。

3.2 读取流程与“Get/Set”抽象层

读取流程与写入类似,但通常更简单,因为主要是返回当前存储的值。这里重点要提的是TI协议栈引入的“Get/Set”函数抽象层,这在实际开发中极大地提高了代码的模块化和可维护性。

对于每个特征值,Profile会提供一对SimpleProfile_GetParameterSimpleProfile_SetParameter函数。这对函数的作用是:

  • 封装数据访问:将特征值值的存储位置(是全局变量还是结构体成员)与访问方式隐藏起来。
  • 集中逻辑控制:在SetParameter函数中,可以集中处理通知/指示的触发逻辑。

以设置特征值4为例,应用层代码非常简洁:

uint8_t charValue4 = 4; SimpleProfile_SetParameter(SIMPLEPROFILE_CHAR4, sizeof(uint8_t), &charValue4);

而在simple_gatt_profile.c内部的SimpleProfile_SetParameter函数中,则完成了所有繁重的工作:

bStatus_t SimpleProfile_SetParameter(uint8_t param, uint8_t len, void *value) { bStatus_t ret = SUCCESS; switch (param) { case SIMPLEPROFILE_CHAR4: if (len == sizeof(uint8_t)) { // 1. 存储值 simpleProfileChar4 = *((uint8_t*)value); // 2. 关键:检查并发送通知 GATTServApp_ProcessCharCfg(simpleProfileChar4Config, &simpleProfileChar4, FALSE, simpleProfileAttrTbl, GATT_NUM_ATTRS(simpleProfileAttrTbl), INVALID_TASK_ID, simpleProfile_ReadAttrCB); } break; } return ret; }

GATTServApp_ProcessCharCfg这个调用是精髓所在。它会检查该特征值的CCCD是否已被客户端使能(即是否订阅了通知)。如果已使能,它会自动构造一个通知(或指示)PDU,并通过协议栈发送出去。这意味着,应用层在更新一个特征值后,完全无需关心“是否需要通知客户端”以及“如何构造通知包”这些底层细节,只需调用SetParameter,剩下的由GATTServApp这个服务应用辅助模块搞定。这大大简化了应用层逻辑。

避坑指南:如果你发现自己调用了SetParameter但手机端没收到通知,请按以下顺序排查:

  1. 客户端是否成功写入了CCCD(通常为0x2902描述符)的值(0x0001使能通知,0x0002使能指示)?这是前提。
  2. SetParameter中,确认你调用了GATTServApp_ProcessCharCfg,并且传入的charCfg数组(此处为simpleProfileChar4Config)指针正确。
  3. 检查GATTServApp_ProcessCharCfg的返回值,看内存分配或发送是否失败。

4. 高级特性:队列写入与内存管理实战

当你的应用需要传输的数据量超过单个ATT_MTU(默认23字节,有效载荷约20字节)时,或者你想确保一系列写操作的原子性时,基础的单次写入就不够用了。这时就需要用到队列写入

4.1 队列写入的原理与配置

队列写入,规范中称为“准备写入与执行写入”,允许客户端发送多个“准备写入请求”,这些请求在服务器端被缓存在一个队列中,直到客户端发送一个“执行写入请求”,服务器才一次性将所有队列中的数据原子性地应用到目标特征值上。如果客户端发送“取消写入”,则队列被清空,所有准备写入的数据被丢弃。

这对于更新一个较长的配置文件(如几十字节的设备名称)或者确保多个相关特征值同时生效非常有用。在TI协议栈中,这个队列的大小默认是5。考虑到默认MTU,这最多可以缓存约90字节的数据。你可以通过以下API调整队列深度:

uint8_t queueSize = 10; // 将队列大小设置为10 GATTServApp_SetParameter(GATT_PARAM_NUM_PREPARE_WRITES, sizeof(uint8_t), &queueSize);

这里有一个关键限制:队列内存是从堆(HEAPMGR)中动态分配的。如果你将队列大小设置得过大,可能会耗尽宝贵的堆内存,导致其他需要动态内存的操作(如发起通知)失败。因此,你需要根据项目最坏情况下的需求,在MAX_NUM_PREPARE_WRITES和可用的堆空间之间做出权衡。通常,在app_ble.capp_cfg.h中定义HEAPMGR_SIZE来调整堆大小。

4.2 GATT过程的内存管理:防止内存泄漏的黄金法则

这是BLE协议栈开发中最容易出错、也最难调试的部分之一。任何需要通过空中发送的ATT PDU(比如通知、指示、读/写响应),其载荷缓冲区都需要动态分配内存。协议栈的GATTServApp_ProcessCharCfg帮我们隐藏了这部分细节,但如果你需要直接调用底层的GATT_Notification()GATT_Indication()函数,就必须手动管理内存。

核心规则是:谁分配,谁释放;协议栈成功发送则它释放,失败则你必须释放。

TI的gattservapp_util.c中的gattServApp_SendNotiInd函数展示了标准流程:

// 1. 尝试为通知载荷分配内存 noti.pValue = (uint8 *)GATT_bm_alloc(connHandle, ATT_HANDLE_VALUE_NOTI, GATT_MAX_MTU, &len); if (noti.pValue != NULL) { // 2. 分配成功,填充数据 status = (*pfnReadAttrCB)(connHandle, pAttr, noti.pValue, &noti.len, 0, len, GATT_LOCAL_READ); if (status == SUCCESS) { noti.handle = pAttr->handle; // 3. 发送通知 status = GATT_Notification(connHandle, &noti, authenticated); } // 4. 关键:如果发送失败(status != SUCCESS),必须手动释放内存! if (status != SUCCESS) { GATT_bm_free((gattMsg_t *)&noti, ATT_HANDLE_VALUE_NOTI); } } else { // 内存分配失败 status = bleNoResources; }

为什么必须这么做?因为GATT_Notification()是一个非阻塞函数。它把通知消息放入协议栈的发送队列后就返回了。如果返回SUCCESS (0x00),意味着协议栈已经接管了这块内存,并会在发送完成后(或出错时)在协议栈上下文中释放它。如果返回其他错误(如blePending表示暂时没有HCI缓冲区),意味着协议栈没有接管,内存仍然由应用层持有,你必须调用GATT_bm_free来释放它,否则就会发生内存泄漏。

血泪教训:我曾在一个需要高频发送传感器指示(Indication,需确认)的项目中,忽略了检查GATT_Indication()的返回值。在连接信号不佳时,blePending错误频发,而我未释放内存。运行几个小时后,设备堆内存耗尽,任务无法分配消息队列而挂起,最终看门狗复位。调试时通过监控堆内存水位才定位到问题。因此,务必处理GATT_Notification/Indication的所有非零返回值,并释放内存

5. 应用层与协议栈的事件交互:注册与处理

有时,应用层需要感知协议栈内部的一些特殊事件,以做出更精细的控制。例如,当协议栈因为缺乏HCI缓冲区而无法立即发送ATT响应时,应用可以决定重试策略。TI协议栈提供了GATT_RegisterForMsgs()函数,允许应用任务注册接收额外的GATT消息。

simple_peripheral.csimple_peripheral_processGATTMsg函数中,我们可以看到三个典型用例的处理:

5.1 处理ATT响应发送失败(blePending)

当协议栈的GATT服务器因为暂时没有可用的HCI缓冲区而无法发出ATT响应时,它会向注册的应用发送一个状态为blePending的消息。

if (pMsg->hdr.status == blePending) { // 没有可用的HCI缓冲区。让我们尝试在下一个连接事件中重传响应。 if (HCI_EXT_ConnEventNoticeCmd(pMsg->connHandle, selfEntity, SBP_CONN_EVT_END_EVT) == SUCCESS) { // 首先释放任何未决的响应(如果有) SimpleBLEPeripheral_freeAttRsp(FAILURE); // 保存响应消息以便重传 pAttRsp = pMsg; // 先不要释放此消息! return (FALSE); } }

这里的处理逻辑是:注册在下一个连接事件结束时接收回调(HCI_EXT_ConnEventNoticeCmd),届时再尝试重新发送(pAttRsp)这个未完成的响应。这是一种流量控制错误恢复机制。如果超过30秒(ATT事务超时时间)仍未完成,协议栈会向上层报告bleTimeout错误。

5.2 处理ATT流控制违规

如果连接的远端设备违反了ATT的请求-响应或指示-确认的流控制规则(例如,在未对上一个指示进行确认的情况下,又发起了新的读请求),协议栈会通知应用。

else if (pMsg->method == ATT_FLOW_CTRL_VIOLATED_EVENT) { // ATT流控制被违反。所有后续的ATT请求或指示都将被丢弃。 // 通知应用,以便其决定是否断开连接。 DISPLAY_WRITE_STRING_VALUE("FC Violated: %d", pMsg->msg.flowCtrlEvt.opcode, LCD_PAGE5); }

这是一个严重错误,通常意味着对端设备的协议栈实现有缺陷。应用层可以选择记录日志,或者直接断开连接(GAPRole_TerminateLink)以保护自身。

5.3 处理MTU更新事件

当客户端与服务器协商出一个新的、更大的MTU后,应用会收到ATT_MTU_UPDATED_EVENT事件。

else if (pMsg->method == ATT_MTU_UPDATED_EVENT) { // MTU大小已更新 DISPLAY_WRITE_STRING_VALUE("MTU Size: %d", pMsg->msg.mtuEvt.MTU, LCD_PAGE5); }

对于需要传输大量数据的应用(如OTA升级),这是一个重要事件。你可以在此时调整后续数据分包的大小,或者重新评估队列写入的配置,以充分利用更大的MTU提升吞吐量。

6. GATT安全机制:从认证到授权的纵深防御

安全不是可选项,而是必选项。GATT在特征值属性层面提供了精细的权限控制,构成了数据安全的第一道防线。

6.1 认证:协议栈自动完成的握手

如前所述,通过在特征值权限字段设置GATT_PERMIT_AUTHEN_READGATT_PERMIT_AUTHEN_WRITE,即要求连接必须经过认证。认证过程(即带MITM保护的配对)完全由协议栈的GAP Bond Manager处理。应用层只需要在初始化时正确配置配对模式(如密码输入、数字比较等),并在配对过程中通过回调函数提供必要的输入输出(如显示密码、确认匹配)。

一旦配对成功,链路加密密钥建立,协议栈会自动允许后续的读写请求通过,并调用应用层的回调函数。应用层无需在回调函数中再次检查连接是否已认证,因为未认证的请求根本到不了这里。

6.2 授权:应用自定义的额外关卡

授权是比认证更高级别的安全概念。它用于实现业务逻辑层面的权限控制。例如,一个智能门锁的特征值,可能允许所有已认证的家庭成员读取锁的状态(认证),但只有管理员才能发送开锁指令(授权)。

授权需要应用层参与决策。首先,在定义特征值时,使用GATT_PERMIT_AUTHOR_READGATT_PERMIT_AUTHOR_WRITE权限。然后,必须在Profile的回调结构体中注册一个授权回调函数:

CONST gattServiceCBs_t simpleProfileCBs = { simpleProfile_ReadAttrCB, // 读回调 simpleProfile_WriteAttrCB, // 写回调 simpleProfile_authorizationCB // 授权回调!必须提供 };

当客户端访问一个需要授权的特征值时,协议栈会先完成认证检查(如果要求认证),然后调用应用层的授权回调函数。在这个回调函数里,你可以实现任何自定义的授权逻辑,比如检查一个存储在设备内的授权令牌列表,或者与云端进行一次验证。

static bStatus_t simpleProfile_authorizationCB(uint16 connHandle, gattAttribute_t *pAttr, uint8 opcode) { // 这是一个示例实现,真实用例需要更复杂的逻辑来判断设备是否被授权 if (clientIsAuthorized(connHandle)) { // 假设的授权检查函数 return SUCCESS; // 授权成功 } else { return ATT_ERR_INSUFFICIENT_AUTHOR; // 授权失败,返回错误码0x08 } }

关键警告:授权回调函数在协议栈上下文中执行,必须快速返回!绝不能在这里执行网络请求、复杂的加解密等耗时操作。如果需要,应该像处理特征值写入一样,设置一个标志,将耗时的授权检查移到应用任务中去处理。

重要提示:如果你为特征值设置了GATT_PERMIT_AUTHOR_*权限,但没有提供授权回调函数,协议栈将返回ATT_ERR_UNLIKELY (0x0E)错误。这个错误码非常晦涩,难以调试。TI官方也强烈建议,只要使用了授权权限,就一定要实现授权回调,哪怕最初只是直接返回SUCCESS

7. 安全连接与绑定管理实战

蓝牙4.2引入了LE安全连接,采用了更强大的椭圆曲线加密算法。GAP Bond Manager模块封装了配对、加密、绑定的复杂流程,极大减轻了应用负担。

7.1 配对模式的选择与配置

选择哪种配对模式,取决于设备的安全需求和I/O能力。TI的GAPBondMgr通过几个关键参数来决定:

  • GAPBOND_PAIRING_MODE:是否发起配对。
  • GAPBOND_MITM_PROTECTION:是否要求中间人保护。
  • GAPBOND_IO_CAPABILITIES:设备的输入输出能力(无、仅显示、仅输入、显示+是/否等)。
  • GAPBOND_SECURE_CONNECTION:是否只使用安全连接。

例如,配置一个要求MITM保护、支持安全连接、并具备“显示+是/否”IO能力的设备进行数字比较配对:

uint8_t pairMode = GAPBOND_PAIRING_MODE_INITIATE; uint8_t mitm = TRUE; uint8_t ioCap = GAPBOND_IO_CAP_DISPLAY_YES_NO; uint8_t scMode = GAPBOND_SECURE_CONNECTION_ONLY; GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode); GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, sizeof(uint8_t), &mitm); GAPBondMgr_SetParameter(GAPBOND_IO_CAPABILITIES, sizeof(uint8_t), &ioCap); GAPBondMgr_SetParameter(GAPBOND_SECURE_CONNECTION, sizeof(uint8_t), &scMode);

配置完成后,当连接建立,GAPBondMgr会自动发起配对流程。应用层需要做的就是实现并注册回调函数(如passcodeCB),在需要时显示密码或确认数字匹配。

7.2 绑定与SNV存储:实现无感重连

配对解决了本次连接的加密问题,而绑定则将配对产生的长期密钥(LTK)保存到非易失性存储器(在TI协议栈中通常是SNV区域)。这样,设备下次重连时,可以直接使用存储的密钥加密链路,无需用户再次配对,实现无缝体验。

启用绑定非常简单:

uint8_t bonding = TRUE; GAPBondMgr_SetParameter(GAPBOND_BONDING_ENABLED, sizeof(uint8_t), &bonding);

绑定信息存储在SNV中,一个完整的绑定记录包括:对端地址、LTK、IRK、CSRK、CCCD状态等。默认最多存储10个绑定记录。当记录存满时,行为由GAPBOND_LRU_BOND_REPLACEMENT参数决定:如果为TRUE,则自动替换最久未使用的记录;如果为FALSE,则无法添加新记录,除非手动删除。

开发注意事项

  1. SNV空间规划:一个绑定记录可能占用上百字节。你需要根据GAP_BONDINGS_MAX和记录大小,评估项目所需的SNV空间(通过OSAL_SNV定义),确保不会与其他使用SNV的功能(如自定义持久化数据)冲突。
  2. 绑定状态处理:在配对状态回调pairStateCB中,首次配对并绑定成功,你会收到GAPBOND_PAIRING_STATE_COMPLETE。后续与已绑定设备重连并加密成功时,你会收到GAPBOND_PAIRING_STATE_BONDED。应用层可以根据不同状态更新UI,例如,对于已绑定设备,可以跳过“等待配对”的界面。

8. 常见问题排查与性能优化实录

基于上述原理和实践,我将项目中遇到的一些典型问题及解决方案整理成下表,供大家快速排查:

问题现象可能原因排查步骤与解决方案
手机能发现服务但读/写特征值失败1. 特征值权限不足(如需要认证但未配对)。
2. 特征值属性(Properties)未包含读/写标志。
3. 应用层读/写回调函数返回错误。
1. 使用蓝牙调试工具(如LightBlue)查看错误码:0x01(无效句柄)、0x02(读不被允许)、0x05(认证不足)等。
2. 检查特征值声明中的charProps是否包含GATT_PROP_READGATT_PROP_WRITE
3. 在应用层回调函数中打印日志,确认函数被调用及返回值。
通知(Notify)无法触发1. 客户端未成功写入CCCD(值非0x0001)。
2.GATTServApp_ProcessCharCfg未被调用或传入参数错误。
3. 内存分配失败,无法发送通知PDU。
1. 确认客户端写入CCCD的操作成功(可抓包确认)。
2. 在SimpleProfile_SetParameter函数中设置断点,确认GATTServApp_ProcessCharCfg被调用,且charCfg指针指向正确的CCCD状态数组。
3. 检查堆内存大小(HEAPMGR_SIZE),如果频繁发送通知,考虑增大堆或优化发送频率。
设备运行一段时间后死机或重启1.内存泄漏,特别是GATT_Notification/Indication失败后未释放内存。
2. 应用层消息队列满,导致任务阻塞。
3. 协议栈任务因某种原因挂起。
1.重点检查所有直接调用GATT_Notification/Indication的地方,确保对非SUCCESS返回值调用GATT_bm_free
2. 监控应用层消息队列使用率,增大队列深度或优化消息处理速度。
3. 启用协议栈内部分析功能(如TI的UART_printf调试),查看协议栈任务是否在正常运行。
配对过程失败1. 两端设备的配对参数(IO能力、MITM要求、SC支持)不兼容。
2. 密码输入错误或用户未确认。
3. 链路层数据包丢失严重。
1. 对照蓝牙核心规范Vol 3, Part H的配对矩阵,检查双方GAPBondMgr的配置。
2. 在passcodeCB回调中打印日志,确认密码生成和传递流程正确。
3. 检查射频环境,确保RSSI良好,尝试缩短连接间隔。
已绑定设备重连后仍需配对1. 绑定信息未成功保存到SNV。
2. SNV数据损坏或版本不兼容。
3. 对端设备删除了绑定信息。
1. 确认GAPBOND_BONDING_ENABLED已设置为TRUE,且配对过程成功完成(收到COMPLETE状态)。
2. 检查SNV分区是否被其他操作意外擦除。可考虑在SNV操作后增加校验。
3. 对端设备(如手机)的蓝牙系统可能清除了配对记录。

性能优化小技巧

  • MTU协商:在连接建立后尽早发起MTU交换请求(GATT_ExchangeMTU),使用更大的MTU(如247字节)可以显著提升大数据量传输的效率,减少协议开销。
  • 连接参数优化:根据应用场景调整连接间隔、从机延迟和监控超时。对于需要快速响应的设备(如遥控器),使用较短的连接间隔(如15-30ms);对于低频数据采集设备,可以使用较长的间隔(如1-2s)以节省功耗。
  • 批量数据发送:对于需要发送大量数据(如传感器历史日志),不要使用多个独立的通知。可以设计一个特征值用于“启动传输”,另一个特征值用于“读取数据块”。客户端通过写请求启动,然后通过一系列带偏移量的读请求来分块获取数据,这样更可靠且易于控制流量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询