u-boot分析与使用

零、U-boot使用

uboot 命令中输入的数字均为十六进制,而非十进制,注意!!!

0.1信息查询命令

buinfo:查看板子信息

printenv:查看环境变量(直接输入print也行)

version:查看uboot版本号

0.2环境变量相关操作

  • setenv:新建、删除、设置或者修改环境变量的值

    1
    setenv [变量名] [变量值]

    如果变量值中有空格,可以用单引号'括起来:

    1
    setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
  • saveenv:保存修改后的环境变量

0.3内存操作

md

内存查看命令,用于显示内存值

1
2
3
md[.b, .w, .l] address [# of objects]
#eg:
md.b 80000000 10

命令中的[.b .w .l]对应 byte、 word 和 long,即分别以 1 个字节、 2 个字节、 4 个字节来显示内存值。 address 就是要查看的内存起始地址, [# of objects]表示要查看的数据长度,这个长度和你所选择的显示格式有关。

nm

修改指定地址的内存值

1
2
3
nm[.b, .w, .l] address
#eg:
nm.b 80000000

同样以.b、 .w 和.l 来指定操作格式。 address 就是要修改的内存起始地址。 值得注意的是该命令修改完后地址不会自增

mm

修改指定地址的内存值

1
2
3
mm[.b, .w, .l] address
#eg:
mm.b 80000000

与nm命令类似,区别就是修改完后地址会自增

mw

使用一个指定的数据填充一段内存

1
2
3
mw[.b, .w, .l] address value [count]
#eg:
md.b 80000000 fe 10

以.b、 .w 和.l 来指定操作格式。 address 表示要填充的内存起始地址,value为要填充的数据, count是填充的长度。

cp

数据拷贝命令,用于将 DRAM 中的数据从一段内存拷贝到另一段内存中,或者把 Nor Flash 中的数据拷贝到 DRAM 中

1
2
3
cp[.b, .w, .l] source target count
#eg:
cp.b 80000000 80000100 10

以.b、 .w 和.l 来指定操作格式。source 为源地址, target 为目的地址, count 为拷贝的长度

cmp

比较命令,用于比较两段内存的数据是否相等

1
2
3
cmp[.b, .w, .l] addr1 addr2 count
#eg:
cmp.b 80000000 80000100 10

以.b、 .w 和.l 来指定操作格式。addr1 为第一段内存首地址, addr2 为第二段内存首地址, count 为要比较的长度。

0.4网络操作

该部分命令需要有以下环境变量的支持:

1
2
3
4
5
6
7
8
9
10
11
#板卡ip地址
setenv ipaddr 192.168.1.106
#板卡MAC地址
setenv ethaddr 00:34:9f:21:a0:56
#网关地址
setenv gatewayip 192.168.1.1
#子网掩码
setenv netmask 255.255.255.0
#服务器ip地址
setenv serverip 192.168.1.100
saveenv

ping

验证网络是否正常,不做过多解释

注意!只能在 uboot 中 ping 其他的机器,其他机器不能 ping uboot(uboot 没有对 ping命令做处理,如果用其他的机器 ping uboot 会失败)

dhcp

dhcp会自动获取 IP 地址,同时还会通过 TFTP 来启动 linux 内核,输入? dhcp可查看uboot中 dhcp 命令详细的信息,这里一般用于自动获取ip地址

nfs

通过nfs(Network File System,网络文件系统)使得板卡能够直接从服务器(服务器需要提前布置好NFS服务与NFS文件目录)上下载相关资源到DRAM中(如zImage、文件系统、设备树等)

1
2
3
4
nfs [loadAddress] [[hostIPaddr:]bootfilename]
#eg:
nfs 80800000 192.168.1.100:/home/null/zImage
nfs 80800000 /home/null/zImage#如果环境变量中存在serverip

其中 loadAddress 是要保存的 DRAM 地址, [[hostIPaddr:]bootfilename]是要下载的文件地址(需要输入服务器中的完整路径)

tftp

与nfs类似,均用于通过网络下载文件到 DRAM 中(服务器需要提前布置好TFTP服务与TFTP文件目录)

1
2
3
4
tftp [loadAddress] [[hostIPaddr:]bootfilename]
#eg:
tftp 80800000 192.168.1.100:zImage
tftp 80800000 zImage#如果环境变量中存在serverip

loadAddress 是 文 件 在 DRAM 中 的存 放 地 址 ,[[hostIPaddr:]bootfilename]是要从 Ubuntu 中下载的文件(这里不需要输入服务器中的完整路径,直接从TFTP文件目录中获取)

0.5EMMC和SD卡操作

mmc info

mmc info命令和mmcinfo命令相同,均为输出当前选中的设备信息,不做过多解释

mmc rescan

用于扫描当前开发板上所有的 MMC 设备,包括 EMMC 和 SD 卡

mmc list

查看当前开发板一共有几个 MMC 设备,通过该命令可以查看当前选中的设备(一般来说0为SD卡,1为EMMC)

mmc dev

切换当前 MMC 设备

1
2
3
mmc dev [dev] [part]
#eg:
mmc dev 0 # 切换到设备0(一般为SD卡,通过mmc list可以查看)的分区0

[dev]用来设置要切换的 MMC 设备号,[part]是分区号(可以不写,默认为分区 0)

mmc part

查看选中的设备对应的分区信息

mmc read

读取当前选中 mmc 设备的数据

1
2
3
mmc read addr blk# cnt
#eg:
mmc read 80800000 600 10 # 从选中设备的第 1536(0x600)个块开始,读取 16(0x10)个块的数据到 DRAM 的0X80800000 地址处

addr 是数据读取到 DRAM 中的地址, blk# 是要读取的块起始地址(十六进制),一个块是 512字节,这里的块和扇区是一个意思,在 MMC 设备中我们通常说扇区, cnt 是要读取的块数量(十六进制)。

mmc write

将数据写入到当前选中 mmc 设备中

1
2
3
mmc write addr blk# cnt
#eg:
mmc write 80800000 600 10 # 从 DRAM 的0X80800000 地址处写入 16(0x10)个块的数据到选中设备的第 1536(0x600)个块开始处

addr 是数据写入到 DRAM 中的地址, blk# 是要写入的块起始地址(十六进制),一个块是 512字节,这里的块和扇区是一个意思,在 MMC 设备中我们通常说扇区, cnt 是要写入的块数量(十六进制)。

mmc erase

擦除 MMC 设备的指定块,最好别用

1
mmc erase blk# cnt

blk# 为要擦除的起始块, cnt 是要擦除的数量。

0.6FAT文件系统操作

需要在 uboot 中对 SD 卡或者 EMMC 中存储的文件进行操作时需要用到该部分操作命令(只支持FAT格式)

fatinfo

查询指定 MMC 设置指定分区的文件系统信息

1
2
3
fatinfo <interface> [<dev[:part]>]
#eg:
fatinfo mmc 1:1 # 查询 EMMC 设备(一般emmc为1,sd卡为0,通过mmc list可以查看)的分区 1 的文件系统信息

interface 表示接口,比如 mmc。dev 是查询的设备号, part 是要查询的分区。

fatls

查询 FAT 格式设备的目录和文件信息

1
2
3
fatls <interface> [<dev[:part]>] [directory]
#eg:
fatls mmc 1:1

interface 是要查询的接口,比如 mmc。dev 是要查询的设备号,part 是要查询的分区,directory是要查询的目录(默认为根目录/

fstype

查看 MMC 设备某个分区的文件系统格式

1
2
3
fstype <interface> <dev>:<part>
#eg:
fstype mmc 1:1

interface 是要查询的接口,比如 mmc。dev 是要查询的设备号,part 是要查询的分区

fatload

将指定的文件读取到 DRAM 中

1
2
3
fatload <interface> [<dev[:part]> [<addr> [<filename> [bytes [pos]]]]]
#eg:
fatload mmc 1:1 80800000 zImage # 将 EMMC 分区 1 中的 zImage 文件读取到 DRAM 中的0X80800000 地址处

interface 为接口,比如 mmc。 dev 是设备号, part 是分区, addr 是保存在 DRAM 中的起始地址, filename 是要读取的文件名字。 bytes 表示读取多少字节的数据(0或者省略表示读取整个文件)。pos 是要读的文件相对于文件首地址的偏移(0或者省略表示从文件首地址开始读取)

fatwrite

uboot 默认没有使能 fatwrite 命令,需要修改板子配置头文件来使能宏CONFIG_FAT_WRITE。该命令用于将 DRAM 中的数据写入到 MMC 设备中

1
2
3
fatwrite <interface> <dev[:part]> <addr> <filename> <bytes>
#eg:
fatwrite mmc 1:1 80800000 zImage 0x5c2720

interface 为接口,比如 mmc, dev 是设备号, part 是分区, addr 是要写入的数据在 DRAM中的起始地址, filename 是写入的数据文件名字, bytes 表示要写入多少字节的数据

0.7EXT文件系统操作

uboot 有 ext2 和 ext4 这两种格式的文件系统的操作命令,常用的就四个命令,分别为:ext2loadext2lsext4loadext4lsext4write。 这些命令与FAT文件系统的类似,在此不做过多叙述。

0.8BOOT操作

bootz

用于启动zImage镜像文件

1
2
3
bootz [addr [initrd[:size]] [fdt]]
#eg:
bootz 80800000 – 83000000

addr 是 Linux 镜像文件在 DRAM 中的位置, initrd 是 initrd 文件在DRAM 中的地址(不使用该参数可以用-代替), fdt 就是设备树文件在 DRAM 中的地址。

bootm

与上述命令类似,但是是启动uImage镜像文件,如果不使用设备树,则命令为:

1
bootm addr

addr 是 uImage 镜像在 DRAM 中的首地址。如果要使用设备树则和bootz命令一致

boot

通过读取环境变量 bootcmd 来启动 Linux 系统

例子:

1
2
3
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
saveenv
boot

0.9其他命令

reset

复位命令,不做过多解释

go

用于运行裸机程序

1
go addr [arg ...]

addr 是应用在 DRAM 中的首地址,arg是传入的参数

run

用于运行环境变量中定义的命令

1
2
3
4
5
6
7
setenv mybootemmc 'fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ullalientek-emmc.dtb;bootz 80800000 - 83000000'
setenv mybootnand 'nand read 80800000 4000000 800000;nand read 83000000 6000000 100000;bootz 80800000 - 83000000'
setenv mybootnet 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
saveenv
run mybootemmc
run mytoobnand
run mybootnet

一、Makefile结构分析

编译u-boot时,一般需要采取以下命令:

1
2
make V=1 xxx_config # V=1输出全部内容
make -j8

下面依次介绍各命令的作用:

其中会用到以下命令,提前熟悉下:

1
2
#搜索文件内的变量名
grep -nR "变量名"

1、make xxx_config

该命令会执行顶层Makefile中的目标%config,具体如下所示:

1
2
%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

其中一些变量在其他地方定义过,总结如下:

  • 相关参数值:

    1
    2
    3
    4
    5
    6
    7
    #使用参数V=1时
    Q = # 输出详细信息
    #不使用参数V=1时
    Q = @ # 此时输出信息为精简版

    MAKE = make
    build = -f ./scripts/Makefile.bulid obj
  • outputmakefile为空,scripts_basic:

    1
    2
    3
    scripts_basic:
    $(Q)$(MAKE) $(build)=scripts/basic
    $(Q)rm -f .tmp_quiet_recordmcount

    展开后:

    1
    2
    3
    scripts_basic:
    make -f ./scripts/Makefile.bulid obj=scripts/basic
    rm -f .tmp_quiet_recordmcount
  • FORCE表示会让该依赖每次都执行

最后实际上运行的命令为:

1
2
3
4
5
#依赖中:
make -f ./scripts/Makefile.bulid obj=scripts/basic
rm -f .tmp_quiet_recordmcount # 删除文件,不用管
#目标中:
make -f ./scripts/Makefile.bulid obj=scripts/kconfig xxx_defconfig

通过上述命令,可以看出都是调用./scripts/Makefile.bulid,该文件也可以视为Makefile,下面分析上述两条命令:


1、命令make -f ./scripts/Makefile.bulid obj=scripts/basic

未指定目标,即采取./scripts/Makefile.bulid文件中PHONY指定的默认目标:__build

1
2
3
4
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
$(subdir-ym) $(always)
@:

利用echo输出各变量,有:

1
2
3
4
5
6
7
8
9
#顶层Makefile中:
KBUILD_MODULES :=
KBUILD_BUILTIN := 1
#echo直接输出:
builtin-target =
lib-target =
extra-y =
subdir-ym =
always = /scripts/basic/fixdep

综上,有:

1
2
__build: /scripts/basic/fixdep
@:

所以这条命令只是编译可执行文件/scripts/basic/fixdep


2、命令make -f ./scripts/Makefile.bulid obj=scripts/kconfig xxx_defconfig

在Makefile.bulid中通过include $(kbuild-file),即调用include ./scripts/kconfig/Makefile,在这里边指定目标:

1
2
%_defconfig: $(obj)/conf
$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$@ $(Kconfig)

这里替换掉各个变量后有:

1
2
%_defconfig: scripts/kconfig/conf
scripts/kconfig/conf --defconfig=arch/../configs/xxx_defconfig Kconfig

即有:

  1. 编译生成软件scripts/kconfig/conf
  2. 调用编译好的软件scripts/kconfig/conf生成配置输出文件.config

2、make

未指定目标,即采取Makefile文件中PHONY指定的默认目标,这里PHONY依赖有点多,但是重心为以下依赖:

PHONY->_all->$(ALL-y)

其中$(ALL-y)中的依赖也很多,但是重点是u-boot.bin依赖,且u-boot.bin依赖的文件也比较多,只需关注以下依赖即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
u-boot.bin: u-boot-nodtb.bin FORCE
$(call if_changed,copy)

#依赖u-boot-nodtb.bin:
u-boot-nodtb.bin: u-boot FORCE
$(call if_changed,objcopy)
$(call DO_STATIC_RELA,$<,$@,$(CONFIG_SYS_TEXT_BASE))
$(BOARD_SIZE_CHECK)

#依赖u-boot:
u-boot: $(u-boot-init) $(u-boot-main) u-boot.lds FORCE
$(call if_changed,u-boot__)
ifeq ($(CONFIG_KALLSYMS),y)
$(call cmd,smap)
$(call cmd,u-boot__) common/system_map.o
endif

#上述依赖中:
u-boot-init := $(head-y)
u-boot-main := $(libs-y)
#其中head-y和CPU架构相关,在arch/arm/Makefile中:
head-y := arch/arm/cpu/$(CPU)/start.o # 这里CPU = armv7
#libs-y有很多,基本上为各种源码库,如lib
  • libs-y会包括很多,但是基本上均为各种源码所对应的.o文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    libs-y += lib/
    libs-$(HAVE_VENDOR_COMMON_LIB) += board/$(VENDOR)/common/
    libs-$(CONFIG_OF_EMBED) += dts/
    libs-y += fs/
    libs-y += net/
    libs-y += disk/
    libs-y += drivers/
    ...
    libs-y := $(patsubst %/, %/built-in.o, $(libs-y))
    • 最后的libs-y := $(patsubst %/, %/built-in.o, $(libs-y))调用了patsubst函数,将libs-y中的/替换为/built-in.o,即将原来的各个目录替换为了对应目录下的built-in.o文件
    • built-in.o文件是通过相同文件夹下.built-in.o.cmd文件生成的

综上,最后简化后的makefile如下:

1
2
u-boot:	arch/arm/cpu/armv7/start.o lib/built-in.o ...(大量的built-in.o文件) u-boot.lds FORCE
$(call if_changed,u-boot__)
  • u-boot.lds为编译出的链接脚本(真正的链接脚本来自arch/arm/cpu/u-boot.lds

3、链接脚本

链接地址

通过make V=1 -j8编译U-boot时,最后会输出Entry Point: xxxxxxxx,此地址即为链接地址

该地址可以通过编译输出时的log信息发现,在最后链接时,会加入-Ttext xxxxxxxx,即在链接过程中即指定了链接地址

通过grep -nR "xxxxxxxx"可以发现最后的链接地址来自于include/configs/mx6_common.h中的**CONFIG_SYS_TEXT_BASE**宏

脚本分析

编译成功后会生成一个u-boot.map文件,其中包括了各段的具体地址,结合该.lds文件分析可以得到各段的地体地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start) //入口地址 0x87800000 具体标号在arch/arm/lib/vectors.S
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text : //代码段
{
*(.__image_copy_start) //镜像起始地址 0x87800000
*(.vectors) //中断向量表地址
arch/arm/cpu/armv7/start.o (.text*) //start.o
*(.text*)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : { //data段
*(.data*)
}
. = ALIGN(4);
. = .;
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*))); //uboot命令存放段
}
. = ALIGN(4);
.image_copy_end :
{
*(.__image_copy_end) //镜像结束地址 0x8785dc6c 300k+
}
.rel_dyn_start :
{
*(.__rel_dyn_start) //rel段(重定位用)起始地址 0x8785dc6c
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end) //rel段(重定位用)结束地址 0x878668a4 35k+
}
.end :
{
*(.__end) //总的代码结束地址 0x878668a4
}
_image_binary_end = .; //0x878668a4
. = ALIGN(4096);
.mmutable : {
*(.mmutable)
}
.bss_start __rel_dyn_start (OVERLAY) : {
KEEP(*(.__bss_start)); //bss段起始地址 0x8785dc6c
__bss_base = .;
}
.bss __bss_base (OVERLAY) : {
*(.bss*)
. = ALIGN(4);
__bss_limit = .;
}
.bss_end __bss_limit (OVERLAY) : {
KEEP(*(.__bss_end)); //bss段结束地址 0x878a8d74
}
.dynsym _image_binary_end : { *(.dynsym) }
.dynbss : { *(.dynbss) }
.dynstr : { *(.dynstr*) }
.dynamic : { *(.dynamic*) }
.plt : { *(.plt*) }
.interp : { *(.interp*) }
.gnu.hash : { *(.gnu.hash) }
.gnu : { *(.gnu*) }
.ARM.exidx : { *(.ARM.exidx*) }
.gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}

