NXP 官方u-boot移植和启动过程

官方U-Boot下载

Github
下载后拷贝tar.bz2至Linux,然后tar -vxjf

U-boot工程目录

编译后的uboot源码
除了文件夹之外,还有一些文件:
编译后的uboot源码

验证defconfig

cd /configs/,确认存在有I.MX6ULL的配置文件:

mx6ull配置文件

新版本uboot中mx6ull_14x14_evk_defconfig用于传统的非安全启动(Non-Secure Boot)模式,而mx6ull_14x14_evk_plugin_defconfig支持安全启动(Secure Boot),允许 U-Boot 在 ROM 加载阶段执行自定义代码,通常用于 HAB 安全启动初始化(如 DRAM 训练)。通常使用前者即可。

添加变量至顶层Makefile

在uboot根目录下的顶层Makefile中添加:

1
2
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

编写make用Shell,编译

  1. touch mx6ull_14x14_evk.sh
1
2
3
4
#!/bin/bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
  1. chmod 777 mx6ull_14x14_evk.sh
  2. ./mx6ull_14x14_evk.sh,编译

这里用的uboot是NXP最新的分支v2022.04,这个uboot版本比较新,如果用老版本的交叉编译器会报错,因为它不支持比较新的语法。这里使用的uboot、kernel和编译器的版本都为:

  • uboot:v2022.04
  • kernel:if 6.6y
  • arm-linux-gnueabihf-gcc: 7.5.0

编译完成后,imxdownload烧录至SD卡,设置开发板从SD卡启动。

此时Uboot启动之后,由于LCD未做配置,LCD上是不会显示NXP的Logo的。下一步是修改LCD驱动。

修改LCD驱动

IO部分不用修改,官方和开发板一致,仅需修改参数。较老版本的LCD参数在board/freescale/mx6ull_aki/mx6ull_14x14_evk_emmc.cdisplay_info_t结构体里。但是新版本uboot全部把诸如LCD之类的外设配置参数全部移到了设备树中。并且,imx6ull是由imx6ul这块板子拓展而来的,二者的设备树也呈非常明显的层次关系,即先读imx6ul的设备树,再读imx6ull的,所以很多外设节点都在/arch/arm/dts/imx6ul-14x14-evk.dtsi这个设备树文件中定义。

打开该设备树文件,找到:

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
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;

display = <&display0>;
status = "okay";

display0: display@0 {
bits-per-pixel = <24>;
bus-width = <24>;

display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <51200000>; //像素时钟频率,Hz
hactive = <1024>; //水平可见像素数(水平分辨率)
vactive = <600>; //垂直可见像素数(垂直分辨率)
hfront-porch = <160>; //水平前肩,HFP
hback-porch = <140>; //水平后肩,HBP
hsync-len = <20>; //水平同步脉冲,HSPW
vback-porch = <20>; //水平后肩,VBP
vfront-porch = <12>; //水平前肩,VFP
vsync-len = <3>; //垂直同步脉冲,VSPW

hsync-active = <0>; //水平同步信号极性
vsync-active = <0>; //垂直同步信号极性
de-active = <1>; //数据使能,高电平有效
pixelclk-active = <0>; //下降沿采样
};
};
};
};

按注释修改即可。新版设备树似乎没有定义LCD名字的地方,感觉不是那么在意设备名,毕竟都用设备树了。

联网

主机部分

物理部分:路由器WAN连接校园网,LAN1接电脑,LAN2接开发板ENET2(网卡1)。

VMware需开启桥接模式:
桥接模式

虚拟网络编辑器中一般会自动设置桥接网卡,不用手动设置。桥接模式下虚拟机和物理主机一样存在于局域网中,可以和主机相通,和互联网相通,和局域网中其它主机相通。

