针对i.MX31自创bootload
关键字:MCIMX31 U-boot ARM bootload 参考资料:<<MCIMX31 and MCIMX31L Applications Processors Reference Manual>>,<<U-boot source>> 开发平台:成都莱得的IMX31开发板http://www.nidetech.com/service-mx31module.htm
小时候总觉得写个操作系统是件很酷的事情。也见过很多牛人动不动就自己编写一个操作系统。可惜本人资质愚笨,做事没有计划没有恒心,不小心光阴似箭,转眼要到“而立”之年。写不出操作系统,那就写个Bootload吧。 自从接触了U-boot之后发现,写个Bootload不是很难的事。
我们多么希望,写bootload就与写一般的C程序一样。就像这样: #include <stdio.h>
int main() { printf("hello\n"); }
然后 [sjl@sjl gcc]$ arm-linux-gcc -o boot.bin a.c 生成的boot.bin文件写到启动设备(比如flash),然后一开电。调试串口就输出"hello"。
这样可以吗?
让我们先来看看一个ELF的可执行程序大致是怎么加载然后运行的。
1、装载程序到指定位置
1.1 “位置无关”与“位置相关”指令
先来看看我们刚才的boot.bin $ arm-linux-objdump -D boot.bin .... 00008490 <main>: 8490: e1a0c00d mov ip, sp 8494: e92dd800 stmdb sp!, {fp, ip, lr, pc} 8498: e24cb004 sub fp, ip, #4 ; 0x4 849c: e59f0008 ldr r0, [pc, #8] ; 84ac <.text+0x110> 84a0: ebffffb7 bl 8384 <.text-0x18> 84a4: e1a00003 mov r0, r3 84a8: e89da800 ldmia sp, {fp, sp, pc} 84ac: 000085b0 streqh r8, [r0], -r0 .... 000085ac <_IO_stdin_used>: 85ac: 00020001 andeq r0, r2, r1 85b0: 6c6c6568 cfstr64vs mvdx6, [ip], #-416 85b4: 00000a6f andeq r0, r0, pc, ror #20 ....
注意这两句代码: 849c: e59f0008 ldr r0, [pc, #8] ; 84ac <.text+0x110> 84a0: ebffffb7 bl 8384 <.text-0x18>
r0=0x85b0,然后r0被当成printf的第一个参数也就是"hello"的地址,传给printf.我们看0x85b0的位置的内容是: 85b0: 6c6c6568 cfstr64vs mvdx6, [ip], #-416 85b4: 00000a6f andeq r0, r0, pc, ror #20 正好是"hello\n"的ASCII编码"68 65 6c 6c 6f 0a"。
再回头看看刚才分析的那几条指令: 849c: e59f0008 ldr r0, [pc, #8] ; 84ac <.text+0x110> 我们知道在ARM里面pc值是当前执行指令的下两条指令的地址也就是说当指令执行到0x849c的时候pc=0x84a4,所以这条指令执行之后r0=[0x84a4+8],r0=0x85b0. 这条指令叫“与位置无关”指令,指令里面的值是根据pc的值计算得出的,也就是说不论我们的程序被装载到哪个位置,这条指令的结果都可以让r0=0x85b0.
再往下r0是printf的第一个参数,也就是字符串"hello\n"的内存地址等于0x85b0.这个时候0x85b0是个内存的地址值,如果程序装载的位置与我们链接时确定的位置不同将无法取得正确的"hello\n".
1.2 目标文件的链接地址与目标文件的定位
好了,谈到这,我们清楚了一件事情,一个程序要能正确执行,需要被装载到一个正确的位置。那这个正确的位置是什么时候确定的,记录在那个地方?简单的讲是在程序链接的时候确定,在ELF头(Program Headers)里面记录的。内核在装载ELF程序的时候根据ELF头的记录来确定程序的虚拟地址,大致流程是load_elf_binary()->elf_map()->do_mmap().
1.3 bootload中目标文件的链接地址确定于目标文件的定位
从上面我们知道Linux根据ELF头里面记录的信息将程序映射到一个指定的地址。而在bootload里面这些工作是要我们自己来做的。 不同的CPU RAM所在的地址不同有些CPU RAM地址从0x00开始,有些从0x80000000开始,有些还可以重新定位到其他地方,还有些。 bootload最终是要在RAM里面运行的。我们在生成目标文件的时候需要根据不同的CPU平台来指定这个地址。 gcc可以通过-T参数和链接脚本文件来指定各个段的起始地址和段的分配排列。
指定段的起始地址: [sjl@sjl gcc]$ arm-linux-gcc -Ttext=0x10000 a.c [sjl@sjl gcc]$ arm-linux-objdump -d a.out
a.out: file format elf32-littlearm
Disassembly of section .text:
00010000 <_start>: 10000: e59fc024 ldr ip, [pc, #36] ; 1002c <_start+0x2c> 10004: e3a0b000 mov fp, #0 ; 0x0 10008: e49d1004 ldr r1, [sp], #4
U-boot里面链接脚本的例子: $ cat u-boot.lds
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000;
. = ALIGN(4); .text : { CPU/mx31/start.o (.text) *(.text) }
. = ALIGN(4); .rodata : { *(.rodata) }
. = ALIGN(4); .data : { *(.data) }
. = ALIGN(4); .got : { *(.got) }
. = .; __u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .;
. = ALIGN(4); __bss_start = .; .bss : { *(.bss) } _end = .; }
链接参数和脚本只是让我们生成的代码能够引用正确的地址(这个往往是落在ram地址区域里面)。但我们知道,bootload是写在flash等设备上面的,不是直接放在ram里面的。所以bootload需要自己把自己搬运到目的地址里面去。U-boot里面一般是这样实现的:
.globl _start _start: b reset .....
_TEXT_BASE: .word TEXT_BASE ..... .globl _armboot_start _armboot_start: .word _start .....
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
注意上面这段代码是“与位置无关”的,所以不管在flash还是在ram里面都是可以正确运行的。结合上面的u-boot.lds看。text段是在目标文件的最开始,_start是text的第一个符号。因为bss段在目标文件是不占空间的,所以_bss_start是目标文件的最后位置。所以通过这两个值可以计算目标文件的大小。
2、栈指针初始化 我想没有那个C程序是不用栈的吧。栈的重要性就不用多说了。
2.1 Linux在加载应用程序的时候,内核负责初始化栈指针,大致流程可以参考内核实现: load_elf_binary()->create_elf_tables()
2.2 bootload里面这种事情当然是我们自己来做了。看看U-boot里面的做法。
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 */ sub sp, r0, #12 /* leave 3 words for abort-stack */
在ARM里面可以让栈向上(高地址)生长,也可以让栈向下(低地址)生长。我们还是习惯于让栈向下生长。看看上面的代码,所谓初始化栈指针就是初始化SP的值。U-boot里面让栈在TEXT段的下面,当然你可以把你的栈放在任何合适的地方,不就是初始化SP的值吗。
3、 BSS段的初始化
在C里面没有初始化的全局变量静态变量是放在BSS段里面的,他们的初始值为0。BSS是个特殊的段,他在目标文件里面的长度是0,但目标文件加载之后他是有长度的。来看看一个例子:
[sjl@sjl gcc]$ cat a.c #include <stdio.h>
int global_a; int global_b = 0x1; int main() { static int static_a; static int static_b = 0x2; } [sjl@sjl gcc]$ readelf -S a.out There are 33 section headers, starting at offset 0xe34:
Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [21] .data PROGBITS 00010680 000680 000014 00 WA 0 0 4 [22] .bss NOBITS 00010694 000694 00000c 00 WA 0 0 4 [23] .comment PROGBITS 00000000 000694 00007e 00 0 0 1 ...
看好了bss的Size是0xc但是他的Type是NOBITS,bss段在目标文件偏移0x694开始,但你看.comment也是从目标文件的0x694开始。想想也可以理解,既然内容全是0就不用再记录一次了,目标文件加载的时候根据bss段的描述,创建一个初始值全0的段就可以了。
3.1 Linux的做法
看来bss段初始值是0是要在加载的时候做工作的。Linux在加载应用程序的时候: load_elf_binary()->clear_user(elf_bss...);
3.2 U-boot的实现 估计我们现在也可以想到怎么做,但还是看看U-boot里面的具体实现吧:
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 bne clbss_l 结合上面提到的u-boot.lds这段代码应该很好理解了。
4、执行程序 程序地址空间分配好了,定位也完成了,该初始化的也都初始化好了,万事俱备只欠东风了,该执行程序了。执行其实无非是给PC寄存器赋值罢了。
4.1 Linux执行 Linux支持多任务,每个任务都有task_struct来记录。每次进程切换的时候,需要把CPU寄存器的现场保存到一个叫pt_regs的结构里面去,下次进程切换回去的时候,将pt_regs里面的内容恢复到CPU,进程就接着上次的地方开始。 好了,既然我们的程序刚加载完成,要从ELF指定的入口地址开始执行,那就让PC指向"入口"吧。看看Linux的实现: load_elf_binary()->start_thread(regs, elf_entry, bprm->p); 直接将pt_regs里面记录的PC变成elf_entry。下次任务调度的时候,我们的程序就开始运行了。
4.2 Bootload的加载 相比应用程序,我们的Bootload是CPU上电自动加载的,所以这个加载过程不需要我们编程控制。
5、变量
估计没有人的C程序里面不用变量,变量当然要放在RAM里面。我们的应用程序总是有很大的地址空间,有操作系统的帮忙,我们很少关心这些地址空间是如何映射到RAM里面去的,我们更少关心RAM的细节,一句话那都是内存。
但到了bootload里面,我们发现不同的RAM(这里的RAM是指SDRAM或者DDR)会有很大的不同。不同的RAM芯片有不同的参数,地址线的多少,芯片的密度,访问的时序等等。在我们做应用程序的时候,从来不用关心像SDRAM这种东西是要被初始化之后才能用的。
不同的CPU SDRAM控制器具体的初始化过程不同,不同的SDRAM芯片,初始化的参数不同。但总的来说,还是有一个规范的流程。这里我们以莱特的IMXC31平台来作例子。莱特的这款MXC31平台用了两片MT46H32M16LFCK-75组成总共128M的SDRAM。这部分代码需要参考<<IMXC31 Reference Manual>>,和Micron这款芯片的DataSheet.
MT46H32M16LFCK-75的关系参数: Banks:4 Row address:A0–A12 Column address:A0–A9 CL:3 Clock Rate:133M Data-Out Window:2.4ns Access Time:6.0ns DQS-DQ Skew:+0.6ns <<IMX31 Reference Manual>>和Micron 的Datasheet里面关于SDRAM的初始化的流程,SDRAM的访问时序,SDRAM与CPU的硬件连接有很详细的叙述,有兴趣的可以去了解。我们先来看看U-boot里面是怎么做的:
#define SDRAM_FULL_PAGE_BIT 0x100 #define SDRAM_FULL_PAGE_MODE 0x37 #define SDRAM_BURST_MODE 0x33
....
.macro setup_sdram, name, bus_width, mode, full_page /* It sets the "Z" flag in the CPSR at the end of the macro */ ldr r0, =ESDCTL_BASE ldr r1, =0x00 str r1, [r0] /* Disable CSD0 and CSD1*/ str r1, [r0, #0x8] ldr r2, =SDRAM_BASE_ADDR ldr r1, =0x0075E73A str r1, [r0, #0x4] ldr r1, =0x2 // reset str r1, [r0, #0x10] ldr r1, SDRAM_PARAM1_\mode str r1, [r0, #0x10] // Hold for more than 200ns ldr r1, =0x10000 1: subs r1, r1, #0x1 bne 1b
ldr r1, =0x92100000/*Precharge command.*/ str r1, [r0] ldr r1, =0x0 ldr r12, SDRAM_PARAM2_\mode str r1, [r12] ldr r1, =0xA2100000/*Auto-refresh Command.*/ str r1, [r0] ldr r1, =0x0 str r1, [r2] ldr r1, =0xB2100000/*Load Mode Register command.*/ str r1, [r0]
ldr r1, =0x0 .if \full_page strb r1, [r2, #SDRAM_FULL_PAGE_MODE] .else strb r1, [r2, #SDRAM_BURST_MODE] .endif
ldr r1, =0xFF ldr r12, =0x81000000 strb r1, [r12] ldr r3, =0x82116080 /*Normal Read/Write.*/ ldr r4, SDRAM_PARAM3_\mode add r3, r3, r4 ldr r4, SDRAM_PARAM4_\bus_width add r3, r3, r4 .if \full_page add r3, r3, #0x100 /* Force to full page mode */ .endif
str r3, [r0] ldr r1, =0xDEADBEEF str r1, [r2] /* Below only for DDR */ ldr r1, [r0, #0x10] ands r1, r1, #0x4 ldrne r1, =0x0000000C strne r1, [r0, #0x10] /* Testing if it is truly DDR */ ldr r1, =SDRAM_COMPARE_CONST1 ldr r0, =SDRAM_BASE_ADDR str r1, [r0] ldr r2, =SDRAM_COMPARE_CONST2 str r2, [r0, #0x4] ldr r2, [r0] cmp r1, r2 .endm .....
init_sdram_start: setup_sdram ddr X32 DDR 0
SDRAM的初始化流程在Micron的datasheet里面有很详细的叙述,有兴趣的可以自己去翻一下(很多时候仔细研究一下这些内容是很有帮助的)。大致步骤是这样的: 1、等待200us 2、发送PRECHARGE ALL 命令 3、NOP 4、发AUTO REFRESH命令 5、通过LOAD MODE REGISTER命令设置SDRAM芯片上参数 6、NOP 7、进入正常的读写模式
个人感觉SDRAM是所有硬件初始化里面最重要的部分,多了解一些有好处,还是建议多看看MXC31的手册和SDRAM芯片的datasheet。
6、其他
应该没有人会告诉我没有用过printf。但这是个C的库函数我们没法在bootload里面使用。我们需要在我们的bootload里面重新实现一个printf。 但都是printf为什么不能用呢? 想想,先考虑其他的内容,C库里面的printf最终是调用write将内容输出到标准输出,而write是一个系统调用,是通过一个中断从用户空间切换到内核空间由内核来完成剩下的功能。而我们的bootload里面是没有这样的机制,所以像printf这样的函数是无法在bootload里面使用的。我们需要实现自己的printf。
都说到这里了,让我们想想类似于strcpy这样的函数可以用吗?因为我们看到在u-boot里面strcpy也是需要自己实现的。
来看看这个例子:
[sjl@sjl gcc]$ cat a.c #include <stdio.h> int main() { char * des = (char *) 0x80208000; strcpy(des, "hello world\n"); } [sjl@sjl gcc]$ arm-linux-gcc -c a.c [sjl@sjl gcc]$ arm-linux-ld -emain -o vmlinux a.o -Bstatic -lc -Ttext 0x80008000 [sjl@sjl gcc]$ arm-linux-objcopy -O binary vmlinux v.bin
在U-boot里面下载并运行v.bin
$ tftp 0x80008000 v.bin $ md 0x80208000 80208000: ea000012 e59ff014 e59ff014 e59ff014 ................ 80208010: e59ff014 e59ff014 e59ff014 e59ff014 ................
$ go 0x80008000 ## Starting application at 0x80008000 ... ## Application terminated, rc = 0x0 $ md 0x80208000 80208000: 6c6c6568 6f77206f 0a646c72 e59ff000 hello world..... 80208010: e59ff014 e59ff014 e59ff014 e59ff014 ................
好像strcpy是可以用的。看来不是所有的C库函数都不能用,要具体情况具体分析。
7、小结
罗马不是一天建成的。 #include <stdio.h> int main() { printf("hello\n"); } 我想,如果你能够让上面的这个hello.c,能够在没有操作系统的帮助下正确运行,那离编写一个切实可用的bootload就不远了。 上面说了一大堆,总结下来要运行这个hello.c要以下几个步骤: 初始化硬件(CPU,SDRAM,串口) 定位hello到链接指定的位置 初始化bss段 初始化栈指针 实现printf等函数 执行main 那就开始干吧。
8、bootload版本的hello.c
软件可以到http://www.nidetech.com/download/boothello.rar下载,大致说一下几个文件作用。
[sjl@sjl boothello]$ ls -l total 52 -rw-rw-r-- 1 sjl sjl 731 2009-07-31 09:53 board.c -rw-rw-r-- 1 sjl sjl 112 2009-07-31 09:55 config.h -rw-rw-r-- 1 sjl sjl 354 2009-07-31 10:00 config.mk drwxrwxr-x 2 sjl sjl 4096 2009-07-31 10:02 drivers drwxrwxr-x 5 sjl sjl 4096 2009-07-31 09:56 include -rw-rw-r-- 1 sjl sjl 1906 2009-07-31 08:14 init.S drwxrwxr-x 2 sjl sjl 4096 2009-07-31 10:02 lib -rw-rw-r-- 1 sjl sjl 9448 2009-07-31 09:57 lowlevel.S -rw-rw-r-- 1 sjl sjl 76 2009-07-31 08:27 main.c -rw-rw-r-- 1 sjl sjl 891 2009-07-31 08:10 Makefile -rw-rw-r-- 1 sjl sjl 342 2009-02-24 15:55 vmlinux.lds
Makefile,config.mk 基本上没什么好说的,参考了u-boot的做法。 board.c lowlevel.S是与板子相关的初始化内容。虽然都是基于mx31的板子,但选择不同的外围器件,初始化参数会有不同。比如不同的SDRAM芯片的参数,不同的SDRAM容量,不同的GPIO使用等等。 main.c 里面实现了我们的main函数。 vmlinux.lds是链接脚本文件。指定了段的分布。 [sjl@sjl boothello]$ ls drivers/ Makefile serial.c drivers里面只有一个串口的“驱动”。要在调试串口输出当然要有“驱动”了。 [sjl@sjl boothello]$ ls lib Makefile string.c stubs.c vsprintf.c lib目录里面实现了printf,strlen等C的库函数。基本上都是从U-boot里面抄的。
下载软件包直接make,生成的image.bin可以直接烧到flash里面运行。你的调试串口(ttyS3)就会输出hello,当然你可以修改config.h来指定不同的调试串口。 顺便说明一下,因为MX31内部有SRAM,所以这个程序可以不初始化SDRAM,不使用SDRAM。也就是说不用焊接SDRAM也可以在MX31的板子上跑程序哦。
[sjl@sjl boothello]$ make ... arm-linux-objcopy -O binary vmlinux image.bin [sjl@sjl boothello]$ ls image.bin -l -rwxrwxr-x 1 sjl sjl 4092 2009-07-31 10:13 image.bin
9、下一步
bootload版的hello,虽然能够说明bootload需要做的初始化动作,但毕竟离正真的bootload还是有距离。比如要操作flash,下载程序,加载操作系统,还有的甚至有网络功能,文件系统功能。这需要一点点往上面添加。还是那句话"罗马不是一天建成的"。 期待下一篇文章<<操作NOR Flash>>。 成都来的科技有限公司开发中心--软件工程师开发心得 成都莱得科技有限公司--是一家专注于嵌入式领域,为客户提供定制服务的高科技公司。根据客户的需求,为客户提供一站式服务:总体解决方案,嵌入式平台硬件设计,底层驱动设计(BSP),应用软件设计,产品生产。 专注于freescale,intel,marvell的嵌入式ARM的开发。
该贴由system转至本版2014-9-9 23:20:24