uboot完全手册---14

时间:2021-10-28 15:22:03

1. u-boot介绍

本次移植采用的是U-Boot-1.2.0版本。

3. U-Boot源码分析

3.1 源码入口的解释

可能大多数的同学上网查资料后都了解到,stage1阶段的启动代码,主要就在start.s文件里。此start.s也是系统上电后执行的第一个代码。它全部由汇编编写。在讲述start.s之前,我们先来了解一下,系统怎么知道它要先去start.s里执行代码。

我们知道,每个可执行的映像Image,肯定会给编译器一个入口,而且是“有且只有一个全局的入口”。我们可以把这个入口放在flash的0x0地址上,然后让系统去找这个0x0即可。

实际上,我们可以通过编写链接文件(lds)和mk文件来告知编译器这些情况。Lds文件可以决定一个可执行代码的各个段的存储位置、入口地址等,详情请参考附录中的文章《u-boot lds文件详解》。这里来说的Mk文件,是在board/下对应开发板子目录中的mk文件。它指定了TEXT_BASE的地址。

3.2 stage1:启动分析

终于开始u-boot源代码的讲述了!本文讲述的u-boot-1.2.0源码,是经笔者修改的代码。不过,笔者也会将它与完整的源码包进行比较分析。首先是start.s文件,刚才说过了,这个是系统启动后运行的第一个代码,我们详细地分析如下:

3.2.1 中断向量表的设置

.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

.balignl 16,0xdeadbeef

Start.s文件一开始,就定义了_start的全局变量。也即,在别的文件,照样能引用这个_start变量。这段代码验证了我们之前学过的arm体系的理论知识:中断向量表放在从0x0开始的地方。其中,每个异常中断的摆放次序,是事先规定的。比如第一个必须是reset异常,第二个必须是未定义的指令异常等等。

需要注意的是,在这里,我们也可以理解:为何系统一上电,会自动运行代码。因为系统上电后,会从0x0地方取指令,而0x0处放置的是reset标签,直接就跳去reset标签处去启动系统了。

另外,这里使用了ldr指令。而ldr指令中的label,分别用一个.word伪操作来定义。比如:

_undefined_instruction: .word undefined_instruction

我们用source insight跟踪代码后,发现,undefined_instruction在start.s的后面给出了具体的操作,如下:

undefined_instruction:

get_bad_stack

bad_save_user_regs

bl do_undefined_instruction

在跳转到中断服务子程序之前,先有两个宏代码,一个是对stack的操作,一个是用户regs的保存。然后才能跳转如中断服务子程序中执行。请参考《ARM体系结构与编程》等相关书籍,自然能获得详细的答案。

值得一提的是,当发生异常时,都将执行u-boot-1.2.0\cpu\arm920t\ interrupts.c中定义的中断函数。也就是说,start.s中要跳转的这些中断子程序的代码,均在u-boot-1.2.0\cpu\arm920t\ interrupts.c中定义。

3.2.2 U-Boot存储器映射定义

该代码段主要是定义u-boot需要使用的一些映射区的label,比如用户堆区、用户栈区、全局数据结构区等。笔者在下页给出了一个图示,把整个u-boot映射的所有区都列出来了,这个图非常经典,网上找的,大家可以好好研究一把。

_TEXT_BASE:

.word TEXT_BASE

.globl _armboot_start

_armboot_start:

.word _start

/* These are defined in the board-specific linker script. */

.globl _bss_start

_bss_start:

.word __bss_start

.globl _bss_end

_bss_end:

.word _end

#ifdef CONFIG_USE_IRQ

/* IRQ stack memory (calculated at run-time) */

.globl IRQ_STACK_START

IRQ_STACK_START:

.word 0x0badc0de

/* IRQ stack memory (calculated at run-time) */

.globl FIQ_STACK_START

FIQ_STACK_START:

.word 0x0badc0de

#endif

从上图也可以清晰地发现,堆和栈是有区别的。而且可以看到,用户栈区是向下递减的,即地址减少的方向生长。

3.2.3 上电后CPUSVC模式

reset:

/* set the cpu to SVC32 mode */

mrs r0,cpsr

bic r0,r0,#0x1f

orr r0,r0,#0xd3

msr cpsr,r0

这是系统复位后执行的“第一个代码段”(严格来说不是)。CPU复位后,系统会立即被设置成SVC模式。记得之前有网友发帖咨询这个问题,问系统复位后,cpu处于哪个处理器模式。这个代码,就回答了这个问题。

从这个代码中,我们也可以得到一个对寄存器操作的经验:读—修改--写。这里先把cpsr的值读到r0中,清除掉我们想修改的bit位,然后用orr指令来保证其他bit位不被改动,并达到修改寄存器低5位值的目的。最后用msr指令把r0的值给cpsr寄存器达到我们的修改目的。

3.2.4 关闭看门狗

#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)

ldr r0, =pWTCON

mov r1, #0x0

str r1, [r0]

根据S3C2440的datasheet文档,系统启动后,看门狗寄存器是被使能的,所以,如果不在预计的时间内“喂狗”,就有“被狗咬”的可能。别说啥了,赶紧先喂狗。上面这段代码即为喂狗代码。u-boot代码编写者把它放在CPU上电修改SVC模式后的第一个代码,是可以理解的。这个代码,也是修改寄存器的代码,它的思路依旧是:读—修改—写。

实际上,u-boot-1.2.0代码在喂狗代码之前,还有一段代码,如下:

#if defined(CONFIG_S3C2400)

# define pWTCON 0x15300000

# define INTMSK 0x14400008 /* Interupt-Controller base addresses */

# define CLKDIVN 0x14800014 /* clock divisor register */

#elif defined(CONFIG_S3C2410)

# define pWTCON 0x53000000 /* 喂狗寄存器*/

# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */

# define INTSUBMSK 0x4A00001C

# define CLKDIVN 0x4C000014 /* clock divisor register */

#endif

