本文是「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 节点六要素
led0:(label 标签) —— 节点的「昵称」。代码里用&led0引用,宏是DT_NODELABEL(led0)。一个节点可以有多个 label。led_0(node-name 节点名) —— 人类可读的节点名称,同一父节点下不能重名(除非靠 unit-address 区分)。@0(unit-address 单元地址) —— 区分同名同类节点,通常对应该外设的寄存器基址。比如i2c@40003000的@40003000必须和它reg属性的第一个值一致。compatible(兼容串) —— 整个设备树最核心的属性。它把节点和一个 binding 文件(.yaml) 关联,binding 定义该节点允许哪些属性、属性是什么类型;驱动也靠 compatible 认领设备。compatible 写错 = binding 找不到 = 一堆属性报错。gpios = <&gpio0 13 GPIO_ACTIVE_LOW>(phandle + 参数) ——&gpio0是 phandle,指向 GPIO 控制器节点;13是引脚号;GPIO_ACTIVE_LOW是有效电平标志。这种「phandle + 若干 cell」的组合叫 phandle-array。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,就得写 <尖括号数字>。
三、设备树的「叠加」模型
新手最容易懵的点:最终设备树不是某一个文件,而是多层叠加合并出来的。

合并顺序(后者优先级更高、可覆盖前者):
- SoC
.dtsi:芯片厂商写,描述 CPU、片上外设地址、中断号(如nrf52840.dtsi)。一般不动它。 - 板级
.dts:开发板厂商写,描述板上器件接线、哪些外设默认开启(如nrf52840dk_nrf52840.dts)。一般也不动它。 - 应用
.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.conf、CMakeLists.txt同级),不是放在src/里!放错地方 = 构建系统根本找不到 = 静默不生效。【📷 截图位:文件管理器里展开工程目录树,红框标出 app.overlay 和 boards/ 与 prj.conf 同级】
五、完整实战工程:改 LED 引脚 + 挂 I2C 传感器
下面是一个复制即可编译的完整工程。目标:把 led0 改到另一个引脚,并在 I2C 总线上挂一个 BME280 温湿度传感器,开机打印它的设备名是否就绪。
工程目录结构(先建好这个结构):
dt_demo/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
└── src/
└── main.c
5.1 app.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.2 prj.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.3 src/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 别名) —— */
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
/* —— 传感器:用我们自己起的 alias 定位 —— */
#define SENSOR_NODE DT_ALIAS(my_sensor) /* 注意:alias 里的 - 在宏里写成 _ */
int main(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 句柄并检查就绪 */
const struct device *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);
}
return 0;
}
全程没有出现引脚号 17、I2C 地址 0x76 的硬编码——这些值全部来自设备树。换板子只改 overlay,main.c 一行不动。这就是设备树的威力。
5.4 CMakeLists.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 步:编译
cd dt_demo
west build -b nrf52840dk/nrf52840 -p always
- 做什么:用 nrf52840dk 板子全新构建(
-p always表示先清空缓存)。 - 为什么:改过 overlay 后必须
-p always,否则 CMake 可能用旧的设备树缓存(这是头号坑,见报错表第 4 条)。 - 预期输出(成功末尾):
[100%] Built target zephyr_final
Memory region Used Size Region Size %age Used
FLASH: 45120 B 1 MB 4.30%
RAM: 8704 B 256 KB 3.32%
第 2 步:验证 overlay 是否真的合并进去了
打开 build/zephyr/zephyr.dts(这是所有 dtsi + dts + overlay 合并后的最终结果),搜索 led_0 和 bme280,应看到:
/* 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 pin 17 ready
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 照闪——这恰好说明设备树是编译期的「描述」,实物在不在是运行期的事。
七、代码如何读设备树:定位 → 取值 → 使用
口诀:先定位 → 再取值 → 后使用。
-
定位节点:把设备树节点变成一个「节点标识」给宏用。四种入口(按推荐度排序):
DT_ALIAS(led0):通过aliases,最推荐,跨板通用;DT_NODELABEL(i2c0):通过 label 标签;DT_PATH(soc, i2c_40003000):通过完整路径;DT_CHOSEN(zephyr_console):通过chosen全局选择。
-
取值:从节点标识里掏出具体数据。
- 普通属性:
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 *。
- 普通属性:
-
使用:调
_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_spec
DEVICE_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 条)
| # | 报错 / 现象 | 原因 | 解决 |
|---|---|---|---|
| 1 | Node 'xxx' not found | label / alias 拼错,或该节点本就不存在 | 去 zephyr.dts 搜节点名核对;alias 里 - 在宏中写成 _ |
| 2 | 'xxx' is not a known property; ... not in binding | 属性没在 compatible 对应的 .yaml binding 里定义 | 核对 compatible 拼写;翻 dts/bindings/ 看该 binding 允许哪些属性 |
| 3 | Unable to find 'compatible' / binding 找不到 | compatible 写错(空格、大小写、缺逗号) | 严格按 厂商,型号,如 bosch,bme280,不能有多余空格 |
| 4 | 改了 overlay 完全没反应 | CMake 用了旧设备树缓存 | west build -p always 全新构建(头号坑) |
| 5 | overlay 里的节点在 zephyr.dts 里根本没出现 | overlay 文件名 / 放置位置不对 | app.overlay 必须放工程根目录;板级用 boards/<board>.overlay,斜杠换下划线 |
| 6 | device_is_ready() 返回假,但编译过了 | 节点 status 不是 okay,或驱动 Kconfig 没开 | overlay 里加 status = "okay";;prj.conf 里开对应 CONFIG_xxx=y |
| 7 | gpio_is_ready_dt 失败 | GPIO 控制器节点被禁用,或 alias 指向了 disabled 节点 | 确认 &gpio0 status=okay;确认 led0 alias 真实存在 |
| 8 | 'DT_N_..._P_xxx' undeclared | 用 DT_PROP 读了一个该节点没有的属性 | 去 zephyr.dts 确认属性确实存在且拼写一致(-→_) |
| 9 | dtc: ... syntax error | overlay 语法错:缺分号、缺花括号、<> 不配对 | 每条属性结尾要 ;,节点结尾 };,整数用 <> |
| 10 | 'reg' is required | binding 要求 reg 但你没写,或 @地址 和 reg 不一致 | I2C 器件 xxx@76 要配 reg = <0x76>;,二者必须一致 |
| 11 | I2C 器件挂上了却读不到数据 | I2C 总线没开 / 地址错 / SENSOR Kconfig 没开 | &i2c0 status="okay";核对器件手册地址;CONFIG_SENSOR=y |
| 12 | EXTRA_DTC_OVERLAY_FILE 指定的文件不生效 | 路径写错或没 -p always | 用相对工程根的路径,配合全新构建 |
| 13 | 多个 overlay 时属性被「莫名覆盖」 | 后加载的 overlay 覆盖了前面同名属性 | 用 zephyr.dts 看最终值;明确叠加顺序 |
| 14 | chosen 里 zephyr,console 改了但串口没换 | chosen 名在宏里要写成 zephyr_console(逗号→下划线) | DT_CHOSEN(zephyr_console);确认目标 uart status=okay |
| 15 | 删了节点报错但 zephyr.dts 还在 | 编辑器没保存 / 改错了文件 / 没重新构建 | 确认改的是工程根 overlay;保存后 -p always |
把这张表存下来,设备树 90% 的坑都在里面了。遇到没列出的报错,先
west build -p always,再看zephyr.dts,八成能定位。
十一、总结
- 设备树 = 硬件的「登记表」,把硬件信息从代码抽离,是「换板不改码」的底层;它是编译期展开成 C 宏,零运行时开销;
- 节点六要素:label、node-name、unit-address、compatible、phandle 属性、status;属性类型对应 binding 里的 type;
- 最终设备树是 SoC
.dtsi→ 板级.dts→ 应用.overlay三层叠加,后者优先级最高,改硬件永远优先写 overlay; - overlay 放工程根目录叫
app.overlay,板级叫boards/<board>.overlay(斜杠换下划线)——放错位置是头号「不生效」原因; - 代码三步:定位(
DT_ALIAS)→ 取值(GPIO_DT_SPEC_GET/DEVICE_DT_GET)→ 使用(_dtAPI); - 排错就盯两个文件:
build/zephyr/zephyr.dts(合并结果)和devicetree_generated.h(生成宏);改完 overlay 必west build -p always。
下一篇《Zephyr Kconfig 配置系统》:设备树管「硬件长什么样」,Kconfig 管「启用哪些功能」(还记得本篇 prj.conf 里那几行 CONFIG_xxx=y 吗?)。二者是 Zephyr 的黄金搭档,缺一不可,下一篇讲透。
如果帮到你,点赞 + 收藏 + 关注三连支持。设备树相关的报错欢迎贴评论区,我帮你看。
保姆级详解:语法 + overlay 实战 + DT 宏 + 排错(含完整可编译工程)&spm=1001.2101.3001.5002&articleId=162280202&d=1&t=3&u=e35f6008782d49f8acee3411013c147a)
928

被折叠的 条评论
为什么被折叠?



