3. BLE基础通信:GAP与GATT协议简介、nRF5 SDK中BLE服务的创建与广播

做UWB和BLE联调,绕不开BLE的基础通信。很多刚入行的朋友一上来就扎进代码,结果被GAP、GATT、Service、Characteristic这些概念搞得晕头转向。我当年也是这样,调了三天广播死活搜不到设备,最后发现是GAP参数配错了。

这一章,咱们就把BLE通信的两个核心协议——GAP和GATT——彻底讲明白。然后我会带你用nRF5 SDK创建一个真实的BLE服务,并让它广播出去。

3.1 先搞清楚角色:GAP协议

GAP(Generic Access Profile)控制的是设备怎么被发现、怎么建立连接。说白了,它定义了设备在无线世界里的“身份”和“行为”。

GAP定义了四种角色:

角色 典型设备 核心行为
Broadcaster(广播者) 信标、温度传感器 只发广播,不连接
Observer(观察者) 扫描仪、网关 只收广播,不连接
Peripheral(外设) 手环、心率带 发广播,等待连接
Central(中心) 手机、平板 扫描并发起连接

在UWB+BLE模组联调的场景里,UWB标签通常作为Peripheral,手机或网关作为Central。嗯,这里要注意:广播不是一直发就完事了,广播间隔、广播数据包的内容,都会影响功耗和连接速度。

我的经验:广播间隔设得太短(比如20ms),功耗会飙升;设得太长(比如1s以上),手机扫描半天搜不到。我一般从100ms起步调试,兼顾功耗和响应速度。

3.2 数据怎么交换:GATT协议

GAP解决了“谁是谁”的问题,GATT(Generic Attribute Profile)解决的是“数据怎么传”。GATT建立在ATT(Attribute Protocol)之上,用属性(Attribute)来描述数据。

GATT的核心概念就三个:

  • Service(服务):一组相关功能的集合。比如“电池服务”包含电量百分比、充电状态等。
  • Characteristic(特征):一个具体的数据点。比如“电量百分比”就是一个Characteristic。
  • Descriptor(描述符):对Characteristic的额外说明,比如单位、配置信息。

你可以把Service想象成一个文件夹,Characteristic是里面的文件,Descriptor是文件的备注。

GATT定义了Client和Server两种角色:

  • GATT Server:持有数据的一方(通常是Peripheral)。
  • GATT Client:读取或写入数据的一方(通常是Central)。

举个例子:你的UWB标签作为GATT Server,暴露一个“距离数据”的Characteristic。手机作为GATT Client,读取这个数据。就这么简单。

关键点:GATT通信是主从模式。Server不能主动给Client发数据,只能等Client来读,或者通过Notification/Indication“通知”Client。我刚开始做时总想Server主动推数据,结果发现根本推不出去——后来才明白,Notification也需要Client先订阅。

3.3 动手实践:nRF5 SDK中创建BLE服务

理论说完了,咱们来点实际的。我用的是nRF5 SDK 17.1.0,芯片是nRF52840。其实nRF52系列都差不多。

创建一个自定义BLE服务,通常分三步:

  1. 定义Service UUID和Characteristic UUID
  2. 初始化Service结构体
  3. 注册Service并添加到广播数据中

3.3.1 定义UUID

BLE使用128位UUID来唯一标识Service和Characteristic。蓝牙联盟定义了一些标准UUID(16位),但咱们做自定义服务,得用128位的。

// 自定义Service UUID: 0000AAAA-0000-1000-8000-00805F9B34FB
#define UWB_SERVICE_UUID_BASE        {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, \
                                      0x00, 0x10, 0x00, 0x00, 0xAA, 0xAA, 0x00, 0x00}
#define UWB_SERVICE_UUID             0xAAAA

// 自定义Characteristic UUID: 0000BBBB-0000-1000-8000-00805F9B34FB
#define UWB_DISTANCE_CHAR_UUID       0xBBBB

UUID的字节序要注意。nRF5 SDK用的是小端序,所以Base UUID的数组顺序和你在蓝牙规范里看到的是反的。我踩过这个坑,调了一下午才发现UUID写反了。

3.3.2 初始化Service

nRF5 SDK用ble_uuid_t和ble_gatts_char_handles_t来管理Service和Characteristic。下面是一个完整的初始化示例:

static ble_uuid_t m_uwb_service_uuid;
static ble_gatts_char_handles_t m_distance_char_handles;

static uint32_t uwb_service_init(void)
{
    uint32_t err_code;
    ble_uuid128_t base_uuid = {UWB_SERVICE_UUID_BASE};
    
    // 注册自定义UUID类型
    err_code = sd_ble_uuid_vs_add(&base_uuid, &m_uwb_service_uuid.type);
    APP_ERROR_CHECK(err_code);
    
    m_uwb_service_uuid.uuid = UWB_SERVICE_UUID;
    
    // 添加Service
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
                                        &m_uwb_service_uuid,
                                        &m_uwb_service_handle);
    APP_ERROR_CHECK(err_code);
    
    // 添加Characteristic
    // ...(见下一步)
    
    return err_code;
}