这是定义寄存器用的。比如根据S3C2440的datasheet文档,喂狗寄存器pWTCON的寄存器地址是0x15300000,需要定义后才能使用。同理,这里还定义了时钟除数寄存器CLKDIVN和中断掩码的INTMSK寄存器的地址。在后续代码中会陆续用到。

3.2.5 关掉中断

/*mask all IRQs by setting all bits in the INTMR - default */

mov r1, #0xffffffff

ldr r0, =INTMSK

str r1, [r0]

# if defined(CONFIG_S3C2410)

ldr r1, =0x3ff

ldr r0, =INTSUBMSK

str r1, [r0]

# endif

从注释可以看出此段代码的作用:屏蔽掉所有的irq中断。为了屏蔽这些中断,我们只要把INTMSK的所有的bit位都置1即可。INTMSK寄存器共32bit位,每个bit对应着不同的中断源。事实上,笔者认为这个代码是多余的,只是为了“心里更踏实”而已。因为S3C2440的datasheet文档里明确指出,cpu在复位的时候,这个寄存器的值就是0XFFFFFFFF,以防止发生异常中断。

3.2.6 修改时钟除数寄存器

/* FCLK:HCLK:PCLK = 1:2:4 */

/* default FCLK is 120 MHz ! */

ldr r0, =CLKDIVN

mov r1, #0 /* 原先的值是3 ,现在是1:1:1*/

str r1, [r0]

在u-boot-1.2.0源码中,给CLKDIVN寄存器赋值的是#0x3,表示FCLK:HCLK:PCLK = 1:2:4,这里笔者将其比例改为1:1:1,没啥特殊的目的,调试代码的时候试验用的,后来调试完毕,就没有再修改了。

3.2.7 调用cpu_init_crit

#ifndef CONFIG_SKIP_LOWLEVEL_INIT

bl cpu_init_crit

#endif

此段代码指明:若未定义CONFIG_SKIP_LOWLEVEL_INIT,就执行cpu_init_crit。我们当然不会跳过底层的初始化。因为LOWLEVEL_INIT会对我们的SDRAM进行初始化,这对我们的cpu是必要的。根据source insight的索引,我们转到了cpu_init_crit的代码中:

#ifndef CONFIG_SKIP_LOWLEVEL_INIT

cpu_init_crit:

/* flush v4 I/D caches */

mov r0, #0

mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */

mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */

/*disable MMU stuff and caches */

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

/* before relocating, we have to setup RAM timing  because memory timing is board-dependend, you will find a lowlevel_init.S in your board directory. */

mov ip, lr

bl lowlevel_init

mov lr, ip

mov pc, lr

#endif /* CONFIG_SKIP_LOWLEVEL_INIT */

非常符合我们的思维,我们无效掉了指令cache和数据cache,并禁止MMU与cache。为什么会有这一步呢?笔者曾经深受cache的伤害。在调试代码的时候,下载完修改的bin文件后,如果只按复位键,而不关掉板子重新上电,就会造成cache中可能残留之前对cache操作的数据。我们称之为“脏数据”,它会映像我们的调试结果,造成假象。

当然,在这里无效cache和MMU肯定还有别的原因。比如在初始化阶段,可以认为我们只有一个任务在跑,没有必要,也不允许使用地址变换。因此最好应该无效掉MMU。

由于在cpu_init_cri子程序中又一次调用子程序lowlevel_init,因此,需要事先保护好lr寄存器的内容。当返回时候,再恢复它。在进入lowlevel_init之前,有必要详细说一下mov ip, lr,这个语句的ip。

为了使单独编译的C语言程序和汇编程序之间能相互调用,必须为子程序间的调用规定一定的规则。这就是ATPCS规则。它规定了一些子程序间调用的基本规则。在寄存器的使用规则里,寄存器R12作用子程序间的scratch寄存器,记做ip。mov ip, lr语句的ip由此而来。笔者认为,这里使用别的通用寄存器来代替ip,实现的功能也是一样的。详情请参考《ARM体系结构与编程》第6章 ATPCS介绍。

3.2.8 调用lowlevel_init

这个函数在u-boot-1.2.0\board\smdk2410\lowlevel_init.S文件中。这是对SDRAM的初始化。

_TEXT_BASE:

.word TEXT_BASE

.globl lowlevel_init

lowlevel_init:

/* memory control configuration */

/* make r0 relative the current location so that it */

/* reads SMRDATA out of FLASH rather than memory ! */

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

/* everything is fine now */

mov pc, lr

.ltorg

/* the literal pools origin */

SMRDATA:

.word(0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))

.word((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC))

.word((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC))

.word((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC))

.word((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC))

.word((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC))

.word((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC))

.word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN))

.word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN))

.word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)

.word 0x32

.word 0x30

.word 0x30

该段代码是对SDRAM控制器相关的寄存器赋值,赋值过程中,采用了一个巧妙的做法,把SDRAM控制器初始化需要用到的13个寄存器的值先保存在文字池(literal pools)中,然后通过LDR伪指令以及.ltorg来访问这个文字池,获取寄存器的值赋值到对应的寄存器地址中去。

很多同学对此代码的两个地址不理解:SMRDATA 与_TEXT_BASE。不理解这两个地址相减之后,到底是一个什么值。为什么要相减呢?

其实编译器进行编译,是按照链接文件进行的。也就是说,编译的时候所有的地址都是相对于这个TEXT_BASE计算出来的。而我们的程序是存放在Flash中的,ARM上电后,假设从nandflash模式启动,那么它会把Nandflash的前4K加载到内存中开始运行,当然是从0x0这个地址开始运行,所以要求我们的代码在还没有搬移到TEXT_BASE(0x38f00000)这个位置以前是不能使用这些label的,只能找到一个相对于0x0的地址出来,才能得到真正的数据。而且这时候,我们编译出来的bin文件是存放在0x0000000的,而不是存放在0x38f00000的。嘿嘿,说的有点乱,不知道有没有把笔者的意思表达出来。关于SDRAM初始化。

3.2.9 代码的搬移

