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

2019-04-14 18:17发布

目录

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! "); return; } printf("Using machid 0x%lx from environment ", machid); } debug("## Transferring control to Linux (at address %08lx)" "... ", (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 测试

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