Zephyr 设备树(Devicetree)保姆级详解:语法 + overlay 实战 + DT 宏 + 排错(含完整可编译工程)
2026/6/25 14:36:49 网站建设 项目流程

本文是「Zephyr 内核从入门到精通」系列第 04 篇。上一篇搭好环境点亮了 LED,本篇彻底讲透设备树——为什么需要它、语法怎么读、overlay 怎么用、代码怎么取值,以及大量实战才会遇到的技巧和坑。

本篇是保姆级:给一个复制即可编译的完整小工程(用 overlay 改 LED 引脚 + 挂一个 I2C 温湿度传感器),每一步都标清楚「文件放哪、叫什么名、去哪看结果」,并给出预期输出改前改后的现象对比。文末有15 条高频报错排查表。

通俗易懂、代码可抄。建议先点赞收藏,跟着敲一遍。

目录

  • 一、设备树到底解决什么问题(一个比喻讲明白)
  • 二、设备树节点语法详解(六要素 + 属性类型)
  • 三、设备树的「叠加」模型:dtsi / dts / overlay
  • 四、overlay 文件放在哪、叫什么名(关键!)
  • 五、完整实战工程:改 LED 引脚 + 挂 I2C 传感器(复制即编)
  • 六、逐步编译 + 预期输出 + 改前改后现象对比
  • 七、代码如何读设备树:定位 → 取值 → 使用
  • 八、常用 DT 宏速查
  • 九、排错圣经:去哪看「真相之源」
  • 十、高频报错排查表(15 条)
  • 十一、总结

一、设备树到底解决什么问题

传统裸机 / FreeRTOS 开发,硬件信息(引脚号、寄存器地址、时钟)硬编码在 C 代码里。换块板子,代码就得满世界改宏定义。

Zephyr 的解法:把「硬件长什么样」从代码里抽出来,用一种专门的数据格式描述。这就是设备树(Devicetree)。

一个比喻:设备树就是硬件的一张**「登记表」**。你的代码只说「我要操作led0」,至于led0接在哪个引脚,登记表说了算。换板子 = 换登记表,代码不动。这就是 Zephyr「一次开发、多板复用」的底层实现。

关键区别:Zephyr 的设备树语法和 Linux 相似,但 Zephyr 是编译期把设备树展开成一堆 C 宏(零运行时开销、不占 Flash 解析代码),Linux 是运行时解析 dtb。这是两者最大的不同,也是为什么 Zephyr 设备树问题大多是「编译报错」而非「运行崩溃」。


二、设备树节点语法详解

设备树由「节点(node)」组成,节点有「属性(property)」。看一个 LED 节点:

led0: led_0@0 { compatible = "gpio-leds"; gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; label = "Green LED"; status = "okay"; };

