U-boot 移植及源码分析( ARM )
- 在s5p6818上移植uboot并分析其启动过程
1. 系统移植
1. 嵌入式Linux构成与移植概述
- 嵌入式Linux移植定义: 移植就是将Bootloader的源代码、Linux内核源代码、文件系统中用户态程序代码根据硬件做少量修改,使其能够在目标硬件平台上运行起来的过程。
- Linux内核功能及本质:
- 功能: 进程管理、文件系统、内存管理、网络协议。
2. Bootloader定义及重要性
- 定义: Bootloader是在操作系统内核运行之前运行,初始化硬件设备、建立内存空间映射,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。
- 为什么需要Bootloader:
- 在嵌入式系统中,Bootloader是一段短小的启动程序,因为没有BIOS那样的固件程序,因此整个系统的加载启动任务就完全由Bootloader来完成。
- 使Linux内核可以在系统主存中跑起来,系统必须符合Linux内核启动的必要条件。
3. Bootloader、Kernel、应用程序之间的关系
- Bootloader:
- 硬件上电后跳到固定位置执行相应代码
- 初始化相应的硬件设备
- 加载操作系统内核代码到内存
- 跳到内核代码起始位置执行
- Kernel (uImage):
- 内核自解压 (uImage)
- 初始化相应的硬件设备
- 初始化静态编译进内核的驱动模块
- 挂载根文件系统
- 直接执行第一个用户空间程序
- 第一个用户空间程序: 配置用户环境和执行服务进程
2. 开发板烧写并启动(先对emmc进行分区,了解整个系统在emmc上的启动过程)
1. emmc分区布局
1 |
|
2. 假设当前通过软件烧录uboot后启动在uboot中,(uboot支持emmc操作)
- 查看emmc信息
mmc list / mmc dev 0 / mmc info
- 按需擦除数据
mmc erase 0 1000000
- 分区(help fdisk)
- fdisk
[part table counts] start:length …. fdisk 2 3 0x100000:0x4000000 0x4100000:2f200000 0x33300000:0
// 从bootloader地址之后开始分区的话
- fdisk
- 通过uboot中的 loadaddr 和 bdinfo 查看DRAM信息, 然后重新烧写uboot
tftp 0x48000000 ubootpak.bin
- Bytes transferred = 342960 (53bb0 hex)
update_mmc 2 2ndboot 0x48000000 0x200 53bb0
- 重启进入uboot
- re
- Linux内核烧写
tftp 48000000 uImage
mmc write 48000000 0x800 0x3000
(写入的扇区位置和数量)
3. 配置NFS服务器
rootfs一般都是从网络启动和挂载,因为方便调试,主机需要配置NFS服务器。
服务端配置
- 编辑
/etc/exports
文件:sudo vi /etc/exports
- 添加NFS共享目录配置:
/opt/rootfs *(rw,sync,no_root_squash)
rw
: 读写权限。sync
: 数据同步写入磁盘。no_root_squash
: 客户端以root身份访问文件。
- 重启NFS服务使配置生效:
sudo /etc/init.d/nfs-kernel-server restart
- 编辑
客户端配置 (U-boot环境变量)
- 设置
bootargs
:setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs ip=192.168.1.6:192.168.1.8:192.168.1.255:255.0 console=ttySAC0 maxcpus=1 lcd=wy070ml tp=gslx680-linux
root=/dev/nfs
: 指定根文件系统类型为NFS。nfsroot=192.168.1.8:/opt/rootfs
: NFS服务器IP地址和共享目录。ip=...
: 开发板的IP地址配置。console=ttySAC0
: 指定控制台输出设备。
- 设置
3. U-boot源码分析
1. U-boot
- U-Boot(Universal Boot Loader)是一个开源、跨平台的 Bootloader,主要用于嵌入式系统启动。它独立于操作系统,运行在裸机环境,支持多种 CPU 架构(ARM、PowerPC、MIPS、x86 等)和操作系统(Linux、VxWorks 等)。
- 特点:模块化设计,易于移植;支持多种存储、外设和网络协议;提供丰富的命令行操作。
2. U-boot编译
- 清理环境(防止残留配置):
- make distclean
- 加载目标板默认配置:
- make
_defconfig - 示例:make x6818_defconfig
- make
- 可选修改配置(菜单界面):
- make menuconfig
- 编译(指定交叉编译器):
- make CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)
- 查看产物:
- u-boot:ELF 格式,可调试,为可执行的ELF格式文件。
ubootpak.bin
为u-boot
通过objcopy
转换得到的二进制文件(用于烧写)。- u-boot-dtb.bin:带设备树版本(部分平台)。
3. 入口点文件分析
编译链接过程 (make V=1)
当执行 make V=1
时,可以看到详细的编译链接过程。例如:make -f scripts/Makefile.build obj=examples/standalone arm-cortex_a9-linux-gnueabi-ld -pte --gc-sections -Bstatic -Ttext 0x43C0000 -o u-boot u-boot.lds arch/arm/cpu/slsiap/start.o --start-group arch/arm/cpu/slsiap/built-in.o arch/arm/cpu/slsiap/s5p6818/built-in.o arch/arm/cpu/slsiap/s5
这里关键的是 -Ttext 0x43C0000 -o u-boot u-boot.lds
,它指定了U-boot的起始地址和链接脚本。
u-boot.lds
链接脚本
链接脚本定义了U-boot各段在内存中的布局。
1 |
|
. = 0x00000000;
: 指定代码的加载起始地址为0x00000000 (通常是内存或Flash的起始地址)。arch/arm/cpu/slsiap/s5p6818/start.o (.text*)
: 这表明start.o
文件中的.text
段(代码段)是U-boot的第一个执行部分。
start.S
(入口点汇编文件)
start.o
是由start.S
汇编文件编译而来,其中包含了U-boot的真正入口点。
1 |
|
_start
: 这是U-boot的入口点符号。b reset
: 上电后CPU执行的第一个指令,直接跳转到reset
函数。reset
函数是U-boot初始化过程的起点。
4. U-boot启动过程
- U-boot启动分为两个主要阶段:
阶段一: 最底层初始化 (汇编代码)
U-Boot 启动的第一阶段:从复位到 C 运行环境
- 异常向量表设置
- CPU 模式切换
- 缓存与 MMU 初始化
- 代码重定位
- BSS 段清零
- 栈和全局数据结构(gd)初始化
- 跳转到 board_init_f()(进入 C 语言环境)
start.S主要代码分析:
- 异常向量表设置
ARM 处理器上电后,从 0x00000000 开始取指(或由 VBAR 指定的基地址),所以必须先建立异常向量表。
1 |
|
定义 _stext 作为代码的起始地址
ARM 异常类型:reset, undefined, SWI, prefetch abort, data abort, IRQ, FIQ
- 通过 ldr pc, xxx 跳转到对应处理函数地址
- 这一步的目的:保证 CPU 出现异常不会直接崩溃,有跳转入口。
- ARM 架构规定,当某种异常发生时,硬件会自动做两件事:
- 切换到对应的异常模式(如 Supervisor、IRQ、FIQ)。
- 把 PC 设置到异常向量表的固定偏移地址(通常是 0x00000000 或者 VBAR 里的值)。
- 例如:
- 发生 Reset → PC = 0x00000000 → 执行 b reset
- 发生 Undefined → PC = 0x00000004 → 执行 ldr pc, _undefined_instruction
- 发生 IRQ → PC = 0x00000018 → 执行 ldr pc, _irq
- 例如:
b handler 是 相对跳转,需要 handler 在当前 32MB 范围内。
ldr pc, _xxx 是 绝对跳转,通过取一个常量地址,可以跳到 U-Boot 任意地方。
这样设计是为了灵活,后面如果重定位代码,这个表不用改,直接改 _xxx 变量的值。
- reset:从复位到基本初始化
reset 是启动的第一个执行函数,主要工作:
- 保存启动参数
- 切换到 SVC 模式
- 关闭看门狗
- 初始化 CPU 关键寄存器
- 如果需要,跳到低级硬件初始化
1 |
|
关键点:
- cpsr 切换 CPU 模式为 SVC(超级用户模式),同时关中断
- 关闭看门狗,避免启动过程中被复位
- cpu_init_cp15:处理 CP15 寄存器,关闭 MMU,invalidate I/D cache
- cpu_init_crit:执行板级初始化(比如 DRAM 控制器),确保后面可以使用内存
- 代码重定位(Relocation)-> 如果平台 开启 CONFIG_RELOC_TO_TEXT_BASE
很多 ARM 平台启动时,U-Boot 最初加载在 NOR Flash 或 SRAM,执行地址并不是最终运行地址。
为了能正确访问全局变量、函数指针,必须把 U-Boot 拷贝到 DRAM 的 TEXT_BASE。
1 |
|
- 比较当前执行地址和目标地址(TEXT_BASE)
- 如果不一样,循环拷贝整个 U-Boot 代码段
- 最后 mov pc, r1,跳转到 DRAM 里的代码继续执行
- 清空 BSS 段
BSS 段用于存放未初始化的全局变量,必须清零。
1 |
|
- 把 BSS 段所有字节清 0
- 避免后面 C 代码访问到脏数据
- 初始化栈和 GD(global data)结构
C 代码需要栈,U-Boot 还需要 gd_t 全局数据结构保存运行状态。
1 |
|
- CONFIG_SYS_INIT_SP_ADDR:通常是 DRAM 高地址(安全)
- GD_SIZE:U-Boot 的全局数据结构
- r9(IP 寄存器)专门存放 gd 指针
- board_init_f函数执行,跳转到 board_init_f 所在的board_f.c 文件中
board_init_f 阶段
- 运行位置:Flash 或 SRAM(未 relocation 前)
- 作用:
- 初始化 PLL/时钟。
- 初始化 DDR 控制器(重点)。
- 初始化 串口(用于输出 early log)。
- 计算 relocation 参数:
- gd->relocaddr:U-Boot 在 DRAM 的目标地址。
- gd->start_addr_sp:新的栈顶地址(在 DRAM)。
- gd->new_gd:新全局数据区域(DRAM)。
- 调用 jump_to_copy() → relocate_code()。
relocate_code 阶段
- 运行位置:仍然在 Flash/SRAM(搬家前)。
- 作用:
- 将 .text、.data 从 当前位置 复制到 gd->relocaddr(DRAM)。
- 清空 .bss 段(DRAM)。
- 切换栈到 DRAM(sp = gd->start_addr_sp)。
- 跳转到 board_init_r 的地址(现在在 DRAM 中)。
这一步才是真正把 U-Boot 搬到 DRAM 并切换执行位置。
- board_init_r函数执行,跳转到 board_init_r 所在的board_r.c 文件中
运行位置:DRAM(relocation 完成)。
作用:
cif (initcall_run_list(init_sequence_r)) hang();
- 初始化全局变量。
- 初始化设备驱动(I2C、SPI、USB、存储设备)。
- 初始化环境变量(env)。
- 初始化控制台。
- 调用 run_main_loop() 进入命令行。
for (;;) main_loop(); // to common/Main.c
common/main.c
1 |
|
- cli_loop时执行输入命令的核心函数(详解)
1 |
|
核心为:
- 调用命令处理函数:根据 argv[0] 查找命令表 cmd_tbl_t, 找到后调用对应的执行函数,比如 do_bootm()
- cmd_process(flag, argc, argv, &repeatable, NULL)
cmdtp = find_cmd(argv[0]); /* Look up command in command table */
for (cmdtp = table; cmdtp != table + table_len; cmdtp++) { if (strncmp (cmd, cmdtp->name, len) == 0) { if (len == strlen (cmdtp->name)) return cmdtp; /* full match */ cmdtp_temp = cmdtp; /* abbreviated command ? */ n_found++; } }
- if (argc > cmdtp->maxargs) /* found - check max args */
- rc = cmd_call(cmdtp, flag, argc, argv); /* If OK so far, then do the command */
- // 直接调用命令函数指针,相当于执行 do_bootm(cmdtp, flag, argc, argv)
- result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
U-Boot bootm 命令流程解析
bootm 命令是 U-Boot 用来启动 Linux 内核或其他操作系统映像的入口命令。内部核心是 do_bootm(),它会依次执行几个状态,每个状态对应不同的操作。
1 |
|
- BOOTM_STATE_START → bootm_start()
- 初始化启动上下文 (bootm_headers 等结构体)。
- 检查镜像类型(uImage、FIT、Legacy 等)。
- 验证镜像合法性(如 magic number、checksum)。
- 确定后续加载流程是否需要解压。
关键点:
- 这里是整个 bootm 流程的“起点”,决定后续镜像解析路径。
- 如果启动 FIT 镜像,还会解析 FIT 描述符。
- BOOTM_STATE_FINDOS → bootm_find_os() → boot_get_kernel()
- 找到操作系统内核镜像在内存中的位置。
- 设置内核映像地址(image->load)和类型。
- 对不同内核格式(zImage、uImage)做处理:
- uImage:提取 header,获取 load address。
- zImage:直接获取 load 地址,通常包含自解压代码。
关键点:
- 这个阶段不执行实际加载,只是确定内核在哪,大小是多少。
- 对于 FIT 镜像,会解析 kernel 节点及其 load 地址。
- BOOTM_STATE_FINDOTHER → bootm_find_other()
- 查找其他启动必需镜像:
- ramdisk(initrd/initramfs)
- FDT(Device Tree Blob,硬件描述)
- 根据镜像类型和配置设置内存地址。
- 为内核启动准备参数。
关键点:
- 确保内核启动时能够找到根文件系统和硬件信息。
- FIT 镜像会在这里解析 DTB 节点、ramdisk 节点。
- BOOTM_STATE_LOADOS → bootm_load_os() → decomp_image()
- 将内核镜像从存储介质(flash、SD、TFTP 等)加载到内存。
- 如果内核是压缩的(如 gzip/zImage/uImage),调用 decomp_image() 解压到目标地址。
- 同时加载 ramdisk 和 FDT 到内存指定位置。
关键点:
- 这里是真正把内核和相关资源搬到内存执行区的步骤。
- decomp_image() 内部处理压缩算法和解压回调。
- BOOTM_STATE_OS_GO → bootm_os_get_boot_func() → 跳转内核
- 获取内核启动入口(kernel_entry)。
- 根据内核类型和架构,设置启动参数(如 ATAGs、FDT 指针、ramdisk 地址)。
- 调用函数指针,真正跳转到内核执行。
关键点:
- U-Boot 在这里退出,CPU 权限切换到内核环境。
- 后续流程由内核接管。
- 初始化本阶段要使用的硬件设备。
- 检测系统内存映射。
- 将内核映像和根文件系统映像从Flash读到RAM空间。
- 为内核设置启动参数。
- 调用内核。
系统上电后的完整流程 (示意):系统上电
-> 设置为SVC工作模式
-> 关闭看门狗
-> 清空CACHE
-> 禁止MMU
-> 清空BSS段
-> 一系列硬件的初始化
-> 执行bootcmd中的命令 (加载linux内核)
-> 设置为SVC工作模式
-> 检查CPUid是否支持
-> 创建页表
-> 开启MMU
-> 创建子线程
-> 子线程中挂载指定的根文件系统
-> 启动用户空间1号进程
-> 开启后续用户空间进程
-> 启动一个shell
-> 用户可以输入命令
imx6ull Uboot 启动详细函数调用流程
1 |
|
图示
U-boot 移植及源码分析( ARM )