以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除。
一、内核移植初体验
1、获取三星官方的内核源码包
三星SMDKV210开发板附带的光盘里有内核源码包:下载地址。
2、构建移植环境
(1)Windows下建立SI工程(用来分析)
- 为了简单与方便,建立SI工程前先删除不必要的目录和文件,包括arch目录下非arm架构的目录文件,以及arch/arm目录下不是三星的mach-xxx、plat-xxx目录。
(2)在ubuntu下解压一份源码包(根据分析进行修改)
3、内核配置与编译
(1)在主Makefile文件的193行之后添加以下内容:
#added by xjh ARCH = arm CROSS_COMPILE = /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
(2)执行“make smdkv210_android_defconfig”。
- 在arch/arm/configs目录下有很多配置文件,我们从中找到与三星相关的、与我们开发板最像的“smdkv210_android_defconfig”。
(3)执行“make menuconfig”。
- 忽略弹出的配置界面,因为目前还不知道修整哪些内容。
(4)执行“make -j4”。
- 直接执行make则会直接单线程编译,如果make -j4则会4线程编译,编译效率会提高。
- 多少线程编译则要看是多少核的cpu:四核则可以4线程,双核则2线程。
4、通过tftp将编译得到的zImage下载到开发板
- 编译得到的内核镜像在arch/arm/boot目录下。
- 通过tftp将镜像下载到开发板的方法见博客:利用tftp将镜像下载到开发板_天糊土的博客
5、运行结果与分析
(1)运行结果
没有显示linux自解压代码打印出的“Uncompressing Linux... done, booting the kernel.”。
(2)现象分析
1)这个现象说明zImage没有被解压成功,因此问题出在解压相关的部分。
2)进一步分析,问题可能与“内核解压后的代码放在哪个内存地址处(暂且叫“解压地址”)”有关。这个解压地址可以由内核配置,如果不对,则内核不能运行。
3)内核的链接地址是一个虚拟地址,而自解压代码解压内核时需要物理地址(即解压地址是物理地址,因为此时mmu还没开,内核还没有开始运行),因此链接地址对应的物理地址必须等于解压地址,否则自解压之后内核无法运行。
4)因此现在问题变成:内核的链接等于多少?内核配置的解压地址是多少?
5)内核的链接地址与对应的物理地址,可以在arch/arm/kernel/head.S中可以查到。
PAGE_OFFSET这个宏定义在/arch/arm/include/asm/memory.h文件中,内容如下。
#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
而在配置内核时生成的.config文件中,CONFIG_PAGE_OFFSET赋值为0xC0000000。
CONFIG_PAGE_OFFSET=0xC0000000
所以PAGE_OFFSET=0xC000_0000。
在/arch/arm/mach-s5pv210/include/mach/memory.h文件中,对PHYS_OFFSET这个宏有如下定义:
根据下面“(5)现象分析”,PHYS_OFFSET这个宏的值应该为0x30000000。
而TEXT_OFFSET这个宏的值应该为0x00008000。(待求证)
经上述分析得知,内核的链接地址KERNEL_RAM_VADDR为0xC0008000,对应的物理地址KERNEL_RAM_PADDR是0x30008000,那么解压地址应该是30008000。
(3)修改解压地址
解压地址定义在/arch/arm/mach-s5pv210/Makefile.boot文件中。
我们在该文件末尾添加以下内容:
(4)编译后重新下载,查看运行结果
因为只修改了一些链接参数,不需要重新配置编译,所以make很快完成。
运行结果如下图所示,自解压代码的解压信息已经显示出来,但内核还是没有运行。
(5)现象分析
1)我们在uboot中配置的地址空间是从3000 0000开始的,而不是2000 0000开始的。
2)但在/arch/arm/mach-s5pv210/include/mach/memory.h文件中,PHYS_OFFSET却定义为2000 0000。
3)因此需要将PHYS_OFFSET从20000000改到30000000,如下所示:
(6)重新编译,运行结果
内核打印出很多运行信息。
二、内核中的机器码
内核支持什么架构、支持哪款cpu,这是如何确定的?主要是通过机器码来确定的。内核中定义了一份机器码,uboot也会给内核传递一个机器码。在内核启动的汇编阶段,head.S文件对比uboot传给内核的机器码和内核定义的机器码,如果匹配则继续启动。
本节内容,主要讲内核如何定义机器码的。其实在内核源码——C语言阶段的start_kernel函数的第二点的第3点的第(4)点也有提及。
1、MACHINE_START宏
这个宏的内容如下:
(1)这个宏用来定义一个数据类型为struct machine_desc的变量,而且这个变量会被定义到一个特定段.arch.info.init,因此这个变量将来会被链接器链接到这个.arch.info.init段中。
(2)比如在/arch/arm/mach-s5pv210/mach-smdkv210.c中,有MACHINE_START(SMDKV210, "SMDKV210"),这就定义了一个数据类型为struct machine_desc的变量,这个变量名为__mach_desc_SMDKV210。这个变量是smdkv210这个开发板的抽象或者说表征。
static const struct machine_desc __mach_desc_SMDKV210 \__used \__attribute__((__section__(".arch.info.init"))) = { \.nr = MACH_TYPE_SMDKV210,//机器码2456 \.name = "SMDKV210",.phys_io = S3C_PA_UART & 0xfff00000,.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,.boot_params = S5P_PA_SDRAM + 0x100,.init_irq = s5pv210_init_irq,.map_io = smdkv210_map_io,.init_machine = smdkv210_machine_init,.timer = &s5p_systimer, };
结构体成员.nr=MACH_TYPE_SMDKV210就是这个开发板的机器码。机器码值“MACH_TYPE_SMDKV210”在/include/generated/mach-types.h文件中定义,该文件在内核配置过程中自动生成。mach-types.h文件维护着该版本内核所支持的全部机器码。
结构体成员“.name= "SMDKV210"”,表示该开发板的名称。
结构体成员“.init_machine = smdkv210_machine_init”,定义了一个函数smdkv210_machine_init,该函数是一个硬件初始化函数,包含了内核中各种硬件的初始化信息。
(3)在/arch/arm/目录下有许多mach-xxx目录(不同目录代表不同CPU),这些mach-xxx目录中又有许多mach-xxx文件(同目录下的不同文件,表示相同CPU的不同开发板)。分析发现,每个mach-xxx.c文件中,都通过MACHINE_START宏,定义了一个表征某个开发板的struct machine_desc类型的变量。这些结构体变量都放到了.arch.info.init段,表示当前内核可以支持这些机器码对应的开发板。
(4)我们的目标开发板叫做X210,采用S5PV210这款CPU。在三星版本内核中到不到X210开发板对应的mach-x210.c文件,但为了减少移植工作量,在三星版本内核的/arch/arm/mach-s5pv210目录下寻找一个mach-xxx.c文件,使得该文件对应的开发板与X210开发板最为接近(使用相同的CPU),并以此文件为基础进行移植。
(5)九鼎的X210开发板是根据三星SMDKV210开发板进行设计的,因此要找SMDKV210开发板对应的文件,按理应该是mach-smdkv210.c文件。但分析/arch/arm/mach-s5pv210/Makefile文件后得知,内核配置生成的.config文件中定义CONFIG_MACH_SMDKV210后,实际绑定的是mach-smdkc110.c这个文件。因此实际上mach-smdkv210.c这个文件根本没用到。
(6)所以后续将以/arch/arm/mach-s5pv210/mach-smdkc110.c这个文件为基础进行移植。
2、硬件驱动的加载和初始化函数执行
/arch/arm/mach-s5pv210/mach-smdkc110.c文件中,有MACHINE_START(SMDKV210, "SMDKV210"),它展开后内容如下:
static const struct machine_desc __mach_desc_SMDKV210 \__used \__attribute__((__section__(".arch.info.init"))) = { \.nr = MACH_TYPE_SMDKV210,//机器码2456 \.name = "SMDKV210",.phys_io = S3C_PA_UART & 0xfff00000,.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,.boot_params = S5P_PA_SDRAM + 0x100,.init_irq = s5pv210_init_irq,.map_io = smdkv210_map_io,.init_machine = smdkv210_machine_init,//初始化各种硬件.timer = &s5p_systimer, };
注意“.init_machine = smdkv210_machine_init,”,init_machine这个成员指向一个机器硬件初始化函数smdkv210_machine_init,这个函数绑定了内核启动过程中会初始化的各种硬件的信息,在此函数中添加的硬件才会被初始化,没有在此函数中添加的硬件不会被初始化。
关于smdkv210_machine_init的更多介绍,见下文第五点“网卡的问题与解决”,或者驱动栏目中的“Linux设备驱动模型”的内容。
三、电源管理IC的问题与解决
1、OOPS信息(内核死亡信息)
(1)内核启动后会有打印信息,这些信息隐藏了问题所在。认真分析这些信息,从中找到对的或者错误的一些信息片段,才能帮助我们找到问题,从而解决问题。
(2)内核启动过程的错误信息的特征
由PC和LR的值可以看出,程序执行到dev_driver_string或者max8698_pmic_probe(这两个是函数或者汇编中的标号)符号部分的时候出错了。我们从这两个符号出发去寻找、思考可能出错的地方,然后试图解决。
2、错误分析
(1)max8698_pmic_probe,看名字是max8698这个电源管理IC的驱动安装函数那部分出错了,应该是开发板系统中配置了支持这个电源管理IC,于是启动时去加载它的驱动,结果驱动在加载执行的过程中出错了OOPS。
(2)这个驱动加载时为什么会出错?结合X210开发板的硬件实际情况来分析,X210开发板上根本就没有max8698这个电源管理IC,既然硬件都没有,执行驱动肯定会出错。之前移植三星版本uboot时,在lowlevel_init.S中也调用了电源管理IC初始化函数(PMIC_init),结果报错,我们屏蔽掉该函数的调用后,uboot就可以成功运行下去。
(3)为什么uboot和内核,都默认调用这个电源管理IC的初始化代码?因为三星SMDKV210开发板中用了max8698这个电源管理IC,因此三星的uboot和kernel中都默认支持这个。但是X210中是没用的,因此uboot和内核中都需要去掉该代码模块。
3、错误解决
(1)如何解决错误
在uboot中是直接改源代码,屏蔽掉那个初始化函数解决的。但在内核中不能这么干,因为linux kernel是高度模块化、高度可配置的,内核中每一个模块都是被配置项条件编译的,因此要去掉某个模块的支持,只需要重新配置去掉选项即可,不用改源代码。所以我们的关键就是要找它对应的配置项。
(2)解决错误的过程
先make menuconfig,然后输入/,搜索"MAX8698"这几个关键字,然后看到这个配置项的路径,然后到路径下去按N键去掉这个模块的支持,保存,重新编译即可。
(3)运行结果
重新编译后下载运行,发现此问题被解决,内核再次启动后直接运行到挂载rootfs才出错。
4、心得体会
(1)CONFIG_MFD_MAX8698这个配置宏,决定了drivers目录下的max8698对应的驱动程序源代码是否被编译,决定了kernel启动过程中是否会调用一些max8698的相关的代码。
(2)kernel是高度模块化和可配置化的,所以在内核中做任何事情(添加、更改、去掉一个模块)都必须按照内核设定的方案和流程。
四、iNand的问题与解决
1、OOPS信息
(1)内核试图挂载根文件系统时失败,失败原因是unknown-block(不能识别块设备)。
(2)分析backstrace,得知错误信息的来源于mount_block_root函数。
mount_block_root函数位于/init/do_mounts.c文件中,内容如下:
2、错误分析
(1)在kernel启动时,uboot给内核传递一个cmdline,其中“root=xxx”指定rootfs在哪个设备上,比如“root=/dev/mmcblk0p2”表示rootfs在mmc设备0的第2个分区(设备0,即在SD0通道上的设备,即iNand)。
(2)unknown-block(0,0),意思是没找到mmc设备0的第2分区。
(3)为什么没找到mmc设备0的第2分区?可能是kernel启动过程中加载mmc驱动时出现问题,驱动没有发现mmc设备0,于是问题定位在mmc相关的驱动方面。
(4)对比待移植的三星版本的内核启动信息、九鼎版本的内核启动信息,从中发现待移植的三星版本的内核启动时,没有找到MMC设备(内置的iNand和外置的SD卡都没有找到),这说明肯定是驱动的问题,因此要移植mmc驱动。
3、知识补充
(1)SD/iNand本身都是由一个一个的扇区组成的,裸机课程中X210的启动时,BL1在SD卡的1扇区开始往后存放,SD卡的0扇区是不用的。SD卡的0扇区是用来放置MBR的。
(2)MBR就是用来描述块设备的分区信息的,事先定义了一个通用的数据结构来描述块设备的分区,我们只要将分区信息写入MBR中即可对该设备完成分区。MBR默认就是在块设备的第0个扇区上存放的。
(3)uboot中的命令fdisk可以对iNand进行分区(执行“fdisk -c 0”即可),而且这个fdisk命令的内部已经用代码写死(将iNand分为多少个分区、每个分区的大小等等信息,已经用宏定义规定好,当然我们也可以修改这些宏定义)。内核通过读取iNand的MBR即可获取iNand的分区信息,不需要通过uboot给内核传参时传递这个分区信息。
(4)如果开发板使用的是nandFlash,分区表一般是在内核中用代码构建的。因此nand版本的内核移植时,一般都需要移植、更改nand分区表。
3、解决错误
暂略。因为涉及到驱动的内容,后续会补充。
五、网卡的问题与解决
1、OOPS信息
内核启动时,关于网卡部分的错误信息显示如下:
2、错误分析
(1)出错的原因是三星版本的内核还没有移植网卡驱动(有网卡驱动但没有移植)。
(2)在/arch/arm/mach-s5pv210/mach-smdkc110.c文件中,smdkc110_machine_init()函数是整个开发板所有硬件的初始化函数。此函数通过platform_add_devices()函数加载的硬件,将来启动时就会被初始化,没有被加载的硬件将来启动时就不管。所以网卡这个硬件相关的初始化内容,肯定位于smdkc110_machine_init()函数中。
(3)分析得知与网卡DM9000有关的是:smdkc110_devices结构体和smdkc110_dm9000_set()函数,需要分别做移植。
(4)smdkc110_dm9000_set()函数用来设置DM9000相关的SROM bank的寄存器,它相当于uboot中dm9000移植时的dm9000_pre_init函数,只是读写寄存器的函数名称不同而已。
(5)smdkc110_devices结构体中与网卡DM9000有关的设置,追踪如下。
在/arch/arm/mach-s5pv210/mach-smdkc110.c文件中有:
在/arch/arm/plat-s5p/devs.c中有:
在/arch/arm/plat-s5p/devs.c中有:
3、错误解决
(1)首先,在make menuconfig中添加DM9000支持。
通过在弹出的界面按/搜索“DM9000”,找到配置项所在路径,然后选择Y。其实这一步本来就是Y,因此这里不用设置。但是有些内核可能默认不是Y,因此要设置。
(2)接着,将九鼎移植好的/arch/arm/mach-s5pv210/mach-x210.c文件中的smdkc110_dm9000_set()函数,覆盖掉三星版本内核的/arch/arm/mach-s5pv210/mach-smdkc110.c文件中的smdkc110_dm9000_set()函数。
这主要是为了减少工作量,但修改的过程需要理解。
(3)接着,在arch/arm/plat-s5p/devs.c中修改DM9000相关的数据配置。
1)上图的S5P_PA_DM9000,定义在/arch/arm/mach-s5pv210/include/mach/map.h中,它表示DM9000的IO基地址,具体的值是与 DM9000接在哪个bank有关的。根据实际情况,将其改为0x88000300,如下所示。
2)因为九鼎的uboot源码/include/configs/x210_sd.h中有如下内容:
所以根据uboot的设置情况,下面的+2改为+4,如下所示。
3)根据实际情况,将两个IRQ_EINT9改成IRQ_EINT10。
(4)重新编译后下载运行,查看结果
网卡移植成功后,其启动信息显示如下:
六、内核启动汇编阶段的调试
1、调试的原因
(1)由之前的内容可知,内核启动汇编阶段主要体现在arch/arm/kernel/head.S文件。该文件中首先进行三个校验(CPU id的校验、机器码的校验、tag的校验),接着创建页表,然后构建C语言运行环境,最后跳转到start_kernel函数。基本上能运行到start_kernel,内核移植就不太会出问题了。
(2)但有时候移植的内核启动后,根本没有启动信息出来。针对这个问题,希望能有一种调试手段来确定问题所在。
2、调试的方法
(1)将led点亮和熄灭的汇编代码,复制粘贴到head.S文件中的合适位置,内核启动后根据led的表现来标明代码有无运行。
(2)典型的led函数如下所示。
/************************************************************************** *函数作用:熄灭X210开发板的三颗LED灯 *函数说明 (1)寄存器r0,r1,r2在head.s文件中是用来给内核传参的,类似于全局变量。 因此不能用这些寄存器,我们可以使用那些没有被使用的寄存器,这里用r3,r4。 (2)X210开发板有四颗LED灯,启动时四颗都是半亮的。这个函数用来熄灭其中的三颗。 我们可以通过LED是否熄灭得知该函数之前的内核代码是否运行正常。 ***************************************************************************/led:// 第一步:把0x11111111写入0xE0200240(GPJ0CON)位置ldr r3, =0x11111111 ldr r4, =0xE0200240 str r3, [r4] // 把r3中的数写入到r4中的数为地址的内存中去 // 第二步:把0xff写入0xE0200244(GPJ0DAT)位置ldr r3, =0xff // 开发板启动时led半亮ldr r4, =0xE0200244str r3, [r4] // 把ff写入到GPJ0DAT寄存器中,引脚即输出高电平,三颗LED熄灭// 函数需要返回mov pc,lr
3、动手测试
首先,在head.S文件中合适的地方(函数集中域处)定义这个led这个函数;
然后,在head.S文件中的某个地方调用这个led函数;
最后,重新编译内核并下载至开发板,通过查看LED的亮灭情况判断这段代码是否被运行。如果运行了,则说明调用这个led函数之前的内容都没有问题的;如果没有运行,则说明错误发生在调用这个led函数之前。