2.1 节点六要素

  1. led0:(label 标签)—— 节点的「昵称」。代码里用&led0引用,宏是DT_NODELABEL(led0)。一个节点可以有多个 label。
  2. led_0(node-name 节点名)—— 人类可读的节点名称,同一父节点下不能重名(除非靠 unit-address 区分)。
  3. @0(unit-address 单元地址)—— 区分同名同类节点,通常对应该外设的寄存器基址。比如i2c@40003000@40003000必须和它reg属性的第一个值一致。
  4. compatible(兼容串)——整个设备树最核心的属性。它把节点和一个binding 文件(.yaml关联,binding 定义该节点允许哪些属性、属性是什么类型;驱动也靠 compatible 认领设备。compatible 写错 = binding 找不到 = 一堆属性报错。
  5. gpios = <&gpio0 13 GPIO_ACTIVE_LOW>(phandle + 参数)——&gpio0phandle,指向 GPIO 控制器节点;13是引脚号;GPIO_ACTIVE_LOW是有效电平标志。这种「phandle + 若干 cell」的组合叫phandle-array
  6. status(状态)——"okay"启用该节点,"disabled"禁用。只有 status = okay 的节点,驱动才会初始化、代码里gpio_is_ready_dt()才返回真。这是新手最常踩的坑(外设没开机)。

2.2 常见属性类型(对应 binding 里的 type)

example_node { a_string = "hello"; /* string:字符串 */ a_int = <100>; /* int / cell:32 位整数 */ an_array = <1 2 3>; /* array:整数数组 */ a_bool; /* boolean:写出来即为真,不写为假 */ reg = <0x40000000 0x1000>; /* reg:地址 + 大小 */ a_phandle = <&another_node>; /* phandle:指向另一个节点 */ };

记住这张对应关系,后面看 binding 报错就不慌:binding 里写type: string,你设备树里就得写带引号的字符串;写type: int,就得写<尖括号数字>


三、设备树的「叠加」模型

新手最容易懵的点:最终设备树不是某一个文件,而是多层叠加合并出来的。

合并顺序(后者优先级更高、可覆盖前者):

  1. SoC.dtsi:芯片厂商写,描述 CPU、片上外设地址、中断号(如nrf52840.dtsi)。一般不动它。
  2. 板级.dts:开发板厂商写,描述板上器件接线、哪些外设默认开启(如nrf52840dk_nrf52840.dts)。一般也不动它。
  3. 应用.overlay你自己写的,优先级最高。在这里改引脚、挂器件、开关外设。

黄金实践:改硬件配置,永远优先写 overlay,不要直接改板厂的 dts。既保留原始定义(别人能复用),又能灵活定制(你能升级 Zephyr 不冲突)。

叠加规则口诀:

  • 新增节点 → 直接写新节点;
  • 修改已有节点的属性 → 用&label { ... }重新打开它,写同名属性即覆盖;
  • 禁用节点 →&label { status = "disabled"; };

四、overlay 文件放在哪、叫什么名(关键!)

这是 90% 新手「overlay 不生效」的根源。记牢命名规则:

文件路径何时生效用途
<工程根>/app.overlay所有board 生效通用改动
<工程根>/boards/<board>.overlay对该 board 生效板子专属改动(推荐)
<工程根>/<board>.overlay仅对该 board(部分版本)兼容旧写法

其中<board>是你west build -b后面跟的板子名。例如板子是nrf52840dk/nrf52840,对应文件名就是boards/nrf52840dk_nrf52840.overlay斜杠换成下划线)。

⚠️重点:app.overlay 必须放在工程根目录(和prj.confCMakeLists.txt同级),不是放在src/里!放错地方 = 构建系统根本找不到 = 静默不生效。

【📷 截图位:文件管理器里展开工程目录树,红框标出 app.overlay 和 boards/ 与 prj.conf 同级】


五、完整实战工程:改 LED 引脚 + 挂 I2C 传感器

下面是一个复制即可编译的完整工程。目标:把led0改到另一个引脚,并在 I2C 总线上挂一个 BME280 温湿度传感器,开机打印它的设备名是否就绪。

工程目录结构(先建好这个结构):

dt_demo/ ├── CMakeLists.txt ├── prj.conf ├── app.overlay └── src/ └── main.c

5.1app.overlay(放工程根目录)

/* app.overlay —— 应用层设备树叠加,优先级最高 */ /* ① 改 LED0 的引脚:从板子默认引脚改到 P0.17,并改成高电平有效 */ &led0 { gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>; }; /* ② 启用 I2C0 外设,并在 0x76 地址挂一个 BME280 传感器 */ &i2c0 { status = "okay"; clock-frequency = <I2C_BITRATE_STANDARD>; bme280: bme280@76 { compatible = "bosch,bme280"; reg = <0x76>; }; }; /* ③ 给传感器起个别名,方便代码用 DT_ALIAS 取(可选但推荐) */ / { aliases { my-sensor = &bme280; }; };

说明:

  • &led0&i2c0&gpio0都是板厂 dts 里已存在的 label,我们「重新打开」来修改。
  • &i2c0在很多板子上默认是disabled,必须显式status = "okay"才会工作。
  • BME280 的 binding 是 Zephyr 自带的,compatible 必须严格写成"bosch,bme280"(厂商,型号),多一个空格、大小写错都会报not in binding

5.2prj.conf(放工程根目录)

# prj.conf —— Kconfig 配置(下一篇专门讲) CONFIG_GPIO=y CONFIG_I2C=y CONFIG_SENSOR=y CONFIG_LOG=y CONFIG_PRINTK=y