配置成桥接模式后虚拟机的IP、网关、DNS、netmask等全部会自动配置。在虚拟机中ifconfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.1.105 netmask 255.255.255.0 broadcast 192.168.1.255
ether 00:0c:29:0d:4a:09 txqueuelen 1000 (以太网)
RX packets 4642 bytes 4241810 (4.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2980 bytes 400799 (400.7 KB)
TX errors 0 dropped 5 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (本地环回)
RX packets 1298 bytes 110055 (110.0 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1298 bytes 110055 (110.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

此时主机的IPv4地址为192.168.1.100,可以看到桥接模式已经生效,主机和虚拟机处于同一个网段下。

板级部分

官方的NXP IMX6ULL EVK开发板使用的不是LAN8720A这个IC,因此需要修改一系列设置。好在官方的板子和正点原子阿尔法开发板的PHY物理地址是一致的,这点从设备树里可以看到:

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
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1>;
phy-mode = "rmii";
phy-handle = <&ethphy0>;
phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
phy-reset-duration = <100>;
phy-reset-post-delay = <100>;
status = "disable";
};

&fec2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet2>;
phy-mode = "rmii";
phy-handle = <&ethphy1>;
phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
phy-reset-duration = <100>;
phy-reset-post-delay = <100>;
status = "okay";

mdio {
#address-cells = <1>;
#size-cells = <0>;

ethphy0: ethernet-phy@2 {
reg = <2>; //PHY0物理地址为2
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET_REF>;
clock-names = "rmii-ref";
};

ethphy1: ethernet-phy@1 {
reg = <1>; //PHY1物理地址为1
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET2_REF>;
clock-names = "rmii-ref";
};
};
};

初始化检查

检查/common/board_r.c,其中从网络初始化入口initr_net开始Uboot会进行网络设置,其中initr_net会调用eth_initialize()进行网络初始化,然后调用reset_phy()对PHY进行复位,而phy_init()中有以下条件宏语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int phy_init(void)
{
...
#ifdef CONFIG_PHY_MICREL_KSZ8XXX
phy_micrel_ksz8xxx_init();
#endif
#ifdef CONFIG_PHY_MICREL_KSZ90X1
phy_micrel_ksz90x1_init();
#endif
...
#ifdef CONFIG_PHY_SMSC
phy_smsc_init();
#endif
...
genphy_init();

return 0;
}

显然这是根据defconfig里的键值来对应不同的PHY初始化函数。阿尔法开发板上的SR8201F是Realtek公司生产的,因此根据phy_initdefconfig里要设置CONFIG_PHY_REALTEK=y才会触发phy_realtek_init()这个函数。

打开/configs/imx6ull_14x14_evk_emmc_defconfig,删除或屏蔽:

1
2
CONFIG_PHY_MICREL=y
CONFIG_PHY_MICREL_KSZ8XXX=y

添加:

1
CONFIG_PHY_REALTEK=y

(不过这里我一开始错误的改成了CONFIG_PHY_SMSC=y,发现网络也能正常使用,不知道为什么)

配置fec复位管脚

fecmxc_probe函数中还调用了fec_gpio_reset()来复位fec网卡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#if CONFIG_IS_ENABLED(DM_GPIO)
fec_gpio_reset(priv);
#endif
/* Reset chip. */
writel(readl(&priv->eth->ecntrl) | FEC_ECNTRL_RESET,
&priv->eth->ecntrl);
start = get_timer(0);
while (readl(&priv->eth->ecntrl) & FEC_ECNTRL_RESET) {
if (get_timer(start) > (CONFIG_SYS_HZ * 5)) {
printf("FEC MXC: Timeout resetting chip\n");
goto err_timeout;
}
udelay(10);
}

而这里设备树会使用fecmxc_of_to_plat这个OF函数来读取节点信息到驱动,该函数中有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#if CONFIG_IS_ENABLED(DM_GPIO)
ret = gpio_request_by_name(dev, "phy-reset-gpios", 0,
&priv->phy_reset_gpio, GPIOD_IS_OUT);
if (ret < 0)
return 0; /* property is optional, don't return error! */

priv->reset_delay = dev_read_u32_default(dev, "phy-reset-duration", 1);
if (priv->reset_delay > 1000) {
printf("FEC MXC: phy reset duration should be <= 1000ms\n");
/* property value wrong, use default value */
priv->reset_delay = 1;
}

priv->reset_post_delay = dev_read_u32_default(dev,
"phy-reset-post-delay",
0);
if (priv->reset_post_delay > 1000) {
printf("FEC MXC: phy reset post delay should be <= 1000ms\n");
/* property value wrong, use default value */
priv->reset_post_delay = 0;
}
#endif

可以看到这里依赖节点的三个用于描述复位的值:

  • phy-reset-gpios
  • phy-reset-duration
  • phy-reset-post-delay

回到设备树,将这三个属性加进节点:

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
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1>;
phy-mode = "rmii";
phy-handle = <&ethphy0>;
phy-reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
phy-reset-duration = <100>;
phy-reset-post-delay = <100>;
status = "disable";
};

