移植Linux内核

嵌入式Linux系统移植之移植Linux内核

《嵌入式Linux应用完全开发手册》第3篇第16章总结归纳

本章目标

  1. 了解内核源码结构,了解内核启动过程
  2. 掌握内核配置方法
  3. 移植内核同时支持S3C2410、S3C2440
  4. 掌握MTD设备的分区方法
  5. 掌握YAFFS文件系统的移植方法

Linux 版本及特点

Linux内核的版本号可以从源代码的顶层目录下的Makefile中看到,比如下面几行它们构成了Linux的版本号:2.6.22.6。

1
2
3
4
VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 22
EXTRAVERSION = .6

其中的“VERSION”和“PATCHLEVEL”组成主版本号,比如2.4、2.5、2.6等,稳定版本的主版本号用偶数表示(比如2.4、2.6),每隔2-3年出现一个稳定版本。开发中的版本用技术表示(2.3、2.5),它是下一个稳定版本的前身。
“SUBLEVEL”称为此版本号,它不分奇偶顺序递增。每隔1-2个月发布一个稳定版本。
“EXTRAVERSION”称为扩展版本号,它不分奇偶,顺序递增。每周发布几次扩展版本号,修正最新的稳定版本的问题。
Linux内核的最初版本在1991年发布,这是Linus Torvalds为他开发的386开发的一个类Minix的操作系统。
Linux 1.0的官方版发行于1994年3月,包含了386的官方支持,仅支持单CPU系统。
Linux 1.2发行于1995年3月,它是第一个包含多平台(Alpha、Sparc、Mips等)支持的官方版本。
Linux 2.0发行于1996年6月,包含很多新的平台支持,但是最重要的是,它是第一个支持SMP(对称多处理器)体系的版本。
Linux 2.2发行于1999年1月,它带来了SMP系统性能的极大提升,同时支持更多的硬件。
Linux 2.2发行于2001年1月,它进一步提升了SMP系统的扩展性,同时它集成了很多用于支持桌面系统的特性:USB、PC卡(PCMCIA)的支持,内置的即插即用等。
Linux 2.6发行于2003年12月,在Linux 2.4的基础上作了极大的改进。2.6内核支持更多的平台,从小规模的嵌入式系统到服务器级的64位的系统;使用新的调度器,进程的切换更高效;内核可被抢占,使得用户的操作可以得到更快速的响应;I/O子系统也经历很大的修改,使得它在各种工作负荷下都更具响应性;模块子系统、文件系统都做了大量的改进。另外,以前使用Linux的变种μClinux来支持没有MMU的处理器,现在2.6版本的Linux中已经合入了μClinux的功能,也可以支持没有MMU的处理器。

Linux移植准备

获取Linux内核源码

登录Linux内核的官方网站http://www.kernel.org/,可以看到下图所示的内容:
img not found
上面标明了Linux内核的最新稳定版本、正在开发的测试版本,图中间的版本号就是各种补丁的链接地址。各种标记符的意义如下表所示:

标记 描述
F 全部代码,单击“F”即可下载全部内核代码
B 当前补丁基于哪个版本的内核,单击“B”可以下载这个内核
V 查看补丁文件的信息,修改了哪些文件
VI 查看与上一个扩展版本相比,修改了哪些文件
C 当前的修改记录
ChangeLog 正式的修改记录,由开发者提供

一般而言,各种补丁文件都是基于内核的某个正式版本生成的,除非使用标记符“B”指明了它所基于的版本。比如有补丁文件patch-2.6.xx.1、patch-2.6.xx.2、patch-2.6.xx.3,它们都是基于内核2.6.xx生成的补丁文件。使用时可以在内核2.6.xx上直接打补丁patch-2.6.xx.3,并不需要先打上补丁文件patch-2.6.xx.1、patch-2.6.xx.2;相应的,如果已经打上了补丁文件patch-2.6.xx.2,在打补丁之前,要先去除补丁文件patch-2.6.xx.2。
本书在Linux2.6.22.6上进行移植开发。下载linux-2.6.22.6.tar.bz2后如下解压即可得到目录linux-2.6.22.6,里面存放了内核源码,如下所示:

1
tar xjf linux-2.6.22.6.tar.bz2

也可以先下载内核源文件linux-2.6.22.tar.bz2、补丁文件patch-2.6.22.6.bz2,然后解压、打补丁:

1
2
3
4
tar xjf linux-2.6.22.tar.bz2
tar xjf patch-2.6.22.6.bz2
cd linux-2.6.22
patch -p1 < ../patch-2.6.22.6

内核源码结构及Makefile分析

内核源码结构

Linux内核文件有近2万,出去其他架构CPU的相关文件,支持S3C2410、S3C2440这两款芯片的完整内核文件有1万多个。这些文件的组织结构并不复杂,它们分别位于顶层目录下的17个子目录,各个目录功能独立。下表描述各目录的功能,最后两个目录不包含代码。

