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 上电后CPU为SVC模式
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 为gd与bd分配空间
/* 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­littlearm", "elf32­littlearm", "elf32­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请求,这里是需要目标机填充的。