本项目的github地址: github
详细的操作步骤置于文末,前面主要对原理进行简要的说明。如果你只是想要查看步骤而对原理不在乎, 可以直接跳到实验步骤整理部分
实验环境
1. 目标平台环境
1.1 硬件环境
- CPU: Cortex-A8, 1GHz
- memery: 512MB
- Flash: 4GB inand-Flash
- 24-bit RGB interface
- Four USB HOST interface
- USB OTG interface
- Two RS232 interface
- Two SDcard interface
- Four LED
- Function Key
- One HDMI video interface
- Audio input/output interface
- Wired internet interface DM9000CEP
- Capacity touch screen
- Camera interface
- Real time clock
- Support for G-sensor, SPI, UART, USB/WIFI, GPS, GPRS, etc.
- 2D/3D high performance graphic process
1.2 软件环境
- Boot loader: uBoot
2. Host 环境
2.1 操作系统
- ubuntu 16.04 LTS
- kernel 4.10.37
2.2 编译器
- arm-linux-gcc (Freescale MAD – Linaro 2011.07 – Built at 2011/08/10 09:20) 4.6.2 20110630 (prerelease)
- GNU Make 4.1
- cmake version 3.5.1
实验原理介绍
1. Bios 与 Bootloader
任何一款计算机或嵌入式系统在进行开机上电到读取内核、内核调用文件系统这一系列程序之前,都会有一个启动初始化阶段。 Bios 和 Bootloader 是固化在系统的硬件中的,开机上电后程序指针会自动指向相应的地方。
1.1 Bios
个人计算机架构下,你想要启动整部系统首先就得要让系统去加载 BIOS (Basic Input Output System),并透过 BIOS 程序去加载 CMOS 的资讯,并且藉由 CMOS 内的配置值取得主机的各项硬件配置, 例如 CPU 与周边设备的沟通时脉、启动装置的搜寻顺序、硬盘的大小与类型、系统时间、 各周边汇流排的是否启动 Plug and Play、各周边设备的 I/O 位址、以及与 CPU 沟通的 IRQ 中断等等的信息。
在取得这些资讯后, BIOS 还会进行启动自我测试然后开始运行硬件侦测的初始化,并配置 PnP 装置, 之后再定义出可启动的装置顺序,接下来通过 INT13 调用,找到 MBR 中的 Boot loader,开始进行启动装置的数据读取。
嵌入式系统中,通常并没有像 BIOS 那样的固件程序,因此整个系统的加载启动任务完全由 bootloader 来完成。 用于引导嵌入式操作系统的 bootloader 有 U-Boot、vivi、 RedBoot 等等。 所以,嵌入式系统的 bootloader 功能分为两个阶段, 第一个阶段通过短小的汇编指令完成类似个人 PC 的 BIOS 的功能, 即硬件设备初始化,复制bootloader 进入 RAM,设置好堆栈, 跳转到第二阶段(也就是 PC 机的真正bootloader 阶段)的入口点。
而我们通过 minicom 控制开发板正式在 bootloader 第一阶段的功能完成后进入的。
1.2 Bootloader
在找到可启动的装置后, BIOS 会指向 MBR,寻找其中的 bootloader。 MBR 的前446 字节为 main boot code,装有系统的引导程序 (boot loader), 后面 64 字节为分区表,指示磁盘的分区信息。 其实,每个文件系统(filesystem,或者是 partition)都会保留一块启动磁区(boot sector) 提供操作系统安装 boot loader,而通常操作系统默认都会安装一份 loader 到他根目录所在的文件系统的 boot sector 上
MBR 中的 bootloader 有以下作用:
- 提供菜单:使用者可以选择不同的启动项目,这也是多重启动的重要功能!
- 我们在用 minicom 控制开发板时,就是通过设置各项启动项目的环境变量,来决定通过什么启动。主要的环境变量有:
– ipaddr, serverip, gatewayip,本机和服务器的 IP 地址,网关。
– bootargs, 启动参数,一般包括监控端口、内核启动参数,加载文件系统等
– bootcmd,启动命令,上电后或者执行 boot 命令后调用。
- 加载核心文件:直接指向可启动的程序区段来开始操作系统;
- 转交其他 loader:将启动管理功能转交给其他 loader 负责。
比如,文件系统是 jffs2 类型的,且在 flash 中, bootargs 的设置应该为
setenv bootargs ‘mem=32M console=ttyS0,115200 noinitrd root=/dev/mtdblock2 rw rootfstype=jffs2 init=/linuxrc’
bootcmd 设置为
` setenv bootcmd ’movi read kernel 0xc0008000;bootm 0xc0008000 `
这样,系统启动后,会从 eMMC 卡读取内核至内存 0xc0008000,然后从按照环境变量所设定的启动内核和文件系统。
Linux 系统安装时,你可以选择将 boot loader 安装到 MBR 去,也可以选择不安装。
但是在 Windows 安装时,他默认会主动的将 MBR 与 boot sector 都装上一份boot loader!
该 boot loader 是直接指向运行的操作系统的,即上述功能 2。
所以即使装了双系统,不通过 UEFI 启动的话,是不会出现系统选择画面的。
实验过程
1. 内核配置和编译
1.1 内核的下载
Linux 的最新内核可以通过登录 https://www.kernel.org/ 网址下载, 可以通过 https://www.kernel.org/pub/linux/kernel/ 下载 kernel 的各种版本, 包括从 v1.0~v4.x 的版本。 目前最新的内核已经到了 4.13.4(稳定版),候补版本已经到了 4.14-rc3。 直到 linux2.5 版本的内核, Linux 都通过简单的编号来区别内核的稳定版和开发板。 每个版本号用三个数字描述,由圆点分割。前两个数字用来表示版本号,第三个数字表示发布号。 第一位版本号从 1996 年开始就没有变过。第二位版本号表示内核的类型:如果为偶数,表示稳定的内核; 否则,表示开发中的内核。 Linux2.6 以后的版本通过加后缀来确定不同的使用途径, 比如 2.6.18-128.ELsmp 表示第 5 次微调 patch、企业版本、支持多处理器。 -rc 代表候补版, -git 代表开发版。目前教学使用的最广泛的内核版本还是 2.6.24, 有大量的书籍是基于此版本。 本实验使用的是 2.6.35。
1.2 内核的编译
载好了内核,通过 tar 命令解压缩后,会得到完整的内核文件夹( linux 中叫目录)。 编译内核的第一步是需要告诉系统编译内核的规则。 因为 Linux 操作系统在不同的使用场合有不同的功能,所以配置内核的方式并不单一。 而如果我们手动用 gcc –o 指令来配置一个如此庞大的代码需要太多时间而且太混乱, 所以我们需要用 make 指令来帮助我们编译。
1.2.1 内核编译原理
make 工具则可自动完成编译工作,并且可以只对程序员在上次编译后修改过的部分进行编译。 Makefile 文件是 make 工具最主要也是最基本的部分, make 的编译工作就是通过 Makefile 文件来描述源程序之间的相互关系来完成的。
make 的执行过程分为两个阶段。第一阶段:递归读取与配置相应的 makefile 文件 (包括“MAKEFILES”变量指定的、指示符“include”指定的、以及命令行选项“-f (–file) ”指定的 makefile 文件), 内建所有的变量、明确规则和隐含规则,并建立所有目标和依赖之间的依赖关系结构链表。 第二阶段:根据第一阶段已经建立的依赖关系结构链表决定哪些目标需要更新,并使用对应的规则来重建这些目标。
上述第一阶段需要读取的 Makefile 文件由.config 文件确定,编译时顶层 Makefile会调用.config 文件, 所以,改变了.config 文件便改变了内核配置的选项。 .config 文件的设定由 make menuconfig 指令实现(基于 ncurses 库), 这是一个图形化配置界面。配置完了将产生新的“.config” 文件,原文件“.config” 更名为“.config.old”。 然后通过 make 指令,系统就会根据顶层 Makefile 指定的规则编译内核,在编译目录生成 zImage 内核映像文件。
1.2.2 内核编译步骤
- 设置内核架构:
export ARCH=arm
,如果 Makefile 文件中写有相关内容则不需要 - 清除之前生成的.o 文件:
make clean
- 配置.config 文件:首先使用
make x210_initrd_defconfig
,导入配置好的相应.comfig 文件, 这样可以大大减少手动配置工作量;然后使用命令:make menuconfig
配置一些剩余选项: 设置交叉编译器,设置 RAM filesystem 支持 gzip文件(这样内核文件系统可以以压缩文件的方式放入物理存储空间中), system type 选择相应开发板,设置内核支持 ramdisk 并设置 RAM block 大小。 - 输入 make 指令,将生成的 zImage 文件上传 tftp 服务器供开发板下载。
1.2.3 内核配置中的必须项
- File system 和 Memory Technology device 相应支持, 因为本实验用的 ramdisk 作为 filesystem,所以对 ramdisk 作为 filesystem 的支持需要配置, 以及配置 ramdisk 的大小,否则无法挂载文件系统
- System type,选择对应的开发板(一般 make x210_initrd_defconfig 已经配置好了) 。
- character device 中 support for console on serial support 和 support for console on virtual terminal,设置了这个前者,在目标板的 linux 启动后, 可以通过串口作为终端操作 Linux,接收 Linux 核心发来的数据和警告等等; 而后者则是使目标板可以像其他 Linux 设备一样,通过一个虚拟的文字界面控制台操作 Linux。
- 交叉编译器设定,否则编译出来的二进制码不符合相应开发板的架构,从而无法运行
2. 嵌入式文件系统构建
只有内核的系统,虽然屏蔽了底层硬件驱动,完成进程管理、内存分配等基本功能, 提供了上层应用的接口,但没有文件系统的操作系统,仍然是一个空壳。 文件系统是操作系统用于明确物理存储设备(常见的是磁盘,也有基于 NAND Flash 的固态硬盘) 或分区上的文件的方法和数据结构——即在存储设备上管理组织文件的方法。 文件系统由三部分组成:文件系统的接口,对对象操纵和管理的软件集合,对象及属性。 具体地说,有了文件系统,用户才能在物理存储空间上建立文件,存入、读出、修改、转储文件,控制文件的存取,完成对其基本的使用。
2.1 文件系统的类型和比较
- ext2fs/ext3fs:可以实现快速符号链接,类似于 Windows 文件系统的快捷方式, 可将目标名称直接存储在索引节点表中,提高了访问速度;支持的内存大至4TB,文件名称很长,可达 1024 个字符; 管理者在创建系统文件时根据需要选择存储逻辑块的大小。 相比 ext2fs, ext3fs 是一个带日志型的文件系统,支持快速自检恢复,具有更高的实用性。 这类文件系统稳定,可靠,健壮,在台式机、服务器、工作站中普遍使用。
- jffs2:支持数据压缩,多种文件节点类型,是一种基于 FLASH 的日志文件系统, 提高了对闪存的利用率,降低了内存的损耗。通过 jffs2,可以通过 flash 来存储数据, 将 flash 当作硬盘来使用,而且系统运行的参数可以实时保存在 flash 中,在系统断电后数据不会丢失。 它在嵌入式系统中很受欢迎。
- romfs: 是一种相对简单,占用空间较少的文件系统。 它比 ext2 文件系统代码少,而且它在建立系统超级块时需要更小的空间。 但它只是可读文件系统,禁止写操作,因此系统同时需要虚拟盘( ramdisk)来支持临时文件和数据文件的存储。
- nfs: 该文件系统能够使文件实现共享。 其最大的功能就是可以通过网络,让不同操作系统的计算机可以共享数据,所以也可以将它看做是一个文件服务器。 NFS文件服务器是 Linux 最常见网络的服务之一。尽管它的规则简单,却有着丰富的内涵。 NFS 服务器可以看作是一个文件服务器,它可以让你的 PC 通过网络将远端的NFS 服务器共享出来的文件挂载到自己的系统中, 在客户端看来使用 NFS 的远端文件就像是在使用本地文件一样。
2.2 根文件系统的制作步骤
制作一个可以使用的文件系统,需要在其根目录下创建 Linux 操作系统启动、运行时所需要的基本文件和目录, 以及具有一些基本的指令集供使用者使用。
2.2.1 启动文件
- initrd:准确来说 initrd 不是一个需要制作的文件,而是操作系统启动时需要配置的一项环境变量。 当 bootloader 将 kernel 读入 ram 后(如果是 ramdisk 启动,则boot 时直接在 bootm 指令后加上 kernel 所在的 ram 地址即可完成此操作),按照一定处理 initrd 所指内容的规则,直接将 initrd 的内容释放到一个未初始化的 Ramdisk里,这个过程与 Ghost 恢复一个分区的过程十分相似。 于是,这个 Ram 中相应的Ramdisk 也就被格式化成该 initrd 所指内容的分区格式, 同时, initrd 中相应的内容被加载到 Ramdisk 中,释放原 initrd 所指内容占用的内存空间, 然后按可读写的方式挂载这个新的 Ramdisk。
- /linuxrc: Ramdisk 挂载后,启动/linuxrc 文件作为 1 号内核进程, 该文件用于挂载 Ramdisk 中的 filesystem 到根文件系统, 通过系统调用 pivot_root()改变根目录,完成文件系统的挂载,然后执行/sbin/init
-
/etc/inittab: /sbin/init 系统初始化程序会执行 inittab 中所描述的设定, 包括sysinit: /etc/init.d/rcS 等等。Inittab 的格式为 identifier: run_level: action: process。 本实验出于简单能用的考虑不需要管前两者。 Action 含义如下: sysinit:只有在启动或重新启动系统并首先进入单用户时,init 才执行这些登记项; askfirst:进入 console 之前先询问一下 shell 要不要进入; once:只要进入指定的运行级就启动一次本进程; ctrlaltdel:允许 init 在用户于控制台键盘上按下 Ctrl+Alt+Del 组合键时, 重新启动系统; restart:系统重启的时候要执行的进程;shutdown:系统关机时需要执行的进程。
- /etc/rc:/etc/rc 是/etc/init.d/rcS 的符号链接, iniitab 被执行时,系统会通过bash( /bin/sh)开始运行/etc/init.d/rcS,即运行/etc/rc,所以该文件需要有可执行性。 该 scripts 主要用于挂载/proc 目录用于进程管理( mount –t proc proc /proc), 以及通过 cat 指令显示/etc/motd 中的内容( /bin/cat /etc/motd), 所以/etc/motd 中内容也就成为了开机画面最后显示的内容了。
2.2.3 系统运行时必须的目录
- /proc 目录用于系统的伪文件系统(pseudo filesystem) 挂载。该文件系统用于进程管理,制作时创建该空目录即可。
- /etc 系统的配置文件都在这个里面
- /sbin /bin 这两个目录存放了一些基本指令集的执行程序, 比如/bin/sh, /sbin/ls 等,这些执行程序并不是真正存在的, 而是以符号链接链接到 Busybox 工具编译出的 busybox 程序中, 用户在使用 ls 指令时,实际上是调用了 busybox 程序相应功能来完成需求的。 Busybox 程序也是类似内核的方式编译生成的,在 menuconfig 中选择交叉编译器, 以及需要编译的程序,只有选择了这些程序后开发板在运行时才能执行相应指令。 然后执行 make、 make install 指令即可。
- /dev 这是一个设备文件目录,任何装置与接口设备都是以文件的型态存在于这个目录当中的。 你只要透过存取这个目录底下的某个文件,就等于存取某个装置。 在构建文件系统的时候,需要创建几个基本的设备文件: null 设备文件类似垃圾站,所有写入该文件的内容都会被舍弃,读取该文件只能返回 EOF; zero 文件用于生成一个指定大小全是 0 的文件,往往用在 dd 指令生成设备文件中; console 设备文件是系统控制台,在 Linux 系统使用时所必须。以上设备是主机电脑中通过 mknod 生成, 输入相应的设备号即可。因为该实验只是 Ramdisk 作为的文件系统,不涉及其他 hdc、USB 等文件的驱动, 所以并不需要安装这些驱动。
- /lib 该目录下存放的各种库文件。在 Linux 很多程序在编译时,为了节省代码量, 都是以共享库或动态库的方式链接到 lib 中的库函数中,所以没有相应的库文件,很多 Linux 基础程序都无法正确运行。
Busybox 中编译了的执行程序(控制台能输入的指令)和 lib 中的库文件基本决定了该开发板在使用时能执行的功能, 和进行编程开发时能调用的库函数。 Busybox中很多指令的正常运行,不仅依赖与库函数是否存在(若是通过动态链接方式编译的话), 而且依赖于内核配置的时候是否配置了相应选项,比如网络接口设置等等。
2.2.3 生成文件系统镜像
为了生成并修改 ramdisk, 需要在主机上创建一个空文件并将它格式化成 ext2fs 文件系统映像。 格式化后的文件就可以像普通文件系统一样在主机上进行挂载和卸载。挂载后可以进行正常的文件和目录操作, 将上述所做的目录和文件复制进去。卸载后,如果原映像文件仍然存在,则更新到卸载之前的操作内容。 然后将此设备文件通过 gzip 打包,上传到 tftpboot 文件夹中。
3. 启动移植的嵌入式操作系统
设置好环境变量后,通过 tftp 下载内核文件系统至开发板的 RAM 相应的地址中中,通过 bootm 指令启动。输入指令如下:
x210# tftp 0x30008000 zImage
x210# tftp 0x40000000 ramdisk_img.gz
x210# bootm 0x30008000
实验步骤整理
1. 编译linux内核
-
将下载好的linux内核源码解压 (可以直接到我的github上下载)。 注意应该解压到一个你拥有读写权限的目录下。比如直接在自己的主目录下创建一个名为workspace的文件夹, 然后将源码压缩包(本实验为: linux-2.6.35.tar.gz)放到该文件夹下,进入文件再解压即可。具体操作命令如下:
mkdir ~/workspace mv your_linux_code_path/linux-2.6.35.tar.gz ~/workspace cd ~/workspace tar -zxvf linux-2.6.35.tar.gz cd linux-2.6.35
-
配置默认编译选项。默认配置文件在 linux-2.6.35/arch/arm/configs 下面。 可以用命令 ` ll arch/arm/configs `得到文件名然后复制就可以了。
export ARCH=arm make clean make x210ii_initrd_defconfig
-
配置其他编译选项。执行以下命令:
make menuconfig
然后就会进入一个图形界面。具体要配置那些选项参见上面内核的编译步骤部分。这里放出我的配置的结果。
进入 General setup
设置(写上)Cross-compiler tool path 以及加上你想要的Local version
(这个可以不用,但是建议可以设置以下,加上自己的标识)。
设置好之后,往下翻到Initial RAM filesystem and RAM disk
那一项。
这里说明一下,因为该实验中是先做内核再做文件系统,所以才做如下的设置,
如果你已经做好了文件系统,则可以把文件系统编到内核里面。
配置了以上几项,基本上就可以了。如果你还需要一些更多的功能,可以自己研究一下里面的配置选项。比如设备文件自动挂载等等。
-
退出配置界面以后,执行
make -jn
(n 表示你想用来编译的线程数)。make -j4
2. 制作busybox文件系统
2.1 编译busybox
将下载的busybox压缩包放到 ~/workspace
,解压。然后进入解压后的目录。执行:
make menuconfig
出现图形配置界面:
进入Busybox Setting
进入 Build Options,选择静态编译,配置交叉编译器路径。
然后退出。
当然,对于APPlets的选择主要由你自己来决定。
执行:
make -j4
编译完成后执行:
make install
busybox 的东西就会安装到了 ./_install 下面(如果你配置的安装路径是这个的话)。
2.2 配置文件系统
进入 _install 目录。
- 创建 etc 目录,在 etc 目录下创建 inittab, rc, motd 三个文件。
/etc/inittab
# /etc/inittab
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::once:/usr/sbin/telnetd -l /bin/login
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
此文件由系统启动程序 init 读取并解释执行。以 # 开头的是注释行。
/etc/rc
#!/bin/sh
hostname x210
mount -t proc proc /proc
/bin/cat/ /etc/motd
此文件要求可执行权限。
/etc/motd
Welcome to
=======================================
ARM-LINUX WORLD
=======================================
x210v3 @ S5PV210/Cortex-A8
ported by yusnows
此文件内容随意,由 /etc/rc 调用打印在终端上。
- 在 etc 目录下再创建 init.d 目录,并将 /etc/rc 向 /etc/init.d/rcS 做符号链接。此文件为 inittab 的制定脚本:
mkdir init.d
cd init.d
ln -s ../rc rcS
- 创建 dev 目录, 并在该目录下创建必要的设备:
mknod console c 5 1
mknod null c 1 3
mknod zero c 15
-
在 _install 目录下创建 proc 空目录, 供 proc 文件系统使用。
-
在 _install 目录下创建 lib 目录,将交叉编译器链接库路径下的几个库复制到 lib 目录:
ld-2.10.1.so, libc-2.10.1.so, libm-2.10.1.so
并做相应的符号链接:
ln -s ld-2.10.1.so ld-linux.so.3
ln -s libc-2.10.1.so libc.so.6
ln -s libm-2.10.1.so libm.so.6
如果 Busybox 以静态方式编译,没有这些库,不影响系统正常启动,但会影响其他动态链接程序运行。
至此文件系统目录构造完毕。从根目录(_install)看下去,应该至少有下面几个目录:
` bin dev etc lib lost+fount mnt proc sbin `
他们是下面制作文件系统镜像的基础。
2.3 制作 ramdisk 文件映像
cd ~
dd if=/dev/zero of=ramdisk_img bs=1k count=8192
mke2fs ramdisk_img
mount ramdisk_img //这里需要在Host的/etc/fstab 写上相应的配置,具体请百度
(将刚才的制作的文件系统文件复制过来)
umount /mnt/ramdisk
gzip ramdisk_img
完成以后就会得到 ramdisk_img.gz 文件。
3. 启动目标平台
将文件系统映像文件已经kernel镜像放到tftp服务器以后,就可启动目标平台了。
首先连接好目标平台的网线,串口等。 在host做好相应的串口(minicom)设置,打开minicom。 给目标平台上电,按下目标平台开机键。 如果串口连接正确,这是host的minicom终端就会显示目标平台的开机信息。 在3秒内按下空格键,进入 bootloader 环境。然后做以下配置:
x210# setenv ipaddr 192.168.1.222 //给目标板的ip地址
x210# setenv serverip 192.168.1.101 //你的Host主机的ip
x210# setenv ramdisk root=/dev/ram rw initrd=0x40000000,8M
x210# setenv bootargs console=ttySAC2,115200 $ramdisk
x210# tftp 0x30008000 zImage
x210# tftp 0x40000000 ramdisk_img.gz
x210# bootm 0x30008000
一切正常的话,就可看到目标平台正确启动了你自己制作的系统了。