Zephyr RTOS开发:从设备树到device_get_binding,解决STM32F103C8T6点灯难题
2026/8/2 14:24:01 网站建设 项目流程

如果你正在尝试用 VSCode 开发 Zephyr RTOS 项目,并且手头恰好有一块经典的“蓝色药丸”——STM32F103C8T6 最小系统板,那么你很可能已经卡在了第一步:如何让 Zephyr 正确识别并配置你的这块开发板?

更具体地说,当你满怀期待地打开一个 Zephyr 示例项目,准备编译时,命令行却无情地抛出一个错误:No device tree node found for alias 'led0',或者更令人困惑的Could not find device tree include: stm32f103c8t6.dts。你翻遍文档,发现问题的核心都指向一个神秘的device字段,它似乎连接着你的代码和那块小小的蓝色电路板。

这不仅仅是配置问题,而是理解 Zephyr 现代嵌入式开发范式的第一道门槛。过去,在 Keil 或 STM32CubeIDE 中,你通过图形化界面点点鼠标就能配置引脚和时钟;但在 Zephyr 的世界里,这一切都通过设备树(Device Tree)Kconfig以声明式的方式完成。device字段正是你代码与设备树描述硬件之间的桥梁。

本文将彻底解决这个问题。我们不只告诉你“是什么”,更会拆解“为什么”以及“怎么做”。你将通过一个完整的、可复现的案例,在 VSCode 环境下,从零开始为一个 STM32F103C8T6 最小系统板创建项目,并学会如何正确获取和使用device字段来控制一个 LED。这篇文章的目标是:让你不仅跑通一个 Demo,更能理解 Zephyr 硬件抽象层的工作机制,从而具备移植和调试新板卡的能力。

1. 这篇文章真正要解决的问题:从“点灯”困境到 Zephyr 开发范式理解

很多开发者第一次接触 Zephyr 时,会带着传统 MCU 开发的思维惯性。在 STM32 的标准库或 HAL 库中,你操作的是GPIOA->ODR这样的寄存器,或者HAL_GPIO_WritePin这样的函数,硬件信息是“硬编码”在代码里的。但在 Zephyr 中,为了达成其“一次编写,随处运行”的跨平台目标,它引入了一套强大的硬件抽象机制。

这套机制的核心是设备树(DTS)驱动模型。你的应用程序不再直接操作硬件,而是通过一个名为device的句柄来访问。这个device是如何与“PA1”这个具体的物理引脚关联起来的呢?答案就在你板卡对应的.dts文件和.overlay文件中。

因此,本文要解决的深层问题有三个:

  1. 认知转换:从“寄存器/库函数”思维转向“设备树与设备驱动”思维。
  2. 实操路径:在 VSCode 中,为一块非官方标准支持的开发板(STM32F103C8T6最小系统板)搭建完整的 Zephyr 开发、编译和调试环境。
  3. 核心技能:掌握如何定位、理解并正确使用device字段,这是使用 Zephyr 所有外设(GPIO, I2C, SPI, UART等)的通用方法。

我们将通过控制一个 LED 闪烁这个最简单的任务来贯穿始终,因为这是验证硬件和软件链路是否打通的最直观方式。

2. 基础概念与核心原理:设备树、设备驱动与device_get_binding

在深入实操前,必须理清几个核心概念,否则后面的代码和配置将是空中楼阁。

2.1 设备树:硬件的“说明书”

设备树是一种描述硬件资源的数据结构。它像一个标准化的“硬件说明书”,以文本形式(.dts.dtsi文件)定义了:

  • CPU 架构和型号。
  • 内存布局。
  • 外设控制器(如 GPIO 控制器)的地址和中断号。
  • 外设节点(如一个具体的 LED、按键)及其属性(如使用的 GPIO 引脚)。

在编译时,Zephyr 的设备树编译器(DTC)会将这个文本文件编译成二进制格式(.dtb),并最终链接到固件中。运行时,系统会解析这份“说明书”来知道有什么硬件可用。

2.2 设备驱动:硬件的“翻译官”

