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。嗯,这里要注意:广播不是一直发就完事了,广播间隔、广播数据包的内容,都会影响功耗和连接速度。
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服务,通常分三步:
- 定义Service UUID和Characteristic UUID
- 初始化Service结构体
- 注册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,这样手机扫描时才能识别出你的设备。
广播配置分两步:
- 构造广播数据(Advertising Data)
- 启动广播(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会显示在广播数据里。
3.5 本章知识体系
下面这张图总结了GAP、GATT和nRF5 SDK实现之间的关系:
从这张图可以看得很清楚:应用层通过GATT读写数据,GATT依赖GAP建立连接,GAP又跑在链路层和物理层之上。nRF5 SDK的SoftDevice把下面三层都封装好了,我们只需要调用API就行。