#ifndef CONFIG_SKIP_RELOCATE_UBOOT

relocate: /* relocate U-Boot to RAM */

adr r0, _start /* r0 <- current position of code */

ldr r1, _TEXT_BASE /* test if we run from flash or RAM */

cmp r0, r1 /* don't reloc during debug */

beq stack_setup

ldr r2, _armboot_start

ldr r3, _bss_start

sub r2, r3, r2 /* r2 <- size of armboot */

add r2, r0, r2 /* r2 <- source end address */

copy_loop:

ldmia r0!, {r3-r10} /* copy from source address [r0] */

stmia r1!, {r3-r10} /* copy to target address [r1] */

cmp r0, r2 /* until source end addreee [r2] */

ble copy_loop

#endif /* CONFIG_SKIP_RELOCATE_UBOOT */

在SDRAM初始化完毕后,我们开始搬移代码,把代码从原先的0x0开始的位置搬移到内存中的适当的位置继续执行。为啥要搬移代码?原因可能如下:

1、运行速度的考虑。

flash的读写速度远小于SDRAM的读写速度,搬移到SDRAM后,可提高运行效率。

2、空间的考虑。

如果是nandflash启动模式,那么只有4KB的空间供用户使用,实际的代码是永远大于4KB的,因此需要重新开辟空间来进行代码的运行工作。

有些版本的u-boot的代码搬移用C语言来实现:bl CopyCode2Ram,也是可以的。因为这时候,我们完全搭建好了C环境。

在这段代码中,还有一个子程序:beq stack_setup,用来设置栈空间的,我们在下节中讲解。

3.2.10 栈空间的设置

stack_setup:

ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */

sub r0, r0, #CFG_MALLOC_LEN /* malloc area */

sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */

#ifdef CONFIG_USE_IRQ

sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)

#endif

sub sp, r0, #12 /* leave 3 words for abort-stack */

这段代码是用来分配各个栈空间的。包括分配动态内存区,全局数据区,IRQ和FIQ的栈空间等。

3.2.11 BSS段的清零

clear_bss:

ldr r0, _bss_start /* find start of bss segment */

ldr r1, _bss_end /* stop here */

mov r2, #0x00000000 /* clear */

clbss_l:

str r2, [r0] /* clear loop... */

add r0, r0, #4

cmp r0, r1

ble clbss_l

本段代码先设置了BSS段的起始地址与结束地址,然后循环清楚所有的BSS段。至此,所有的cpu初始化工作(stage1阶段)已经全部结束了。后面的代码,将通过ldr pc, _start_armboot,进入C代码执行。这个C入口的函数,是在u-boot-1.1.6\lib_arm\board.c文件中。它标志着后续将全面启动C语言程序,同时它也是整个u-boot的主函数。

3.3 stage2:C代码分析

上节提到,start_armboot函数不仅标志着后续将全面启动C语言程序,同时它也是整个u-boot的主函数。那么该函数完成什么操作呢?

3.3.1 gdbd分配空间

/* Pointer is writable since we allocated a register for it */

gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));

/* compiler optimization barrier needed for GCC >= 3.4 */

__asm__ __volatile__("": : :"memory");

memset ((void*)gd, 0, sizeof (gd_t));

gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));

memset (gd->bd, 0, sizeof (bd_t));

如同使用变量之前,需要声明定义一样,这里使用全局变量gd和bd之前,我们需要先设置它的地址,并用memset函数为它分配合适的空间。u-boot的注释告知我们,gd和bd是一个可写的指针,实际上不过是一个地址而已。

代码中的这句话:__asm__ __volatile__("": : :"memory");目的就是告诉编译器内存被修改过了。更详细的关于C程序中内嵌汇编的文档,请参考附录中的文献《ARM GCC 内嵌(inline)汇编手册》。

3.3.2 执行初始化列表函数

for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {

if ((*init_fnc_ptr)() != 0) {

hang (); }

}

这是一个for语句,却完成了板子初始化列表函数的功能。我们先来看一下for语句的初始值:init_sequence。用source insight跟踪后发现,它是一个指针数组:

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 */

#if defined(CONFIG_DISPLAY_CPUINFO)

print_cpuinfo, /* display cpu info (and speed) */

#endif

#if defined(CONFIG_DISPLAY_BOARDINFO)

checkboard, /* display board info */

#endif

dram_init, /* configure available RAM banks */

display_dram_config,

NULL,

};

指针数组的每个成员都对应着一个函数名(函数指针),指向的是init_fnc_t类型的函数。For语句每次都会判断当前的函数是不是NULL,如果是,则跳出for语句,完成当前的板子初始化列表函数的功能。

可能大家都注意到了一个类型:init_fnc_t,它表示什么意思呢?我们看到了在初始化列表函数之前,有一个新的数据类型,它是个typedef语句:

typedef int (init_fnc_t) (void);

可能有的同学对此不太理解,为啥非得用一个typedef呢?笔者认为,可以不用typedef,但是用了init_fnc_t后,团队中别的成员来看代码的时候,会很轻松地知道,这是一个初始化(init)的函数(fnc),增加了代码的可读性。如果您对typedef用法还不是很理解,那就赶紧咯,复习下typedef的用法。我们在附录C中给出了《typedef用法小结》,附录D中给出了《u-boot中typedef应用解析》,以上两篇文档均摘自互联网资料,可供参考。

现在,我们对每个初始化列表函数,都进行分析,由于代码量太大,我们不一一列出代码,大家可以参考u-boot-1.2.0的源码包。

Cpu_init函数,并没有做实质性的工作,而且我们现在暂时没有定义CONFIG_USE_IRQ,因此,代码执行到这里,直接就return 0;

Board_init函数,是初始化与硬件平台有关的函数。它的工作很明显:时钟的设置,引脚IO口的设置,并且在这里把数据cache和指令cache也打开了。以上工作的完成,标志着板子已经准备好工作了。当然,考虑到可能系统会发生意外中断,所以我们还需要初始化中断,让中断也准备好工作,因此u-boot代码中下一步就开始了中断的初始化。

