拿到一块STM32MP157D-DK1,烧了官方镜像,系统能跑,但真到给板子加外设时,设备树这关绕不开。我最初的想法是偷懒,直接在运行中的系统里改/boot下的dtb,结果发现开发板用的SD卡根本没有传统的/boot目录,所有文件都放在FAT分区里,没有任何“现成设备树编辑器”可用。上网一搜,大部分教程都在让你走Yocto、bitbake,光下载源码就能等半个下午。其实ST官方提供了Developer Package,就是专门干这个的:在PC本地编译U-Boot、内核和设备树,再把产物拷到SD卡。这篇文章就是我基于STM32MP157D-DK1,用Developer Package从零编译设备树的完整记录,包括为什么需要它、环境怎么搭、命令怎么敲、怎么验证,以及我实际踩过的几个坑。
1. 双包机制下编译设备树的真实场景:为什么不用bitbake
1.1 开发者包与发行包的分工
接触STM32MP1系列一段时间后,会发现官方资料里反复出现两个名字:Distribution Package和Developer Package。前者是完整的Yocto发行版源码,下载下来好几个GB,第一次构建镜像至少两小时起步,好处是所有软件包都能从源码定制;后者则面向只改BSP的场景,里面包含一个成熟的交叉编译SDK、内核源码、U-Boot源码,以及一堆预设好的配置文件。
很多新手会误以为“改设备树必须用发行包”,其实不是。设备树编译只依赖内核源码、交叉编译工具链和内核的构建脚本,完全不需要Yocto那一整套环境。Developer Package的精妙之处在于,它把SDK单独抽出来,让你像平时编译普通Linux内核一样,直接make一个.dtb文件。所以如果你的需求只是“把某个外设打开”“调整引脚复用”“加一个设备节点”,根本不需要陷入bitbake的依赖地狱。
但也要说清楚:Developer Package不是万能钥匙。它更适合做BSP级别的增量修改,比如设备树、内核驱动模块、U-Boot配置。如果你要裁剪rootfs、添加系统服务、定制整个镜像,那还是回到Distribution Package更靠谱。设备树编译这件事,恰恰是Developer Package的主场。
1.2 设备树在STM32MP157上管哪些事
STM32MP157D-DK1虽然是块评估板,但硬件布线和资源占用并不简单。SoC内部有多个Cortex-A7核心、大量外设控制器,系统起来后哪个引脚给I2C、哪个引脚给UART、哪个节点被禁用,全都由设备树描述。在Linux下,这不是一个配置文件的问题,而是一组.dts和.dtsi源文件的问题。
设备树主要描述四类信息:SoC级IP信息(寄存器基地址、中断号、时钟)、板级硬件差异(外设是否启用、GPIO扩展器地址)、引脚复用状态(pinmux)、以及设备特定属性(比如SPI设备的mode)。在STM32MP157上,最常见的修改就是第三种:把某个引脚从默认的UART功能改成I2C,或者把一个本来disabled的外设通过status = "okay"打开。
还有一个容易忽略的点:STM32MP157D-DK1的设备树里,led和button是官方默认启用的。如果你要把这些GPIO用来控制其他硬件,就需要手动把对应的GPIO节点改掉或禁用,否则两个驱动会争抢同一组引脚,启动日志里会出现一串pin already requested警告。
1.3 什么时候你必须走这条手动流程
我在实际项目里总结出了三条“必须手动编译设备树”的路标。
第一,你需要在官方硬件上外接一颗传感器、或启用一个默认没打开的接口,比如在DK1上点I2C4接一个触摸屏。官方镜像里的设备树已经把引脚分给其他功能了,不改设备树基本没法用。
第二,你用自己的KiCad或者设计工具画了底板,硬件布局和DK1不完全一致。哪怕只差一个GPIO的上下拉,你也得自己维护一套板级设备树。
第三,你在做产品原型,需要在同一份内核源码下同时管理DK1、DK2或者自己的板卡,用不同dtb文件区分硬件版本。这个时候,手动编译流程每天要跑很多次,纯靠Yocto显然低效。
如果你属于这三种情况之一,下面这套操作就是给你准备的。
2. 环境准备:SDK安装、交叉工具链与容器化方案
2.1 下载对应生态版本的Developer Package SDK
从ST官网进入STM32MP1系列嵌入式软件页面,下载Developer Package时,会看到一个巨大的.sh文件,通常叫en.SDK-x86_64-stm32mp1-openstlinux-5.10-xxx-linux-gnueabihf.sh。这个文件就是SDK安装脚本。
我建议在干净目录下安装:
mkdir -p /opt/stm32mp1-sdk sudo sh ./en.SDK-x86_64-stm32mp1-openstlinux-5.10-*.sh -d /opt/stm32mp1-sdk安装完成后,目录里会出现一个以environment-setup-开头的文件,比如environment-setup-cortexa7t2hf-neon-vfpv4-openstlinux_weston-linux-gnueabihf。这个文件就是整个SDK的“总开关”,后面所有编译工作都要先source它。
有一点必须留意:版本要跟你目标镜像的内核版本对应。如果你用OpenSTLinux 4.1生态,内核是5.10;换了其他生态版本,SDK的内核头文件和工具链也会变。版本混用最典型的症状是设备树编译能过,但板子启动后外设行为诡异,因为生成的dtb格式或宏定义可能不匹配。
2.2 source SDK环境后,确认哪些变量变了
每次打开新终端,第一步都是:
source /opt/stm32mp1-sdk/environment-setup-cortexa7t2hf-neon-vfpv4-openstlinux_weston-linux-gnueabihf执行完后,CC会变成arm-openstlinux_weston-linux-gnueabihf-gcc,PATH里也会加入SDK的交叉工具链目录。可以用which arm-openstlinux_weston-linux-gnueabihf-gcc验证,能看到实际路径就是成功。
不过我遇到过几次情况:这个环境脚本不一定帮你设好ARCH和CROSS_COMPILE。编译内核设备树时,我习惯手动再显式指定一次,避免Makefile走到宿主机的x86默认配置上去:
export ARCH=arm export CROSS_COMPILE=arm-openstlinux_weston-linux-gnueabihf-这两个变量是整个编译流程的地基。ARCH=arm告诉内核构建系统你编的是32位ARM平台,CROSS_COMPILE指定交叉编译器的前缀。缺了哪一个,都会出现奇怪错误,比如生成的.dtb里混进x86架构的头文件。
2.3 容器化编译方案,懒人做法
如果你不想在主力开发机上装一堆SDK,Docker是个不错的选择。我自己就维护了一个编译用的Ubuntu 20.04容器,把SDK安装脚本映射进去,容器内完成后退出,宿主环境保持干净。
命令大致是这样:
docker run -it --rm -v /opt/stm32mp1-sdk:/sdk -v ~/work:/work ubuntu:20.04 bash进容器后,装好基础运行库,然后:
apt update && apt install -y libncurses5-dev u-boot-tools bash /sdk/en.SDK-x86_64-stm32mp1-openstlinux-*.sh -d /opt/sdk-install后面所有编译步骤和宿主机完全一样。容器方案最大优势是方便CI,把编译命令写进Dockerfile,团队其他成员拉下来就能复现。但我个人的建议是:如果你只是短期折腾,直接装在Ubuntu宿主机上反而更省心,因为SDK本身就是独立sysroot,很少污染系统环境。
2.4 工具链与内核版本的匹配问题
STM32MP1系列使用Cortex-A7核心,默认工具链是32位ARM版本,所以编译器前缀里有arm而不是aarch64。这点经常有刚从ARM64平台转过来的朋友踩坑,拿着aarch64-none-linux-gnu-gcc去编,设备树虽然能编出来,但某些内联汇编和头文件路径会不匹配,导致启动时内核无法解析设备树。
更稳妥的做法是:只用Developer Package自带的工具链。这个工具链经过ST官方验证,与内核源码中的stm32mp157*.dts所依赖的头文件是一致的。用其他工具链不是不行,但没必要在环境这种基础环节上给自己增加变量。
还有一种情况是系统里同时装了很多交叉编译器,CROSS_COMPILE指定为arm-none-linux-gnueabihf-也可能会导致头文件路径不同。所以每次编译前,我用echo $CROSS_COMPILE确认一下,已经变成了一种肌肉记忆。
3. 定位并修改DK1板级设备树:源码树中的关键文件
3.1 拉取内核源码并checkout到正确分支
环境就绪后,需要拿到内核源码。两种途径:一是从Developer Package页面下载linux-*.tar.xz源码包,二是从ST的GitHub镜像拉取linux仓库,切到对应的v5.10-stm32mp-r1这类分支。
如果从GitHub拉取,建议加--depth 1 --branch限制历史,不然仓库很大:
git clone --depth 1 --branch v5.10-stm32mp-r1 https://github.com/STMicroelectronics/linux.git kernel-source如果你下载的是tar.xz包,直接解压到工作目录即可。接下来建议先看一眼源码树根目录,确认里面确实有arch/arm/boot/dts/stm32mp157d-dk1.dts。如果该文件不存在,检查一下你拉的分支是不是面向STM32MP157家族的。
3.2 stm32mp157d-dk1.dts的继承关系与include图
在arch/arm/boot/dts目录下,stm32mp157d-dk1.dts这个文件本身通常很小,也就几十行。它做的事情主要是#include一堆公共文件,再定义model和compatible属性。打开它,你会看到类似这样的结构:
/dts-v1/; #include "stm32mp157.dtsi" #include "stm32mp157d.dtsi" #include "stm32mp15xx-dkx.dtsi" ...真正管板级外设的配置,大部分在stm32mp15xx-dkx.dtsi这个公共板级文件里。DK1和DK2的不少外设配置是共享的,所以当你需要改I2C、SPI、UART、GPIO时,第一反应不应该是打开stm32mp157d-dk1.dts,而是先去公共文件里搜索对应节点。
另一个常用操作是搜索别名。比如想找i2c4,直接:
grep -n "i2c4" arch/arm/boot/dts/stm32mp15xx-dkx.dtsi这个命令能帮你快速看到节点是在哪个层级定义的、当前status是什么、引用了哪个pinctrl。我调试外设时,超过一半的时间都在做这类搜索。
3.3 用STM32CubeMX的.ioc输出直接覆盖
ST官方推荐的硬件配置方式是STM32CubeMX。你在图形界面里勾选引脚、配置外设时钟,然后生成设备树源文件,生成的位置一般在项目的/Src目录下,文件名和板卡相关。
但这里有个大坑:CubeMX生成的设备树文件,是针对你在图形界面中选的板卡配置的,它不一定完整包含内核源码中所有的SoC级节点。如果你直接把整个stm32mp157d-dk1.dts覆盖到内核里,大概率会丢掉一些默认必需的内容,比如clocks、soc的某些子节点。
我的经验是:只用CubeMX生成的文件作为“引脚复用参考”。它生成的pinctrl片段通常很干净,格式直接对应内核的stm32-pinfunc.h宏。我会把需要的i2c4_pins_b之类的节点片段复制到内核源码的板级pinctrl文件中,而不是整篇替换。
3.4 手工改pinmux的一个简单例子
假设我要在DK1上启用I2C4,并且把引脚改到PD12和PD13。首先需要在pinctrl头文件里确认这两个脚的复用功能编号,比如:
i2c4_pins_b: i2c4-1 { pins { pinmux = <STM32_PINMUX('D', 12, AF4)>, <STM32_PINMUX('D', 13, AF4)>; bias-disable; drive-open-drain; slew-rate = <0>; }; };然后在板级文件里,找到&i2c4节点并覆盖:
&i2c4 { pinctrl-names = "default"; pinctrl-0 = <&i2c4_pins_b>; status = "okay"; };这类修改看起来简单,但很容易因为拼写错误导致编译出来的dtb不包含某个节点。建议修改后先编译、再反编译,确认节点确实以status = "okay"形式存在,而不是靠肉眼猜。
4. 编译设备树的完整命令与输出物解析
4.1 先配置内核:multi_v7_defconfig和stm32_defconfig怎么选
进入内核源码目录,先执行:
source /opt/stm32mp1-sdk/environment-setup-cortexa7t2hf-neon-vfpv4-openstlinux_weston-linux-gnueabihf export ARCH=arm export CROSS_COMPILE=arm-openstlinux_weston-linux-gnueabihf- make multi_v7_defconfig这一步是生成内核构建基础目录结构,比如include/generated等头文件。很多人会问:我只是编设备树,为什么要配置内核?因为设备树编译依赖Kconfig生成的autoconf.h和一些编译规则,没有.config,构建系统不知道当前架构和设备树使能情况。
multi_v7_defconfig是ST官方镜像使用的配置,覆盖大量ARMv7平台设备,编译出来的dtb也最多;stm32_defconfig是针对STM32MP1的精简配置,启动更干净。对于DK1开发,我推荐先用multi_v7_defconfig,因为和官方镜像内核行为最接近,排查问题时参考资料最多。
4.2 单独编译dtb的命令与作用机制
配置完成后的核心命令非常简单:
make stm32mp157d-dk1.dtb这条命令会让内核构建系统进入arch/arm/boot/dts/目录,找到stm32mp157d-dk1.dts,先通过C预处理器处理#include,再调用内核自带的dtc编译成二进制.dtb文件,最后输出到arch/arm/boot/dts/stm32mp157d-dk1.dtb。
如果报错找不到generated/utsrelease.h之类文件,说明上一步的defconfig或prepare没有跑完整,可以执行:
make ARCH=arm prepare然后再编译。正常情况下,整个编译过程不会超过一分钟,比整个镜像的构建时间快了几个量级。
如果不想把编译产物混在源码目录里,也可以指定外部构建目录:
make O=$PWD/build ARCH=arm multi_v7_defconfig make O=$PWD/build ARCH=arm stm32mp157d-dk1.dtb输出文件会在build/arch/arm/boot/dts/下。推荐在需要同时管理多块板卡时用这种方式,能保留多个构建目录。
4.3 内核scripts和dtc的依赖
设备树编译过程中,dtc编译器是关键工具。内核源码里自带一份dtc源码,构建系统会自动编译scripts/dtc/dtc来使用。
所以如果你在源码树里执行make stm32mp157d-dk1.dtb,第一次运行时它会顺带编译dtc,你不用担心系统有没有安装。但如果你的PATH里刚好有另一个旧版dtc,比如某些系统包自带的,内核构建脚本可能会优先找到它并提示版本太低。解决方法是确保sourceSDK环境后,PATH最前面是SDK的sysroots目录,也可以执行which dtc确认路径。
还有一个小细节:设备树源文件用到了很多#define宏,这些宏头文件放在内核源码的include/dt-bindings/下。如果你手动调用dtc而不是通过内核Makefile,经常会因为宏被展开导致语法错误。所以我始终建议,只要不是在做实验,就老老实实用make来编。
4.4 另一种方式:直接调用dtc编译
如果你只是临时做一个语法验证,不想搭完整内核配置,确实可以手动拼接cpp和dtc:
cpp -nostdinc -I arch/arm/boot/dts -I include -I include/uapi -undef -D__DTS__ -x assembler-with-cpp arch/arm/boot/dts/stm32mp157d-dk1.dts | dtc -I dts -O dtb -o /tmp/test.dtb -这个命令的本质是把设备树源文件先做C语言预处理器展开,再交给dtc。问题在于,手动拼的include路径稍微漏一个,宏就展开不出来,报错信息又非常不直观。我踩过一次后就没再用它做正经事,只用来快速验证某个.dtsi语法是否正确。
5. 部署到DK1的SD卡并验证加载结果
5.1 理解DK1启动流程中dtb的加载位置
STM32MP157D-DK1从SD卡启动时,BootROM会执行FSBL,通常是TF-A或U-Boot SPL,然后由U-Boot读取第二分区中的设备树,再启动Linux内核。这就是为什么你改完.dtb后要放到SD卡的FAT分区,而不是rootfs分区。
把SD卡插入读卡器,用lsblk查看设备节点。常见布局是/dev/sdb1为FAT32的bootfs分区,/dev/sdb2为rootfs。挂载bootfs:
mkdir -p /mnt/bootfs sudo mount /dev/sdb1 /mnt/bootfs查看目录,你会看到uImage、stm32mp157d-dk1.dtb、*.scr之类的文件。不同官方镜像版本文件名略有差异,但原则一致。
5.2 替换bootfs中的dtb并保证文件权限
替换之前,我强烈建议先备份原版:
sudo cp /mnt/bootfs/stm32mp157d-dk1.dtb /mnt/bootfs/stm32mp157d-dk1.dtb.bak然后把编译产物拷贝过去:
sudo cp arch/arm/boot/dts/stm32mp157d-dk1.dtb /mnt/bootfs/ sync sudo umount /mnt/bootfs这里有个经验:FAT分区虽然不感知Unix权限,但拷贝完后sync非常重要,不然直接拔卡很容易导致文件系统不一致。我遇到过两次卡插回板子上启动后,U-Boot报FAT read error,重新插到电脑上执行fsck.vfat才修好,都是因为没等写盘完成。
拷贝完成后可以在宿主机上md5sum校验一下源文件和目标文件,防止Windows系统或者读卡器造成内容错乱。如果你是在Windows下拷贝,一定要选“快速删除”策略,避免写入缓存丢失。
5.3 系统起来后确认设备树是否加载了你的修改
板子启动后,进入Linux终端。设备树被内核解析后,会以“扁平设备树”形式展现在/sys/firmware/devicetree/base目录下。
如果你改的是I2C4节点,可以这样检查:
ls /sys/firmware/devicetree/base/soc/i2c@40012000/或者用属性名来确认status:
cat /sys/firmware/devicetree/base/soc/i2c@40012000/status更直观的方式是把运行时设备树反编译出来:
dtc -I fs -O dts /sys/firmware/devicetree/base -o /tmp/current.dts然后打开/tmp/current.dts搜索你修改的节点,看status、pinctrl-0和pinmux是否都在。这个方法还有个好处:它能看出系统实际使用的引脚参数是否被内核进一步修正过,避免你改了半天但节点没生效。
5.4 启动失败时的原始备件恢复
如果新dtb有问题,最常见的现象是U-Boot启动后立刻重启,或者内核起来后某个外设疯狂打印错误。这时把SD卡插回电脑,挂载bootfs,直接把刚才的.bak复制回去即可:
sudo cp /mnt/bootfs/stm32mp157d-dk1.dtb.bak /mnt/bootfs/stm32mp157d-dk1.dtb sync我甚至建议准备第二张SD卡,烧录官方出厂镜像作为“复活卡”,专门用于对比验证。因为有些问题是设备树本身没坏,但和当前U-Boot版本不匹配,导致U-Boot解析dtb直接失败。这时候手边有一张已知正常的SD卡,能快速排除硬件或启动链路故障。
6. 设备树编译踩坑Top5:从准备到落地最容易翻车的地方
6.1 没先跑prepare指令导致dtc报头文件缺失
第一次编译时最常见的错是类似:
fatal error: generated/utsrelease.h: No such file or directory主要原因是没有先执行make multi_v7_defconfig,或者执行后.config被后续操作覆盖掉了。内核Makefile里,很多生成头文件依赖prepare阶段,如果跳过,设备树编译虽然调用的是dtc,但预处理器还是会找include/generated/下的文件。
解决办法也很简单,回到源码根目录,执行:
make ARCH=arm multi_v7_defconfig make ARCH=arm prepare再重新编dtb。不要自己手动创建一个空generated/utsrelease.h,这样虽然能骗过预处理器,但版本信息缺失,后续排查问题会绕弯路。
6.2 选错内核defconfig导致目标dtb根本没有被编入
还有一次我在一个精简的stm32_defconfig下执行make stm32mp157d-dk1.dtb,结果报“No rule to make target”。原因是这个配置没有使能某些设备树目标对应的Kconfig项,导致arch/arm/boot/dts/Makefile里对应的dtb-$(CONFIG_XXX)没有值,构建系统自然找不到规则。
所以遇到“No rule”时,第一反应不是去硬改Makefile,而是回看.config里相关配置项。最省事的方法是用multi_v7_defconfig,ST官方已经把STM32MP157家族的所有板级设备树都纳入了编译列表。你也可以用:
grep -n "stm32mp157d-dk1" arch/arm/boot/dts/Makefile确认目标确实由哪个Kconfig控制。
6.3 CubeMX生成文件与当前内核版本不匹配
使用STM32CubeMX的人大概都有过这种体验:图形界面里配置挺顺利,生成出来一大摞文件,但拷到内核源码里一编译,各种宏找不到。原因通常是CubeMX自动生成时使用的内核版本和当前源码树不一致。
STM32MP1系列不同版本头文件里的宏名会变化。比如老版本使用STM32_PIN,新版本改成STM32_PINMUX;有些时钟节点属性也会从clocks改为clock-names。最简单的规避方式是把官方内核自带的dts作为基准,只从CubeMX生成文件里“抄”你新增的引脚组定义,而不是整体覆盖。
如果必须使用CubeMX整套输出,建议先做一次make stm32mp157d-dk1.dtb验证,确认它和当前内核能映射上。否则你后续所有修改都可能建立在一个虚浮的基础上,启动时很难定位错误。
6.4 同名dtsi覆盖顺序导致修改“不见”
设备树的#include本质上是文本级别的展开,多个文件里如果定义同一个节点,后面的定义会覆盖前面同名的属性。SoC级文件里可能已经给&i2c4设置了pinctrl-0 = <&i2c4_pins_a>,而你在板级文件里又设置了pinctrl-0 = <&i2c4_pins_b>,如果板级文件在include顺序中靠前,事后再被SoC级文件覆盖,你的修改就会静默丢失。
排查这类问题最有效的方法是直接看预处理后的完整设备树源文件。可以手动执行:
make ARCH=arm stm32mp157d-dk1.dt.bin或者用dtc -O dts反编译生成的dtb,再搜索你期望的引脚节点。如果你发现编译出来的dtb里仍然是i2c4_pins_a,那就说明在某个层级上有同名覆盖。建议把自定义修改直接放到顶层stm32mp157d-dk1.dts中,那里优先级最高,不会被其他板级文件覆盖。
6.5 板卡版本RevB/RevD差异导致的引脚冲突
STM32MP157D-DK1本身有硬件版本迭代,我在调试时发现,官方内核里有些引脚配置其实是按新版板卡来的,比如LED2、按键、部分扩展GPIO的默认状态。如果你手里的板子丝印是旧版,直接烧官方dtb可能表现为某个按键失灵、或者某个外设上电状态不对。
处理方式是拿到设备树源文件后,先查一下当前板卡型号和版本。可以根据板卡丝印上的编号,在stm32mp15xx-dkx.dtsi里搜索对应的GPIO定义,和原理图对比。大多数情况下,新旧版本差异并不大,但一旦遇到差异,调试成本很高,因为问题看起来像驱动不工作,其实是引脚资源冲突。
我现在的习惯是拿到任何一块STM32MP1板子后,第一时间把原厂dtb反编译并存档,同时在源码仓库里建立一个board-rev分支,专门记录不同硬件版本的差异。这样后续硬件改版时,设备树管理不会变成一团浆糊。
设备树编译这件事,看起来就是两条make命令,但真正决定你能不能顺利跑起来的,还是对平台启动流程和源码结构的理解。我现在改DK1设备树的标准流程是:先用CubeMX生成pinctrl片段当参考,再在源码树里搜索内核已有的相似pin group,复制过来做微调;编译只用multi_v7_defconfig加make <board>.dtb;部署前永远备份原版dtb,启动后用/sys/firmware/devicetree/base确认节点状态。这套流程跑顺之后,改一个引脚从修改到验证基本控制在几分钟内。希望这篇记录能让你少走我走过的弯路。