做 Zephyr BLE 开发,bt_enable(NULL) 这行代码你大概写过很多遍。它就像 BLE 的开机键——调一下,协议栈就跑起来了。但这个开机键背后,Host 和 Controller 各自忙了一长串事情:打开 HCI 通道、握手、读能力、初始化连接子系统、注册一堆回调。这些步骤搞不清楚,后面遇到"广播起不来""连接回调没触发""GATT 写数据收不到"这类问题,就只能瞎猜。
这篇把 Zephyr BLE 协议栈的总体框架和 bt_enable() 之后的完整初始化流程拆开讲清楚。代码基于 NCS v3.2.1 / nRF54L15,行号可对照源码。
一、先看整体:协议栈分五层,核心是 HCI 解耦
nRF54L15 上的 Zephyr BLE 协议栈自上而下是五层:
- 应用层:定义 GATT 服务、注册连接回调、调 bt_enable 和 bt_le_adv_start 这些 API。
- Host 层:在 zephyr/subsys/bluetooth/host/ 目录下,包含 GATT、ATT、L2CAP、SMP、HCI Core 这几个模块,对应 gatt.c、att.c、l2cap.c、smp.c、hci_core.c、conn.c。Host 负责协议栈上半部:GATT/ATT 数据库、L2CAP 通道分发、SMP 配对加密、HCI 命令封装与事件分发、连接管理。
- HCI 边界:Host 和 Controller 之间的标准接口,定义在 bt_hci_driver_api 里,就四个函数:open、send、close、setup。包格式用 H:4 编码。这条边界是整个协议栈最重要的设计——Host 不关心 Controller 是闭源的 SoftDevice Controller 还是开源的软链路层,只认这个标准接口和 devicetree 里的 chosen 配置。
- Controller 层:nRF54L15 默认用 Nordic 的 SoftDevice Controller,在 nrfxlib/softdevice_controller/ 下,是个闭源库,负责 radio 时序调度、链路层状态机、HCI 事件生成。接入驱动在 nrf/subsys/bluetooth/controller/hci_driver.c。也可以选 Zephyr 自己的开源软链路层 BT_LL_SW_SPLIT,在 zephyr/subsys/bluetooth/controller/ll_sw/,这部分代码是公开的。
- 硬件层:nRF54L15 的 radio 外设、定时器、随机数源,还有 MPSL 多协议调度库。

这套分层最值得记住的一点是:Host 和 Controller 通过标准 HCI 解耦。同一套 Host 能配合各种 Controller,根本原因就在这。你换一个 Controller(比如从 SoftDevice 换成软 LL),Host 代码一行不用改,只改 devicetree 的 chosen 指向和 Kconfig 配置。
二、bt_enable() 把初始化分成两个阶段
应用调 bt_enable(cb) 之后,初始化分两个阶段走。
阶段一打开 HCI 通道、启动 RX workqueue。Host 先检查 HCI 设备就绪(从 devicetree chosen 来的),做 settings 初始化(密钥持久化),初始化命令信号量和命令发送队列,启动 BT RX 工作队列,然后调 bt_hci_open 打开 Controller 并注册接收回调。这一步在 hci_core.c:4651。
阶段二做 HCI 握手和 Host 子系统初始化。如果传的回调 cb 是 NULL,就同步直接调 bt_init();如果 cb 不为 NULL,就异步提交一个 work,最终也调到 bt_init()。bt_init() 在 hci_core.c:4535,里面依次调 hci_init() 做 HCI 握手和能力探测、调 bt_conn_init() 初始化连接/ATT/SMP/L2CAP、最后调 bt_finalize_init() 置 BT_DEV_READY 标志。完成后通过 cb(err) 通知应用(异步)或直接返回 err(同步)。
为什么分两个阶段?因为阶段一里 bt_hci_open 要真正启动 Controller 的 radio 调度,这是个"动起来"的动作;阶段二的 HCI 握手要等 Controller 能收发 HCI 包了才能做。先打开通道,再握手,顺序不能反。