目录名 描述
arch 体系结构相关的代码,对于每个架构的CPU,arch下有一个对应的子目录,比如arch/armarch/i386
block 块设备的通用函数
crypto 常用的加密和散列算法(AES、SHA),还有一些压缩和CRC校验算法
drivers 所有的设备驱动程序,里面的每一个子目录对应一类驱动程序,比如drivers/block为块设备驱动程序,drivers/char为字符设备驱动程序,drivers/mtd为NOR Flash、NAND Flash等存储设备的驱动程序
fs Linux支持的文件系统的代码,每一个子目录对应一种文件系统,比如fs/jffs2fs/ext2fs/ext3
include 内核头文件,有基本头文件(存放在include/linux目录下)、各种驱动或功能部件的头文件(比如include/mediainclude/mtdinclude/net)、各种体系相关的头文件(比如include/asm-arminclude/asm-i386)。当配置内核之后,include/asm是某个include/asm-xxx的链接
init 内核的初始化代码(不是系统的引导代码),其中的main.c文件中的start_kernel函数是内核引导后运行的第一个函数
ipc 进程间的通信代码
Kernel 内核管理的核心代码,与处理器相关的代码位于arch/*/kernel
lib 内核用到的一些库函数的代码,比如crc32.cstring.c,与处理器相关的库函数代码位于arch/*/lib目录下
mm 内存管理代码,与处理器相关的内存管理代码位于arch/*/mm目录下
net 网络支持代码,每个子目录对应与网络的一个方面
security 安全、密钥相关的代码
sound 音频设备的驱动代码
usr 用来制作一个压缩的cpio归档文件:initrd的镜像,它可以作为内核启动后挂接的第一个文件系统
Documentation 内核文档
scripts 用于配置、编译内核的脚本文件

对于ARM架构的S3C2410、S3C2440,其体系相关的代码在arch/arm目录下,在后面进行Linux移植时,开始的工作正是修改这个目录下的文件。内核代码的层次结构如下图:
img not found

Linux Makefile 分析

内核中的哪些文件将被编译,怎样被编译,连接顺序如何确定,哪个文件在最前面,哪些文件或函数先执行。这些都是通过Makefile来管理的。

  1. 决定编译哪些文件
  2. 怎样编译这些文件
  3. 怎样连接这些文件

Linux内核源码中含有很多个Makefile文件,这些Makefile文件又要包含其他一些文件(比如配置信息、通用的规则等 )。这些文件构成了Linux的Makefile体系:

名称 描述
顶层Makefile 它是所有Makefile文件的核心,从总体上控制着内核的编译、连接
.config 配置文件,在配置内核时生成,所有的Makefile文件(包括顶层目录及各级子目录)都是根据.config文件来决定使用哪些文件
arch/$(ARCH)/Makefile 对应体系结构的Makefile,它用来决定哪些体系结构相关的文件参与内核的生成,并提供一些规则来生成特定格式的内核映像
scipts/Makefile.* Makefile共用的通用规则、脚本等
kbuild Makefiles 各级子目录下的Makefile,它们相对简单,被上一层Makefile调用来编译当前子目录下的文件

内核文档Documentation/kbuild/makefiles.txt对内核中的Makefile作用、用法讲解的非常透彻,以下根据前面总结的Makefile的3大作用分析这5类文件。

  1. 决定编译哪些文件
    Linux 内核的编译过程从顶层Makefile开始,然后递归进入各级子目录调用它们的Makefile,分为3个步骤。
    ①顶层Makefile决定内核根目录下哪些子目录将被编进内核。
    arch/$(ARCH)/Makefile决定arch/$(ARCH)目录下哪些文件、哪些目录将被编进内核。
    ③各级子目录下的Makefile决定所在目录下哪些文件将被编进内核,哪些文件将被编成模块(即驱动程序),进入哪些子目录继续调用它们的Makefile。

先看步骤①,在顶层Makefile中可以看到如下内容:

1
2
3
4
5
6
7
init-y          :   init/
drivers-y : drivers/ sound/
net-y : net/
libs-y : lib/
core-y : usr/
...
core-y += kernerl/ mm/ fs/ ipc/ security/ crypto/ block/

可见,顶层Makefile将这13个子目录分为5类:init-y、drivers-y、net-y、libs-y和core-y。之前上表中的17个子目录,出去include目录和后面两个不包含内核代码的目录外,还有一个arch目录没有出现在内核中。它在arch/$(ARCH)/Makefile中被包含进内核,在顶层Makefile中直接包含了这个Makefile,如下所示:

1
include $(srctree)/arch/$(ARCH)/Makefile

对于ARCH变量,可以在执行make命令时传入,比如“make ARCH=arm …”。另外,对于非x86平台,还需要指定交叉编译工具,这也可以在执行make命令时传入,比如“make CROSS_COMPILE=arm-linux- …”。为了方便,常在顶层Makefile中进行修改。
修改前:

1
2
ARCH    ?= $(SUBARCH)
CROSS_COMPILE ?=

修改后:

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

对于步骤②的arch/$(ARCH)/Makefile,以ARM体系为例,在arch/arm/Makefile中可以看到如下内容:

1
2
3
4
5
6
7
8
9
10
head-y  := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o 
...
core-y += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
core-y += $(MACHINE)
core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2400/
core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2412/
core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2440/
...
libs-y := arch/arm/lib/ $(libs-y)
...

由第1行可知,除前面的5类子目录外,又出现了另一类:head-y,不过它是直接以文件名出现。MMUEXT在arch/arm/Makefile前面定义,对于没有MMU的处理器,MMUEXT的值为-nommu,使用文件head-nommu.S;对于有MMU的处理器,MMUEXT的值为空,使用文件head.S。
arch/arm/Makefile中类似第3、4、5行的代码进一步扩展了core-y的内容,第9行扩展了libs-y的内容,这些都是体系结构相关的目录。第5-7行中的CONFIG_ARCH_S3C2410在配置内核时定义,它的值有3种:y、m、空。y表示编进内核,m表示编为模块,空表示不使用。
编译内核时,将依次进入init-y、core-y、libs-y、drivers-y和net-y所列出的目录执行它们的Makefile,每个子目录都会生成一个build-in.o(libs-y所列目录下,有可能lib.a文件)。最后,head-y所表示的文件将和这些build-in.o、lib.a一起被连接成内核映像文件vmlinux。
最后,步骤③是如何进行的。
在配置内核时,生成配置文件.config。内核顶层Makefile使用如下语句间接包含.config文件,以后就根据.config中定义的各个变量来决定编译哪些文件。之所以说是“间接”包含,是因为包含的是include/config/auto.conf文件,而它只是将.config文件中的注释去掉,并根据顶层Makefile中定义的变量增加一些变量而已。

1
2
#Read in config
-include include/config/auto.conf

include/config/auto.conf文件的生成过程不再描述,它与.config的格式相同,摘选部分内容如下:

1
2
3
4
5
6
7
8
CONFIG_ARCH_SMDK2410=y
CONFIG_ARCH_S3C2440=y
# .config中没有下面这行,它是根据顶层Makefile中定义的内核的版本号增加的
CONFIG_KERNELVERSION="2.6.22.6"
# .config中没有下面这行,它是根据顶层Makefile中定义的ARCH变量增加的
CONFIG_ARCH="arm"
CONFIG_JFFS2_FS=y
CONFIG_LEDS_S3C24XX=m

include/config/auto.conf文件中,变量的值主要有两类:“y”和“m”。各级子目录的Makefile使用这些变量来决定哪些文件被编译进内核中,哪些文件被编成模块(即驱动程序),要进入哪些下一级子目录继续编译,这通过以下4种方法来确定(obj-y、obj-m、lib-y是Makefile中的变量)。

①obj-y用来定义哪些文件被编进(build-in)内核。
obj-y中定义的.o文件由当前目录下的.c或.S文件编译生成,它们连同下级子目录的build-in.o文件一起被组合成(使用“$(LD) -r”命令)当前目录下的build-in.o文件。这个build-in.o文件将被它的上一层Makefile使用。
obj-y中各个.o文件的顺序是有意义的,因为内核中用moudule_init__initcall定义的函数将按照它们的连接顺序被调用。
例子1,当下面的CONFIG_ISDN、CONFIG_ISDN_PPP_BSDCOMP在.config中被定义为y时,isdn.c或isdn.S、isdn_bsdcomp.c或isdn_bsdcomp.S被编译成isdn.o、isdn_bssdcomp.o。这两个.o文件被组合进buidl-in.o文件中,最后被链接进入内核。假如isdn.o、isdn_bsdcomp.o中分别用moudule_init(A)moudule_init(B)定义了函数A、B,则内核启动时A先被调用,然后是B。

1
2
obj-$(CONFIG_ISDN)      += isdn.o
obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o

②obj-m用来定义哪些文件被编译成可加载模块(Loadable module)。
obj-m中定义的.o文件有当前目录下的.c或.S文件编译生成,它们不会被编进build-in.o中,而是被编成可加载模块。
一个模块可以由一个或多个.o文件组成。对于只有一个源文件的模块,在obj-m中直接增加它的.o文件即可。对于有多个源文件的模块,除在obj-m中增加一个.o文件外,还要定义一个-objs变量来告诉Makefile这个.o文件由哪些文件组成。
例子2,当下面的CONFIG_ISDN_PPP_BSDCOMP在.config文件中被定义为m时,isdn_bsdcomp.c或isdn_bsdcomp.S将被编译成isdn_bsdcomp.o文件,它最后被制作成isdn_bsdcomp.ko模块。

1
2
#drivers/isdn/i4l/Makefile
obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o

例子3,当下面的CONFIG_ISDN在.config文件中被定义为m时,将会生成一个isdn.o文件,它由isdn-objs中定义的isdn-net-lib.o、isdn_v110.o、isdn_common.o等3个文件组合而成。isdn.o最后被制作成isdn.ko模块。

1
2
3
#drivers/isdn/i4l/Makefile
obj-$(CONFIG_ISDN) += isdn.o
isdn-objs := isdn_net_lib.o isdn_v110.o isdn_common.o

③lib-y用来定义哪些文件被编成库文件
lib-y中定义的.o文件由当前目录下的.c或.S文件编译生成,它们被打包成当前目录下的一个库文件:lib.a。
同时出现在obj-y、lib-y中的.o文件,不会被包含进lib.a中。
要把这个lib.a编译进入内核中,需要在顶层Makefile中libs-y变量中列出当前目录。要编成库文件的内核代码一般都在这两个目录下:lib/arch/$(ARCH)/lib

④obj-y、obj-m还可以用来指定要进入的下一级子目录。
Linux中一个Makefile文件只负责生成当前目录下的目标文件,子目录下的目标文件由子目录的Makefile生成。Linux的编译系统会自动进入这些子目录调用他们的Makefile,只是需要在进入之前指定这些子目录。
这要用到obj-y、obj-m,只要在其中增加这些子目录名即可。
例子4fs/Makefile中有如下一行,当CONFIG_JFFS2_FS被定义为y或m时,在编译时将会进入jffs2目录进行编译。Linu的编译系统只会根据这些信息决定是否进入下一级目录,而下一级目录的文件如何编译成build-in.o或模块由它的Makefile决定。

1
obj-$(CONFIG_JFFS2_FS)      += jffs2
  1. 怎么编译这些文件
    即编译选项、连接选项是什么。这些选项分为3类:全局的,适用于整个内核代码树;局部的,仅适用于某个Makefile中的所有文件;个体的,仅适用于某个文件。
    全局选项在顶层Makefile和arch/$(ARCH)/Makefile中定义,这些选项的名称为:CFLAGS、AFLAGS、LDFLAGS、ARFLAGS,它们分别是编译C文件的选项、编译汇编文件的选项、连接文件的选项、制作库文件的选项。
    需要使用局部选项时,它们在各个子目录中定义,名称为:EXTRA_FLAGS、EXTRA_ALAGS、EXTRA_LDFLAGS、EXTRA_ARFLAGS,它们的用途与前述选项相同,只是适用范围比较小,它们针对当前的Makefile中的所有文件。
    另外,如果想针对某个文件定义它的编译选项,可以使用CFLAGS_$@,AFLAGS_$@。前者用于编译某个C文件,后者用于编译某个汇编文件。$@表示某个目标文件名,比如以下代码表示编译aha152x.c时,选项中要额外加上“-DAHA152X_STAT -DAUTOCONF”。

    1
    2
    # drivers/scsi/Makefile
    CFLAGS_aha152x.o = -DAHA152X_STAT -DAUTOCONF

    需要注意的是,这3类选项是一起用的,在scropts/Makefile.lib中可以看到。

    1
    _c_flags  = $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$(basetarget).o)
  2. 怎样连接这些文件,它们的顺序如何
    前面分析有哪些文件需要编译进入内核时,顶层Makefile和arch/$(ARCH)/Makefile定义了6类目录(或文件):head-y、init-y、drivers-y、net-y、libs-y和core-y。它们的初始值如下(以ARM体系为例):
    arch/arm/Makefile中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    head-y    := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
    ...
    core-y := arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
    core-y := $(MACHINE)
    core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2400/
    core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2412/
    core-$(CONFIG_ARCH_S3C2410) += arch/arm/mach-s3c2440/
    ...
    libs-y := arch/arm/lib/ $(libs-y)

    顶层Makefile中:

    1
    2
    3
    4
    5
    6
    7
    init-y  :=  init/
    drivers := drivers/ sound/
    net-y := net/
    libs-y := lib/
    core-y := usr/
    ...
    core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/

    可见,除head-y之外,其余的init-y、drivers-y等都是目录名。在顶层Makefile中,这些目录名的后面直接加上build-in.o或lib.a,表示要连接进内核的文件,如下所示:

    1
    2
    3
    4
    5
    6
    7
    init-y  :=  $(patsubst %/, %/build-in.o, $(init-y))
    core-y := $(patsubst %/, %/build-in.o, $(core-y))
    drivers-y := $(patsubst %/, %/build-in.o, $(drivers-y))
    net-y := $(patsubst %/, %/lib.a, $(net-y))
    libs-y1 := $(patsubst %/, %/build-in.o, $(libs-y))
    libs-y2 := $(patsubst %/, %/build-in.o, $(libs-y))
    libs-y := $(libs-y1 $(libs-y2))

    上面的patsubst是个字符串处理函数,它的用法如下:

    1
    $(patsubst pattern,replacement,text)

    表示寻找“text”中符合格式“pattern”的字,用“replacement”替换它们。比如上面的init-y初值为“init/”,经过交换之后,“init-y”变为“init/build-in.o”。
    顶层Makefile中,继续往下看:

    1
    2
    3
    4
    vmlinux-init  :=  $(head-y) $(init-y)
    vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
    vmlinux-all := $(vmlinux-linux) $(vmlinux-main)
    vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds

    第3行的vmlinux-all表示所有构成内核映像文件vmlinux的目标文件,从第1-3行可知这些目标文件的顺序为:head-y、init-y、core-y、libs-y、drivers-y、net-y,即arch/arm/kernel/head.o(假设有MMU,否则为head-nommu.o)、arch/arm/kernel/init_task.oinit/build-in.ousr/build-in.o等。
    第4行表示连接脚本为arch/$(ARCH)/kernel/vmlinux.lds。对于ARM体系,连接脚本就是arch/arm/kernel/vmlinux.lds,它由arch/arm/kernel/vmlinux.lds.S文件生成。规则在scripts/Makefile.build中,如下所示:

    1
    2
    $(obj)/%.lds: $(src)/%.lds.S  FORCE
    $(call if_changed_dep,cpp_lds_S)

    现将生成的arch/arm/kernel/vmlinux.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
    SECTIONS
    {
    ...
    . = 0xc0000000 + 0x00008000; /* 代码段起始地址,这是个虚拟地址 */

    .text.head : {
    _stext = .;
    _sinittext = .;
    *(.text.head)
    }

    .init : { /* 内核初始化的代码和数据 */
    ...
    }
    ...
    .text : { /* 真正的代码段 */
    _text = .; /* 代码段和只读数据段的开始地址 */
    ...
    }

    /* 只读数据 */
    . = ALIGN((4096)); .rodata:AT(ADDR(.rodata) - 0) {......} . = ALIGN((4096));

    _etext = .; /* 代码段和只读数据段的结束地址 */
    ... ...
    .data : AT(__data_loc) { /* 数据段 */
    __data_start = .; /* 数据段起始地址 */
    ... ...
    _edata = .; /* 数据段结束地址 */
    }
    _edata_loc = __data_loc + SIZEOF(.data); /* 数据段结束地址 */

    .bss : { /* BSS段,没有初始化或初值为0的全局、静态变量 */
    __bss_start = .; /* BSS段起始地址 */
    *(.bss)
    *(COMMON)
    _end = .; /* BSS段结束地址 */
    }

    .stab 0 : { *(.stab) } /* 调试信息段 */
    ... ...

    下面对本节分析Makefile的结果作一下总结。

  3. 配置文件.config中定义了一系列的变量,Makefile将结合它们来决定哪些文件被编进内核、哪些文件被编成模块、涉及哪些子目录。

  4. 顶层Makefile和arch/$(ARCH)/Makefile决定根目录下哪些子目录、arch/$(ARCH)目录下哪些文件和目录将被编进内核。

  5. 最后,各级子目录下的Makefile决定所在目录下哪些文件将被编进内核,哪些文件将被编成模块(驱动程序),进入哪些子目录继续调用它们的Makefile。

  6. 顶层Makefile和arch/$(ARCH)/Makefile设置了可以影响所有文件的编译、连接选项:CFLAGS、AFLAGS、LDFLAGS、ARFLAGS。

  7. 各级子目录下的Makefile中可以设置能够影响当前目录下所有文件的编译、连接选项:EXTRA_CFLAGS、EXTRA_AFLAGS、EXTRA_LDFLAGS、EXTRA_ARFLAGS;还可以设置可以影响某个文件的编译选项:CFLAGS_$@,AFLAGS_$@。

  8. 顶层Makefile按照一定的顺序组织文件,根据连接脚本arch/$(ARCH)/kernel/vmlinux.lds生成内核映像文件vmlinux。

