移植linux-4.12到JZ2440(上:uboot如何启动内核与创建单板)

目录

1. uboot如何启动内核与创建单板
    1.1 uboot如何启动内核

    1.2 准备工作
        1.2.1 环境
        1.2.2 获取linux-4.12源码
1.3 创建单板
        1.3.1 创建JZ2440相关单板文件夹
        1.3.2 测试
        1.3.3 分析启动内核为什么无输出
        1.3.4 解决内核无输出
        1.3.5 修改MTD分区
                1.3.5.1 分析调用过程
                1.3.5.2 修改分区
        1.3.6 测试


1. uboot如何启动内核与创建单板

1.1 uboot如何启动内核

    在移植u-boot-2016.11到JZ2440中我们已经实现了移植新uboot并启动内核,首先来分析该版本uboot是如何启动内核的。
    当uboot启动完成后如果设置了bootcmd环境变量的如下:

bootcmd=nand read 0x30000000 kernel; bootm 0x30000000        //从0x30000000处启动内核

    uboot会执行这两条命令,第一条先从NAND Flash里的kernel分区读出内核到0x30000000地址;第二条bootm 0x30000000命令,下面具体分析该命令的执行过程。bootm命令定义在cmd/bootm.c文件中:
   
    可以看到实际调用得是do_bootm()函数(cmd/bootm.c文件):

int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
	argc--; argv++;
	if (argc > 0) {
		char *endp;

		simple_strtoul(argv[0], &endp, 16);    /* 将参数转换成字符串 */

		if ((*endp != 0) && (*endp != ':') && (*endp != '#'))
			return do_bootm_subcommand(cmdtp, flag, argc, argv);
	}

        ... ...
}

    simple_strtoul函数将输入的命令转换成字符串,存入endp指针,由于我们的命令中没有‘:’、‘#’字符,所以该函数返回的是do_bootm_subcommand函数。代码如下(cmd/bootm.c文件)

static int do_bootm_subcommand(cmd_tbl_t *cmdtp, int flag, int argc,
			char * const argv[])
{
	int ret = 0;
	long state;
	cmd_tbl_t *c;

	c = find_cmd_tbl(argv[0], &cmd_bootm_sub[0], ARRAY_SIZE(cmd_bootm_sub));    /* 查找命令 */
	argc--; argv++;

	if (c) {
		state = (long)c->cmd;
		if (state == BOOTM_STATE_START)
			state |= BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER;
	} else {
		/* Unrecognized command */
		return CMD_RET_USAGE;
	}

        ... ...

	ret = do_bootm_states(cmdtp, flag, argc, argv, state, &images, 0);    /* 启动内核 */

	return ret;
}

    do_bootm_states函数代码如下(cmd/bootm.c文件)

int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
		    int states, bootm_headers_t *images, int boot_progress)
{
	boot_os_fn *boot_fn;
	ulong iflag = 0;
	int ret = 0, need_boot_fn;

	images->state |= states;

	if (states & BOOTM_STATE_START)
		ret = bootm_start(cmdtp, flag, argc, argv);    /* 得到内核头部,填充images结构体 */
        
        ... ...

	boot_fn = bootm_os_get_boot_func(images->os.os);    /* 通过头部标识从boot_os数组选择启动内核函数 */

	/* Now run the OS! We hope this doesn't return */
	if (!ret && (states & BOOTM_STATE_OS_GO))
		ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
				images, boot_fn);    /* 启动os内核 */

	/* Deal with any fallout */
err:
        ... ...
}

    bootm_os_get_boot_func函数里会通过头部标识从boot_os数组选择启动内核的函数,boot_os数组如下(common/bootm_os.c文件中)
   
    对于我们使用的uImage使用的是do_bootm_linux函数,该函数代码如下(arch/arm/lib/bootm.c文件)

int do_bootm_linux(int flag, int argc, char * const argv[],
		   bootm_headers_t *images)
{
	/* No need for those on ARM */
	if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
		return -1;

	if (flag & BOOTM_STATE_OS_PREP) {
		boot_prep_linux(images);
		return 0;
	}

	if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
		boot_jump_linux(images, flag);
		return 0;
	}

        /* 该函数会将各个tag参数保存在指定位置,比如:内存tag、bootargs环境变量tag、串口tag等 */
	boot_prep_linux(images);

        /* 该函数会跳转到内核起始地址 */
	boot_jump_linux(images, flag);
	return 0;
}

    最终调用boot_jump_linux()函数跳到内核起始地址运行内核,boot_jump_linux()函数代码如下(common/bootm.c文件)

