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:代表具体的数据项(比如“温度值”、“湿度值”、“设备配置”)。

在代码层面,创建流程大致分三步:

  1. 注册Service UUID:告诉协议栈我要创建一个什么类型的服务。
  2. 添加Characteristic:在Service下挂载具体的特征值,并配置其属性(读、写、通知)。
  3. 启动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就是你的照妖镜。

验证步骤:

  1. 扫描设备:打开nRF Connect,点击Scan,找到你的设备(广播名可以在代码中配置)。
  2. 连接设备:点击Connect,进入服务列表界面。
  3. 查看Service:你应该能看到你定义的Service(UUID: 0x180D)出现在列表中。
  4. 查看Characteristic:展开Service,看到Characteristic(UUID: 0x2A37),旁边有Read、Write、Notify三个按钮。
  5. 测试读:点击Read按钮,看返回的值是不是你设置的初始值(比如0xAA)。
  6. 测试写:在Write Value输入框中输入一个值(比如0x01),点击Send。如果代码中处理了写事件,你可以在串口日志中看到数据。
  7. 测试通知:点击Notify按钮旁边的开关,使能CCCD。然后设备端主动调用sd_ble_gatts_hvx(),看手机是否收到推送。

下面这张图展示了整个GATT Server的创建与验证流程:

GATT Server创建与验证流程图 GATT Server创建与验证流程 1. 初始化协议栈 2. 添加Service 3. 添加Characteristic 4. 启动广播 5. nRF Connect验证 读/写/通知测试 关键点 • Service是容器 • Characteristic是数据实体 • 权限配置决定交互方式 验证工具 • nRF Connect • 串口日志 • 逻辑分析仪(可选)

嗯,这里要注意一点: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(系统属性未保存)。

我曾经踩过的坑:有一次Notify死活不工作,查了半天发现是sd_ble_gatts_hvx()中的p_hvx_params->type设置成了BLE_GATT_HVX_INDICATION,而手机端只支持Notification。Indication需要客户端确认,Notification不需要。两者不能混用。