嵌入式ARM + Linux系统移植
作者:周曙明
时间:2011年3月1日
注:该博文是从word文档中拷贝过来的,其中有一些地方不能显示的请见谅!
开发板配置
开发板:MINI2440
CPU:S3C2440
Nand Flash:K9F2G08 258MB
Nor Flash:S29AL016MB 2MB
网卡:DM9000A
开发平台
PC机:双系统Windows XP + Ubuntu 10.04
编译环境:arm-linux-gcc 工具集
参考资料:
《嵌入式Linux应用开发完全手册》 韦东山 著
《ARM嵌入式系统基础教程》周立功 著
《ARM体系结构与编程》 杜春雷 著
第一章 ARM基础
由于网络与通信技术的发展,嵌入式系统在经历了近20年的发展历程后,又进入了一个新的历史发展阶段,即从普通的低端应用进入到一个高、低端并行发展,并且不断提升低端应用技术水平的时代,其标志是近年来32位MCU的发展。
当网络、通信和多媒体信息家电业兴起后,ARM公司适时地推出了32 位ARM系列嵌入式微处理器,以其明显的性能优势和知识产权平台扇出的运行方式,迅速形成32位机高端应用的主流地位。
1.1 ARM公司简介
ARM公司是一家知识产权(IP)供应商,它与一般的半导体公司最大的不同就是不制造芯片且不向终端用户出售芯片,而是通过转让设计方案,由合作伙伴生产出各具特色的芯片。ARM公司利用这种双赢的伙伴关系迅速成为了全球的RISC微处理器标准的缔造者。这种模式也给用户带来了巨大的好处,因为用户只需要掌握一种ARM内核结构及其开发手段,就能够使用多家公司相同的ARM内核的芯片。
1.2 RISC结构特性
ARM内核采用精简指令集计算机(RISC)体系结构,是一款小门数的计算机。其指令集和相关的译码机制比复杂指令集计算机(CISC)要简单得多,其目标就是设计出一套能在高时名师钟频率下单周期执行,简单而有效的指令集。RISC的设计重点在于降低处理器中指令执行部件的硬件复杂度,这是因为软件比硬件更容易提供更大的灵活性和更高的智能化。
1.3 常用ARM处理器系列
ARM公司开发了很多系列的ARM处理器,应用比较多的是ARM7系列、ARM9系列、ARM10系列、ARM11系列,Intel的Xscale系列和MPCore系列;还有针对低端8位MCU市场最新推出的Cortex-M3系列,其具有32位CPU的性能、8位MCU的价格。我的开发板用的就是S3C2440CPU,它是一款ARM9芯片。
1.4 ARM9体系结构
1.4.1 3级流水线
ARM处理器使用流水线来增加处理器指令的速度,这样可以使几个操作同时进行,并使处理器和存储器系统之间的操作更加流畅、连续,能提供0.9MIPS/MHz的指令执行速度。3级流水线如图1所示
指令1 PC-8
指令2 PC-4
指令3 PC
指令4 PC+4
|
图1 ARM 3级流水线
1.4.2 ARM处理器模式
ARM处理器共支持7种处理器模式,并以当前程序状态寄存器CPSR中的控制位M[4:0]反映处理器正在操作的模式,如表1所示。除了用户模式(usr)以外的其他6种处理器模式称之为特权模式。
处理器模式 |
说明 |
备注 |
用户(usr) |
正常程序运行的工作模式 |
不能直接从用户模式切换到其他模式 |
特权模式 |
系统(sys) |
用于支持操作系统的特权任务等 |
与用户模式类似,但具有直接切换到其他模式等特权 |
异常模式 |
管理(svc) |
供操作系统使用的一种保护模式 |
只有在系统复位和软件中断响应时,才进入此模式 |
中止(abt) |
用于虚拟内存和(或)存储器保护 |
当处理器访问存储器失败时,进入数据访问中止模式 |
未定义(und) |
支持软件仿真的硬件协处理器 |
只有在未定义指令异常响应时,才进入此模式 |
中断(irq) |
中断请求处理 |
只有在IRQ模式响应时,才进入此模式 |
快速中断(fiq) |
快速中断请求处理 |
只有在FIQ异常响应时,才进入此模式 |
表1 七种ARM处理器模式
1.4.3 ARM内部寄存器
在ARM处理器内部共有37个用户可访问的32位寄存器,表2列出所有这些寄存器。
寄存器类别 |
寄存器在汇编中的名称 |
各种模式下实际访问的寄存器 |
用户 |
系统 |
管理 |
中止 |
未定义 |
中断 |
快速中断 |
通用寄存器和程序计数器 |
R0(a1) |
R0 |
R1(a2) |
R1 |
R2(a3) |
R2 |
R3(a4) |
R3 |
R4(v1) |
R4 |
R5(v2) |
R5 |
R6(v3) |
R6 |
R7(v4) |
R7 |
R8(v5) |
R8 |
R8_fiq |
R9(SB,v6) |
R9 |
R9_fiq |
R10(SL,v7) |
R10 |
R10_fiq |
R11(FP,v8) |
R11 |
R11_fiq |
R12(IP) |
R12 |
R12_fiq |
R13(SP) |
R13 |
R13_svc |
R13_abt |
R13_und |
R13_irq |
R13_fiq |
R14(LR) |
R14 |
R14_svc |
R14_abt |
R14_und |
R14_irq |
R14_fiq |
R15(PC) |
R15 |
状态寄存器 |
CPSR |
CPSR |
SPSR |
— |
SPSR_svc |
SPSR_abt |
SPSR_und |
SPSR_irq |
SPSR_fiq |
|
|
|
|
|
|
|
|
|
|
|
|
|
表2 ARM状态各种模式下的寄存器
一般的的通用寄存器
寄存器R0~R7为保存数据或地址值的通用寄存器,这是因为在任何处理器模式下,R0~R7中的每一个寄存器都是同一个32位物理寄存器。寄存器R0~R7是完全通用的寄存器,不会被体系结构作为特殊的用途,并且可用于任何使用通用寄存器的指令。
第二章 UBOOT程序分析
在学单片机时,我们都是写裸机程序直接在MCU上面运行,ARM芯片也可以,但是这样ARM跟单片机就没两样了,最多也只能算是一块运行速度快一点的单片机(S3C2440的主频可以达到300MHz)。所以这样就不能体现出ARM芯片的长处,ARM芯片是一款适合跑操作系统的芯片,所以下面我就来开始让我们的开发板运行操作系统。
首先,一个操作系统是由引导程序、操作系统内核、文件系统、应用程序组成的。引导程序的任务是把存储在Flash上面的操作系统内核拷贝到内存上,然后,引导程序就跳转到内核去执行。别看引导程序的工作就是一个把内核拷贝到内存,其实实现这个功能会涉及到很多方面的问题。让我们来一个一个地解决这些问题吧。
下面我们就来开始研究引导程序。
我们使用开源的Uboot做为我们的引导程序。Uboot下载网址为:ftp://ftp.denx.de/pub/u-boot/ ,这是一个ftp站点。我们选择Uboot1.2.0作为我们的研究代码。本文中如无特殊说明,Uboot即指Uboot1.2.0。我们把uboot1.2.0源码下载下来后,放到linux虚拟机内解压,注意,不要在windows系统里解压,因为windows系统不区分文件名大小写,而linux系统对大小写区分,Uboot中很多文件名的大小写不同的,windows会认为是同一个文件,而把它覆盖了。
Uboot解压后,我们会发现它里面有很多的文件夹和文件,看到这么多文件夹时会不会先就产生一种恐惧感呢?当然对新手来说多少都会有点的,但是这些文件夹是为了更好管理代码,我们并不会全部用到这些代码,我们的arm开发板只会用到其中的一小部分代码。我们只要认真去看,就会知道这些文件夹里的代码是干什么用的。网上这方面的教程太多了,我在这里也多说了。
2.1下面我们来分析uboot的程序。
2.1.1 Uboot 程序是从start.s文件开始的。
异常向量表:
.globl _start
_start:
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq
在start.s文件一开始我们就会看到这些代码,很明显这是arm 的异常向量表。_start标号告诉链接器这是程序开始的地方,也就是指明第一条指令。
定义变量:
_TEXT_BASE:
.word TEXT_BASE
.globl _armboot_start
_armboot_start:
.word _start
这里是定义了两个变量,或者也可以说是定义了两个常量。在start.s文件中定义的变量不止这两个,还有__bss_start,_end等。.globl表示定义的是一个全局变量。
程序开始:
reset:
mrs r0, cpsr
bic r0, r0, #0x1f /* 00011111 */
orr r0, r0, #0xd3 /* 11010011 */
msr cpsr, r0
这几行程序是把CPU置为管理模式。
接下来就是关闭看门狗,关中断了
ldr r0, =pWTCON /*pWTCON = 0x53000000 */
mov r1, #0x0
str r1, [r0] /* 关闭看门狗 */
mov r1, #0xffffffff
ldr r0, =INTMSK /* INTMSK = 0x4A000000 */
str r1, [r0] /*屏闭所有中断*/
ldr r1, =0x7ff /* s3c2440 0x7ff;s3c2410 0x3ff */
ldr r0, =INTSUBMSK
str r1, [r0] /* 屏闭所有子中断 */
接下来是执行cpu初使化
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
#endif
因为我们的mini2440.h配置文件中并没有定义CONFIG_SKIP_LOWLEVEL_INIT,所以会执行bl cpu_init_crit。 所以就跳到cpu_init_crit函数去执行了。
cpu_init_crit:
mov r0, #0
mcr p15, 0, r0, c7, c7, 0 /* 使无效cache */
mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
orr r0, r0, #0x00000002 @ set bit 2 (A) Align
orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
mcr p15, 0, r0, c1, c0, 0
mov ip, lr
bl lowlevel_init
mov lr, ip
mov pc, lr
执行到bl lowlevel_init时就跳到lowlevel_init函数去执行了。它在lowlevel_init.s中定义。
.globl lowlevel_init
lowlevel_init:
ldr r0, =SMRDATA
ldr r1, _TEXT_BASE
sub r0, r0, r1
ldr r1, =BWSCON /* Bus Width Status Controller */
add r2, r0, #13*4
0:
ldr r3, [r0], #4
str r3, [r1], #4
cmp r2, r0
bne 0b
mov pc, lr
这里是在设置s3c2440的8个bank的寄存器。这里不多说。
然后程序返回,返回到cpu_init_crit函数,cpu_init_crit函数又返回到bl cpu_init_crit的地方,继续执行。
然后执行
relocate:
adr r0, _start
ldr r1, _TEXT_BASE
cmp r0, r1
beq stack_setup
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2
add r2, r0, r2
copy_loop:
ldmia r0!, {r3-r10}
stmia r1!, {r3-r10}
cmp r0, r2
ble copy_loop
这段程序是把nor flash里面的uboot程序拷贝到ram 里面去。因为我们烧录时是把uboot烧入到nor flash中的,它要执行程序时需要把程序拷贝到ram里面去。
我们来分析一下这段程序:
adr r0,_start 这条指令是根据PC值和偏移计算出_start标号所在的地址,因为是根据PC值来计算的,所以r0里面存储的是物理地址。也就是第一条指令的地址。现在我们是把uboot烧写到了nor flash 里面,nor flash的起始地址是0,所以这时r0 = 0;而有时候,为了调试,我们不会把程序烧写到nor flash中,而是把它下载到ram 的某个地方,比如0x31000000,这时r0 的值就是0x31000000。所以r0是当前程序所处的实际地址。
ldr r1, _TEXT_BASE 这条指令中r1 = _TEXT_BASE的值,这是我们设定的一个值,它是程序重定位的起始地址。
接下的程序就是判断r0和r1的值了,比较它们是不是相等,如果相等,就说明程序本来就处在设置的地方不需要重定位,所以就会执行beq stack_setup,跳过后面的重定位代码,而执行栈设置。如果r0和r1不相等,则跳过beq stack_setup,执行后面的程序。后面的代码就是完成代码重定位了,不多说。
stack_setup:
ldr r0, _TEXT_BASE
sub r0, r0, #CFG_MALLOC_LEN
sub r0, r0, #CFG_GBL_DATA_SIZE
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ + CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12
clear_bss:
ldr r0, _bss_start
ldr r1, _bss_end
mov r2, #0x00000000
clbss_l:
str r2, [r0]
add r0, r0, #4
cmp r0, r1
ble clbss_l
ldr pc, _start_armboot
_start_armboot: .word start_armboot
这一段,首先是设置栈,然后是清除bss段,然后是跳转到start_armboot函数去。
从start_armboot开始就是C函数了。因为前面设置好了栈,所以现在可以运行C代码了。
跳转到start_armboot后,会执行一个for循环
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr)
{
if ((*init_fnc_ptr)() != 0)
{
hang ();
}
}
这个for循环会一个一个执行init_fnc_t *init_sequence[]数组里的函数成员,它有下面这些成员:
cpu_init, /* basic cpu dependent setup */
board_init, /* basic board dependent setup */
interrupt_init, /* set up exceptions */
env_init, /* initialize environment */
init_baudrate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_banner, /* say that we are here */
dram_init, /* configure available RAM banks */
display_dram_config,
NULL,
cpu_init函数没有做什么工作。
Board_init是一个关键的函数,因为它设置了关于开发板的一些关键参数。它里面设置了时钟参数
int board_init (void)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();
clk_power->LOCKTIME = 0xFFFFFFFF;
clk_power->CLKDIVN = 0x03; /* Fclk : Hclk : Pclk = 1:2:4 */
clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);
delay (4000);
clk_power->UPLLCON = ((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV);
delay (8000);
/* set up the I/O ports */
gpio->GPACON = 0x007FFFFF;
gpio->GPBCON = 0x00044555;
gpio->GPBUP = 0x000007FF;
gpio->GPCCON = 0xAAAAAAAA;
gpio->GPCUP = 0x0000FFFF;
gpio->GPDCON = 0xAAAAAAAA;
gpio->GPDUP = 0x0000FFFF;
gpio->GPECON = 0xAAAAAAAA;
gpio->GPEUP = 0x0000FFFF;
gpio->GPFCON = 0x000055AA;
gpio->GPFUP = 0x000000FF;
gpio->GPGCON = 0xFF95FFBA;
gpio->GPGUP = 0x0000FFFF;
gpio->GPHCON = 0x002AFAAA;
gpio->GPHUP = 0x000007FF;
gd->bd->bi_arch_number = MACH_TYPE_MINI2440;/* 1999 */
gd->bd->bi_boot_params = 0x30000100;
icache_enable();
dcache_enable();
return 0;
}
这个函数中设置了MPLL、UPLL、机器码,MPLL设置了机器的时钟频率,UPLL设置了USB的时钟频率,机器码(gd->bd->bi_arch_number)传递给linux内核的参数,如果跟linux中的机器码不配,就会出现启动不了系统的现象。在uboot1.2.0中,我们要把这个值设为1999。
执行完board_init函数后,继续执行后面的函数。
执行到serial_init函数后,它会调用get_PLLCLK(int pllreg)
这个函数我们要改一下,因为s3c2410跟s3c2440的PLLCLK计算公式不同。现在它是按s3c2410来写的程序,我们要改一下,按成s3c2440的PLLCLK计算公式。
改好后就应该可以在串口终端看到输出了。这样,接下来的移植就好办多了。我们可以通过串口输出信息找到移植过程中出错的地方。好,后面的C程序我们就不必多说了,自己一步一步分析下来就会差不多。
下面我们来分析一下uboot是如何启动linux内核的。
Uboot中有一个bootm指令,它是用来从ram中启动linux的。
在uboot的common/cmd_bootm.c中有一个函数do_bootm,这就是我们在执行bootm 32000000 uImage时执行的指令。我们来看看它的代码吧。
do_bootm函数中有一个switch(hdr->ih_os)查询语句,它里面有这么一段代码。
case IH_OS_LINUX:
do_bootm_linux (cmdtp, flag, argc, argv, addr, len_ptr, verify);/*启动linux内核*/
break;
因为uImage里的头信息里面包含了linux内核基本信息,其中就有操作系统这个参数,它告诉uboot,这是一个linux内核。所以就会执行这个case语句,启动linux内核。
do_bootm_linux里面有一个theKernel函数,它是一个指针函数,它的地址就linux内核开始的地址,执行到这个函数时,uboot就算结束了。它正式把CPU交给linux内核了。执行theKernel时,它还传递了几个参数
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
bd->bi_arch_number是机器代码,我们的mini2440的机器代码为1999,它应该跟linux内核的机器代码相同。否则的话,就会出现启动不了linux内核。出现如下情况:
Starting kernel ...
Uncompressing Linux.............................................................
...................................... done, booting the kernel.
程序执行到这里就停止了。
bd->bi_boot_params是uboot传递给linux内核的参数。它是开发板相关的一些参数信息。
Uboot的程序我们就分析到这里,这里面的很多函数还是要靠我们自己去分析,在这里我们只能给大家提供一条主线。下章我们将开始分析linux内核的启动代码了,linux内核是一个庞大的系统,我们作为初学者没必要去深入钻研它的全部代码。在这里我们只分析一下linux内核的启动部分。以及需要修改的部分。
第三章 Linux内核程序分析
Linux是一种类unix操作系统,unix是一个古老的操作系统,也是最强大的操作系统,但因为它是一个商业操作系统,它的授权费用很高,所以作为学生的Linus Torvalds就开始设计一个类unix操作系统,并把源代码免费公开,因此在Internet发达的今天,linux发展很快,无数的linux迷在为它提供无偿的开发。
我们可以从http://www.kernel.org网站上下载到Linux的内核,linux内核分为稳定版和开发版,比如2.4.xx为稳定版、2.5.xx为开发版,现在linux内核己经发展到2.6了。我们分析用的内核版本为2.6.22.6。所以我们先从网上把它下载下来。并在虚拟机上把它解压出来。下面我们来开始分析它的代码。
3.1 head.s文件分析
Linux内核是从linux2.6.22.6/arch/arm/kernel/head.s开始的。在linux内核中还有一个head.s文件,就是linux2.6.22.6/arch/arm/boot/compressed/head.s,它是用来完成linux内核自解压的。而真正linux开始是从linux2.6.22.6/arch/arm/kernel/head.s开始。所以我们来分析这个文件。
.section ".text.head", "ax"
.type stext, %function
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE
@ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __create_page_tables
第一条指令是msr指令,它确保cpu处于管理模式、关闭FIQ和IRQ。第二条指令是读取s3c2440的ID。s3c2440的CPU ID 是0x41129200。所以r9的值是0x41129200,然后跳转到__lookup_processor_type函数执行。
.type __lookup_processor_type, %function
__lookup_processor_type:
adr r3, 3f
ldmda r3, {r5 - r7}
sub r3, r3, r7 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
.long __proc_info_begin
.long __proc_info_end
3: .long .
.long __arch_info_begin
.long __arch_info_end
上面程序中执行 ldmda r3, {r5 - r7}后,寄存器r5 = __proc_info_begin、r6 =__proc_info_end,这两个值是在vmlinux.lds.S 中定义的。
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
所以__proc_info_begin和__proc_info_end是.proc.info.init段的开始地址和结束地址。它们中间储存的linux内核所支持的CPU。我们可以在arch/arm/mm/proc-arm920.s文件中找到arm920t 内核的信息。(约在450行)
.section ".proc.info.init", #alloc, #execinstr
.type __arm920_proc_info,#object
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
……
这样,我们再来看就会明白程序它是在干什么了。其中r5 和r6都要减去r3,因为现在还没有启动MMU,而程序在链接时是按照MMU启动时的地址。所以这个地方要做一下减运算。接下来是ldria r5,{r3,r4},这样r3 = 0x41009200,r4 = 0xff00fff0,而r9是前面的CPU ID,r9 = 0x41129200,接下来的指令是and r4, r4, r9 ,r4 和r9作位与运算,0xff00fff0 & 0x41129200,所以r4 = 0x 41009200,与r3相等。所以接下来继续执行就会返回,返回到
bl __lookup_processor_type 处继续执行下去,如果前面的程序中没有找到相应的CPU ID,r5就会被赋值为0,就会执行下面的beq __error_p。
接下来,就是执行bl __lookup_machine_type了,它的代码如下:
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
上面的程序中ldmia r3,{r4,r5,r6},r5 = __arch_info_begin,r6 = __arch_info_end,而在vmlinux.lds.s中有这赋值这两个变量,
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
在arch/arm/mach-s3c2440/mach-smdk2440.c中,有如下一段代码:
MACHINE_START(S3C2440, "MINI2440")
.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_START(S3C2440, "MINI2440")是一个宏定义,它定义在include/asm-arm/mach/arch.h文件中:
#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 \
};
从中我们可以看到里面,.nr = MACH_TYPE_MINI2440,而MACH_TYPE_MINI2440是1999,这就跟我们uboot中传递人机器参数1999相联系了。执行完这些就返回
bl __lookup_machine_type
继续执行。
我们要确保前面这些是没有问题的。如果这部分有错误就会出现执行到
Uncompressing Linux.............................................................
...................................... done, booting the kernel. 就停止了。
3.2 修改时钟频率
因为smdk2410开发板的时钟频率是16.934400MHz,而我们的MINI2440开发板是12MHz的晶振。我们打开arch/arm/mach-s3c2440.c/mach-smdk2440.c文件,里面有
static void __init smdk2440_map_io(void)
{
s3c24xx_init_io(smdk2440_iodesc, ARRAY_SIZE(smdk2440_iodesc));
s3c24xx_init_clocks(12000000);
s3c24xx_init_uarts(smdk2440_uartcfgs, ARRAY_SIZE(smdk2440_uartcfgs));
}
我们要把时钟设为上面的12000000,这样编译出来的内核应该就可以在我们的开发板上面运行了。我们可以从串口输出中看到输出信息。这样对我们的调试很有帮助。
3.3 修改网卡驱动
为什么我们要修改网卡驱动?因为,现在内核是可以启动了,但是还缺少文件系统,我们先把网卡驱动好,就可以利用NFS来使用我们主机上的根文件系统。根据《嵌入式linux应用开发完全手册》修改。
下面我们来修改网卡驱动。
Linux内核的网卡驱动文件是drivers/net/dm9000.c,它即可以编译进内核,也可以编译为一个模块。入口函数是dm9000_init,代码如下:
static int __init
dm9000_init(void)
{
printk(KERN_INFO "%s Ethernet Driver\n", CARDNAME);
return platform_driver_register(&dm9000_driver); /* search board and register */
}
Platform_driver_register()向内核注册平台驱动dm9000_driver。dm9000_driver结构的名称为“dm9000”,如果内核中有相同名称的平台设备,则调用dm9000_probe函数。Dm9000_driver结构如下定义:
static struct platform_driver dm9000_driver =
{
.driver =
{
.name = "dm9000",
.owner = THIS_MODULE,
},
.probe = dm9000_probe,
.remove = dm9000_drv_remove,
.suspend = dm9000_drv_suspend,
.resume = dm9000_drv_resume,
};
所以首先要为dm9000定义一个平台设备的数据结构,然后修改drivers/net/dm9000.c,增加一些开发板相关的代码。
3.3.1 增加DM9000平台设备
增加平台设备的方法在移植串口驱动程序时己经介绍过,过程相似。这需要修改arch/arm/plat-s3c24xx/common-smdk.c文件。
1. 添加要包含的头文件,增加以下代码:
#if defined (CONFIG_DM9000) || defined(CONFIG_DM9000_MODULE)
#include <linux/dm9000.c>
#endif
2. 添加DM9000的平台设备结构,增加以下代码:
#if defined(CONFIG_DM9000) || defined (CONFIG_DM9000_MODULE)
Static struct resource s3c_dm9k_resource[] = {
[0] = {
.start = S3C2410_CS4,
.end = S3C2410_CS4 +3,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = S3C2410_CS4 + 4,
.end = S3C2410_CS4 +4 + 3,
.flags = IORESOURCE_MEM,
},
[2] = {
.start = IRQ_EINT7,
.end = IRQ_EINT7,
.flags = IORESOURCE_IRQ,
}
};
Static struct dm9000_plat_data s3c_dm9k_platdata = {
.flags = DM9000_PLAT_16BITONLY,
}
Static struct platform_device s3c_device_dm9k = {
.name = “dm9000”,
.id = 0,
.num_resources = s3c_dm9k_resource,
.dev = {
.platform_data = &s3c_dm9k_platdata,
}
};
#endif
3. 加入内核设备列表中
把平台设备s3c_device_dm9k加入smdk_devs数组中即可,系统启动时会把这个数组中的设备注册进内核中。增加的代码如下:
static struct platform_device __initdata *smdk_devs[] = {
&s3c_device_nand,
……
&smdk_led7,
#if defined(CONFIG_DM9000) || defined(CONFIG_DM9000_MODULE)
&s3c_device_dm9k,
#endif
……
};
3.3.2 修改drivers/net/dm9000.c文件
1. 添加要包含的头文件,增加以下代码
#if defined (CONFIG_ARCH_S3C2410)
#include<asm/arch-s3c2410/regs-mem.h>
#endif
2. 设置存储控制器使用BANK4可用,设置默认MAC地址(这不是必需的)
static int
dm9000_probe(struct platform_device *pdev)
{
u32 id_val;
#if defined(CONFIG_ARCH_S3C2410)
unsigned int oldval_bwscon;
unsigned int oldval_bankcon4;
#endif
……
PRINTK2("dm9000_probe()");
#if defined(CONFIG_ARCH_S3C2410)
oldval_bwscon = *((volatile unsigned int *) S3C2410_BWSCON);
*((volatile unsigned int *)S3C2410_BWSCON) = (oldval_bwscon & ~(3<<6))|S3C2410_BWSCON_DW4_16 | S3C2410_BWSCON_WS4 | S3C2410_BWSCON_ST4;
oldval_bankcon4 = *((volatile unsigned int *)S3C2410_BANKCON4);
*((volatile unsigned int *)S3C2410_BANKCON4) = 0x1f7c;
#endif
……
if (!is_valid_ether_addr(ndev->dev_addr)){
printk("%s: Invalid ethernet MAC address. Please "
"set using ifconfig\n", ndev->name);
#if defined(CONFIG_ARCH_S3C2410)
printk("Now use the default MAC address: 08:90:90:90:90:90 \n");
ndev->dev_addr[0] = 0x80;
ndev->dev_addr[1] = 0x90;
ndev->dev_addr[2] = 0x90;
ndev->dev_addr[3] = 0x90;
ndev->dev_addr[4] = 0x90;
ndev->dev_addr[5] = 0x90;
#endif
}
……
release:
out:
printk("%s: not found (%d).\n", CARDNAME, ret);
#if defined(CONFIG_ARCH_S3C2410)
*((volatile unsigned int *) S3C2410_BWSCON) = oldval_bwscon;
*((volatile unsigned int *) S3C2410_BANKCON4) = oldval_bankcon4;
#endif
……
}
3. 注册中断时,指定触发方式
在dm9000_open中使用request_irq函数注册中断处理函数,修改它即可。DM9000的中断触发方式为上升沿触发。修改的代码如下:
static int
dm9000_open(struct net_device *dev)
{
board_info_t *db = (board_info_t *) dev->priv;
PRINTK2("entering dm9000_open\n");
#if defined(CONFIG_ARCH_S3C2410)
if (request_irq(dev->irq, &dm9000_interrupt, IRQF_SHARED | IRQF_TRIGGER_RISING,dev->name,dev))
#else
if (request_irq(dev->irq, &dm9000_interrupt, IRQF_SHARED, dev->name, dev))
#endif
return -EAGAIN;
……
}
4. 使用网卡DM9000
使用Make menuconfig命令
Device Drivers --à
Network device support --à
[*] Network device support --à
Ethernet (10 or 100Mbit) --à
<*> DM9000 support
这样再重新编译,make uImage,这时网卡就可以用了。
这样,我们只要做一个文件系统放到主机上就可以在开发板上挂载NFS根文件系统。
第四章 制作根文件系统
第一节 根文件系统简述
Linux文件系统一般都遵循FHS标准(Filesystem Hierarchy Standard,文件系统层次准)。它定义了文件系统中目录、文件分类存放的原则,定义了系统运行所需的最小文件、目录的集合,并列举了不遵循这些原则的例外情况及其原因。FHS不是一个强制的标准,但是大多的Linux、Unix发行版都会遵循FHS。
下面讲述Linux根文件系统各个目录的作用:
1. /bin目录
该目录下存放所有用户(包括系统管理员和一般用户)都可以使用的、基本的命令,这些命令在挂接其它文件系统之前就可以使用,所以/bin目录必须和根文件系统在同一个分区中。
/bin目录下常用的命令有:cat、chgrp、chmod、cp、ls、kill、mount、umount、mkdir、mknod等。
2. /sbin目录
该目录下存放系统命令,即只有管理员能够使用的命令,系统命还可以存放在/usr/sbin、/usr/local/sbin 目录下。/sbin目录中存放的是基本的系统命令,它们是用于启动系统、修复系统等。与/bin目录相似,在挂接其他文件系统之前就可以使用/sbin,所以/sbin目录必须和根文件系统在一个分区。
/sbin目录下常用的命令有:shutdown、reboot、fdisk、fsck等。
3. /dev目录
该目录下存放的是设备文件。设备文件是Linux中特有的文件类型,在Linux系统下,以文件的方式访问和种外设, 即通过读写某个设备文件操作某个具体硬件。比如,通过“/dev/ttySAC0”文件可以操作串口0,通过“/dev/mtdblock1”可以访问MTD设备(NAND Flash、Nor Flash等)的第2个分区。
4. /etc目录
该目录下存放各种配置文件。对于PC机上的Linux系统,/etc目录下目录、文件非常多。这些目录、文件是可选的,它们依懒于系统中所拥有的应用程序,依懒于这些程序是否需要配置文件。在嵌入式系统中,这些内容可以大为精减。
5. /lib目录
该目录下存放共享库和可加载模块(即驱动程序),共享库用于启动系统、运行根文件系统中的目可执行程序,比如/bin、/sbin目录下的程序。其他不是根文件系统所必需的库文件可以存放在其他目录,比如/usr/lib、/usr/X11R6/lib、/var/lib等。
6. /home目录
用户目录,它是可选的。对于每个普通用户,在/home目录下都有一个以用户名命名的子目录,里面存放用户相关的配置文件。
7. /root目录
根用户(用户名为root)的目录,与此对应,普通用户的目录是/home下的某个子目录。
8. /usr目录
/usr目录的内容可以存在另一个分区中,在系统启动后再挂接到根文件系统中的/usr目录下。里面存放的是共享、只读的程序和数据,这表明/usr目录下的内容可以在多个主机间共享,这些主机也是符合FHS标准的,/usr中的文件应该是只读的,其他主机相关、可变的文件应该保存在其他目录下,比如/var。
9. /var目录
与/usr目录相反,/var目录中存放可变的数据,比如spool目录(mail、news、打印机等用的),log文件、临时文件。
10. /proc目录
这是一个空目录,常作为proc文件系统的挂接点。Proc文件系统是个虚拟的文件系统,它没有实际的存储设备,里面的目录、文件都是由内核临时生成的,用来表示系统的运行状态,也可以操作其中的文件控制系统。
系统启动后,使用以下命令挂接proc文件系统(常在/etc/fstab进行设置以自动挂接)。
#mount –t proc none /proc
11. /mnt 目录
用于临时挂接某个文件系统的挂接点,通常是空目录;也可以在里面创建一些空的子目录,比如/mnt/cdram、/mnt/hda1等,用来临时挂接光盘、硬盘。
12. /tmp目录
用于存放临时文件,通常是空目录。一些需要生成的临时文件的程序要用到/tmp目录,所以/tmp目录必须存在并可以访问。
为减少对Flash的操作,当在/tmp目录上挂接内存文件系统时,如下所示:
# mount -t tmpfs none /tmp
第二节 制作根文件系
制作根文件系统就是创建各种目录,并且在里面创建各种文件,我们要用到busybox程序。
Busybox是一个开源软件,Busybox将众多的unix命令集合进一个很小的可执行程序。Busybox中各种命令与相应的GNU工具相比,所能提供的选项较少,但是能够满足一般的应用。Busybox为各种小型系统或嵌入式系统提供了一个比较完全的工具集。
编译和安装Busybox
我们可以从网上免费下载到busybox的代码,网址是http://www.busybox.net/downloads/, 我们下载busybox1.7.0开作为我们的根文件系统。
下载代码后,我们就可以编译了,但是我们需要修改一个地方。我们要修改一下Makefile:
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-
然后,我们就可以编译了,直接make 就行。
接下来就是安装了。我们可以make CONFIG_PREFIX=dir_name install,就可以将busybox安装在dir_name目录下,这样dir_name就是一个根目录了。但是在正式挂接它之前,因为我们还在加一些配置文件,所以我们还要做一点工作。
首先,我们得说一下,我们可以看到在dir_name目录下有很多的文件,包括子目录下面也有很多的文件,确切的说他们是linux系统的指令,但是实际上busybox只有一个可执行文件,就是/bin/busybox这个文件是可执行文件,其它的指令都到/bin/busybox的符号连接。我们用ll指令查看如下:
<此处要插入信息>
1. 安装glibc库
首先我们来看看busybox中用到了哪些库,用如下命令:
Arm-linux-readelf –a “your binary” | grep “Shared”
如:
Arm-linux-readelf –a ./busybox | grep “Shared”
显示busybox依懒 libcrypt.so.1、libm.so.6、libc.so.6等库文件。这些文件都在/usr/local/arm/3.4.1/arm-linux
我们ll libcrypt.so.1 libm.so.6 libc.so.6 :
zhousm@zhousm-desktop:/usr/local/arm/3.4.1/arm-linux/lib$ ll libcrypt.so.1 libm.so.6 libc.so.6
lrwxrwxrwx 1 root root 17 2010-11-26 16:06 libcrypt.so.1 -> libcrypt-2.3.2.so*
lrwxrwxrwx 1 root root 13 2010-11-26 16:06 libc.so.6 -> libc-2.3.2.so*
lrwxrwxrwx 1 root root 13 2010-11-26 16:06 libm.so.6 -> libm-2.3.2.so*
它们都是链接文件。
Libcrypt.so.1是到libcrypt-2.3.2.so的链接,libc.so.6是到libc-2.3.2.so链接,libm.so.6是到libm-2.3.2.so的链接。所以我们要把它们都放到我们的根文件系统。并建立链接。Libcrypt-2.3.2.so、libc-2.3.2.so、libm-2.3.2.so这些文件是每个不同版本的编译器不同的。而libcrypt.so.1、libc.so.6、libm.so.6这些链接文件都是相同的。
2. 构建etc目录
Init进程根据/etc/initab文件来创建其他子进程,比如调用脚本文件配置IP地址、挂接其他文件系统,最后启动shell等。
etc 目录下的内容是取决于要运行的程序,本节只需要创建3个文件:etc/inittab、etc/init.d/rcS、etc/fstab。
etc/inittab内容如下:
# /etc/inittab
::sysinit:/etc/ini.d/rcS
ttySAC0::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount –s –t
etc/init.d/rcS文件
# !/bin/sh
ifconfig eth0 192.168.0.10
mount –a
etc/fstab文件:
#device mount-point type options dump fsck order
proc /porc proc defaults 0 0
cmpfs /tmp tmpfs defaults 0 0
3. 构建dev目录
$ mkdir -p /work/nfs_root/fs_mini/dev
$ cd /work/nfs_root/fs_mini/dev
$ mknod console c 5 1
$ mknod null c 1 3
$ mknod ttySAC0 c 204 64
$ mknod mtdblock0 b 31 0
$ mknod mtdblock1 b 31 1
$ mknod mtdblock2 b 32 2
$ mknod mtdblock3 b 32 3
$ mknod mtdblock4 b 32 4
……
Mtdblock*是nand flash或nor flash的分区表。Mtdblock0表示nand flash或nor flash的第一个分区,mtdblock1表示第二个分区,依此类推。
4. 构建其它目录:
Mkdir proc mnt tmp sys root
到这里,一个基本的根文件系统就差不多建好了。
我们就可以通过NFS 把它挂载到开发板的linux内核上。
第三节 挂载根文件系统
首先要使用NFS挂载根文件系统就要在主机上安装NFS服务器软件。安装步骤略。我们假设NFS服务器安装成功。
这时我们就要在Uboot中设置环境变量。
Setenv bootargs root=/dev/nfs rw nfsroot=192.168.0.3:/mnt/nfs/rootfs ip=192.168.0.10:192.168.0.3:255.255.255.0 console=ttySAC0,115200
nfsroot=192.168.0.3:/mnt/nfs/rootfs表示主机服务器IP地址是192.168.0.3,根目录是/mnt/nfs/rootfs。
ip=192.168.0.10:192.168.0.3:255.255.255.0 表示开发板的ip地址是192.168.0.10,
主机ip是192.168.0.3,子网掩码是255.255.255.0
root=/dev/nfs表示是使用nfs挂载根文件系统。
这样完成后,理论上我们应该是可以从终端看到linux启动起来了。
……
IP-Config: Guessing netmask 255.255.255.0
IP-Config: Gateway not on directly connected network.
Looking up port of RPC 100003/2 on 192.168.0.3
eth0: link up, 100Mbps, full-duplex, lpa 0x45E1
Looking up port of RPC 100005/1 on 192.168.0.3
VFS: Mounted root (nfs filesystem).
Freeing init memory: 132K
init started: BusyBox v1.7.0 (2011-03-01 10:31:59 CST)
starting pid 741, tty '': '/etc/init.d/rcS'
mount: mounting tmpfs on /tmp failed: Invalid argument
Please press Enter to activate this console.
starting pid 744, tty '/dev/ttySAC0': '/bin/sh'
#
# ls
bin etc lib mnt proc sbin usr
dev home linuxrc opt root tmp var
#