4. BLE协议栈初始化:从Controller到GATT Profile

好,咱们进入第四章。这一章可以说是整个蓝牙网关的"地基工程"。协议栈初始化如果没搞对,后面所有数据透传都是空中楼阁。我刚开始做BLE项目时,就因为在初始化顺序上栽过跟头——芯片跑起来死活搜不到设备,折腾了两天才发现是GAP角色配置的顺序错了。

说白了,BLE协议栈初始化就四步:Controller初始化 → Host初始化 → GAP角色配置 → GATT Profile注册。每一步都有坑,咱们一个一个来拆。

4.1 Controller初始化:让射频硬件"活"起来

Controller层是离硬件最近的。它负责物理层(PHY)和链路层(LL)的底层操作。初始化Controller,说白了就是告诉射频芯片:"嘿,准备好干活了!"

我个人习惯把Controller初始化拆成三个子步骤:

  1. 硬件时钟与电源管理——开启射频模块的时钟源,配置LDO输出电压
  2. 射频参数校准——设置发射功率、接收灵敏度、频偏补偿
  3. 链路层状态机复位——把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()) {
    // 轮询等待,实际项目建议加超时保护
}
⚠️ 我曾经踩过的坑: 低频时钟源选错了。用内部RC振荡器虽然省成本,但精度只有±250ppm,会导致蓝牙连接后频繁断连。量产项目一定要用外部晶振(XTAL),精度至少20ppm。

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);
💡 我的经验: 如果芯片RAM只有64KB,建议把最大连接数控制在4个以内,ATT MTU用默认的23。否则堆栈溢出会让你排查到怀疑人生。

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
🔑 关键决策: 网关场景下,Central角色是必须的。如果还需要广播自身信息,建议用扩展广播(Extended Advertising)来复用同一个射频通道,避免角色冲突。

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操作码+句柄)
⚠️ 避坑指南: 我曾经把Characteristic的权限配成"可读+可写+可通知",结果手机端同时读写时触发了ATT冲突,导致连接断开。后来改成"写+通知"分离,用两个Characteristic分别处理,问题解决。

4.5 整体初始化流程图

下面这张图把整个初始化流程串起来了。建议你对照着代码看,心里会更清楚。

BLE协议栈初始化流程 1. Controller初始化 2. Host初始化 3. GAP角色配置 Broadcaster Observer Peripheral Central 4. GATT Profile注册 💡 网关角色组合 Central + Broadcaster (连接设备 + 广播状态) 注意射频时间片分配

4.6 初始化顺序的"潜规则"

最后说一个很多人忽略的点:初始化顺序不能乱。Controller必须在Host之前,GAP必须在GATT之前。为什么?

因为每一层都依赖下一层提供的服务。Controller提供链路层接口,Host才能建立L2CAP通道;GAP定义了设备角色,GATT才知道用哪种方式传输数据。你想想看,如果先注册GATT Profile再配GAP角色,协议栈连你是Central还是Peripheral都不知道,怎么知道该用Client还是Server的ATT操作?