二、初始化流程

详见思维导图:U-Boot启动流程.xmind

U-Boot启动流程

三、u-boot命令实现

  1. 所有命令通过结构体cmd_tbl_t进行封装(在include/command.h中定义)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct cmd_tbl_s {
    char *name; /* Command Name */
    int maxargs; /* maximum number of arguments */
    int repeatable; /* autorepeat allowed? */
    /* Implementation function */
    int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);
    char *usage; /* Usage message (short) */
    #ifdef CONFIG_SYS_LONGHELP
    char *help; /* Help message (long) */
    #endif
    #ifdef CONFIG_AUTO_COMPLETE
    /* do auto completion on the arguments */
    int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
    #endif
    };

    typedef struct cmd_tbl_s cmd_tbl_t;
  2. 所有命令通过结构体中的name成员进行查找

  3. 这些结构体存在一个uboot自定义的段内:

    1
    2
    3
    4
    5
    6
    7
    8
    SECTIONS
    {
    ...
    .u_boot_list : {
    KEEP(*(SORT(.u_boot_list*))); //uboot命令存放段
    }
    ...
    }
  4. 通过宏U_BOOT_CMD可以快速定义这些命令结构体:

1
2
3
4
5
U_BOOT_CMD(
名字,最大参数,是否可重复,cmd函数,
短字符串,
长字符串
);

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
U_BOOT_CMD(
bootm, CFG_MAXARGS, 1, do_bootm,
"bootm - boot application image from memory\n",
"[addr [arg ...]]\n - boot application image stored in memory\n"
"\tpassing arguments 'arg ...'; when booting a Linux kernel,\n"
"\t'arg' can be the address of an initrd image\n"
#ifdef CONFIG_OF_FLAT_TREE
"\tWhen booting a Linux kernel which requires a flat device-tree\n"
"\ta third argument is required which is the address of the of the\n"
"\tdevice-tree blob. To boot that kernel without an initrd image,\n"
"\tuse a '-' for the second argument. If you do not pass a third\n"
"\ta bd_info struct will be passed instead\n"
#endif
);

四、一般的移植思路

  1. 芯片厂商或者uboot官网找到uboot的源码
  2. 直接在configs/下复制类似芯片或板卡的xxx_defconfig默认配置文件
  3. include/configs下找到类似芯片或板卡的配置头文件,直接复制
  4. board/下找到类似芯片或板卡的板级文件夹,直接复制