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)在启动时是分开加载的。Bootloader(U-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++、make
- bison、flex
- libncurses-dev
- device-tree-compiler(这个用来做设备树反编译和校验)
在Ubuntu下执行:
sudo apt update sudo apt install gcc g++ make bison flex libncurses-dev device-tree-compiler2.3 获取并解压Developer Package
ST官方通过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.dts
- stm32mp157d-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; }; memory@c0000000 { device_type = "memory"; reg = <0xc0000000 0x20000000>; }; };这里有必要解释几个关键概念。model和compatible是内核用来识别硬件平台的关键字段,compatible会与内核里machine_desc的匹配字符串比对,决定哪一套初始化流程被使用。aliases里的serial0定义串口的别名,serial0通常对应ttySTM0,ST的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"; lm75@48 { 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 ARCH=arm stm32mp157d-dk1.dtbmake的时候,内核的顶层Makefile会根据ARCH=arm选择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是否正确,引脚号是否正确。比如你加了lm75@48节点,反编译输出里应该能看到:
i2c@40013000 { lm75@48 { 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/i2c@40013000/如果设备树里有lm75@48,那么/sys/firmware/devicetree/base/lm75@48应该存在,或者通过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”,说明设备树已经成功被解析,可以继续往下调。这个动作我每次都会做,也算是一个低成本高回报的确认方式。