AT91RM9200嵌入式Linux移植全流程与调试实践

1. 为什么十几年前的AT91RM9200还值得动手移植一遍

手里这块AT91RM9200的开发板已经在抽屉里躺了很久。板子是基于ARM920T内核的老型号,主频不高,SDRAM容量按今天的标准看小得可怜,但每次翻出来都舍不得处理掉。原因很简单:它是我理解"嵌入式Linux到底是怎么跑起来的"最干净的一块试验田。现在的开发板太友好了,厂商把BSP、设备树、镜像、烧写脚本全部打包好,编译几条命令就能点亮屏幕,问题是这一整套流程里,你知道自己在干什么的部分其实很少。而AT91RM9200这种老平台,内核源码里还留着大量板级文件,启动流程、时钟初始化、串口注册、存储介质识别,几乎每一行都要你自己确认,这种"什么都要自己接上"的过程,是移植能力真正长出来的地方。

这次我打算把整个嵌入式Linux移植过程完整走一遍,从工具链准备、启动介质的选择、引导程序适配,到内核配置裁剪、根文件系统搭建、串口控制台调通,再到后面调试中遇到的各种问题。整篇内容面向的是有一点C语言和Linux使用基础、但没怎么独立做过板级移植的读者,同时也适合手上有类似老ARM平台、想把它们重新用起来的人。文中涉及的板子、工具和参数都会给出具体做法和取舍理由,你完全可以照着复现一遍。

先说清楚一件事:AT91RM9200这颗芯片的移植难点,不在"Linux能不能跑",它当然能跑,社区里跑通过的人不少;难点在于细节极其琐碎。它的启动方式、时钟树、内存映射、串口引脚复用,每一处都有坑,而且这些坑在文档里往往写得很模糊,只能靠读数据手册和源码一点点对。我在这块板子上前后折腾过好几轮,有些问题是当时解决了但没记录,后来重新做又踩了一遍,这次索性边做边写,把踩坑的完整链路也留下来。

提示:本文所有操作针对的是AT91RM9200这类早期ARM920T平台,涉及的具体寄存器地址、时钟频率、分区大小请以你手上板子的原理图和数据手册为准。文中给出的是我实际使用的一套参数,可以直接作为起点,但不建议不加确认地照搬。

另外要提前说明,老平台的移植有一个很现实的约束:很多当年的工具链、编译器版本、甚至交叉编译环境都已经很难直接装上了。这不是坏事,反而逼着你去理解"工具链到底是什么",而不是把它当成一个黑盒。后面我会专门讲工具链怎么选、为什么这么选,以及新版本工具链在老平台上会遇到哪类问题。

2. 移植之前必须弄清楚的硬件底座

动手之前如果不把硬件搞清楚,后面所有的调试都会变成盲猜。我在第一次做这块板子的时候就是急着编译内核,结果串口一直没输出,折腾了两天才发现是晶振频率在配置文件里写错了,导致波特率算出来完全不对。这类问题看起来低级,但在板级移植里占比极高。

2.1 时钟、内存与启动介质的实际配置

AT91RM9200的时钟结构相对简单,但也需要认真核对。板子上通常有一颗主晶振,经过片内PLL倍频后产生处理器主频和外设时钟。我这块板子用的是18.432MHz晶振,这个频率不是随便选的,它和串口的波特率分频关系比较友好,能减少误差。处理器主频配置到180MHz左右,总线时钟相应分频。这些值必须和引导程序、内核里的配置保持一致,任何一处对不上,最直接的后果就是串口输出乱码,严重的话系统直接起不来。

内存方面,板子挂的是两片SDRAM,总容量64MB,地址从0x20000000开始。这个地址很关键,因为引导程序需要把自己搬到内存里运行,内核也要加载到这个区域。SDRAM的刷新参数、时序参数需要按芯片手册配置,早期平台没有设备树自动描述这些,全靠板级文件里写死,配错了表现是随机的内存错误,非常难查。

启动介质这块,AT91RM9200支持从多种介质启动,包括并行NOR Flash、串行DataFlash、以及通过串口或USB的引导加载。我这块板子上有一片DataFlash和一片NOR Flash,实际移植时我选择把引导程序放在DataFlash里,内核和根文件系统放在NOR Flash的分区中。DataFlash的好处是接口简单、占用引脚少,缺点是容量小、写入速度慢,只适合放引导程序这种体量很小的东西。

