网络研讨会:Zephyr 技术工坊 —— 设备驱动开发

Zephyr 网络研讨会日期:2025 年 4 月 24 日

主要内容提要

本次技术工坊中, Shawn Hymel 将指导大家完成基于 Zephyr 的 I2C 温度传感器设备驱动开发流程。设备驱动是 Zephyr 系统的核心组成部分之一,通过自主开发驱动,开发者能够掌握 C 语言编程、CMake 构建、Kconfig 配置以及设备树(Devicetree)的实战应用。掌握这些语言和工具链,是开展 Zephyr 应用开发的关键前提。

若你计划同步实操,建议准备以下硬件:

常见问题解答

  1. 能否推荐一些基于 NRF5340、Zephyr 和蓝牙低功耗(BLE)的项目案例? 基于支持 BLE 的微控制器,可开发的项目类型十分丰富。以下为部分参考方向:智能手表、智能家居传感器、可穿戴计步器、电子墨水屏待办清单。

  2. 可以将 Zephyr 理解为一款开发框架吗? 在嵌入式与编程领域,“框架” 这一术语的定义相对宽泛。但从本质而言,Zephyr 属于一款集成式开发框架,其核心包含实时操作系统(RTOS)、对接厂商硬件抽象层(HAL)的抽象接口层、构建系统、中间件库,以及各类测试与调试工具。

  3. 相较于 FreeRTOS,Zephyr 的优势体现在哪里? FreeRTOS 的核心仅为一个任务调度器。尽管亚马逊近年来为其新增了部分网络协议栈,以简化物联网(IoT)应用开发,但 FreeRTOS 并未提供 Zephyr 所具备的设备无关抽象层。这意味着,使用 FreeRTOS 开发时,开发者仍需依赖芯片厂商提供的 HAL 库才能操作具体硬件;而基于 Zephyr 开发的应用可实现真正的设备无关性,大幅降低代码向其他硬件平台移植的难度。

  4. 是否有现成脚本可用于快速搭建驱动桩代码? 目前暂无专用的桩代码搭建脚本,但 Nordic 半导体的下述文档可作为入门参考:docs.nordicsemi.com

  5. Zephyr 系统会产生多少资源开销? 以简单的 LED 闪烁程序为例,Zephyr 的运行开销约为 16KB 内存(RAM)32-64KB 闪存(Flash);上下文切换操作约占用数百个 CPU 时钟周期。需注意的是,若启用 WiFi、蓝牙、LVGL 图形库等更多功能或驱动,系统开销会相应增加。以下文档提供了 Zephyr 与 RIOT-OS 的性能对比基准测试:github.com/zephyrproject-rtos/zephyr

  6. sample_fetch()channel_get() 这两个函数会在什么场景下被调用?调用方是谁? 非常抱歉,研讨会中对此部分的说明不够清晰。严格来说,sample_fetch()channel_get() 并非回调函数,而是被映射到API 结构体中的函数指针。在系统启动阶段,内核完成驱动初始化后,上层应用程序会通过该 API 结构体直接调用这两个函数

  7. 为什么 Zephyr 项目在设计中借鉴了 C++ 的部分机制? 具体设计初衷暂无公开资料可查,但推测 Zephyr 开发团队的考量是:确保代码可在几乎所有微控制器上编译运行,包括仅支持传统 C 语言编译器的老旧硬件平台。

  8. 我在 WSL2 中运行 Zephyr 开发容器,如何将 USB/ACM 设备接入容器以实现调试或查看串口控制台?能否避免手动指定设备并重新创建容器? 该方案的稳定性在不同主机操作系统中表现不一。推荐尝试 USB-IP 技术,下述社区讨论可作为参考:
    How to use a host USB device in a container in Docker Desktop? - Docker Desktop - Docker Community Forums

  9. 我使用远程容器扩展插件运行容器,如何将 ACM 设备接入容器进行调试或查看串口?目前已知的方法是通过指定设备参数创建新容器,是否有更优方案? 暂无其他更简便的方法。若你的主机操作系统支持 USB-IP 技术,这将是最佳选择。调试方面,可通过网络协议跨容器边界将 GDB 连接至 OpenOCD。

  10. 能否为 Zephyr 添加未内置的传感器通道? 需通过修改 Zephyr 源码实现。更稳妥的方案是:复用现有传感器通道,或基于 Zephyr 的 “device” 设备类型,自定义一套类传感器接口。

  11. 我使用的是 HiFive1 Rev B 开发板,本次研讨会的教程是否适用于该硬件? 本人暂无该开发板的使用经验,建议参考 Zephyr 官方文档:https://docs.zephyrproject.org/latest/boards/sifive/hifive1/doc/index.html

  12. 驱动中的数值转换逻辑能否正确处理负温度数据?(我推测是通过 val1 存储负数、val2 存储正数实现) 理论上该逻辑可支持负温度数据的处理,其核心是对符号位的判断。但该功能未经实际测试验证。若你发现存在异常,可通过 GitHub 代码仓库提交 Issue 或 Pull Request: GitHub - ShawnHymel/workshop-zephyr-device-driver

  13. 管理 Zephyr 版本的最佳实践是什么?例如使用 GitHub 标签?日常开发中如何指定 Zephyr 版本? 个人推荐在 Dockerfile 中固定 Zephyr 版本,这种方式更便于制作技术教程类内容。Zephyr 官方建议使用 West 元工具管理项目版本及依赖(包括 Zephyr 内核本身),相关用法可参考官方文档: West (Zephyr’s meta-tool) — Zephyr Project Documentation

  14. Zephyr 可集成哪些图形用户界面(GUI)? Zephyr 默认支持两种方案:一是用于显示简单文本的 字符帧缓冲(参考文档: Character frame buffer — Zephyr Project Documentation);二是 LVGL 图形库(参考文档: LVGL basic sample — Zephyr Project Documentation)。

  15. Node MCU 开发板的 .dtsi 设备树文件是否可在 Zephyr 官方仓库中找到? Zephyr 官方暂未支持 Node MCU 开发板,因此无法直接获取对应的 .dtsi 文件。不过 Zephyr 已支持 ESP8266 芯片(参考文档: ESP-8266 Modules — Zephyr Project Documentation)。若需适配 Node MCU,需自行编写自定义板级支持包,具体可参考 Zephyr 官方的板级移植指南: Board Porting Guide — Zephyr Project Documentation

  16. 驱动的匹配过程是在运行时还是编译时完成的? 推测你所指的是 Zephyr 构建系统通过 “compatible” 兼容性字符串,将驱动源码与设备树节点进行匹配的过程。该过程是在 编译阶段 完成的。

  17. 模块(Module)属于 SDK 的一部分吗? 该问题的表述稍显模糊。从定义来看,SDK 是工具、库、文档及示例代码的集合,因此 Zephyr 本身可被视为一套 SDK。而模块本质上是符合 Zephyr 规范的软件库;若为该模块配套开发工具、文档、示例等资源,则该模块(或模块集合)可被纳入 SDK 的范畴。

  18. 能否开发依赖并调用其他设备驱动的驱动程序? 可以。本次研讨会的示例中,我们开发的温度传感器驱动就依赖并调用了 I2C 驱动,希望这个案例能为你提供参考。

  19. i2c_write/read_dt 函数是否会关联到 ESP 芯片的裸机驱动? 是的。你可以查看 Zephyr 源码的 drivers/i2c 目录(仓库链接: zephyr/drivers/i2c at main · zephyrproject-rtos/zephyr · GitHub),其中展示了 i2c_write/read 等抽象接口函数与底层硬件驱动的关联逻辑。这些底层硬件驱动通常由芯片厂商或 Zephyr 官方维护。当设备树指定了具体的开发板、芯片及 I2C 总线后,上层的 i2c_write 函数会自动调用该芯片对应的底层驱动函数(例如 ESP32 平台的 i2c_esp32_transmit())。

  20. 是否有计划推出现代嵌入式 C++ 相关的教程系列? 目前暂无相关规划,但这是我未来希望涉足的方向。C++ 和 Rust 语言在嵌入式领域的应用正日益广泛,我也期待后续能推出相关内容。

  21. 编写 MCP9808 驱动时,是否需要参考芯片数据手册,逐寄存器编写代码?还是只需遵循 Zephyr 的驱动开发规范,调用乐鑫(Espressif)提供的 API 即可? 必须参考芯片数据手册,明确芯片的设备地址、寄存器地址、寄存器读写位定义,以及需实现的功能特性。Zephyr 驱动开发规范仅提供框架,芯片相关的底层逻辑仍需开发者基于数据手册完成。

  22. 本次 Zephyr 编译生成的固件大小是多少? 最终编译出的固件大小为 138KB。对于一个简单的 I2C 数据读取示例而言,该体积已属于较大范畴,这也体现了 Zephyr 系统本身的资源开销特性。

  23. Zephyr 是否提供或支持 JESD204C 驱动? 据我所知,Zephyr 暂未实现 JESD204C 协议的驱动

  24. Zephyr 的驱动架构是否与 Linux 的 FileOps 结构体类似? 是的。你会发现 Zephyr 与 Linux 在驱动架构设计上存在诸多相似之处。

  25. 能否为驱动的文件目录结构创建模板? Zephyr 未内置目录结构模板工具,但可通过编写简单的 Bash 或 Python 脚本实现该需求。

  26. 如何在 Zephyr 中调用动态 C 语言库?例如我有一个基于 C/C++ 开发的传感器数据分析专用 AI 模块。 需在 Kconfig 配置中启用 CONFIG_CPP 选项,使 Zephyr 构建系统能够编译并链接 C++ 库。相关配置方法可参考官方文档: C++ Language Support — Zephyr Project Documentation

  27. 在主函数中逐一检查设备状态是否为 Zephyr 开发的通用范式? 是的。在 Zephyr 开发中,通常需要执行两步检查:一是通过空指针检查,确认设备是否存在于设备树中;二是调用 device_is_ready() 函数,确认设备是否已在系统启动阶段完成初始化。

  28. 能否切回研讨会的 “问题” 标签页? 非常抱歉,直播过程中未能及时查看该标签页。当时列出的问题主要是 IntelliSense 无法识别部分符号(例如 CONFIG_SYS_CLOCK_TICKS_PER_SEC),但这些符号在编译过程中均可正常调用。尽管 IntelliSense 工具存在小瑕疵,但已能满足 Zephyr 开发与教学的基本需求。

  29. 我的测试代码仅实现简单功能,却占用了 50KB 内存…… 可见 Zephyr 在后台加载了大量组件。 确实如此。Zephyr 包含丰富的抽象层,且系统启动后会默认运行一个或多个后台线程,用于处理工作队列、日志等任务,这些都会产生额外的内存开销。

  30. 我接手了一个已完成的 Zephyr 项目,需要对其进行功能扩展,但目前毫无头绪 —— 既要学习 Zephyr 框架,又要理解项目本身。你有什么入门建议? 建议动手完成其中的实战练习,以深入理解 Zephyr 的核心开发流程。在此基础上,可将目标项目拆解为多个模块进行分析:主函数的位置在哪里?主函数调用了哪些功能函数?深入这些函数内部,理清所依赖的驱动模块;同时分析项目针对特定开发板的设备树与 Kconfig 配置。Zephyr 项目的复杂度较高,这个学习过程可能需要数天甚至数周时间。你可以尝试将代码片段复制到 ChatGPT 或 Claude 等工具中,辅助理解代码逻辑。希望这些建议能帮到你!

  31. 调试 Zephyr 项目时,更推荐使用 Visual Studio(VS)还是其他 IDE?例如 J-Link 配套的 Embedded Studio? 推荐使用你最熟悉的 IDE。我个人更偏好 VS Code,原因是我对其操作较为熟练,且该工具在开发者群体中普及率较高。Zephyr 基于 GDB 进行调试,而 GDB 可与 OpenOCD 无缝对接。绝大多数 IDE 都支持配置 GDB,实现单步调试、内存查看等功能。

  32. 是否推荐使用 T3:树形拓扑结构管理 Zephyr 项目? 我在教学中使用的是类似 T3 的拓扑结构,但为了简化学生的操作流程,并未采用 West 工具管理代码仓库。T3 拓扑适用于多项目场景,且适合直接基于 Zephyr 内核进行开发。而 T1 拓扑则是官方推荐用于大型复杂项目的架构。

  33. 我已成功通过 usbipd 工具将 J-Link 调试器接入 Docker 容器 —— 该过程虽需一些配置工作,但实现了在容器内通过 West Flash 命令烧录固件。West 工具真的非常好用! 很高兴听到这个消息!我也非常喜欢使用 West 工具进行固件烧录。但遗憾的是,我尚未找到能在所有主流操作系统中稳定运行的 USB 透传方案(尤其是 macOS 系统),而教学场景需要兼顾跨平台兼容性。

  34. 编译器能否对函数指针调用进行内联优化?还是说运行时会产生大量的间接调用开销? 当驱动中将 API 函数指针赋值给 API 结构体时,编译器无法对这些函数调用进行内联优化,因此运行时会产生一定的间接调用开销。这是 Zephyr 为实现更高的硬件抽象性所付出的必要代价。

  35. 我是 Zephyr 新手,想请教:在引入大量抽象层的情况下,Zephyr 的实时性与确定性表现如何? 函数间接调用机制确实会带来一定的处理器开销(尤其是将函数指针赋值给 API 结构体的场景)。但需明确的是,Zephyr 的核心定位是实时操作系统,其大部分功能都能保证确定性。需要注意的是,部分功能不具备确定性(例如网络协议栈、日志系统、动态内存分配、工作队列等)。

  36. 为什么在一个文件中 “my-mcp9808” 采用连字符命名,而在另一个文件中却使用下划线命名? 这是为了演示 Zephyr 如何通过字符替换机制匹配设备树中的字符串(直播时我可能遗漏了这一点的说明),该机制与 “compatible” 兼容性字符串的处理逻辑类似。按照惯例,设备树中的别名标签使用连字符分隔;而 C 语言宏定义不支持连字符,因此 Zephyr 构建系统会自动将设备树中的连字符替换为下划线,以实现字符串匹配。

  37. 为什么驱动代码的目录结构中会出现两次 “MCP9808” 命名?例如 MCP9808/drivers/MCP9808,父目录已明确是 MCP9808 相关,子目录重复命名是否多余? 这是 Zephyr 源码的命名惯例,并非强制要求。查看 Zephyr 官方驱动即可发现,类似 <device_name>/drivers/<device_name> 的目录结构十分常见。实际上 Zephyr 构建系统并不校验目录名称,只要满足以下条件,驱动即可正常编译并链接:

  • 设备树绑定文件存放路径正确;
  • 顶层目录包含 zephyr/module.yaml 配置文件;
  • CMakeLists.txt 脚本正确配置了驱动源码的编译目标。
  1. 驱动绑定文件的命名为什么使用逗号而非下划线? 这是 Zephyr 借鉴自 Linux 的命名惯例:设备树 “compatible” 兼容性字符串通常采用逗号分隔格式。该命名方式的设计并未过多考虑其在 C 语言中的兼容性(尤其是作为宏定义时)。你也可以使用下划线命名,功能不受影响,但建议遵循 Zephyr 的官方惯例。

  2. 在本教程的语境中,“宏(macro)” 具体指什么? 在 C 语言中,宏是一段由预处理器在编译前展开的代码片段,常见形式包括 #if 条件编译指令、#define 宏定义指令等。

  3. 是否可以自定义配置项,例如 CONFIG_NAME_MCP9809=y 基本可以实现。你可在 Kconfig 文件中定义自定义配置项,Zephyr 构建系统会自动识别这些配置。本次研讨会的示例中,我们在 modules/mcp9808/drivers/mcp9808/Kconfig 文件中定义了自定义配置项 MCP9808,在 C 代码中即可通过 CONFIG_MCP9808 宏获取该配置项的状态。

  4. Zephyr 的 I2C 驱动能否使用中断模式? 可以。通常在设备树中进行配置,将 I2C 外设(或其他外设)与中断控制器关联即可。多数开发板与芯片厂商的默认配置均为中断模式(而非轮询模式)。你可参考 ESP32S3 芯片的设备树文件,其中 I2C0 节点默认启用了中断控制器(intc):zephyr/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi at bef5abd293440a6c76655787f1c43d4ee511ca7e · zephyrproject-rtos/zephyr · GitHub

  5. 如何组织 Zephyr 应用项目?是将所有应用放在一个工作区,还是为每个应用创建独立工作区? 存在多种组织方式。Zephyr 官方推荐为每个大型复杂应用创建独立工作区,这对应官方文档中的 “T1 拓扑结构”。我在教学中则更倾向于使用 “T3 拓扑结构”,将多个小型项目集中在一个工作区中管理。关于这些推荐拓扑结构的详细说明,可参考官方文档: Workspaces — Zephyr Project Documentation

  6. 驱动依赖 I2C 时,为什么不使用 select I2C 配置项?这种写法与直接依赖相比有何优劣? 你提出的观点非常正确:使用 select I2C 配置项是更优方案,可自动启用 I2C 依赖。我在教程中未采用该写法,主要是为了演示依赖项的配置原理(如果时间充裕,本计划在 menuconfig 配置界面中展示这一点)。

  7. 对 Zephyr 的 USB 协议栈有何评价?关于在同一 USB 总线上实现多设备类的文档较少,是否推荐集成 TinyUSB 协议栈? 本人暂无 Zephyr USB 协议栈的使用经验,无法给出具体评价。从官方文档来看,Zephyr 已支持大部分常见的 USB 设备类,相关信息可参考: USB device support (deprecated) — Zephyr Project Documentation

  8. 虽然本次研讨会的主题不涉及,但能否简要介绍 ztest 测试框架?例如其用途、使用方法等。我们能否通过测试固件的方式深化对 Zephyr 的学习? 本人暂无 ztest 框架的使用经验,原本计划在后续内容中补充,但本次研讨会及视频系列的时间有限,未能涉及。总体而言,建议对量产级固件采用测试驱动开发(TDD)模式。尽管编写测试代码会增加工作量,但能有效提前发现潜在问题,避免故障在实际场景中发生。据我了解,ztest 的基本用法是:基于该框架编写单元测试,然后通过 twister 工具管理测试流程。

  9. 驱动目录结构中为何需要重复的 MCP9808 命名?父目录的命名已能体现用途,是否多余? 这是 Zephyr 源码的命名惯例,并非强制要求。查看 Zephyr 官方驱动即可发现,类似 <device_name>/drivers/<device_name> 的目录结构十分常见。实际上 Zephyr 构建系统并不校验目录名称,只要满足以下条件,驱动即可正常编译并链接:

  • 设备树绑定文件存放路径正确;
  • 顶层目录包含 zephyr/module.yaml 配置文件;
  • CMakeLists.txt 脚本正确配置了驱动源码的编译目标。
  1. 读取传感器数据时,将 fetch(数据采集)与 get(数据读取)操作分离有何优势?为什么要将这两个接口暴露给应用层,而非在驱动内部封装? 我并不清楚 Zephyr 开发团队的具体设计考量。个人推测,分离这两个操作的优势在于:可将数据采集与数据读取任务分配到不同线程—— 数据采集可在后台线程中完成,上层应用只需获取最新的采集结果,无需等待冗长的 I2C 读写周期。

  2. 若使用的传感器未被 Zephyr 传感器通道枚举包含,是否仍推荐使用 “sensor” 驱动模板? 并非必须。对于此类传感器,使用通用的 “device” 设备类型 / 接口即可满足需求。“sensor” 接口的优势在于:内置了 fetch/get 分离机制,且提供了标准化的数据存储结构。你也可以从传感器通道枚举中选取一个占位通道,同样能实现功能。

  3. 超级循环(super loop)是否会在每次迭代中调用所有设备的接口? 并非如此。Zephyr 构建系统会展开驱动代码末尾的宏定义,为设备树中的每个设备实例生成对应的驱动结构体副本。作为开发者,你可根据需求选择是否调用这些设备的 fetch/get 函数。

  4. 超级循环仅会调用指定地址的设备,若需支持其他设备,是否需要修改超级循环代码? 是的,你的理解完全正确。

  5. 是否有资料解释 Zephyr 驱动目录结构的设计原理?区分该结构中的 node_idinst 字段十分重要,二者的映射关系对开发影响很大。 目前暂无专门讲解目录结构设计原理的资料,但以下资源可帮助你从零开始学习驱动开发:
    Device Driver Model — Zephyr Project Documentation
    How to Build Drivers for Zephyr RTOS – Zephyr Project

  6. 本次研讨会是否涉及 Zephyr 跨平台代码的实现原理?是否通过预处理指令实现? Zephyr 实现跨平台应用开发的机制有多种:其一,大量使用预处理指令(这一点在本次研讨会的驱动代码中已有体现);其二,借助设备树(Devicetree)实现。当你在构建命令中指定目标开发板时,Zephyr 会为应用(.overlay 文件)、开发板(.dts.dtsi 文件)以及控制器(通常为 .dtsi 文件)生成编译后的设备树。该设备树包含目标硬件的所有外设信息,Zephyr 会据此匹配对应的驱动源码(例如 ESP32 的 I2C 驱动)。通过统一的抽象接口(如本次提到的 fetch/get 函数)和函数指针机制,Zephyr 会调用底层驱动的具体实现。若修改设备树并切换至其他开发板(如 STM32 Nucleo),Zephyr 会在编译时自动匹配 STM32 的 I2C 驱动,此时 I2C 相关函数会指向意法半导体(ST)提供的驱动实现。多数情况下,底层硬件驱动由芯片厂商维护,开发者只需专注于应用代码及非标硬件(如特殊传感器)的驱动开发。这一机制可大幅降低代码向其他微控制器平台移植的难度,仅需编写适配新硬件的设备树覆盖文件(.overlay)即可。

  7. 头文件 <zephyr/drivers/sensor.h> 应该用双引号 " 包裹,还是尖括号 <>?它不是用户自定义头文件吗? 所有以 zephyr/ 开头的头文件均属于 Zephyr 官方源码。因此,我将其视为系统级头文件,使用尖括号 <> 包裹(表示从系统头文件路径中查找)。Zephyr 官方示例代码也采用了同样的写法,可参考: example-application/app/src/main.c at main · zephyrproject-rtos/example-application · GitHub

研讨会链接参考

如需获取本次研讨会的更多资料,可查看以下内容: event.on24.com/wcc/r/4870160/B0654D058D6BC12D5A08B9607370682A?partnerref=techfrm

拓展学习资源

  1. 马丁・兰帕赫尔(Martin Lampacher)的 Memfault 专栏: Author: lampacher | Interrupt

  2. Zephyr 官方文档: Build System (CMake) — Zephyr Project Documentation

  3. Zephyr 官方文档: Devicetree bindings — Zephyr Project Documentation

  4. DigiKey 网络研讨会中心: Webinars free, live and on demand | DigiKey

  5. DigiKey 技术论坛网络研讨会专栏: Latest webinar topics - DigiKey TechForum - An Electronic Component and Engineering Solution Forum