注意:设备树管「硬件长什么样」,但驱动开关在 Kconfig。挂了 I2C 器件却没开CONFIG_I2C=y,驱动不会编进去,device_is_ready()永远是假。这是设备树 + Kconfig 必须配合的经典场景。

5.3src/main.c

/* src/main.c */#include<zephyr/kernel.h>#include<zephyr/device.h>#include<zephyr/drivers/gpio.h>#include<zephyr/sys/printk.h>/* —— LED:用 alias 定位(板厂一般已定义 led0 别名) —— */#defineLED0_NODEDT_ALIAS(led0)staticconststructgpio_dt_specled=GPIO_DT_SPEC_GET(LED0_NODE,gpios);/* —— 传感器:用我们自己起的 alias 定位 —— */#defineSENSOR_NODEDT_ALIAS(my_sensor)/* 注意:alias 里的 - 在宏里写成 _ */intmain(void){/* ① LED 就绪检查(编译期已确认节点存在 & status=okay) */if(!gpio_is_ready_dt(&led)){printk("Error: LED gpio not ready\n");return-1;}gpio_pin_configure_dt(&led,GPIO_OUTPUT_ACTIVE);printk("LED on pin %d ready\n",led.pin);/* 会打印我们 overlay 里设的 17 *//* ② 取传感器 device 句柄并检查就绪 */conststructdevice*sensor=DEVICE_DT_GET(SENSOR_NODE);if(!device_is_ready(sensor)){printk("Error: sensor %s not ready\n",sensor->name);}else{printk("Sensor %s ready, I2C addr = 0x%02x\n",sensor->name,DT_REG_ADDR(SENSOR_NODE));}/* ③ 闪灯,证明引脚生效 */while(1){gpio_pin_toggle_dt(&led);k_msleep(500);}return0;}

全程没有出现引脚号 17、I2C 地址 0x76 的硬编码——这些值全部来自设备树。换板子只改 overlay,main.c 一行不动。这就是设备树的威力。

5.4CMakeLists.txt

# CMakeLists.txt cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(dt_demo) target_sources(app PRIVATE src/main.c)

构建系统会自动发现工程根目录的app.overlay,无需在 CMakeLists.txt 里手动指定。如果你的 overlay 文件名很特殊,才需要set(EXTRA_DTC_OVERLAY_FILE my.overlay)


六、逐步编译 + 预期输出 + 现象对比

【📷 截图位:左边 app.overlay 的 led0 节点,中间 zephyr.dts 合并结果,右边 main.c 的 DT_ALIAS——用三段式箭头串起「设备树 → 生成宏 → 代码取值」】

第 1 步:编译

cddt_demo west build-bnrf52840dk/nrf52840-palways
  • 做什么:用 nrf52840dk 板子全新构建(-p always表示先清空缓存)。
  • 为什么:改过 overlay 后必须-p always,否则 CMake 可能用旧的设备树缓存(这是头号坑,见报错表第 4 条)。
  • 预期输出(成功末尾):
[100%]Built target zephyr_final Memory region Used Size Region Size %age Used FLASH:45120B1MB4.30% RAM:8704B256KB3.32%

第 2 步:验证 overlay 是否真的合并进去了

打开build/zephyr/zephyr.dts(这是所有 dtsi + dts + overlay 合并后的最终结果),搜索led_0bme280,应看到:

/* build/zephyr/zephyr.dts 片段 —— 注意这是合并后的最终设备树 */ led_0: led_0 { gpios = < &gpio0 0x11 GPIO_ACTIVE_HIGH >; /* 0x11 = 17,证明 overlay 生效! */ label = "Green LED"; }; i2c0: i2c@40003000 { status = "okay"; /* 已被我们 overlay 打开 */ clock-frequency = < 0x186a0 >; bme280: bme280@76 { compatible = "bosch,bme280"; reg = < 0x76 >; }; };

✅ 看到0x11(十进制 17)和bme280@76就说明 overlay 100% 生效了。这是最权威的验证方法,比看现象靠谱。

第 3 步:看生成的 C 宏(进阶排错用)

