仅做为学习记录

理解嵌入式系统的启动流程和U-Boot源码,是深入嵌入式开发的关键一步。这不仅有助于解决实际启动问题,更能提升对系统底层运作的认知。作为BSP底层开发工程师,对于Uboot的学习至关重要,所以在这里以嵌入式系统为例,重点聚焦U-Boot的启动流程及其关键源码实现。

FS4412 SOC的启动过程

解析图中的内容:

Cortax A9 :其是芯片核心,也就是中央处理器——CPU。

Internal Rom :是一个只读存储器,里面存储了代码,总大小为64K。它的功能是用于读写pin脚,其作用是用来告诉系统从何处去读取uboot代码。uboot的代码可以从Nand、SD/MMC、eMMC、USB OTG等地方去启动。

也就说uboot的代码可以存储在外部的存储介质里面,通过拨动Opertaing Mode Pin(拨码开关),来选择从其中某个介质里去读取。而这些读取的驱动代码就集成在iROM之中。

其次iROM还有一个功能是读取外部的代码,将其读入内部的 Internal SRAM 之中,也就是BL1和BL2(未画)。BL代码就是我们自己写的BootLoader代码,然后程序计数器(PC指针)就会从iROM移动到Internal SRAM内部,执行我们的BootLoader代码。

(在这里简单说一下程序计数器(PC指针):PC指针存储当前正在执行指令的下一条指令地址。CPU每执行一条指令后,PC会自动递增(或根据跳转指令修改),指向新的待执行指令地址,驱动程序流程推进。当iROM将BL1/BL2代码加载到SRAM后,需要​​主动修改PC指针的值​​,将其指向SRAM中BootLoader代码的起始地址。此后CPU便从SRAM中取指令执行,实现控制权移交。)

Internal SRAM :集成在芯片内部的内存。执行SRAM的代码,也就是开始执行我们的BootLoader代码,它会完成一些硬件的初始化工作,如: 初始化时钟和 我们上面的 DramController控制器 等,这样我们才能够使用DRAM内存。接着将OS加载到DRAM之中。

DRAM :uboot初始化结束之后,会将控制器交给DRAM之中的OS,这时候我们的可访问内存空间的大小就由256K扩展到外部的DRAM大小。

在这里简单说一下Dram和Sram,以及为什么bootloader加载到SRAM而OS加载到DRAM:

 简单的说,就是因为SRAM体积小成本高,适合那种不需要初始化马上可以用的小任务,然后把Dram初始化之后,把大体积的操作系统OS整个加载到DRAM中去慢慢运行 ,这就是为什么SRAM加载bootloader而DRAM加载OS

总结一下:

  • 芯片的启动过程就是由iROM开始,读取BL到SRAM。
  • 然后执行Internal SRAM的BootLoader代码,初始化硬件,加载OS到DRAM之中。
  • 接着BootLoader将控制器交给DRAM的OS,至此整个SOC的启动流程的分析完成了。

(传统的)u-boot的启动流程

链接脚本

​ 在进行uboot的链接启动流程分析之前,我们需要先看一下uboot的链接脚本。在C语言中,程序可以分为以下几个部分:

  1. 代码段(.text)
  2. 数据段(.data)
  3. 只读数据段(.rodata)
  4. 未初始化的数据段(.bss)
  5. 堆和栈

其中堆和栈属于动态区域,在程序运行时动态分配和释放。而代码段、数据段、只读数据段在链接之后产生。

一个C语言程序分为映象和运行时两种状态:

  1. 映象只包含代码段、数据段、只读数据段;
  2. 运行时还将包含动态形成的堆和栈区域;

而链接脚本的作用就是:

  1. 指定代码段和数据段、只读数据段在内存中的存放地址;
  2. 指定代码的入口地址;

 如:uboot-2012.04.01的uboot的lds文件,其部分代码如下:

然后再给出一个他的详细注释版:

