
在嵌入式开发领域Zephyr RTOS 因其模块化、跨平台特性和对多种无线协议的原生支持正成为物联网设备开发的热门选择。然而对于习惯了传统 MCU 裸机或 HAL 库开发的工程师来说Zephyr 引入的设备树Device Tree概念常常是第一个“拦路虎”。很多开发者会困惑为什么以前直接操作寄存器或调用库函数就能点亮的 LED现在需要写一堆看似复杂的.dts文件本文将以 STM32F103C8T6 最小系统板上的 GPIO 控制 LED 为例带你从零开始不仅实现 LED 的状态指示更深入理解 Zephyr 设备树的工作机制、它与驱动的关联以及如何利用这套体系简化多平台开发。本文适合有一定嵌入式基础了解 GPIO 基本概念希望从传统开发转向 Zephyr 的开发者。你将学习到如何为一块新的开发板配置 Zephyr 环境、如何编写设备树描述硬件、如何通过 Zephyr 的标准 GPIO API 操作外设并最终理解这套机制如何实现“一次编写到处运行”。我们将从环境搭建开始逐步完成一个可编译、可下载、可观察 LED 闪烁的完整项目。1. 理解 Zephyr 设备树从硬件抽象到驱动绑定在传统嵌入式开发中我们通常通过宏定义或全局变量来标识硬件资源例如#define LED_GPIO_PORT GPIOA #define LED_PIN GPIO_PIN_5这种方式简单直接但硬编码的信息散落在代码各处。当更换 MCU 型号或引脚时需要修改多处源代码并重新编译。对于支持多种硬件的 RTOS 或 SDK这种方式难以维护。Zephyr 借鉴了 Linux 的设备树Device Tree思想来解决这个问题。其核心逻辑是将硬件描述与驱动代码分离。1.1 设备树是什么设备树是一种描述硬件组件及其连接关系的结构化数据格式通常以.dts源文件或.dtsi包含文件形式存在。它不包含任何可执行代码只声明硬件信息例如片上外设如 GPIO 控制器、I2C、ADC的内存映射地址、中断号。板级外设如 LED、按键、传感器连接到了哪个控制器的哪个引脚。外设的初始状态启用/禁用和特定参数如时钟频率。在 Zephyr 构建过程中这些.dts文件会被编译处理最终生成一个 C 头文件通常是devicetree_generated.h其中包含了所有硬件信息的宏定义。驱动代码通过一套固定的 API 来查询这些宏从而获知如何操作具体的硬件。1.2 为什么需要设备树硬件描述与代码解耦同一份驱动代码如 GPIO 驱动可以服务于不同板卡只需提供不同的设备树文件即可。统一硬件接口为不同的 MCU 厂商如 Nordic、ST、NXP提供了统一的硬件描述方式驱动开发者只需针对设备树接口编程。构建时配置通过设备树中的status “okay”或status “disabled”属性可以在构建时决定启用或禁用某个外设无需修改代码。工具链支持配合binding文件YAML 格式可以在编辑.dts时获得语法检查、自动补全和文档提示降低出错概率。1.3 设备树、驱动与应用程序的关系理解这三者的关系至关重要设备树.dts/.dtsi声明硬件“有什么”和“怎么连”。例如声明开发板上 PA5 引脚连接了一个用户 LED。驱动Driver知道“怎么用”。例如Zephyr 的 GPIO 驱动知道如何配置 STM32 的 GPIO 控制器为输出模式如何设置引脚电平。驱动通过读取设备树提供的信息来初始化具体的硬件实例。应用程序Application发出“做什么”的指令。例如应用程序调用gpio_pin_set()函数。它不关心 LED 具体接在哪个引脚只关心操作名为led0的设备。构建系统CMake负责将它们串联起来它解析设备树根据其中的compatible属性找到对应的驱动并将硬件配置信息传递给驱动进行初始化。最终应用程序通过一个统一的设备句柄来操作硬件。2. 环境准备与项目创建在开始编码前我们需要准备好 Zephyr 开发环境。这里假设你使用 Windows 或 Linux 系统。2.1 安装 Zephyr SDK 和工具链最简便的方式是使用 Zephyr 的官方工具west进行管理。安装 Python 3.8 和 pip。安装 westpip install west获取 Zephyr 源码并初始化此步骤耗时较长需下载数 GB 数据west init zephyrproject cd zephyrproject west update导出 Zephyr 环境变量Linux/macOS:source zephyr/zephyr-env.shWindows (PowerShell):.\zephyr\zephyr-env.cmd安装依赖根据 Zephyr 文档安装额外的工具如 CMake, Ninja, DTK, 等。通常west会给出提示。2.2 创建项目目录结构我们不直接在 Zephyr 源码树中开发而是创建一个独立的应用项目。mkdir zephyr_led_blink cd zephyr_led_blink创建以下文件和目录zephyr_led_blink/ ├── CMakeLists.txt ├── prj.conf ├── src/ │ └── main.c └── boards/ └── stm32f103c8t6.overlay2.3 配置项目构建文件CMakeLists.txt: 告诉构建系统这是一个 Zephyr 应用。cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(led_blink) target_sources(app PRIVATE src/main.c)prj.conf: 项目配置启用我们需要的模块。对于简单的 GPIO 控制至少需要# 启用 GPIO 驱动 CONFIG_GPIOy3. 为 STM32F103C8T6 配置设备树STM32F103C8T6蓝桥杯、野火等常见最小系统板在 Zephyr 中已有基本的支持。我们需要做的是在板级描述的基础上通过overlay文件添加我们自定义的 LED 节点。3.1 理解板级基础设备树Zephyr 已经为stm32f103c8t6这类芯片提供了 SoC 级定义dts/arm/st/stm32f103c8t6.dtsi和板级定义。我们可以先检查现有定义中是否已经包含了用户 LED。通常最小系统板不会预定义 LED需要我们自己添加。3.2 创建板级覆盖文件Overlay在boards/目录下创建stm32f103c8t6.overlay文件。Overlay 文件的作用是覆盖或扩展基础设备树中的定义。我们需要做两件事确保 GPIO 控制器gpioa已启用。添加一个 LED 节点并指定它连接到gpioa的某个引脚例如 PA5这是很多 STM32 最小系统板用户 LED 的连接引脚。// boards/stm32f103c8t6.overlay / { leds { compatible gpio-leds; led0: led_0 { gpios gpioa 5 GPIO_ACTIVE_HIGH; label User LED; }; }; };逐行解释/ { ... };: 表示从设备树的根节点开始添加内容。leds { ... };: 创建一个名为leds的节点。节点名称可以自定义但gpio-leds驱动会查找名为leds的节点。compatible “gpio-leds”;:这是最关键的一行。它告诉 Zephyr这个节点与gpio-leds驱动兼容。构建系统会根据这个字符串去匹配驱动。led0: led_0 { ... };: 定义一个子节点led0是标签label用于在代码中引用此节点。led_0是节点名称。gpios gpioa 5 GPIO_ACTIVE_HIGH;: 定义 LED 的硬件连接。gpioa: 引用 GPIOA 控制器节点。这个节点在 SoC 的.dtsi文件中已定义。5: 引脚号即 PA5。GPIO_ACTIVE_HIGH: 这是一个宏表示高电平点亮 LED。如果 LED 是低电平点亮则使用GPIO_ACTIVE_LOW。3.3 设备树中的“域”Domain概念你可能注意到gpios属性的格式有点特殊controller pin flags。这引出了设备树中一个重要的逻辑概念域Domain。除了基于地址的树状结构设备树还逻辑上附加了诸如 GPIO 域、中断域等。gpios属性就是一个phandle-array类型。它的第一个元素是一个句柄gpioa指向该 LED 所属的控制器GPIO 域。后续元素5,GPIO_ACTIVE_HIGH则是该设备在域内的具体配置称为specifier。GPIO 控制器节点中会定义#gpio-cells属性例如#gpio-cells 2;这表示每个 specifier 需要占用 2 个单元cells。在我们的例子中5是引脚号GPIO_ACTIVE_HIGH是标志位。这种设计使得驱动能以一种统一的方式解析来自不同控制器的配置。4. 编写应用程序代码现在硬件已在设备树中描述我们可以编写应用程序代码来操作它。src/main.c:#include zephyr/kernel.h #include zephyr/drivers/gpio.h /* 1000 msec 1 sec */ #define SLEEP_TIME_MS 1000 /* 通过设备树获取 LED0 的设备指针。 * led0 是我们在 overlay 文件中定义的节点标签label。 */ #define LED0_NODE DT_ALIAS(led0) /* 检查设备树中是否存在 led0 这个别名。 * 如果不存在则定义一个空设备。 */ #if DT_NODE_HAS_STATUS(LED0_NODE, okay) #define LED0 DT_GPIO_LABEL(LED0_NODE, gpios) /* 已废弃的旧 API仅作演示 */ #define PIN DT_GPIO_PIN(LED0_NODE, gpios) #define FLAGS DT_GPIO_FLAGS(LED0_NODE, gpios) #else /* 如果未定义则生成构建错误 */ #error Unsupported board: led0 devicetree alias is not defined #define LED0 #define PIN 0 #define FLAGS 0 #endif /* 使用新的、推荐的 gpio_dt_spec API */ static const struct gpio_dt_spec led GPIO_DT_SPEC_GET(LED0_NODE, gpios); void main(void) { int ret; bool led_state false; /* 检查设备是否就绪 */ if (!device_is_ready(led.port)) { printk(Error: LED device %s is not ready\n, led.port-name); return; } /* 配置 GPIO 引脚为输出模式并初始化为低电平 */ ret gpio_pin_configure_dt(led, GPIO_OUTPUT_INACTIVE); if (ret 0) { printk(Error %d: failed to configure LED pin\n, ret); return; } printk(Blinking LED on %s pin %d\n, led.port-name, led.pin); while (1) { /* 切换 LED 状态 */ led_state !led_state; ret gpio_pin_set_dt(led, (int)led_state); if (ret 0) { printk(Error %d: failed to set LED pin\n, ret); return; } /* 延时 */ k_msleep(SLEEP_TIME_MS); } }代码关键点解析包含头文件zephyr/kernel.h提供内核 API如k_msleepzephyr/drivers/gpio.h提供 GPIO 驱动 API。设备树节点访问DT_ALIAS(led0): 获取设备树中别名为led0的节点标识符。我们可以在主设备树或 overlay 中通过/aliases { led0 led0; };来设置别名这样代码更具可移植性。在我们的 overlay 中节点标签就是led0所以DT_NODELABEL(led0)也可以但使用别名是更推荐的做法。DT_NODE_HAS_STATUS: 检查节点状态是否为 “okay”。这是确保设备可用的标准方法。GPIO_DT_SPEC_GET:这是推荐的现代 API。它一次性从设备树节点中提取出gpio_dt_spec结构体其中包含了设备指针、引脚号和标志位无需再分别调用DT_GPIO_LABEL,DT_GPIO_PIN,DT_GPIO_FLAGS。设备就绪检查device_is_ready()是必须的。它检查驱动是否已成功初始化并与硬件绑定。引脚配置gpio_pin_configure_dt()使用gpio_dt_spec配置引脚。GPIO_OUTPUT_INACTIVE表示初始化为输出模式且输出低电平对于GPIO_ACTIVE_HIGH的 LED 来说是熄灭状态。引脚控制gpio_pin_set_dt()用于设置引脚电平。5. 构建、烧录与运行5.1 构建项目在项目根目录zephyr_led_blink下打开终端执行以下命令进行构建# 指定开发板为 stm32f103c8t6构建目录为 build west build -b stm32f103c8t6 --build-dir build-b参数指定板型Zephyr 会根据此名称寻找对应的板级配置文件包括我们写的 overlay。5.2 常见构建问题排查问题现象可能原因检查与解决Board stm32f103c8t6 not found1. 板型名称拼写错误。2. Zephyr 未支持该板型或支持不完整。1. 检查west boards输出中是否有stm32f103c8t6。2. 可能需要使用相近的板型如stm32f103c8t6_blackpill如果存在并相应调整 overlay 文件名。No such file or directory: boards/stm32f103c8t6.overlayOverlay 文件路径或名称错误。确保 overlay 文件在boards/目录下且文件名与板型名完全一致包括大小写。Undefined reference to __device_dts_ord_XX设备树节点定义正确但对应的驱动Kconfig未启用。检查prj.conf确保已启用相关驱动CONFIG_GPIOy。对于 LED可能还需要CONFIG_GPIO_LEDy。error: ‘led0’ undeclared设备树中未正确定义led0节点或标签。检查 overlay 文件语法确保节点标签led0:存在且compatible属性正确。使用west build -t menuconfig检查设备树生成结果。5.3 烧录与调试构建成功后固件位于build/zephyr/zephyr.bin或build/zephyr/zephyr.hex。使用 OpenOCD 和 ST-Link 烧录示例# 假设 OpenOCD 已安装且 ST-Link 已连接 openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c program build/zephyr/zephyr.bin exit 0x08000000使用 pyOCD 烧录pyocd flash -t stm32f103c8t6 build/zephyr/zephyr.bin使用 STM32CubeProgrammer通过 GUI 或命令行工具加载.bin或.hex文件。烧录完成后复位开发板你应该能看到连接在 PA5 的 LED 以 1 秒的间隔闪烁。5.4 验证与调试如果 LED 没有闪烁请按以下步骤排查检查硬件连接确认 LED 确实连接在 PA5且电路正确通常 LED 阳极接 PA5阴极通过限流电阻接地。检查设备树生成查看最终合并的设备树文件确认我们的 overlay 已生效。# 查看生成的完整设备树 cat build/zephyr/zephyr.dts | grep -A 5 -B 5 “leds”你应该能看到类似下面的输出确认gpios属性指向gpioa 5 ...。检查驱动初始化在prj.conf中增加日志输出级别。CONFIG_LOGy CONFIG_GPIO_LOG_LEVEL_DBGy重新构建并运行通过串口查看日志例如使用minicom或picocom连接 USART1波特率 115200观察 GPIO 驱动初始化信息。使用调试器连接 ST-Link 等调试器单步调试main函数检查device_is_ready和gpio_pin_configure_dt的返回值。6. 深入设备树绑定Binding与驱动匹配我们的 LED 之所以能工作背后是设备树绑定Device Tree Binding机制在起作用。6.1 绑定文件.yaml绑定文件定义了设备树节点的“模式”。它规定了节点必须required:或可选optional:包含哪些属性。每个属性的数据类型type:如int,string,phandle-array。属性的约束条件如枚举值、范围。属性的描述信息。对于gpio-ledsZephyr 源码中已有其绑定文件dts/bindings/led/gpio-leds.yaml。我们的 overlay 节点必须符合该绑定的规定否则构建时会报错。6.2 驱动如何找到设备构建阶段Zephyr 的构建系统收集所有.dts、.dtsi和.overlay文件合并成最终的zephyr.dts。驱动声明GPIO LED 驱动如drivers/led/led_gpio.c通过DT_DRV_COMPAT宏声明其兼容性为gpio_leds。实例化构建系统使用遍历宏如DT_INST_FOREACH_STATUS_OKAY扫描zephyr.dts寻找所有compatible “gpio-leds”且status “okay”的节点。为每个这样的节点生成一个唯一的设备实例struct device并调用驱动的初始化函数。应用访问我们的应用代码通过DT_ALIAS(led0)或DT_NODELABEL(led0)获取到该设备实例的标识符进而通过DEVICE_DT_GET拿到设备指针进行操作。6.3 查看绑定文件你可以通过以下命令或在 VS Code 中安装 nRF Connect 插件查看绑定文件# 在 Zephyr 源码目录下查找 find . -name “gpio-leds.yaml”查看其内容你会看到它对gpios属性的定义这正是我们能在 overlay 中写gpios gpioa 5 GPIO_ACTIVE_HIGH;的依据。7. 扩展添加更多外设与最佳实践7.1 添加一个按键输入在同一个 overlay 文件中我们可以轻松添加一个按键/ { gpio_keys { compatible “gpio-keys”; button0: button_0 { gpios gpioc 13 (GPIO_PULL_UP | GPIO_ACTIVE_LOW); label “User Button”; }; }; };在代码中可以通过DT_ALIAS(sw0)如果别名是sw0或DT_NODELABEL(button0)来获取此按键的设备规格并使用gpio_pin_configure_dt()配置为输入使用gpio_pin_get_dt()或中断来读取状态。7.2 使用别名Aliases提高代码可移植性在 overlay 中或主设备树中定义别名可以让应用代码不依赖具体的节点标签。// 在 overlay 的根节点添加 / { aliases { led0 led0; sw0 button0; }; };这样在代码中就可以始终使用DT_ALIAS(led0)和DT_ALIAS(sw0)即使底层硬件定义变了代码也无需修改。7.3 生产环境注意事项错误处理生产代码必须检查每一个 API 调用的返回值并进行适当的错误处理如重试、记录日志、进入安全状态。电源管理对于电池供电设备在不使用 GPIO 时应考虑将其配置为模拟输入或关闭时钟以省电。配置外置化将设备树 overlay 作为项目的一部分进行版本管理。对于需要频繁更改的配置如 LED 闪烁频率可以考虑将其移至 Kconfig (prj.conf) 或作为运行时可配置参数。引脚冲突检查确保在 overlay 中定义的引脚没有被其他外设如 UART、SPI重复使用。可以查阅 MCU 的数据手册和 Zephyr 的板级定义。使用硬件抽象层对于更复杂的应用建议将 LED 操作封装成独立的模块如led_controller.c内部使用 Zephyr GPIO API但对外提供如led_on(),led_off(),led_blink()等抽象接口。这提高了代码的可测试性和可移植性。8. 总结从 GPIO 控制看 Zephyr 开发范式通过这个简单的 LED 控制项目我们实践了 Zephyr 开发的核心流程硬件描述在设备树 overlay.overlay中以声明式语言描述硬件连接gpios gpioa 5 …和兼容性compatible “gpio-leds”。驱动匹配Zephyr 构建系统根据compatible属性将硬件节点与对应的驱动如led_gpio.c绑定并在启动前完成初始化。应用编程在应用程序中通过设备树 APIDT_ALIAS(),GPIO_DT_SPEC_GET()获取硬件配置通过统一的设备驱动 APIgpio_pin_configure_dt(),gpio_pin_set_dt()操作硬件。这种模式的优点是显而易见的应用代码与具体硬件解耦。如果你想将代码移植到另一块使用不同引脚或不同厂商 MCU 的开发板通常只需修改设备树 overlay 文件而无需触碰应用代码。驱动开发者芯片厂商和维护者板卡厂商负责提供正确的设备树描述和驱动应用开发者则可以更专注于业务逻辑。对于更复杂的外设如 I2C 传感器、ADC、PWM原理完全相同在设备树中描述外设连接如i2c总线地址、adc输入通道在代码中通过对应的*_dt_spec_getAPI 获取配置然后调用标准的 Zephyr 驱动 API 进行操作。掌握设备树是解锁 Zephyr 强大跨平台能力的关键一步。