打开build/zephyr/include/generated/zephyr/devicetree_generated.h,能搜到一堆以节点为前缀的宏定义。设备树就是被展开成这个文件里的几千行#define,DT_ALIAS / DT_PROP 最终都指向这里。平时不用看,遇到「宏取值不对」时它是终极证据。

第 4 步:烧录看现象,体会「改前 vs 改后」

改前(overlay 里没有&led0那段,用板子默认引脚):板载某颗 LED 闪烁,串口打印LED on pin 13 ready(假设默认是 13)。

改后(加上我们的 overlay,引脚改 17):串口打印变成:

*** Booting Zephyr OS *** LED on pin17ready Sensor bme280@76 ready, I2C addr=0x76

接在P0.17上的 LED(或杜邦线接的外置 LED)开始闪烁,原来 13 脚那颗不动了。引脚号从 13 变成 17,main.c 没改一个字——这就是设备树带来的「改硬件不改代码」。

如果你手头没有 BME280 实物,device_is_ready会返回假,打印Error: sensor ... not ready,但编译能过、LED 照闪——这恰好说明设备树是编译期的「描述」,实物在不在是运行期的事。


七、代码如何读设备树:定位 → 取值 → 使用

口诀:先定位 → 再取值 → 后使用。

  1. 定位节点:把设备树节点变成一个「节点标识」给宏用。四种入口(按推荐度排序):

    • DT_ALIAS(led0):通过aliases最推荐,跨板通用;
    • DT_NODELABEL(i2c0):通过 label 标签;
    • DT_PATH(soc, i2c_40003000):通过完整路径;
    • DT_CHOSEN(zephyr_console):通过chosen全局选择。
  2. 取值:从节点标识里掏出具体数据。

    • 普通属性:DT_PROP(node, clock_frequency)
    • reg 地址:DT_REG_ADDR(node)
    • GPIO 三件套打包:GPIO_DT_SPEC_GET(node, gpios)→ 得到gpio_dt_spec
    • 设备句柄:DEVICE_DT_GET(node)→ 得到struct device *
  3. 使用:调_dt系列 API,参数直接传上一步的结构体,无需再拆引脚号:

    • gpio_is_ready_dt(&led)
    • gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE)
    • gpio_pin_toggle_dt(&led)

为什么强烈推荐_dt后缀的 API?因为它们直接吃gpio_dt_spec,引脚号、port、flag 全帮你填好了,杜绝手动传错参数。


八、常用 DT 宏速查

/* —— 定位节点 —— */DT_ALIAS(led0)// 通过 aliases 别名(最推荐)DT_NODELABEL(i2c0)// 通过 label 标签DT_PATH(soc,i2c_40003000)// 通过路径DT_CHOSEN(zephyr_console)// 通过 chosen/* —— 读属性 —— */DT_PROP(node,clock_frequency)// 读普通属性(注意 - 写成 _)DT_PROP_LEN(node,gpios)// 读数组/phandle-array 长度DT_REG_ADDR(node)// 读 reg 第一个地址DT_REG_SIZE(node)// 读 reg 大小/* —— GPIO / 设备实例 —— */GPIO_DT_SPEC_GET(node,gpios)// 构造 gpio_dt_specDEVICE_DT_GET(node)// 拿 struct device*/* —— 条件判断(全是编译期!) —— */DT_NODE_EXISTS(node)// 节点是否存在DT_NODE_HAS_STATUS(node,okay)// 节点是否启用

小贴士:设备树里属性名用连字符clock-frequency,到了 C 宏里统一换成下划线clock_frequency。这是高频低级错误,记死它。


九、排错圣经:去哪看「真相之源」

设备树排错只需盯两个生成文件,遇事不决先看它们:

想知道什么去哪看
overlay 到底有没有合并进去、属性最终是什么值build/zephyr/zephyr.dts
某个 DT 宏到底展开成了什么build/zephyr/include/generated/zephyr/devicetree_generated.h
某 compatible 允许哪些属性zephyr/dts/bindings/下按 compatible 找对应.yaml

💡最该养成的习惯:遇到设备树问题,第一时间打开build/zephyr/zephyr.dts。它是排错的「真相之源」,能省掉大量瞎猜。看到节点不在里面 = 没合并;看到status = "disabled"= 外设没开机;看到属性值不对 = overlay 写法有误。