驱动是知道如何操作特定硬件的软件。在 Zephyr 中,驱动与设备树节点绑定。当设备树描述了一个“stm32-gpio”控制器下的“led0”节点时,Zephyr 会在启动时自动为这个节点实例化一个对应的设备驱动实例。

2.3device_get_binding:获取硬件的“操作手柄”

这是连接你的应用程序和硬件驱动的关键函数。它通过一个字符串标识符(通常就是设备树中节点的label属性)来查找对应的设备实例,并返回一个const struct device *类型的指针。这个指针就是你后续所有 API 调用的“手柄”。

// 例如,获取标签为 "led0" 的设备 const struct device *led_dev = device_get_binding("led0"); if (led_dev == NULL) { // 获取失败,可能是设备树未正确定义或 label 写错 return -ENODEV; } // 现在可以通过 led_dev 来操作这个 LED 了

它们之间的关系:设备树定义硬件存在(有一个LED在PA1引脚),驱动提供操作能力(知道怎么控制STM32的GPIO),device_get_binding让你的代码拿到这个能力的入口。

2.4 为什么 STM32F103C8T6 需要特殊关注?

Zephyr 官方支持许多开发板,如nucleo_f103rb。但对于市面上广泛流通的“STM32F103C8T6最小系统板”(即“蓝色药丸”),它通常没有对应的官方板级定义。这意味着:

  1. 没有现成的<board>.dts文件。
  2. 需要我们自己通过设备树覆盖(.overlay)文件来“告诉”Zephyr这块板子的具体硬件连接(比如LED接在哪个引脚)。

这是我们本次实践的重点和价值所在。

3. 环境准备与前置条件

在开始写代码之前,必须搭建好开发环境。以下步骤在 Windows 10/11、macOS 或 Linux 上均适用,但本文以 Windows + VSCode 为例进行说明。

3.1 安装必要的工具链

  1. Zephyr SDK:这是包含编译器、调试器、工具链的一站式安装包。

    • 前往 Zephyr SDK 发布页面 下载最新版本(如zephyr-sdk-<version>_windows-x86_64.zip)。
    • 解压到不含中文和空格的路径,例如C:\zephyr-sdk
    • 以管理员身份运行解压目录下的setup.cmd,它会设置环境变量。
  2. Python 3.8+:Zephyr 的构建系统依赖 Python。

    • 从 Python官网 下载安装。务必勾选 “Add Python to PATH”
    • 安装后,打开新的命令行,验证:
      python --version pip --version
  3. Git:用于获取 Zephyr 源码。

    • 从 Git官网 下载安装。
  4. VSCode:我们的主力开发环境。

    • 从 VSCode官网 下载安装。
    • 建议安装扩展:C/C++(Microsoft),CMake Tools(Microsoft)。

3.2 获取 Zephyr 源码并初始化环境

这是最关键的一步,我们将使用 Zephyr 推荐的west工具进行管理。

  1. 在一个合适的目录(如C:\Users\YourName)打开命令行。
  2. 使用west初始化工作区并拉取源码:
    # 安装 west 工具 pip install west # 初始化工作区,zephyrproject 是目录名,可自定义 west init zephyrproject cd zephyrproject # 拉取 Zephyr 源码及其所有模块(此过程较久,需耐心等待) west update # 导出 Zephyr 环境变量(每次打开新终端都需要执行) west zephyr-export
  3. 安装 Python 依赖
    # 进入 Zephyr 源码目录 cd zephyr pip install -r requirements.txt

3.3 硬件准备:STM32F103C8T6 最小系统板

  • 确保你有一块 STM32F103C8T6 核心的最小系统板。
  • 准备一个 USB 转 TTL 串口模块(如 CH340、CP2102),用于查看日志输出。
  • 准备一个 ST-Link V2 调试器(或带 ST-Link 的下载器),用于烧录和调试。
  • 连接方式:
    • ST-LinkSWDIO-> 板子的DIO(PA13)
    • ST-LinkSWCLK-> 板子的DCLK(PA14)
    • ST-LinkGND-> 板子的GND
    • ST-Link3.3V-> 板子的3.3V(可选,但建议由目标板自供电)
    • USB-TTLRX-> 板子的A9(USART1_TX)
    • USB-TTLTX-> 板子的A10(USART1_RX)
    • USB-TTLGND-> 板子的GND
  • 在板上找一个可用的 LED,或者自己接一个 LED 和限流电阻到某个 GPIO 引脚(例如PC13,这是板上通常已有的用户LED引脚)。

