启动代码是处理器上电复位后最先运行的一段代码。主要是用来把处理器初始化到一个确定的状态,为c运行环境作好准备(如:设置异常向量表,初始化系统时钟,初始化外部内存,把用户代码拷贝到外部内存,初始化栈,清0全局变量区,静态变量区等)。与体系结构相关的部分,只能用汇编来写。
由于S3C2416、S3C2450、S3C2451目前在各个编译器中都还没有启动代码文件,因此笔者在MDK下主要参考S3C2440的启动代码来写一个S3C2416的启动代码。
1、启动代码流程
启动代码的内容,顺序等都没有特定的要求,只要能达到初始化c运行环境即可。这里笔者说明一下,编写的启动代码应该是地址无关的,即链接到存储器的任意地址都是可以运行的。
例如:S3C2416在NandFlash boot/NorFlash boot时用户的第一条代码是在0x0处执行的,而在IROM boot时,用户的第一条代码是在0x40000000处执行的,启动代码对不同的启动模式都是要能正确运行的。此外,启动代码文件内容应该是板级无关的,不同的开发板,只要是同一处理器(S3C2416/50/51),启动代码文件都是适用的。即板级的代码如GPIO口的初始化不适合放在启动代码中实现。
此处笔者只以笔者的启动代码思路来作讲解。
1.1、异常向量表
处理器在上电复位完成后,第一条代码进入的是异常向量表中对应的复位向量地址。对于arm,这个异常向量表的地址通常是0x00000000偏移处,如发生复位,则arm进入0x0处的复位向量地址,发生IRQ中断则进入0x18处的IRQ异常向量地址。
异常向量表就是用来记录各个异常进入时的代码处理位置。
1.2、关看门狗
复位代码最先做的事应该是关看门狗,因为如果看门狗打开的话,在启动代码进行初始化过程中是无法喂狗的,可能造成处理器一直不停复位。
1.3、关闭所有中断
启动代码未完成时,各个状态都还不是确定的,如果有中断打开并引起中断异常,可能造成代码跑飞。
1.4、初始化系统时钟
一般来说,处理器复位后都是运行在一个较低速的时钟下,为加快启动,通常尽可能快地设置处理器的各个时钟。
1.5、初始化外部内存
除了Nor flash可以直接执行代码外,其它的代码存储器如nand flash、sd/mmc都是不能直接执行代码的。因此需要初始化外部内存,用来存储代码,变量。(本章节为避免复杂化,暂时不讲解,注释掉调用DDR2的初始化及用户代码的搬移)
1.6、用户代码拷贝到外部内存
对于代码存储在nand、sd/mmc等不能直接执行代码的存储器,初始代码是一定需要把用户代码从这些设备读入到特定的内存中执行的。而对于Nor flash可直接执行代码的存储器,通常为了提高性能,也是会把代码从Nor flash读出,在内存中执行的。
1.7、MMU初始化
MMU的初始化不总是必须的,主要是为提高性能,开D-cache必须开MMU。如果是iROM SD/MMC或iROM NAND方式运行代码,用到了中断,则也必须开MMU,因为iROM启动在0x0地址处为iROM固化代码,需要对中断向量表重新映射到RAM。(本章节为避免复杂化,暂时不讲解,注释掉MMU的初始化)
1.8、初始化栈
不管是用汇编还是用c编写代码,栈是一定要分配及初始化的。arm7/arm9有七种工作模式,每种模式的栈均应该设置,最后是进入用户模式。
1.9、跳转到c入口
进入c入口之前是需要初始化c环境(如:清0全局变量、静态变量区等)。此处为保持跟MDK的其它arm启动代码一致,直接跳转到__main,利用编译器的库函数来完成c环境的初始化,最后才是真正的用户main函数。启动代码完成后,最后是用绝对地址来跳转到c入口__main的。
2、启动代码的实现
笔者对启动代码都是有一定的注释,有些寄存器的设置是需要参考芯片数据手册,S3C2416数据手册,arm架构以及汇编语法学习笔者在之前的文章已给出相关资料的链接,欢迎下载学习。只要加入了启动代码,就可以任意用c语言来开发S3C2416了。
;/********************************************************/
;/* s3c2416.s: start code forsamsung s3c2416/50/51(arm9) */
;/********************************************************/
;外部时钟设置
; Clock setting(External Crystal = 12M):
; MPLLCLK = 800M, EPLLCLK = 96M
; ARMCLK = 400M, HCLK = 133M
; DDRCLK = 266M, SSMCCLK = 66M,PCLK = 66M
; HSMMC1 = 24M
;ISR偏移地址
; Standard definitions of Mode bitsand Interrupt (I & F) flags in PSRs
Mode_USR EQU 0x10
Mode_FIQ EQU 0x11
Mode_IRQ EQU 0x12
Mode_SVC EQU 0x13
Mode_ABT EQU 0x17
Mode_UND EQU 0x1B
Mode_SYS EQU 0x1F
;IRQ禁止使能总开关
; when I bit is set, IRQ isdisabled
I_Bit EQU 0x80
;FIQ禁止使能总开关
; when F bit is set, FIQ isdisabled
F_Bit EQU 0x40
;栈大小分配
; Stack Configuration
UND_Stack_Size EQU 0x00000020
SVC_Stack_Size EQU 0x00000020
ABT_Stack_Size EQU 0x00000020
FIQ_Stack_Size EQU 0x00000100
IRQ_Stack_Size EQU 0x00000400
USR_Stack_Size EQU 0x00001000
ISR_Stack_Size EQU (UND_Stack_Size + SVC_Stack_Size + \
ABT_Stack_Size+ FIQ_Stack_Size + \
IRQ_Stack_Size)
;栈区域
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE USR_Stack_Size
__initial_sp SPACE ISR_Stack_Size
Stack_Top
;堆大小
; Heap Configuration
Heap_Size EQU 0x00000200
;堆区域
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
;iRAM基地址
; Internal Memory Base Addresses
IRAM_BASE EQU 0x40000000
; Watchdog Timer Base Address
WT_BASE EQU 0x53000000
; Interrupt Register Base Address
INT_BASE EQU 0x4A000000
INTMSK1_OFS EQU 0x08
INTSUBMSK_OFS EQU 0x1C
INTMSK2_OFS EQU 0x48
; Clock Base Address 时钟基地址
CLOCK_BASE EQU 0x4C000000
LOCKCON0_OFS EQU 0x00
LOCKCON1_OFS EQU 0x04
MPLLCON_OFS EQU 0x10
EPLLCON_OFS EQU 0x18
CLKSRC_OFS EQU 0x20
CLKDIV0_OFS EQU 0x24
CLKDIV1_OFS EQU 0x28
CLKDIV2_OFS EQU 0x2C
;----------------------- CODE-------------------------------------------
PRESERVE8
; 入口位置
; Area Definition and Entry Point
; 启动代码必须被链接在它能运行的地址处
; Startup Code must be linked first at Address at which it expects to run.
;复位处理
AREA RESET, CODE, READONLY
; ENTRY
ARM
Vectors B Reset_Handler ;先调用“复位处理模块”
LDR PC, Undef_Addr
LDR PC, SWI_Addr
LDR PC, PAbt_Addr
LDR PC, DAbt_Addr
LDR PC, Notuse_Addr
LDR PC, IRQ_Addr
LDR PC, FIQ_Addr
Reset_Addr DCD Reset_Handler
Undef_Addr DCD Undef_Handler
SWI_Addr DCD SWI_Handler
PAbt_Addr DCD PAbt_Handler
DAbt_Addr DCD DAbt_Handler
Notuse_Addr DCD 0 ;Reserved Address
IRQ_Addr DCD IRQ_Handler
FIQ_Addr DCD FIQ_Handler
Undef_Handler B Undef_Handler
SWI_Handler B SWI_Handler
PAbt_Handler B PAbt_Handler
DAbt_Handler B DAbt_Handler
IRQ_Handler B IRQ_Handler
FIQ_Handler B FIQ_Handler
;复位处理模块
EXPORT Reset_Handler
Reset_Handler
;/*******************************************************************/
; 看门狗关闭
LDR R0, =WT_BASE
LDR R1, =0
STR R1, [R0]
;/*******************************************************************/
; 关闭所有外设中断
LDR R0, =INT_BASE
LDR R1, =0xFFFFFFFF
STR R1, [R0, #INTMSK1_OFS]
STR R1, [R0, #INTMSK2_OFS]
STR R1, [R0, #INTSUBMSK_OFS]
;/********************************************************************/
; 系统时钟设置
LDR R0, =CLOCK_BASE
LDR R1, =3600
; MPLL锁定时间需要大于300us,以外部晶振12M计算:
;1/12M=0.0000000833s
;0.0000000833s*3600=0.0003s=300us
STR R1, [R0, #LOCKCON0_OFS]
LDR R1, =3600
; EPLL锁定时间大于300us
STR R1, [R0, #LOCKCON1_OFS]
; PLL锁定时间设小了也应该不会有致命的问题,只是改变PLL输出后,过早地输出不稳定的时钟给system。
;
; MPLL(或外部旁路分频输出)通过ARM分频器输出ARM clock(400M),通过预分频器输
; 出给HCLK(133M), DDRCLK(266M),SSMCCLK(Memory Controllers,133M),PCLK(66M)。
; 此时我们设置如下:
; 设置PCLK=HCLK/2,SSMCCLK=HCLK/2,设置MPLL输出时钟800M,ARM clock分频比设2,
; 得到ARM clock 400M,预分频器分频比设为3,输出266M后再HCLK分频器2分频输出
; 给HCLK=266M/2=133M,HCLKDIV[1:0],PREDIV[5:4],ARMDIV[11:9],
; ARMCLK Ratio=(ARMDIV+1),HCLKRatio=(PREDIV+1)*(HCLKDIV+1)
;下面设置CLKDIV0:
LDR R1,=(0x1<<0)+(1<<2)+(1<<3)+(0x2<<4)+(0x1<<9)
STR R1,[R0, #CLKDIV0_OFS]
; EPLL(或外部旁路分频输出)通过各自的分频器输出给SPI(CLKSRC可选由MPLL供),
; DISP,I2S,UART,HSMMC1,USBHOST
LDR R1,=(0x0<<4)+(0x3<<6)+(0x0<<8)+(0x0<<12)+ \
(0x0<<16)+(0x0<<24)
STR R1, [R0, #CLKDIV1_OFS]
; HSMMC0时钟由EPPL供,SPI时钟可由MPLL供
LDR R1, =(0x0<<0)+(0x0<<6)
STR R1, [R0, #CLKDIV2_OFS]
; 设置EPLL输出96M:
;MDIV=32,PDIV=1,SDIV=2,Fout=((MDIV+(KDIV/2^16))*Fin)/(PDIV*2^SDIV),KDIV=0
LDR R1,=(2<<0)+(1<<8)+(32<<16)+(0x0<<24)+(0x0<<25)
STR R1, [R0, #EPLLCON_OFS]
; 给EPLLCON写入值并打开EPLL,此时EPLL clock锁定不输出,
; 延时LOCKCON1个时钟稳定后才输出时钟(LOCKCON1的值,前面设置的为3600)
; 外部晶振12M,设置MPLL输出为800M,
;MDIV=400,PDIV=3,SDIV=1,Fout=(MDIV*Fin)/(PDIV*2^SDIV)
LDR R1,=(1<<0)+(3<<5)+(400<<14)+(0x0<<24)+(0x0<<25)
STR R1, [R0, #MPLLCON_OFS]
; 给MPLLCON写入值并打开MPLL,此时MPLL clock锁定不输出,
; 延时LOCKCON0个时钟稳定后才输出时钟(LOCKCON0的值,前面设置的为3600)
; 时钟源设置MPLL和EPLL输出:
LDR R1, =(1<<4)+(1<<6)
STR R1,[R0, #CLKSRC_OFS]
;/************************************************************************/
; 外部内存控制设置
; IMPORT ERAM_Init
; BLX ERAM_Init ; 外部RAM初始化
; LDR SP, =Stack_Top
; RAM初始化后调整栈指针到外部RAM
;/************************************************************************/
; 拷贝用户代码到RAM
; IMPORT CopyCodeToRAM
; BLX CopyCodeToRAM
;/************************************************************************/
; MMU初始化
; IMPORT MMU_Init
; BLX MMU_Init
;/*************************************************************************/
; 堆栈初始化
LDR R0, =Stack_Top
;进入未定义指令模式,并设置此模式下对应的栈指针:
; Enter Undefined Instruction Mode and set its Stack Pointer
MSR CPSR_c, #Mode_UND:OR:I_Bit:OR:F_Bit
MOV SP, R0
SUB R0, R0, #UND_Stack_Size
;以下同理
; Enter Abort Mode and set its Stack Pointer
MSR CPSR_c, #Mode_ABT:OR:I_Bit:OR:F_Bit
MOV SP, R0
SUB R0, R0, #ABT_Stack_Size
;
; Enter FIQ Mode and set its Stack Pointer
MSR CPSR_c, #Mode_FIQ:OR:I_Bit:OR:F_Bit
MOV SP, R0
SUB R0, R0, #FIQ_Stack_Size
;
; Enter IRQ Mode and set its Stack Pointer
MSR CPSR_c, #Mode_IRQ:OR:I_Bit:OR:F_Bit
MOV SP, R0
SUB R0, R0, #IRQ_Stack_Size
;
; Enter Supervisor Mode and set its Stack Pointer
MSR CPSR_c, #Mode_SVC:OR:I_Bit:OR:F_Bit
MOV SP, R0
SUB R0, R0, #SVC_Stack_Size
;
; Enter System Mode and set its Stack Pointer
MSR CPSR_c, #Mode_SYS
MOV SP, R0
SUB SL, SP, #USR_Stack_Size
; 是否使用了KEIL的微库
IF :DEF:__MICROLIB
EXPORT __initial_sp
ELSE
MOV SP, R0
SUB SL, SP, #USR_Stack_Size
ENDIF
;/***********************************************************************/
; 绝对地址跳转到c入口
IMPORT __main
LDR R0, =__main
BX R0
; 是否使用了KEIL的微库
IF :DEF:__MICROLIB
EXPORT __heap_base
EXPORT __heap_limit
ELSE
; User Initial Stack & Heap
AREA |.text|, CODE, READONLY
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + USR_Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ENDIF
END
3、流水灯c代码
启动代码写好后,就可以运行用户的main函数了。本章节的启动代码暂未实现外部内存的初始化以及用户代码的拷贝,因此可运行的代码实际是在Steppingstone中的,只有8k大小,因此本章学习用的代码不要超过8k。
#include "s3c2416.h"
// IO port for controling LEDs
#define LED2 (13)// GPE13 LED2
#define LED3 (11)// GPE11 LED3
#define LED4 (13)// GPL13 LED4
#define LED5 (12)// GPE12 LED5
#define LED6 (2) // GPG2 LED6
#define LED7 (15)// GPA15 LED7
// c调用汇编查看ATPCS
__asm void Delay_ms(unsigned int nCount)
{
//延时1ms,共延时nCount(R0) ms
Delay1
LDR R1, =100000 // Arm clock为400M
Delay2
SUBS R1, R1,#1 // 一个Arm clock
BNE Delay2 // 跳转会清流水线,3个Arm clock
SUBS R0, R0, #1 // 调用者确保nCount不为0
BNE Delay1
BX LR
}
void Gpio_LED2(unsigned char On)
{
if (!On)
{
rGPEDAT&= ~(1<<LED2);
}
else
{
rGPEDAT|= (1<<LED2);
}
}
void Gpio_LED3(unsigned char On)
{
if (!On)
{
rGPEDAT&= ~(1<<LED3);
}
else
{
rGPEDAT|= (1<<LED3);
}
}
void Gpio_LED4(unsigned char On)
{
if (!On)
{
rGPLDAT&= ~(1<<LED4);
}
else
{
rGPLDAT|= (1<<LED4);
}
}
void Gpio_LED5(unsigned char On)
{
if (!On)
{
rGPEDAT&= ~(1<<LED5);
}
else
{
rGPEDAT|= (1<<LED5);
}
}
void Gpio_LED6(unsigned char On)
{
if (!On)
{
rGPGDAT&= ~(1<<LED6);
}
else
{
rGPGDAT|= (1<<LED6);
}
}
void Gpio_LED7(unsigned char On)
{
if (!On)
{
rGPADAT&= ~(1<<LED7);
}
else
{
rGPADAT|= (1<<LED7);
}
}
void Gpio_Init()
{
// GPA15 LED7output
rGPACON &=~(1<<LED7);
// GPG2 LED6output
rGPGCON &=~(0x03 << (LED6<<1));
rGPGCON |=(0x01 << (LED6<<1));
// GPE11,12,13LED3,LED5,LED2 output
rGPECON &=~((0x03<<(LED3<<1)) | (0x03<<(LED5<<1))
|(0x03<<(LED2<<1)));
rGPECON |=((0x01<<(LED3<<1)) | (0x01<<(LED5<<1))
|(0x01<<(LED2<<1)));
// GPL13 LED4output
rGPLCON &=~(0x03 << (LED4<<1));
rGPLCON |=(0x01 << (LED4<<1));
}
int main()
{
Gpio_Init();
while(1) {
Gpio_LED2(0);
Gpio_LED3(0);
Gpio_LED4(0);
Gpio_LED5(0);
Gpio_LED6(0);
Gpio_LED7(0);
Delay_ms(1000);
Gpio_LED2(1);
Delay_ms(1000);
Gpio_LED2(0);
Gpio_LED3(1);
Delay_ms(1000);
Gpio_LED3(0);
Gpio_LED4(1);
Delay_ms(1000);
Gpio_LED4(0);
Gpio_LED5(1);
Delay_ms(1000);
Gpio_LED5(0);
Gpio_LED6(1);
Delay_ms(1000);
Gpio_LED6(0);
Gpio_LED7(1);
Delay_ms(1000);
Gpio_LED7(0);
}
}
4、链接文件
链接器可以把多个目标文件及库文件链接成一个可执行的镜像文件。对于arm开发,通常通过链接文件来指定内存中代码和数据的存放位置(Eastar: 我的理解Scatter指定的域为运行域 ^_^)。MDK用scatter文件作为链接脚本,由于我们需要从sd卡启动,代码是需要在0x40000000的Steppingstone中执行,因此我们设置链接文件链接地址为0x40000000,8k大小,变量放在IRAM 64k的剩余区域。
在Keil软件中,Linker下取消勾选Use Memory Layout from Target Dialog,在Scatter File中点Edit即可编辑分散加载文件了(图4-1)。链接文件设置见图4-2。
5、代码烧写启动
5.1. 打开SdBoot工具,把编译器生成的二进制可执行代码s3c2416_Startup.bin转换成sd卡启动的代码格式,工具生成转换好的二进制可执行代码s3c2416_Startup_1.bin,SdBoot工具使用请参考上一章节。
5.2. 把转换好格式的可执行代码s3c2416_Startup_1.bin通过IROM_Fusing_Tool.exe烧写进sd卡。
5.3. 设置成sd卡启动,即可运行流水灯代码。
附注:
此章节的MDK工程,包含源码: http://pan.baidu.com/s/1eQzfnME