4、第一个GATT Server:创建Service与Characteristic、配置属性权限(Read/Write/Notify)、实现基础数据收发、使用nRF Connect验证。
好,到了真正动手的时刻了。
前面几章我们聊了GATT的架构、Profile的结构,说白了都是理论。这一章,我们要亲手搭建一个GATT Server。你想想看,当你的手机通过nRF Connect连上你的开发板,看到你自己定义的Service和Characteristic,那种感觉是完全不一样的。
我个人习惯,每接触一个新的协议栈,第一件事就是跑通一个最简的GATT Server。这就像学编程先写“Hello World”一样,能帮你快速验证整个工具链是否正常。
4.1 从零搭建:Service与Characteristic的创建流程
我们先理清一个核心概念:Service是逻辑容器,Characteristic才是数据交换的实体。一个Service可以包含多个Characteristic,每个Characteristic负责一个具体的数据点。
以我们后续要做的数据采集为例,我通常会这样设计:
- Service:代表一个传感器或一个功能模块(比如“温湿度服务”)。
- Characteristic:代表具体的数据项(比如“温度值”、“湿度值”、“设备配置”)。
在代码层面,创建流程大致分三步:
- 注册Service UUID:告诉协议栈我要创建一个什么类型的服务。
- 添加Characteristic:在Service下挂载具体的特征值,并配置其属性(读、写、通知)。
- 启动Service:将配置好的Service注册到GATT数据库中,等待客户端连接。
核心要点:Characteristic的“值”是存储在协议栈的GATT数据库中的。你通过读写这个值,来实现与对端设备的数据交换。
4.2 配置属性权限:Read、Write、Notify的区别
权限配置是新手最容易踩坑的地方。我刚开始做的时候,就遇到过“为什么我手机写进去了,设备收不到?”或者“设备数据变了,手机为什么不通知?”这类问题。
说白了,就是权限没配对。
我们常用的三种权限,它们的区别如下:
| 属性 | 含义 | 典型应用场景 | 注意事项 |
|---|---|---|---|
| Read | 客户端可以主动读取Characteristic的值 | 读取设备静态信息(如固件版本、设备名称) | 读操作是同步的,数据量大时会阻塞连接 |
| Write | 客户端可以主动写入数据到Characteristic | 下发配置参数、控制指令 | 注意区分“有响应写”和“无响应写”,后者更快但不可靠 |
| Notify | 设备主动推送数据给客户端,无需客户端轮询 | 实时传感器数据上报(温度、心率等) | 客户端必须先使能CCCD(Client Characteristic Configuration Descriptor) |
我曾经踩过的坑:只配置了Notify属性,但忘记添加CCCD描述符。结果手机端死活收不到推送。记住,Notify必须搭配CCCD使用,否则协议栈不会主动发送数据。
4.3 代码实战:实现基础数据收发
我们以Nordic的nRF5 SDK为例,写一个最简的GATT Server。假设我们创建一个自定义Service(UUID: 0x180D),包含一个可读可写的Characteristic(UUID: 0x2A37)。
代码结构如下:
// 1. 定义Service和Characteristic的UUID
#define BLE_UUID_CUSTOM_SERVICE 0x180D
#define BLE_UUID_CUSTOM_CHAR 0x2A37
// 2. 添加Service
uint32_t ble_custom_service_init(void)
{
uint32_t err_code;
ble_uuid_t ble_uuid;
// 设置UUID类型
ble_uuid.type = BLE_UUID_TYPE_BLE;
ble_uuid.uuid = BLE_UUID_CUSTOM_SERVICE;
// 添加Service
err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
&ble_uuid,
&m_custom_service.service_handle);
APP_ERROR_CHECK(err_code);
// 3. 添加Characteristic
ble_gatts_char_md_t char_md;
ble_gatts_attr_t attr_char_value;
ble_uuid_t char_uuid;
ble_gatts_attr_md_t attr_md;
memset(&char_md, 0, sizeof(char_md));
memset(&attr_char_value, 0, sizeof(attr_char_value));
memset(&attr_md, 0, sizeof(attr_md));
// 配置属性权限:可读、可写、可通知
char_md.char_props.read = 1;
char_md.char_props.write = 1;
char_md.char_props.notify = 1;
// 配置安全权限(这里用Open,实际项目建议加密)
attr_md.read_perm = (ble_gap_conn_sec_mode_t){.sm = 1, .lv = 1};
attr_md.write_perm = (ble_gap_conn_sec_mode_t){.sm = 1, .lv = 1};
// 设置Characteristic的UUID
char_uuid.type = BLE_UUID_TYPE_BLE;
char_uuid.uuid = BLE_UUID_CUSTOM_CHAR;
// 配置Characteristic值属性
attr_char_value.p_uuid = &char_uuid;
attr_char_value.p_attr_md = &attr_md;
attr_char_value.init_len = sizeof(uint8_t);
attr_char_value.max_len = sizeof(uint8_t);
attr_char_value.p_value = NULL; // 初始值为空,后续通过sd_ble_gatts_value_set设置
// 添加Characteristic到Service
err_code = sd_ble_gatts_characteristic_add(m_custom_service.service_handle,
&char_md,
&attr_char_value,
&m_custom_service.char_handles);
APP_ERROR_CHECK(err_code);
return NRF_SUCCESS;
}
这段代码做了三件事:
- 创建了一个Primary Service。
- 在这个Service下添加了一个Characteristic,支持读、写、通知。
- 配置了访问权限为Open(不加密),方便调试。
数据收发逻辑:
- 读操作:客户端发起读请求,协议栈自动返回当前Characteristic的值。
- 写操作:客户端写入数据,协议栈会触发一个写事件(BLE_GATTS_EVT_WRITE),你在事件处理函数中解析数据即可。
- 通知操作:设备端调用
sd_ble_gatts_hvx()函数,将数据推送给已使能CCCD的客户端。
我的个人习惯:在调试阶段,我会把Characteristic的初始值设为一个特殊数字(比如0xAA),这样用nRF Connect一读就知道通信链路是否正常。
4.4 使用nRF Connect验证
代码烧录进去后,怎么知道它跑对了?nRF Connect就是你的照妖镜。
验证步骤:
- 扫描设备:打开nRF Connect,点击Scan,找到你的设备(广播名可以在代码中配置)。
- 连接设备:点击Connect,进入服务列表界面。
- 查看Service:你应该能看到你定义的Service(UUID: 0x180D)出现在列表中。
- 查看Characteristic:展开Service,看到Characteristic(UUID: 0x2A37),旁边有Read、Write、Notify三个按钮。
- 测试读:点击Read按钮,看返回的值是不是你设置的初始值(比如0xAA)。
- 测试写:在Write Value输入框中输入一个值(比如0x01),点击Send。如果代码中处理了写事件,你可以在串口日志中看到数据。
- 测试通知:点击Notify按钮旁边的开关,使能CCCD。然后设备端主动调用
sd_ble_gatts_hvx(),看手机是否收到推送。
下面这张图展示了整个GATT Server的创建与验证流程:
嗯,这里要注意一点:nRF Connect显示的Service和Characteristic,其UUID必须是标准的16位或128位格式。如果你用了自定义的128位UUID,记得在nRF Connect中手动添加,否则它只会显示Unknown Service。
4.5 常见问题与避坑指南
- 问题:nRF Connect扫描不到设备
- 原因:广播没有启动,或者广播间隔设置太长。
- 解决:检查
sd_ble_gap_adv_start()是否调用成功,广播间隔建议设为100ms以内。
- 问题:读操作返回全0
- 原因:Characteristic的初始值没有设置。
- 解决:在添加Characteristic后,调用
sd_ble_gatts_value_set()设置一个非零初始值。
- 问题:写操作无响应
- 原因:写事件处理函数中没有正确处理数据。
- 解决:在BLE事件回调中,检查事件类型是否为
BLE_GATTS_EVT_WRITE,并解析ble_evt->evt.gatts_evt.params.write中的数据。
- 问题:Notify收不到数据
- 原因:CCCD没有使能,或者
sd_ble_gatts_hvx()调用失败。 - 解决:先确认nRF Connect中Notify按钮已打开;再检查
sd_ble_gatts_hvx()的返回值,常见错误是NRF_ERROR_INVALID_STATE(连接未建立)或BLE_ERROR_GATTS_SYS_ATTR_MISSING(系统属性未保存)。
- 原因:CCCD没有使能,或者
我曾经踩过的坑:有一次Notify死活不工作,查了半天发现是sd_ble_gatts_hvx()中的p_hvx_params->type设置成了BLE_GATT_HVX_INDICATION,而手机端只支持Notification。Indication需要客户端确认,Notification不需要。两者不能混用。