/* --- 全局设置:定义程序的基本属性 --- */
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/* 目标文件格式:ARM架构的32位小端ELF格式 */
/* 三个相同参数表示无论默认/大端/小端模式都使用此格式 */

OUTPUT_ARCH(arm) 
/* 目标处理器架构:ARM系列芯片 */

ENTRY(_start)  
/* 程序入口点:_start符号(在start.S中定义)*/
/* CPU上电后第一条执行的指令地址 */
/* --- 内存布局定义开始 --- */
SECTIONS
{
    /* 当前位置计数器置零(虚拟地址基准)*/
    /* 实际运行时会被加载到物理地址(如SRAM的0x02020000)*/
    . = 0x00000000;
    
    /* 4字节对齐(ARM体系结构要求)*/
    . = ALIGN(4);  // 确保后续代码从4倍数地址开始
    
    /* --- .text段:存放所有可执行代码 --- */
    .text : 
    {
        /* 关键符号:镜像复制起始地址 */
        __image_copy_start = .;  
        /* 在重定位时计算源地址偏移: */
        /* 实际地址 = __image_copy_start + (物理加载地址 - 0x0) */
        
        /* 强制特定文件顺序(启动依赖)*/
        CPUDIR/start.o (.text)          // 1. CPU核心初始化代码
        board/samsung/smdk2440/libsmdk2440.o (.text) // 2. 开发板特定初始化
        *(.text)                        // 3. 其他所有目标文件的代码段
    }
    /* --- 段间分隔符 --- */
    . = ALIGN(4);  // 4字节对齐分界
    
    /* --- .rodata段:存放只读数据 --- */
    .rodata : {
        /* 优化指令:双重排序保证内存紧凑 */
        *(SORT_BY_ALIGNMENT(  // 先按对齐要求排序(避免内存空洞)
          SORT_BY_NAME(.rodata*)  // 再按名称排序(提高缓存命中率)
        )) 
        /* 包含:字符串常量、只读全局变量等 */
    }
    
    /* --- 段间分隔符 --- */
    . = ALIGN(4);  // 4字节对齐分界
    
    /* --- .data段:已初始化的全局变量 --- */
    .data : {
        *(.data)  // 如:int debug_enabled = 1;
    }
    
    /* --- 段间分隔符 --- */
    . = ALIGN(4);  // 4字节对齐分界
    . = .;         // 显式声明当前位置(增强可读性)
    
    /* --- U-Boot命令表专用段 --- */
    /* 符号标记:命令表起始位置 */
    __u_boot_cmd_start = .;  
    .u_boot_cmd : { 
        *(.u_boot_cmd)  // 收集所有U_BOOT_CMD宏定义的命令结构体
    }
    /* 符号标记:命令表结束位置 */
    __u_boot_cmd_end = .; 
    
    /* 命令表在U-Boot中的应用:*/
    /* for(cmd = __u_boot_cmd_start; cmd < __u_boot_cmd_end; cmd++) */
}

这里简要说明一下:

从上面的代码我们可以看到有**.text段,其指定了所有uboot文件的.text段的存放位置**。往下是**.rodata段,其指定只读数据的存放位置**,紧跟着.text段,并且进行了4字节的对齐操作。再往下是数据段.data的存放位置。其中有一个__u_boot_cmd_start,它充当一个标号的作用,其指定了boot_cmd的起始地址,cmd也就是我们在boot的shell中使用的那些指令,而__u_boot_cmd_end指定的是结束地址。

uboot程序的入口地址,在第3行的  ENTRY(_start)  处指定了 

uboot代码追踪

我们可以使用 arm-linux-nm u-boot 显示我们生成的uboot文件的符号信息。节选部分如下:

有了这个命令后,我们就可以很方便的查找到我们的uboot启动代码的地址, 通过 arm-linux-nm u-boot | grep _start,我们找到我们的起始地址 ,其输出信息如下面: 

 