硬件项我这块板子的配置移植中的关注点
主晶振18.432MHz与引导程序、内核配置必须一致
处理器主频约180MHzPLL倍频参数需按手册计算
SDRAM2片,共64MB,起始0x20000000时序与刷新参数必须匹配芯片
引导介质DataFlash容量小,只放引导程序
存储介质NOR Flash需规划内核与文件系统分区
调试串口DBGU(调试串口)引脚固定,优先用它做控制台

2.2 调试串口为什么一定要先用DBGU

这颗芯片上有多个串口,其中DBGU是专门的调试串口,它的引脚是固定的,不涉及复用配置,也不需要额外的收发器使能控制。移植初期所有信息都应该从它出来。我见过有人一上来就想用普通USART做控制台,结果卡在引脚复用和时钟使能上,连引导程序的第一行输出都看不到,等于失去了唯一的观测手段。

用DBGU的原则很简单:在引导程序和内核里都把控制台指向它,先把"能看到输出"这件事解决,再去折腾其他外设。等系统完全跑起来、其他串口也调通了,再考虑换控制台。这个顺序不能反,移植过程中最怕的就是失去观测能力,一旦看不到任何输出,所有判断都只能靠猜。

串口参数一般设为115200波特率、8位数据、无校验、1位停止位。这个组合在老平台上最稳,因为分频误差最小。如果你的晶振频率比较特殊,算出来的波特率可能有偏差,表现为能收到字符但偶尔出错,这时候要回头检查分频计算,而不是怀疑线缆。

2.3 存储分区规划:别等内核放不下了再返工

分区规划是很多人容易跳过的一步,觉得先跑起来再说。我的建议正好相反,因为老平台的Flash容量通常很紧张,分区一旦定了,后面引导程序里写死的偏移量、内核启动参数都要跟着改,返工成本很高。

我的规划是这样的:把NOR Flash开头一小段留给引导程序的环境变量或者备份区,接着是内核分区,再往后是根文件系统分区。具体的偏移和大小要根据你编译出来的内核镜像和文件系统镜像实际大小留出余量。这里有个经验:内核分区至少留出实际镜像两倍的空间,因为调试期间你会反复换内核版本,有时候还会临时塞一个带调试符号的大内核进去,空间不够会直接卡住整个流程。

至于根文件系统,初期建议用只读的 cramfs 或者 initramfs 起步,等系统稳定了再换成可读写的 jffs2。原因是初期调试阶段系统经常会异常掉电或者复位,可读写文件系统在这种反复断电的情况下很容易损坏,而只读文件系统无论怎么折腾都不会坏,能帮你排除掉一大类问题。

3. 交叉工具链的选型与验证方法

工具链这块我要多说几句,因为它是整个移植过程中最容易被轻视、又最容易出问题的地方。很多教程直接给一条命令让你装某个版本的编译器,然后就开始编译,结果编译出来的东西在板子上跑不起来,或者跑起来行为诡异,最后排查半天发现是工具链的ABI和内核配置不匹配。

3.1 老平台该选什么样的编译器

AT91RM9200是ARM920T核心,属于ARMv4T架构,不支持Thumb-2之类的新指令集。这意味着你需要一个能正确生成ARMv4T代码的工具链。现在的通用交叉编译器大多默认面向较新的架构,虽然可以通过参数指定目标架构,但在某些细节上(比如默认的浮点ABI、对齐要求)可能和当年为这个平台优化的工具链有差异。

我的做法是优先使用能明确指定目标架构、且支持软浮点的工具链。软浮点这条很重要,因为AT91RM9200没有硬件浮点单元,如果工具链默认按硬浮点生成代码,链接阶段可能报错,或者运行时出现奇怪的异常。配置内核时也要确认浮点相关的选项和老平台匹配,这一点在后面内核配置章节会细说。

另一个考虑是C库的选择。glibc功能全但体积大,对老平台不太友好;uClibc或者musl体积小,更适合这种资源紧张的板子。我这套方案用的是uClibc,根文件系统整体能压到几MB以内,跑起来内存占用也低。代价是某些依赖glibc特性的程序需要重新编译或者打补丁,这个在应用移植时要注意。

3.2 用一个小程序验证工具链是否真的可用

装好工具链之后,不要急着编译内核,先写一个最小的测试程序验证一遍。这一步很多人省略,但它能帮你提前发现大量问题。