3.3.3 添加Characteristic

Characteristic需要配置读写权限、通知属性等。这里我添加一个“距离数据”的Characteristic,支持Read和Notify:

// 在uwb_service_init()中继续
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_md, 0, sizeof(attr_md));
memset(&attr_char_value, 0, sizeof(attr_char_value));

// 配置Characteristic属性:可读+可通知
char_md.char_props.read = 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};
attr_md.vloc = BLE_GATTS_VLOC_STACK;

// 配置Characteristic UUID
char_uuid.type = m_uwb_service_uuid.type;
char_uuid.uuid = UWB_DISTANCE_CHAR_UUID;

// 配置Characteristic值
attr_char_value.p_uuid = &char_uuid;
attr_char_value.p_attr_md = &attr_md;
attr_char_value.init_len = sizeof(uint32_t);
attr_char_value.max_len = sizeof(uint32_t);
attr_char_value.p_value = NULL;  // 初始值为空,后续更新

err_code = sd_ble_gatts_characteristic_add(m_uwb_service_handle,
                                           &char_md,
                                           &attr_char_value,
                                           &m_distance_char_handles);
APP_ERROR_CHECK(err_code);
注意:attr_md.vloc决定了Characteristic值存在哪里。用BLE_GATTS_VLOC_STACK存在协议栈里,读写快但空间有限;用BLE_GATTS_VLOC_USER存在用户内存里,灵活但需要自己管理。我建议小数据用STACK,大数据用USER。

3.4 让设备被发现:广播配置

Service创建好了,但手机搜不到——因为你还没广播。广播数据里要包含Service UUID,这样手机扫描时才能识别出你的设备。

广播配置分两步:

  1. 构造广播数据(Advertising Data)
  2. 启动广播(Start Advertising)

3.4.1 构造广播数据

static void advertising_init(void)
{
    uint32_t err_code;
    ble_advdata_t advdata;
    ble_advdata_t scanrsp;
    uint8_t flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
    
    // 广播数据:包含Flags和Service UUID
    memset(&advdata, 0, sizeof(advdata));
    advdata.name_type = BLE_ADVDATA_FULL_NAME;
    advdata.flags = flags;
    advdata.uuids_complete.uuid_cnt = 1;
    advdata.uuids_complete.p_uuids = &m_uwb_service_uuid;
    
    // 扫描响应数据:可以放更多信息
    memset(&scanrsp, 0, sizeof(scanrsp));
    scanrsp.uuids_complete.uuid_cnt = 1;
    scanrsp.uuids_complete.p_uuids = &m_uwb_service_uuid;
    
    // 配置广播参数
    ble_gap_adv_params_t adv_params;
    memset(&adv_params, 0, sizeof(adv_params));
    adv_params.type = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED;
    adv_params.p_interval = NULL;  // 使用默认间隔
    adv_params.timeout = 0;        // 永不超时
    
    err_code = sd_ble_gap_adv_set_configure(&m_adv_handle,
                                            &advdata,
                                            &scanrsp,
                                            &adv_params);
    APP_ERROR_CHECK(err_code);
}

3.4.2 启动广播

static void advertising_start(void)
{
    uint32_t err_code;
    
    err_code = sd_ble_gap_adv_start(m_adv_handle, APP_BLE_CONN_CFG_TAG);
    APP_ERROR_CHECK(err_code);
    
    // 可以加个LED指示
    bsp_board_led_on(BSP_BOARD_LED_0);
}

启动广播后,用nRF Connect手机App扫描,就能看到你的设备了。Service UUID会显示在广播数据里。

避坑指南:我曾经把广播数据包塞得太满,结果手机死活连不上。BLE广播包最大31字节,扫描响应也是31字节。Service UUID占2字节(16位)或16字节(128位),设备名再占几个字节,很容易超。建议广播里只放关键信息,详细数据放扫描响应里。

3.5 本章知识体系

下面这张图总结了GAP、GATT和nRF5 SDK实现之间的关系:

BLE通信协议栈与nRF5 SDK实现关系图 BLE通信协议栈与nRF5 SDK实现关系 应用层(Application) UWB距离数据采集、业务逻辑 GATT层(Generic Attribute Profile) Service / Characteristic / Descriptor 定义与数据交换 GAP层(Generic Access Profile) 广播 / 扫描 / 连接管理 / 角色定义 链路层(LL) + 物理层(PHY) nRF52840 硬件 + SoftDevice协议栈 nRF5 SDK 关键API: sd_ble_gatts_service_add() → 创建Service sd_ble_gatts_characteristic_add() → 添加Characteristic sd_ble_gap_adv_set_configure() → 配置广播 sd_ble_gap_adv_start() → 启动广播

从这张图可以看得很清楚:应用层通过GATT读写数据,GATT依赖GAP建立连接,GAP又跑在链路层和物理层之上。nRF5 SDK的SoftDevice把下面三层都封装好了,我们只需要调用API就行。