&fec2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet2>;
phy-mode = "rmii";
phy-handle = <&ethphy1>;
phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
phy-reset-duration = <100>;
phy-reset-post-delay = <100>;
status = "okay";

mdio {
#address-cells = <1>;
#size-cells = <0>;

ethphy0: ethernet-phy@2 {
reg = <2>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET_REF>;
clock-names = "rmii-ref";
};

ethphy1: ethernet-phy@1 {
reg = <1>;
micrel,led-mode = <1>;
clocks = <&clks IMX6UL_CLK_ENET2_REF>;
clock-names = "rmii-ref";
};
};
};

GPIO5_IO07GPIO5_IO08会跟SPI4的两个IO冲突,这里直接把spi4 disable掉即可。通常uboot会自动加载fec2,为了方便起见,我把fec1也disable了。

最后,还需要在/drviers/net/phy中,找到genphy_config_aneg函数,这个函数负责PHY自协商的完整配置,在初始化PHY时会调用(通过操作BMCR_RESET位)。在函数中加入:

1
2
3
4
5
6
7
8
9
10
int genphy_config_aneg(struct phy_device *phydev)
{
int result;

/* Soft reset */
phy_reset(phydev);
mdelay(150); //IC要求复位后延时150ms

......
}

uboot设置

在uboot内设置开发板网络配置:

1
2
3
4
5
setenv ipaddr 192.168.1.55
setenv ethaddr b8:ae:1d:01:00:00
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0
setenv serverip 192.168.1.105 # Ubuntu IP

然后saveenv,保存变量。

uboot支持自动配置物理地址,只需要在defconfig中加入:CONFIG_NET_RANDOM_ETHADDR=y

如果对静态IP没有要求,或者想要简单一点,可以直接在Uboot内使用DHCP命令。DHCP会自动申请可用IP,自动配置网关、DNS、子网掩码等。

此时在Uboot内ping 虚拟机地址192.168.1.105,应当是能Ping通的。为防止IP被复用,关闭虚拟机,再Ping一次虚拟机,如果无法Ping通,说明配置正确:
Ping测试

反过来,从虚拟机Ping开发板,肯定是Ping不通的(Ping通了说明有问题,这个IP被其他的什么东西复用了),因为此时Linux内核还未启动,网口不会被使能。

此时,三台机器的IP地址分别为:

  • 主机:192.168.1.100
  • 虚拟机:192.168.1.105
  • 开发板:192.168.1.101(DHCP获取)

uboot启动流程

reset函数

uboot编译后会在根目录下生成链接脚本u-boot.lds。其内容的前几行里有:

1
2
3
4
5
6
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
........

