STM32MP157上使用Developer Package编译设备树实操指南
STM32MP157D-DK1这块板子这几年在嵌入式Linux圈子里出镜率一直不低。不管你是做产品原型验证还是纯粹想上手研究ST的MPU大概率都会遇到一个绕不开的操作改设备树device tree。很多朋友第一次拿到Developer Package包的工程时第一反应是“这玩意儿到底怎么编译bin文件在哪为什么我改了dts不生效”这篇文章把我自己实际跑过的流程完整写一遍。内容包括为什么要用Developer Package编译设备树、环境怎么搭、dts源码从哪里找、改哪些节点最常用、编译命令到底怎么敲、生成后的dtb文件怎么放进开发板以及我踩过的一些坑和排查手段。内容按实际项目走下来的顺序整理适合刚接触STM32MP1系列、对设备树只有概念但没完整实操过的读者。1. 整体思路拆解为什么单独编译设备树1.1 内核镜像和设备树是两套东西STM32MP157D-DK1跑的是OpenSTLinux内核镜像uImage或者fitImage和设备树二进制dtb在启动时是分开加载的。BootloaderU-Boot会先读到内核镜像然后根据启动配置里的FDT路径去另一个位置加载dtb最后把dtb地址传给内核。也就是说你改外设配置、改引脚复用、加一个I2C设备都不需要重新编译整个内核只需要重新生成dtb文件替换掉就行。这就是为什么很多工作流里“只编设备树”而不是“全量编译内核”。在Developer Package环境下官方已经帮你把内核源码、交叉编译链、配置环境都准备好了目的就是让开发者能快速改外设、编dtb、编内核模块而不需要从头跑一整套Yocto流程。1.2 Developer Package和Source Package到底选哪个ST官方对STM32MP1系列提供了两种开发者包Developer Package和Source Package。很多人一开始搞不清区别。Developer Package的定位是“在已构建好的发行版上进行增量开发”它包含交叉编译工具链、内核源码树、U-Boot源码以及一些辅助脚本和SDK环境。它不从头构建整个Linux发行版所以编译设备树、编译内核模块这类工作非常轻快。Source Package则是给你完整的Yocto构建环境适合要修改系统组件、重新生成根文件系统、做定制发行版的人。如果你只是像标题里说的那样“用Developer Package编译设备树”那不需要碰Yocto也不需要了解bitbake的繁琐概念装好SDK环境直接进到内核源码目录用交叉编译命令把dtb编出来就能解决问题。这也是大多数板级开发者的实际日常。1.3 本地修改和最终烧录的路径关系整个流程简单画个脉络拿到Developer Package后环境里会有一份对应版本的内核源码源码路径下的arch/arm/boot/dts/里放着所有STM32MP1的设备树源文件我们改的是这个目录里的stm32mp157d-dk1.dts然后用SDK提供的交叉编译环境执行make命令生成stm32mp157d-dk1.dtb最后把这个dtb文件拷贝到开发板SD卡或者扩展分区里替换掉原有的dtb。注意这里说的“替换”一定要先备份后面我会具体讲。2. 环境准备把Developer Package跑起来2.1 硬件准备与启动介质我用的是STM32MP157D-DK1开发板它有两个USB Type-C口一个是电源/ST-LINK调试一个是对外的USB口。实际操作时建议准备一张至少8GB的高速SD卡因为OpenSTLinux官方镜像默认烧录到SD卡启动方式也是从SD卡启动。连接方式Type-C线接开发板电源口插到电脑上另一端ST-LINK的虚拟串口驱动装好Ubuntu里一般不需要额外驱Windows下需要ST官网的驱动。开发板的串口调试终端建议用115200 8N1这个在启动日志里能看到也是STM32MP1系列默认的调试串口速率。2.2 宿主机系统要求我在Ubuntu 20.04 LTS 64位上跑通了整个流程。ST官方文档里也推荐Ubuntu 18.04或20.04。这里有个小建议不要用最新的Ubuntu 24.04去折腾有些老版本的SDK脚本在太新的系统上会出现兼容问题特别是依赖库路径变化的情况。如果你只有高版本系统也不是不行只是可能会需要手动补装一些32位库绕一圈回来反而浪费时间。安装Developer Package前尽量先确保系统里有下面这些基础工具gcc、g、makebison、flexlibncurses-devdevice-tree-compiler这个用来做设备树反编译和校验在Ubuntu下执行sudo apt update sudo apt install gcc g make bison flex libncurses-dev device-tree-compiler2.3 获取并解压Developer PackageST官方通过STMicroelectronics的官网提供Developer Package下载通常是以tar.xz格式打包的SDK。你打开页面后会看到一个类似这样的文件en.stm32mp1-openstlinux-5.15-yocto-kirkstone-mp1-v23.06.21.tar.gz文件名里的数字是版本信息不同日期版本大同小异。下载后解压到工作目录我习惯放在/opt/st/下面sudo mkdir -p /opt/st sudo chown $USER:$USER /opt/st tar xf en.stm32mp1-openstlinux-5.15-yocto-kirkstone-mp1-v23.06.21.tar.gz -C /opt/st解压完成后目录里会有一个sdk目录下面放着environment-setup开头的脚本这个脚本就是整个Developer Package的入口。2.4 导入SDK环境变量每次新开一个终端如果要使用交叉编译链需要先source一下环境脚本source /opt/st/STM32MP1-Ecosystem-v5.0.0/Developer-Package/stm32mp1-openstlinux-5.15-yocto-kirkstone-mp1-v23.06.21/sdk/environment-setup-cortexa7t2hf-neon-vfpv4-openstlinux_weston-linux-gnueabisource完之后可以用下面命令确认交叉编译器是否生效$CC --version如果你看到类似arm-openstlinux_weston-linux-gnueabi-gcc (GCC) 10.2.0说明环境正常。这个$CC变量的值已经指向了ST定制的交叉编译器后面编译设备树时会用到。这里有个很关键的点如果不source环境脚本直接用系统自带的gcc去编设备树大概率会遇到头文件找不到或者架构不匹配的错误。这一点很多新手容易忽略。3. 设备树源码解析找到文件并看懂结构3.1 内核源码中dts所在的位置Developer Package安装好之后内核源码并不会自动解压你需要先通过devtool或者手动解压获得源码树。以我用的版本为例Developer Package目录下有一个sources目录里面放着linux-stm32mp的内核压缩包。手动解压cd /opt/st/STM32MP1-Ecosystem-v5.0.0/Developer-Package/stm32mp1-openstlinux-5.15-yocto-kirkstone-mp1-v23.06.21/sources/arm-ostl-linux-gnueabi/linux-stm32mp-5.15.24 tar xf linux-stm32mp-5.15.24.tar.xz解压后进入内核源码目录cd linux-stm32mp-5.15.24设备树源文件统一放在arch/arm/boot/dts/下其中和STM32MP157D-DK1直接相关的文件是stm32mp157d-dk1.dtsstm32mp157d-dk1-m4.dts带协处理器内存配置的版本stm32mp157a-dk1.dts等其他型号对应变体用编辑器打开stm32mp157d-dk1.dts你会发现它的内容非常简洁大量内容是靠#include引入公共dtsi文件来复用的。3.2 dts文件文本拆解打开后你会看到类似这样的结构/dts-v1/; #include stm32mp157.dtsi #include stm32mp15xa.dtsi #include stm32mp15-pinctrl.dtsi #include stm32mp15xxaa-pinctrl.dtsi #include dt-bindings/gpio/gpio.h #include dt-bindings/input/input.h #include dt-bindings/leds/common.h / { model STMicroelectronics STM32MP157D-DK1 Discovery Board; compatible st,stm32mp157d-dk1, st,stm32mp157; aliases { serial0 uart4; }; chosen { stdout-path uart4; }; memoryc0000000 { device_type memory; reg 0xc0000000 0x20000000; }; };这里有必要解释几个关键概念。model和compatible是内核用来识别硬件平台的关键字段compatible会与内核里machine_desc的匹配字符串比对决定哪一套初始化流程被使用。aliases里的serial0定义串口的别名serial0通常对应ttySTM0ST的debug串口就是uart4。chosen里stdout-path指向uart4意思是内核启动时earlycon和console都输出到uart4。memory节点定义了内存起始地址和大小STM32MP157D-DK1板载1GB DDR3L所以reg是0xc0000000开始、长度为0x20000000。再往下走你会看到很多外设节点它们不是直接定义在根节点里而是通过“”引用来展开和覆盖uart4 { pinctrl-names default, sleep; pinctrl-0 uart4_pins_a; pinctrl-1 uart4_pins_sleep_a; status okay; }; i2c1 { pinctrl-names default, sleep; pinctrl-0 i2c1_pins_a; pinctrl-1 i2c1_pins_sleep_a; status okay; /delete-property/ dmas; /delete-property/ dma-names; };这种写法的好处是通用定义放在stm32mp157.dtsi里板级差异放在板级dts里。你改板子相关外设时基本不需要动大文件只需要在板级dts里追加或覆盖节点。3.3 最常见的三种修改场景实际项目里改设备树通常跑不出这三类一是修改某个引脚的复用功能比如把一个原本用作GPIO的引脚改成UART或PWM二是在某个I2C总线上挂一个外设传感器比如温度传感器或者EEPROM三是禁用板上默认用不到的外设比如把SDMMC相关的节点status改成disabled以节省引脚或功耗。举一个I2C外设的例子。比如板子上i2c1上要挂一个地址为0x48的温度传感器就可以写成i2c1 { status okay; lm7548 { compatible national,lm75; reg 0x48; }; };注意compatible里的“national”是历史遗留命名linux内核的lm75驱动里一直是这么匹配的这个不算错误。reg地址要和传感器硬件地址对应。如果是修改GPIO控制器比如把一个LED接在PA5引脚对应代码片段led-blue { label blue; gpios gpioa 5 GPIO_ACTIVE_LOW; default-state off; };这里gpioa是GPIO控制器节点5是引脚编号GPIO_ACTIVE_LOW表示低电平点亮。这些细节看起来不复杂但方向写反或者编号搞错最终表现就是“灯不受控”或者“引脚不输出”。这类问题排查起来非常耗时间所以我建议每次都先在电路图和板卡标注上确认引脚编号。4. 编译设备树的实操流程4.1 官方推荐在源码目录中直接make dtbs进入内核源码目录之前先确保环境脚本已source。然后执行cd arch/arm/boot/dts make stm32mp157d-dk1.dtb如果你是在内核源码根目录下执行同样可以make ARCHarm stm32mp157d-dk1.dtbmake的时候内核的顶层Makefile会根据ARCHarm选择arm架构编译器则使用环境变量里的CC。为什么单独编译一个dtb不会触发整棵内核的构建因为dtb文件被Makefile标记为一个独立的、仅依赖dts源文件和头文件的target。但它会依赖一些dtsi文件里引用的宏定义比如dt-bindings下的头文件所以一旦那些头文件缺失编译会直接报错。我第一次编的时候没有source环境脚本结果报了一堆“请先设置CROSS_COMPILE”一类的错误。养成习惯终端里先执行source /opt/st/your-path/environment-setup-cortexa7t2hf-neon-vfpv4-openstlinux_weston-linux-gnueabi然后再编译。4.2 手动使用dtc编译的备选方案除了用内核的Makefile还有一种更轻量的方案是直接用dtc工具。dtc是设备树编译器通常在你的Linux系统里已经安装或者在SDK的sysroots目录里也存在一个交叉编译版。手动编译的样例dtc -I dts -O dtb -o stm32mp157d-dk1.dtb stm32mp157d-dk1.dts -不过我不推荐作为主要流程。原因很简单board级dts里大量使用#include和宏直接输入给dtc的时候C预处理器必须先处理一遍。你需要先执行cpp -nostdinc -I arch/arm/boot/dts -I include -undef -D__DTS__ -x assembler-with-cpp stm32mp157d-dk1.dts | dtc -I dts -O dtb -o stm32mp157d-dk1.dtb这一长串命令看起来就很麻烦而且不同内核版本对预处理参数的需求还不完全一样。所以我最终建议还是用make机制来做把复杂路径都交给内核构建系统。4.3 编译产物验证反编译检查编译成功后会生成stm32mp157d-dk1.dtb在你编译的当前目录下也就是arch/arm/boot/dts/里面。怎么确认这个dtb是对的我用dtc反编译dtc -I dtb -O dts -o output.dts stm32mp157d-dk1.dtb然后打开output.dts检查你改的节点是否在status是否正确引脚号是否正确。比如你加了lm7548节点反编译输出里应该能看到i2c40013000 { lm7548 { compatible national,lm75; reg 0x48; }; };如果看到了说明修改进了dtb如果看不到多半是编译的还是旧源文件或者文件路径不对。反编译这一步非常建议养成习惯能省下大量上板验证的时间。4.4 拷贝dtb到开发板并正确引导STM32MP157D-DK1从SD卡启动时它使用的镜像布局比较复杂不像树莓派那样把boot文件直接扔FAT分区就行。SD卡上一般有三个分区其中一个文件系统里包含了bootfs相关文件。当你插入SD卡到Ubuntu下挂载会看到类似第一个分区EFI或FAT存放uImage、dtb、extlinux配置第二个分区根文件系统以OpenSTLinux默认镜像为例dtb通常位于bootfs分区下的/stm32mp157d-dk1.dtb。你只需要把编译好的dtb文件以同名覆盖这个文件。但覆盖之前把原文件复制一份留底cp /media/$USER/ST_bootfs/stm32mp157d-dk1.dtb /media/$USER/ST_bootfs/stm32mp157d-dk1.dtb.bak cp arch/arm/boot/dts/stm32mp157d-dk1.dtb /media/$USER/ST_bootfs/stm32mp157d-dk1.dtb sync另外U-Boot启动时会读取bootfs分区里的extlinux/extlinux.conf配置文件里面会写LABEL OpenSTLinux KERNEL /stm32mp157d-dk1.bin FDT /stm32mp157d-dk1.dtbFDT字段指定的路径必须和实际文件路径一致。如果你把dtb改成了自定义名字比如myboard.dtb那么必须同步修改extlinux.conf里的FDT字段否则U-Boot会找回默认的stm32mp157d-dk1.dtb你的修改完全不生效。这类“改了没反应”的问题十有八九是U-Boot加载了别的dtb。5. 常见问题与排查技巧实录5.1 编译时报头文件或宏找不到典型错误是类似fatal error: dt-bindings/gpio/gpio.h: No such file or directory这说明C预处理器没有正确运行。原因通常是你在源码目录里make的时候没有指定ARCH或者直接手动跑了dtc而没加预处理参数。解决方式回到内核源码根目录确保环境脚本已source然后用make命令而不是dtc。还有一个隐蔽坑如果你在别处拷贝了一个单独的dts文件放在/tmp下直接编译找不到头文件是必然的。因为dts引用的宏定义在include/和arch/arm/boot/dts/下面这个问题目前没有别的优雅解法只能把文件放回源码树中编译或者用4.2节里的完整cpp命令。5.2 启动时设备树没有生效这个排查思路比较固定。首先在U-Boot启动日志里按任意键打断自动启动进U-Boot命令行执行printenv看fdtfile这个环境变量是不是stm32mp157d-dk1.dtb。如果fdtfile指向别的dtb说明你覆盖错文件了。还有一种情况OpenSTLinux有些镜像用的是fitImage格式相当于把内核和多个dtb打包进一个itb文件里。这种格式下U-Boot会从itb里选一个dtb加载你单独替换FAT分区里的dtb是无效的。具体看启动日志里出现的是“Loading kernel from FIT Image”还是分别加载kernel和dtb。如果遇到fitImage设备树的修改就不能靠覆盖文件来搞需要重新生成fitImage或者修改U-Boot配置让它直接用外部FDT。排查设备树是否加载成功的另一个常用路数是进入Linux系统后查看cat /proc/device-tree/model如果输出的是修改后的型号字符串说明dtb确实加载了。还可以直接对比节点比如ls /proc/device-tree/ ls /sys/firmware/devicetree/base/i2c40013000/如果设备树里有lm7548那么/sys/firmware/devicetree/base/lm7548应该存在或者通过i2cdetect检测到地址。没有看到节点就说明dtb没换成新的。5.3 如何快速查看和对比设备树内容命令行下最常用的三个查看手段cat /proc/device-tree/直接读取设备树运行时展开后的信息和原始dtb会略有差异因为内核会对它做一些dynamic修改。dtc -I fs -O dts’把运行时的/proc/device-tree导出成dts格式方便对比。fdtget直接读取dtb中的指定节点属性适合脚本里快速验证。网上也有一些图形化的设备树查看工具比如一些Windows平台下的USB设备树查看器或者第三方“device tree viewer”软件可以像浏览文件树一样看节点层级。这类工具在排查复杂节点关系时很直观但实际工程中我发现命令行的效率反而更高尤其是配合grep和diff。建议你可以把“原始dts、反编译后的dts、运行时设备树”三者做一次diff很多不一致问题一眼就能定位。具体操作是先保存三份文件dtc -I dts -O dts -o pre.dts arch/arm/boot/dts/stm32mp157d-dk1.dts dtc -I dtb -O dts -o final.dts arch/arm/boot/dts/stm32mp157d-dk1.dtb dtc -I fs -O dts -o runtime.dts /proc/device-tree diff pre.dts final.dts diff final.dts runtime.dts如果pre和final一致说明编译没问题如果final和runtime差异很大说明内核启动后的动态修改或者U-Boot覆盖导致两者不一致。5.4 踩过的一个引脚下发坑我在做GPIO控制时曾把某个按键接到了PG10设备树里写gpios gpiog 10 GPIO_ACTIVE_LOW结果内核上报的中断号完全不对。排查了很久最后用gpiodetect发现PG10在驱动里被复用成了某个以太网pinctrl功能。问题根源是板级dts的pinctrl配置和GPIO配置冲突。正确处理方式是如果某个引脚要被GPIO驱动操控需要确保pinctrl设置里没有其他外设节点引用该引脚。在dts里可以查看对应pinctrl节点或者用以下命令查看当前引脚复用状态cat /sys/kernel/debug/pinctrl/pinctrl-hogs/pins这个文件会列出所有pinctrl占用的引脚。如果发现冲突返回dts里把对应外设节点删掉。这类问题不是语法错误编译完全正常但行为不对是最容易让人抓狂的一类。6. 版本匹配与升级注意事项6.1 Developer Package版本和内核源码强相关Device tree的编译结果本质上依赖内核源码的dtsi定义和头文件所以版本必须严格匹配。比如你下载的是5.15内核的Developer Package就不要拿5.10内核源码里的dts来编译那样生成的dtb可能包含不兼容的节点属性内核启动时看到看不懂的属性会选择忽略但有时也会直接卡在初始化阶段。更值得注意的一个细节是不同版本里某些外设节点的compatible名会变比如老的I2C节点地址和现在不同。因此我在工程中坚持一个原则修改设备树永远在对应版本的源码树里改不要跨版本拷贝dts内容。哪怕是网上找到的很相似的修改也要对照当前源码确认节点名和属性名。6.2 保留一份原始dtb的重要性前面提到拷贝dtb前要备份原文件这里强调一下原因STM32MP157D-DK1的默认dtb厂商在出厂镜像里调教得比较稳定如果你改乱了随时能通过备份恢复。我每次改设备树前都会把原始dtb复制到本地保存命名带上日期例如stm32mp157d-dk1.dtb.bak-20240512。这个习惯帮我在好几个项目里快速回退不用重新刷写整个SD卡镜像。6.3 关于M4协处理器相关的设备树STM32MP157是异构双核Cortex-A7和Cortex-M4。如果你在DK1上同时运行M4核程序需要关注是否有独立的dtb用于M4固件。在YOCTO生态里M4相关资源通常会通过resource table远程加载设备树上也会出现reserved-memory节点。我提醒这个是因为如果你只是修改了A7侧设备树但M4侧程序仍占用某个DMA或内存区域可能会导致A7侧外设初始化失败。出现这种情况时控制台日志通常会有“failed to reserve memory”之类的信息。查看源码里stm32mp157d-dk1-m4.dts确认内存预留区域是否和你的改动冲突。7. 实战小结与个人建议我自己的体会是设备树编译本身并不复杂真正的门槛在于理解这套“源文件—预处理—编译—加载—运行时展开”的完整链路。很多新手把精力花在背命令上结果遇到“设备树没生效”还是无从下手。与其背命令不如先搞懂U-Boot加载流程搞清楚它到底从哪个路径加载哪个FDT文件这一步打通后后面所有问题都会顺很多。最后再分享一个小技巧。每次改完dts上板验证时别急着测业务先用dmesg | grep -i of|fdt|machine扫一遍启动日志确认内核识别到的machine型号和compatible字符串是否符合预期。如果终端里出现“Machine model: STMicroelectronics STM32MP157D-DK1 Discovery Board”说明设备树已经成功被解析可以继续往下调。这个动作我每次都会做也算是一个低成本高回报的确认方式。