内核的Kconfig分析

在内核目录下执行“make menuconfig ARCH=arm CROSS_COMPILE=arm-linux-”时,就会看到一个如下图所示的菜单:
img not found
这就是内核的配置界面。通过配置界面,可以选择芯片类型、选择需要支持的文件系统,去除不需要的选项等,这就称为“配置内核”。注意,也有其他形式的配置界面,比如“make config”命令启动字符配置界面,对于每个选项都会依次出现一行提示信息,逐个回答;“make xconfig”命令启动X-windows图形配置界面。
所有配置工具都是通过读取arch/$(ARCH)/Kconfig文件来生成配置界面,这个文件是所有配置文件的总入口,它会包含其他目录的Kconfig文件。配置界面如上图所示。
内核源码的每个子目录中,都有一个Makefile文件和Kconfig文件。Makefile的作用前面已经讲述,Kconfig用于配置内核,它就是各种配置界面的源文件。内核的配置工具读取各个Kconfig文件,生成配置界面供开发人员配置内核,最后生成配置文件.config。
内核的配置界面以树状的菜单形式组织,主菜单下有若干子菜单,子菜单下又有子菜单或配置选项。每个子菜单或选项可以有依赖关系,这些依赖关系用来确定它们上是否显示。只有被依赖项的父项已经被选中,子项才会显示。
Kconfig文件的语法可以参考Documentation/kbuild/kconfig-language.txt文件,下面讲述几个常用的语法,并在最后介绍菜单形式的配置界面操作方法。

Kconfig 文件的基本要素:config条目(entry)

config条目常被其他条目包含,用来生成菜单、进行多项选择等。
config条目用来配置一个选项,或者这么说,它用于生成一个变量,这个变量会连同它的值一起被写入配置文件.config中。比如有一个config条目用来配置CONFIG_LEDS_S3C24XX,根据用户的选择,.config文件中可能出现下面3种配置结果中的一个。

1
2
3
CONFIG_LEDS_S3C24XX=y         # 对应的文件被编进内核
CONFIG_LEDS_S3C24XX=m # 对应的文件被编成模块
#CONFIG_LEDS_S3C24XX # 对应的文件没有被使用

以一个例子说明config条目格式,下面代码选自fs/Kconfig文件,它用于配置CONFIG_JFFS2_FS_POSIX_ACL选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
config JFFS2_FS_POSIX_ACL
bool "JFFS2 POSIX Access Control Lists"
depends on JFFS2_FS_XATTR
default y
select FS_POSIX_ACL
help
Posix Access Control Lists (ACLs) support permissions for users and
groups beyond the owner/group/world scheme.

To learn more about Access Control Lists, visit the Posix ACLs for
Linux website <http://acl.bestbits.at/>.

If you don't know what Access Control Lists are, say N

代码中包含了几乎所有的元素,下面一一说明:
第1行中,config是关键字,表示一个配置选项的开始;紧跟着的JFFS2_FS_POSIX_ACL是配置选项的名称,省略了前缀“CONFIG_”。
第2行中,boot表示变量类型,即CONFIG_JFFS2_FS_POSIX_ACL的类型。有5种类型:bool、tristate、string、hex和int,其中的tristate和string是基本的类型,其他类型是它们的变种。boot变量取值有两种:y和n;tristate变量取值有3种:y、n和m;string变量取值为字符串;hex变量取值为十六进制的数据;int变量取值为十进制的数据。
“boot”之后的字符串是提示信息,在配置界面中上下移动光标选中它时,就可以通过按空格或回车来设置CONFIG_JFFS2_FS_POSIX_ACL的值。提示信息的完整格式如下,如果使用“if”,则当expr为真时才显示提示信息。在实际使用时,prompt关键字可以省略。

1
"prompt" <prompt> ["if" <expr>]

第3行表示依赖关系,格式如下。只有JFFS2_FS_XATTR配置选项被选中时,当前配置选项中的提示信息才会出现,才能设置当前配置选项。注意,如果依赖条件不满足,则它取默认值。

1
"depend on "/"requires" <expr>

第4行的表示默认值为y,格式如下:

1
"default" <expr> ["if" <expr>]

第5行表示当前配置选项JFFS2_FS_POSIX_ACL被选中时,配置选项FS_POSIX_ACL也会被自动选中,格式如下:

1
"select" <symbol> ["if" <expr>]

第6行表示下面几行是帮助信息,帮助信息的关键字有如下两种,它们完全一样。当遇到一行的缩进距离比第一行帮助信息的缩进距离小时,表示帮助信息已经结束。

1
"help" or "---help---"

menu条目用于生成菜单,格式如下:

1
2
3
4
"menu" <prompt>
<menu options>
<menu block>
"endmenu"

它的实际使用并不如它的标准格式那样复杂,下面是一个例子:

1
2
3
4
5
6
7
8
menu "Floating point emulation"

config FPE_NWFPE
... ...
config FPE_NWFPE_XP
... ...
... ...
endmenu

menu之后的字符串是菜单名,“menu”和“endmenu”之间有很多config条目。在配置界面会出现如下字样的菜单,移动光标选中它后按回车键进入,就会看到这些config条目定义的配置选项。

1
Floating point emulation  --->

choice 条目

choice条目将多个类似的配置选项组合在一起,供用户单选或多选,格式如下:

1
2
3
4
"choice"
<choice options>
<choice block>
"endchioce"

实际使用中,也是在“choice”和“endchoice”之间定义多个config条目,比如arch/arm/Kconfig中有如下代码:

1
2
3
4
5
6
7
8
9
choice 
prompt "ARM system type"
default ARCH_VERSATILE

config ARCH_AAEC2000
...
config ARCH_INTEGRATOR
...
endchoice

prompt“ARM system type”给出提示信息“ARM system type”,光标选中它之后按回车键进入,就可以看到多个config条目定义的配置选项。
choice条目中定义的变量类型只能有两种:bool和tristate,不能同时有这两种类型的变量。对于bool类型的choice条目,只能在多个选项中选择一个;对于tristate类型的choice条目,要么就把一个或多个选项色设为m;要么就像bool类型的choice条目一样,只能选择一个。这是可以理解的,比如对于同一个硬件,它有多个驱动程序,可以选择将其中之一编译进内核(配置选项设置为y),也可以都将它们编译成模块(配置选项设置为m)。

comment 条目

comment条目用于定义一些帮助信息,它在配置过程中出现界面的第一行;并且这些帮助信息会出现在配置文件中,格式如下:

1
2
"comment" <prompt>
<comment options>

实际使用也很简单,比如arch/arm/Kconfig

1
2
3
4
menu "Floating point emulation"

comment "At least one emulation must be selected"
...

进入菜单“Floating point emulation —>”之后,在第一行会看到如下内容:

1
--- At least one emulation must be selected

而在.config文件中也会看到如下内容:

1
2
3
#
# At least one emulation must be selected
#

source 条目

source 条目用于读入另一个Kconfig文件,格式如下:

1
"source" <prompt>

下面是一个例子,取自arch/arm/Kconfig,它读入net/Kconfig文件。

1
source "net/Kconfig"

菜单形式的配置界面操作方法

