1. 项目背景与核心需求
最近在调试沁恒微电子(WCH)的CH57x系列蓝牙芯片时,遇到一个典型需求:如何为蓝牙从机设备添加自定义服务和特征值。这个操作在物联网设备开发中非常常见,比如要为智能手环添加心率监测服务,或者为智能家居设备添加控制指令通道。
沁恒微的蓝牙协议栈采用分层设计,应用层通过GATT(通用属性规范)来管理服务和特征。与常见蓝牙芯片不同,WCH的协议栈在服务注册方式上有些特殊设计,官方例程虽然提供了基础框架,但实际开发时仍会遇到不少坑点。
2. 开发环境准备
2.1 硬件选型要点
推荐使用CH573开发板(零售价约30元),其特点包括:
- 内置BLE 5.0射频
- 32位RISC-V内核
- 丰富的外设接口
- 最低功耗可达1.5μA(睡眠模式)
注意:CH571/CH573/CH579引脚兼容,但CH579支持USB主机功能,价格稍贵。如果不需要USB功能,选择CH573性价比最高。
2.2 软件工具链
- 编译环境:MounRiver Studio(基于Eclipse定制)
- 调试工具:WCH-Link(支持SWD调试)
- 协议分析:nRF Connect或LightBlue(蓝牙调试APP)
安装MounRiver时常见问题:
- 需要Java 8运行环境
- 工程路径不要包含中文
- 首次编译前需安装RISC-V工具链(IDE会自动提示)
3. 服务与特征值添加实战
3.1 GATT数据库构建原理
WCH的协议栈采用静态GATT表设计,所有服务和特征需要在gattprofile.h中预定义。这与动态注册的蓝牙协议栈(如BlueZ)有本质区别。
典型服务定义结构:
c复制#define SIMPLEPROFILE_SERV_UUID 0xFFF0
#define SIMPLEPROFILE_CHAR1_UUID 0xFFF1
CONST gattAttribute_t simpleProfileAttrTbl[] = {
// 主服务声明
{
{ ATT_BT_UUID_SIZE, primaryServiceUUID },
GATT_PERMIT_READ,
0,
(uint8_t *)&SIMPLEPROFILE_SERV_UUID
},
// 特征值1声明
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
&simpleProfileChar1Props
},
// 特征值1的UUID
{
{ ATT_BT_UUID_SIZE, SIMPLEPROFILE_CHAR1_UUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
simpleProfileChar1
}
};
3.2 自定义服务添加步骤
-
UUID规划:
- 16位UUID需向蓝牙技术联盟注册(基础服务直接用标准UUID)
- 自定义服务建议使用128位UUID(格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
-
修改gattprofile.c:
c复制// 在文件头部添加新服务的UUID定义
#define CUSTOM_SERVICE_UUID 0xABCD
#define CUSTOM_CHAR_READ_UUID 0xABCE
#define CUSTOM_CHAR_WRITE_UUID 0xABCF
// 在属性表中追加新服务
CONST gattAttribute_t customAttrTbl[] = {
// 服务声明
{
{ ATT_BT_UUID_SIZE, primaryServiceUUID },
GATT_PERMIT_READ,
0,
(uint8_t *)&CUSTOM_SERVICE_UUID
},
// 可读特征
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&customCharReadProps
},
{
{ ATT_BT_UUID_SIZE, CUSTOM_CHAR_READ_UUID },
GATT_PERMIT_READ,
0,
customCharReadValue
}
};
- 注册服务到协议栈:
在GATT_Init函数中合并属性表:
c复制gattAttribute_t *combinedAttrTbl[] = {
simpleProfileAttrTbl,
customAttrTbl,
NULL // 结束标志
};
3.3 特征值权限设置技巧
权限组合使用位掩码控制:
c复制// 常见组合示例
#define READ_ONLY GATT_PERMIT_READ
#define WRITE_ONLY GATT_PERMIT_WRITE
#define RW (GATT_PERMIT_READ | GATT_PERMIT_WRITE)
#define ENCRYPTED GATT_PERMIT_AUTHEN_READ
重要提示:WRITE_REQ和WRITE_CMD的区别:
- WRITE_REQ需要对方回复确认(可靠传输)
- WRITE_CMD是单向发送(低延迟)
4. 数据交互实现
4.1 读取操作处理
在simpleProfile_ReadAttrCB回调中处理读取请求:
c复制static bStatus_t custom_ReadAttrCB( uint16_t connHandle, gattAttribute_t *pAttr,
uint8_t *pValue, uint16_t *pLen, uint16_t offset )
{
if(pAttr->type == CUSTOM_CHAR_READ_UUID) {
*pLen = sizeof(customData);
memcpy(pValue, &customData, *pLen);
return SUCCESS;
}
return ATT_ERR_ATTR_NOT_FOUND;
}
4.2 写入操作处理
在simpleProfile_WriteAttrCB中处理写入:
c复制static bStatus_t custom_WriteAttrCB( uint16_t connHandle, gattAttribute_t *pAttr,
uint8_t *pValue, uint16_t len, uint16_t offset )
{
if(pAttr->type == CUSTOM_CHAR_WRITE_UUID) {
if(len > MAX_DATA_LEN) return ATT_ERR_INVALID_VALUE_SIZE;
memcpy(&customData, pValue, len);
// 触发数据处理事件
osal_set_event( customTaskID, DATA_RECEIVED_EVT );
return SUCCESS;
}
return ATT_ERR_ATTR_NOT_FOUND;
}
4.3 通知功能实现
- 在特征属性中启用通知:
c复制#define CUSTOM_CHAR_NOTIFY_UUID 0xABD0
uint8_t customCharNotifyProps = GATT_PROP_READ | GATT_PROP_NOTIFY;
- 发送通知的典型流程:
c复制attHandleValueNoti_t noti;
noti.handle = customCharNotifyHandle;
noti.len = sizeof(notiData);
memcpy(noti.value, ¬iData, noti.len);
GATT_Notification(connHandle, ¬i, false);
性能优化:高频通知建议使用
GATT_Notification的第三个参数设为true启用流控
5. 调试与问题排查
5.1 常见错误代码
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| 0x01 | 无效句柄 | 检查GATT表定义是否完整 |
| 0x02 | 权限不足 | 确认特征值的读写权限设置 |
| 0x0D | 数据超长 | 检查MTU大小(默认23字节) |
5.2 典型问题排查
问题现象:手机APP无法发现新添加的服务
- 检查GATT表是否正确定义
- 确认服务UUID没有被过滤(某些APP会隐藏非标准服务)
- 使用蓝牙嗅探器确认广播数据
问题现象:写入操作返回错误0x03
- 检查特征值属性是否包含GATT_PERMIT_WRITE
- 确认写入数据长度不超过声明长度
- 如果是加密通道,检查配对绑定状态
5.3 协议分析技巧
- 使用Wireshark配合BLE嗅探器抓包
- 关键字段观察点:
- ATT Opcode(0x12表示写请求)
- Handle Value(对应GATT表中的特征句柄)
- Error Code(出现在响应包中)
6. 性能优化建议
6.1 内存优化
- 共享特征值缓冲区(多个特征共用同一内存区域)
- 使用
__attribute__((aligned(4)))确保数据结构对齐 - 禁用未使用的GAP/GATT特性减少ROM占用
6.2 功耗优化
- 调整广播间隔(建议20ms-1s之间)
- 使用
GAPROLE_ADVERT_OFF在连接后停止广播 - 对低频更新特征启用
GATT_PROP_EXTENDED属性
6.3 吞吐量提升
- 协商更大的MTU(最高可达247字节):
c复制GATT_ExchangeMTU(connHandle, 247);
- 启用数据长度扩展:
c复制HCI_LE_SetDataLengthCmd(connHandle, 251, 2120);
- 使用Write Without Response减少交互次数
在实际项目中,我发现CH57x的RF性能相当不错,在10dBm发射功率下实测吞吐量可达80kbps(MTU=247)。如果遇到数据丢包,可以尝试以下调整:
- 降低发射功率(减少邻道干扰)
- 调整连接间隔(建议11.25-15ms)
- 启用白名单过滤无效连接请求