环境至此准备完毕。接下来,我们进入激动人心的实战环节。

4. 核心流程拆解:为自定义板卡创建 Zephyr 项目

我们的目标是创建一个独立于 Zephyr 源码树之外的项目,这是推荐的做法。

4.1 创建项目目录结构

在你的工作区外(例如C:\Projects)创建一个新目录:

mkdir my_zephyr_blinky cd my_zephyr_blinky

项目基础结构如下:

my_zephyr_blinky/ ├── CMakeLists.txt # 项目主构建文件 ├── prj.conf # 项目 Kconfig 配置 ├── src/ │ └── main.c # 应用程序主代码 └── boards/ # 板级定义(核心!) └── arm/ └── my_f103c8t6/ # 我们自定义的板卡目录 ├── Kconfig.board ├── Kconfig.defconfig ├── board.cmake └── my_f103c8t6.dts # 设备树源文件

4.2 创建板级支持文件(关键步骤)

boards/arm/my_f103c8t6/目录下创建以下文件:

  1. Kconfig.board: 定义板卡名称和架构。
    # SPDX-License-Identifier: Apache-2.0 config BOARD_MY_F103C8T6 bool "My STM32F103C8T6 Minimum System Board" depends on SOC_STM32F103X8
  2. Kconfig.defconfig: 设置板卡的默认配置。
    # SPDX-License-Identifier: Apache-2.0 if BOARD_MY_F103C8T6 config BOARD default "my_f103c8t6" endif # BOARD_MY_F103C8T6
  3. board.cmake: 指定板卡对应的 SoC 和 CPU 类型。
    # SPDX-License-Identifier: Apache-2.0 set(SUPPORTED_EMU_PLATFORMS qemu) board_runner_args(stm32cubeprogrammer "--port=swd" "--reset-mode=hw") include(${ZEPHYR_BASE}/boards/common/stm32cubeprogrammer.board.cmake) include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
  4. my_f103c8t6.dts:设备树源文件,描述硬件。这是最核心的文件。
    /dts-v1/; #include <st/f1/stm32f103X8.dtsi> #include <st/f1/stm32f103c(8-b)tx-pinctrl.dtsi> #include <zephyr/dt-bindings/input/input-event-codes.h> / { model = "My STM32F103C8T6 Minimum System Board"; compatible = "my,f103c8t6", "st,stm32f103"; chosen { zephyr,console = &usart1; zephyr,shell-uart = &usart1; zephyr,sram = &sram0; zephyr,flash = &flash0; }; leds { compatible = "gpio-leds"; led0: led_0 { gpios = <&gpioc 13 GPIO_ACTIVE_LOW>; /* PC13 */ label = "User LED"; }; }; aliases { led0 = &led0; }; }; &usart1 { pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; pinctrl-names = "default"; current-speed = <115200>; status = "okay"; }; &clk_hse { clock-frequency = <8000000>; /* 外部 8MHz 晶振 */ }; &pll { mul = <9>; clocks = <&clk_hse>; status = "okay"; }; &rcc { clocks = <&pll>; clock-frequency = <72000000>; /* 系统时钟 72MHz */ ahb-prescaler = <1>; apb1-prescaler = <2>; apb2-prescaler = <1>; };
    关键解释
    • #include: 引入了 STM32F103 系列的通用设备树定义和引脚控制定义。
    • chosen: 指定了系统控制台和内存区域。
    • leds节点:定义了一个兼容gpio-leds的 LED 设备,gpios属性指定了它连接在 GPIOC 的第 13 引脚,且低电平有效(因为很多最小系统板的 LED 是共阳极接法)。
    • aliases: 创建了一个别名led0,指向led_0节点。这个别名led0就是后续device_get_binding("led0")中使用的字符串!
    • &usart1: 配置了串口1的引脚和波特率,用于日志输出。
    • &clk_hse,&pll,&rcc: 配置了时钟树,从外部 8MHz 晶振倍频到 72MHz 系统时钟。