配置界面的开始几行就是它的操作方法,如下图所示:
img not found
内核scripts/kconfig/mconf.c文件的注释给出了更详细的操作方法,讲解如下:
一些特定功能的文件可以直接编译进内核中,或者编译成一个可加载模块,或者根本不使用它们。还有一些内核参数,必须给它们赋一个值:十进制数、十六进制数,或者一个字符串。
配置界面中,以[*]<M>[]开头的选项表示相应的功能被编译进内核中、被编译成一个模块,或者没有使用。尖括号<>表示相应功能的文件可以被编译成模块。
方向键用来高亮选中某个配置选项,如果要进入某个子菜单,先选中它,然后按回车键进入。配置选项的名字后有--->表示它是一个子菜单。配置选项的名称中有一个高亮的字母,它被称为热键(hotkey),直接输入热键就可以选中该配置选项,或者循环选中具有相同热键的配置选项。
可以使用翻页键<Page Up><Page Down>来移动配置界面中的内容。
要退出配置界面,使用方向键选中<Exit>按钮,然后按回车键。如果没有配置选项使用后面这些按键作为热键的话,也可以按两次<ESC><E><X>键退出。
<TAB>键可以在<Select>ExitHelp这3个按钮中循环选中它们。
要想阅读某个配置选项的帮助信息,选中它之后,再选中<Help>按钮,按回车键;也可以选中配置选项后,直接按<H><?>键。
对于choice条目中的多个配置选项,使用方向键高亮选中某个配置选项,按<S>或空格键选中它;也可以通过输入配置选项的首字母,按<S>或空格键选中它。
对于int、hex或string类型的配置选项,要输入它们的值时,先高亮选中选中它,按回车键,输入数据,再按回车键。对于十六进制数据,前缀0x可以省略掉。
配置界面的最下面,如下图红框所示:
img not found
前者用于加载某个配置文件,后者用于将当前的配置保存到某个配置文件中去。需要注意的是,如果不使用这两个选项,配置的加载文件、输出文件都默认为.config文件;如果加载了其他的文件(假设文件名为A),然后在它的基础上进行修改,最后退出保存时,这些变化会保存到A中去,而不是.config。
当然,可以先加载文件A,然后修改,最后保存到.config中去。

Linux 内核配置选项

Linux内核配置选项多达上千个,一个个的进行选择既耗费时间,对开发人员的要求也比较高(需要了解每个配置选项的作用)。一般的做法是在某个默认配置文件的基础上进行修改,比如我们可以先加载配置文件arch/arm/configs/s3c2410_defconfig,再增加、去除某些配置选项。

配置界面主菜单的类别

下表讲解了主菜单的类别,以后读者配置内核时,可以根据自己所要设置的功能进入某个菜单,然后根据其中的各个配置选项的帮助信息进行配置。

配置界面主菜单 描述
Code maturity level options 代码成熟度选项:用于包含一些正在开发的或者不成熟的代码、驱动程序。一般不设置
Genaral setup 常规设置:比如增加附加的内核版本号、支持 内存交换功能、System V进程间通信等。除非很熟悉其中的内容,否则一般使用默认配置
Loadable module support 可加载模块支持:一般都会打开可加载模块支持(Enable loadable module support)、允许卸载已经加载到模块(Module unloading)、让内核通过运行modprobe来自动加载所需要的模块(Automatic kernel module loading)
Block layer 块设备层:用于设置块设备的一些总体参数,比如是否支持大于2TB的块设备、是否支持大于2TB的文件、设置I/O调度器等。一般使用默认值即可
System Type 系统类型:选择CPU的架构、开发板类型等与开发板相关的配置选项
Bus support PCMCIA/CardBus总线的支持
Kernel Features 用于设置内核的一些参数,比如是否支持内核抢占、是否支持动态修改系统时钟等
Boot options 启动参数:比如设置默认的命令行参数等
Floating point emulation 浮点运算仿真功能:目前Linux 还不支持硬件浮点运算,所以要选择一个浮点仿真器,一般选择“NWFPE math emulation”
Userspace binary formats 可执行文件格式:一般都支持ELF、a.out格式
Power management options 电源管理选项
Networking 网络协议选项:一般选择“Networking support”以支持网络功能,选择“Packet socket”以支持socket接口功能,选择“TCP/IP networking”以支持TCP/IP网络协议。通常可以在选择“Networking support”后使用默认配置
Device Drivers 设备驱动程序:几乎包含了Linux的所有驱动程序
File systems 文件系统:可以在里面选择要支持的文件系统,比如EXT2、JFFS2等
Profiling support 对系统的活动进行分析,仅供内核开发者使用
Kernel hacking 调试内核时的各种选项
Security options 安全选项,一般使用默认配置
Cryptographic options 加密选项
Library routines 库子程序:比如CRC32检验函数、zlib压缩函数等。不包含在内核源码中的第三方内核模块可能需要这些库,可以全不选,内核中若有其他部分依赖它,会自动选上

“System Type” 菜单:系统类型

对于arm平台(在顶层Makefile中修改“ARCH ?= arm”),执行“make menuconfig”后,在配置界面可以看到“System Type”字样,进入它得到另一个界面,如下图所示:
img not found
第一行“ARM system type”用来选择体系结构,进入它之后选中“Samsung S3C2410,S3C2412,S3C2413,S3C2440,S3C2442,S3C2443”,查看帮助信息可以知道它对应CONFIG_ARCH_S3C2410配置项。
下面几行用来设置S3C2410(包括S3C2412等)系统的特性,比如选中“S3C2410 UART to use for low-level message”后按回车键,可以输入数字,表示使用哪个串口来输入内核打印信息:选中“S3C2410 DMA support”表示支持DMA功能。
再往下的“S3C2410 Machine —>”、“S3C2440 Machine —>”表示这又是一个菜单,它们用来选择开发板类型。比如进入“S3C2410 Machine”菜单后,可以看到如下内容:
img not found
它们表示目前内核中支持S3C2410的8种开发板。选中某个开发板之后,它相应的文件就会被编译进内核中。比如对于开发板SMDK2410/A9M2410,它的配置项为CONFIG_ARCH_SMDK2410,在arch/arm/mach-s3c2410/Makefile中可以看到如下一行,表示如果选择支持该开发板,则arch/arm/mach-s3c2410/mach-smdk2410.c文件被编进内核中。

1
obj-$(CONFIG_ARCH_SMDK2410) 	+= mach-smdk2410.o

在移植内核时,可以选中某个配置相似的开发板,然后在上面进行修改。

“Device Drivers” 菜单:设备驱动程序

执行“make menuconfig”后,在配置界面可以看到“Device Drivers”字样,进入它得到另一个界面,如下图所示:
img not found
图中的各个子菜单与内核源码drivres/目录下各个子目录一一对应,如下表所示,在配置过程中可以参考这个表格找到对应的配置选项,在添加新驱动时,也可以参考它来决定代码放在哪个目录下。

Device Drivers 子菜单 描述
Generic Driver Options 对应divers/base目录,这是设备驱动程序中一些基本和通用的配置选项
Connector - undefined userspace <-> kernelspace linker 对应divers/connector目录
Memory Technology Device (MTD)support 对应divers/mtd目录,用于支持各种新型的存储设备,比如NOR Flash、NAND Flash等
Parallel port support 对应divers/parport目录,用于支持各种并口设备
Plug and Play support 对应divers/pnp目录,支持各种“即插即用”设备
Block devices 对应divers/block目录,包括回环设备、RAMDISK等的驱动
ATA/ATAPI/MFM/RLL support 对应divers/ide目录,它用来支持ATA/ATAPI/MFM/RLL 接口的硬盘、软盘、光盘等
SCSI device support 对应divers/scsi目录,支持各种scsi接口的设备
Serial ATA(prod)and parallel ATA (experiment)drivers 对应divers/ata目录,支持SATA与PATA设备
Multi-device support (RAID and LVM) 对应divers/md目录,表示多设备支持(RAID和LVM),RAID和LVM的作用是使多个物理设备组建成一个单独的逻辑磁盘
Network device support 对应divers/net目录,用来支持各种网络设备,比如CS8900、DM9000等
ISDN subsystem 对应divers/isdn目录,用来提供综合业务数字网(Integrate Service Digital Network)的驱动程序
Input device support 对应divers/input目录,支持各类输入设备,比如键盘、鼠标等
Character devices 对应divers/char目录,它包含各种字符设备驱动程序。串口的配置也是从这个菜单调用的,但是串口的代码在drivers/serial
I2C support 对应divers/i2c目录,支持各类I2C设备
SPI support 对应divers/spi目录,支持各类SPI设备
Dallas’s 1-wire bus 对应divers/w1目录,支持一线总线
Hardware Monitoring support 对应divers/hwmon目录
Misc devices 对应divers/misc目录,用来支持一些不好分类的设备,称为杂项设备
Multifuction device drivers 对应divers/mfd目录,用来支持多功能的设备,比如SM501,它既可以用于显示图像,也可以用作串口
LED devices 对应divers/leds目录,包含各种LED驱动程序
Multimedia devices 对应divers/media目录,包含多媒体驱动,比如V4L(Video for Linux),它用于向上提供统一的图像、声音接口
Graphics support 对应divers/video目录,提供图形设备/显卡的支持
Sound 对应sound/目录(它不在drivers下),用来支持各种声卡
HID Devices 对应divers/hid目录,用来支持各种USB-HID设备,或者符合USB-HID规范的设备(蓝牙)。HID(Human Interface Device),比如各种USB接口的鼠标/键盘/游戏杆/手写板等输入设备
USB support 对应divers/usb目录,包括各种USB Host和USB Device 设备
MMC/SD card support 对应divers/mmc目录,用来支持各种MMC/SD卡
Real Time Clock 对应divers/rtc目录,用来支持各种实时时钟设备。比如S3C24x0上就集成了RTC芯片

Linux 内核移植

本节将修改Linux-2.6.22.6内核,使得它可以同时在本书使用的S3C2410、S3C2440开发板上运行,并修改相关驱动使它支持网络功能、支持JFFS2、YAFFS文件系统,同时修改MTD设备分区,使得内核可以挂接NAND Flash上的文件系统。

Linux 内核启动过程概述

