Zephyr 设备树(Devicetree)保姆级详解:语法 + overlay 实战 + DT 宏 + 排错(含完整可编译工程)
2026/6/25 14:37:00
网站开发
本文是「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_00 { compatible gpio-leds; gpios gpio0 13 GPIO_ACTIVE_LOW; label Green LED; status okay; };2.1 节点六要素led0:label 标签—— 节点的「昵称」。代码里用led0引用宏是DT_NODELABEL(led0)。一个节点可以有多个 label。led_0node-name 节点名—— 人类可读的节点名称同一父节点下不能重名除非靠 unit-address 区分。0unit-address 单元地址—— 区分同名同类节点通常对应该外设的寄存器基址。比如i2c40003000的40003000必须和它reg属性的第一个值一致。compatible兼容串——整个设备树最核心的属性。它把节点和一个binding 文件.yaml关联binding 定义该节点允许哪些属性、属性是什么类型驱动也靠 compatible 认领设备。compatible 写错 binding 找不到 一堆属性报错。gpios gpio0 13 GPIO_ACTIVE_LOWphandle 参数——gpio0是phandle指向 GPIO 控制器节点13是引脚号GPIO_ACTIVE_LOW是有效电平标志。这种「phandle 若干 cell」的组合叫phandle-array。status状态——okay启用该节点disabled禁用。只有 status okay 的节点驱动才会初始化、代码里gpio_is_ready_dt()才返回真。这是新手最常踩的坑外设没开机。2.2 常见属性类型对应 binding 里的 typeexample_node { a_string hello; /* string字符串 */ a_int 100; /* int / cell32 位整数 */ 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.c5.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: bme28076 { 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_GPIOy CONFIG_I2Cy CONFIG_SENSORy CONFIG_LOGy CONFIG_PRINTKy注意设备树管「硬件长什么样」但驱动开关在 Kconfig。挂了 I2C 器件却没开CONFIG_I2Cy驱动不会编进去device_is_ready()永远是假。这是设备树 Kconfig 必须配合的经典场景。5.3src/main.c/* src/main.c */#includezephyr/kernel.h#includezephyr/device.h#includezephyr/drivers/gpio.h#includezephyr/sys/printk.h/* —— LED用 alias 定位板厂一般已定义 led0 别名 —— */#defineLED0_NODEDT_ALIAS(led0)staticconststructgpio_dt_specledGPIO_DT_SPEC_GET(LED0_NODE,gpios);/* —— 传感器用我们自己起的 alias 定位 —— */#defineSENSOR_NODEDT_ALIAS(my_sensor)/* 注意alias 里的 - 在宏里写成 _ */intmain(void){/* ① LED 就绪检查编译期已确认节点存在 statusokay */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*sensorDEVICE_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 的硬编码——这些值全部来自设备树。换板子只改 overlaymain.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_0和bme280应看到/* build/zephyr/zephyr.dts 片段 —— 注意这是合并后的最终设备树 */ led_0: led_0 { gpios gpio0 0x11 GPIO_ACTIVE_HIGH ; /* 0x11 17证明 overlay 生效 */ label Green LED; }; i2c0: i2c40003000 { status okay; /* 已被我们 overlay 打开 */ clock-frequency 0x186a0 ; bme280: bme28076 { compatible bosch,bme280; reg 0x76 ; }; };✅ 看到0x11十进制 17和bme28076就说明 overlay 100% 生效了。这是最权威的验证方法比看现象靠谱。第 3 步看生成的 C 宏进阶排错用打开build/zephyr/include/generated/zephyr/devicetree_generated.h能搜到一堆以节点为前缀的宏定义。设备树就是被展开成这个文件里的几千行#defineDT_ALIAS / DT_PROP 最终都指向这里。平时不用看遇到「宏取值不对」时它是终极证据。第 4 步烧录看现象体会「改前 vs 改后」改前overlay 里没有led0那段用板子默认引脚板载某颗 LED 闪烁串口打印LED on pin 13 ready假设默认是 13。改后加上我们的 overlay引脚改 17串口打印变成*** Booting Zephyr OS *** LED on pin17ready Sensor bme28076 ready, I2C addr0x76接在P0.17上的 LED或杜邦线接的外置 LED开始闪烁原来 13 脚那颗不动了。引脚号从 13 变成 17main.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_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 里-在宏中写成_2xxx 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_xxxy7gpio_is_ready_dt失败GPIO 控制器节点被禁用或 alias 指向了 disabled 节点确认gpio0statusokay确认 led0 alias 真实存在8DT_N_..._P_xxx undeclared用DT_PROP读了一个该节点没有的属性去 zephyr.dts 确认属性确实存在且拼写一致-→_9dtc: ... syntax erroroverlay 语法错缺分号、缺花括号、不配对每条属性结尾要;节点结尾};整数用10reg is requiredbinding 要求 reg 但你没写或地址和 reg 不一致I2C 器件xxx76要配reg 0x76;二者必须一致11I2C 器件挂上了却读不到数据I2C 总线没开 / 地址错 / SENSOR Kconfig 没开i2c0 statusokay核对器件手册地址CONFIG_SENSORy12EXTRA_DTC_OVERLAY_FILE指定的文件不生效路径写错或没-p always用相对工程根的路径配合全新构建13多个 overlay 时属性被「莫名覆盖」后加载的 overlay 覆盖了前面同名属性用zephyr.dts看最终值明确叠加顺序14chosen里zephyr,console改了但串口没换chosen 名在宏里要写成zephyr_console逗号→下划线DT_CHOSEN(zephyr_console)确认目标 uart statusokay15删了节点报错但 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三层叠加后者优先级最高改硬件永远优先写 overlayoverlay 放工程根目录叫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_xxxy吗。二者是 Zephyr 的黄金搭档缺一不可下一篇讲透。如果帮到你点赞 收藏 关注三连支持。设备树相关的报错欢迎贴评论区我帮你看。