ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zephyr设备树实战:从原理到STM32F103驱动配置

Zephyr设备树实战:从原理到STM32F103驱动配置 写嵌入式的人大概率听过Zephyr也大概率听过设备树。可真正在Zephyr项目里动手配一个外设时很多刚从Linux转过来的朋友会先懵一下Zephyr不是轻量级RTOS吗怎么也要和设备树打交道Zephyr项目里的设备树不是可选项而是整个驱动模型的地基。从GPIO、UART、I2C到SSD1306这类小屏驱动能不能被系统识别、外设地址和参数怎么注入到代码里全都靠设备树在编译期完成“翻译”。这篇文章不打算念手册而是从实际项目出发把设备树在Zephyr里到底怎么工作、怎么写、怎么排错讲清楚。无论你是在Ubuntu上装了Zephyr SDK准备入门还是正卡在STM32F103、RK3568这类板子的配置上都可以把这篇当个起点。1. 为什么Zephyr项目离不开设备树1.1 设备树到底解决了什么问题先聊一个老场景。早期撸嵌入式裸机或者RTOS我们习惯把硬件信息直接写死在C文件里LED引脚是PA5UART基地址是0x40013800SPI最大速率是8MHz。这些宏散落在各个头文件里换一块板子就把对应的宏改一遍驱动代码被#ifdef切得稀碎。设备树就是来终结这种“宏定义地狱”的它把硬件是怎么连接的、地址是多少、引脚复用成什么功能、外设型号是哪颗芯片全部从C代码里剥离出来放进独立的.dts/.dtsi源文件里用统一的语法描述。Zephyr从2.x开始全面转向这套机制。以前可能用一个CONFIG_SSD1306_BASE_ADDRESS来指定控制器地址现在你会在设备树里写reg 0x3c以前用#define GPIO_LED_PIN 5现在会在设备树节点上挂一个gpios属性。驱动代码本身不再关心“我接在哪个引脚、地址是多少”只需要通过设备树API拿到设备实例然后调用标准驱动接口就行。这样做最大的好处是“硬件描述”和“软件逻辑”解耦了。同一份驱动代码只要设备树节点里的 compatible 字符串对得上换板子基本不用改C文件。设备树在Zephyr里不是锦上添花而是驱动模型的地基。你不用它等于自己搭了一套平行世界后面升级SDK、换芯片的时候会非常痛苦。1.2 它在Zephyr构建流程里的位置Zephyr的构建过程和传统Makefile项目很不一样。你执行west build -b 你的板子之后构建系统会先做一大堆“生成”工作设备树是其中最关键的一环。整个过程大概是这样的根据 board 名称找到对应的.dts和.dtsi文件解析这些文件同时读取dts/bindings/目录下的 binding YAML 文件用 Zephyr 的设备树生成器基于 DTC 和 edtlib把 dts 源文件转换成一个中间表示最终生成build/zephyr/include/generated/zephyr/devicetree.h以及一份合并后的完整设备树build/zephyr/zephyr.dts。这里有个新手最容易绕晕的地方设备树在Zephyr里是编译期处理的不是运行时候解析的。你在.dts里写的节点经过编译期生成器变成C语言可以用的宏和结构体程序跑起来之后并不会去读任何设备树二进制也不会动态枚举外设。所有“这个设备存不存在、地址是多少”的判断在编译那一刻已经定死了。所以排查问题的时候很多人习惯到 build 目录里找devicetree.h看看自己的节点有没有生成对应的宏。这是非常好的习惯。生成的头文件才是“最终结果”.dts只是“输入”。如果改了.dts之后重新编译没生效十有八九是增量构建缓存没更新直接west build -p强制重编一次往往就解决了。1.3 为什么Zephyr选择编译期处理而不是运行时解析用过Linux的都知道Linux是把设备树编成DTB然后由引导程序加载给内核内核在启动阶段解析设备树动态创建platform设备。Zephyr为什么不照搬这套因为RTOS资源太紧张了。设备树解析要消耗不少内存和时间Zephyr跑在单片机上的时候往往只有几十KB RAM不可能为了描述硬件再维护一棵运行时树。编译期处理带来的另一个好处是代码可以直接用宏访问设备信息。比如DT_NODELABEL(uart0)返回一个节点标识符DT_REG_ADDR(...)取地址这些操作在C代码里看起来像是函数实际编译后就变成了常量。没有运行时开销也没有动态内存分配非常适合裸机思维。代价是什么呢就是你每次改硬件配置都必须重新编译整个工程。对于嵌入式开发来说这个代价完全能接受。理解了编译期生成这个大前提后面看什么DT_ALIAS、DT_NODELABEL、binding都不会觉得神神叨叨了。2. 看懂设备树语法、节点与Zephyr binding2.1 一个最小节点拆开看设备树的基本单元是节点。一个节点代表一个设备、一个控制器或者一个子总线。用SSD1306这颗OLED驱动芯片举个例子最小节点大概是这样的ssd1306: ssd13063c { compatible solomon,ssd1306; reg 0x3c; status okay; width 128; height 64; };拆开看四个要素ssd1306:是节点标签node label相当于给这个节点起了一个可以在其他地方引用的“变量名”。别的地方想引用它用ssd1306就行。ssd13063c是节点名字后面的3c是单元地址。这个地址要和reg对应通常是为了人眼可读不是强制的但规范建议写。compatible solomon,ssd1306是驱动匹配的关键字符串。格式一般是“厂商,型号”小写。Zephyr驱动注册的时候会用DT_DRV_COMPAT指定自己支持的 compatible两者对上这个节点才可能被这个驱动接管。reg 0x3c是设备地址。对I2C设备来说就是7位从机地址对SPI设备来说可能是片选编号对内存映射外设来说就是寄存器基地址。status okay是设备开关。默认很多控制器节点在.dtsi里是disabled你要用哪个就把它改成okay。这个属性非常基础但也是“设备不可用”最常见的坑。看节点的时候不要被一堆字段吓到。大部分属性都是按照 binding 文件约定填写的。你不认识某个属性去dts/bindings/里搜对应的 compatible通常能找到每个属性的含义和类型。2.2 binding决定属性含义的规则文件设备树本身只是树形数据光有width 128这种写法编译器不知道 width 是“屏幕宽度”还是“总线消息长度”。Zephyr 用一套YAML binding 文件来给节点模板加规则。每个compatible会对应一个.yaml文件例如dts/bindings/display/solomon,ssd1306.yaml里面声明了这个设备支持哪些属性、哪些必填、哪些可选、属性类型是什么。一个简化版的 binding 长这样compatible: solomon,ssd1306 include: [i2c-device.yaml, display.yaml] properties: width: type: int required: true height: type: int required: true segment-offset: type: int required: false这段的意思很直白compatible声明这套规则绑定的设备include嵌入通用的I2C设备和显示设备规则properties描述自定义属性。你如果在.dts里写了一个 binding 里没有的属性设备树生成器不会报错但不会给你生成对应宏代码里引用就会编译失败。反过来binding 里标了required: true的属性你在.dts里漏写了生成器可能会给出警告甚至错误。所以遇到“宏找不到”的问题第一反应不应该是去C代码里找而应该检查.dts节点属性和 binding 是否匹配。设备树语法是一层binding是另一层两层都对最终生成结果才正确。2.3 aliases、chosen与Zephyr的特殊约定除了普通节点设备树顶层还会看到aliases和chosen两个特殊块。它们不描述具体硬件而是给硬件节点提供“逻辑别名”或“系统级选项”。/ { aliases { oled ssd1306; }; chosen { zephyr,console uart0; zephyr,sram sram0; }; };aliases的作用是给节点起一个稳定的短名字。比如多个I2C总线上可能挂了屏幕、传感器、EEPROM代码里写DT_ALIAS(oled)就比写完整的DT_NODELABEL(ssd1306)更符合“这个板子上OLED就是那个节点”的语义。换一块板子只要 aliases 里仍把 OLED 指到新节点C代码一个字都不用改。chosen更像系统配置。zephyr,console指定控制台串口zephyr,sram指定系统内存区域。Zephyr的很多子系统依赖这些固定名称的chosen属性比如zephyr,flash、zephyr,display。用Linux的设备树经验看这些名字会觉得很奇怪但它们是Zephyr生态自己的约定必须按文档来。2.4 和Linux设备树的关键差异别把两个体系混在一起搜索“RK3568 触摸屏竖屏改为横屏”时你看到的是Linux设备树的改法在触摸屏节点里加touchscreen-inverted-x或者rotation之类的属性。这些经验能不能直接套到Zephyr上不能。两者虽然共用.dts语法这一套“外壳”但运行时行为、属性集合、驱动匹配机制是完全不同的体系。Linux是在运行时用of_系列API解析设备树驱动开发者可以在probe函数里动态读取属性Zephyr是在编译期生成宏驱动通过DT_*宏把节点信息编译进固件。属性命名方面Zephyr除了通用属性还有很多zephyr,前缀的自定义属性比如zephyr,console、zephyr,bt-mon-uart。Linux设备树很少看到这种命名。另外Zephyr的设备树通常直接编译进固件不需要单独的DTB文件甚至不需要bootloader传参数Linux的设备树由bootloader加载给内核bootloader和内核都可能修改它。这也意味着你把一个Linux平台的.dts复制到Zephyr工程里大概率会构建失败因为引用的很多节点和 binding 都不存在。跨界参考语法可以跨界抄文件不行。3. 实操给STM32F103配一个SSD1306 OLED3.1 准备工作在Ubuntu上跑通Zephyr工程在动手改设备树之前得先有一个能编译的Zephyr环境。我自己的流程是在Ubuntu上装Zephyr SDK和west工具这一套官方文档写得算清晰。重点提醒几个容易踩的坑安装west之后别忘记west init和west update把Zephyr仓库和模块同步下来。记得设置ZEPHYR_TOOLCHAIN_VARIANTzephyr并且把SDK的路径配好。环境变量没设对编译时会报找不到编译器但报错信息可能很误导人。新版本Zephyr对cmake和Python版本要求不低Ubuntu自带的版本太老的话建议先装好新版cmake。环境准备好之后先用官方示例验证一遍。west build -b 你的板子 samples/hello_world如果能正常烧录并看到串口输出说明基础链路没问题后面改设备树才有意义。3.2 定位板级设备树文件Zephyr的board目录结构一般长这样boards/ arm/ stm32f103_mini/ stm32f103_mini.dts stm32f103_mini.yaml Kconfig.stm32f103_mini ...不同厂商芯片的默认外设描述放在SoC的.dtsi文件里。比如STM32F103的I2C1、UART1、SPI1等控制器的基地址和中断号通常已经在SoC的.dtsi里定义好了。板级.dts负责决定“这个板子上哪些外设被引出、引脚复用成什么、挂了什么芯片”。拿到一个新板子我建议先去看官方board目录里的.dts和.dtsi搜索status disabled的节点。这样你能快速知道SoC有哪些外设哪些被板卡默认关闭了。比如你要用I2C1就搜i2c1看它的status是不是okay看有没有pinctrl配置。3.3 使用overlay添加外设节点实际项目中我不建议直接大改官方board文件因为Zephyr SDK更新时官方文件会被覆盖。更好的做法是使用overlay机制在项目目录里放一个板名.overlay构建时自动叠加到原board dts上。一个最小项目结构可以是my_oled/ CMakeLists.txt prj.conf src/ main.c boards/ olimex_stm32_h103.overlay在overlay文件里写/ { aliases { oled ssd1306; }; }; i2c1 { status okay; ssd1306: ssd13063c { compatible solomon,ssd1306; reg 0x3c; width 128; height 64; }; };注意几点如果SoC的.dtsi里I2C1默认是 disabled必须在引用它的地方先status okay否则节点存在但设备不可用。ssd1306的reg地址要确认你的模块是0x3C还是0x3D很多淘宝OLED模块背面有电阻可以改地址。I2C地址写错驱动初始化的时候会一直找不到设备。另外SSD1306的可用属性不同Zephyr版本略有差异建议打开dts/bindings/display/solomon,ssd1306.yaml核一遍。如果不想每个项目都建一个 board 目录下的 overlay也可以在构建命令里指定west build -b olimex_stm32_h103 -- -DOVERLAY_CONFIGboards/olimex_stm32_h103.overlay3.4 在C代码中通过设备树API访问设备设备树节点配置好之后C代码里不需要再去到处找地址。以显示设备为例最常用的方式是#include zephyr/device.h #include zephyr/drivers/display.h #include zephyr/devicetree.h static const struct device *oled DEVICE_DT_GET(DT_ALIAS(oled)); void main(void) { if (!device_is_ready(oled)) { printk(OLED device not ready\n); return; } display_blanking_off(oled); /* 后续就能用 display_write 或 display_buffer_map 等接口画东西了 */ }DEVICE_DT_GET(DT_ALIAS(oled))做的事情是通过 aliases 里的oled找到节点再拿到该节点对应的设备对象。还有一个常见写法是DEVICE_DT_GET(DT_NODELABEL(ssd1306))。两者差别在于DT_NODELABEL是直接用节点标签DT_ALIAS是走别名。如果未来换板子换了OLED节点命名用DT_ALIAS的代码不需要动。如果驱动需要知道DTS里某个属性值比如屏幕尺寸可以用#define OLED_WIDTH DT_PROP(DT_ALIAS(oled), width) #define OLED_HEIGHT DT_PROP(DT_ALIAS(oled), height)这里的DT_PROP就是编译期从设备树节点里取属性值的宏。它本质上是读取生成头文件里的宏不会产生运行时开销。3.5 编译验证的完整流程配置完之后编译命令最好带--pristine强制全量编译避免Zephyr的设备树生成机制没有刷新west build -p -b olimex_stm32_h103 .编译成功之后想确认设备树到底生成成什么样可以看两个文件。一个是build/zephyr/zephyr.dts这是所有 dts 源文件叠加之后的完整设备树另一个是build/zephyr/include/generated/zephyr/devicetree.h这是C代码实际用的宏清单。我习惯先在devicetree.h里搜一下ssd1306grep -n ssd1306 build/zephyr/include/generated/zephyr/devicetree.h如果能搜到对应宏说明节点被正确解析了如果搜不到赶紧回头查.dts语法或者 binding。另外也可以在代码里临时加一个编译期断言BUILD_ASSERT(DT_NODE_HAS_STATUS(DT_ALIAS(oled), okay), OLED node is not okay);这样一旦节点状态不对编译直接报错比运行时打印一个“not ready”更早把问题暴露出来。4. 设备树排查与避坑指南4.1 常见问题速查表设备树报错有个特点看着是编译错误根因五花八门。我把实际中遇到最多的问题整理了一张速查表照着查顺序能省很多时间。问题现象可能原因排查方向编译报undefined node labeli2c1引用了不存在的节点检查SoC dtsi里是否有i2c1拼写是否一致设备树生成有multiple matches警告多个节点 compatible 完全一样把不需要的节点status disableddevice_is_ready返回 falsestatus不是okay或驱动没编译检查节点status、Kconfig是否启用驱动代码里DT_PROP宏找不到binding里没定义该属性或者属性名写错打开对应binding yaml核对属性名屏幕/传感器I2C通信失败I2C地址错误、上拉电阻缺失用逻辑分析仪看I2C总线波形外设引脚不工作pinctrl没配置或配置错误检查pinctrl-0节点和各state定义devicetree.h里没有自己的节点节点写在overlay里但没被加载确认overlay文件名和board名一致构建时DTC报语法错误缺少分号或大括号不匹配查看报错行号检查上一节点收尾很多问题在设备树层显示不出来要到驱动层才暴露。“设备树解析成功”不等于“硬件正常工作”中间还隔着Kconfig、引脚复用、电气连接。排查的时候要一层层来不要一上来就怀疑设备树。先确认devicetree.h有宏再确认CONFIG_有使能驱动最后再查硬件。4.2 从build目录反推问题Zephyr的设备树生成器会生成中间文件build目录里有很多线索。我最常用的三个文件build/zephyr/zephyr.dts合并后的完整设备树。如果你不确定overlay有没有生效直接看这个文件最靠谱。build/zephyr/include/generated/zephyr/devicetree.h所有设备树宏。代码编译不过的时候先来这里搜节点名。build/zephyr/.config最终生效的Kconfig配置。设备没绑定但驱动没编译时这个文件会告诉你哪个选项是# CONFIG_SSD1306 is not set。如果你改完.dts重新编译发现zephyr.dts里还是旧内容那就是缓存问题。Zephyr虽然默认做增量构建但设备树/overlay的依赖追踪偶尔也会抽风。遇到灵异现象直接west build -p重来一编百分之八十能解决。4.3 pinctrl、中断、PHY等特殊节点怎么排查普通外设配好 compatible 和 reg 就能跑但GPIO、UART、I2C这类接引脚的外设必须配pinctrl。Zephyr的pinctrl节点长这样i2c1 { pinctrl-0 i2c1_scl_pb6 i2c1_sda_pb7; pinctrl-names default; status okay; };i2c1_scl_pb6是引脚复用子节点定义在dtsi里。如果你的板子引出的I2C脚不是PB6/PB7而是别的引脚就要检查pinctrl配置是否和实际硬件一致。很多开发板厂商提供的原理图只写I2C1不会告诉你引脚但设备树里pinctrl已经固化了。这里最容易出现“节点看起来都对但总线没波形”的问题。中断相关的排查也有点类似。Zephyr设备树里的interrupts X Y需要和SoC的中断控制器定义匹配。如果你在写一个轮询方式的驱动而不是中断驱动也要注意某些Zephyr驱动在节点里检测不到有效中断时会自动fallback到polling模式。这个行为正好对应了“Zephyr polling API详解”那类话题设备树里中断属性缺失并不一定导致设备不可用只是驱动会走轮询路径。要确认驱动到底走没走中断可以打开CONFIG_DEBUG_DRIVER或CONFIG_ASSERT或者看驱动日志。关于PHY这类网络外设比如YT8521我的建议是直接看Zephyr仓库里对应drivers/net/phy绑定的YAML文件。不要拿Linux设备树的PHY配置来套尤其是寄存器读写时序和复位引脚属性两边差异很大。实在不确定节点怎么写去Zephyr的测试板目录里搜一个同样PHY的dts参考。4.4 给新手的三个实操建议最后说几个我自己的习惯都是踩坑踩出来的。第一拿到新板子先别急着写代码花半小时读一遍官方board的.dts和.dtsi。把每个 disabled 的节点过一遍看看哪些外设可能可用再搜一下aliases和chosen了解系统默认配置。这一步能做到“心里有树”。第二尽量用 overlay 而不是直接改官方 board 文件。overlay 文件放在自己项目目录里跟代码一起走版本管理。Zephyr升级后官方文件被更新我们的 overlay 不受影响。如果非要改官方 dts也要用 git 记录下来不然下次west update更新SDK的时候修改会被默默覆盖。第三排错时先区分“设备树层”和“驱动层”。设备树层看devicetree.h里有没有宏、zephyr.dts里有没有节点驱动层看Kconfig是否使能、device_is_ready是否通过。这两步别乱跳乱改DTS只会把问题搞得更复杂。我自己在实际项目里的体会是设备树在Zephyr中的学习曲线确实偏陡尤其是从Linux转过来的朋友容易带着“运行时解析”的预期到处碰壁。但一旦转过弯来理解了“编译期生成宏”这个关键设计再去看那些DT_*API会发现一切都非常直接。设备树本质上就是把硬件信息用一套结构化的语法固定下来让驱动更通用让板级差异更透明。遇到新板子不要慌先把.dts打开把节点一层层拆开看大部分问题都能在心里画出清晰的脉络了。
返回列表