static void boot_jump_linux(bootm_headers_t *images, int flag)
{
#ifdef CONFIG_ARM64    /* 未定义CONFIG_ARM64,执行else分支 */
        ... ...
#else
        /* gd->bd->bi_arch_number在boot启动第二阶段init_sequence_r数组里的board_init函数里设置 */
	unsigned long machid = gd->bd->bi_arch_number;    /* 设置默认机器ID */
	char *s;
	void (*kernel_entry)(int zero, int arch, uint params);
	unsigned long r2;
	int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

	kernel_entry = (void (*)(int, int, uint))images->ep;    /* 设置kernel_entry()的地址为0x30000000 */

	s = getenv("machid");    /* 判断环境变量machid是否设置,若设置则使用环境变量里的值 */
	if (s) {
                /* 重新获取机器ID,使用环境变量的machid */
		if (strict_strtoul(s, 16, &machid) < 0) {
			debug("strict_strtoul failed!\n");
			return;
		}
		printf("Using machid 0x%lx from environment\n", machid);
	}

	debug("## Transferring control to Linux (at address %08lx)" \
		"...\n", (ulong) kernel_entry);
	bootstage_mark(BOOTSTAGE_ID_RUN_OS);
	announce_and_cleanup(fake);

	if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)    /* 执行else分支 */
		r2 = (unsigned long)images->ft_addr;
	else
                /* 获取tag参数地址, gd->bd->bi_boot_params在boot启动第二阶段init_sequence_r
                数组里的board_init函数里被设置为0x30000100 */
		r2 = gd->bd->bi_boot_params;

	if (!fake) {
		kernel_entry(0, machid, r2);    /* 跳转到0x30000000,r0=0,r1=机器ID,r2=tag参数地址0x30000100 */
	}
#endif
}

    运行内核后工作内容如下:
      1. 比较机器ID;
      2. 解析uboot传入的启动参数;
      3. 挂接根文件系统、执行第一个应用程序。

1.2 准备工作

1.2.1 环境

    交叉编译工具:arm-linux-gcc-4.4.3
    开发板:JZ2440V3

1.2.2 获取linux-4.12源码

    从官方下载源码,地址:https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/,找到linux-4.12.tar.gz并下载。
    解压该文件,得到如下:
   
    下面简单介绍一下这些文件夹的功能:

    arch体系结构相关代码,对于每个架构的CPUarch目录下有一个对应的子目录,比如arch/arm
    block:块设备的通用函数;
    crypto常用加密和散列算法,还有一些压缩和CRC校验算法;
    Documentation内核文档;
    drivers所有的设备驱动程序,里面每一个子目录对应一类驱动程序,比如drivers/block/为块设备驱动程序,drivers/char/为字符设备驱动程序,drivers/mtd/NOR Flash、NAND Flash等存储设备的驱动程序。
    fsLinux支持的文件系统的代码,每个字目录对应一种文件系统,比如fs/jffs2/、fs/ext2
    include这 个目录包含linux源代码目录树中绝大部分头文件,每个体系架构都在该目录下对应一个子目录,该子目录中包含了给定体系结构所必需的宏定义和内联函数。
    init该目录中存放的是系统核心初始化代码,内核初始化入口函数start_kernel就是在该目录下的文件main.c内实现的。内核初始化入口函数start_kernel负责调用其它模块的初始化函数,完成系统的初始化工作。
    ipc用于实现System V的进程间通信(Inter Process Communication,IPC)模块。
    kernel用于存放特定体系结构特有信号量的实现代码和对称多处理器(Symmetric MultiProccessing,简称SMP)相关模块。
    lib内核用到的一些库函数代码,比如crc32.c、string.c,与处理器相关的库函数代码位于arch/*/lib目录下;
    mm内存管理代码,与处理器相关的内存管理代码位于arch/*/mm目录下;
    net网络支持代码,每个子目录对应于网络的一个方面;
    samples内核编程的例子;
    scripts该目录下没有内核代码,只包含了用来配置内核的脚本文件。当运行make menuconfig或者make xconfig之类的命令配置内核时,用户就是和位于这个目录下的脚本进行交互的。
    security这个目录包括了不同的Linux安全模型的代码,比如NSA Security-Enhanced Linux;
    sound音频设备的驱动程序
    tools与内核交互,以便在用户态时测试相关内核功能;
    usr目录下是initramfs相关的,和linux内核的启动有关.实现了用于打包和压缩的的cpio等。

1.3 创建单板

   下面开始创建针对JZ2440开发板的相关单板文件。

1.3.1 创建JZ2440相关单板文件夹

    1. 在服务器上解压linux-4.12.tar.gz文件,执行如下命令:
      tar xzf linux-4.12.tar.gz
    2. 进入根目录,vi Makefile修改顶层Makefile,修改如下内容:

ARCH		?= $(SUBARCH)
CROSS_COMPILE	?= $(CONFIG_CROSS_COMPILE:"%"=%)

    改为:

ARCH		?= arm
CROSS_COMPILE	?= arm-linux-

    3. 配置编译,执行如下命令:
      make s3c2410_defconfig    (使用相近单板配置文件进行配置,在arch/arm/configs下有许多配置文件选择)
      make uImage
   
这样在arch/arm/boot目录下就会生成uImage
   

1.3.2 测试

    使用移植u-boot-2016.11到JZ2440移植的uboot下载uImage开发板上运行:
      tftp 30000000 uImage    (或者将uImage拷贝到/work/nfs_root/, nfs 30000000 192.168.1.13:/work/nfs_root/uImage)
      bootm 30000000
   

    可以看到uboot去启动内核后就没有任何输出了,猜测可能是波特率设置问题,所以重启开发板在uboot里输入如下命令添加设置波特率:
      set bootargs console=ttySAC0,115200 root=/dev/mtdblock3
      save

   
重新下载启动内核发现还是无输出,下面分析原因。

1.3.3 分析启动内核为什么无输出

    执行bootm 0x30000000命令会调用到boot_jump_linux()函数,该函数里有如下代码(u-boot-2016.11/arch/arm/lib/bootm.c文件中)
   
    移植u-boot-2016.11到JZ2440移植的uboot里,在u-boot-2016.11/board/samsung/jz2440/jz2440.c中设置了默认ID
   
    宏MACH_TYPE_SMDK2410定义在u-boot-2016.11/arch/arm/include/asm/mach-types.h文件中,如下:
   

    同时在board_f.c (common)board_init_f()函数中也有设置ID
   
    只不过uboot配置文件jz2440.h(include/configs)没有定义CONFIG_MACH_TYPE该宏,所以使用的是前面定义的MACH_TYPE_SMDK2410宏也就是193=0xc1
    在uboot任意设置一个机器ID值(set machid 33333),这样再次下载启动内核时,内核识别不出来,就会打印出当前的内核配置所支持的单板机器ID,如下(内核支持的所有机器ID定义在linux-3.4.2/include/generated/mach-types.h文件中)
   
    由于之前配置(make s3c2410_defconfig),编译器就会将该类型的单板文件编译进内核,在arch/arm目录下输入"find -name "mach*.o"",如下:   
   
    启动内核时内核就会匹配uboot传入的机器ID,根据不同的ID使用不同mach-*.c文件,比如在1.3.2 测试中传入内核的ID193,启动内核时查找内核里的机器ID单板文件,结果与如下定义匹配(arch/arm/mach-s3c24xx/mach-smdk2410.c文件中)
   
    MACHINE_START定义如下:
   
    用MACHINE_START宏替换后mach-smdk2410.c文件的.nr=MACH_TYPE_SMDK2410,查看该宏定义(include/generated/mach-types.h文件中)刚好就是193所以调用的machine_desc结构体成员函数是使用arch/arm/mach-s3c24xx/mach-smdk2410.c文件里的这样显然不对,我们应该使用arch/arm/mach-s3c24xx/mach-smdk2440.c文件里的函数