/* toolchain_test.c - 最小验证程序 */ #include <stdio.h> int main(void) { volatile int a = 1; volatile int b = 2; int c = a + b; printf("toolchain ok, result=%d, pointer_size=%d\n", c, (int)sizeof(void *)); return 0; }

编译并检查产物的属性:

arm-linux-gcc -march=armv4t -msoft-float -O2 -o toolchain_test toolchain_test.c arm-linux-readelf -h toolchain_test arm-linux-readelf -A toolchain_test

用 readelf 看头部信息时,重点确认三件事:一是机器类型是ARM;二是标志位里没有出现要求新架构的特性;三是属性段里浮点ABI是软浮点。如果这三点都符合,说明工具链的基本方向没问题。然后把这个程序放到板子上跑一遍,看输出是否正常,指针大小应该是4字节(32位平台)。这个程序跑通了,后面编译内核和文件系统才有意义。

注意:不要用宿主机上的 gcc 编译这个测试程序然后拿到板子上跑,也不要因为交叉编译出来的程序在宿主机上无法执行就怀疑它有问题,架构不同本来就不能直接运行。判断标准是 readelf 的信息和板子上的实际运行结果。

3.3 工具链与内核版本的兼容性陷阱

这里有一个我实际踩过的坑。早期我用了一个比较新的工具链去编译老版本内核,编译过程一路顺利,没有报错,但生成的镜像在板子上跑到一半就挂,串口输出到某一行就停了,没有任何异常信息。后来把工具链换成更老一点的版本,同样的内核源码编译出来就能正常启动。

原因在于新编译器对某些代码的优化行为和预期不一致,特别是内核里那些依赖内联汇编、内存屏障、以及未定义行为边界的老代码,新编译器可能会做出"合法但不符合原作者预期"的优化。这类问题极难定位,因为编译日志干干净净。

我的建议是:如果内核版本比较老,工具链也不要一味求新,尽量选和内核同期或者稍晚一些的版本。这不是保守,而是减少变量。移植阶段你要排查的问题已经够多了,不要在工具链这个非核心变量上给自己增加难度。等系统完全跑通、稳定运行之后,再考虑逐步升级工具链并回归测试。

4. 引导程序适配:让板子先能说话

引导程序是整个移植的第一个里程碑。它的任务很明确:初始化最基本的硬件(时钟、内存、串口),把内核从存储介质里搬到内存,设置好启动参数,然后跳过去执行。听起来简单,但在老平台上每一步都要自己接。

4.1 为什么选U-Boot,以及要改哪些地方

我这套方案用的是U-Boot。选择理由不是它多先进,而是它对AT91系列的支持相对完整,源码里能找到同系列芯片的板级配置可以参考,改动量可控。对于AT91RM9200,U-Boot里通常已经有接近的板级目录,你需要做的是基于它复制一份,然后按自己板子的实际情况修改。

要改的地方主要集中在几处。第一处是时钟和内存的初始化,这部分通常在板级文件里的初始化函数中,负责配置PLL、SDRAM控制器、以及基本的引脚复用。第二处是存储介质的识别,DataFlash和NOR Flash的访问方式不同,需要确认对应的驱动被使能。第三处是环境变量和启动参数,包括内核在Flash中的偏移地址、内存中的加载地址、控制台设备名和波特率。

# 以某一同系列板级配置为基础,复制出自己板子的配置 cp -r board/atmel/at91rm9200ek board/atmel/myboard # 修改板级目录中的配置文件、Makefile、以及初始化代码 # 在顶层配置文件中登记新板子

编译完成后,先不要急着烧到Flash里,优先通过片内引导或者串口加载的方式把U-Boot放到内存里试跑。这一步的意义是:如果U-Boot本身有问题,你不需要反复擦写Flash,重新上电加载一次就行,迭代速度快很多。等串口能稳定打印出U-Boot的提示符,再考虑固化到Flash。

4.2 串口没有任何输出时的排查顺序

U-Boot阶段最常见的问题就是串口一片安静。这时候不要东改一处西改一处,按固定顺序排查效率最高。

先确认供电和复位是否正常。用万用表看核心电压和IO电压是否在范围内,复位引脚在上电后是否正常释放。这一步看着基础,但确实遇到过电源纹波过大导致芯片反复复位的情况,表现就是串口偶尔吐几个乱码字符。