很显然第3行的ENTRY(_start)中的_start是代码入口点,在arch/arm/lib/vectors.S中定义:

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
/*
* vectors - Generic ARM exception table code
*
* Copyright (c) 1998 Dan Malek <dmalek@jlc.net>
* Copyright (c) 1999 Magnus Damm <kieraypc01.p.y.kie.era.ericsson.se>
* Copyright (c) 2000 Wolfgang Denk <wd@denx.de>
* Copyright (c) 2001 Alex Züpke <azu@sysgo.de>
* Copyright (c) 2001 Marius Gröger <mag@sysgo.de>
* Copyright (c) 2002 Alex Züpke <azu@sysgo.de>
* Copyright (c) 2002 Gary Jennejohn <garyj@denx.de>
* Copyright (c) 2002 Kyle Harris <kharris@nexus-tech.net>
*
* SPDX-License-Identifier: GPL-2.0+
*/

#include <config.h>

/*
*************************************************************************
*
* Symbol _start is referenced elsewhere, so make it global
*
*************************************************************************
*/

.globl _start

/*
*************************************************************************
*
* Vectors have their own section so linker script can map them easily
*
*************************************************************************
*/

.section ".vectors", "ax"

/*
*************************************************************************
*
* Exception vectors as described in ARM reference manuals
*
* Uses indirect branch to allow reaching handlers anywhere in memory.
*
*************************************************************************
*/

_start:

#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif

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

.................

显然_start后面就是中断向量表,.section ".vectors", "ax"表明此代码存放在vectors里面,具体的地址可以在u-boot.map中查询。b reset跳转至reset函数内,该函数在arch/arm/cpu/armv7/start.S里:

1
2
3
reset:
/* Allow the board to save important registers */
b save_boot_params

reset函数紧接着又跳转到了save_boot_params函数,该函数同样定义在start.S里:

1
2
ENTRY(save_boot_params)
b save_boot_params_ret @ back to my caller

这个函数又直接跳转到了save_boot_params_ret函数:

1
2
3
4
5
6
7
8
9
10
11
12
save_boot_params_ret:
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32
* mode, except if in HYP mode already
*/
mrs r0, cpsr @ 读取CPSR
and r1, r0, #0x1f @ 提取bit0 - bit4,用于设置Cortex-A7工作模式
teq r1, #0x1a @ 判断处理器是否处于HYP模式
bicne r0, r0, #0x1f @ 若不处于HYP模式,清除模式位
orrne r0, r0, #0x13 @ 进入SVC模式(特权)
orr r0, r0, #0xc0 @ 关闭FIQ和IRQ
msr cpsr,r0 @ 恢复CPSR

然后,继续执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* Setup vector:
* (OMAP4 spl TEXT_BASE is not 32 byte aligned.
* Continue to use ROM code vector only in OMAP4 spl)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0, 0 @ 读取 CP15 SCTLR 寄存器
bic r0, #CR_V @ V = 0
mcr p15, 0, r0, c1, c0, 0 @ 写 CP15 SCTLR 寄存器,V清零,准备重定位向量表

/* CP15 VBAR 寄存器中设置向量表地址 */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif

继续执行:

1
2
3
4
5
6
7
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15 @ 设置CP15相关内容
bl cpu_init_crit
#endif

bl _main

这已经是跳转至main的最后一步。cpu_init_crit定义如下:

1
2
3
4
5
6
7
8
9
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)

注释已经写了,该函数即将要跳转到的lowlevel_init会设置PLL、MUX和内存。

lowlevel_init

函数在arch/arm/cpu/armv7/lowlevel_init.S中定义:

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
/*
* A lowlevel_init function that sets up the stack to call a C function to
* perform further init.
*
* (C) Copyright 2010
* Texas Instruments, <www.ti.com>
*
* Author :
* Aneesh V <aneesh@ti.com>
*
* SPDX-License-Identifier: GPL-2.0+
*/

#include <asm-offsets.h>
#include <config.h>
#include <linux/linkage.h>

ENTRY(lowlevel_init)
/*
* Setup a temporary stack. Global data is not available yet.
*/
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_DM
mov r9, #0
#else
/*
* Set up global data for boards that still need it. This will be
* removed soon.
*/
#ifdef CONFIG_SPL_BUILD
ldr r9, =gdata
#else
sub sp, sp, #GD_SIZE
bic sp, sp, #7
mov r9, sp
#endif
#endif
/*
* Save the old lr(passed in ip) and the current lr to stack
*/
push {ip, lr}