interrupt_init函数,这实际上是定时器的中断初始化。和我们之前的培训课程相符的是,我们看到了中断初始化中的那几个熟悉的寄存器,首先是两个配置寄存器TCFG0和TCFG1。晕倒,怎么代码中只有TCFG0的设置,没有TCFG1的设置?很明显,TCFG1采用的是默认值。然后配置寄存器的下载值,最后打开启动开关,启动定时器工作。

env_init函数,这是对我们板子的环境做出初始化配置。顺便提一下,我们修改的配置文件里,用的是nand flash来存放环境变量的值。

#define CFG_ENV_IS_IN_NAND 1

#define CFG_ENV_OFFSET 0x40000

#define CFG_ENV_SIZE64 0xc000 /* Total Size of Environment Sector */

#define CFG_ENV_SIZE 0x20000 /* Total Size of Environment Sector */

因此,我们在进入u-boot命令行之后,运行的关于环境变量的操作,只要它被保存,saveenv,肯定是save在nandflash中的某个位置。

init_baudrate函数,初始化波特率。我们心里要很明确,初始化波特率,目的只有一个:让串口打印调试信息。因此,下一个函数,肯定是串口的初始化函数。所以,我们可以在调试的时候,先算好波特率的值,直接赋值给gd->bd->bi_baudrate,注释掉该函数中的其他代码。调试完毕,再恢复出原先的代码。这样,我们可以不用考虑别的因素导致串口打印不出信息。

serial_init函数,串口的初始化函数。这里调用了另一个函数来配置串口寄存器:serial_setbrg();在这个函数中,我们看到了关于串口的5个寄存器的配置。关于每个寄存器的更详细的配置信息,请参考ARM技术交流网推出的串口课程讲解部分。

console_init_f函数,这个函数的功能只有一个,就是指出我们目前是使用串口的,因此有此句:gd->have_console = 1;然后直接返回0。

display_banner函数。OK,现在串口初始化完毕,我们可以打印信息了。这是u-boot代码中第一次打印信息。我们可以在这里加入我们自己的代码,比如笔者移植的u-boot代码中,就加入了如下“欢迎”的代码信息:

printf ("\n\n");

printf("*************************************************\n");

printf("* *\n");

printf("* ARM技术交流网欢迎您! *\n");

printf("* www.arm79.com *\n");

printf("* *\n");

printf("*************************************************\n");

出现打印信息后,可以说,u-boot移植已经成功了一半。有了打印信息,我们可以随时用打印信息来调试。初始化列表函数中,还有几个函数,比较简单,我这里就不说了。随后开始的是一系列外设的初始化。

3.3.3 配置可用的flash区:flash_init

当您跟踪到flash_init函数的时候,您会发现,这里只兼容AMD系列的flash芯片,比如LV400及LV800。如果您的开发板上刚好就是AMD的芯片,那么恭喜,您可能就不需要修改flash ID号了。可惜,笔者用的开发板上用的是EON生产的flash芯片。笔者只好把AMD的所有代码,都改成EON的代码。比如,笔者嫌麻烦,直接补上

#define EN29LV160AB_ID 0x2249001c

再来一个:

#define CONFIG_EON_29LV160AB 1

后面再修改FLASH_BANK_SIZE、CFG_MAX_FLASH_SECT、PHYS_FLASH_1等信息,来配置笔者的板子上可用的flash区域。

3.3.4 初始化内存分配函数

mem_malloc_init函数,这是非常关键的一步,请大家引起注意。我们必须配置好内存的起始地址和结束地址,然后把这块区域清零,以便后续来使用它。

3.3.5 nand flash的初始化

这部分代码,可能隐含是不执行的。如果您想使用它,需要自行打开,然后添加自己的nand flash驱动的代码。笔者自己没有写nand flash的代码,而是直接copy别人的代码,拿过来改一改。如果想验证自己修改或者自己写的nand flash的驱动是否正确,可以试着从nand flash中读取或写入一个数据,并用串口打印出来(笔者修改的nand flash驱动代码,将在ARM技术交流网上公布,需要的可以随时下载)。后面的代码,一直到main_loop函数,我们都不需要修改。main_loop函数是进入命令循环的函数,它接受用户从串口输入的命令,然后执行相应的工作,这也是整个u-boot的工作循环。

注意,它并没有使用中断来触发命令的操作,而是用循环来做这部分的工作:

/* main_loop() can return to retry autoboot, if so just run it again. */

for (;;) {

main_loop ();

}

至此,u-boot代码的分析接近尾声。

4. U-Boot移植过程参考

4.1 移植准备

我们采用的是u-boot-1.2.0版本。

4.2 U-Boot移植过程分析

本章节将详细给出整个u-boot移植的过程,您只需要按照此过程操作,即可轻松地移植,并定制属于您自己的u-boot-1.2.0版本到您的开发板上!

说明:交叉编译工具的制作,请自行完成!事实上,许多开发板厂商都给出了详细的制作过程供用户参考。

4.2.1 修改Makefile文件

我们建议,除非您只是体验一次u-boot,而非研究u-boot。否则,请抽时间浏览一下u-boot根目录下的readme文档。这将对您理解u-boot大有帮助。

请点击您的鼠标,打开makefile文件。如果您是在linux环境下开发,使用vi makefile命令可打开该文件。使用ctrl + F键,查找“smdk2400_config”,找到后,您会看到如下代码:

smdk2400_config : unconfig

@$(MKCONFIG) $(@:_config=) arm arm920t smdk2400 NULL s3c24x0

我们解释一下代码:

arm,就表示现在用的是CPU的架构是arm体系结构。

arm920t,指明这是cpu的内核类型,它对应于cpu/arm920t目录。

Smdk2400,这是开发板的型号,它的目录在board/smdk2400目录下。您也可以自己命名您的开发板。比如:ARM79。

NULL,表示开发者或者经销商是谁(vender)。