知道了地址,我们怎么定位文件的位置呢?

可以通过 arm-linux-addr2line 来定位,其可以将地址转成行号输出显示出来

我们 可以通过 arm-linux-addr2line -e u-boot 33f80000 找到我们起始地址所在的文件,以及其在文件中的位置 ,输出如下:

上图为uboot的启动流程代码:

  1. 首先会先执行start.S,其主要完成的工作将CPU设置SVC模式(保护模式)、关闭中断看门狗等;
  2. 接着会执行lowlevel_init.S的代码,主要完成一些列初始化工作;
  3. 接着开始进入C语言程序,在进行C语言之前需要先开辟一个栈空间,由ctr0.S来完成;
  4. 进入C语言程序后,执行board_init_f()函数,进行自搬移操作前期的内存分配;
  5. 自搬移由crt0.S来完成,然后第二次初始化C语言的运行环境,接着程序就完全进入C语言环境执行;
  6. board_init_r()会进行MMC和网络初始化,然后进入自启动模式,如果在倒计时之前按下回车就进入main loop循环,也就是我们看到的uboot的shell界面。

启动流程解析

用通俗易懂的语言对上面的图和文字做更加详细的解释就是:

​1️⃣ 临时工棚阶段(汇编裸奔)​

相当于在工地旁搭临时帐篷干活

  • ​做什么​​:
    • start.S:给CPU保安戴工牌(设保护模式)→ ​​关大门避免干扰​​(关中断)
    • lowlevel_init.S:指挥水电工布线(时钟)、挖地基(内存)、接水管(串口)
  • ​为什么​​:

    刚通电时系统像毛坯房,连桌椅都没有(内存未初始化),只能在狭小的工棚(SRAM)里做最基础的准备工作。

​2️⃣ 搭临时办公点(C语言初体验)​

在工棚里搭建临时办公室

  • ​做什么​​:
    • crt0.S:搬几个塑料凳当座位(栈空间)→ ​​勉强能开小组会​
    • board_init_f():规划新房图纸(计算搬运路线)
  • ​关键限制​​:

    工棚又小又挤(SRAM只有256KB),无法展开大型家具(完整U-Boot),必须先为新家(DRAM)做规划。

​3️⃣ 新房装修(惊天大搬运)​

把整个公司搬到新大楼

  • ​核心操作​​:自搬移 (crt0.S流程)
    1. 把公司所有文件(U-Boot代码)​​整箱打包​​复制到新地址
    2. 修正所有文件标签(重定位:更新函数地址)
    3. 在新大楼重建办公室(第二次初始化C环境)→ ​​这次有真皮沙发(完整栈)​
    4. 大扫除清空地下室(.bss段清零,全局变量归零)

​4️⃣ 正式营业(迎接用户)​

新公司开张迎客

  • ​做什么​​:
    • board_init_r():布置各部门
      💾 安装文件柜(MMC存储)
      🌐 开通网络专线(网卡驱动)
      🔌 调试电力系统(设备树解析)
    • ​最终状态​​:
      倒计时5秒...  # 自启动模式
      按回车进入→ [U-Boot] # 您看到的命令行

​为什么需要两次C语言初始化?​

​第一次(临时工棚)​ ​第二次(新房大楼)​
❗空间限制:SRAM仅256KB ✅ 解放空间:DRAM有512MB+
⚠️ 只能放折叠椅(基本栈空间) 🛋️ 全套办公家具(完整栈+堆)
🧹 没时间打扫(不处理BSS段) 🧼 全面保洁(BSS段清零)

相当于:工棚会议只能站着开会(第一次初始化),搬进写字楼才能开正式董事会(第二次初始化)