/*
* Call the very early init function. This should do only the
* absolute bare minimum to get started. It should not:
*
* - set up DRAM
* - use global_data
* - clear BSS
* - try to start a console
*
* For boards with SPL this should be empty since SPL can do all of
* this init in the SPL board_init_f() function which is called
* immediately after this.
*/
bl s_init
pop {ip, pc}
ENDPROC(lowlevel_init)

第22行ldr sp, =CONFIG_SYS_INIT_SP_ADDR,将栈指针指向CONFIG_SYS_INIT_SP_ADDR,这个宏在include/configs/mx6ullevk.h里定义:

1
2
3
4
5
6
7
#define CONFIG_SYS_INIT_RAM_ADDR IRAM_BASE_ADDR
#define CONFIG_SYS_INIT_RAM_SIZE IRAM_SIZE

#define CONFIG_SYS_INIT_SP_OFFSET \
(CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)
#define CONFIG_SYS_INIT_SP_ADDR \
(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)

IRAM_BASE_ADDRIRAM_SIZEarch/arm/include/asm/arch-mx6/imx-regs.h中定义,实际上就是MX6ULL内部OCRAM的首地址和大小。这里CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB

根据这两行:

1
2
3
4
#define CONFIG_SYS_INIT_SP_OFFSET \
(CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)
#define CONFIG_SYS_INIT_SP_ADDR \
(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)

显然还需要知道GENERATED_GBL_DATA_SIZE的值,在include/generated/generic-asm-offsets.h中定义,大小为256.那么综上,CONFIG_SYS_INIT_SP_ADDR的值如下:

1
2
CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 - 256 = 0x1FF00
CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0x1FF00 = 0x0091FF00

此时栈指针SP指向0x0091FF00,这属于IMX6ULL的内部RAM。

回到lowlevel_init.S,bic sp, sp, #7对栈指针作8字节对齐处理。第34行sub sp, sp, #GD_SIZE用栈指针减去GD_SIZE,这个变量在generic-asm-offsets.h中定义,大小为248;第35行bic sp, sp, #7,8字节对齐SP,此时SP=0x0091FF00 - 248 = 0x0091FE08。第36行将SP保存至R9,第42行将ip和lr入栈,57行调用s_init函数,然后58行出栈ip和lr,并将lr赋给pc。

s_init

lowlevel_init在最后回调用s_init,定义在arch/arm/cpu/armv7/mx6/soc.c中。对于MX6ULL而言,s_init()是个空函数(不满足触发要求)。至此lowlevel_init结束,返回cpu_init_crit,该函数也结束,最后返回save_boot_params_ret,紧接着就会执行_main函数。

_main

定义在arch/arm/lib/crt0.S。总体而言非常长,总共调用了4个函数:board_init_frelocate_coderelocate_vectorsboard_init_r

board_init_f

common/board_f.c中定义,该函数的工作:

  • 初始化外设,包括串口、定时器等,并打印一些消息
  • 初始化 gd 的各个成员变量,uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存map,在后面重定位 uboot 的时候就会用到这个内存map。

该函数中包括非常关键的一行:

1
2
if (initcall_run_list(init_sequence_f))
hang();

