尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Zephyr BLE demo 编译全流程:west + CMake + Kconfig + Devicetree 四件套源码拆解(NCS v3.2.1)

Zephyr BLE demo 编译全流程:west + CMake + Kconfig + Devicetree 四件套源码拆解(NCS v3.2.1) 数据来源说明本文基于 Zephyr 构建系统开源部分撰写。west/CMake/Kconfig/Devicetree 均为开源基础设施samples/bluetooth/peripheral、subsys/bluetooth/下的 CMakeLists.txt 与 Kconfig 可对照源码阅读。文中部分文件已用 gitee 镜像 main 分支核对https://gitee.com/zephyrproject-rtos/zephyr/raw/main/核对结果与 NCS v3.2.1 研究文档有出入处已按实际源码更正并标注gitee 镜像只有 main 分支ncs-v3.2.1 tag 不在镜像上行号以 main 为参考并标注版本差异。SoftDevice Controller 闭源本文只讲它在构建里怎么被链入不冒充看过其源码。做 Zephyr BLE 开发敲一行west build -b nrf54l15dk_nrf54l15_cpuapp samples/bluetooth/peripheral等一会儿build/zephyr/zephyr.elf就出来了。但这个 elf 里到底塞了什么、为什么塞这些、靠什么决定塞哪些文件多数人没深究过。遇到我明明开了 CONFIG_BT为什么没编进 controller换个板子 controller 选不中prj.conf 改了不生效这类问题就只能反复 clean 重试。这篇把一个 BLE demo 从源码到固件的完整编译框架拆开讲清楚。核心是 west、CMake、Kconfig、Devicetree 这四件套怎么协同代码基于 NCS v3.2.1 / nRF54L15关键文件已对照 gitee 镜像 main 分支核对。一、全景四件套各管一摊谁也别越界先建立全景认知四件套职责分明west 管工作区拉哪些仓库、各仓库哪个版本它说了算。CMake 管构建编排编译哪些文件、怎么链接它说了算。Kconfig 管功能裁剪CONFIG_* 开关决定编不编译某个模块。Devicetree 管硬件描述板子有什么、HCI 节点使能哪个 controller它说了算。四者不是平行关系是一条流水线west 调起 cmake → cmake 先处理 devicetree从 dts 节点生成 Kconfig 依赖符号DT_HAS_ENABLED→ cmake 跑 Kconfig 解析器合并 board defconfig 和 app prj.conf生成.config和autoconf.h→ cmake 再根据 CONFIG和 devicetree 决定编译哪些源文件 → 最后链接成 elf。记住这条流水线的顺序很重要后面每一环都是顺着它走的。devicetree 在 Kconfig 之前处理所以 Kconfig 能依赖 devicetree 生成的符号而 CMake 的源文件裁剪又依赖 Kconfig 的结果。三个工具串成一条链谁在前谁在后是固定的。二、west 工作区与 manifest代码从哪来工作区根目录有.west/config指明 manifest 仓库是nrf/manifest 文件是nrf/west.ymlZephyr 树在zephyr/。nrf/west.yml是 NCS 的 manifest定义所有要拉取的仓库。zephyr的 revision 是ncs-v3.2.1它是 Zephyr 核心还会import自己的子模块cmsis、hal_nordic、littlefs 等。nrfxlib的 revision 是v3.2.1Nordic 库里面含 SoftDevice Controller 的预编译库。还有 mcuboot、mbedtls、trusted-firmware-m 这些。磁盘上大致是这个布局v3.2.1/下面zephyr/是 RTOS 核心OS、构建系统、samples、subsys、dtsnrf/是 Nordic 专属boards、subsys/bluetooth/controller、应用nrfxlib/是 Nordic 库softdevice_controller、mpsl、cryptobootloader/是 mcubootmodules/是第三方模块。west 的活儿到这就结束了——它把代码摆到正确位置剩下的交给 CMake。理解这点能避免一个常见误区别去 west 里找编译逻辑west 不管编译它只管拉代码。三、west build 做了什么本质是调 cmakewest build -b nrf54l15dk_nrf54l15_cpuapp zephyr/samples/bluetooth/peripheral本质上是调 cmake。实现见zephyr/scripts/west_commands/build.py它拼出来的命令大致是cmake -DBOARDnrf54l15dk_nrf54l15_cpuapp \ -S zephyr/samples/bluetooth/peripheral \ -B build -G Ninja-DBOARD指定目标板-S指定应用源码目录-B指定构建目录-G Ninja指定用 Ninja 做底层生成器。然后应用的CMakeLists.txt用find_package(Zephyr)把整个 Zephyr 构建系统拉进来。所以 west build 不是什么神秘魔法它就是个 cmake 的包装器。真要看构建细节直接读 CMakeLists.txt别在 west_commands 里转。四、CMake 层级从应用到协议栈的递进CMake 是分层的从应用层一路递进到协议栈。应用层最简单zephyr/samples/bluetooth/peripheral/CMakeLists.txt已对照 gitee 镜像 main 分支核对实际内容是cmake_minimum_required(VERSION 3.13.1) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(peripheral) target_sources(app PRIVATE src/main.c src/cts.c )这里有两处和某些文档说法不一样按实际源码更正第一cmake_minimum_required是3.13.1不是 3.20.0。第二应用贡献的不止main.c还有cts.cCurrent Time Service这个样例带了个时间服务。find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})是关键一行它触发整个 Zephyr 构建系统的加载。Zephyr 顶层zephyr/CMakeLists.txt由find_package触发关键是子目录遍历顺序先add_subdirectory(soc)再add_subdirectory(boards)再add_subdirectory(subsys)Bluetooth 从这里进入再add_subdirectory(drivers)然后遍历ZEPHYR_MODULE_NAMES把 nrf/、nrfxlib/ 等模块加进来。这个顺序不是随便排的soc 和 boards 先处理才能为后面的 devicetree 和 Kconfig 提供板级信息。五、Bluetooth 子系统的 CMakeadd_subdirectory_ifdef 是裁剪的核心到了zephyr/subsys/bluetooth/CMakeLists.txt已对照 gitee 镜像核对核心机制全在这。实际内容add_library(subsys__bluetooth INTERFACE) target_include_directories(subsys__bluetooth INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(common) add_subdirectory_ifdef(CONFIG_BT_HCI host) add_subdirectory_ifdef(CONFIG_BT_SHELL shell) add_subdirectory_ifdef(CONFIG_BT_CONN services) add_subdirectory_ifdef(CONFIG_BT_MESH mesh) add_subdirectory_ifdef(CONFIG_BT_AUDIO audio) if(CONFIG_BT_CTLR AND CONFIG_BT_LL_SW_SPLIT) add_subdirectory(controller) endif()add_subdirectory_ifdef(CONFIG_X dir)的含义是只有CONFIG_Xy时才进入dir编译。这是 Kconfig 控制编译范围的核心机制。host 在CONFIG_BT_HCI时编services 在CONFIG_BT_CONN时编mesh 在CONFIG_BT_MESH时编。这里有个容易看错的点按实际源码更正Zephyr 软链路层 controller 目录的进入条件是if(CONFIG_BT_CTLR AND CONFIG_BT_LL_SW_SPLIT)两个条件都要满足不是只看CONFIG_BT_LL_SW_SPLIT。CONFIG_BT_CTLR是是否使用控制器的总开关CONFIG_BT_LL_SW_SPLIT是用 Zephyr 软链路层的具体选择两者是包含关系。少了CONFIG_BT_CTLR这个前提单独看BT_LL_SW_SPLIT会误判。Host 源文件裁剪在zephyr/subsys/bluetooth/host/CMakeLists.txt已对照 gitee 核对按 Kconfig 选源文件if(CONFIG_BT_HCI_HOST) zephyr_library_sources(uuid.c addr.c buf.c hci_core.c hci_common.c id.c) zephyr_library_sources_ifdef(CONFIG_BT_BROADCASTER adv.c) zephyr_library_sources_ifdef(CONFIG_BT_OBSERVER scan.c) if(CONFIG_BT_CONN) zephyr_library_sources(conn.c l2cap.c att.c gatt.c) if(CONFIG_BT_SMP) zephyr_library_sources(smp.c keys.c) else() zephyr_library_sources(smp_null.c) endif() endif() endif()zephyr_library_sources_ifdef(CONFIG_X file.c)是另一个裁剪原语CONFIG_Xy才把file.c加进编译列表。peripheral 样例 prj.conf 设了CONFIG_BT_PERIPHERALy、CONFIG_BT_SMPy而BT_PERIPHERAL会 selectBT_BROADCASTER和BT_CONN所以最终编译hci_core.c, adv.c, conn.c, l2cap.c, att.c, gatt.c, smp.c, keys.c这些。注意 SMP 没开时走的是smp_null.c空实现这是实际源码里的 else 分支别以为不开 SMP 就什么都不编。六、Kconfig 机制prj.conf 怎么变成 CONFIG_*zephyr/samples/bluetooth/peripheral/prj.conf关键项CONFIG_BTy是总开关CONFIG_BT_PERIPHERALy选 peripheral 角色CONFIG_BT_SMPy开配对加密CONFIG_BT_BASy/HRSy这些是内置 GATT 服务CONFIG_BT_SETTINGSy做密钥持久化。构建时 Zephyr 跑 Kconfig 解析器合并 board defconfig 和 app prj.conf生成build/zephyr/.config和autoconf.h。.config给人看autoconf.h给编译器用——所有CONFIG_Xy在autoconf.h里变成#define CONFIG_X 1C 代码里就能用#ifdef CONFIG_X来条件编译。Bluetooth Kconfig 树在zephyr/subsys/bluetooth/Kconfig已对照 gitee 核对menuconfig BT是总开关choice BT_STACK_SELECTION选栈类型config BT_HCI是默认项HCI 架构hostcontrollerHCI driverconfig BT_CUSTOM是自定义非 HCI 栈极少用。if BT_HCI下面config BT_PERIPHERAL会 selectBT_BROADCASTER和BT_CONNconfig BT_CENTRAL会 selectBT_OBSERVER和BT_CONN。最后source subsys/bluetooth/controller/Kconfig把 controller 选择拉进来。select 是 Kconfig 的关键机制选了BT_PERIPHERAL它依赖的BT_BROADCASTER、BT_CONN会被自动选中不用你手动开。这就是为什么 prj.conf 里只写角色不写一堆底层开关——select 链帮你补全了。七、Controller 选择两个独立 config 各依赖一个 devicetree 符号这是决定用哪个链路层的核心也是最绕的一环。关键认知controller 选择不是用 Kconfig 的 choice而是两个独立的 config各自依赖一个 devicetree 自动生成的符号。Zephyr 软链路层在zephyr/subsys/bluetooth/controller/Kconfiggitee 镜像对该文件触发内容过滤未能独立联网核对此处依据研究文档并标注config BT_LL_SW_SPLIT bool Software-based Bluetooth LE Link Layer [EXPERIMENTAL] default y depends on DT_HAS_ZEPHYR_BT_HCI_LL_SW_SPLIT_ENABLEDSoftDevice Controller 在nrf/subsys/bluetooth/controller/Kconfigconfig BT_LL_SOFTDEVICE bool SoftDevice Link Layer default y depends on SOC_SERIES_NRF54LX depends on DT_HAS_NORDIC_BT_HCI_SDC_ENABLEDDT_HAS_COMPAT_ENABLED是从 devicetree 自动生成的 Kconfig 符号只要有一个compatible xxx且status okay的节点对应符号就是 y。两个 config 都default y但只有 DT 节点使能的那个能真正被选中——因为depends on不满足时config 不会出现在可选列表里。这个设计把用哪个链路层的决定权交给了 devicetree而不是让用户在 Kconfig 里手动选。板子的 dts 描述了硬件上有什么 controller 节点Kconfig 自动跟着走。换板子时只要 dts 配对controller 选择自动切换不用改 prj.conf。八、Devicetreenrf54l15dk 怎么选中 SoftDevicedevicetree 这块分两步理解这两步的连锁反应是关键。第一步radio 节点下声明两个 HCI 子节点zephyr/dts/vendor/nordic/nrf54l_05_10_15.dtsiradio: radio8a000 { compatible nordic,nrf-radio; bt_hci_sdc: bt_hci_sdc { compatible nordic,bt-hci-sdc; status disabled; }; bt_hci_controller: bt_hci_controller { compatible zephyr,bt-hci-ll-sw-split; status disabled; }; };两个子节点默认都是 disabled。bt_hci_sdc对应 SoftDevice Controllerbt_hci_controller对应 Zephyr 软链路层。第二步SoC 级 dtsi 启用 SDCzephyr/dts/arm/nordic/nrf54l_05_10_15_cpuapp.dtsi/ { chosen { zephyr,bt-hci bt_hci_sdc; zephyr,entropy psa_rng; }; }; bt_hci_sdc { status okay; };这两步的连锁效应是bt_hci_sdc { status okay }让DT_HAS_NORDIC_BT_HCI_SDC_ENABLEDy于是BT_LL_SOFTDEVICE可选且默认 yzephyr,bt-hci bt_hci_sdc这个 chosen 让 host 知道用哪个 HCI 设备bt_hci_controller保持 disabledBT_LL_SW_SPLIT不可选。chosen 是 devicetree 里系统级指定的机制类似这个角色由谁扮演。zephyr,bt-hci这个 chosen 节点告诉 host你的 HCI 设备是bt_hci_sdc。后面 host 就是靠这个 chosen 找到 HCI 驱动的。九、host 怎么拿到 HCI 设备chosen 到驱动的两条路径这里要讲清楚一个容易混淆的点。研究文档里提到 host 在hci_core.c用DT_CHOSEN(zephyr_bt_hci)找到 HCI 设备。我对照 gitee 镜像 main 分支的subsys/bluetooth/host/hci_core.c核对发现上游 main 分支当前仍保留较老的bt_hci_driver_register()回调注册模型bt_init→hci_initdriver 通过bt_hci_driver_register(drv)注册host 调bt_dev.drv-open()、bt_dev.drv-send()收发并没有在固定某一行用DT_CHOSEN(zephyr_bt_hci)查找。这里需要分清两件事devicetree 层面的 chosen 指定zephyr,bt-hci bt_hci_sdc这个在 dtsi 里确实存在和 host 代码层面怎么消费这个 chosen。在 NCS v3.2.1 这条用 SoftDevice Controller 的路径上HCI 驱动是 nrf 仓库里的hci_driver.c它用DT_DRV_COMPAT nordic_bt_hci_sdcDEVICE_DT_INST_DEFINE为每个statusokay的nordic,bt-hci-sdc节点实例化一个驱动设备host 再通过 chosen 找到这个设备、拿到它的hci_driver_apiopen/send/close。而上游 zephyr main 的 hci_core.c 用的是回调式bt_hci_driver_register两者是不同版本/不同路径的注册方式。所以准确的表述是chosen 在 devicetree 层把用哪个 HCI 节点定下来具体到代码里 host 怎么拿到这个设备NCS v3.2.1 走的是基于 devicetree 实例化的设备模型DT_DRV_COMPATDEVICE_DT_INST_DEFINE而上游 main 还保留着回调注册的老模型。行号会随版本变动别死记某一行抓住chosen 指定节点 → 驱动按 compatible 实例化 → host 通过 chosen 或注册拿到 api这条主线即可。这部分我已对照 gitee main 核对ncs-v3.2.1 tag 不在镜像上以实际 NCS 源码为准。十、compatible binding节点的类型说明书两个 compatible 各有一个 binding 文件说明属性。zephyr,bt-hci-ll-sw-split的 binding 在zephyr/dts/bindings/bluetooth/zephyr,bt-hci-ll-sw-split.yamlnordic,bt-hci-sdc的 binding 在nrf/dts/bindings/bluetooth/nordic,bt-hci-sdc.yaml两者都include: bt-hci.yaml继承公共属性bt-hci-name、bt-hci-bus、bt-hci-quirks。binding 是 devicetree 的类型说明书它定义某个 compatible 的节点能有哪些属性、属性什么类型、默认值多少。构建系统读 binding 来校验 dts 写得对不对也靠 binding 生成给 C 代码用的宏比如DT_HAS_NORDIC_BT_HCI_SDC_ENABLED这种符号就是从 binding 节点状态自动生成的。十一、模块怎么进入构建nrf 和 nrfxlib 的注册nrf/通过nrf/zephyr/module.yml注册为 Zephyr 模块build: cmake: . kconfig: Kconfig.nrf settings: soc_root: . board_root: . dts_root: .soc_root、board_root、dts_root都指向 nrf 自己意思是让 nrf/dts 的 binding、nrf/boards 也被构建系统搜索到。这就是为什么 nrf 仓库里定义的nordic,bt-hci-sdcbinding 能被识别——模块注册时把自己的 dts_root 加进了搜索路径。nrfxlib/通过nrfxlib/zephyr/module.yml带cmake-ext: True作为外部库接入。它不贡献源码进 Zephyr 树而是以预编译库的形式被链接。模块机制是 Zephyr 构建系统的扩展点。第三方芯片厂、协议栈厂商要接入 Zephyr不用改 Zephyr 核心写个 module.yml 注册进来就行。Nordic 的 nrf 和 nrfxlib 就是这么接进来的。十二、最终链接了什么elf 里的成分清单对 nrf54l15dk app core最终固件包含这些成分。应用层是main.c和 cts.c前面更正过。Zephyr Host 是hci_core.c, conn.c, l2cap.c, att.c, gatt.c, smp.c, keys.c, adv.c这些。Nordic HCI driver 是nrf/subsys/bluetooth/controller/hci_driver.c仅BT_LL_SOFTDEVICEy时编译见nrf/subsys/bluetooth/CMakeLists.txt的add_subdirectory_ifdef(CONFIG_BT_LL_SOFTDEVICE controller)。SoftDevice Controller 是预编译静态库nrfxlib/softdevice_controller/lib/nrf54l/.../libsoftdevice_controller_variant.a由nrfxlib/softdevice_controller/CMakeLists.txt链入。还有 MPSL多协议调度库SDC 依赖、内核、驱动。hci_driver.c的设备注册nrf/subsys/bluetooth/controller/hci_driver.c#define DT_DRV_COMPAT nordic_bt_hci_sdc #define BT_HCI_CONTROLLER_INIT(inst) \ DEVICE_DT_INST_DEFINE(inst, hci_driver_init, ..., hci_driver_api) BT_HCI_CONTROLLER_INIT(0)DT_DRV_COMPAT声明这个驱动管的是nordic,bt-hci-sdc这个 compatibleDEVICE_DT_INST_DEFINE为每个statusokay的这种节点实例化一个驱动设备。host 通过 chosen 找到它拿到hci_driver_api里的 open/send/close。注意这里hci_driver.c在 nrf 仓库闭源的 SoftDevice Controller 库在 nrfxlib两者分开。hci_driver.c 是开源的 HCI 适配层它本身不含链路层逻辑只是把 host 的 HCI 调用转给 SoftDevice 库SoftDevice 库才是闭源的链路层实现。本文只讲 hci_driver.c 在构建里怎么被编进去、怎么注册设备不涉及 SoftDevice 库内部。十三、完整构建链路图把整条链路串起来看一次west build -b nrf54l15dk_nrf54l15_cpuapp samples/bluetooth/peripheral │ ▼ cmake -DBOARD... -S sample -B build │ ▼ 应用 CMakeLists.txt: find_package(Zephyr) → zephyr/CMakeLists.txt │ ▼ 处理 devicetree: bt_hci_sdc 节点 statusokay → DT_HAS_NORDIC_BT_HCI_SDC_ENABLEDy │ ▼ 处理 Kconfig: prj.conf(CONFIG_BTy, BT_PERIPHERALy) board BT_LL_SOFTDEVICEy → .config / autoconf.h │ ▼ zephyr/subsys/bluetooth/CMakeLists.txt: CONFIG_BT_HCIy → 编 host/; CONFIG_BT_LL_SW_SPLIT 未选 → 跳过 zephyr controller/ │ ▼ nrf/subsys/CMakeLists.txt: CONFIG_BTy → 进 nrf/subsys/bluetooth/; CONFIG_BT_LL_SOFTDEVICEy → 编 nrf controller/hci_driver.c │ ▼ nrfxlib/softdevice_controller/CMakeLists.txt: CONFIG_BT_LL_SOFTDEVICEy → 链接 libsoftdevice_controller_multirole.a │ ▼ host 通过 chosen 找到 bt_hci_sdc 设备 → hci_driver_api │ ▼ 链接: app Zephyr host nrf hci_driver SoftDevice 库 MPSL kernel → zephyr.elf这条链路里devicetree 是起点决定有哪些节点Kconfig 是中转把节点状态变成 CONFIG 开关CMake 是执行按开关决定编什么最后链接成 elf。四件套各管一摊但顺序固定、环环相扣。十四、动手跟读建议想真正吃透这套构建系统建议照着源码跟读一遍。第一步读zephyr/samples/bluetooth/peripheral/CMakeLists.txt和prj.conf看应用层最简的 CMake 和配置长什么样。第二步读zephyr/subsys/bluetooth/CMakeLists.txt理解add_subdirectory_ifdef怎么按 CONFIG 裁剪目录。第三步读zephyr/subsys/bluetooth/host/CMakeLists.txt看zephyr_library_sources_ifdef怎么按 CONFIG 选源文件注意 SMP 的 else 分支。第四步读zephyr/subsys/bluetooth/Kconfig的menuconfig BT和choice BT_STACK_SELECTION看 select 怎么自动补全依赖。第五步对照zephyr/subsys/bluetooth/controller/Kconfig和nrf/subsys/bluetooth/controller/Kconfig理解 controller 选择的 DT 依赖。第六步读nrf54l_05_10_15.dtsi的 radio 子节点和nrf54l_05_10_15_cpuapp.dtsi的 chosen enable看 devicetree 怎么定 controller。第七步读nrf/subsys/bluetooth/controller/hci_driver.c的DT_DRV_COMPAT和DEVICE_DT_INST_DEFINE看驱动怎么按节点实例化。跟读的时候重点抓三个东西devicetree 节点状态怎么变成 Kconfig 符号、Kconfig 符号怎么控制 CMake 编译范围、chosen 怎么把 host 和具体 HCI 驱动连起来。这三点搞清楚构建系统的骨架就立起来了。写在最后Zephyr 的构建系统看起来四件套很复杂拆开看就一条主线devicetree 描述硬件 → Kconfig 把硬件状态变成编译开关 → CMake 按开关决定编哪些文件 → 链接成固件。west 只负责把代码摆到位。把add_subdirectory_ifdef的目录裁剪、zephyr_library_sources_ifdef的文件裁剪、DT_HAS_*_ENABLED的 DT 到 Kconfig 桥接、chosen 的节点指定这四点想通后面遇到任何为什么没编进去为什么选不中的构建问题都能顺着这条链路定位到是哪一环出了问题。下一篇会展开讲空中报文接收的完整链路看一个无线包从 radio 收下来经 Controller、HCI到 Host 怎么一层层往上送。如果你正在啃 Zephyr 构建系统建议照着源码把这条链路跟读一遍从west build一直跟到zephyr.elf走一遍比看十遍文档都管用。标签#Zephyr #BLE #构建系统 #CMake #Kconfig #Devicetree #嵌入式开发 #NCS #nRF54L #west
返回列表