4.3 创建项目主构建文件

在项目根目录 (my_zephyr_blinky/) 创建CMakeLists.txt

# SPDX-License-Identifier: Apache-2.0 cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(my_blinky) target_sources(app PRIVATE src/main.c)

4.4 创建项目配置文件

在项目根目录创建prj.conf,用于启用必要的内核功能:

# 启用 GPIO 驱动 CONFIG_GPIO=y # 启用日志系统并设置默认日志级别为 INF CONFIG_LOG=y CONFIG_LOG_DEFAULT_LEVEL=3 # 启用硬件时钟(如果需要) CONFIG_CLOCK_CONTROL=y CONFIG_CLOCK_STM32_CUBE=y # 启用串口控制台 CONFIG_SERIAL=y CONFIG_CONSOLE=y CONFIG_UART_CONSOLE=y

5. 完整示例与代码实现:编写应用程序

现在,我们来编写应用程序代码,演示如何获取device并控制 LED。

src/main.c中写入以下内容:

/* * SPDX-License-Identifier: Apache-2.0 */ #include <zephyr/kernel.h> #include <zephyr/drivers/gpio.h> /* 定义线程栈大小和优先级 */ #define STACK_SIZE 1024 #define THREAD_PRIORITY 5 /* 线程栈空间 */ K_THREAD_STACK_DEFINE(blinky_stack, STACK_SIZE); /* 线程数据结构 */ static struct k_thread blinky_thread; /* LED 闪烁线程的主函数 */ void blinky_thread_entry(void *p1, void *p2, void *p3) { const struct device *led_dev; int ret; /* 1. 通过设备树中的别名 "led0" 获取设备句柄 */ led_dev = device_get_binding("led0"); if (led_dev == NULL) { printk("Error: Failed to get LED device binding.\n"); return; } /* 2. 配置 LED 引脚为输出模式 */ ret = gpio_pin_configure(led_dev, 0, GPIO_OUTPUT_ACTIVE); if (ret < 0) { printk("Error %d: Failed to configure LED pin.\n", ret); return; } printk("LED device initialized successfully. Starting to blink...\n"); /* 3. 主循环,实现 LED 闪烁 */ while (1) { /* 点亮 LED (对于 ACTIVE_LOW,设置低电平) */ gpio_pin_set(led_dev, 0, 1); k_msleep(500); // 延时 500ms /* 熄灭 LED (设置高电平) */ gpio_pin_set(led_dev, 0, 0); k_msleep(500); } } /* 主函数(应用入口) */ int main(void) { printk("Zephyr Blinky Example on STM32F103C8T6\n"); /* 创建并启动 LED 闪烁线程 */ k_thread_create(&blinky_thread, blinky_stack, STACK_SIZE, blinky_thread_entry, NULL, NULL, NULL, THREAD_PRIORITY, 0, // 无特殊选项 K_NO_WAIT); /* 主线程无事可做,可以进入休眠或处理其他任务 */ while (1) { k_msleep(1000); } return 0; }

代码关键点解析

  1. device_get_binding("led0"):这是本文的核心。函数通过字符串"led0"在系统设备树中查找对应的设备节点。这个"led0"必须与我们在my_f103c8t6.dts文件的aliases部分定义的别名完全一致。
  2. gpio_pin_configure:配置 GPIO 引脚。这里0不是引脚号,而是该 GPIO 控制器内的引脚索引。对于gpio-leds兼容的设备,通常索引 0 就对应我们在设备树gpios属性中定义的引脚。
  3. gpio_pin_set:设置 GPIO 输出电平。注意,电平的逻辑(1为亮还是灭)取决于设备树中GPIO_ACTIVE_LOWGPIO_ACTIVE_HIGH的定义。
  4. 线程化:示例使用了 Zephyr 的内核线程,让 LED 闪烁在独立的线程中运行,不影响主线程。这是 RTOS 编程的良好实践。

6. 运行结果与效果验证:编译、烧录与调试