再确认时钟。如果板子上有可以测量的时钟输出引脚,用示波器看一下频率对不对。没有示波器的话,就回头检查PLL配置寄存器的值是否按手册公式算对了,晶振频率是否和代码里写的一致。前面提到的晶振频率写错导致波特率不对,就是在这个环节暴露的。

然后确认串口本身的配置。DBGU的引脚是否被其他功能占用,波特率分频值是否算对,数据格式是否匹配。这里有个小技巧:把波特率临时降到一个很低的数值(比如9600),如果低波特率能出字符而高波特率不行,基本可以确定是时钟或者分频计算的问题,因为低波特率对误差的容忍度高得多。

最后才怀疑代码逻辑。把初始化流程从头到尾读一遍,确认每一步的执行顺序符合手册要求,特别是时钟使能和引脚配置的先后关系。有些外设必须先使能时钟再配置寄存器,顺序反了配置不生效,而且不会报错,只是静默失败。

4.3 环境变量与启动参数的设置要点

U-Boot跑起来之后,把启动参数配置好,这是衔接内核的关键。核心参数包括控制台设备、根文件系统的位置和类型、内存大小。

setenv bootargs console=ttyS0,115200 root=/dev/mtdblock2 rootfstype=cramfs setenv bootcmd nand read 0x21000000 0x40000 0x200000 \; bootm 0x21000000

上面这段只是示意,实际参数要按你的分区规划来写。这里有几个容易出错的点:一是控制台设备名,早期内核里调试串口的设备名可能不是 ttyS0,需要确认内核配置中哪个串口驱动被编译进去、注册成了哪个设备节点;二是 root 参数里的分区编号,要和你实际的分区表对应;三是内核加载地址不能和U-Boot自身占用的内存区域重叠,否则搬移内核时会把U-Boot自己覆盖掉,表现为跳转后直接死机。

提示:把 bootargs 和 bootcmd 保存到环境变量里之前,先在命令行手动执行一遍,确认能正常启动再 saveenv。否则参数写错又保存了,下次上电还是错的,还得想办法进命令行改回来。

5. 内核配置裁剪与板级适配

内核这一步是整个移植里工作量最大的部分,也是最考验对平台理解的地方。AT91RM9200属于比较早期的平台,内核里还保留了arch/arm/mach-at91这样的板级目录结构,很多配置是写死在板级文件里的,需要你逐项核对。

5.1 从哪个配置起步最省力

不要从零开始配,那基本是自找麻烦。正确的做法是找一个同系列、同架构的默认配置作为起点,然后在这个基础上删改。

make ARCH=arm CROSS_COMPILE=arm-linux- at91rm9200_defconfig make ARCH=arm CROSS_COMPILE=arm-linux- menuconfig

用默认配置起步的好处是,大部分和平台相关的基础选项已经是对的,你只需要关注自己板子上的差异部分。需要重点确认的几类选项:处理器型号和架构版本要选对,浮点支持要按软浮点配置,内存起始地址和大小要匹配,串口驱动要包含DBGU对应的那一项,存储介质的驱动要包含DataFlash和NOR Flash的支持。

裁剪的原则是"先全后减"。第一次编译建议把相关驱动都带上,先让系统跑起来,确认基本功能正常,然后再逐个去掉不需要的选项,每去掉一批就重新编译测试一次。反过来做,一开始就极致裁剪,一旦启动失败你根本不知道是裁掉了什么关键东西,排查成本极高。

5.2 板级文件里那几个必须改的参数

板级文件通常包含机器描述、内存映射、以及各种设备的注册信息。对AT91RM9200来说,需要重点核对的是内存起始地址和大小、串口注册信息、以及Flash分区的定义。

内存参数如果写错,最典型的表现是内核启动到内存初始化阶段就挂,或者启动成功但可用内存明显偏少。Flash分区定义如果和实际不符,根文件系统挂载会失败,报找不到设备或者读取错误。分区定义这块建议直接把实际的偏移和大小写清楚,并且在注释里标明和U-Boot里分区规划的一致性,方便以后对照。

还有一个容易被忽略的点是机器ID。早期ARM Linux的启动协议要求引导程序把机器ID通过寄存器传给内核,内核根据这个ID匹配对应的板级描述。如果机器ID对不上,内核会直接报"不支持的机器"然后停住。这个ID在U-Boot的板级配置和内核的板级文件里都要有,且必须一致。我第一次做的时候就是这里没对上,串口打印了一行错误就没了,查了很久才反应过来是两个地方的定义不同步。

5.3 编译产物的检查与镜像格式转换

内核编译完成后会生成一个压缩镜像,但很多引导程序需要的是带引导头的格式。这时候需要做格式转换:

make ARCH=arm CROSS_COMPILE=arm-linux- zImage # 生成 uImage(如果需要U-Boot引导头) mkimage -A arm -O linux -T kernel -C none \ -a 0x20008000 -e 0x20008000 \ -n "myboard-kernel" -d arch/arm/boot/zImage kernel.uImage

加载地址和入口地址要按你的内存布局来填,通常和U-Boot里配置的内核加载地址一致。这两个地址填错的后果是内核被搬到错误位置,跳转后直接跑飞。转换完成后可以用工具查看镜像头信息,确认架构、类型、加载地址都正确,再放到板子上试。

编译过程中如果出现链接错误,提示某些符号未定义,通常是配置里选了某个驱动但依赖的库或者选项没开。这时候不要急着去改代码,先回到配置界面把相关的依赖项打开。老内核的配置依赖关系有时候提示不够明确,需要你大致清楚这个驱动依赖哪些子系统。

6. 根文件系统搭建:让内核有事可做

内核能启动、串口有输出,只是完成了第一步。没有根文件系统,内核启动到最后会报一个"无法挂载根文件系统"的错误然后停住。这一步的目标是搭一个最小可用的根文件系统,让系统能进到shell。

6.1 最小根文件系统里到底需要什么

最小系统需要的东西其实很少:一个 init 程序、一个 shell、基础的设备节点、以及必要的库文件。我一开始用 BusyBox 来搭建,它一个可执行文件就能提供 shell、常用命令、以及 init 功能,非常适合资源受限的平台。

目录结构按标准来建:bin、sbin、etc、dev、proc、sys、lib、tmp。其中 dev 目录下的设备节点是早期系统必须手动创建的,因为那时候 devtmpfs 还不普及。至少要有 console、null、tty 这几个。创建的时候要注意主次设备号,控制台设备的主设备号是5、次设备号是1,这个不能错。

# 手动创建设备节点的示例 mknod dev/console c 5 1 mknod dev/null c 1 3 mknod dev/tty c 5 0

库文件这块要特别注意。如果程序是动态链接的,需要把对应的C库和动态加载器复制到根文件系统的 lib 目录下,路径要和程序里记录的路径一致。路径不对的表现是程序启动时报"找不到解释器"或者"找不到共享库"。用 readelf 查看程序的 interpreter 字段,确认加载器路径,然后按这个路径放置文件。

6.2 用 cramfs 还是 jffs2:先只读后读写

前面提到过,初期建议用只读文件系统。cramfs 的优点是压缩率高、只读不会损坏、挂载速度快,非常适合调试阶段。生成方式也简单,用一个工具把目录打包成镜像即可。

等系统稳定了,如果需要保存配置或者运行时要写文件,再换成 jffs2。jffs2 是专为Flash设计的可读写文件系统,有磨损均衡和掉电保护机制,比直接挂 ext2 到Flash上要可靠得多。切换的时候要注意内核配置里对应的文件系统支持要打开,并且引导参数里的 rootfstype 要改。

文件系统适用阶段优点注意事项
cramfs初期调试只读不损坏、压缩率高无法写入,配置不能持久化
initramfs初期调试打包进内核,无需单独分区占用内存,镜像变大
jffs2稳定运行可读写、有磨损均衡挂载稍慢,需要正确分区
ext2不推荐兼容性好无磨损均衡,Flash上易损坏

6.3 init 脚本与串口登录的打通

系统启动到最后,需要 init 程序接管。BusyBox 的 init 会读取配置文件,按里面的条目执行初始化脚本。最简配置里至少要挂载 proc 和 sysfs,然后启动一个 shell 到控制台。

# /etc/inittab 的最简内容 ::sysinit:/etc/init.d/rcS ::respawm:/bin/sh

rcS 脚本里做几件事:挂载 proc、挂载 sysfs、设置主机名、配置网络(如果需要)。挂载 proc 是必须的,很多程序依赖它读取系统信息,没挂载的话会出现各种奇怪的报错。