1.3.4 解决内核无输出

    我们不使用mach-smdk2410.c该单板文件,使用更相近的arch/arm/mach-s3c24xx/mach-smdk2440.c文件,所以应该设置机器ID16a。设置ID可以修改UBOOT源码(按照1.3.3 分析启动内核为什么无输出中调用过程修改宏MACH_TYPE_SMDK2410CONFIG_MACH_TYPE),也可以直接设置环境变量来修改机器ID
    如果设置机器ID16a,那么使用的单板文件就是arch/arm/mach-s3c24xx/mach-smdk2440.c文件,该文件的时钟设置如下:
   
    可以看出晶振频率设置不对,将其中的晶振频率修改为12MHZ(本人使用的JZ2440开发板上用的是12MHZ晶振)也就是12000000,如下:
   
    然后重新编译内核make uImage
    重启开发板在uboot命令行输入如下命令:
      set machid 16a    (如果没有设置波特率还要设置:set bootargs console=ttySAC0,115200 root=/dev/mtdblock3
      save
    重新下载启动内核:
      tftp 30000000 uImage    (或者将uImage拷贝到/work/nfs_root/, nfs 30000000 192.168.1.13:/work/nfs_root/uImage
      bootm 30000000
   
如下可以看到内核输出信息了:
   

1.3.5 修改MTD分区

    上面启动内核后还有如下输出:
   
    uboot传递的文件系统路径root=/dev/mtdblock3,所以内核便卡死在启动文件系统上。内核默认创建了8个分区,显然与我们之前在uboot中划分的分区不符,下面分析内核划分分区的过程。

1.3.5.1 分析调用过程

    内核启动后,会调用如下arch_initcall标示的函数:

static int __init customize_machine(void)
{
	/*
	 * customizes platform devices, or adds new ones
	 * On DT based machines, we fall back to populating the
	 * machine from the device tree, if no callback is provided,
	 * otherwise we would always need an init_machine callback.
	 */
	if (machine_desc->init_machine)
		machine_desc->init_machine();

	return 0;
}
arch_initcall(customize_machine);

    由于之前我们设置机器ID对应为arch/arm/mach-s3c24xx/mach-smdk2440.c文件,所以customize_machine()函数调用的init_machine()函数就是mach-smdk2440.c文件中如下代码:
  
    smdk2440_machine_init()函数又调用smdk_machine_init函数,代码如下:

static struct platform_device __initdata *smdk_devs[] = {
	&s3c_device_nand,
	&smdk_led4,
	&smdk_led5,
	&smdk_led6,
	&smdk_led7,
};

void __init smdk_machine_init(void)
{
        ... ...

	s3c_nand_set_platdata(&smdk_nand_info);    /* 设置platform数据 */

	platform_add_devices(smdk_devs, ARRAY_SIZE(smdk_devs));    /* 注册platform_device */

        ... ...
}

    该函数会注册一些platform_device,其中s3c_device_nand结构体定义如下(arch/arm/plat-samsung/devs.c文件中)
   
    而该name成员会被s3c244x_map_io函数调用s3c_nand_setname函数修改为"s3c2440-nand",如下(arch/arm/mach-s3c24xx/s3c244x.c文件中)
   
    所以内核注册了名为"s3c2410-nand"的platform_device结构。我们之前使用的内核配置文件arch/arm/configs/s3c2410_defconfig中有如下代码:
   
    而在drivers/mtd/nand/Makefile文件中有如下代码:
   
    所以内核启动时会调用s3c2410.c驱动,在该驱动入口函数中会调用platform_driver_register()函数注册platform_driver结构,通过id_table匹配到之前注册的名为"s3c2440-nand"的platform_device结构,根据平台总线设备驱动模型,最终调用.probe函数也就是s3c24xx_nand_probe()函数,过程如下(drivers/mtd/nand/s3c2410.c文件中)
   
    s3c24xx_nand_probe()函数最终会调用到add_mtd_partitions()函数添加分区(详细内容可以看十七、Linux驱动之nand flash驱动,使用的参数就是前面smdk_machine_init()函数调用的s3c_nand_set_platdata()函数里设置的smdk_nand_info结构体,如下(arch/arm/mach-s3c24xx/common-smdk.c文件中)
   
    最终add_mtd_partitions()函数添加分区就是使用的smdk_default_nand_part数组里的参数,该数组定义如下(arch/arm/mach-s3c24xx/common-smdk.c文件中)

static struct mtd_partition smdk_default_nand_part[] = {
	[0] = {
		.name	= "Boot Agent",
		.size	= SZ_16K,
		.offset	= 0,
	},
	[1] = {
		.name	= "S3C2410 flash partition 1",
		.offset = 0,
		.size	= SZ_2M,
	},
	[2] = {
		.name	= "S3C2410 flash partition 2",
		.offset = SZ_4M,
		.size	= SZ_4M,
	},
	[3] = {
		.name	= "S3C2410 flash partition 3",
		.offset	= SZ_8M,
		.size	= SZ_2M,
	},
	[4] = {
		.name	= "S3C2410 flash partition 4",
		.offset = SZ_1M * 10,
		.size	= SZ_4M,
	},
	[5] = {
		.name	= "S3C2410 flash partition 5",
		.offset	= SZ_1M * 14,
		.size	= SZ_1M * 10,
	},
	[6] = {
		.name	= "S3C2410 flash partition 6",
		.offset	= SZ_1M * 24,
		.size	= SZ_1M * 24,
	},
	[7] = {
		.name	= "S3C2410 flash partition 7",
		.offset = SZ_1M * 48,
		.size	= MTDPART_SIZ_FULL,
	}
};

    可以看到这里划分了8个分区,与我们启动内核打印出的信息一致。

1.3.5.2 修改分区

    修改smdk_default_nand_part[]数组如下(arch/arm/mach-s3c24xx/common-smdk.c文件中)

static struct mtd_partition smdk_default_nand_part[] = {
	[0] = {
		.name	= "bootloader",
		.size	= SZ_256K,
		.offset	= 0,
	},
	[1] = {
		.name	= "params",
		.offset = MTDPART_OFS_APPEND,
		.size	= SZ_128K,
	},
	[2] = {
		.name	= "kernel",
		.offset = MTDPART_OFS_APPEND,
		.size	= SZ_2M,
	},
	[3] = {
		.name	= "rootfs",
		.offset	= MTDPART_OFS_APPEND,
		.size	= MTDPART_SIZ_FULL,
	}
};

1.3.6 测试

    重新编译启动内核,有如下输出:
   

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值