S3c24x0,表示开发板上的cpu是啥。对于我们的开发板,当然是S3C2440了。

根据以上的解释,我们可以自己模仿着建立自己的编译项:

arm79_config : unconfig

@$(MKCONFIG) $(@:_config=) arm arm920t arm79 NULL s3c24x0

OK,修改完毕,可以保存、退出makefile。

4.2.2 建立自己的开发板文件

为了使得u-boot具有自己的特征,我们需要在board目录下建立自己的文件:

1、复制board/smdk2410,并更名为board/arm79。

2、复制board/smdk2410/smdk2410.c,并更名为board/arm79/arm79.c

OK,我们的开发板是自己花钱买的,现在开发板上面跑的u-boot,我们也可以假装是自己写的代码了。

4.2.3 建立自己的配置文件

配置文件在:include/configs/smdk2410.h。大家还希望用别人的配置文件吗?当然不想!所以,改过来!复制include/configs/smdk2410.h,并更名为:include/configs/arm79.h。这时候,可以暂时保留arm79.h中的配置信息。一会再来修改它。我们现在有更重要的事情要做。

4.2.4 修改交叉编译工具的路径

交叉编译工具,您可以使用开发板公司为您提供的制作包即可。修改交叉编译工具的路径,请参考每个开发板公司的用户手册。这里无法给出一个定性的答案。一般都是在/etc/profile文件下修改,增加一个.bin目录。

4.2.5 测试编译u-boot-1.2.0版本

其实,u-boot虽然号称经典,但是有些版本在某些特定的arm平台或者powerpc平台是编译不通过的。笔者在实习时候,在公司产品上移植了一个u-boot版本,就是不行的。换成u-boot-1.2.0版本,可以编译通过。因此,笔者本次移植也采用了u-boot-1.2.0版本。

cd u-boot-1.2.0 /* 切换到u-boot目录下 */

make arm79_config

这时候,命令行界面上会显示:Configuring for arm79 board…然后您再敲入make,回车。如果您的交叉编译工具安装正确的话,这时候就开始编译了,大约几分钟后,您就会看到窗口中出现了.bin文件的打印信息,回到您的u-bot根目录下,您就会发现,那里多出了一个u-boot.bin文件。

当然,当您在调试的时候,或许您还想得到u-boot的反汇编代码,那么,请再次打开makefile文件,用ctrl + F键,查找到“u-boot.bin”所在的行,大约在第239行(如果您之前没有在makefile中修改别的信息的话):

ALL = $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND)

这行的代码,是指定编译后,输出啥文件的。可以看到,编译结果,会输出u-boot.srec文件,u-boot.bin文件,system.map文件,等等。这时候,您如果想让它输出u-boot的反汇编文件,只要这样做:

ALL = $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(obj)u-boot.dis $(U_BOOT_NAND)

对比一下,发现我们现在增加了“$(obj)u-boot.dis”。对了,这就是指定编译结果,要输出u-boot反汇编文件。

4.2.6 修改配置文件

之前已经提到,笔者的配置文件已经改为arm79.h,目录在include/configs/arm79.h。由于配置文件修改较多,而且是根据具体开发板进行配置的,因此笔者直接给出了修改完的配置文件,并作出详细的注释,希望对您有所帮助!

#ifndef __CONFIG_H

#define __CONFIG_H

/* High Level Configuration Options (easy to change) */

#define CONFIG_ARM920T 1 /* This is an ARM920T Core */

#define CONFIG_S3C2410 1 /* in a SAMSUNG S3C2410 SoC */

#define CONFIG_SMDK2410 1 /* on a SAMSUNG SMDK2410 Board */ /* input clock of PLL */

#define CONFIG_SYS_CLK_FREQ 12000000 /* 输入时钟12M */

#define USE_920T_MMU 1

#undef CONFIG_USE_IRQ /* 暂时不使用IRQ */

/* Size of malloc() pool */

#define CFG_MALLOC_LEN (CFG_ENV_SIZE + 128*1024)

#define CFG_GBL_DATA_SIZE 128 /* size in bytes reserved for initial data */

/*网卡的配置信息 */

#define CONFIG_DRIVER_DM9000 1

#define CONFIG_DM9000_BASE 0x20000300

#define DM9000_IO CONFIG_DM9000_BASE

#define DM9000_DATA (CONFIG_DM9000_BASE + 4)

#define CONFIG_DM9000_USE_16BIT

/* select serial console configuration */

#define CONFIG_SERIAL1 1 /* 使用串口 */

/****RTC *****/

#define CONFIG_RTC_S3C24X0 1

/* allow to overwrite serial and ethaddr */

#define CONFIG_ENV_OVERWRITE

#define CONFIG_BAUDRATE 38400 /* 波特率使用38400 */

/********* Command definition *********/

#define CONFIG_COMMANDS \

(CONFIG_CMD_DFL | \

CFG_CMD_LOADS | \

CFG_CMD_LOADB | \

CFG_CMD_CACHE | \

CFG_CMD_NAND | \

CFG_CMD_FLASH | \

CFG_CMD_PING | \

/*CFG_CMD_EEPROM |*/ \

/*CFG_CMD_I2C |*/ \

/*CFG_CMD_USB |*/ \

CFG_CMD_REGINFO | \

CFG_CMD_DATE | \

CFG_CMD_ELF)

/* this must be included AFTER the definition of CONFIG_COMMANDS (if any) */

#include <cmd_confdefs.h>

#define CONFIG_BOOTDELAY 3     /* 进入命令行的等待时间3s */

/*#define CONFIG_BOOTARGS "root=ramfs devfs=mount console=ttySA0,9600" */

/*#define CONFIG_ETHADDR 08:00:3e:26:0a:5b */

#define CONFIG_NETMASK 255.255.255.0

#define CONFIG_IPADDR 10.0.0.110

#define CONFIG_SERVERIP 10.0.0.1

/*#define CONFIG_BOOTFILE "elinos-lart" */