十、高频报错排查表(15 条)

#报错 / 现象原因解决
1Node 'xxx' not foundlabel / alias 拼错,或该节点本就不存在zephyr.dts搜节点名核对;alias 里-在宏中写成_
2'xxx' is not a known property; ... not in binding属性没在 compatible 对应的.yamlbinding 里定义核对 compatible 拼写;翻dts/bindings/看该 binding 允许哪些属性
3Unable to find 'compatible'/ binding 找不到compatible 写错(空格、大小写、缺逗号)严格按厂商,型号,如bosch,bme280,不能有多余空格
4改了 overlay 完全没反应CMake 用了旧设备树缓存west build -p always全新构建(头号坑
5overlay 里的节点在 zephyr.dts 里根本没出现overlay 文件名 / 放置位置不对app.overlay 必须放工程根目录;板级用boards/<board>.overlay,斜杠换下划线
6device_is_ready()返回假,但编译过了节点status不是 okay,或驱动 Kconfig 没开overlay 里加status = "okay";;prj.conf 里开对应CONFIG_xxx=y
7gpio_is_ready_dt失败GPIO 控制器节点被禁用,或 alias 指向了 disabled 节点确认&gpio0status=okay;确认 led0 alias 真实存在
8'DT_N_..._P_xxx' undeclaredDT_PROP读了一个该节点没有的属性去 zephyr.dts 确认属性确实存在且拼写一致(-_
9dtc: ... syntax erroroverlay 语法错:缺分号、缺花括号、<>不配对每条属性结尾要;,节点结尾};,整数用<>
10'reg' is requiredbinding 要求 reg 但你没写,或@地址和 reg 不一致I2C 器件xxx@76要配reg = <0x76>;,二者必须一致
11I2C 器件挂上了却读不到数据I2C 总线没开 / 地址错 / SENSOR Kconfig 没开&i2c0 status="okay";核对器件手册地址;CONFIG_SENSOR=y
12EXTRA_DTC_OVERLAY_FILE指定的文件不生效路径写错或没-p always用相对工程根的路径,配合全新构建
13多个 overlay 时属性被「莫名覆盖」后加载的 overlay 覆盖了前面同名属性zephyr.dts看最终值;明确叠加顺序
14chosenzephyr,console改了但串口没换chosen 名在宏里要写成zephyr_console(逗号→下划线)DT_CHOSEN(zephyr_console);确认目标 uart status=okay
15删了节点报错但 zephyr.dts 还在编辑器没保存 / 改错了文件 / 没重新构建确认改的是工程根 overlay;保存后-p always

把这张表存下来,设备树 90% 的坑都在里面了。遇到没列出的报错,west build -p always,再看zephyr.dts,八成能定位。


十一、总结

  1. 设备树 = 硬件的「登记表」,把硬件信息从代码抽离,是「换板不改码」的底层;它是编译期展开成 C 宏,零运行时开销;
  2. 节点六要素:label、node-name、unit-address、compatible、phandle 属性、status;属性类型对应 binding 里的 type;
  3. 最终设备树是SoC.dtsi→ 板级.dts→ 应用.overlay三层叠加,后者优先级最高,改硬件永远优先写 overlay
  4. overlay 放工程根目录叫app.overlay,板级叫boards/<board>.overlay(斜杠换下划线)——放错位置是头号「不生效」原因;
  5. 代码三步:定位(DT_ALIAS)→ 取值(GPIO_DT_SPEC_GET/DEVICE_DT_GET)→ 使用(_dtAPI);
  6. 排错就盯两个文件:build/zephyr/zephyr.dts(合并结果)和devicetree_generated.h(生成宏);改完 overlay 必west build -p always

下一篇《Zephyr Kconfig 配置系统》:设备树管「硬件长什么样」,Kconfig 管「启用哪些功能」(还记得本篇 prj.conf 里那几行CONFIG_xxx=y吗?)。二者是 Zephyr 的黄金搭档,缺一不可,下一篇讲透。

如果帮到你,点赞 + 收藏 + 关注三连支持。设备树相关的报错欢迎贴评论区,我帮你看。

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

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

立即咨询