1. 项目概述
在蓝牙低功耗(BLE)开发中,GATT(通用属性规范)是构建服务与特征值交互的核心框架。本文将以沁恒微CH58x系列芯片为例,深入讲解如何在蓝牙从机设备中添加自定义服务和特征值,包括标准16位UUID和128位UUID的实现方式,以及Notify和Indicate两种不同通知机制的应用。
2. GATT基础架构解析
2.1 服务与特征值的关系
GATT采用树状结构组织数据:
- 服务(Service)作为容器,包含一个或多个特征值(Characteristic)
- 每个特征值由声明(Declaration)、值(Value)和可选描述符(Descriptor)组成
- 服务通过UUID标识,分为标准服务(SIG定义)和自定义服务
2.2 属性表(Attribute Table)结构
在沁恒微SDK中,gattAttribute_t结构体定义了每个属性项:
c复制typedef struct {
uint8_t typeLen; // UUID长度
uint8_t *type; // UUID指针
uint16_t permissions;// 访问权限
uint16_t handle; // 句柄(初始为0)
uint8_t *pValue; // 属性值指针
} gattAttribute_t;
关键权限标志说明:
- GATT_PERMIT_READ:允许读取
- GATT_PERMIT_WRITE:允许写入
- GATT_PERMIT_AUTHEN_READ:需要认证后读取
- GATT_PERMIT_ENCRYPT_READ:需要加密后读取
3. 服务添加实战
3.1 标准16位UUID服务实现
以官方示例的0xFFE0服务为例:
c复制// 服务声明项
{
{ATT_BT_UUID_SIZE, primaryServiceUUID}, // 固定0x2800
GATT_PERMIT_READ,
0,
(uint8_t *)&simpleProfileService // 服务UUID(0xFFE0)
}
// 特征值声明项
{
{ATT_BT_UUID_SIZE, characterUUID}, // 固定0x2803
GATT_PERMIT_READ,
0,
&simpleProfileChar1Props // 特征属性(GATT_PROP_READ等)
}
// 特征值数据项
{
{ATT_BT_UUID_SIZE, simpleProfilechar1UUID}, // 特征UUID(0xFFE1)
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
simpleProfileChar1 // 实际数据存储位置
}
3.2 128位UUID服务实现
自定义128位UUID服务需要特殊处理:
c复制// 自定义128位UUID(示例)
const uint8_t customServiceUUID[16] = {
0xFB,0x34,0x9B,0x5F,0x80,0x00,0x00,0x80,
0x00,0x10,0x00,0x00,0x01,0x00,0x00,0x00
};
// 服务声明
{
{ATT_128_UUID_SIZE, customServiceUUID},
GATT_PERMIT_READ,
0,
(uint8_t *)&customServiceUUID
}
注意:128位UUID需要完整16字节定义,建议使用UUID生成工具确保唯一性
4. 特征值属性详解
4.1 基本属性类型
| 属性类型 | 宏定义 | 功能说明 |
|---|---|---|
| 可读 | GATT_PROP_READ | 客户端可读取特征值 |
| 可写 | GATT_PROP_WRITE | 客户端可写入特征值 |
| 通知 | GATT_PROP_NOTIFY | 服务器可主动通知(无确认) |
| 指示 | GATT_PROP_INDICATE | 服务器可指示(需客户端确认) |
4.2 Notify与Indicate实现对比
Notify实现:
c复制// 特征声明
{
{ATT_BT_UUID_SIZE, characterUUID},
GATT_PERMIT_READ,
0,
&charNotifyProps // GATT_PROP_NOTIFY
}
// CCCD描述符(必须)
{
{ATT_BT_UUID_SIZE, clientCharCfgUUID}, // 0x2902
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8_t *)¬ifyConfig
}
Indicate实现差异:
- 特征属性使用GATT_PROP_INDICATE
- 需要处理确认响应:
c复制void SimpleProfile_HandleConnStatusCB(uint8_t taskId, uint8_t event, uint16_t connHandle)
{
if(event == GATT_MSG_HANDLE_VALUE_CFM) {
// Indication确认处理
}
}
5. 关键问题解决方案
5.1 特征值长度设置
根据BLE规范:
- 理论最大长度:512字节
- 推荐设置:
- 默认MTU(23字节)时:≤20字节
- 协商MTU后:≤(MTU-3)字节
- 单帧优化:≤244字节(避免分包)
c复制// 在gattprofile.h中定义
#define CHARACTER_MAX_LEN 244
5.2 动态特征值处理
对于需要动态改变长度的特征值:
- 在gattAttribute_t中使用指针存储值
- 实现回调函数处理动态分配:
c复制bStatus_t readCallback(uint16_t connHandle, gattAttribute_t *pAttr,
uint8_t *pValue, uint16_t *pLen)
{
*pLen = getCurrentDataLength(); // 动态获取长度
memcpy(pValue, getCurrentData(), *pLen);
return SUCCESS;
}
6. 调试技巧与常见问题
6.1 调试工具对比
| 工具名称 | 优势 | 局限性 |
|---|---|---|
| nRF Connect | 功能全面,支持协议分析 | 对自定义UUID显示不友好 |
| 沁恒BLE助手 | 专为CH58x优化,显示原始数据 | 功能相对简单 |
| Wireshark | 底层协议分析 | 需要专用嗅探硬件 |
6.2 典型问题排查
问题1:服务无法发现
- 检查服务声明项的UUID类型是否为0x2800
- 确认permissions包含GATT_PERMIT_READ
- 验证服务UUID是否正确注册到属性表
问题2:特征值写入失败
- 确认特征值属性包含GATT_PROP_WRITE
- 检查permissions是否匹配操作类型
- 验证写入长度≤声明的最大长度
问题3:Notify/Indicate不工作
- 确认客户端已启用CCCD通知(写入0x0001)
- 检查GATT_PROP_NOTIFY/INDICATE属性设置
- 验证服务端是否正确调用GATT_Notification()
7. 性能优化建议
7.1 内存优化策略
- 共享缓冲区:多个特征值共用同一内存区域
- 动态分配:仅在需要时分配特征值内存
- 使用const存储固定UUID:
c复制static const uint8_t fixedUUID[16] = {...};
7.2 通信效率提升
- 合理设置MTU:通过GATT_ExchangeMTU()协商最大值
- 批量通知:使用长特征值+适当分包
- 错峰发送:避免同时触发多个Notify/Indicate
8. 扩展应用实例
8.1 多服务协同工作
实现多个服务间的数据共享:
- 在peripheral.c中定义共享数据结构
- 各服务通过回调函数访问共享数据
- 使用信号量保护共享资源
8.2 动态服务注册
运行时动态添加服务的方法:
- 预分配属性表空间
- 使用GATTServApp_RegisterService()动态注册
- 更新广播数据反映服务变更
9. 开发注意事项
-
华为手机兼容性:华为设备会缓存服务信息,测试时需:
- 清除蓝牙缓存
- 或使用新设备测试
-
特征值命名:自定义UUID特征在通用工具中显示为"Unknown",可通过:
- 添加0x2901描述符提供可读名称
- 在专用APP中按UUID解析
-
安全分级:根据数据敏感程度设置不同权限:
- 公开数据:GATT_PERMIT_READ
- 敏感数据:GATT_PERMIT_AUTHEN_READ
- 控制命令:GATT_PERMIT_AUTHEN_WRITE
10. 代码结构最佳实践
推荐的项目文件组织方式:
code复制/Project
/BLE
gatt_profile.c // 标准GATT服务
custom_service.c // 自定义服务
gatt_interface.h // 统一接口
/App
ble_application.c // 业务逻辑
/Lib
ble_stack // 协议栈文件
关键设计原则:
- 服务实现与业务逻辑分离
- 通过回调函数实现解耦
- 统一错误代码定义
在实际开发中遇到最棘手的问题是Indicate确认响应超时处理。经过多次测试发现,当客户端未及时回复确认时,协议栈会阻塞后续Indicate发送。解决方案是添加超时重传机制:
c复制#define INDICATE_TIMEOUT 2000 // 2秒超时
void handleIndicateTimeout()
{
if(indicatePending && tmos_getDeltaTime(indicateTimestamp) > INDICATE_TIMEOUT) {
indicatePending = FALSE;
// 触发重传或错误处理
}
}
蓝牙服务开发中,最容易被忽视但极其重要的是属性表的排列顺序。错误的排列会导致句柄分配混乱,特别是当存在多个服务和特征值时。建议采用以下验证步骤:
- 使用nRF Connect查看完整的属性表
- 对照代码检查每个属性的位置
- 特别注意CCCD描述符必须紧跟在Notify/Indicate特征值后
通过本文的详细拆解,开发者应能掌握在沁恒微蓝牙平台上构建自定义服务的全套技能。实际项目中,建议先从官方示例出发,逐步添加新特性,并通过抓包工具验证每个阶段的实现效果。