/*#define CONFIG_BOOTCOMMAND "tftp; bootm" */

#if (CONFIG_COMMANDS & CFG_CMD_KGDB)

#define CONFIG_KGDB_BAUDRATE 9600 /* speed to run kgdb serial port */

/* what's this ? it's not used anywhere */

#define CONFIG_KGDB_SER_INDEX 1 /* which serial port to use */

#endif

/* Miscellaneous configurable options */

#define CFG_LONGHELP /* undef to save memory */

#define CFG_PROMPT "[arm79-uboot-1.2.0]# " /* Monitor Command Prompt */

#define CFG_CBSIZE 256 /* Console I/O Buffer Size */

#define CFG_PBSIZE (CFG_CBSIZE+sizeof(CFG_PROMPT)+16) /* Print Buffer Size */

#define CFG_MAXARGS 16 /* max number of command args */

#define CFG_BARGSIZE CFG_CBSIZE /* Boot Argument Buffer Size */

#define CFG_MEMTEST_START0x30000000 /* memtest works on */

#define CFG_MEMTEST_END 0x33F00000 /* 63 MB in DRAM */

#undef CFG_CLKS_IN_HZ /* everything, incl board info, in Hz */

#define CFG_LOAD_ADDR 0x33000000 /* default load address */

/* the PWM TImer 4 uses a counter of 15625 for 10 ms, so we need */

/* it to wrap 100 times (total 1562500) to get 1 sec. */

#define CFG_HZ 1562500

/* valid baudrates */

#define CFG_BAUDRATE_TABLE { 9600, 19200, 38400, 57600, 115200 }

/The stack sizes are set up in start.S using the settings below */

#define CONFIG_STACKSIZE (128*1024) /* regular stack */

#ifdef CONFIG_USE_IRQ

#define CONFIG_STACKSIZE_IRQ (4*1024) /* IRQ stack */

#define CONFIG_STACKSIZE_FIQ (4*1024) /* FIQ stack */

#endif

/* Physical Memory Map */

#define CONFIG_NR_DRAM_BANKS 1 /* we have 1 bank of DRAM */

#define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */

#define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */

#define PHYS_FLASH_1 0x00000000 /* Flash Bank #1 */

#define CFG_FLASH_BASE PHYS_FLASH_1

/*FLASH and environment organization */

#if 0

#define CONFIG_AMD_LV400 1 /* uncomment this if you have a LV400 flash */

#define CONFIG_AMD_LV800 1 /* uncomment this if you have a LV800 flash

#endif

#define CONFIG_EON_29LV160AB 1

/*added by www.arm79.con */

#define CFG_MAX_FLASH_BANKS 1 /* max number of memory banks */

#ifdef CONFIG_EON_29LV160AB

#define PHYS_FLASH_SIZE 0x00200000 /* 2MB */

#define CFG_MAX_FLASH_SECT (35) /* max number of sectors on one chip */

#define CFG_ENV_ADDR (CFG_FLASH_BASE + 0x1F0000) /* addr of environment */

#endif

#ifdef CONFIG_AMD_LV800

#define PHYS_FLASH_SIZE 0x00200000 /* 1MB */

#define CFG_MAX_FLASH_SECT (19) /* max number of sectors on one chip */

#define CFG_ENV_ADDR (CFG_FLASH_BASE + 0x1F0000) /* addr of environment */

#endif

#ifdef CONFIG_AMD_LV400

#define PHYS_FLASH_SIZE 0x00080000 /* 512KB */

#define CFG_MAX_FLASH_SECT (11) /* max number of sectors on one chip */

#define CFG_ENV_ADDR (CFG_FLASH_BASE + 0x070000) /* addr of environment */

#endif

/* timeout values are in ticks */

#define CFG_FLASH_ERASE_TOUT (5*CFG_HZ) /* Timeout for Flash Erase */

#define CFG_FLASH_WRITE_TOUT (5*CFG_HZ) /* Timeout for Flash Write */

//#define CFG_ENV_IS_IN_FLASH 1

#define CFG_ENV_IS_IN_NAND 1

#define CFG_ENV_OFFSET 0x40000

#define CFG_ENV_SIZE64 0xc000 /* Total Size of Environment Sector */

#define CFG_ENV_SIZE 0x20000 /* Total Size of Environment Sector */

#define CFG_NAND_BASE 0

#define CFG_MAX_NAND_DEVICE 1

#define NAND_MAX_CHIPS 1

#endif /* __CONFIG_H */

4.2.7 修改start.s文件

这是系统启动运行的第一个文件。大部分代码是不需要修改的,毕竟S3C2410和S3C2440的启动时差别不大的。笔者修改了下时钟:

/* FCLK:HCLK:PCLK = 1:2:4 */

/* default FCLK is 120 MHz ! */

ldr r0, =CLKDIVN

mov r1, #0 /* 原先的值是3 ,现在是1:1:1*/

str r1, [r0]

事实上,没有必要修改这个。笔者也是调试的时候修改的,调试结束,也就没有再改回去。其他地方就不需要修改了:SDRAM初始化部分,代码搬移部分,都可以直接用。

4.2.8 修改board/arm79/arm79.c

这个文件是由原来的board/smdk2410/smdk2410.c来的。笔者修改了这段:

#if FCLK_SPEED==0 /* Fout = 203MHz, Fin = 12MHz for Audio */

#define M_MDIV 0xC3

#define M_PDIV 0x4

#define M_SDIV 0x1

#elif FCLK_SPEED==1 /* Fout = 75MHz */

#define M_MDIV 42 /* 42*/

#define M_PDIV 0x2 /* 0x3 */

#define M_SDIV 0x2

#endif

这段代码修改了MPLL的时钟。它是为了迎合波特率计算公式的设置的。然后在该文件里的board_init函数,笔者把UPLLCON的配置和MPLLCON的配置顺序颠倒下。可能这是2410与2440的区别。S3C2440的datasheet文档中明确规定,必须先初始化UPLLCON,然后延迟一段时间后才能初始化MPLLCON。代码如下:

/* configure UPLL */

clk_power->UPLLCON = ((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV);

/* some delay between MPLL and UPLL */

delay(0xffff);

delay(0xffff);

delay(0xffff);

/* configure MPLL */

clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);

/* some delay between MPLL and UPLL */

delay(0xffff);

delay(0xffff);

delay(0xffff);

另外,笔者修改了该函数里的IO口的初始化配置部分,这是根据笔者开发板上面的硬件结构修改的代码:

/* set up the I/O ports */

gpio->GPACON = 0x007FFFFF;

gpio->GPBCON = 0x00055555;

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 = 0xFF94FFBA;

gpio->GPGUP = 0x0000FFEF;

gpio->GPGDAT = gpio->GPGDAT & (~(1<<4)) | (1<<4) ;

gpio->GPHCON = 0x002AFAAA;

gpio->GPHUP = 0x000007FF;

4.2.9 修改cpu/arm920t/s3c24x0/speed.c

修改该文件,是因为u-boot版本中没有S3C2440对应的版本,只有2410的版本。而2410与2440在计算MPLL的公式上有区别。2440芯片的MPLL计算公式中,多了一个“乘以2”。代码修改的是get_PLLCLK函数:

static ulong get_PLLCLK(int pllreg)

{

S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();

ulong r, m, p, s;

if (pllreg == MPLL)

r = clk_power->MPLLCON;

else if (pllreg == UPLL)

r = clk_power->UPLLCON;

else

hang();

if (pllreg == MPLL)

m = 2*(((r & 0xFF000) >> 12) + 8);

else if (pllreg == UPLL)

m = ((r & 0xFF000) >> 12) + 8;

else

hang();

p = ((r & 0x003F0) >> 4) + 2;

s = r & 0x3;

return((CONFIG_SYS_CLK_FREQ * m) / (p << s));

}

笔者承认,这个代码修改的很不成功。大家可以看到,笔者只是增加了:

if (pllreg == MPLL)

m = 2*(((r & 0xFF000) >> 12) + 8);

而这段代码,根本不具移植性。假设以后出了新的产品,升级版,那么这个代码无法移植,需要重新修改。最好的代码修改思路应该是,在return语句上修改:如果当前是2440的芯片,就return乘以2的时钟;如果是2410芯片,就不乘以2;或者2442的芯片等等。这样,有几个版本的CPU,只要增加这里的代码兼容性即可。

4.2.10 修改board.c文件

由于在修改的时候,还未编写nand flash驱动的代码,所以这时候最好屏蔽掉nand_init函数。本文件中的其他函数不需要修改。

4.2.11 重新编译u-boot

现在,我们可以试一下之前修改的u-boot是否可行。我们执行命令:cd u-boot-1.2.0进入u-boot根目录,然后make一下,执行编译。当生成u-boot.bin文件后,把它用JTAG软件烧到nor flash或者nand flash中,启动开发板,如果之前的修改工作正确的话,就会出现如下界面:

*************************************************

* * ARM技术交流网欢迎您!

* *www.arm79.com

*************************************************

U-Boot 1.2.0 (Dec 2 2009 - 16:51:34)

U-Boot code: 33F80000 -> 33FA0A4C BSS: -> 33FA5DB4

DRAM: 64 MB

Nor Flash: 2 MB

Nand Flash: 256 MiB

In: serial

Out: serial

Err: serial

[arm79-uboot-1.2.0]#

[arm79-uboot-1.2.0]#

说明您的u-boot移植工作基本完成。但是,我们还需要验证一下它是否可以执行我们需要的命令。所以,我们在这里一边介绍u-boot命令,一边演示。

附A、U-Boot的lds文件详解

对于.lds文件,决定一个可执行程序的各个段的存储位置,以及入口地址,这也是链接定位的作用。这里以u-boot的lds为例说明uboot的链接过程。

首先看一下GNU官方网站上对.lds文件形式的完整描述:

SECTIONS {

...

secname start BLOCK(align) (NOLOAD) : AT ( ldadr )

{ contents } >region :phdr =fill

...

}

secname和contents是必须的,前者用来命名这个段,后者用来确定代码中的什么部分放在这个段,以下是对这个描述中的一些关键字的解释。

1、secname:段名

2、contents:决定哪些内容放在本段,可以是整个目标文件,也可以是目标文件中的某段(代码段、数据段等)

3、start:是段的重定位地址,本段连接(运行)的地址,如果代码中有位置无关指令,程序运行时这个段必须放在这个地址上。start可以用任意一种描述地址的符号来描述。

4、AT(ldadr):定义本段存储(加载)的地址,如果不使用这个选项,则加载地址等于运行地址,通过这个选项可以控制各段分别保存于输出文件中不同的位置。

例:

/* nand.lds */

SECTIONS {

firtst 0x00000000 : { head.o init.o }

second 0x30000000 : AT(4096) { main.o }

}

以上,head.o放在0x00000000地址开始处,init.o放在head.o后面,他们的运行地址也是0x00000000,即连接和存储地址相同(没有AT指定);main.o放在4096(0x1000,是AT指定的,存储地址)开始处,但它的运行地址在0x30000000,运行之前需要从0x1000(加载地址处)复制到0x30000000(运行地址处),此过程也就需要读取 flash,把程序拷贝到相应位置才能运行。这就是存储地址和运行地址的不同,称为加载时域和运行时域,可以在.lds连接脚本文件中分别指定。

ARM 技术交流网 版权所有 请勿用于商业用途 违者必究 45帮助客户成功!

编写好的.lds文件,在用arm-linux-ld连接命令时带-Tfilename来调用执行,如

arm-linux-ld –Tnand.lds x.o y.o –o xy.o。也用-Ttext参数直接指定连接地址,如

arm-linux-ld –Ttext 0x30000000 x.o y.o –o xy.o。

既然程序有了两种地址,就涉及到一些跳转指令的区别。

