1. 从一次“连不上的PANIC”说起
我盯着串口终端里的日志,最后一行是Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(179,10)。那一刻我反倒松了口气——至少说明U-Boot已经跑起来了,问题出在后面。然后我习惯性打开STM32CubeProgrammer,想用ST-LINK重新烧一版镜像进eMMC,结果点了Connect之后,软件卡在连接界面,过了几秒直接报错:“Error: No STM32 target found”。再试几次,还是一样。
这个场景太熟悉了。很多折腾STM32MP1系列的朋友估计都经历过:板子从SD卡启动一切正常,一旦切到eMMC启动,要么PANIC,要么直接“变砖”,最难受的是CubeProgrammer也连不上,连救回来的手段都没有。我这次用的是STM32MP135DAF这颗单核Cortex-A7芯片,配合自己基于Yocto定制的machine,整个过程从“看起来很简单”到“差点以为板子报废”,最终花了差不多一个晚上才彻底摸清问题链路。这篇就把整个排查过程、根因、恢复步骤和Yocto层面的根治方案都写清楚,给正在踩同款坑的人一个可以直接抄作业的参考。
先说结论,省得你看一半着急:eMMC启动PANIC大概率是分区表、设备树或内核cmdline三者之一出了问题;而CubeProgrammer连不上,绝大多数时候不是调试器坏了,而是芯片在eMMC启动模式下被“拉”进了一个CubeProgrammer难以稳定复位连接的状态。两个问题叠在一起,才造成了“变砖”的假象。
2. eMMC启动PANIC的根因链路:从ROM到rootfs逐个排查
要搞清楚为什么PANIC,不能只看最后那一行。STM32MP135的启动链是一层套一层的:片上ROM先根据BOOT引脚选择启动源,然后加载FSBL(也就是TF-A BL2或者U-Boot SPL),FSBL再加载FIP镜像(里面包含BL31和U-Boot proper),U-Boot再去bootfs里读设备树和内核,最后内核挂载rootfs。任何一环出错,表现都是“启动失败”,但日志停的位置完全不同。
2.1 串口日志每一行对应的“排错地图”
我习惯先把U-Boot完整日志拉下来,然后一行一行对照,判断是卡在哪一环。下面这个表格是我这次排查时的快速定位方式:
| 日志关键特征 | 所在阶段 | 常见根因 |
|---|---|---|
完全没有输出,或者卡在DDR:之前 | ROM / FSBL初期 | eMMC boot分区里没有有效FSBL,或BOOT引脚配置不对 |
DDR: 512 MiB之后无进展 | FSBL到FIP之间 | FIP镜像未放在eMMC boot分区,或FIP里的DTB与硬件不匹配 |
MMC: sdmmc1: 0, sdmmc2: 1 | U-Boot枚举MMC设备 | eMMC被识别为哪个设备号,直接决定后面root=参数怎么写 |
switch to partitions #0/mmc1 is current device | U-Boot访问eMMC | 分区表损坏或boot partition未切换 |
Failed to mount root fs/VFS: Unable to mount root fs | 内核挂载rootfs | root=设备节点写错,或rootfs分区类型/位置不对 |
Kernel panic - not syncing: No working FDT found | 内核启动早期 | DTB没加载,或FIT镜像里DTB路径不对 |
这次我的日志停在了VFS: Unable to mount root fs on unknown-block(179,10)。179,10这个设备号对应的其实是mmcblk0p10。也就是说,内核在试图挂载/dev/mmcblk0p10这个分区,但这个分区要么不存在,要么类型不对。
2.2 实际定位到的“元凶”
排查到最后,问题出在两个方面叠加。
第一个是U-Boot把eMMC枚举成了mmc1而不是mmc0。STM32MP135上通常sdmmc1接SD卡、sdmmc2接eMMC,但U-Boot的设备号并不一定按这个顺序来。它在启动时根据设备树别名(aliases)里的mmc0、mmc1定义来编号。我自定义的Yocto machine里,设备树没有把sdmmc2设为mmc0,结果eMMC变成了mmc1,SD卡是mmc0。而我在内核cmdline里写的却是root=/dev/mmcblk0p10,内核自然找不到这个设备。
第二个问题是eMMC本身的分区布局。ST的官方flashlayout工具烧录时,会把FSBL写进eMMC的boot partition 1,把bootfs、rootfs这些写进UDA(用户数据区)的指定偏移。我基于Yocto定制的machine时,为了省事直接改了官方WKS分区脚本,把rootfs从第6分区挪到了第10分区,但U-Boot的bootcmd里读uEnv.txt的路径还是老的mmc 1:4,内核cmdline也是旧的。这一套“不配套”组合下来,eMMC启动时连FSBL都勉强跑起来了,但到了内核挂载rootfs阶段,设备和分区完全对不上,PANIC就成了必然。
2.3 用U-Boot命令行快速验证,不用反复烧录
当时我为了确认到底是U-Boot阶段还是内核阶段的问题,做过一个很有效的操作:在U-Boot倒计时期间按任意键打断自动启动,然后手动执行几条命令。
# 查看U-Boot枚举到的MMC设备 mmc list # 查看当前eMMC设备是否有合法分区表 mmc dev 1 mmc part # 列出分区内容,确认bootfs里到底有什么 ls mmc 1:4 # 查看当前环境变量里的bootcmd和bootargs printenv bootcmd printenv bootargs env print这几条命令能在不重烧的情况下直接告诉你:eMMC到底被编号成几号、分区表在不在、bootfs里的文件名对不对、环境变量指向哪个分区。我当时就是靠mmc list发现sdmmc2被枚举成mmc1,然后又靠ls mmc 1:4发现bootfs其实在第4分区而不是我记忆里的位置,瞬间就确定了根因方向。这套“三板斧”比反复烧录试错高效得多。
3. CubeProgrammer为什么连不上:它比想象中更依赖“复位行为”
现在回到最让人崩溃的问题:为什么改完eMMC启动之后,STM32CubeProgrammer就再也连不上目标板了?
我最初怀疑是ST-LINK线坏了,换了一根线,不行;怀疑驱动掉了,重装CubeProgrammer,不行;怀疑板子电源不稳,用外部稳压源单独供电,还是不行。踩完这一串弯路之后我才意识到,问题压根不在调试器,而在“目标板当前的启动状态”和“CubeProgrammer默认的复位连接方式”之间的冲突。
3.1 三根BOOT引脚决定了CubeProgrammer能不能“看懂”芯片
STM32MP135上有专门配置启动源的BOOT引脚(具体编号组合要看板卡原理图,常见的是BOOT0/BOOT1/BOOT2拨码开关)。芯片每次复位或上电时,片上ROM都会采样这几根引脚的电平,决定从哪个设备加载第一段启动代码。
问题在于:如果你把BOOT拨码拨到了eMMC启动,那么CubeProgrammer通过ST-LINK连接时,默认会去复位目标芯片。复位释放的一瞬间,芯片立刻进入eMMC启动流程。我的eMMC里虽然FSBL能跑,但到了U-Boot阶段,板子已经初始化了DDR、MMC控制器、时钟树,甚至ETZPC(扩展TrustZone外围控制器)可能把调试接口相关的总线访问权限改成了secure-only。这种情况下,调试器再去扫描SWD内核,可能会发现CPU核心被“锁”在了一个无法正常halt的状态,或者干脆扫描不到任何core,于是报“No STM32 target found”。
更麻烦的是,如果U-Boot或内核里开启了独立看门狗(IWDG),而系统在FIP加载或内核启动阶段反复panic,看门狗会周期性复位芯片,导致CubeProgrammer每次尝试连接时都正好撞上芯片在复位-启动-PANIC-复位的循环里。从软件角度看,这就是一个永远抓不到稳定目标的状态。
| 连接时BOOT引脚状态 | 芯片复位后的行为 | CubeProgrammer大概率能看到什么 |
|---|---|---|
| 拨到SD卡启动 | 从SD卡正常加载,不会锁调试口 | 能看到核心,连接成功 |
| 拨到eMMC启动(系统正常) | 正常进入Linux,但调试权限可能受限 | 有时能连,有时只能看到core0 |
| 拨到eMMC启动(系统PANIC) | 反复启动/复位,可能触发看门狗 | 扫描不到稳定target,连接失败 |
| 拨到USB DFU/UART工程模式 | ROM代码停在那里等待DFU命令,不跑用户代码 | 一定能连接,通过USB或UART识别 |
这就是为什么“之前SD卡启动时能连,一切到eMMC启动就再也连不上”的现象如此普遍:不是板子烧了,是你的BOOT引脚一直锁定在eMMC模式,而eMMC里的代码一出问题,整个系统就被困在异常循环里,调试器连不进去。
3.2 CubeProgrammer的复位模式:一个关键参数解决大半问题
很多人不知道,STM32CubeProgrammer连接ST-LINK时,复位连接方式是可以选的。GUI界面的“Connect”按钮旁边通常有设置,命令行下对应的是mode=参数。
| 模式 | 说明 | 适用场景 |
|---|---|---|
mode=NORMAL | 默认,先复位目标板再连接 | 目标板当前运行状态正常时可用 |
mode=UNDER_RESET | 拉低NRST并保持,在复位期间抓住内核 | 目标板启动代码异常、反复跑飞时非常有效 |
mode=HOTPLUG | 不控制复位,直接扫描SWD | 目标板由外部电源独立供电,或复位线路异常时使用 |
我最开始一直用默认的NORMAL模式,怎么都连不上。后来换成UNDER_RESET模式,CubeProgrammer在芯片被按住复位的瞬间去扫描内核,这时候芯片还没有机会跳到eMMC的异常启动流程里,自然就能连上了。
命令行下的操作方式是这样的:
# 使用ST-LINK + SWD,在复位期间连接 STM32_Programmer_CLI -c port=SWD mode=UR # 如果芯片被锁死了,先做整片擦除,注意这会清掉eMMC/UDA所有数据 STM32_Programmer_CLI -c port=SWD mode=UR -e allmode=UR这个参数,建议所有折腾STM32MP1系列的人先记下来。它可能就是你从“变砖”到“救活”之间最短的一条路。
3.3 还有一个总被忽略的:调试接口是否被安全机制封锁
我在排查过程中还发现了另一种导致“连不上”的情况:U-Boot/FIP里如果启用了TrustZone相关的总线过滤配置(ETZPC、TZC400),而对应的外设(包括DBGMCU调试单元)被配置成secure访问权限,那么非安全世界的调试器工具扫描SWD时会一无所获。这种情况通常不会因为你换一个mode=UR就解决,因为它不是复位状态问题,而是访问权限问题。
这时候最干净的办法就是:把BOOT引脚切换到USB DFU或UART工程模式,让芯片根本不去执行eMMC里那段会锁总线的代码,然后通过USB DFU接口重新烧录。这个方法也是我最终的选择。
4. 完整抢救步骤:从“疑似变砖”恢复到可烧录状态
如果你现在板子症状和我一样——eMMC启动PANIC,CubeProgrammer连不上——不要慌,下面这套流程是我实测有效的恢复路径。前提是你手上还有原厂或自己构建的烧录镜像,以及一块能正常工作的ST-LINK,或者一条能进USB DFU模式的USB线。
4.1 第一步:把芯片从错误的启动循环中“摘”出来
这个步骤听起来简单,但最容易忽略。
先断开所有供电,包括ST-LINK的3V3供电、外部电源、USB线。然后把板子上的BOOT拨码开关拨到“工程模式”,也就是进USB DFU或UART下载模式。具体哪一组拨码对应哪个模式,务必查你手上的板卡用户手册或原理图,别凭记忆猜。我用的板子上是三位拨码,其中一组组合是USB DFU,另一组是UART,还有一组是eMMC。
拨好之后,先用USB线连接板子上标注为“USB DFU”或“OTG”的接口(不是ST-LINK那个USB口),再接供电。上电后,在设备管理器里观察是否出现一个STM32 BOOTLOADER设备。如果出现了,说明芯片已经停在了ROM的DFU等待状态,没有去执行eMMC里的代码,接下来就很好办了。
4.2 第二步:用CLI而不是GUI,连不上时命令行才给反馈
图形界面看起来直观,但出了问题它只会弹一个笼统的报错框。我建议在恢复阶段直接用命令行工具,反馈信息更详细,也更容易定位。
先试ST-LINK + SWD连接:
STM32_Programmer_CLI -c port=SWD mode=UR如果这一步成功了,你会看到目标芯片的IDR信息,比如STM32MP135的设备ID。如果还不行,转到USB DFU方式:
# 先确认系统识别到了STM32 BOOTLOADER设备 STM32_Programmer_CLI -l usb # 通过USB DFU连接 STM32_Programmer_CLI -c port=USB1USB DFU方式下,芯片是ROM状态,不依赖任何flash里的代码,所以几乎不会失败。这也是为什么我强烈建议:设计板卡时一定要把USB DFU的BOOT拨码引出来,否则遇到芯片锁死的情况,只能拆flash或者用ST-LINK硬扛。
4.3 第三步:用flashlayout重新烧录eMMC镜像
恢复连接之后,直接看Yocto构建产物目录。STM32MP1的Yocto BSP通常会生成一堆tf-a-stm32mp13*.stm32、fip.bin、u-boot.stm32,以及一个flashlayout文件夹,里面有.tsv分区描述文件。STM32CubeProgrammer烧录eMMC时,就是靠这个TSV文件告诉它“哪些数据写到eMMC的哪个位置”。
我这次用的是自己改过的flashlayout_emmc.tsv,关键内容大致如下:
#Opt Id Name Type IP Offset Binary - 0x01 fsbl1-boot Binary mmc0-boot1 0x0 tf-a-stm32mp135da-fsbl.stm32 - 0x03 fip-boot Binary mmc0-boot1 0x0 fip.bin P 0x04 bootfs System mmc0p4 0x0 bootfs P 0x05 vendorfs System mmc0p5 0x0 vendorfs P 0x06 rootfs FileSystem mmc0p6 0x0 rootfs.ext4在执行烧录前,有几件事必须确认:
mmc0-boot1意味着数据要写进eMMC的内部boot分区1,而不是用户数据区。很多自定义Yocto镜像会在这一步出错,因为某些build流程默认把FSBL写到了UDA开头,ROM code根本不去那里找。- 后面的
mmc0p4、mmc0p6是指UDA里的第4分区、第6分区,这个编号必须和WKS分区脚本一致。我前面PANIC的根因之一,就是TSV里写的第10分区和WKS里的实际布局不一致。 - 如果你不确定当前的TSV分区布局,先用U-Boot的
mmc part查看在线eMMC分区表,再比对TSV,别急着写。
烧录命令如下:
STM32_Programmer_CLI -c port=USB1 -w flashlayout_emmc.tsv如果想顺便把整块eMMC备份出来,可以在烧录前先读:
# 读取eMMC UDA全部内容到本地文件,注意容量大小,别把磁盘塞满 STM32_Programmer_CLI -c port=USB1 -r8 0x00000000 0x2000000 emmc_backup.bin0x2000000对应256MB,如果你的rootfs较大,自行调整大小。
4.4 烧录后的验证:别急着断电
烧录完成后,先不要急着重启看结果。我建议在CubeProgrammer里做一次“校验”操作,确认写入的数据没问题,然后再断电,把BOOT拨码拨回eMMC启动模式,上电看串口日志。
验证阶段我会按这个顺序检查:
- 串口是否出现DDR初始化信息。
- 是否枚举到MMC设备,
mmc1还是mmc0。 - 是否成功加载FIP和U-Boot。
- 内核是否挂载到正确的rootfs设备。
只有看到系统进到登录提示符,才算真正救活。
5. 从Yocto层面根治:自定义machine的第二次机会
把板子救回来之后,你一定会问:怎么改,才能让下一次构建出来的镜像直接能eMMC启动,不再PANIC?这就得回到Yocto的machine配置上。既然你是自定义的machine,说明你不想一直用ST官方现成的配置,那就要把自定义的部分和eMMC启动链路对齐。
5.1 Machine配置里与eMMC启动强相关的三处
第一处是machine conf里的boot设备定义。ST的Yocto BSP里,stm32mp13common.inc这类公共配置通常定义了一系列默认变量,包括MACHINE特性、PREFERRED_PROVIDER_u-boot、内核设备树列表等。如果你自定义的machine没有正确继承这些公共配置,或者覆盖了其中和MMC顺序有关的变量,U-Boot生成时就会用错设备树别名。
第二处是设备树本身。sdmmc2节点必须配置正确的pinctrl和电压设置,特别是eMMC通常需要1.8V I/O,如果引脚mux成3.3V逻辑,eMMC在高速模式下会连续CRC错误。这个错误在SD卡启动时完全看不出来,因为SD卡走的是sdmmc1,跟sdmmc2无关。
第三处是内核cmdline。U-Boot的默认环境变量(或者uEnv.txt)里的bootargs必须包含正确的root=参数。我这里以实际配置为例:
# 如果你的eMMC被U-Boot枚举为mmc1,且rootfs在UDA第6分区 setenv bootargs "console=ttySTM0,115200 root=/dev/mmcblk1p6 rootwait rw"注意是mmcblk1p6,不是mmcblk0p6。这个“设备号+分区号”的组合,是eMMC启动能否成功的关键。
5.2 WKS分区脚本和TSV必须“对齐”,差一个数字都不行
自定义machine时最常见的问题,就是改了WKS分区脚本,但没有同步修改flashlayout TSV。Yocto在构建镜像时会用WKS脚本生成最终的.wic镜像,但STM32CubeProgrammer烧录时看的是TSV里的偏移。如果两者不一致,就像寄快递时收件人地址写错一个小区,快递永远送不到正确的人手里。
我现在的做法是:在machine conf里固定一个明确的eMMC分区方案,让WKS和TSV都从同一份定义读取。下面是我的WKS脚本片段:
part /boot --source bootimg-partition --ondisk mmcblk1 --fstype=ext4 --label bootfs --active --align 4096 part / --source rootfs --ondisk mmcblk1 --fstype=ext4 --label rootfs --align 4096对应的TSV就是:
P 0x04 bootfs System mmc0p4 0x0 bootfs P 0x06 rootfs FileSystem mmc0p6 0x0 rootfs.ext4这里0x04对应WKS里的第一个分区,0x06对应第二个分区,中间空出来的0x05可以留给vendorfs之类。分区编号一旦定下来,就不要乱动。
5.3 U-Boot环境变量和boot.scr:让内核自动找到正确设备
另一个非常隐蔽的坑来自boot.scr或uEnv.txt。ST官方镜像里,U-Boot会去bootfs分区找一个启动脚本,里面定义了bootcmd、kernel_addr_r、fdt_addr_r这些变量。如果你自定义machine时生成的bootfs里包含的还是官方默认的启动脚本,它会按照官方分区布局去加载内核,而你的自定义布局可能完全不同。
我的建议是,在machine conf里用IMAGE_BOOT_FILES显式指定bootfs里放哪些文件,同时确保U-Boot的bootcmd能找到这个脚本:
IMAGE_BOOT_FILES = " \ uImage \ stm32mp135da-custom.dtb \ boot.scr \ "boot.scr的内容我一般会在U-Boot源码目录下维护一个boot.cmd,生成后编译进镜像。关键就是设置正确的root设备:
setenv bootargs "console=ttySTM0,115200 root=/dev/mmcblk1p6 rootwait rw" fatload mmc 1:4 ${kernel_addr_r} uImage fatload mmc 1:4 ${fdt_addr_r} stm32mp135da-custom.dtb bootm ${kernel_addr_r} - ${fdt_addr_r}注意这里加载路径写的是mmc 1:4,代表第1个MMC设备(eMMC)的第4分区(bootfs)。如果你的U-Boot把eMMC枚举成了mmc0,这段脚本要相应改成mmc 0:4,否则又会回到“设备号写错”的老路上。
5.4 一个可靠的验证顺序:先在U-Boot里手动测,再固化到脚本
不管你怎么改Yocto配置,我建议第一次不要直接信任自动生成的启动流程。烧录成功并把BOOT拨回eMMC模式后,在U-Boot命令行里手动执行一遍启动命令,确认每个地址、每个分区号都是对的,再改成boot.scr固化。顺序是:
mmc list确认eMMC设备号。mmc dev 1切换到eMMC。mmc part确认分区布局。fatload mmc 1:4 ${kernel_addr_r} uImage测试能否读到内核。fatload mmc 1:4 ${fdt_addr_r} stm32mp135da-custom.dtb测试设备树。bootm手动启动。
这套命令全部通过,说明硬件、分区、DTB、内核镜像都没问题,然后把同样的内容固化到boot.scr里,你的Yocto镜像才算真正“eMMC-ready”。
6. 折腾之后的几条硬经验
板子救回来之后,我把这次踩坑的过程整理成了几条以后不再犯的清单,也分享给你。
第一条,改动Yocto machine之前,先备份一份原厂或当前可用镜像。备份不花多少时间,真到了CubeProgrammer连不上的时候,你才知道有个后备镜像有多重要。最好是连eMMC UDA完整dd一份出来,放移动硬盘里吃灰。
第二条,板卡的BOOT拨码组合必须第一时间写进硬件调试笔记里,尤其是“USB DFU工程模式”对应哪几根引脚。这次CubeProgrammer连不上的时候,我就是靠着这个记忆才快速切到USB DFU模式恢复的。如果板子上没引BOOT引脚,建议飞线也要引出来。
第三条,eMMC启动的PANIC根因多数不在“eMMC烧录”本身,而在“分区布局”和“设备号”。U-Boot里手动执行一遍mmc list、mmc part、ls mmc x:y,能帮你把问题从“玄学”变成“数学”。
第四条,CubeProgrammer连不上时,第一个要改的不是线,不是驱动,而是连接模式。命令行mode=UR、mode=HOTPLUG挨个试,再不行就切BOOT到DFU模式。绝大多数“连不上”都是启动状态和复位方式不匹配,不是硬件损坏。
最后一条,也是我这次体会最深的:Yocto自定义machine最怕的不是改错,而是改了A却忘了同步B。分区表、U-Boot环境变量、设备树里的MMC alias、内核cmdline、TSV文件,这几个文件必须被当作一份完整的“启动契约”来维护。任何一个环节私自变了,eMMC启动的某一环就会静默失败,最终变成屏幕上那个让你血压升高的PANIC。