免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Zephyr BLE 协议栈 bt_enable() 初始化流程源码深度解析(NCS v3.2.1)

Zephyr BLE 协议栈 bt_enable() 初始化流程源码深度解析(NCS v3.2.1) 做 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 换成软 LLHost 代码一行不用改只改 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:1261bt_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/EDRnRF54L 不用 */ } err set_event_mask(); /* 使能感兴趣的事件 */ err hci_vs_init(); /* vendor specific 初始化 */ }common_init在hci_core.c:3508发一串 HCI 命令读 Controller 信息HCI 命令作用结果存入BT_HCI_OP_RESET复位 Controller—BT_HCI_OP_READ_LOCAL_FEATURES读支持的功能bt_dev.featuresBT_HCI_OP_READ_LOCAL_VERSION_INFO读版本和厂商bt_dev.hci_versionBT_HCI_OP_READ_SUPPORTED_COMMANDS读支持的命令bt_dev.supported_commandsset_flow_controlACL 流控—le_init在hci_core.c:3782读 LE 专属能力HCI 命令作用结果存入read_le_local_supported_featuresLE 功能bt_dev.le.featuresBT_HCI_OP_LE_READ_BUFFER_SIZELE 缓冲大小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注册给 Controllerint 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 等走 workqueuemeta_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注册到 CIDATT 的固定通道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_recvl2cap.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-writeEzBle_RecvWrite被调用对端读数据时attr-readEzBle_ReadNew被调用。以对端写数据为例完整链路是对端发 Write Request → radio → Controller →bt_hci_recv(ACL) →rx_work_handler→hci_acl→bt_conn_recv→bt_l2cap_recv按 CID0x0004→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后续行为据此决策。换一个能力不同的 ControllerHost 的行为也会跟着变——比如有没有扩展广播、数据包多大都是这步摸出来的。回调链是注册-触发模型。每类回调都在初始化时注册链接器段或函数指针数据或事件到来时按表分发触发。应用只需要用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 Controllernrfxlib/softdevice_controller/部分通过 HCI 标准边界如实表述未冒充查阅其内部源码。Zephyr #BLE #蓝牙 #嵌入式开发 #NCS #nRF54L #协议栈 #HCI #GATT
返回列表