与移植U-Boot的过程相似,在移植Linux之前,先了解它的启动过程。Linux的启动过程可以分为两部分:架构/开发板相关的引导过程、后续的通用启动过程。下图所示是ARM架构处理器上Linux内核vmlinux的启动过程。之所以强调是vmlinux,是因为其他格式的内核在进行与vmlinux相同的流程之前会有一些独特的操作。比如对于压缩格式的内核zImage,它首先进行自解压得到vmlinux,然后执行vmlinux开始正常的启动流程。
引导阶段通常使用汇编语言编写,它首先检查内核是否支持当前架构的处理器,然后检查是否支持当前开发板。通过检查后,就为调用下一阶段的start_kernel函数准备了。主要分为如下两个步骤。

  1. 连接内核时使用的虚拟地址,所以要设置页表、使能MMU。
  2. 调用C函数start_kernel之前的常规工作,包括复制数据段、清除BSS段、调用start_kernel函数。

第二阶段的关键代码主要使用C语言编写。它进行内核初始化的全部工作,最后调用rest_init函数启动init过程,创建系统第一个进程:init进程。在第二阶段,仍有部分架构/开发板相关的代码,比如下图中的setup_arch函数用于进行架构/开发板相关的设置(重新设置页表、设置系统时钟、初始化串口等)。
img not found

修改Linux内核以支持S3C2410/S3C2440开发板

首先配置、编译内核,确保内核可以正确编译。得到内核源码后,先修改顶层Makefile,如下所示:

1
2
3
4
5
ARCH 	?= $(SUBARCH)
CROSS_COMPILR ?=
改为
ARCH ?= arm
CROSS_COMPILR ?= arm-linux-

然后执行如下命令,使用arch/arm/configs/smdk2410_defconfig文件来配置内核,它生成.config配置文件,后面就可以直接使用“make menuconfig”修改配置了。

1
make smdk2410_defconfig

最后是编译生成内核,执行“make”命令将在顶层目录下生成内核映像文件vmlinux;执行“make uImage”除生成vmlinux外,还在arch/arm/boot目录下生成U-Boot格式的内核映像文件uImage。
对于S3C2410开发板,上面生成的uImage是可以使用的。在U-Boot控制界面中使用如下命令下载uImage并启动它:

1
2
tftp 0x32000000 uImage 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage/
bootm 0x32000000

在串口可以看到内核的启动信息,只是在最后看到如下的panic信息,这是因为没有修改MTD分区,没有增加对yaffs文件系统的支持。

1
2
3
4
5
6
7
8
9
10
11
12
VFS: Uable to mount root fs via NFS, trying flopping
VFS: Cannot open root device "mtdblock/2" or unknown-block(2,0)
Please append a correct "root=" boot option;here are the availaable partitions:
1f00 16 mtdblock0 (driver?)
1f01 2048 mtdblock1 (driver?)
1f02 4096 mtdblock2 (driver?)
1f03 2048 mtdblock3 (driver?)
1f04 4096 mtdblock4 (driver?)
1f05 10240 mtdblock5 (driver?)
1f06 24576 mtdblock6 (driver?)
1f07 16384 mtdblock7 (driver?)
Kernel panic - not syncing: VFS: Uable to mount root fs on unkown-block(2,0)

对于S3C2440开发板,使用同样的命令启动uImage,在打印如下信息之后(U-Boot打印),就会出现一大堆乱码:
img not found
所以,Linux 2.6.22.6还不支持本书所用的S3C2440开发板,这个开发板的配置与内核所支持的开发板不一致。
要让内核支持当前的S3C2410开发板,需要进行一些修改。

引导阶段代码分析

由前面对内核Makefile的分析,可知arch/arm/kernel/head.S是内核执行的第一个文件。另外。U-Boot调用内核时,r1寄存器中存储“机器类型ID”,内核会用到它。
移植Linux内核时,对于arch/arm/kernel/head.S,只需要关注开头几条命令,如下所示:

1
2
3
4
5
6
7
8
9
10
ENTRY(stext)
msr cpsr_c, PSR_F_BIT | PSR_I_BIT | SVC_MODE @确保进入管理(SVC)模式,并禁止中断
mrc p15, 0, r9, c0, c0 @读取CPU ID,存入r9寄存器
bl __lookup_processor_type @调用函数,输入参数r9=cpuid,返回值r5=procinfo
movs r10,r5 @如果不支持当前CPU,则返回值r5=0
beq __error_p @如果r5=0,则打印错误
bl __lookup_machine_type @调用函数,返回值r5=machinfo
movs r8, r5 @如果不支持当前机器,则返回值r5=0
beq __error_a @如果r5=0,则打印错误
...

第2行通过设置CPSR寄存器来确保处理器进入管理(SVC)模式,并且禁止中断。
第3行读取协处理器CP15的寄存器C0获得CPU ID,CPU ID格式如下所示:
img not found

含义
[31:24] 厂商编号有如下值。
0x41 = A,表示ARM公司
0x44 = D,表示Digital Equipment公司
0x69 = 1,表示Intel公司
[23:20] 由厂商定义,当产品编号相同时,使用子编号来区分不同的产品子类,如产品中不同的高速缓存大小等
[19:16] ARM体系版本号,目前取值如下。
0x01,表示ARM体系版本4
0x02,表示ARM体系版本4T
0x03,表示ARM体系版本5
0x04,表示ARM体系版本5
0x05,表示ARM体系版本5T
0x06,表示ARM体系版本5TE
[15:4] 产品主编号
[3:0] 处理器版本号

比如S3C2410的CPU ID为0x41129200,S3C2440的CPU ID也是0x41129200。注意,S3C2410和S3C2440称为片山系统(SOC),除CPU外,还集成了UART、USB控制器、NAND Flash控制器等设备。从它们的CPU ID可知,它们的CPU是相同的,只是片上外设不一样。
第4行调用__lookup_processor_type函数,确定内核是否支持当前CPU。如果支持,r5寄存器将会返回一个用来描述处理器的结构体的地址,否则r5的值为0。
第7行调用__lookup_machine_type函数,确定内核是否支持当前开发板。如果支持,r5寄存器将会返回一个用来描述这个开发板的结构体的地址,否则r5的值为0。
如果__lookup_processor_type__lookup_machine_type这两个函数中有一个返回值为0,则内核不能启动,如果配置内核时选择了CONFIG_DEBUG_ALL,还会打印一些提示信息。
__lookup_processor_type__lookup_machine_type函数都是在arch/arm/kernel/head-common.S中定义的。
内核映像中,定义了若干个proc_info_list结构(结构体原型在include/asm-arm/procinfo.h中定义),表示它支持的CPU。对于ARM架构的CPU,这些结构体的源码在arch/arm/mm/目录下,比如proc-arm920.S中的如下代码,它表示arm920 CPU的proc_info_list结构。

1
2
3
4
5
6
7
.section ".proc.info.init", #alloc, #execinstr

.type __arm920_proc_info, #object
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
...

不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中。在连接内核时,这些结构体被组织在一起,开始地址为__proc_info_begin,结束地址为__proc_info_end。这可以从连接脚本arch/arm/kernel/vmlinux.lds中看出来。

1
2
3
__proc_info_begin = .;		//proc_info_init结构的开始地址
*(.proc.info.init)
__proc_info_end = .; //proc_info_init结构的结束地址

__lookup_processor_type函数就是根据前面读出的CPU ID (存在r9 寄存器中),从这些proc_info_init结构中找出匹配的,它的代码如下(arch/arm/kernel/head-common.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
	.type	__lookup_processor_type, %function
__lookup_processor_type:
adr r3, 3f @ r3 = 最后一行代码的物理地址(3)
ldmda r3, {r5 - r7} @ r5 = __proc_info_begin,r6 = __proc_info_end,它们是虚拟地址。r7 = 最后一行代码的虚拟地址
sub r3, r3, r7 @ r3 = r3 - r7,即物理地址和虚拟地址的差值
add r5, r5, r3 @ r5 = __proc_info_begin 对应的物理地址
add r6, r6, r3 @ r6 = __proc_info_end 对应的物理地址
1: ldmia r5, {r3, r4} @ r3、r4 = proc_info_list结构中的cpu_val、cpu_mask
and r4, r4, r9 @ r4 = (r4 & r9) = (cpu_mask & 传入的CPU ID)
teq r3, r4 @ 比较
beq 2f @ 如果相等,表示找到匹配的proc_info_list结构,跳到(2)
add r5, r5, #PROC_INFO_SZ @ r5指向下一个proc_info_list结构 PROC_INFO_SZ = sizeof(proc_info_list)
cmp r5, r6 @ 是否已经比较完所有的proc_info_list结构
blo 1b @ 没有则继续比较
mov r5, #0 @ 比较完毕,但是没有匹配的proc_info_list结构,r5 = 0
2: mov pc, lr @ 返回

...

/*
* Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for
* more information about the __proc_info and __arch_info structures.
*/
.long __proc_info_begin @ proc_info_list结构的开始地址,这是连接地址,也是虚拟地址
.long __proc_info_end @ proc_info_list结构的结束地址,这是链接地址,也是虚拟地址
3: .long . @ “.”表示当前这行代码编译连接后的虚拟地址

在调用__enable_mmu函数之前使用的都是物理地址,而内核却是以虚拟地址连接的。所以在访问proc_info_list结构前,先将它的虚拟地址转换为物理地址,第3-7行就是用来转换地址的。
第3行用来获得第26行代码的物理地址。adr指令基于pc寄存器计算地址值,由于这时候还没使能MMU,pc寄存器中使用的还是物理地址,所以执行“adr,r3,3f”后,r3寄存器中存放的就是第26行代码的物理地址。
第4行用来获得第24-26行定义的数据:__proc_info_begin、__proc_info_end和“.”。这3个数据都是在连接内核时确定,它们是虚拟地址,前两个表示proc_info_list结构的开始地址和结束地址,“.”表示当前行的代码在编译连接后的虚拟地址。
第5行计算物理地址和虚拟地址的差值,第6-7行根据这个差值计算__proc_info_begin__proc_info_end的物理地址。
下面的代码依次读取每个proc_info_list结构前面的两个成员(cpu_val和cpu_mask),判断cpu_val是否等于(r9 & cpu_mask),r9是arch/arm/kernel/head.S中调用__lookup_processor_type时传入的CPU ID。如果比较相等,则表示当前proc_info_list结构适用于这个CPU,直接返回这个结构的地址(存在r5中)。如果__proc_info_begin__proc_info_end之间的所有proc_info_list结构都不支持这个CPU,则返回0(r5)。
对于S3C2410、S3C2440开发板,它们的CPU ID都是0x41129200,而在arch/arm/mm/proc-arm920.S中定义的__arm920_proc_info结构中,cpu_val、cpu_mask等于0x41009200、0xff00fff0,刚好匹配。内核中要包含这个文件,在arch/arm/mm/Makefile中可以看到下面这行,它表示需要配置CONFIG_CPU_ARM920T(配置菜单中System Type->Support ARM920T procrssor)。

1
obj-$(CONFIG_CPU_ARM920T)      += proc_arm920.o

下面讲解__ lookup_machine_type,它和__lookup_processor_type函数代码相似。
内核中对于每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义了开发板相关的一些属性和函数。比如机器类型ID、起始I/O物理地址,Bootloader传入的参数的地址、中断初始化函数、I/O映射函数等。比如对于SDMK2440开发板,在arch/arm/mach-s3c2440/mach-smdk2440.c中定义如下:

1
2
3
4
5
6
7
8
9
10
11
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <ben@fluff.org> */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,

.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END

MACHINE_STARTMACHINE_ENDinclude/asm-arm/mach/arch.h文件中定义

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Set of macros to define architecture features. This is built into
* a table by the linker.
*/
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,

#define MACHINE_END \
};

所以上一段代码扩展开来就是:

1
2
3
4
5
6
static const struct machine_desc __mach_desc_S3C2440	\
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_S3C2440, \
.name = _name,
};

其中的MACH_TYPE_S3C2440在arch/arm/tools/mach-types中定义,它最后会被换成一个头文件include/asm-arm/mach-types.h供其他文件包含。machine_desc在include/asm-arm/mach/arch.h文件中定义。所有的machine_desc结构都处于“.arch.info.init”段中,在连接内核时,它们被组织在一起,开始地址为__arch_info_begin,结束地址为__arch_info_end。这可以从连接脚本文件arch/arm/kernel/vmlinux.lds中看出来:

1
2
3
__arch_info_begin = .;				//machine_desc结构的开始地址
*(.arch.info.init)
__arch_info_end = .; //machine_desc结构的结束地址

不同的machine_desc结构用于不同的开发板,U-Boot调用内核时,会在r1寄存器中给出开发板的标记(机器类型ID)。__loockup_machine_type函数将这个值与machine_desc中的nr成员比较,如果两者相等则表示找到匹配的machine_desc结构,于是返回它的地址(存在r5中)。如果__arch_info_begin__arch_info_end之间所有的machine_desc结构的nr成员都不等于r1寄存器的值,则返回0(r5)。
对于本书所用的S3C2410、S3C2440开发板,U-Boot传入的机器类型ID为MACH_TYPE_SMDK2410、MACH_TYPE_SMDK2440。它们对应的machine_desc结构分别在arch/arm/mach-s3c2440/mach-smdk2440.carch/arm/mach-s3c2410/mach-smdk2410.c中定义,所以两个文件要编进内核。在配置菜单中,选中下面两个开发板即可。

1
2
System Type --> S3C2410 Machines --> SMDK2410/A9M2410
System Type --> S3C2440 Machines --> SMDK2440

__lookup_machine_type函数的代码如下(/arch/arm/kernel/head-common.S):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3:	.long	.
.long __arch_info_begin
.long __arch_info_end
...
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b @ r3 = 第1行的物理地址
ldmia r3, {r4, r5, r6} @ r4 = 第1行的虚拟地址 r5 = __arch_info_begin r6 = __arch_info_end 它们是虚拟地址
sub r3, r3, r4 @ r3 = r3 - r4 即物理地址和虚拟地址的差值
add r5, r5, r3 @ r5 = __arch_info_begin 对应的物理地址
add r6, r6, r3 @ r6 = __arch_info_end 对应的物理地址
1: ldr r3, [r5, #MACHINFO_TYPE] @ r5是machine_desc 结构的地址 r3 = machine_desc 结构中定义的nr成员,即机器类型ID
teq r3, r1 @ r1是Bootloader调用内核时传入的机器类型ID,测试是否相等
beq 2f @ 若相等,跳到(2)
add r5, r5, #SIZEOF_MACHINE_DESC @ 否则,r5指向下一个machine_desc结构,SIZEOF_MACHINE_DESC = sizeof(machine_desc)
cmp r5, r6 @ 是否比较完所有的machine_desc结构
blo 1b @ 没有则继续比较
mov r5, #0 @ 比较完毕,但是没有匹配的machine_desc结构,r5 = 0
2: mov pc, lr @ 返回

如果__lookup_processor_tyep__lookup_machine_type函数都返回成功,则后续引导程序将继续执行下去。其中的__create_page_tables函数用来创建一级页表以建立虚拟地址到物理地址的映射关系,它用到__lookup_processor_type函数返回的proc_info_list结构。在引导的最后,调用start_kernel函数进入内核启动的第二阶段。__lookup_machine_type函数确定的machine_desc结构将在第二阶段多次使用。

start_kernel 函数部分代码分析

进入start_kernel函数(init/main.c)之后,如果串口上没有看到内核的启动信息,一般而言有两个原因:Bootloader传入的命令行参数不对,或者setup_arch函数(arch/arm/kernel/setup.c)针对开发板的设置不正确。
在调用setup_arch函数之前就已经调用“printk(linux_banner)”了,但是这个时候printk函数只是将打印信息放在缓存区中,并没有打印到控制台上(串口、LCD屏等),因为这个时候控制台还未初始化。printk打印的内容在console_init函数注册、初始化控制台之后才真正输出。
移植U-Boot时,U-Boot传给内核的参数有两类:预先存在某个地址的tag列表和调用内核时在r1寄存器中指定的机器类型ID。后者在引导阶段的__lookup_machine_type函数已经用到。而tag列表将在setup_arch函数中进行初步处理。本节将重点介绍setup_arch函数、console_init函数、以tag列表的处理(内存tag、命令行tag)、串口控制台的初始化为主线。

setup_arch 函数分析

setup_arch函数在arch/arm/kernel/setup.c中定义,其部分代码及流程图如下:
img not found

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __init setup_arch(char **cmdline_p)
{
...
setup_processor(); //进行处理器相关的一些设置
mdesc = setup_machine(machine_arch_type); //获得开发板的machine_desc结构
...
if (mdesc->boot_params) //定义了Bootloader传入参数的地址
tags = phys_to_virt(mdesc->boot_params); //这个地址就是tag列表的首地址
...
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0) //如果已经在内核中定义了meminfo结构
squash_mem_tags(tags); //则忽略内存tag
parse_tags(tags); //解释每一个tag
}
...
memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-1] = '\0';
parse_cmdline(cmdline_p, from); //对命令行进行一些先期的处理
paging_init(&meminfo, mdesc); //重新初始化页表
...
}

首先,第[4]行的setup_processor函数被用来进行处理器相关的一些设置,它会调用引导阶段的lookup_processor_type函数以获得该处理器的proc_info_list结构。
接下来,第[5]行的setup_machine函数被用来获得开发板的machine_desc结构,这通过调用引导阶段lookup_machine_type函数来实现。以后就会根据开发板的machine_desc结构来进行一些开发板的相关操作,
第[7]-[8]行用来确定Bootloader传入的启动参数的地址,它在开发板的machine_desc结构中指定,第[8]行将它转换为虚拟地址。比如对于S3C2440开发板,在arch/arm/mach-s3c2440/mach-smdk2440.c中有如下定义。启动参数的地址就是(S3C2410_SDRAM_PA + 0x100),即0x30000100。

1
2
3
MACHINE_START(S3C2440,"SMDK2440")
...
.bootm_params = S3C2440_SDRAM_PA + 0x100,

第[13]行处理每个tag。文件arch/arm/kernel/setup.c对每种tag都定义了相应的处理函数,比如对于内存tag、命令行tag,使用如下两行代码指定了它们的处理函数为parse_tag_mem32、parse_tag_cmdline

1
2
__tagtable(ATAG_MEM,parse_tag_mem32);
__tagtable(ATAG_CMDLINE,parse_tag_cmdline);

parse_tag_mem32函数根据内存tag定义的内存起始地址、长度,在全局结构变量meminfo中增加内存的描述信息。以后内核就可以通过meminfo结构了解开发板的的内存信息。
parse_tag_cmdline只是简单的将命令行tag的内容复制到字符串default_command_line中保存下来,后面才进一步处理。
第[18]行扫描命令行参数,对其中的一些参数进行先期的处理。这些参数使用“__early_param”来定义,比如arch/arm/kernel/setup.c中下面的一行代码,它表示如果命令行中有“mem=…”的字样,就调用early_mem(在include/asm-arm/setup.h中定义)对它进行处理:

1
__early_param("mem=",early_mem);

“mem=…”用来强制限制Linux系统所能使用的内存总量,比如“mem=60M”使得系统只能使用60MB的内存,即使内存tag中指明了共有64MB内存。类似的参数还有“initrd=”等。
此时命令行的处理还没有结束,在setup_arch函数之外还会进行一系列后续处理,比如start_kernel函数中调用如下代码:

1
2
3
4
5
6
setup_command_line(command_line);
...
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);

比如对于命令行中的“console=ttySAC0”,它的处理过程就是第[4]行的parse_args函数调用第[6]行传入的unknown_bootoption函数,最后调用下面代码指定的处理函数console_setup(在kernel/printk.c中定义)。

1
__setup("console=",console_setup);

命令行参数“console=…”用来指定要使用的控制台的名称、序号、参数。比如对于“console=ttySAC0,115200”,表示要使用的控制台名称为ttySAC,序号为0(第一个串口),波特率为115200。经过console_setup处理后,会在全局结构变量console_cmdline中保存这些信息,在后面console_init函数初始化控制台时会根据这些信息选择要使用的控制台。
setup_arch函数后面会调用paging_init函数,这也是一个开发板相关的函数。

paging_init函数分析

这个函数在setup_arch函数中的调用形式如下:

1
paging_init(&meminfo,mdesc);

meminfo中存放内存的信息,前面解释内存tag时确定了构建这个全局结构。
mdesc就是前面lookup_machine_type函数返回的machine_desc结构。对于S3C2440开发板,这个结构在arch/arm/mach-s3c2440/mach-smdk2440.c中有如下定义:

1
2
3
4
5
6
7
8
9
10
11
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <ben@fluff.org> */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,

.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END

上面几行代码是移植Linux必须关注的数据结构。对于S3C2410开发板,它在arch/arm/mach-s3c2410/mach-smdk2410.c
paging_init函数在arch/arm/mm/mmu.c中定义,根据我们的移植目的–让内核可以在S3C2440上运行。关注如下流程:

1
paging_init -> devicemaps_init -> mdesc -> map_io()

对于S3C2440开发板,就是调用smdk2410_map_io函数,它也是arch/arm/mach-s3c2440/mach-s3c2440.c中定义。

1
2
3
4
5
6
static void __init smdk2440_map_io(void)
{
s3c24xx_init_io(smdk2440_iodesc, ARRAY_SIZE(smdk2440_iodesc));
s3c24xx_init_clocks(16934400);
s3c24xx_init_uarts(smdk2440_uartcfgs, ARRAY_SIZE(smdk2440_uartcfgs));
}

上述三个函数所实现的功能,从名字就可以看出,第四行中参数值表示开发板晶振的频率。当前开发板所使用的晶振频率是12MHz,不是16934400,这就是S3C2440开发板上启动uImage时串口输出乱码的原因,将它改为12000000即可。

console_init 函数分析

虽然上面已经找到内核无法正常输出信息的原因,但我们不该止步于此。在2.4的内核中,命令行参数常用“console=ttyS0”来指定控制台为串口0,在2.6版本的内核中改为“console=ssySAC0”。分析console_init函数的功能就可以了解这点。
console_init函数被start_kernel函数调用,它在drivers/char/tty_io.c文件中定义

1
2
3
4
5
6
7
8
9
10
void __init console_init(void)
{
initcall_t *call;
...
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}

它调用地址范围__con_initcall_start__con_initcall_end之间的定义的每个函数,这些函数使用console_initcall宏来指定。比如drivers/serial/s3c2410.c中:

1
console_initcall(s3c24xx_serial_initconsole);

s3c24xx_serial_initconsole函数也是在drivers/serial/s3c2410.c中定义,它初始化S3C24xx类SoC的串口控制台,部分代码如下:

1
2
3
4
5
6
static int s3c24xx_serial_initconsole(void)
{
...
register_console(&s3c24xx_serial_console);
return 0;
}

s3c24xx_serial_console结构在drivers/serial/s3c2410.c中定义如下:

1
2
3
4
5
6
7
8
9
10
static struct console s3c24xx_serial_console =
{
.name = S3C24XX_SERIAL_NAME, //即“ttySAC”
.device = uart_console_device, //以后使用/dev/console时,用来构造设备节点
.flags = CON_PRINTBUFFER, //控制台可用之前,printk已经在缓冲区打印了很多信息,CON_PRINTBUFFER表示注册控制台之后
//打印这些“过去”的信息
.index = -1, //-1可以匹配任意序号。比如ttySAC0/1/2
.write = s3c24xx_serial_console_write, //打印函数
.setup = s3c24xx_serial_console_setup //设置函数
};

register_console(&s3c24xx_serial_console);在内核中注册控制台,就是把s3c24xx_serial_console结构链入一个全局链表console_drivers中(它在kernel/printf.c中定义)。并且使用其中的name和index与前面的“console=…”指定的控制台相比较,如果相符,则以后的printk信息从这个控制台输出。
对于本书的情况,“console=ttySAC0”,而s3c24xx_serial_console结构中名字为“ttySAC”,序号为-1(可取任意值),所以两者匹配,printk信息将从串口0输出。
现在总结一下上面分析的内核启动第二阶段的函数调用过程,相同的缩进表示它们是在同一个函数中被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
start_kernel ->
setup_arch ->
setup_processor
setup_machine
...
parse_tags
...
parse_cmdline
paging_init ->
devicemaps_init ->
mdesc_map_io() ->
s3c24xx_init_io
s3c24xx_init_clocks
s3c24xx_init_uarts
...
console_init ->
s3c24xx_serial_initconsole ->
register_console(&s3c24xx_serial_initconsole)
...

修改内核

arch/arm/mach-s3c2440/mach-smdk2440.c中做如下修改:

1
2
3
s3c24xx_init_clocks(16934400);
改为
s3c24xx_init_clocks(12000000);

然后执行“make uImage”生成uImage。
对于S3C2410、S3C2440开发板,上面生成的uImage都可以使用了。

1
2
tftp 0x32000000 uImage 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage
bootm 0x32000000

可以看到内核的启动信息,最后出现panic信息(这需要修改mtd分区、增加对yaffs文件系统的支持)。

修改 MTD 分区

MTD(Memory Technology Device),即内存技术设备,是Linux中对ROM、NOR Flash、NAND Flash等存储设备抽象出来的设备层。它向上提供统一的访问接口:读、写、擦除等;屏蔽了底层硬件的操作、各类存储设备的差别。得益于MTD设备的作用,重新划分NAND Flash的分区很简单。

驱动对设备的识别过程

驱动程序识别设备时,有以下两种方法。

  1. 驱动程序本身带有设备的信息,比如开始地址、中断号等;加载驱动程序时,就可以根据这些信息来识别设备。
  2. 驱动程序本身没有设备的信息,但是内核中已经根据其他方式确定了很多设备的信息;加载驱动程序时,将驱动程序与这些设备逐个比较,确定两者是否匹配,如果成功匹配,那么就可以通过驱动程序操作这个设备了。
    内核常使用第二种方法来识别设备,这可以将各种设备集中在一个文件中管理,当开发板的配置改变时,便于修改代码。在内核文件include/linux/paltform/device.h中,定义了两个数据结构来表示这些设备和驱动程序:paltform_device结构用来描述设备的名称、ID、所占用的资源(内存地址/大小、中断号)等;platform_driver结构用来描述各种操作函数,比如枚举函数、移除设备函数、驱动的名称等。
    内核启动后,首先构造链表将描述设备的platform_device结构组织起来,得到一个设备的列表;当加载某个驱动程序的platform_driver结构时,使用一些匹配函数来检查驱动程序能否支持这些设备,常用的检察方式很简单:比较驱动程序和设备的名称。
    以S3C2440开发板为例,在arch/arm/mach-s3c2440/mach-smdk2440.c中定义了如下设备:
    1
    2
    3
    4
    5
    6
    7
    static struct platform_device *smdk2440_devices[] __initdata = {
    &s3c_device_usb,
    &s3c_device_lcd,
    &s3c_device_wdt,
    &s3c_device_i2c,
    &s3c_device_iis,
    };
    arch/arm/plat-s3c24xx/common-smdk.c中定义了如下设备:
    1
    2
    3
    4
    5
    6
    7
    static struct platform_device __initdata *smdk_devs[] = {
    &s3c_device_nand,
    &smdk_led4,
    &smdk_led5,
    &smdk_led6,
    &smdk_led7,
    };
    这些设备在smdk2410_init函数或smdk2440_init函数中,通过platform_add_devices函数注册进内核中。
    NAND Flash设备s3c_device_nand在arch/arm/plat-s3c24xx/devs.c中的定义如下:
    1
    2
    3
    4
    5
    6
    struct platform_device s3c_device_nand = {
    .name = "s3c2410-nand",
    .id = -1,
    .num_resources = ARRAY_SIZE(s3c_nand_resource),
    .resource = s3c_nand_resource,
    };
    对于S3C2440开发板,s3c_device_nand结构的名字会在s3c24xx_map_io函数中修改为“s3c2440-nand”,这个函数在arch/arm/plat-s3c24xx/s3c244x.c中的定义如下:
    1
    2
    3
    4
    5
    6
    7
    void __init s3c244x_map_io(struct map_desc *mach_desc, int size)
    {
    ...
    s3c_device_i2c.name = "s3c2440-i2c";
    s3c_device_nand.name = "s3c2440-nand";
    s3c_device_usbgadget.name = "s3c2440-usbgadget";
    }
    有了NAND Flash设备,还要有NAND Flash驱动程序,内核针对S3C2410、S3C2412、S3C2440定义了3个驱动。它们在drivers/mtd/s3c2410.c中的s3c2410_nand_init函数中注册进内核,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    static int __init s3c2410_nand_init(void)
    {
    printk("S3C24XX NAND Driver, (c) 2004 Simtec Electronics\n");

    platform_driver_register(&s3c2412_nand_driver);
    platform_driver_register(&s3c2440_nand_driver);
    return platform_driver_register(&s3c2410_nand_driver);
    }
    其中的s3c2440_nand_driver结构也是在相同的文件中定义,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static struct platform_driver s3c2440_nand_driver = {
    .probe = s3c2440_nand_probe,
    .remove = s3c2410_nand_remove,
    .suspend = s3c24xx_nand_suspend,
    .resume = s3c24xx_nand_resume,
    .driver = {
    .name = "s3c2440-nand",
    .owner = THIS_MODULE,
    },
    };
    可见,s3c_device_nand结构和s3c2440_nand_driver结构中的name成员相同,都是“s3c2440-nand”。platform_driver_register函数就是根据这点确定它们是匹配的,所以调用s3c2440_nand_probe函数来枚举NAND Flash设备s3c_device_nand。
    s3c2440_nand_probe函数开始,可以一直找到对NAND Flash分区的识别,如下所示:
    1
    2
    3
    4
    5
    s3c2440_nand_probe(&s3c_device_nand) ->								//这个参数是为了便于理解加上的
    s3c24xx_nand_probe(&s3c_device_nand, TYPE_S3C2440) -> //
    struct s3c2410_platform_nand *plat = to_nand_plat(pdev) -> //plat = &smdk_nand_info
    s3c2410_nand_add_partition(info, nmtd, sets); -> //sets就是smdk_nand_info
    add_mtd_partitions //实际的参数为smdk_default_nand_part
    这些函数都在drivers/mtd/nand/s3c2410.c中定义,最后的add_mtd_partitions函数根据smdk_default_nand_part结构来确定分区。这个结构在arch/arm/plat-s3c24xx/common-smdk.c中定义,要改变分区时修改它即可。

