PHY6222 BLE从机开发实战:从simpleBLEPeripheral源码构建自定义GATT服务
第一次拿到PHY6222开发板时,看着官方示例代码里密密麻麻的属性表和回调函数,我盯着屏幕发了半小时呆——这和我预想的"快速实现传感器数据上报"相差甚远。直到后来在项目deadline的压力下,才不得不硬着头皮梳理清楚GATT服务的构建逻辑。本文将分享如何通过解剖simpleBLEPeripheral这个"教学标本",避开那些让新手开发者夜不能寐的典型陷阱。
1. 开发环境与源码结构解密
拿到PHY6222开发套件后,建议先准备好以下工具链:
- PHY62xx SDK(版本建议≥2.3.0)
- ARM GCC工具链(与SDK兼容版本)
- J-Link调试器(用于实时日志输出)
- BLE调试APP(如nRF Connect或LightBlue)
官方示例代码的目录结构看似复杂,其实主要关注三个核心文件:
├── simpleBLEPeripheral │ ├── main.c # 硬件初始化入口 │ ├── simpleBLEPeripheral.c # 应用逻辑主战场 │ └── gapgattserver.c # GATT服务实现细节在main.c中有一个关键细节常被忽略:
extern void hal_rfphy_init(void); extern void hal_init(void);这些外部声明函数实际运行在ROM中,意味着我们无法修改其底层实现。这解释了为什么在调试射频参数时,某些配置看起来"不生效"——因为它们可能已被ROM代码覆盖。
2. GATT服务构建的黄金法则
2.1 属性表的解剖学
GATT服务的本质是一组属性表的有机组合。在simpleBLEPeripheral.c中,服务添加流程是这样的:
GATTServApp_AddService(&simpleBLEPeripheral_Attrs, &simpleBLEPeripheral_cb, &simpleBLEPeripheral_Handles);属性表(simpleBLEPeripheral_Attrs)的每个条目都遵循相同结构:
typedef struct gattAttribute_t { uint16_t handle; // 由系统自动分配 uint16_t type; // UUID缩写或完整值 uint8_t permissions;// 读写权限组合 uint16_t len; // 数据长度 uint8_t *pValue; // 数据存储地址 } gattAttribute_t;新手常见坑点:
- 忘记为
pValue分配静态存储空间(直接使用栈变量导致内存异常) - 混淆
type字段的UUID格式(16位短UUID需转换为128位完整格式) - 权限位设置冲突(如设置了GATT_PERMIT_READ却未实现读回调)
2.2 回调函数的生存周期
当BLE主机发起读写操作时,系统会依次触发以下回调:
- 权限检查(检查属性表的
permissions字段) - 回调函数执行(如
simpleBLEPeripheral_ReadAttrCB) - 数据同步(自动更新客户端缓存)
一个典型的读回调实现示例:
static uint8_t simpleBLEPeripheral_ReadAttrCB(uint16_t connHandle, gattAttribute_t *pAttr, uint8_t *pValue, uint16_t *pLen) { if(pAttr->type == SIMPLEPROFILE_CHAR6_UUID) { memcpy(pValue, pAttr->pValue, pAttr->len); *pLen = pAttr->len; return SUCCESS; } return ATT_ERR_ATTR_NOT_FOUND; }关键提示:回调函数执行在中断上下文中,必须保证处理时间小于10ms,否则可能触发看门狗复位。
3. 广播数据配置的艺术
广播包和扫描响应包是设备被发现的"第一印象",其配置在simpleBLEPeripheral.c的初始化阶段完成:
static uint8_t advertData[] = { 0x02, // 长度 GAP_ADTYPE_FLAGS, GAP_ADTYPE_FLAGS_GENERAL | GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED, 0x03, // 长度 GAP_ADTYPE_16BIT_MORE, 0xAA, 0xFE // 自定义服务UUID };广播数据必须遵循严格的TLV格式(Type-Length-Value),常见配置错误包括:
- 长度字段与实际数据不匹配
- UUID字节序错误(蓝牙使用小端序)
- 未包含必要的GAP标志位
优化技巧:使用GAP_UpdateAdvertisingData()可动态更新广播内容,适合需要实时切换可见状态的场景。
4. 实战:构建温度监测服务
现在我们用前文知识构建一个真实的温度监测服务。首先定义特征值UUID:
#define TEMP_SERVICE_UUID 0xAA01 #define TEMP_VALUE_UUID 0xAA02 #define TEMP_CONFIG_UUID 0xAA03接着创建属性表:
static uint8_t tempValue[4] = {0}; // 32位浮点温度值 static uint8_t tempConfig = 0; // 配置字节 static gattAttribute_t tempServiceAttrTbl[] = { // 服务声明 { .type = GATT_PRIMARY_SERVICE_UUID, .permissions = GATT_PERMIT_READ, .len = 2, .pValue = (uint8_t *)&TEMP_SERVICE_UUID }, // 温度值特征声明 { .type = GATT_CHAR_DECL_UUID, .permissions = GATT_PERMIT_READ, .len = 5, .pValue = (uint8_t[]){0x12, 0x00, TEMP_VALUE_UUID, 0x02} }, { .type = TEMP_VALUE_UUID, .permissions = GATT_PERMIT_READ | GATT_PERMIT_NOTIFY, .len = sizeof(tempValue), .pValue = tempValue }, // 配置特征 { .type = GATT_CHAR_DECL_UUID, .permissions = GATT_PERMIT_READ, .len = 5, .pValue = (uint8_t[]){0x02, 0x00, TEMP_CONFIG_UUID, 0x08} }, { .type = TEMP_CONFIG_UUID, .permissions = GATT_PERMIT_READ | GATT_PERMIT_WRITE, .len = sizeof(tempConfig), .pValue = &tempConfig } };最后实现写回调处理配置变更:
static uint8_t temp_WriteAttrCB(uint16_t connHandle, gattAttribute_t *pAttr, uint8_t *pValue, uint16_t len) { if(pAttr->type == TEMP_CONFIG_UUID) { tempConfig = *pValue; // 更新配置 if(tempConfig & 0x01) { // 启用通知时立即发送当前值 GATT_Notification(connHandle, &tempHandle, tempValue, sizeof(tempValue)); } return SUCCESS; } return ATT_ERR_ATTR_NOT_FOUND; }5. 调试技巧与性能优化
当服务出现连接异常时,建议按以下顺序排查:
- 广播可见性:用手机APP扫描确认广播包格式正确
- 服务发现:检查GATT服务列表是否包含目标UUID
- 属性权限:尝试读写操作验证权限设置
- 回调函数:在关键路径添加日志输出
性能优化关键点:
- 将频繁访问的特征值声明为
static const减少拷贝开销 - 使用
GATT_Notification()代替GATT_Indication()降低延迟 - 对多个特征值更新使用
GATT_WriteMultipleValues()减少协议开销
在完成第一个可用的GATT服务后,我习惯用逻辑分析仪捕获空中包,对比预期数据和实际传输的差异——这往往能发现那些隐藏在协议栈深处的魔鬼细节。