手中有OK6410开发板,一直想试试通过Linux来做做裸机开发,在网络论坛上也搜过一些资料作参考,整理了一下并做了解释或改动,希望这些东西可以大家分享下。
裸机程序的构成
基本的裸机程序由启动代码和C函数文件构成。而启动代码包括:硬件设备初始化、调用C函数。
本次分析中代码文件有:
start.S 汇编写的启动代码
commom.h 一些通用的函数
irq.c 中断初始化,中断处理等
regs.h
s3c6410的寄存器,用到的寄存器在本文件里声明/定义
sdram.c 关于sdram内存初始化等操作
time.c 对于系统时钟的设置,如锁相环等
main.c 这个就是主函数了,主程序就在这编写,这里可以写一个流水灯程序
main.lds 该文件为链接脚本,描述了各个输入文件的各个section怎样映射到输出文件的各个section中,还控制输出文件中section和符号等的内存布局。
Makefile makefile文件
1、 学习启动代码有助于我们以后开发uboot,uboot的启动代码跟裸机的相近。
下面把start.S代码贴出来,其中代码中也有注释。
@**************************************
@ File: start.S
@ Function: cpu initial and jump to c program
@**************************************
.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 ;初始化系统时钟
bl sdram_init ;初始化SDRAM
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
clean_bss:
ldr r0, = bss_start
ldr r1, = bss_end
mov r3, #0
cmp r0, r1
ldreq pc, =
on_ddr ;清BSS段
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 ;调用C函数
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函数
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<
) | ( (val)<
#define set_2bit( addr, bit, val ) (
(vi addr) = (( vi
addr)&(~(3<
| ( (val)<
#define set_nbit( addr, bit,
len, val ) \
( (vi addr) = ((( vi addr)&(~((
((1<
)<
(val)<
#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
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)
4、 regs.h
这是一个6410中声明和定义寄存器的,需要哪个寄存器就在该文件当中定义,在后面的文件当中直接调用即可。
5、 sdram.c
下面我们来看一下6410核心板DDR的原理图
从图中我们可以看到,该核心板有两个DDR级联而得,每个有16位,两个一共32位。每个DDR有15根地址线,而2^15=32K,内存芯片K4X1G163PC的datasheet可以看出为每片64M*16
= 1G bit,15根地址线必然不够,所以地址应该是分两次发出来的。
BA0和BA1可以访问4块bank,而它提供13条行地址和10条列地址。而6410提供了DDR控制器,只要控制DDR控制器即可。关于初始化DDR:
(1)地址线设置
(2)告诉位宽
(3)设置时序
可参照s3c6410的datasheet:设置DDR控制器
初始化DDR芯片,
参照代码,初始化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
);
set_nbit( P1MEMCFG, 3, 3, 0x2
); set_zero( P1MEMCFG, 6
); set_nbit( P1MEMCFG, 15, 3, 0x2 );
set_nbit( P1MEMCFG2,
0, 4, 0x5 );
set_2bit( P1MEMCFG2, 6, 0x1
); set_nbit( P1MEMCFG2, 8, 3, 0x3 );
set_2bit( P1MEMCFG2, 11, 0x1 );
set_one(
P1_chip_0_cfg, 16 );
// memory 初始化
set_val( P1DIRECTCMD, 0xc0000 ); // NOP
set_val( P1DIRECTCMD, 0x000 ); // 预充电
set_val( P1DIRECTCMD, 0x40000 );// 自刷新
set_val( P1DIRECTCMD, 0x40000 );// 自刷新
set_val( P1DIRECTCMD, 0xa0000 ); // EMRS
set_val( P1DIRECTCMD, 0x80032 ); // MRS
set_val( MEM_SYS_CFG,
0x0 );
//
设置内存控制器为"运行"状态 set_val( P1MEMCCMD, 0x000 );
// wait ready
while( !(( read_val( P1MEMSTAT )
& 0x3 ) == 0x1));
}
6、 time.c
对系统时钟进行初始化设置,而对于6410的晶振是12M,需要通过一系列的变频,分频来产生500~600M的时钟。对时钟t进行设置需要参照6410的datasheet中的系统控制器一章的时钟体系。
初始化设置系统时钟,无非就是对相应的寄存器进行设置,设置分频等,下面说几个知识点:
图中ARMCLK是ARM11的CPU时钟,一般设置为532MHz
HCLK为133MHz,一般为NandFlash和DDR提供时钟,PCLK为67MHz
SCLK为某些特殊设备提供时钟
当系统上电,晶振开始起振,不可能一下子从12M就变为532M,需要一段缓冲的时间,这段时间称为LOCKTIME,如下图所示:
#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 #define HCLKX2_RATIO 4 #define HCLK_RATIO 0 #define PCLK_RATIO 1 #define MPLL_RATIO 0
#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;
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; MPLL_CON = MPLL_CON_VAL;
CLK_SRC = 0x03;
}
下面对时钟设置步骤进行说明:
(1)设置LockTime,包括APLL_LOCK,MPLL_LOCK,EPLL_LOCK,一般设置为默认值即可,也可以不用设置,因为它复位后即为默认值。
(2)设置为异步模式,当CPU时钟和内存时钟不相等的时候,需要设置为异步模式,主要是设置寄存器OTHERS,然后在查询相对应位是否为0,一直等待设置完毕。
(3)然后沿着上图的时钟体系设置PLL寄存器的值
7、 main.c
主函数文件可以自己去写,实现何种功能由C来完成
8、 main.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片内ram运行即可,但是程序很大时,那就难以在片内ram中运行程序了。就需要用到SDRAM,这就得涉及到链接地址。
一个程序可分为下面几个部分:
(1)代码段(text):就是我们写的代码,指令
(2)数据段(data):有初始值的全局变量或静态变量
(3)Bss段(Bss):未初始化或初始值为0的全局变量或静态变量
分析反汇编文件我们得出:访问全局变量使用的是链接地址来访问的。在系统上电后,系统会自动的把NandFlash中的前8K程序拷贝到片内8K内存当中去,而一个程序要执行,应该位于链接地址。当程序的链接地址不等于当前地址时,就需要重定位,将程序拷贝到相应的链接地址中去执行。
位置无关码:相对跳转指令,不访问全局变量。下面看一下重定位代码:
adr r0,
_start ;获得_start的当前地址:
0
ldr r1, =
_start ; _start的链接地址
ldr r2, =
bss_start ; bss 段的气质链接地址
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
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
在分析过程中,我们可参照反汇编文件来分析
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
main.o
led.bin : $(objs)
$(LD) –Tmain.lds -o main_elf $^
$(OBJCOPY) -O binary -S main_elf $@
$(OBJDUMP) -D -m arm main_elf >
main.dis
%.o : %.c $(CC) $(CFLAGS) -c -o $@
$<
%.o : %.S
$(CC) $(CFLAGS) -c -o $@
$<
clean:
rm -f *.dis *.bin *_elf *.o
对于Makefile的理解学习可以在网上搜索一下《跟我一起写 Makefile》这个文档,看着写的不错。
欢迎关注飞凌嵌入式官方微信: