尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
深入uboot第一阶段源码:ARM启动流程与内存初始化实战
1. 为什么还要啃uboot第一阶段源码很多人学嵌入式Linux到了uboot这一关就开始犯怵。网上的教程一抓一大把但大多数停留在编译、烧录、能跑就行的层面真正翻开源码一行行读下去的少之又少。我当初也是这么过来的——板子能启动串口有输出就觉得万事大吉。直到有一次遇到一个奇怪的启动问题板子上电后串口一片死寂连第一行打印都没有这时候才发现如果不懂uboot第一阶段到底干了什么你连从哪儿下手排查都不知道。uboot的启动流程分为两个阶段这是设计上的刻意为之。第一阶段Stage 1通常用汇编编写代码量不大但干的都是性命攸关的事设置CPU运行模式、关闭看门狗、初始化时钟和内存、准备C语言运行环境最后跳转到第二阶段的C入口。这一阶段之所以用汇编是因为此时内存还没初始化好栈都没地方放C语言根本跑不起来。你可以把它理解成盖房子之前打地基——地基没打好上面盖什么都是白搭。这篇文章面向的是有一定ARM体系结构基础、正在或者准备深入uboot源码的嵌入式开发者。我会以常见的ARMv7平台比如Cortex-A系列为背景把uboot第一阶段的核心源码逐段拆开来讲不光告诉你这行代码干了什么更要讲清楚为什么要这么干以及不这么干会怎样。文章里涉及的具体文件路径和函数名不同版本的uboot会有差异但核心逻辑是相通的你对照自己手头的源码看就行。需要提前说明的是uboot版本众多从早期的2010.x到现在的2024.x目录结构和代码组织方式变化不小。我这里分析的是比较经典的架构源码主要位于arch/arm/cpu/目录下的启动文件以及arch/arm/lib/中的相关汇编。如果你用的是厂商定制版uboot比如某些芯片原厂提供的SDK代码可能被改得面目全非但主干逻辑不会变。2. 第一阶段到底在干什么整体设计思路拆解2.1 两个阶段的划分逻辑uboot把启动分成两个阶段这不是拍脑袋决定的而是被硬件现实逼出来的。上电那一刻CPU刚从复位向量跳出来DRAM控制器还没配置内存完全不可用。这时候你能用的只有CPU内部的SRAM通常几十KB到几百KB和寄存器。C语言函数调用需要栈全局变量需要可读写的数据段这些都得有内存支撑。所以在内存初始化完成之前只能用汇编小心翼翼地操作寄存器和SRAM。第一阶段的使命很明确用最少的代码把系统从裸机状态带到能跑C语言的状态。具体来说它要完成这几件事进入SVC模式关中断让CPU处于一个可控的状态关闭看门狗防止在初始化过程中被复位关闭MMU和Cache或者做必要的Cache配置保证内存访问的一致性配置时钟系统让CPU和总线跑在合适的频率上初始化DRAM控制器把内存唤醒设置栈指针为C语言准备运行环境清零BSS段拷贝代码到内存如果需要跳转到第二阶段的C入口board_init_f这些步骤的顺序不能乱。比如你必须先关看门狗再去做耗时的内存初始化否则看门狗超时直接把系统复位了你必须先配置好时钟再初始化内存因为DRAM控制器的时序参数依赖于时钟频率。2.2 链接地址与运行地址的分离设计第一阶段有一个非常关键的设计概念链接地址link address和运行地址run address可能不一致。链接地址是编译时指定的代码里的标号、跳转指令都基于这个地址生成。运行地址是代码实际被加载到的地方。为什么会有这个差异因为uboot可能从NOR Flash、NAND Flash、SD卡、SPI Flash等不同介质启动。有些介质支持XIP就地执行代码可以直接在Flash里跑有些介质不支持必须先把代码搬到RAM里。而RAM的地址和Flash的地址显然不一样。uboot用CONFIG_SYS_TEXT_BASE来定义链接地址用位置无关代码PIC或者重定位机制来处理这个差异。在ARM平台上第一阶段早期通常使用相对跳转b、bl指令而不是绝对跳转ldr pc, label因为相对跳转的偏移量是固定的不管代码在哪里跑都没问题。等到需要访问绝对地址的数据时才通过adr、ldr等指令配合_TEXT_BASE来计算实际地址。这个设计的好处是灵活性极高同一份uboot代码可以适配多种启动介质。代价是代码可读性下降初学者看到ldr r0, _TEXT_BASE这种写法容易懵。我的建议是读源码时先在脑子里明确当前代码在哪里运行很多困惑就迎刃而解了。2.3 汇编与C的分界线第一阶段的终点是_main函数在arch/arm/lib/crt0.S中这个函数虽然还是汇编但它已经开始为C语言做最后的准备了。_main执行完毕后会调用board_init_f这才是第二阶段的C入口。这条分界线不是随便画的。在_main之前栈还没设置好你没法调用C函数在_main之后栈有了BSS清零了C语言可以正常跑了。所以_main是一个过渡层它自己用汇编写但为C语言铺好了路。理解这条分界线很重要因为它决定了你调试时的策略。如果问题出在_main之前你只能用汇编级调试手段比如点灯、JTAG单步如果问题出在_main之后你可以用printf、用GDB排查效率高得多。3. 核心源码逐段拆解与实操要点3.1 复位向量与异常向量表CPU上电后第一条指令从哪里取这取决于芯片的启动模式。ARM处理器通常有一个固定的复位向量地址比如0x00000000或者0xFFFF0000具体由芯片设计决定。uboot的入口点通过链接脚本控制在arch/arm/cpu/目录下的u-boot.lds中你会看到类似这样的定义ENTRY(_start) SECTIONS { . 0x00000000; . ALIGN(4); .text : { *(.__image_copy_start) *(.vectors) arch/arm/cpu/armv7/start.o (.text*) *(.text*) } }注意*(.vectors)这一行它把异常向量表放在了最前面。异常向量表是ARM架构的硬性要求复位、未定义指令、软中断、预取中止、数据中止、IRQ、FIQ这七种异常各有固定的入口地址间隔通常是4字节ARM状态或4字Thumb状态。uboot的向量表定义在arch/arm/lib/vectors.S中大致长这样_start: b reset ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq ldr pc, _fiq第一行b reset就是复位后执行的第一条指令跳转到reset标号。这里用b而不是ldr pc, reset是因为b是相对跳转不依赖绝对地址在重定位之前也能正确执行。注意有些芯片的复位向量不在0x00000000比如有些芯片上电后从0xFFFF0000取指令。这种情况下uboot的链接脚本和向量表位置都要相应调整。如果你移植uboot时发现第一条指令就跑飞了先确认芯片手册里的复位向量地址。3.2 reset标号进入SVC模式与关中断reset标号在arch/arm/cpu/armv7/start.S中这是第一阶段真正开始干活的地方。开头几行通常是这样的reset: /* 允许访问CP15协处理器 */ mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002000 /* 清除V位异常向量表基址设为0x00000000 */ bic r0, r0, #0x00000007 /* 清除I位和F位关闭IRQ和FIQ */ mcr p15, 0, r0, c1, c0, 0 /* 进入SVC模式 */ mrs r0, cpsr bic r0, r0, #0x1f orr r0, r0, #0x13 msr cpsr, r0这几行代码干了三件事设置异常向量表基址、关闭中断、切换到SVC模式。先说异常向量表基址。CP15的c1寄存器系统控制寄存器有一个V位bit 13它决定异常向量表是在0x00000000还是0xFFFF0000。uboot通常把它清0让向量表位于低地址。这个设置要和链接脚本里的地址对应否则中断来了会跳到错误的地方。再说关中断。CPSR的I位bit 7控制IRQF位bit 6控制FIQ。启动阶段关中断是必须的因为此时中断向量表可能还没完全准备好中断服务程序也不存在一旦来中断就是死路一条。等第二阶段初始化完中断控制器再按需打开。最后是模式切换。ARM有七种运行模式上电后默认是什么模式取决于芯片设计但uboot需要SVC模式管理模式因为只有SVC模式能访问所有特权寄存器。CPSR的低5位M[4:0]定义当前模式0x13就是SVC模式。实操心得如果你在调试时发现uboot跑着跑着就进了未定义指令异常先检查CPSR的模式位对不对。有些芯片上电后默认在Hyp模式或者Monitor模式不切换到SVC模式的话某些特权指令执行会出问题。3.3 关闭看门狗与时钟早期配置关看门狗这件事不同芯片的实现差异很大。有些芯片的看门狗在复位后是默认关闭的有些则是默认开启的。uboot通常会在reset之后不久就处理这件事代码可能在start.S里也可能在board_init_f的早期阶段。以某款常见的ARM SoC为例看门狗控制寄存器可能长这样/* 关闭看门狗 */ ldr r0, WTCON ldr r1, 0x0 str r1, [r0]简单粗暴直接把控制寄存器写0。但有些芯片的看门狗需要先解锁再操作或者有多个看门狗比如安全域一个、非安全域一个那就得逐个处理。时钟配置也是类似的情况。有些芯片上电后跑在内部低速时钟比如24MHz晶振或者内部RC振荡器需要切换到PLL输出的高频时钟。时钟配置涉及PLL锁定时间、分频系数等参数算错了轻则跑得慢重则直接跑飞。/* 配置PLL */ ldr r0, PLL_CON0 ldr r1, PLL_ENABLE | PLL_LOCKTIME | PLL_MDIV | PLL_PDIV | PLL_SDIV str r1, [r0] /* 等待PLL锁定 */ wait_pll_lock: ldr r0, PLL_CON0 ldr r1, [r0] tst r1, #PLL_LOCKED beq wait_pll_lock /* 切换时钟源 */ ldr r0, CLK_SRC ldr r1, CLK_SRC_APLL str r1, [r0]这段代码的逻辑是先配置PLL参数并使能然后轮询等待PLL锁定最后把系统时钟源切换到PLL。PLL锁定时间通常在几百微秒到几毫秒之间这个等待循环是必须的跳过的话系统会不稳定。注意时钟配置的参数计算通常有公式比如Fout Fin * MDIV / (PDIV * 2^SDIV)。这些参数不是随便填的要参考芯片手册里的推荐值和你的晶振频率来算。我见过有人直接把参考板的参数抄过来结果晶振频率不一样跑出来的时钟差了十万八千里。3.4 内存初始化DRAM控制器的配置内存初始化是第一阶段的重头戏也是最容易出问题的环节。DRAM控制器的配置涉及大量时序参数这些参数取决于你用的DRAM颗粒型号、PCB走线长度、工作频率等因素。以DDR3为例需要配置的参数包括参数含义典型值来源tRCD行激活到列命令延迟DRAM手册tRP预充电到行激活延迟DRAM手册tRAS行激活到预充电最小时间DRAM手册tRFC刷新周期时间DRAM手册CLCAS延迟DRAM手册tWR写恢复时间DRAM手册这些参数通常以时钟周期为单位需要根据你的DRAM工作频率换算。比如tRCD在DRAM手册里标的是13.75ns你的DDR时钟是400MHz周期2.5ns那tRCD就是13.75/2.55.5向上取整为6个周期。uboot里内存初始化的代码通常在arch/arm/cpu/armv7/下的某个ddr.c或者sdram.c文件中也可能在board目录下。代码结构一般是void dram_init(void) { /* 配置DDR控制器寄存器 */ writel(DDR_PHY_CON0, ...); writel(DDR_PHY_CON1, ...); /* ... 几十个寄存器 ... */ /* 执行DDR初始化序列 */ writel(DDR_CMD, DDR_CMD_INIT); while (readl(DDR_STATUS) DDR_STATUS_BUSY); /* 检查初始化是否成功 */ if (readl(DDR_STATUS) DDR_STATUS_ERROR) { /* 初始化失败 */ } }这段代码看起来简单但每个寄存器的值都是精心计算出来的。我见过一个案例某块板子的DDR初始化代码是从参考板抄来的但参考板用的是三星颗粒实际板子用的是海力士颗粒tRFC参数不一样结果系统跑一会儿就死机。后来把tRFC从160ns改成260ns问题解决。实操心得调试DDR初始化时如果手头有示波器可以量一下DDR时钟和命令线的波形确认频率和时序对不对。如果没有示波器可以在初始化前后分别读写一段内存看数据能不能正确写入和读出。最简单的办法是写一个已知模式比如0xAA55AA55到内存再读回来比较。3.5 设置栈指针与BSS段清零内存初始化完成后就可以设置栈指针了。栈的位置通常选在内存的高地址区域向下增长。uboot里一般这样设置/* 设置栈指针 */ ldr r0, CONFIG_SYS_INIT_SP_ADDR mov sp, r0CONFIG_SYS_INIT_SP_ADDR是一个配置宏通常指向SRAM的顶部或者DRAM的某个位置。在重定位之前栈一般放在SRAM里因为此时DRAM虽然初始化了但uboot代码可能还在Flash里跑用DRAM做栈需要确保DRAM稳定。栈设置好之后就可以调用C函数了。但还有一个问题BSS段需要清零。BSS段存放的是未初始化的全局变量和静态变量C语言标准要求它们初始值为0。在裸机环境下这个清零工作得自己来做。/* 清零BSS段 */ ldr r0, __bss_start ldr r1, __bss_end mov r2, #0 clear_bss: cmp r0, r1 strlt r2, [r0], #4 blt clear_bss__bss_start和__bss_end是链接脚本里定义的符号标记了BSS段的起止地址。这段循环把BSS段每个字节都写成0。注意BSS清零必须在C代码使用任何全局变量之前完成。如果顺序搞反了全局变量的初始值就是内存里的随机数据程序行为完全不可预测。我遇到过一个问题某块板子上电后某个全局标志位偶尔是1偶尔是0查了半天发现是BSS清零代码被优化掉了编译器认为那段内存没被使用。3.6 重定位把代码搬到该去的地方重定位relocation是第一阶段的最后一个大动作。前面说过uboot可能从Flash启动但最终要跑到DRAM里。重定位就是把代码从加载地址搬到链接地址的过程。重定位的代码通常在arch/arm/lib/relocate.S中核心逻辑是relocate_code: /* r0 新的代码起始地址 */ /* r1 旧的代码起始地址 */ /* r2 代码长度 */ /* 拷贝代码 */ copy_loop: ldr r3, [r1], #4 str r3, [r0], #4 cmp r1, r2 blt copy_loop /* 跳转到新地址继续执行 */ ldr pc, _main实际的重定位代码比这复杂得多因为还要处理重定位偏移量、修正绝对地址引用等问题。uboot使用了一个叫做GOT全局偏移表的机制来记录哪些地方需要修正。重定位完成后程序计数器跳转到DRAM里的_main函数第一阶段就算正式结束了。实操心得重定位是uboot调试中最容易出问题的环节之一。常见的问题包括重定位后的代码跑飞、全局变量访问错误、函数指针失效等。排查时可以在重定位前后分别打印一些关键地址确认代码确实被搬到了正确的位置。另外重定位的代码长度要算对少拷贝了或者多拷贝了都会出问题。4. 常见问题与排查技巧实录4.1 串口无输出从哪开始查串口无输出是uboot第一阶段最典型的问题。遇到这种情况不要慌按下面的顺序排查第一步确认硬件。量一下串口TX引脚有没有波形晶振有没有起振电源电压对不对。硬件问题占这类故障的一半以上。第二步确认启动介质。芯片的启动模式引脚设置对不对是从SD卡启动还是从SPI Flash启动如果启动介质选错了芯片根本不会去读uboot。第三步确认向量表。用JTAG或者调试器读一下复位向量地址的内容看看是不是b reset指令。如果不是说明uboot没被正确加载到启动地址。第四步确认时钟和串口配置。如果向量表对了但串口还是没输出可能是时钟配置错了导致波特率不对或者串口引脚复用没配置。这时候可以用示波器量TX引脚看有没有数据波形有波形但乱码就是波特率问题。第五步确认栈和BSS。如果串口能输出一部分然后卡死多半是栈溢出或者BSS没清零导致全局变量异常。4.2 常见问题速查表现象可能原因排查方法上电无任何输出启动介质错误、时钟未配置、串口引脚未复用检查启动引脚、量晶振、查引脚复用寄存器输出乱码波特率不对、时钟频率错误量TX波形算波特率、检查PLL配置输出几行后卡死栈溢出、BSS未清零、内存初始化不稳定增大栈空间、检查BSS清零代码、降频测试重定位后跑飞重定位地址错误、代码长度算错、GOT表未修正打印重定位前后地址、检查链接脚本偶尔启动失败电源不稳定、DDR时序余量不足、看门狗未关量电源纹波、放宽DDR时序、确认看门狗寄存器4.3 几个容易踩的坑坑一看门狗没关干净。有些芯片有多个看门狗或者看门狗需要先解锁才能关闭。我见过一个案例开发者只关了非安全域的看门狗安全域的看门狗还在跑结果系统跑几秒就复位。排查这种问题要仔细看芯片手册里看门狗章节确认所有看门狗都被处理了。坑二Cache没关导致内存访问异常。第一阶段早期MMU和Cache通常是关闭的。但有些芯片上电后Cache可能是开启状态或者你在代码里不小心打开了Cache。Cache开启后写内存可能只是写到Cache里没有真正写到DRAM导致后续读取数据不一致。排查时可以在关键位置插入Cache刷新操作或者确认CP15的Cache控制位。坑三栈指针设置到了未初始化的内存。栈指针指向的内存必须是可读写的。如果栈指针指向了还没初始化的DRAM区域压栈操作会失败。我建议在设置栈指针之前先往栈顶地址写一个测试值再读回来确认内存可用。坑四重定位时忘了修正函数指针。uboot里有很多函数指针比如中断向量表、驱动操作集等。重定位后这些指针还指向旧地址调用就会跑飞。uboot的GOT机制就是用来处理这个问题的但如果你自己添加了函数指针记得也要加入GOT表。5. 调试手段与工具选型5.1 点灯大法最原始但最有效在没有串口输出、没有JTAG的情况下点灯是最可靠的调试手段。在代码的关键位置插入GPIO操作点亮或熄灭LED通过LED的状态判断程序执行到了哪里。/* 点亮LED */ ldr r0, GPIO_BASE ldr r1, GPIO_LED_PIN str r1, [r0, #GPIO_SET_OFFSET]点灯大法的优点是简单直接不依赖任何外设初始化。缺点是信息量少只能判断到了或没到不能看变量值。但在第一阶段早期这已经够用了。5.2 JTAG调试单步跟踪的利器如果板子有JTAG接口那调试效率会高很多。你可以设置断点、单步执行、查看寄存器和内存。对于第一阶段这种汇编代码JTAG几乎是必备工具。用JTAG调试时我通常会在reset标号处设一个断点然后单步执行观察每一步寄存器的变化。特别是CPSR、SCTLR这些控制寄存器它们的值直接决定了CPU的行为。注意有些芯片的JTAG引脚和GPIO复用需要在启动时通过引脚配置或者eFuse设置才能启用JTAG。如果你接上JTAG发现连不上先确认JTAG功能有没有被使能。5.3 串口打印第二阶段的主力到了第二阶段串口打印就是最主要的调试手段了。printf、debug、puts这些函数可以输出变量值、执行流程等信息。uboot的串口驱动通常在drivers/serial/目录下初始化代码在board_init_f或者board_init_r中调用。串口打印的缺点是依赖串口初始化成功如果串口本身有问题那就什么都看不到。所以第一阶段早期的问题还是得靠点灯和JTAG。5.4 工具选型对比调试手段适用阶段优点缺点点灯全阶段简单、不依赖外设信息量少JTAG全阶段可单步、可看寄存器需要硬件支持、速度慢串口打印第二阶段信息量大、使用方便依赖串口初始化逻辑分析仪全阶段可看时序、可抓总线需要硬件、分析复杂6. 从第一阶段到第二阶段的交接细节6.1 _main函数的承上启下作用_main函数位于arch/arm/lib/crt0.S它是汇编和C语言的交接点。这个函数主要做三件事设置C运行环境栈、BSS、调用board_init_f、处理返回后的重定位。_main: /* 设置栈 */ ldr sp, (CONFIG_SYS_INIT_SP_ADDR) /* 清零BSS */ ldr r0, __bss_start ldr r1, __bss_end mov r2, #0 bl memset /* 调用board_init_f */ bl board_init_f /* board_init_f返回后执行重定位 */ bl relocate_code /* 跳转到重定位后的地址 */ ldr pc, board_init_rboard_init_f是第二阶段的第一个C函数它负责早期的板级初始化包括串口、定时器、环境变量等。board_init_f执行完毕后会返回一个重定位目标地址然后_main调用relocate_code把uboot搬到DRAM的高端地址最后跳转到board_init_r。6.2 全局数据结构gd和bduboot使用两个全局数据结构来传递信息gdglobal_data和bdbd_info。gd在board_init_f之前就已经在内存中预留了位置board_init_f负责填充它。bd通常在board_init_r阶段填充。gd的定义在include/asm-generic/global_data.h中包含了大量字段比如typedef struct global_data { bd_t *bd; unsigned long flags; unsigned int baudrate; unsigned long cpu_clk; unsigned long bus_clk; unsigned long relocaddr; phys_size_t ram_size; /* ... 更多字段 ... */ } gd_t;gd的地址通常存在一个固定的寄存器里比如ARM的r9这样在任何地方都能快速访问。这个设计很巧妙避免了全局变量在重定位后地址失效的问题。实操心得如果你在board_init_f里添加了自己的初始化代码记得把结果存到gd里而不是用普通的全局变量。因为board_init_f执行时uboot还在Flash里跑全局变量的地址是链接地址重定位后这些地址就失效了。gd是专门为跨重定位传递数据设计的。6.3 重定位后的地址修正重定位完成后uboot运行在DRAM里但代码里很多绝对地址引用还是旧的链接地址。uboot通过relocate_code里的GOT表来修正这些引用。GOT表在链接时生成记录了所有需要修正的地址位置。relocate_code: /* r0 新地址r1 旧地址r2 长度 */ /* 拷贝代码 */ /* ... */ /* 修正GOT表 */ ldr r3, __got_start ldr r4, __got_end fix_got: cmp r3, r4 ldrlt r5, [r3] addlt r5, r5, r0 sublt r5, r5, r1 strlt r5, [r3], #4 blt fix_got /* 跳转到新地址 */ ldr pc, _mainGOT表的修正逻辑是对于每个需要修正的地址加上新地址和旧地址的差值。这样原来指向旧地址的指针就变成了指向新地址。这个机制对开发者是透明的你不需要手动处理。但如果你在汇编里硬编码了绝对地址那重定位后就会出问题。所以uboot的汇编代码里大量使用相对跳转和adr指令就是为了避免这个问题。7. 移植与适配的实战建议7.1 从参考板到自定义板uboot移植的第一步通常是找一个最接近的参考板复制它的配置和代码然后逐步修改。这个过程中第一阶段需要改的地方主要有时钟配置根据你的晶振频率和CPU目标频率重新计算PLL参数内存配置根据你的DRAM颗粒型号和PCB设计调整DDR控制器参数串口配置根据你用的串口控制器和波特率修改串口初始化代码GPIO配置根据你的LED和按键连接修改GPIO初始化这些修改中内存配置是最容易出问题的。我的建议是先用保守的参数降低频率、放宽时序让系统跑起来然后再逐步优化。7.2 配置宏的梳理uboot有大量的配置宏分布在include/configs/目录下的头文件中。第一阶段相关的宏主要有宏名含义典型值CONFIG_SYS_TEXT_BASE链接地址0x80800000CONFIG_SYS_INIT_SP_ADDR初始栈指针0x80800000 - 0x1000CONFIG_SYS_SDRAM_BASEDRAM起始地址0x80000000CONFIG_SYS_BAUDRATE_TABLE支持的波特率{9600, 19200, 38400, 57600, 115200}CONFIG_SYS_CPUCPU类型armv7这些宏的值要和你的硬件设计对应。比如CONFIG_SYS_SDRAM_BASE必须和DDR控制器的地址映射一致否则内存访问会出错。7.3 调试顺序建议移植uboot时我建议按这个顺序调试先让串口有输出。哪怕只是几个乱码也说明CPU在跑时钟和串口基本正常。再让串口输出正确。调整波特率和时钟配置直到能看到清晰的打印信息。然后确认内存可用。在board_init_f里加一段内存测试代码读写一段DRAM确认数据正确。最后调试外设。网口、USB、MMC等外设的调试放在最后因为它们依赖内存和时钟都正常。这个顺序的核心逻辑是从最基础的CPU和时钟开始逐步向上层外设推进。每一步都确认无误后再进行下一步避免问题叠加导致排查困难。实操心得我在移植uboot时习惯在board_init_f的最开始加一句串口打印输出当前CPU频率和内存大小。这样每次启动都能看到关键信息一旦有问题一眼就能看出是哪个环节出了错。这个习惯帮我省了很多调试时间。8. 几个值得深挖的细节8.1 为什么用汇编而不是C第一阶段用汇编根本原因是C语言运行需要栈而栈需要内存内存还没初始化。这是一个鸡生蛋蛋生鸡的问题。汇编可以直接操作寄存器和物理地址不需要栈所以能在内存初始化之前运行。另一个原因是汇编可以精确控制指令的执行顺序和时序。比如DDR初始化对时序要求很严格某些操作之间需要精确的延迟用C语言很难保证编译器不会插入额外的指令。8.2 位置无关代码的实现位置无关代码PIC是指代码不管加载到哪个地址都能正确执行。ARM汇编里实现PIC主要靠相对跳转和adr指令。/* 相对跳转不管代码在哪里都能跳到label */ b label /* adr指令获取label的当前地址 */ adr r0, label /* 绝对跳转依赖链接地址 */ ldr pc, labelb和bl指令的跳转偏移是相对于当前PC的所以是位置无关的。adr指令也是基于PC计算地址同样是位置无关的。而ldr pc, label会把label的链接地址加载到PC如果代码不在链接地址运行就会跳错地方。uboot在第一阶段早期大量使用b和adr就是为了在重定位之前也能正确执行。等到重定位完成后才可以使用绝对跳转。8.3 异常向量表的两种位置前面提到过异常向量表可以在0x00000000或者0xFFFF0000。这个选择由CP15的V位控制。为什么有两个位置这是ARM架构的历史遗留设计。0x00000000是传统位置但有些系统会把0x00000000映射到RAM这样向量表可以被动态修改。0xFFFF0000是高地址位置通常映射到ROM或者固定的内存区域适合那些需要向量表固定不变的场景。uboot通常使用0x00000000因为uboot需要动态设置向量表比如在运行时修改中断处理函数。但具体用哪个还要看芯片设计。有些芯片的BootROM会占用0x00000000那uboot就只能用0xFFFF0000。8.4 Cache和MMU的早期处理第一阶段早期Cache和MMU通常是关闭的。但有些芯片上电后Cache可能是开启的或者BootROM在跳转到uboot之前开启了Cache。这种情况下uboot需要在reset之后尽快关闭Cache避免内存访问不一致。关闭Cache的代码在CP15的c1寄存器mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00000004 /* 关闭D-Cache */ bic r0, r0, #0x00001000 /* 关闭I-Cache */ mcr p15, 0, r0, c1, c0, 0关闭Cache后所有内存访问都直接到DRAM虽然慢但一致性好。等第二阶段需要性能时再重新开启Cache并配置MMU。注意关闭Cache之前如果Cache里有脏数据需要先刷新到内存。否则关闭Cache后这些数据就丢了。uboot通常在关闭Cache之前会执行一次Cache刷新操作。9. 个人实操体会读uboot第一阶段源码这件事我前前后后折腾了好几块板子才算真正搞明白。最开始是照着教程一行行看看完觉得懂了但一换板子就懵。后来才意识到光看代码不够得结合芯片手册一起看知道每个寄存器是干什么的为什么要写那个值。我踩过的最大的一个坑是DDR初始化。当时从参考板抄了一套参数板子能启动但跑memtest偶尔会报错。查了很久才发现是tRFC参数不对参考板的DRAM颗粒和我的不一样。后来把tRFC从160ns改成260nsmemtest跑了一整夜都没报错。这件事让我明白uboot移植里没有抄作业这回事每个参数都得自己算、自己验证。另一个体会是调试第一阶段的问题工具比经验重要。有JTAG的话很多问题单步跟一遍就清楚了没有JTAG只能靠点灯和串口效率差很多。所以如果条件允许尽量给板子留一个JTAG接口哪怕只是调试用。最后说一个实用技巧在board_init_f里加一段代码把gd结构体的关键字段打印出来包括CPU频率、内存大小、重定位地址等。这段打印在每次启动时都会输出一旦系统出问题看一眼这些信息就能快速定位是哪个环节的配置不对。这个习惯我保持了好几年帮我省了无数调试时间。
RELATED

相关推荐

车载终端开发实战:同时满足JT/T808与GBT19056的完整指南

车载终端开发实战:同时满足JT/T808与GBT19056的完整指南

项目一开始,我其实是被“行车记录仪”五个字带偏了的。客户说要做一台能对标平台、能导出数据、又能装车的终端,我第一反应还是老式行业记录仪的老路子——本地存曲线、锁文件、接上位机。后来翻到项目标题里的两个标准编号才反应过来,这是要…

📅 2026/10/12 13:23:53
SHAP力图Matplotlib标签重叠的解决方案与批量出图指南

SHAP力图Matplotlib标签重叠的解决方案与批量出图指南

做模型解释的时候,SHAP力图几乎是绕不开的标配,但每次调用Matplotlib渲染,我都会被同一个问题折腾一番:明明模型训练得很顺,一到出图,特征名和数值标签就挤成一团,红的蓝的色块互相叠盖&#xf…

📅 2026/10/12 13:23:53
MT-Bench人工判断评估器基准测试:用TaoToken统一Key跑通LLM评估链路

MT-Bench人工判断评估器基准测试:用TaoToken统一Key跑通LLM评估链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/12 13:18:53
MORE NEWS

更多资讯

📰

sim-use 3分钟上手:observe-act-verify循环让AI验证自己写的移动App

【免费下载链接】sim-use Give your AI agent eyes and hands on iOS Simulator and Android emulator/devices. 项目地址: https://gitcode.com/gh_mirrors/si/sim-use 点击查看 免费下载 sim-use 是一个跨平台 CLI 工具,它为 AI Agent 提供在 iOS 模拟…

📰

智谱刚把 GLM-5.3 搬上 Amazon Bedrock,股价应声涨近 5%:国产大模型出海怎么突然就火了

智谱刚把 GLM-5.3 搬上 Amazon Bedrock,股价应声涨近 5%:国产大模型出海怎么突然就火了 【免费下载链接】GLM-5.3 GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升…

📰

C++手写CSV解析器:从状态机到流式处理实战

1. 从一次数据清洗任务说起:为什么CSV值得单独写一篇文章前阵子帮一个做实验数据分析的朋友处理一批传感器日志,对方甩过来一个压缩包,解压后是几十个后缀为.csv的文件,每个文件大概几万行,列数从十几列到三十几列不等…

📰

Pomotroid 土耳其语本地化实践:基于 Paraglide 的 8 语言架构与新增 locale 全流程

【免费下载链接】pomotroid :tomato: Simple and visually-pleasing Pomodoro timer 项目地址: https://gitcode.com/gh_mirrors/po/pomotroid 点击查看 免费下载 本文以 Pomotroid 新增土耳其语(tr)支持这一真实变更为主线,讲解…

📰

openJiuwen DeepSearch 研究规划提示模板(planner/user.md)深度解析:从 Jinja 渲染到计划生成的全链路实现

人工智能大模型AI AgentRAG深度研究搜索引擎后端代码智能体 【免费下载链接】deepsearch openJiuwen DeepSearch是一款知识增强的深度搜索与研究框架,有业界领先的片段级引用和溯源推理能力,提供精准Agentic搜索与研究能力 项目地址: https:/…

📰

【宠粉赠书】分布式系统性能优化:方法与实践——用 TaoToken 统一 Key 打通压测与调优链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