这一个月来,我一直投身于Linux内核移植的伟大而光荣的事业,相当的痛苦了……
不过收获也是很大的,昨天晚上,哦应该是今天凌晨12:00,终于我的板子上出现了Busybox启动的好消息。Linux内核的移植终于取得了阶段性的成果,虽然后面的路还很长。
从我开始搞Linux,我就一直在网上或者图书馆中寻找“HOWTO”性质的文章,遗憾的是还真少。我也希望我的Blog成为我做的这块板子的“HOWTO”大全,呵呵。但是今天我不写“HOWTO”,对我的工作来个概括性的总结。
如果你是猎人的QQ群里过来看这个Blog,而且想获得与SBC2410X自带的VIVI和MIZI Linux的相关信息的,我很抱歉。我认为猎人板子里提供的资料实在是比较垃圾(实际上是友善之臂公司的资料),应该说是一些仅仅针对SBC2410X一种方式应用的“HOWTO”,而且没有提供其他方式应用的任何说明(呵呵,当然我没有责怪猎人的意思,这块板子硬件还是相当不错的,价钱也非常公道)。嵌入式系统的一个很大的特点就是其针对不同应用的可裁减性。举个例子,我希望把ARM用在机器人上,不需要网卡、声卡、USB,只需一个串口,用友善之臂给的资料能做什么?甚至连mkyaffsimage这个制作yaffs文件系统映像工具的源码都没有。其实其他厂家的也一样,可能这些所谓的开发板仅仅是一种技术垄断,尤其是那些动辄好几千块钱的。猎人提供的廉价开发板,我希望能通过我提供的这些开发资料,像开源工程一样,让更多的人受益。
作为一个初学者,最关心的是怎么下手。我先简要说明一下:
首先必须明确你需要的是什么样的系统
ARM Linux应该归类为一种非抢占式的分时多任务操作系统(相关术语可以Google一下),它不适合用来做伺服控制、图像处理等对系统实时性要求较高的控制。uCOS-II,VxWork可以胜任那样的工作。Linux的优点在于开源(Open Source)、网络支持和跨平台支持。其中我最看重的是跨平台支持。因为我的实验室经常使用DSP、单片机控制机器人,而这些程序不具有平台移植性,所以每届硕士做机器人的控制都是从学习DSP或单片机做起,相当浪费时间。我的工作的目的就是利用ARM、Linux、uCOS-II,设计ARM-DSP双核系统,搭建一个控制平台。
然后必须建立开发环境。包括硬件环境和软件环境。
硬件环境就是必须购买一个ARM开发板,推荐买猎人的,这个与三星的SMDK2410这块“大众型”开发板完全兼容。搞嵌入式开发和普通的DSP、单片机开发不同,需要最多的是代码的移植,一个与“大众”系统兼容的开发板,可以节约移植别人编写的针对“大众”系统代码的时间。软件环境指的是需要有一台可以运行Linux的PC,以及PC的软件。这些软件包括gcc编译器、gcc交叉编译器(在PC平台编译非PC平台程序的编译器)、Linux NFS支持(用于网络下载内核到开发板)、Linux SMB支持(Linux访问Windows网络共享)、代码察看软件(例如Source Navigator)、Bootloader和内核源代码。其中前四项在安装Linux时候就必须选定,后几项从网络上获得。推荐在Windows下使用虚拟机建立开发系统,可以带来非常大的方便。具体的软硬件系统建立步骤“HOWTO”请看5月15日我写的文章。Linux内核和相应的编译器的下载地址我后面的文章会写出来。另外,有一定硬件电路开发经验的人会有需不需要仿真器的疑问,答案是:根本不需要也不可能用仿真器来调试,一个烧写电缆足以,除非你搞ARM7TDMI-S这种经过裁剪的ARM CPU的程序。主要的调试信息显示渠道是串口,主要的代码下载渠道是串口或者以太网——这由你的Bootloader决定,一个好的Bootloader可以大大减少内核调试的时间。我调试内核使用的是U-Boot的网络下载功能,800kB的内核只需要5~6秒就可以下载结束,我的开发板原配的VIVI的串口需要3分钟多,效率的差距显而易见,更不用说内核之后动辄几个或者几十个MB的文件系统了。
接下来就可以进行开发了,首先是Bootloader
Bootloader是系统上电以后执行的第一段程序,它主要的功能和作用有:初始化硬件,复制Linux内核和Ramdisk文件(如果使用的话)到内存中,把预先设定的内核参数传递给内核并启动,提供硬件诊断、Flash烧写、网络连接等功能。推荐使用U-Boot,U-Boot的下载地址http://sourceforge.net/projects/u-boot。U-Boot的“HOWTO”可以看我5月23、30日写的文章。
还必须说明的是,通常从开源工程网站下来的都是软件的源码,需要自己进行编译。编译由源码目录下的“Makefile”这个文件决定,其中包含有关于编译的配置和操作类型的信息。一般用于多平台的源码,Makefile中都有CROSS_COMPILE这一项,在编译之前必须将它指定到交叉编译器。对于arm-linux-gcc这个交叉编译器,如果设置了指向arm-linux-gcc的缺省路径,可以填写CROSS_COMPILE=arm-linux-,注意,后面的“-”不可以少。通常由于开源代码的编译器兼容性太差,你需要在你的电脑里安装多个版本的交叉编译器,例如我的U-Boot用2.95.3编译器编译,而Linux内核用的是3.4.1,我设置2.95.3为默认编译器。对于使用非默认路径编译器的代码,可以填写CROSS_COMPILE到绝对路径,例如CROSS_COMPILE=/usr/local/arm/3.4.1/bin/arm-linux-。另外,Linux内核的Makefile还有一个ARCH选项要修改,以后说。
编译开源代码的一般步骤为:清除之前编译结果(clean)、设定编译配置(config)、编译(make)
1.清除之前编译结果
一般可以用:
# make clean
或者
# make distclean
这2种写法的区别在于,distclean不仅仅清除编译生成的*.o文件,而且还清除已经设定的编译配置。
2.设定编译配置
主要作用是将繁杂的代码进行组织,屏蔽所需编译目标不需用到的源代码文件,并将这个组织的结果保存下来。设定编译配置命令例如:
Linux内核编译的:
# make menuconfig
# make config
# make smdk2410_defconfig
U-Boot编译的:
# make smdk2410_config
menuconfig是最常用的一种方式,它将调用本机编译器(HOSTCC)产生一个交互的文本菜单式界面,对内核的编译选项、驱动支持、文件系统、调试、外设等进行设置。config是一个提问式的配置方式,很麻烦,不推荐使用。smdk2410_defconfig是针对三星的SMDK2410开发板的基本硬件进行配置,这可以节约一些配置内核的时间。类似这种xxxx_config的配置,体现了购买“大众”型开发板的优势,可以用最短的时间,建立起一套有效的嵌入式系统。
3.最后是编译
一般可以直接输入make。但是对于Linux2.4内核,之前还必须输入
# make dep
用以产生源代码之间的关联信息,2.6版本的内核不再需要这个步骤。
对于Linux内核,编译可以生成不同格式的映像文件,例如:
# make zImage
# make uImage
zImage是ARM Linux常用的一种压缩映像文件,uImage是U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的“头”,说明这个映像文件的类型、加载位置、生成时间、大小等信息。换句话说,如果直接从uImage的0x40位置开始执行,zImage和uImage没有任何区别。另外,Linux2.4内核不支持uImage,Linux2.6内核加入了很多对嵌入式系统的支持,但是uImage的生成也需要设置,这个以后我会介绍。
另外,我假设内核不采用任何模块。没有编译模块这一步。
然后是内核
内核的移植和调试是整个嵌入式系统开发中最麻烦的一步,建议如果没有良好的硬件和C语言基础,以及健壮的神经和充裕的时间,不要涉足内核的开发,尽可能采用网络上例如我的Blog中
提供的现成方法,包括所有细节都要一一仿制,完成内核的移植。其实,这是考验你运气的一个环节,可能有的人一次就移植成功,可能有的人因为一些小小环节的
问题,例如编译器版本问题、内核版本问题、代码修改输入错误之类的,遇到一些稀奇古怪的错误,卡住你的工作而无法继续。同时,这对你的耐心和毅力也是一个
考验。呵呵,我被考验了一个月,也算是比较倒霉吧。内核修改和编译需要做的工作很多,由于Linux设备的驱动是集成在内核里的(当然还可以作为模块挂载,但是我不讨论模块问题),需要对内核代码进行修改。以后我会专门介绍。内核实现的功能是对硬件资源如内存、串口、液晶控制器的管理,以及对操作系统执行的任务程序的调度。
最后是文件系统
内核不能包括一个嵌入式系统所需的所有东西,例如程序,资料等,而且如果没有文件系统,对嵌入式系统工作过程产生信息的纪录也会成问题,所以一般的嵌入式Linux系统需要文件系统。当然没有文件系统内核也不是不能工作,这我就不太懂了,毕竟是个高深的问题……以我的跟踪内核运行认识来看,只要把你所需要执行的任务编译进内核,然后对初始化部分进行修改,让其在没有root文件系统的时侯继续下去,对你的任务进行调度也许就可以。
嵌入式Linux和PCLinux不同,一般嵌入式系统没有硬盘,所以传统的Linux文件系统如ext2,hpfs之类的不能用在嵌入式Linux上。常见的嵌入式文件系统有:romfs,cramfs,jffs,yaffs等,当然也可以通过Ramdisk使用ext2。这些文件系统的特性可以自己Google一下。
我最初接触到文件系统的时候,曾经有个很纯朴的想法,既然电脑硬盘上有文件系统,需要分区后格式化,那么嵌入式系统的Flash是不是也必须进行这样的操作呢,而且,分区表纪录在那里呢?在嵌入式Linux中,对Flash并不需要进行分区,目前我的解决方法是把“分区表”信息写入内核,然后直接在内核中分区表信息对应的Flash区域读写,这在内核中叫做MTD支持(Memomry Techonlogy Device),Flash同样需要擦除后写入相应内容——这可以通过Bootloader来完成,不像PC中用的是安装程序。
关于文件系统的建立,我还没有完全研究完,我仅仅是利用了从我的开发板原始文件系统中剥离的一个文件系统,还有很多错误,以后我会补充上文件系统建立的详细文章。
在这些总结背后,我做了许多工作,包括跟踪U-Boot启动Linux部分的代码,内核启动代码,内核初始化代码等。我利用了内核的一些汇编的接口,用以在内核启动过程中输出调试信息。如下:
这是保存寄存器的宏:
/*
* Lu Xianzi's Kernel debugging macro
*/
#define LXZ_LL_DEBUG 1
.macro lxz_save_regs
stmfd sp!, {r0-r12}
.endm
.macro lxz_restore_regs
ldmfd sp!, {r0-r12}
.endm
这是一个调用的例子:
/* =============================================================== *
* Tracing code added by Lu Xianzi
*/
#ifdef LXZ_LL_DEBUG
lxz_save_regs
adr r0, lxz_str_into_creat_pt
bl printascii
adr r0, lxz_str_page_tbl_addr
bl printascii
mov r0, r4
bl printhex8
mov r0, #'/n'
bl printch
lxz_restore_regs
b 1f
lxz_str_into_creat_pt:
.asciz "/nInto create page table section!/n"
.align
lxz_str_page_tbl_addr:
.asciz "Page table address = "
.align
1:
#endif
/* =============================================================== */
需要提醒的是,printascii,printhex,printch这几个函数只是使用了r0~r3这几个寄存器。
跟踪内核的运行是一件非常痛苦的事情,但真能解决问题,而且学到很多东西。