6.1 使用 VSCode 和命令行编译

  1. 在 VSCode 中打开项目文件夹my_zephyr_blinky
  2. 打开集成终端(Terminal -> New Terminal)。
  3. 使用west构建项目,并指定我们的自定义板卡my_f103c8t6
    # 确保在项目根目录 west build -b my_f103c8t6
    -b参数指定板卡名称,它必须与Kconfig.boardconfig BOARD_后面的名字(大写)匹配,但在这里使用小写。
  4. 如果一切顺利,编译完成后会在build目录下生成zephyr/zephyr.elfzephyr/zephyr.binzephyr/zephyr.hex等文件。

6.2 烧录固件到开发板

连接好 ST-Link 和开发板。使用west flash命令烧录:

west flash

west会根据board.cmake中定义的runner(这里是stm32cubeprogrammeropenocd)自动调用对应的工具进行烧录。如果遇到权限问题,可能需要以管理员身份运行命令行,或者检查 ST-Link 驱动是否安装(如 STM32 ST-LINK Utility 附带的驱动)。

6.3 验证运行结果

  1. 将 USB-TTL 串口模块连接到电脑,并使用串口终端工具(如 PuTTY、Tera Term、VS Code Serial Monitor 扩展)打开对应的 COM 口,波特率设置为 115200。
  2. 给开发板上电或复位。你应该在串口终端看到输出:
    Zephyr Blinky Example on STM32F103C8T6 LED device initialized successfully. Starting to blink...
  3. 同时,开发板上连接在 PC13 的 LED 应该开始以 1Hz 的频率闪烁。

恭喜!至此,你已经成功地在自定义的 STM32F103C8T6 最小系统板上运行了 Zephyr,并通过正确的device获取方式控制了硬件。这标志着你已经打通了 Zephyr 开发的核心链路。

7. 常见问题与排查思路

在实践过程中,你几乎一定会遇到一些问题。下表列出了最常见的问题及其解决方法:

问题现象可能原因排查方式解决方案
编译错误:No device tree node found for alias 'led0'1. 设备树文件(.dts)中未定义led0别名。
2. 设备树文件未正确编译或包含。
3. 板卡目录结构或文件命名错误,导致构建系统未找到你的.dts文件。
1. 检查boards/arm/my_f103c8t6/my_f103c8t6.dtsaliases部分是否正确定义了led0
2. 检查构建输出目录build/zephyr/include/generated/devicetree_generated.h,搜索led0,看其是否被正确生成。
确保.dts文件语法正确,且板卡配置(Kconfig.board,board.cmake)能使构建系统找到并处理该文件。
运行时device_get_binding("led0")返回NULL1. 设备树别名led0定义错误或不存在。
2. 对应的 GPIO 控制器驱动未启用或初始化失败。
3. 设备树中 LED 节点的status不是"okay"
1. 在main.c中添加printk打印返回值。
2. 检查prj.conf是否启用了CONFIG_GPIO=y
3. 检查设备树中 LED 节点及其父节点(如&gpioc)的status属性。
仔细核对设备树别名,确保 Kconfig 配置正确,并检查设备树节点状态。
LED 不闪烁或状态相反1. GPIO 引脚配置的电平极性(GPIO_ACTIVE_LOW/HIGH)与实际电路不符。
2. 引脚号在设备树中定义错误。
3. 硬件连接问题(如 LED 损坏、电阻过大)。
1. 用万用表测量引脚电平变化。
2. 在代码中尝试反转gpio_pin_set的参数。
3. 核对原理图,确认 LED 连接的正确引脚和极性。
调整设备树gpios属性中的<&gpioc 13 GPIO_ACTIVE_LOW>,将GPIO_ACTIVE_LOW改为GPIO_ACTIVE_HIGH或反之。
west flash失败,提示找不到设备或编程器1. ST-Link 驱动未安装或连接不稳定。
2.westrunner配置不支持你的调试器。
3. 开发板未进入编程模式(Boot0 引脚电平)。
1. 使用 STM32CubeProgrammer 或 OpenOCD 单独测试连接。
2. 检查board.cmake中定义的runner,并确保系统已安装对应工具。
3. 确保 Boot0 跳线接地(从主闪存启动)。
安装正确的 ST-Link 驱动,尝试使用west flash --runner openocd指定 runner,或检查硬件连接。
串口无输出1. 串口引脚(PA9/PA10)配置错误或被其他功能占用。
2. 设备树中usart1status不是"okay"
3. 串口终端波特率设置错误。
4.prj.conf中未启用CONFIG_SERIALCONSOLE
1. 检查设备树中usart1pinctrl-0引脚定义。
2. 确认chosen节点中zephyr,console设置为&usart1
3. 核对终端波特率是否为 115200。
确保设备树串口配置正确,Kconfig 启用控制台,并使用正确的引脚连接 USB-TTL 模块。
编译时警告clock-frequency被覆盖设备树中时钟配置(如&rcc)与 SoC 默认定义冲突。查看编译输出信息,确认是哪个节点。这通常是良性警告。如果系统时钟频率符合预期(72MHz),可以忽略。如需消除,可查阅 Zephyr 中对应 SoC 的.dtsi文件,了解默认值。

