4. BLE协议栈初始化:从Controller到GATT Profile
好,咱们进入第四章。这一章可以说是整个蓝牙网关的"地基工程"。协议栈初始化如果没搞对,后面所有数据透传都是空中楼阁。我刚开始做BLE项目时,就因为在初始化顺序上栽过跟头——芯片跑起来死活搜不到设备,折腾了两天才发现是GAP角色配置的顺序错了。
说白了,BLE协议栈初始化就四步:Controller初始化 → Host初始化 → GAP角色配置 → GATT Profile注册。每一步都有坑,咱们一个一个来拆。
4.1 Controller初始化:让射频硬件"活"起来
Controller层是离硬件最近的。它负责物理层(PHY)和链路层(LL)的底层操作。初始化Controller,说白了就是告诉射频芯片:"嘿,准备好干活了!"
我个人习惯把Controller初始化拆成三个子步骤:
- 硬件时钟与电源管理——开启射频模块的时钟源,配置LDO输出电压
- 射频参数校准——设置发射功率、接收灵敏度、频偏补偿
- 链路层状态机复位——把LL的状态机从"空闲"切到"待命"
以Nordic nRF5 SDK为例,Controller初始化的核心代码长这样:
// 1. 初始化软硬件定时器
nrf_clock_lf_cfg_t clock_cfg = {
.source = NRF_CLOCK_LF_SRC_XTAL,
.rc_ctiv = 0,
.rc_temp_ctiv = 0,
.accuracy = NRF_CLOCK_LF_ACCURACY_20_PPM
};
sd_clock_hfclk_request();
// 2. 使能BLE协议栈(包含Controller+Host)
nrf_sdh_enable_request();
// 3. 等待协议栈就绪
while (!nrf_sdh_is_enabled()) {
// 轮询等待,实际项目建议加超时保护
}
4.2 Host初始化:让协议栈"上层建筑"就位
Host层包含L2CAP、SM、ATT、GAP、GATT这些协议模块。初始化Host,就是把这些模块的配置参数填好,把内存分配好。
你想想看,Host初始化最核心的是什么?内存管理。BLE协议栈需要为每个连接分配ATT MTU缓冲区、L2CAP通道、GATT数据库等。内存给少了,多连接时直接崩;给多了,小RAM芯片又扛不住。
我一般这样配:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 最大连接数 | 8(网关场景) | 每多一个连接,RAM多占约1.5KB |
| ATT MTU | 247字节 | 默认23,数据透传建议拉到最大 |
| GATT数据库大小 | 根据Service数量定 | 每个Service+Characteristic约占用80-120字节 |
| L2CAP通道数 | 3-5 | 包含固定通道和动态通道 |
代码示例(基于Zephyr RTOS):
// 配置BLE协议栈参数
struct bt_le_stack_params stack_params = {
.max_conn = 8,
.max_adv_sets = 1,
.enable_coded_phy = false,
.enable_ext_adv = true
};
// 初始化Host层
bt_enable(&stack_params);
// 注册GATT回调
bt_gatt_register_callback(&gatt_cb);
4.3 GAP角色配置:选对身份再上场
GAP层定义了设备在蓝牙网络中的"社会角色"。一共四种:
- Broadcaster(广播者)——只发不连,适合信标
- Observer(观察者)——只听不连,适合扫描
- Peripheral(外设)——可被连接,通常是传感器节点
- Central(中心)——主动连接,通常是手机或网关
咱们做蓝牙网关,网关本身必须是Central角色,同时可能还需要Broadcaster角色来广播网关自身状态。一个设备可以同时支持多个角色吗?嗯,这里要注意——硬件上可以,但软件上要小心资源冲突。
我曾经在一个项目里让网关同时做Central和Peripheral,结果广播间隔和扫描窗口撞在一起,导致连接成功率从95%掉到60%。后来把角色拆到两个射频时间片里才解决。
角色配置代码(以TI CC26xx为例):
// 设置GAP角色为Central
GAP_DeviceInit(GAP_PROFILE_CENTRAL, self_entity, addr, &scanParams);
// 如果需要同时做Broadcaster,额外调用
GAP_MakeDiscoverable(self_entity, &advParams);
// 配置连接参数
GAP_SetParamValue(TGAP_CONN_EST_INT_MIN, 80); // 100ms
GAP_SetParamValue(TGAP_CONN_EST_INT_MAX, 80); // 100ms
4.4 GATT Profile注册:定义你的"数据字典"
GATT Profile是BLE应用层的核心。它定义了设备能提供什么数据、数据长什么样、怎么读写。注册GATT Profile,就是把你自定义的Service和Characteristic告诉协议栈。
一个典型的GATT Profile结构:
// 自定义数据透传Service
// UUID: 0xFFE0
// - Characteristic: 透传数据通道 (UUID: 0xFFE1)
// - 属性: 通知(Notify) + 写(Write)
// - 长度: 20字节
static struct bt_gatt_attr attrs[] = {
BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_16(0xFFE0)),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(0xFFE1),
BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE,
NULL, write_cb, &data_value),
};
// 注册到协议栈
bt_gatt_register(attrs, ARRAY_SIZE(attrs));
注册时要注意几个点:
- UUID分配——自定义Service用16位UUID(0xFF00-0xFFFF),标准Service用SIG分配的
- 属性权限——读/写/通知/指示,按需配置,别给多了
- MTU对齐——Characteristic长度建议不超过MTU-3(3字节是ATT操作码+句柄)
4.5 整体初始化流程图
下面这张图把整个初始化流程串起来了。建议你对照着代码看,心里会更清楚。
4.6 初始化顺序的"潜规则"
最后说一个很多人忽略的点:初始化顺序不能乱。Controller必须在Host之前,GAP必须在GATT之前。为什么?
因为每一层都依赖下一层提供的服务。Controller提供链路层接口,Host才能建立L2CAP通道;GAP定义了设备角色,GATT才知道用哪种方式传输数据。你想想看,如果先注册GATT Profile再配GAP角色,协议栈连你是Central还是Peripheral都不知道,怎么知道该用Client还是Server的ATT操作?