ARM汇编中,常有两种跳转方法:b跳转指令、ldr指令向PC赋值。

要特别注意这两条指令的意思:

(1)b step:b跳转指令是相对跳转,依赖当前PC的值,偏移量是通过该指令本身的bit[23:0]算出来的,这使得使用b指令的程序不依赖于要跳到的代码的位置,只看指令本身。

(2)ldr pc, =step :该指令是一个伪指令编译后会生成以下代码:

ldr pc, 0x30008000

<0x30008000>

step

是从内存中的某个位置(step)读出数据并赋给PC,同样依赖当前PC的值,但是偏移量是step的连接地址(运行时的地址),所以可以用它实现从Flash到RAM的程序跳转。

(3) 此外,有必要回味一下adr伪指令,U-boot中那段relocate代码就是通过adr实现当前程序是在RAM中还是flash中:

relocate: /* 把U-Boot重新定位到RAM */

adr r0, _start /* r0是代码的当前位置 */

/* adr伪指令,汇编器自动通过当前PC的值算出这条指令中“_start"的值,执行到_start时PC的值放到r0中:

当此段在flash中执行时r0 = _start = 0;当此段在RAM中执行时_start = _TEXT_BASE(在board/smdk2410/config.mk中指定的值为0x33F80000,即u-boot在把代码拷贝到RAM中去执行的代码段的开始) */

ldr r1, _TEXT_BASE /* 测试判断是从Flash启动,还是RAM */

/* 此句执行的结果r1始终是0x33FF80000,因为此值是链接指定的 */

cmp r0, r1 /* 比较r0和r1,调试的时候不要执行重定位 */

结合u-boot.lds谈谈连接脚本

OUTPUT_FORMAT("elf32&shy;littlearm", "elf32&shy;littlearm", "elf32&shy;littlearm")

;指定输出可执行文件是elf格式,32位ARM指令,小端

OUTPUT_ARCH(arm)

;指定输出可执行文件的平台为ARM

ENTRY(_start)

;指定输出可执行文件的起始代码段为_start.

SECTIONS

{

. = 0x00000000 ; 定位当前地址为0地址

. = ALIGN(4) ; 代码以4字节对齐

ARM 技术交流网 版权所有 请勿用于商业用途 违者必究 46帮助客户成功!

.text : ; 指定代码段

{

cpu/arm920t/start.o (.text) ; 代码的第一个代码部分

*(.text) ; 其它代码部分

}

. = ALIGN(4)

.rodata : { *(.rodata) } ; 指定只读数据段

. = ALIGN(4);

.data : { *(.data) } ; 指定读/写数据段

. = ALIGN(4);

.got : { *(.got) } ; 指定got段, got段式是uboot自定义的一个段, 非标准段

__u_boot_cmd_start = . ; 把__u_boot_cmd_start赋值为当前位置, 即起始位置

.u_boot_cmd : { *(.u_boot_cmd) } ;指定u_boot_cmd段, uboot把所有的uboot命令放在该段.

__u_boot_cmd_end = . ;把__u_boot_cmd_end赋值为当前位置,即结束位置

. = ALIGN(4);

__bss_start = . ; 把__bss_start赋值为当前位置,即bss段的开始位置

.bss : { *(.bss) } ; 指定bss段

_end = . ; 把_end赋值为当前位置,即bss段的结束位置

}

E: Ping命令使用的ARP协议

如果您详细查看u-boot代码的net.c文件,您会发现,里面的ping命令,使用了arp协议。所以我们简单地来介绍下arp协议。如需深入研究,请查看网络协议IEEE 802.3的官方文档。

ARP协议原理简述

ARP协议(Address Resolution Protocol 地址解析协议),在局域网中,网络中实际传输的是“帧”,帧里面有目标主机的MAC地址。在以太网中,一个注意要和另一个主机进行直接通信,必须要知道目标主机的MAC地址。这个MAC地址就是标识我们的网卡芯片唯一性的地址。但这个目标MAC地址是如何获得的呢?这就用到了我们这里讲到的地址解析协议。所有“地址解析”,就是主机在发送帧前将目标IP地址转换成MAC地址的过程。ARP协议的基本功能就是通过目标设备的IP地址,查询目标设备的MAC地址,以保证通信的顺利进行。所以在第一次通信前,我们知道目标机的IP地址,想要获知目标机的MAC地址,就要发送ARP报文(即ARP数据包)。它的传输过程简单的说就是:我知道目标机的IP地址,那么我就向网络中所有的机器发送一个ARP请求,请求中有目标机的IP地址,请求的意思是目标机要是收到了此请求,就把你的MAC地址告诉我。如果目标机不存在,那么此请求自然不会有人回应。若目标机接收到了此请求,它就会发送一个ARP应答,这个应答是明确发给请求者的,应答中有MAC地址。我接到了这个应答,我就知道了目标机的MAC地址,就可以进行以后的通信了。因为每次通信都要用到MAC地址。

ARP报文被封装在以太网帧头部中传输,如图为ARP请求报文的头部格式。

注意,以太网的传输存储是“大端格式”,即先发送高字节后发送低字节。例如,两个字节的数据,先发送高8位后发送低8位。所以接收数据的时候要注意存储顺序。

整个报文分成两部分,以太网首部和ARP请求/应答。下面挑重点讲述。

“以太网目的地址”字段:若是发送ARP请求,应填写广播类型的MAC地址FF-FF-FF-FF-FF-FF,意思是让网络上的所有机器接收到;

“帧类型”字段:填写08-06表示次报文是ARP协议;

“硬件类型”字段:填写00-01表示以太网地址,即MAC地址;

“协议类型”字段:填写08-00表示IP,即通过IP地址查询MAC地址;

“硬件地址长度”字段:MAC地址长度为6(以字节为单位);

“协议地址长度”字段:IP地址长度为4(以字节为单位);

“操作类型”字段:ARP数据包类型,0表示ARP请求,1表示ARP应答;

“目的以太网地址”字段:若是发送ARP请求,这里是需要目标机填充的。