​自搬移:嵌入式系统的魔术​

  • ​为什么必须搬​​?
    😱 工棚就在新房地基上(SRAM地址被内核占用),不搬就会被推土机压扁!
  • ​如何无伤搬运​​:
    1️⃣ 工头记住所有文件位置(__image_copy_start标记起点)
    2️⃣ 克隆文件到新地址(memcpy整段复制)
    3️⃣ 更新所有名片地址(重定位:旧地址+偏移量=新地址

​最终视图:U-Boot启动全貌​

我用自己的话再说一遍Uboot 的启动流程:芯片上电的时候,CPU从start.C开始执行,首先把CPU设置成SVC模式,并且关闭中断,看门狗等功能,是因为防止在启动期间CPU被打扰。然后,会执行lowlevel_init.S代码,对一些基本的外设进行初始化(时钟,内存,串口等)。然后运行crt0.S代码,分配一个小小的栈空间,为的是能开辟一个小小的空间给C代码运行。然后运行board_init_f(),运行board_init_f()是为了自搬移做一些前期的准备(具体工作是定义在DRAM中所需要的大小,起始的地址)。接下来就是继续运行crt0.S代码,对uboot程序进行自搬移,从SRAM转移到DRAM中去执行。然后crt0.S在DRAM中会开辟一个完整的堆栈,给C程序(board_init_r())进行完整的运行(对设备完整进行初始化,加载驱动,加载设备树),并且清除掉.bss文件,到这里uboot启动程序就大致完成了,进入自启动或命令行状态 

uboot源码分析 

了解了整个uboot大致的启动流程后,我们就可以开始着手进行uboot的源代码分析了。我们从start.S文件开始分析,也就是uboot的入口地址_start,其节选代码如下:

在第3行有一条跳转指令 b reset 跳转到reset标号处去执行,在reset下面的那些 ldr xxxx 指令是用于后面设置中断向量表使用的。reset标号处的代码如下: 

从注释中我们就可以看到,其作用就是关闭中断,将CPU设置为SVC32模式。(reset

接着_start的代码往下走:

 

从注释中我们可以看到,行2~14大致完成内容是设置SCTRL寄存器,也就是系统控制器 以及设置中断向量表 ,这个中断向量表的内容就是我们前面在reset下面看到的那些 ldr xxxx 指令,这个需要借助CPU的协处理器CP15进行设置,具体细节不深究。

/* 相当于给120/110设置快速拨号 */
mrc ...       // 解锁CPU的隐藏控制面板
bic r0, #CR_V // 关掉原厂默认的急救电话
mcr ...       // 确认操作

ldr r0, =_start // 把我们的急救电话簿(_start位置)
mcr p15,0,r0... // 刻到CPU的快速拨号键(VBAR寄存器)

完成上面操作后, 会跳转到cpu_init_cp15和cpu_init_crit标号执行,最后转到_main函数去执行 

bl cpu_init_cp15 // 医生检查CPU核心状态
bl cpu_init_crit // 检测心跳(时钟)、呼吸(内存)

我们一步一步进行分析,先看cpu_init_cp15标号处的代码:

从代码的注释我们可以看出, 该标号处的代码主要完成关闭缓存、关闭虚拟内存MMU的作用 ,以及设置了diagnostic寄存器。

 

接着我们回到reset处的cpu_init_crit继续往下追踪:

cpu_init_crit处的代码很简单,就是调用lowlevel_init进行初始化 

由流程图和注释我们可以知道, lowlevel_init主要完成板级相关的一些初始化工作,如:时钟、内存、网卡、串口的初始化 

​BSP工程师(Board Support Package工程师)在移植U-Boot时通常需要编写或修改lowlevel_init.S文件,以适配具体硬件的外设初始化需求​​。

 

接着我们回到reset处的_main继续往下追踪。_main标号定义在文件arch/arm/lib/crt0.S里,可以通过arm-linux-nm和arm-linux-address2line进行定位。

下面我们对_main进行追踪:

 

 从注释里我们可以看出, _main首先初始化了一下C的运行环境,接着调用board_init_f函数 。除此以外进行sp寄存器的设置,也就是第10行处的代码,还有在栈空间里为 GD 变量分配了内存。

接着我们继续追踪board_init_f:

其中有两个最重要的全局变量 bd_t *bd 和 gd_t *id 它们用于存储一些信息,这两个全局变量的存储空间就是在调用board_init_f之前已经在栈中进行分配了。

board_init_f主要完成两个工作,一是对 gd_t *id 进行初始化,二是它会调用一些列的函数(在第10行处的代码)完成一些列工作。 GD (也就是 gd_t *id )主要是记录一些地址信息,用于后续的自搬移操作使用。

用自己的话总结:所以说bd是计算Uboot需要的内存大小,gd是规划uboot在DRAM中的目标地址,这两个变量都是在第board_init_f中进行定义的,所以说这个函数至关重要,因为只有进行了这个函数才可以进行下一步的uboot自搬移 

接着我们回到_main,接着board_init_f继续往下看:

在最后一行有一个 b relocate_code 跳转指令,从名字我们就可以知道,其完成的工作就是对代码进行自搬移操作,接着我们继续往下看: 

自搬移结束后,就准备进入C语言环境运行了。 

从注释中可以看到设置了bss段,也就是对bss段进行了清零,标号 clbss_l 标号处的循环代码。

之后就会通过board_init_r标号,跳转到C语言函数,我们继续追踪board_init_r:

board_init_r会打印一些列的信息到串口,然后会进入main_loop()循环,main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。

 总结

 

uboot的启动如下,假设我们使用SD卡进行启动:

  1. iROM会将SD卡内的BL1加载到iRAM中,然后将控制器交给BL1;

  2. BL1会将BL2以及填充,一共16K的代码,加载到iRAM中;

    uboot由两部分组成,一部分是SPL完成硬件相关操作,另一部分是uboot代码。

  3. 接着SPL部分会将uboot代码加载DRAM之中,此时uboot位于DRAM的内存地址0x43E00000处;

  4. uboot加载需要加载Linux系统的uImage到内存,一般加载到0x4008000处,为了防止把uboot自己给覆盖了,所以uboot还需要进行一次自搬移操作;

  5. 然后CPU跳转新的uboot地址执行,这时我们就能看到串口打印一些列控制信息了。


一、嵌入式系统启动流程精要

1. ​​芯片级启动(iROM阶段)​
  • ​硬件基础​​:
    • iROM固化64KB代码(厂商提供)
    • 通过拨码开关选择启动介质(SD/eMMC/NAND等)
  • ​核心任务​​:
    • 加载BL1(SPL/U-Boot头部)到SRAM
    • 移交执行权:PC指针跳转至SRAM入口
2. ​​SRAM阶段(BootLoader初始化)​
  • ​关键操作​​:
    • 时钟/内存控制器初始化(使能DRAM)
    • 串口初始化(建立调试通道)
    • 加载完整U-Boot到DRAM
  • ​空间限制​​:
    • SRAM仅256KB → 仅存放核心初始化代码
3. ​​DRAM阶段(操作系统引导)​
  • ​两阶段过渡​​:
    1. U-Boot自搬移(避免被内核覆盖)
    2. 设备树/内核加载到DRAM
  • ​终点​​:移交控制权给Linux内核

​启动流程关键函数​

​函数​ 执行位置 核心职责 技术亮点
start.S SRAM 关中断/SVC模式/关MMU 协处理器操作(mcr/mrc)
lowlevel_init.S SRAM 时钟/内存/串口初始化 BSP工程师定制点
board_init_f() SRAM 规划自搬移地址(gd/br结构体) 栈空间预分配技术
relocate_code() SRAM→DRAM 代码自搬移+地址重定位 动态修正函数指针
board_init_r() DRAM 初始化网卡/存储/加载内核 无限循环容错设计

至此uboot的启动流程大致分析就结束了。

    Logo

    openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

    更多推荐