三、阶段一:Controller 怎么被"打开"
阶段一 Host 侧的代码在 hci_core.c:4651 的 bt_enable 里:
int bt_enable(bt_ready_cb_t cb)
{
if (!device_is_ready(bt_dev.hci)) { return -ENODEV; } /* ① HCI 设备就绪 */
if (IS_ENABLED(CONFIG_BT_SETTINGS)) { err = bt_settings_init(); } /* ② 密钥持久化 */
k_sem_init(&bt_dev.ncmd_sem, 1, 1); /* ③ 命令信号量 */
k_fifo_init(&bt_dev.cmd_tx_queue); /* 命令发送队列 */
k_work_queue_start(&bt_workq, …); /* ④ 启动 RX workqueue */
err = bt_hci_open(bt_dev.hci, bt_hci_recv); /* ⑤ 打开 Controller + 注册接收回调 */
if (!cb) { return bt_init(); } /* ⑥ 同步:直接初始化 */
k_work_submit(&bt_dev.init); /* 异步:init_work → bt_init */
return 0;
}
bt_dev.hci 来自 DT_CHOSEN(zephyr_bt_hci),也就是 devicetree 里 &bt_hci_sdc 指向的设备。bt_hci_recv 是 Controller→Host 上行数据的入口回调,所有上行数据都从它进 Host。bt_workq 是 Host 的 RX 工作队列,后续所有 ACL 和 EVT 包都在这里处理。
Controller 侧的 hci_driver_open 在 nrf/…/hci_driver.c:1261,bt_hci_open 调到它完成 Controller 启动:
static int hci_driver_open(const struct device *dev, bt_hci_recv_t recv_func)
{
k_work_init(&receive_work, receive_work_handler);
sdc_rand_source_register(&rand_functions); /* 随机数源(加密用) */
err = mpsl_lib_init(); /* 多协议调度库 */
sdc_default_tx_power_set(RADIO_TXP_DEFAULT); /* 默认发射功率 */
err = sdc_enable(hci_driver_receive_process, sdc_mempool); /* ★ 启动 SDC */
driver_data->recv_func = recv_func; /* 存 host 的 recv 回调 */
return 0;
}
sdc_enable 是真正启动 radio 调度的动作,hci_driver_receive_process 是它注册的接收回调。最后把 Host 传进来的 recv_func(即 bt_hci_recv)存起来,作为 Controller→Host 边界的回调。
这里有个容易混淆的点:hci_driver_init 和 hci_driver_open 是两回事。hci_driver_init 在系统启动时由 DEVICE_DT_INST_DEFINE 触发,调 sdc_init 加配置内存,是 Controller 的"静态准备",此时 Controller 库已加载但 radio 没启动。hci_driver_open 在 bt_enable 时调,调 sdc_enable 真正启动 radio 调度,是 Controller 的"动态启动"。一个在 boot 阶段,一个在应用运行时,别搞混。
四、阶段二:HCI 握手是"能力探测"
阶段二的 bt_init 在 hci_core.c:4535,依次调 hci_init()、bt_conn_init()、bt_finalize_init():
static int bt_init(void)
{
err = hci_init(); /* ① HCI 握手 + 读能力 */
if (IS_ENABLED(CONFIG_BT_CONN)) {
err = bt_conn_init(); /* ② 连接/ATT/SMP/L2CAP */
}
if (IS_ENABLED(CONFIG_BT_ISO)) {
err = bt_conn_iso_init();
}
bt_finalize_init(); /* ③ 置 BT_DEV_READY */
return 0;
}
hci_init 在 hci_core.c:4251,这一步 Host 通过 HCI 命令"问"Controller 能力,把结果存进 bt_dev 这个全局结构。它先做可选的 vendor setup(设公共地址等),然后调 common_init() 做通用能力探测、调 le_init() 做 LE 专属能力探测,如果支持 BR/EDR 再调 bt_br_init()(nRF54L 不用),最后设事件掩码、做 vendor specific 初始化:
static int hci_init(void)
{
/* 可选:vendor setup(设公共地址等) */
bt_hci_setup(bt_dev.hci, &setup_params);
err = common_init(); /* 通用能力探测 */
err = le_init(); /* LE 专属能力探测 */
if (BT_FEAT_BREDR(bt_dev.features)) {
err = bt_br_init(); /* BR/EDR(nRF54L 不用) */
}
err = set_event_mask(); /* 使能感兴趣的事件 */
err = hci_vs_init(); /* vendor specific 初始化 */
}
common_init 在 hci_core.c:3508,发一串 HCI 命令读 Controller 信息:
| BT_HCI_OP_RESET | 复位 Controller | — |
| BT_HCI_OP_READ_LOCAL_FEATURES | 读支持的功能 | bt_dev.features |
| BT_HCI_OP_READ_LOCAL_VERSION_INFO | 读版本和厂商 | bt_dev.hci_version |
| BT_HCI_OP_READ_SUPPORTED_COMMANDS | 读支持的命令 | bt_dev.supported_commands |
| set_flow_control | ACL 流控 | — |
le_init 在 hci_core.c:3782,读 LE 专属能力:
| read_le_local_supported_features | LE 功能 | bt_dev.le.features |
| BT_HCI_OP_LE_READ_BUFFER_SIZE | LE 缓冲大小 | bt_dev.le.acl_mtu 等 |
| BT_HCI_OP_LE_READ_MAX_ADV_DATA_LEN | 最大广播数据长度 | bt_dev.le.max_adv_data_len |
这些探测结果不是存着好看的,它们直接决定后续行为。比如 BT_FEAT_LE_EXT_ADV 决定是否用扩展广播,le.acl_mtu 决定数据包大小。所以 hci_init 这步本质是 Host 在"摸底"——搞清楚对面这个 Controller 到底能干什么,然后据此调整自己的策略。
接着 bt_conn_init 在 conn.c:4373,初始化连接子系统。它初始化发送上下文池,调 bt_att_init() 做 ATT 通道注册和 GATT 数据库初始化,调 bt_smp_init() 做 SMP 配对加密和 ECDH 公钥生成,调 bt_l2cap_init() 初始化 L2CAP。其中 bt_att_init 在 att.c:3839,会调 bt_gatt_init() 遍历 bt_gatt_service_static 链接器段注册所有静态服务(这块在第二部分 GATT 那篇展开)。bt_smp_init 在 smp.c:6423,检测 Secure Connections 支持,调 bt_pub_key_gen 生成 ECDH 公钥(配对用),还跑 smp_self_test 自测。
最后 bt_finalize_init 在 hci_core.c:4524,置 BT_DEV_READY 标志。这个标志位很关键,置位之后 bt_le_adv_start、bt_conn_le_create 这些 API 才允许调用。没置位就调,会报错。所以应用里如果用异步 bt_enable,一定要在 ready 回调里再启动广播,不能 bt_enable 刚返回就调 bt_le_adv_start。