串口登录这块,如果只是调试用,直接把 shell 起在控制台上就够了。如果需要登录认证,可以用 getty 加 login 的组合,但会增加复杂度,调试阶段不建议。等系统完全稳定了再考虑加认证。

注意:inittab 里 respawn 那一行的拼写要仔细检查,写错了 init 不会报错,只是不启动 shell,表现为系统启动完成后串口没有任何交互,看起来像卡住了。我第一次遇到这个问题时以为是内核挂了,查了半天才发现是配置文件里一个字母写错。

7. 联调阶段那些反复出现的具体故障

前面几章是按流程走的,实际做的时候问题不会这么整齐地出现。这一章我把联调阶段遇到的几类典型故障整理出来,包括排查链条,方便你对照。

7.1 内核启动到一半停止输出

这个现象很常见,可能的原因有好几类,需要按顺序排查。先看停止的位置:如果停在"Uncompressing Linux"之后,说明解压或者内存搬移有问题,重点查加载地址和内存参数;如果停在内存初始化附近,查SDRAM时序配置;如果停在串口初始化之后、根文件系统挂载之前,查存储介质驱动和分区定义。

我遇到过一次停在中途且没有任何错误信息的情况,最后定位到是内核配置里打开了某个调试选项,导致在早期阶段访问了尚未初始化的硬件,直接触发异常但异常处理本身还没准备好,所以什么信息都没打印。关掉那个选项后正常启动。这类问题的教训是:调试阶段的内核配置尽量保守,不确定的选项先关掉。

7.2 根文件系统挂载失败

报错通常是"VFS: Cannot open root device"或者"Kernel panic - not syncing: VFS"。排查顺序:先确认引导参数里的 root 参数指向的设备对不对,这个设备名要和内核识别出来的MTD分区对应;再确认分区表里那个分区的偏移和大小是否和实际烧写的位置一致;然后确认文件系统类型是否写对,且内核里对应的文件系统驱动已编译进去。

有一次我改了分区大小但忘了重新生成文件系统镜像,结果镜像大小和新分区不匹配,挂载时报读取错误。这种不同步的问题在调试期间很常见,建议改任何一侧参数时都在笔记里记一笔,另一侧同步修改。

7.3 系统能起来但网络或其他外设不工作

系统进到shell之后,如果发现网络不通,先确认网卡驱动是否编译进去、是否被正确识别(看启动日志里有没有探测信息)、PHY是否复位成功。老平台的网卡驱动经常需要手动配置PHY地址和复位引脚,这些信息在原理图上。

如果外设完全不响应,回头检查引脚复用配置。AT91RM9200的引脚复用是通过寄存器配置的,某个引脚被配置成外设功能还是GPIO,直接决定外设能不能用。这类配置错误不会报错,只是外设静默不工作,需要对照原理图逐引脚核对。

8. 把这套流程复现出来需要注意的几件事

整套流程走下来,最大的感受是:老平台移植的价值不在于最后得到一个能跑的系统,而在于中间每一步都必须想清楚"为什么是这样"。现代开发板把这些都封装好了,你很难有机会去理解引导程序怎么把控制权交给内核、内核怎么找到根文件系统、设备节点为什么是那个主次设备号。

如果你打算照这套流程复现,我的建议是先把手上的板子硬件信息整理成一张表,包括晶振频率、内存型号和容量、Flash型号和分区规划、串口引脚。这张表在后续每一步都会用到,而且能帮你避免大量"两边不一致"的问题。然后把流程拆成几个可验证的里程碑:引导程序串口有输出、引导程序能加载内核、内核启动有输出、内核能挂载根文件系统、系统能进shell。每达成一个再往下走,不要跳步,跳步之后出问题你无法判断是哪一步引入的。

还有一点体会比较深:调试期间的所有改动都要记录下来,包括改了哪个文件、改了哪个参数、改之前是什么值。老平台的问题往往不是一次改对的,而是试了几次才找到正确配置,如果不记录,很容易在几个错误配置之间来回打转。我自己就曾经因为忘了之前改过什么,把一个已经排除的配置又改回去,白折腾了半天。

最后分享一个小技巧:在板子旁边放一个串口日志的持续记录工具,把每次上电的输出都存下来。出问题的时候对比正常启动和异常启动的日志差异,往往能直接定位到出问题的阶段,比盯着屏幕一行行看要高效得多。