修改 MTD 分区

如上所述,要改变分区时,修改arch/arm/plat-s3c24xx/common-smdk.c文件中的smdk_default_nand_part结构即可。本章节将NAND Flash划分为3个分区,前2MB用于存放内核,接下来的8MB用于存放JFFS2文件系统,剩下的用来存放YAFFS文件系统。
smdk_default_nand_part结构修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static struct mtd_partition smdk_default_nand_part[] = {
[0] = {
.name = "Kernel",
.size = SZ_2M,
.offset = 0,
},
[1] = {
.name = "jffs2",
.offset = MTDPART_OFS_APPEND,
.size = SZ_8M,
},
[2] = {
.name = "yaffs",
.offset = MTDPART_OFS_APPEND,
.size = MTDPART_SIZ_FULL,
},
};

其中的MTDPART_OFS_APPEND表示当前分区紧接着上一个分区,MTDPART_SIZ_FULL表示当前分区的大小为剩余的Flash空间。
执行“make uImage”重新生成内核映像,重新启动后可以看到内核打印出如下分区信息。

1
2
3
4
Creating 3 MTD partitions on "NAND 64MiB 3,3V 8-bit"
0x00000000-0x00200000: "kernel"
0x00200000-0x00a00000: "jffs2"
0x00a00000-0x04000000: "yaffs"

由于目标板没有写入文件系统映像,也没有设置命令行使用网络文件系统(nfs),内核启动后还是会出现panic信息。

移植 YAFFS 文件系统

YAFFS 文件系统介绍

YAFFS(yet another flash file system)是一种类似于JFFS/JFFS2、专门为NAND Flash 设计的嵌入式文件系统,适用于大容量的存储设备。它是日志结构的文件系统,提供了损耗平衡和掉电保护,可以有效避免意外掉电对文件系统一致性和完整性的影响。与JFFS相比,它减少了一些功能,因此速度更快,占用内存更少。
YAFFS充分考虑了NAND Flash的特点,根据NAND Flash以页面为单位存取的特点,将文件组织成固定大小的数据段。利用NAND Flash提供的每个页面16字节的OOB空间来存储ECC(Error Correction Code)和文件系统的组织信息,不仅能实现错误检测和坏块处理,也能够提高文件系统的加载速度。YAFFS采用了一种多策略混合的垃圾回收算法,结合了贪心策略的高效性和随机选择的平均性,达到了兼顾系统开销和损耗平衡的目的。
YAFFS文件系统具有很好的移植性,可以在Linux、WindowsCE、pSOS、ThreadX、DSP-BIOS等多种操作系统上工作。为NAND Flash提供了一种可靠的操作系统,并且适合于对能耗要求高的嵌入式系统。
YAFFS文件系统目前已经发展到第二版本:YAFFS2,它向前兼容YAFFS1,主要特点是支持每页容量大于512字节的NAND Flash。

比较 YAFFS2 YAFFS1
写操作 快1-3倍 1.5MB/s-4.5MB/s 1.5MB/s
读操作 快1-2倍 7.6MB/s-16.7MB/s 7.6MB/s
删除操作 快4-34倍 7.8MB/s-62.5MB/s 1.8MB/s
垃圾回收 快2-7倍 2.1MB/s-7.7MB/s 1.1MB/s
内存消耗 减少25%-50% - -

一般而言,在NOR Flash上使用JFFS2文件系统,在NAND Flash上使用YAFFS文件系统。JFFS2与YAFFS的性能比较如下。
| 性能 | JFFS2 | YAFFS |
| 内存消耗 | 每个节点(node)占用16字节
128MB的Flash将占用4MB内存 | 每页占用2字节
128MB的Flash将占用512KB内存 |
| 第一次启动时的扫描时间 | 128MB的Flash上时间为25S | 只需要读取OOB,时间为3S |
| 是否压缩 | 压缩 | 不压缩 |
| 代码复杂度 | 复杂 | 简单 |
| 使用的操作系统 | Linux、eCos | 很多,容易移植 |
| 启动时间 | Flash容量为4MB(or 8MB)时为4S | Flash容量为30MB时为7S |

YAFFS 文件系统移植

http://www.aleph1.co.uk/cgi-bin/viewcvs.cgi/获取源代码文件root.tar.gz。解压得到Development目录,里面有两个子目录:yaffs和yaffs2。yaffs目录不在维护,yaffs2兼容yaffs。

将yaffs2代码加入内核

可以通过yaffs2目录下的脚本文件patch.ker.sh来给内核打补丁,用法如下:

1
2
usage: ./patch-ker.sh c/l kernelpath
if c/l is c,then copy. if 1 then link

这表明,如果“c/l”为“c”,则yaffs2的代码会被复制到内核目录下;如果是“1”,则仅仅是在内核目录下创建一些连接文件。
假设下载解压后的yaffs2源码目录为/work/system/Development/yaffs2,内核源码目录为/work/system/linux-2.6.22.6,执行以下命令打补丁:

1
2
cd /work/system/Development/yaffs2
./patch-ker.sh c /work/system/linux-2.6.22.6

上述命令完成以下3件事情。

  1. 修改内核fs/Kconfig文件,增加以下两行
    1
    2
    # Patched by YAFFS
    source "fs/yaffs/Kconfig"
  2. 修改内核fs/Makefile文件,增加以下两行
    1
    2
    # Patched by YAFFS
    obj-$(CONFIG_YAFFS_FS) += yaffs2/
  3. 在内核fs/目录下创建yaffs2子目录,然后复制如下文件。
    将yaffs2源码目录下得Makefile.kernel文件复制为内核fs/yaffs2/Makefile文件。
    将yaffs2源码目录下的Kconfig文件复制到内核fs/yaffs2目录下。
    将yaffs2源码目录下的*.c、*.h(不包括子目录下的文件)复制到内核fs/yaffs2目录下。

配置、编译内核

阅读fs/yaffs2/Kconfig文件可以了解各个配置选项的作用。

  1. CONFIG_YAFFS_FS:支持YAFFS文件系统
  2. CONFIG_YAFFS_YAFFS1:支持YAFFS1文件系统
    对于每页大小为512字节的NAND Flash,要选上这个配置项
  3. CONFIG_YAFFS_YAFFS1:支持YAFFS2文件系统
    对于每页大小为2048字节的NAND Flash,要选上这个配置项。本书所用的NAND Flash每页为512字节,这个配置项可以不选。
  4. CONFIG_YAFFS_AUTO_YAFFS2:自动选择YAFFS2格式。
    如果不设置这个配置项,必须使用“yaffs2”字样来表示YAFFS2文件系统格式;如果设置了这个配置项,则可以使用“yaffs”字样来统一表示YAFFS、YAFFS2文件系统格式,驱动程序会根据NAND Flash页的大小自动分辨是YAFFS还是YAFFS2。
  5. CONFIG_YAFFS_9BYTE_TAGS
    老的YAFFS1文件系统中,使用oob区中9个字节作为文件系统的标记(tag),比新的YAFFS1多了1个字节—“pageStatus”,它用来表示页的状态。
    如果要使用老的YAFFS1,这个配置项要选上,另外要修改MTD设备层以使用老的oob layout结构。oob layout就是内核文件drivers/mtd/nand/nand_base.c中的nand_oob_16结构。
    Linux-2.6.22.6内核使用新的oob layout,格式如下。它表示ECC码存放的位置是oob区中0、1、…、7这8个字节;剩下的空间就称为可用空间,供文件系统使用,代码中将这些数据称为标记(tag):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static struct nand_ecclayout nand_oob_16 = {
    .eccbytes = 6,
    .eccpos = {0,1,2,3,6,7},
    .oobfree = {
    {
    .offset = 8,
    .length = 8
    }
    }
    };
    以前的内核使用老的oob layout,格式如下,ECC码的位置不一样,标记的位置也不一样。
    1
    2
    3
    4
    5
    6
    static struct nand_ecclayout nand_oob_16 = {
    .eccbytes = 6,
    .eccpos = {8,9,10,13,14,15},
    .oobavail = 9,
    .oobfree = {{0,4},{6,2},{11,2},{4,1}}
    };
    如果要使用老格式的YAFFS1映像文件,定义CONFIG_YAFFS_9BYTE_TAGS配置项,并且修改nand_oob_16结构为老的格式。
  6. CONFIG_YAFFS_DOES_ECC:使用YAFFS本身的ECC校验参数。
    一般使用MTD设备层的ECC校验函数,这个配置项不用设置。
    了解各配置项的意义后,就可以配置内核,选上对YAFFS的支持了。在内核配置界面选中“YAFFS2 file system support”即可,其他配置选项使用默认值。
    1
    2
    3
    File systems	--->
    Miscellaneous filesystems --->
    <*> YAFFS2 file system support
    最后执行“make uImage”编译内核。

编译、烧写、启动内核

到目前为止,内核已经同时支持了S3C2410和S3C2440,修改了NAND Flash的分区,增加了对YAFFS文件系统的支持。另外,内核原来已经支持JFFS2文件系统。现在的内核。已经基本可用,可以将它烧入NAND Flash中。

编译内核

1
make uImage

烧写内核

1
2
3
tftp 0x32000000 uImage 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage
nand erase 0 0x200000 //擦除NAND Flash 前2MB
nand write.jffs2 0x32000000 0 $(filesize) //烧写uImage

启动内核

1
2
nboot 0x32000000 0 0
bootm 0x32000000

要想开发板上电自动启动内核,可以设置bootcmd环境变量

1
2
set bootcmd 'nboot 0x32000000 0 0; bootm 0x32000000'
setenv