五、四类回调:数据怎么从 Controller 流到你的代码
这是理解 Zephyr BLE 数据流转的关键。BLE 数据通信涉及四类回调,分布在 Controller→Host→应用的全链路。搞清楚每个回调在哪里注册、什么时候触发,数据怎么流转就清楚了。
第一类:Controller→Host 的上行入口。 注册点是 bt_enable 里调 bt_hci_open(bt_dev.hci, bt_hci_recv),把 bt_hci_recv 注册给 Controller:
int bt_hci_recv(const struct device *dev, struct net_buf *buf)
{
k_sched_lock();
err = bt_recv_unsafe(buf); /* 按 H:4 类型分流 */
k_sched_unlock();
return err;
}
Controller 那边(SDC 库)产生 HCI 包后,经 hci_driver_receive_process → process_hci_msg → driver_data->recv_func(dev, buf) 调到 bt_hci_recv。这是 Controller 和 Host 的唯一数据通道,所有上行数据(HCI 事件、ACL 数据、ISO 数据)都从这一个回调进 Host。bt_recv_unsafe 按 H:4 类型把 buf 放进 bt_dev.rx_queue。
第二类:Host RX workqueue 处理。 注册点是 bt_enable 启动 bt_workq,处理 rx_work。rx_work_handler 从 rx_queue 取 buf,按 H:4 类型分流:
static void rx_work_handler(struct k_work *work)
{
buf = net_buf_slist_get(&bt_dev.rx_queue);
type = net_buf_pull_u8(buf);
switch (type) {
case BT_HCI_H4_ACL: hci_acl(buf); break; /* ACL 数据 */
case BT_HCI_H4_ISO: hci_iso(buf); break;
case BT_HCI_H4_EVT: hci_event(buf); break; /* HCI 事件 */
}
}
第三类:HCI 事件分发表和连接回调。 这是 Host 把 HCI 事件翻译成应用可见回调的关键环节,分两层。第一层是 HCI 事件分发表,Host 用三张表把 HCI 事件码映射到处理函数:
- prio_events[] 在 hci_core.c:4377,处理高优先级事件(CMD_COMPLETE、CMD_STATUS、DISCONN_COMPLETE、NUM_COMPLETED_PACKETS),走中断级处理
- normal_events[] 在 hci_core.c:3109,处理普通优先级事件(VENDOR、LE_META_EVENT 等),走 workqueue
- meta_events[] 在 hci_core.c:2918,处理 LE 子事件(LE_ADV_REPORT、LE_CONN_COMPLETE、LE_PHY_UPDATE、LE_LTK_REQUEST 等)
static const struct event_handler meta_events[] = {
EVENT_HANDLER(BT_HCI_EVT_LE_CONN_COMPLETE, le_legacy_conn_complete, …),
EVENT_HANDLER(BT_HCI_EVT_LE_ENH_CONN_COMPLETE, le_enh_conn_complete, …),
EVENT_HANDLER(BT_HCI_EVT_LE_PHY_UPDATE_COMPLETE, le_phy_update_complete, …),
EVENT_HANDLER(BT_HCI_EVT_LE_LTK_REQUEST, le_ltk_request, …),
…
};
第二层是连接回调 bt_conn_cb,应用注册的。le_conn_complete 这些处理函数解析事件、更新 bt_conn 状态,最后调 notify_connected 触发应用回调:
static void notify_connected(struct bt_conn *conn)
{
BT_CONN_CB_DYNAMIC_FOREACH(callback) { /* 动态注册 */
if (callback->connected) { callback->connected(conn, conn->err); }
}
STRUCT_SECTION_FOREACH(bt_conn_cb, cb) { /* 静态注册 */
if (cb->connected) { cb->connected(conn, conn->err); }
}
}
应用用 BT_CONN_CB_DEFINE 宏静态注册,宏展开后放进 bt_conn_cb 链接器段,notify_connected 用 STRUCT_SECTION_FOREACH 遍历它:
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected, /* 连接建立 */
.disconnected = disconnected, /* 连接断开 */
.le_param_updated = paramUpdated, /* 连接参数更新 */
.le_phy_updated = phyUpdated, /* PHY 更新 */
};
以连接建立为例,完整触发链是这样的:Controller 产生 LE_ENH_CONN_COMPLETE 事件,经 bt_hci_recv → rx_queue_put → rx_work_handler → hci_event → normal_events[] 命中 hci_le_meta_event → meta_events[] 命中 le_enh_conn_complete → bt_conn_set_state(BT_CONN_CONNECTED) → notify_connected → 遍历 bt_conn_cb 段 → 调用你写的 connected(conn, err) 回调。
第四类:L2CAP→ATT→GATT 的数据回调。 ATT 通道的 recv 回调注册点是 att.c:3507,用 BT_L2CAP_CHANNEL_DEFINE 把 bt_att_recv 注册到 CID=ATT 的固定通道:
BT_L2CAP_CHANNEL_DEFINE(z_att_fixed_chan, BT_L2CAP_CID_ATT, bt_att_accept, NULL);
/* bt_att_accept 设置 .recv = bt_att_recv */
触发时机是 hci_acl → bt_conn_recv → bt_l2cap_recv(l2cap.c:2868)按 CID 找到 ATT 通道,调 ops->recv(chan, buf) 即 bt_att_recv。然后 bt_att_recv 经 handlers[] 分发表命中 att_read_req 或 att_write_req,调 bt_gatt_foreach_attr 查属性数据库,最终调到 attr->read 或 attr->write——这就是你在 GATT 服务定义里注册的应用回调,也是应用收发 BLE 数据的核心入口:
static struct bt_gatt_attr ezAttrs[] = {
BT_GATT_PRIMARY_SERVICE(EZ_PRI_SERVICE),
BT_GATT_CHARACTERISTIC(EZ_WRITE_CHAR,
BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE | BT_GATT_PERM_PREPARE_WRITE,
NULL, /* read 回调 */
EzBle_RecvWrite, /* ★ write 回调:对端写数据进这里 */
NULL),
BT_GATT_CHARACTERISTIC(EZ_READ_TRANS_PROTO,
BT_GATT_CHRC_READ, BT_GATT_PERM_READ,
EzBle_ReadNew, /* ★ read 回调:对端读数据调这里 */
NULL, NULL),
BT_GATT_CCC(EzBle_Notify, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
};
BT_GATT_CHARACTERISTIC 的参数顺序是 (UUID, 属性, 权限, read_cb, write_cb, user_data)。对端写数据时 attr->write = EzBle_RecvWrite 被调用,对端读数据时 attr->read = EzBle_ReadNew 被调用。
以对端写数据为例,完整链路是:对端发 Write Request → radio → Controller → bt_hci_recv(ACL) → rx_work_handler → hci_acl → bt_conn_recv → bt_l2cap_recv(按 CID=0x0004)→ bt_att_recv → handlers[] 命中 att_write_req → att_write_rsp → bt_gatt_foreach_attr(handle) → attr->write(conn, attr, value, len, offset, flags),也就是你注册的那个写回调。

六、几个值得记住的认知
初始化是两阶段,这点最容易混淆。hci_driver_init 在 boot 时做 Controller 的静态准备,bt_enable 在运行时做 Host 握手和子系统初始化加 Controller 启动。很多人搞不清为什么 Controller 有两个 init 函数,根子就在这。
HCI 握手本质是能力探测。hci_init 发一堆 HCI 命令读 Controller 能力,存进 bt_dev,后续行为据此决策。换一个能力不同的 Controller,Host 的行为也会跟着变——比如有没有扩展广播、数据包多大,都是这步摸出来的。
回调链是"注册-触发"模型。每类回调都在初始化时注册(链接器段或函数指针),数据或事件到来时按表分发触发。应用只需要用 BT_CONN_CB_DEFINE 和 BT_GATT_CHARACTERISTIC 注册,不用关心中间链路。这是 Zephyr BLE 设计上对应用友好的地方——你写的是终点回调,中间七八层转发它都帮你接好了。
连接回调有两套注册方式:BT_CONN_CB_DEFINE 是静态的,放链接器段,推荐用;bt_conn_cb_register 是动态的,运行时注册。notify_connected 两种都遍历。
GATT 回调是数据通信的终点。所有上行 BLE 数据最终落到 attr->read 或 attr->write,这是应用处理数据的唯一入口。Notify 和 Indicate 是例外,那是 Server 主动发,方向相反。HCI 事件分发表则是 Host 的"翻译层",把标准 HCI 事件码翻译成 bt_conn_cb 这些应用可见的回调,三张表按优先级和类型分层处理。
写在最后
Zephyr BLE 的初始化看起来步骤多,拆开看其实就两件事:把 Controller 打开,把 Host 的各子系统拉起来并摸清 Controller 的能力。真正决定数据怎么流的,是那四类回调的注册和分发。把 bt_enable 之后的时序和这四类回调搞清楚,后面调试广播、连接、GATT 收发的问题,就有抓手了。
下一篇会展开讲 GATT Server 从注册到收发的完整调用链,看 BT_GATT_SERVICE_DEFINE 宏背后做了什么、ATT 怎么分发 PDU、读写请求怎么一路调到你的回调。
如果你正在啃 Zephyr BLE 协议栈,建议照着源码行号跟读一遍 bt_enable 的完整流程,再跟一个完整的数据链路(对端写 → bt_hci_recv → rx_work_handler → hci_acl → bt_l2cap_recv → bt_att_recv → att_write_req → 你的写回调),走一遍比看十遍文档都管用。
数据来源说明:本文基于 Zephyr BLE 协议栈开源部分(Host 层 zephyr/subsys/bluetooth/host/、软链路层 controller/ll_sw/)的公开源码与 NCS v3.2.1 研究文档整理,函数名与行号对照源码。闭源的 SoftDevice Controller(nrfxlib/softdevice_controller/)部分通过 HCI 标准边界如实表述,未冒充查阅其内部源码。
Zephyr #BLE #蓝牙 #嵌入式开发 #NCS #nRF54L #协议栈 #HCI #GATT