自从自己买了一块OK6410的开发板,一直想从裸机程序开始,但由于资金问题,而且觉得JLink太贵不划算,因此没有真正的弄过裸机程序,我的裸机程序开发是应用uboot来下载和启动的。这个方法省掉了一部分钱,但是有时也会出现难以避免的问题,例如串口程序就无法进行。开发裸机程序,我本人觉得:要从底层一步一步的自己写,自己分析,这才是裸机,我实在受不了Windows下的集成开发环境,当然,如果你要赶工期,那么Windows下是最好的,不过对于我们嵌入式爱好者和开发者,在Linux下开发无疑是最好的选择。下面便是记录我学习Linux下的裸机开发,同样目的只是记录自己所做的和给新手一个指导。

一、 裸机程序的构成

     1. 基本的裸机程序由启动代码和C函数文件构成。而启动代码包括:硬件设备初始化、调用C函数。

本次分析中代码文件有:

start.S              启动代码,都是汇编写的

commom.h   一些通用的函数,比如设置某寄存器的某位为1或0

irq.c               中断初始化,中断处理等

regs.h            6410的寄存器地址,需要哪些寄存器可以在本文件中声明和定义

sdram.c          有关sdram的一些操作,如sdram初始化等

time.c             系统时钟的有关设置,如PLLclock等

led.c                这个就是主函数了,主程序就在这里编写,本次只是演示,控制开发板的led灯循环点亮,也就是流水灯

led.lds             该文件为链接脚本,描述了各个输入文件的各个section如何映射到输出文件的各section中,并控制输出文件中section和符号的内存布局。

Makefile         这个文件就不用说了吧。。。

     

1.1 学习启动代码有助于我们以后开发uboot,uboot的启动代码跟裸机的差不多。

        下面把start.S代码贴出来,其中代码中也有注释。

@**************************************
@ File: start.S
@ Function: cpu initial and jump to c program
@ author: lixiaoming
@ time: 2012/7/27 21:40
@**************************************

.extern main
.text
.global _start
_start:
	b	reset			@ when reset, cpu jump to 0 address
	b	halt			@ldr	pc, _undefined_instruction
	b	halt			@ldr	pc, _software_interrupt
	b	halt			@ldr	pc, _prefetch_abort
	b	halt			@ldr	pc, _data_abort
	b	halt			@ldr	pc, _not_used
	ldr	pc, _irq
	b	halt			@ldr	pc, _fiq

_irq:
	.word vector_irq

vector_irq:
	ldr	sp, = 0x54000000	@ save location
	sub	lr, lr, #4
	stmdb	sp!, {r0-r12, lr}	

	bl	do_irq			@ deal with exception
	
	@ backing out
	ldmia	sp!, {r0-r12, pc}^

reset:
	ldr	r0, = 0x70000000	@ Peripheral port base address
	orr	r0, r0, #0x13
	mcr	p15,0,r0,c15,c2,4	@ 256M

	ldr	r0, = 0x7e004000	@ watchdog register address
	mov	r1, #0x0
	str	r1, [r0]		@ write 0, disable watchdog

	ldr	sp, =1024*8		@ set stack, notice: can't larger than 8K
	bl	clock_init		@ system clock initial
	bl	sdram_init		@ sdram initial

	/* relocation */
	adr	r0, _start		@ get _start's current address: 0 
	ldr	r1, = _start		@ _start's link address
	ldr	r2, = bss_start	@ bss section's begining link address
	cmp	r0, r1
	beq	clean_bss
copy_loop:
	ldr r3, [r0], #4
	str r3, [r1], #4
	cmp r1, r2
	bne copy_loop
/* clear bss section */ clean_bss: ldr r0, = bss_start ldr r1, = bss_end mov r3, #0 cmp r0, r1 ldreq pc, = on_ddr clean_loop: str r3, [r0], #4 cmp r0, r1 bne clean_loop ldr pc, = on_ddr on_ddr: bl irq_init @ initial IRQ
    @mrs     r0, cpsr
	bic	r0, r0, #0x9f
	orr	r0, r0, #0x10
	msr	cpsr, r0				@ enter user mode

        ldr     sp, = 0x57000000
	@bl	main				@ call c program's main function