8. 最佳实践与工程建议

掌握了基础操作后,以下建议能帮助你将 Zephyr 用于更严肃的项目。

8.1 设备树管理进阶

  • 使用 Overlay 文件:对于项目特定的硬件修改(比如你换了一个 LED 引脚),最佳实践不是直接修改板级.dts文件,而是在项目根目录或boards目录下创建<board>.overlay文件。构建系统会自动将其覆盖到默认设备树上。
    // 在项目根目录创建 `my_f103c8t6.overlay` / { leds { led0: led_0 { gpios = <&gpiob 12 GPIO_ACTIVE_LOW>; /* 将 LED 改为 PB12 */ }; }; };
  • 理解设备树绑定(Bindings)compatible = "gpio-leds"这样的字符串对应着dts/bindings/gpio/gpio-leds.yaml等绑定文件。学习编写简单的绑定文件可以让你为自定义外设定义属性。

8.2 设备获取的健壮性

  • 使用DEVICE_DT_GET:在新版本 Zephyr 中,更推荐使用设备树宏来获取设备,它在编译时就能检查节点是否存在,比运行时device_get_binding更安全。
    #include <zephyr/device.h> #define LED0_NODE DT_ALIAS(led0) // 从别名获取节点标识符 static const struct device *const led_dev = DEVICE_DT_GET(LED0_NODE); if (!device_is_ready(led_dev)) { printk("LED device not ready\n"); return; }
  • 检查设备状态:在获取设备后,使用device_is_ready()检查设备是否初始化成功。

8.3 项目结构与配置

  • 版本控制:将你的自定义板卡目录 (boards/arm/my_f103c8t6) 和项目配置文件放入版本控制(如 Git)。但通常不将庞大的build目录和 Zephyr 源码本身纳入。
  • 模块化代码:将不同外设的驱动代码、业务逻辑分开放置在src下的不同文件中。
  • 利用 Kconfig:除了prj.conf,可以创建Kconfig文件为你的项目定义可配置选项,并通过menuconfig(west build -t menuconfig) 进行图形化配置。

8.4 调试与日志

  • 善用日志系统:Zephyr 的日志系统非常强大。使用LOG_MODULE_REGISTERLOG_INF,LOG_ERR等宏,可以方便地控制日志级别和输出模块。
  • 使用调试器:在 VSCode 中配置launch.json,使用 Cortex-Debug 等扩展,可以直接进行单步调试、查看变量和寄存器,效率远高于printk

通过这个完整的从零到一的案例,我们不仅解决了“如何获取 device”这个具体问题,更揭示了 Zephyr 开发的核心工作流:通过设备树声明硬件,通过 Kconfig 配置系统,在代码中通过设备 API 操作硬件。对于 STM32F103C8T6 这类广泛使用但非官方直接支持的芯片,掌握自定义板级定义的能力至关重要。

下次当你面对一块新的开发板,或者需要驱动一个新的传感器时,你都可以遵循同样的思路:先定义设备树,再在代码中获取设备句柄。这扇通往现代、可移植嵌入式开发的大门,现在已经为你敞开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询