initcall_run_list会初始化一系列外设:

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
static init_fnc_t init_sequence_f[] = {
setup_mon_len, //设置代码长度
initf_malloc, //初始化malloc相关的成员变量
initf_console_record, //无效,返回0
arch_cpu_init, //基本的arch cpu依赖项初始化
initf_dm,
arch_cpu_init_dm,
mark_bootstage,
board_early_init_f, //初始化串口IO配置
timer_init, //初始化Cortex-A7内核定时器,为Uboot提供时间(类似systick)
board_postclk_init, //设置VDDSOC电压
get_clocks, //获取SD卡外设时钟
env_init, //初始化环境变量
init_baud_rate, //初始化波特率
serial_init, //初始化串口
console_init_f,
display_options, //串口打印信息
display_text_info, //打印调试信息
print_cpuinfo, //打印CPU信息
show_board_info, //打印板子信息,调用checkboard()
INIT_FUNC_WATCHDOG_INIT //无效
INIT_FUNC_WATCHDOG_RESET //无效
init_func_i2c, //初始化I2C
announce_dram_init,
dram_init, //设置gd->ram_size
post_init_f, //测试函数
INIT_FUNC_WATCHDOG_RESET
testdram, //无效
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
/*
* Now that we have DRAM mapped and working, we can
* relocate the code and continue running from DRAM.
*
* Reserve memory at end of RAM for (top down in that order):
* - area that won't get touched by U-Boot and Linux (optional)
* - kernel log buffer
* - protected RAM
* - LCD framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr, //设置gd->ram_size(ram大小),gd->ram_top(ram最高地址)和gd->relocaddr(重定位后最高地址)
reserve_round_4k, //gd->relocaddr 4字节对齐
reserve_mmu, //留出MMU的TLB表位置
reserve_trace,
reserve_uboot, //留出重定位后uboot所占内存区域,大小由gd->monlen指定,留出的空间需4KB对齐并设置gd->start_addr_sp
reserve_malloc, //留出malloc区域,由宏TOTAL_MALLOC_LEN定义(默认16MB)
reserve_board, //留出bd_t内存区
setup_machine, //无效
reserve_global_data, //留出gd_t内存区域
reserve_fdt, //无效
reserve_arch, //空
reserve_stacks, //留出栈空间,如果使能IRQ还需要留出IRQ相应的内存
setup_dram_config, //设置DRAM,告知linux DRAM起始地址和大小
show_dram_config,
display_new_sp, //显示新的sp位置
INIT_FUNC_WATCHDOG_RESET
reloc_fdt, //无效
setup_reloc, //设置gd的其他成员变量
NULL,
};

至此,board_init_f执行完成,最终的内存分配:
内存分配

relocate_code / relocate_vectors

用于拷贝代码和重定位向量表。

board_init_r

用于初始化高级外设,包括EMMC、LCD、BSP芯片、网络等。

小结

总的来说,uboot的汇编启动阶段包括:

  1. 入口点:_start(arch/arm/lib/vectors.S)设置异常向量表
  2. CPU模式切换:进入SVC模式,禁用IRQ/FIQ
  3. 关闭MMU/Cache,确保直接访问物理地址
  4. 初始化栈指针
  5. lowlevel_init(板级特定):
    • 配置系统时钟
    • DDR控制器初始化
  6. 代码重定位:将U-Boot从加载地址(如SRAM)拷贝到DDR目标地址

C语言初始化阶段(board_init_f)包括:

  1. 全局数据结构初始化:gd_tbd_t
  2. 低级驱动初始化:
    • 串口、GPIO、MMU、内核定时器
    • DDR
  3. 重定位准备
    • 计算uboot在DDR中的最终位置,预留内核、DTB、initrd、代码空间
    • 执行重定位,更新代码地址相关引用(如全局变量指针)
  4. 设备树预解析:若使用DTS,加载并校验DTB

重定位后主流程(board_init_r)包括:

  1. 高级外设初始化:
    • 以太网、SD、USB、EEPROM等
    • 展开DTB,匹配驱动,设置硬件参数
  2. 环境变量加载:从存储介质读取uEnv.txt或环境分区
  3. 用户交互
    • 检测输入
    • 加载zImage、设备树、initrd到指定内存地址
    • 准备加载内核

NXP 官方u-boot移植和启动过程
http://akichen891.github.io/2025/03/07/uboot移植/
作者
Aki
发布于
2025年3月7日
更新于
2025年3月19日
许可协议