ldr pc, = main halt: b halt
 
启动代码的一般流程如下:
        . 硬件相关设置:把外设基地址告诉CPU(ARM11特用)
          (分析:ldr r0, = 0x70000000 其中0x70000000是外设的基地址,从6410的datasheet的第二章存储器映射一章可以找到
                         orr r0, r0, #0x13 指r0中的值是256,代表256M,这是ARM11规定的,具体在ARM11datasheet中)
 
        . 关看门狗
      (分析:ldr r0, = 0x7e004000 加载地址0x7e004000上的数据放入r0中
                     mov r1, #0x0
                     str r1, [r0] 将r1中的数据存储到r0指向的存储单元中——把看门狗寄存器写0)
 
        .设置堆栈(后面要调用c函数,调用函数就要先设置栈,片内8K内存)
        .初始化时钟
        .初始化SDRAM
        .重定位
        .清BSS段
        .调用C函数
 
1.2 commom.h共用的头文件
       里面编写了一些方便的函数,都是对寄存器的某位或多位进行操作的函数,缩短我们写代码的时间,下面贴出来,原理没什么可讲的,自己分析就知道了。
#ifndef __COMMON_H
#define __COMMON_H

#define vi *( volatile unsigned int * ) 

#define set_zero( addr, bit ) ( (vi addr) &= ( ~ ( 1 << (bit) ) ) )
#define set_one( addr, bit ) ( (vi addr) |= ( 1 << ( bit ) ) )

#define set_bit( addr, bit, val ) ( (vi addr) = (( vi addr)&=(~(1<<(bit))) ) | ( (val)<<(bit) ) )

#define set_2bit( addr, bit, val ) ( (vi addr) = (( vi addr)&(~(3<<(bit))) ) | ( (val)<<(bit) ) )

#define set_nbit( addr, bit, len,  val ) \
	( (vi addr) = ((( vi addr)&(~(( ((1<<(len))-1) )<<(bit))))  | ( (val)<<(bit) ) ))

#define get_bit( addr, bit ) ( (( vi addr ) & ( 1 << (bit) )) > 0  )

#define get_val( addr, val ) ( (val) = vi addr )
#define read_val( addr ) ( vi ( addr ) )
#define set_val( addr, val ) ( (vi addr) = (val) )
#define or_val( addr, val ) ( (vi addr) |= (val) ) 

///////////////////////////////

typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

// function declare

int delay( int );

#endif /* __COMMON_H */
 
1.3 irq.c
       这是一个中断初始化和中断处理函数文件。
下面对ARM异常进行介绍:
        中断也是异常的一种,ARM处理器用7中工作模式:
(1)用户模式(user)              usr     正常的执行模式
(2)快速中断模式(FIQ)          fiq      高优先级中断产生进入的模式(高速数据传输等情况使用)
(3)外部中断模式(IRQ)          irq      低优先级中断产生进入的模式(一般的外部中断)
(4)特权模式(Superviser)      svc     复位或软中断,供操作系统使用的保护模式
(5)数据访问中止模式(Abort)  abt      存取数据异常,用于虚拟存储或存储保护
(6)未定义指令模式(undefine)und      当执行未定义指令时进入的模式
(7)系统模式(system)          sys      运行特权级操作系统任务
中断过程:
(1)系统上电,CPU处于svc模式
(2)如果发生中断,那么CPU进入IRQ模式;R13、R14切换到自己的R13、R14;跳到相应的中断向量地址
 
如何进行中断编程?
(1)中断初始化
a. 设置中断源(也就是配置引脚模式)
b. 设置中断控制器(参照6410datasheet中断控制器一章)
c. 打开总中断开关(设置CPSR)
 
 
 
1.4 regs.h
      这是一个6410中声明和定义寄存器的,需要哪个寄存器就在该文件当中定义,在后面的文件当中直接调用即可。
 
1.5 sdram.c
      下面我们来看一下6410核心板DDR的原理图
image
       从图中我们可以看到,该核心板有两个DDR级联而得,每个有16位,两个一共32位。一个DDR有15根地址线,寻址空间为2^15=32K,参照K4X1G163PC的datasheet可以算出总的容量为:
       64M*16*2 = 2G  则每块为1G,算下来地址线不够,因此地址肯定是多次发出来。
       BA0和BA1说明可以访问4块,而它提供13条行地址和10条列地址。而6410提供DDR控制器,只需要控制DDR控制器即可。那怎样初始化DDR?
        (1)地址线设置
        (2)告诉位宽
        (3)设置时序
image
设置DDR控制器
image
初始化DDR芯片
image
下面参照代码,看看初始化DDR的顺序
 
#include "common.h"

#define MEMCCMD		0x7e001004
#define P1REFRESH	0x7e001010
#define P1CASLAT	0x7e001014
#define MEM_SYS_CFG	0x7e00f120
#define P1MEMCFG	0x7e00100c
#define P1T_DQSS	0x7e001018
#define P1T_MRD		0x7e00101c
#define P1T_RAS		0x7e001020
#define P1T_RC		0x7e001024
#define P1T_RCD		0x7e001028
#define P1T_RFC		0x7e00102c
#define P1T_RP		0x7e001030
#define P1T_RRD		0x7e001034
#define P1T_WR		0x7e001038
#define P1T_WTR		0x7e00103c
#define P1T_XP		0x7e001040
#define P1T_XSR		0x7e001044
#define P1T_ESR		0x7e001048
#define P1MEMCFG2	0X7e00104c
#define P1_chip_0_cfg	0x7e001200

#define P1MEMSTAT	0x7e001000
#define P1MEMCCMD	0x7e001004
#define P1DIRECTCMD	0x7e001008

	
#define HCLK	133000000

#define nstoclk(ns)	(ns/( 1000000000/HCLK)+1)

void sdram_init( void )
{
	// tell dramc to configure
	set_val(MEMCCMD, 0x4 );

	// set refresh period
	set_val( P1REFRESH, nstoclk(7800) );

	// set timing para
	set_val( P1CASLAT, ( 3 << 1 ) );  
	set_val( P1T_DQSS, 0x1 );	// 0.75 - 1.25
	set_val( P1T_MRD, 0x2 );
	set_val( P1T_RAS, nstoclk(45) );
	set_val( P1T_RC, nstoclk(68) );

	u32 trcd = nstoclk( 23 );
	set_val( P1T_RCD, trcd | (( trcd - 3 ) << 3 ) );
	u32 trfc = nstoclk( 80 );
	set_val( P1T_RFC, trfc | ( ( trfc-3 ) << 5 ) );   
	u32 trp = nstoclk( 23 );
	set_val( P1T_RP, trp | ( ( trp - 3 ) << 3 ) ); 
	set_val( P1T_RRD, nstoclk(15) );
	set_val( P1T_WR, nstoclk(15) );
	set_val( P1T_WTR, 0x7 );
	set_val( P1T_XP, 0x2 );
	set_val( P1T_XSR, nstoclk(120) );
	set_val( P1T_ESR, nstoclk(120) );

	// set mem cfg 
	set_nbit( P1MEMCFG, 0, 3, 0x2 );  /* 10 column address */

	/* set_nbit: 把从第bit位开始的一共len位消零,然后把这几位设为val */
	set_nbit( P1MEMCFG, 3, 3, 0x2 );  /* 13 row address */
	set_zero( P1MEMCFG, 6 );		  /* A10/AP */
	set_nbit( P1MEMCFG, 15, 3, 0x2 ); /* Burst 4 */

	set_nbit( P1MEMCFG2, 0, 4, 0x5 );
	set_2bit( P1MEMCFG2, 6, 0x1 );    /* 32 bit */
	set_nbit( P1MEMCFG2, 8, 3, 0x3 ); /* Mobile DDR SDRAM */
	set_2bit( P1MEMCFG2, 11, 0x1 );

	set_one( P1_chip_0_cfg, 16 );     /* Bank-Row-Column organization */

	// memory init
	set_val( P1DIRECTCMD, 0xc0000 ); // NOP
	set_val( P1DIRECTCMD, 0x000 );  // precharge
	set_val( P1DIRECTCMD, 0x40000 );// auto refresh
	set_val( P1DIRECTCMD, 0x40000 );// auto refresh
	set_val( P1DIRECTCMD, 0xa0000 ); // EMRS
	set_val( P1DIRECTCMD, 0x80032 ); // MRS

	set_val( MEM_SYS_CFG, 0x0 );

	// set dramc to "go" status	
	set_val( P1MEMCCMD, 0x000 );

	// wait ready
	while( !(( read_val( P1MEMSTAT ) & 0x3 ) == 0x1));
}
 
1.6 time.c
       对系统时钟进行初始化设置,而对于6410的晶振是12M,需要通过一系列的变频,分频来产生500~600M的时钟。对时钟t进行设置需要参照6410的datasheet中的系统控制器一章的时钟体系。
image
   初始化设置系统时钟,无非就是对相应的寄存器进行设置,设置分频等,下面说几个知识点:
  图中ARMCLK是ARM11的CPU时钟,一般设置为532MHz
         HCLK为133MHz,一般为NandFlash和DDR提供时钟,PCLK为67MHz
         SCLK为某些特殊设备提供时钟
当系统上电,晶振开始起振,不可能一下子从12M就变为532M,需要一段缓冲的时间,这段时间称为LOCKTIME,如下图所示:
image
#define APLL_LOCK (*((volatile unsigned long *)0x7E00F000))
#define MPLL_LOCK (*((volatile unsigned long *)0x7E00F004))
#define EPLL_LOCK (*((volatile unsigned long *)0x7E00F008))

#define OTHERS    (*((volatile unsigned long *)0x7e00f900))

#define CLK_DIV0  (*((volatile unsigned long *)0x7E00F020))

#define ARM_RATIO    0   /* ARMCLK = DOUTAPLL / (ARM_RATIO + 1)    */
#define HCLKX2_RATIO 4   /* HCLKX2 = HCLKX2IN / (HCLKX2_RATIO + 1) = 100MHz */
#define HCLK_RATIO   0   /* HCLK = HCLKX2 / (HCLK_RATIO + 1)   = 100MHz       */
#define PCLK_RATIO   1   /* PCLK   = HCLKX2 / (PCLK_RATIO + 1) = 50MHz    */
#define MPLL_RATIO   0   /* DOUTMPLL = MOUTMPLL / (MPLL_RATIO + 1)     */


#define APLL_CON  (*((volatile unsigned long *)0x7E00F00C))
#define APLL_CON_VAL  ((1<<31) | (266 << 16) | (3 << 8) | (1))

#define MPLL_CON  (*((volatile unsigned long *)0x7E00F010))
#define MPLL_CON_VAL  ((1<<31) | (266 << 16) | (3 << 8) | (1))

#define CLK_SRC  (*((volatile unsigned long *)0x7E00F01C))

void clock_init(void)
{
	APLL_LOCK = 0xffff;
	MPLL_LOCK = 0xffff;
	EPLL_LOCK = 0xffff;

	/* set async mode */
	OTHERS &= ~(0xc0);
	while((OTHERS & 0xf00) != 0);

	CLK_DIV0 = (ARM_RATIO) | (MPLL_RATIO << 4)
		| (HCLK_RATIO << 8) | (HCLKX2_RATIO << 9) 
		| (PCLK_RATIO << 12);
		
	APLL_CON = APLL_CON_VAL;  /* 532MHz */
	MPLL_CON = MPLL_CON_VAL;  /* 532MHz */

	CLK_SRC = 0x03;
}
 
下面对时钟设置步骤进行说明:
(1)设置LockTime,包括APLL_LOCK,MPLL_LOCK,EPLL_LOCK,一般设置为默认值即可,也可以不用设置,因为它复位后就是默认值。
(2)设置为异步模式,当CPU时钟和内存时钟不相等的时候,需要设置为异步模式,主要是设置寄存器OTHERS,然后在查询相对应位是否为0,一直等待设置完毕。
(3)然后沿着上图的时钟体系设置PLL寄存器的值
 
1.7 led.c
     主函数文件就不说了,关键在于你想实现什么功能了
 
1.8  led.lds
     该裸机程序的链接脚本,首先先把该文件中的内容贴出来:
SECTIONS {
	. = 0x50000000;                     //当前地址
	. = ALIGN(4);
	.text	: {                          //段名称,放置所有文件的代码段
		start.o (.text)
		time.o (.text)
		irq.o (.text)
		led.o (.text)
	}

	. = ALIGN(4);                       //4位对齐
	.rodata	: {
		* (.rodata)
	}

	. = ALIGN(4);
	.data	: {
		* (.data)
	}

	. = ALIGN(4);
	bss_start = .;                      //bss段开始处
	.bss	: {                          //放置所用bss段
		* (.bss)
	}
	bss_end = .;                        //bss段结束处
}

下面说一下,之前我们写的简单程序,没有用到DDR,只是将程序在6410的8K片内内存中运行,但是如果程序很大,那就不能指望在片内内存中运行我们的程序了。下面就要用到SDRAM,就要涉及到链接地址。

      简单的说,一个程序分为下面几个部分:

      (1)代码段(text):就是我们所写的代码,指令

      (2)数据段(data):有初始值的全局变量或静态变量

      (3)Bss段(Bss):未初始化或初始值为0的全局变量或静态变量

     分析反汇编文件我们得出:访问全局变量使用的是链接地址来访问的。在系统上电后,系统会自动的把NandFlash中的前8K程序拷贝到片内8K内存当中去,而一个程序要执行,应该位于链接地址。当程序的链接地址不等于当前地址时,就需要重定位,将程序拷贝到相应的链接地址中去执行。

     位置无关码:相对跳转指令,不访问全局变量。下面看一下重定位代码:

/* relocation */
	adr	r0, _start			@ get _start's current address: 0 
	ldr	r1, = _start		       @ _start's link address
	ldr	r2, = bss_start		@ bss section's begining link address
	cmp	r0, r1                      @  compare isnot equal 
	beq	clean_bss

copy_loop:
	ldr r3, [r0], #4
	str r3, [r1], #4
	cmp r1, r2
	bne copy_loop
	/* clear bss section */
clean_bss:
	ldr	r0, = bss_start
	ldr	r1, = bss_end
	mov	r3, #0
	cmp	r0, r1
	ldreq	pc, = on_ddr
clean_loop:
	str	r3, [r0], #4
	cmp	r0, r1
	bne	clean_loop
	ldr	pc, = on_ddr
在分析过程中,我们可以参照反汇编文件来分析!

 

1.9 Makefile

      由于是在Linux下开发,了解Makefile也是很有必要的,下面是本模板的Makefile代码:

CC = arm-linux-gcc
LD = arm-linux-ld
AR = arm-linux-ar
OBJCOPY = arm-linux-objcopy
OBJDUMP = arm-linux-objdump
CFLAGS = -Wall -Os -fno-builtin-printf

export CC LD AR OBJCOPY OBJDUMP CFLAGS

objs := start.o time.o sdram.o irq.o led.o

led.bin : $(objs)
	$(LD) -Tled.lds -o led_elf $^
	$(OBJCOPY) -O binary -S led_elf $@
	$(OBJDUMP) -D -m arm led_elf > led.dis

%.o : %.c	
	$(CC) $(CFLAGS) -c -o $@ $<
%.o : %.S
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.dis *.bin *_elf *.o

 

       总结:(待续……)

 

   
       
posted on 2013-03-27 21:07  程序马  阅读(698)  评论(0编辑  收藏  举报