基于ARM的芯片多数为复杂的片上系统(SoC),这种复杂系统里的多数硬件模块都是可配置的,需要由软件来设置其需要的工作状态。因此在用户的应用程序启动之前,需要有专门的一段启动代码来完成对系统的初始化。由于这类代码直接面对处理器内核和硬件控制器进行编程,一般都使用汇编语言。系统启动程序所执行的操作跟具体的目标系统和开发系统相关,一般通用的内容包括:
◇ 中断向量表;
◇ 初始化存储器系统;
◇ 初始化堆栈;
◇ 初始化有特殊要求的端口、设备;
◇ 初始化应用程序执行环境;
◇ 改变处理器模式;
◇ 呼叫主应用程序。
1 中断向量表
ARM要求中断向量表必须放置在从0地址开始,连续8×4字节的空间内(ARM720T和ARM9/10及以后的ARM处理器也支持从0xFFFF0000开始的高地址向量表,在本文的其它地方对此不再另加说明)。各个中断向量在向量表中的位置分配如图1所示。
图1 中断向量表
每当一个中断发生以后,ARM处理器便强制把PC指针置为向量表中对应中断类型的地址值。因为每个中断只占据向量表中1个字的存储器空间,只能放置1条ARM指令,所以通常在向量表中放的是跳转指令,使程序能从向量表里跳转到存储器里的其它地方,再执行中断处理。
中断向量表的程序实现通常如下所示:
AREA Boot, CODE, READONLY
ENTRY
B Reset_Handler ; Reset_Handler is a label
B Undef_Handler
B SWI_Handler
B PreAbort_Handler
B DataAbort_Handler
B . ; for reserved interrupt, stop here
B IRQ_Handler
B FIQ_Handler
其中的关键字ENTRY是指定编译器保留这段代码,因为编译器可能会认为这是一段冗余代码而加以优化。链接的时候要确保这段代码被链接在0地址处,并且作为整个程序的入口点(关键字ENTRY并非总是用来设置程序入口点,所以通常需要在链接选项里显式地指定程序入口点)。
2 初始化存储器系统
初始化存储器系统的编程对象是系统的存储器控制器。存储器控制器并不是ARM内核的一部分,不同的系统其设计不尽相同,所以应该针对具体的要求来完成这部分的程序设计。一般来说,下面这两个方面是比较通用的。
2.1 存储器类型和时序配置
一个复杂的系统可能存在多种存储器类型的接口,需要根据实际的系统设计对此加以正确配置。对同一种存储器类型来说,也因为访问速度的差异,需要不同的时序设置。
通常Flash 和SRAM同属于静态存储器类型,可以合用同一个存储器端口;而DRAM因为有动态刷新和地址线复用等特性,通常配有专用的存储器端口。
存储器端口的接口时序优化是非常重要的,这会影响到整个系统的性能。因为一般系统运行的速度瓶颈都存在于存储器访问,所以存储器访问时序应尽可能地快;但同时又要考虑由此带来的稳定性问题。只有根据具体选定的芯片,进行多次的测试之后,才能确定最佳的时序配置。
2.2 存储器地址分布
有些系统具有非常灵活的存储器地址分配特性,进行存储器初始化设计的时候,一定要根据应用程序的具体要求来完成地址分配。
一种典型的情况是启动ROM的地址重映射(remap)。如前面第1节所述,当一个系统上电后,程序将自动从0地址处开始执行,因此在系统的初始状态,必须保证在0地址处存在正确的代码,即要求0地址开始处的存储器是非易性的ROM或Flash等。但是因为ROM或Flash的访问速度相对较慢,每次中断发生后,都要从读取ROM或Flash上面的向量表开始,影响了中断响应速度。因此,有的系统便提供一种灵活的地址重映射方法,可以把0地址重新指向到RAM中去。在这种地址映射的变化过程中,程序员需要仔细考虑的是:程序的执行流程不能被这种变化所打断。比如如图2所示的情况。
系统上电后从Flash内的0地址开始执行,启动代码位于地址0x100开始的空间,当执行到地址0x200时,完成了一次地址的重映射,把原来0开始的地址空间由Flash转给了RAM。接下去执行的指令(这里为了简化起见,忽略流水线指令预取的模型)将来自从0x204开始的RAM空间。如果预先没有对RAM内容进行正确的设置,则里面的数据都是随机的,这样处理器在执行完0x200地址处的指令之后,再往下取指执行就会出错。解决的方法是:使RAM在使用之前准备好正确的内容,包括开头的向量表部分。
有的系统不具备存储器地址重映射的功能,所有的空间地址就相对简单一些,不需要考虑这方面的问题。
3 初始化堆栈
因为ARM处理器有7种执行状态,每一种状态的堆栈指针寄存器(SP)都是独立的(System和User模式使用相同的SP寄存器)。因此,对程序中需要用到的每一种模式都要给SP寄存器定义一个堆栈地址。方法是改变状态寄存器(CPSR)内的状态位,使处理器切换到不同的状态,然后给SP赋值。注意:不要切换到User模式进行User模式的堆栈设置,因为进入User模式后就不能再操作CPSR回到别的模式了,可能会对接下去的程序执行造成影响。
图2 启动ROM的地址重映射对程序执行流程的影响
一般堆栈的大小要根据需要而定,但是要尽可能给堆栈分配快速和高带宽的存储器。堆栈性能的提高对系统整体性能的影响是非常明显的。
这是一段堆栈初始化的代码示例,其中只定义了三种模式的SP指针:
MRS R0, CPSR ; CPSR=>R0
BIC R0, R0, #MODEMASK ; 安全起见,屏蔽模式 ; 位以外的其它位
ORR R1, R0, #IRQMODE ; 把设置模式位设置成 ; 需要的模式
MSR CPSR_cxsf, R1 ; 转到IRQ模式
LDR SP, =UndefStack ; 设置 SP_irq ORR R1,R0,#FIQMODE MSR CPSR_cxsf, R1 ; FIQ Mode LDR SP, =FIQStack
ORR R1, R0, #SVCMODE
MSR CPSR_cxsf, R1 ; SVC Mode
LDR SP, =SVCStack
注意,上面的程序使用到的3个SP寄存器是不同的物理寄存器:SP_irq,SP_fiq和SP_svc。引用的几个标号假设已经正确定义。
4 初始化有特殊要求的端口、设备
这要由具体的系统和用户需求而定。一般的外设初始化可以在系统初始化之后进行。
比较典型的应用是驱动一些简单的输出设备,如LED等,来指示系统启动的进程和状态。
5 初始化应用程序执行环境
一个简单的可执行程序的映像结构通常如图3所示。
图3 程序映像的结构
映像一开始总是存储在ROM/Flash里面的,其RO部分既可以在ROM/Flash里面执行,也可以转移到速度更快的RAM中去;而RW和ZI这两部分是必须转移到可写的RAM里去。所谓应用程序执行环境的初始化,就是完成必要的从ROM到RAM的数据传输和内容清零。
不同的工具链会提供一些不同的机制和方法帮助用户完成这一步操作,主要是跟链接器(Linker)相关。下面是在ARM开发工具环境(ADS或RVCT)下,一种常用存储器模型的直接实现:
LDR r0, = Image$$RO$$Limit ;Get pointer to ;ROM data
LDR r1, = Image$$RW$$Base ;RAM copy ;address
LDR r3, = Image$$ZI$$Base ; Zero init base => ;top of initialized ;data
CMP r0, r1 ;Check that they ;are different
BEQ %F1
0
CMP r1, r3 ; Copy init data
LDRCC r2, [r0], #4 ; ([r0]=> r2) and ; (r0+4)
STRCC r2, [r1], #4 ; (r2 => [r1]) and ; (r1+4)
BCC %B0
1
LDR r1, = Image$$ZI$$Limit ; Top of zero init ;segment
MOV r2, #0
2
CMP r3, r1
STRCC r2, [r3], #4 ; (0=> [r3]) and ; (r3+4)
BCC %B2
程序实现了RW数据的拷贝和ZI区域的清零功能。其中引用到的4个符号是由链接器(linker)定义输出的。
Image$$RO$$Limit :表示RO区末地址后面的地址,即RW数据源的起始地址。
Image$$RW$$Base :RW区在RAM里的执行区起始地址,也就是编译选项RW_Base指定的地址;程序里是RW数据拷贝的目标地址。
Image$$ZI$$Base :ZI区在RAM里面的起始地址。
Image$$ZI$$Limit :ZI区在RAM里面的结束地址后面的一个地址。
程序先把ROM里 Image$$RO$$Limit 开始的RW初始数据拷贝到RAM里 Image$$RW$$Base 开始的地址,当RAM这边的目标地址到达 Image$$ZI$$Base 后就表示RW区的结束和ZI区的开始,接下去就对这片ZI区进行清零操作,直到遇到结束地址 Image$$ZI$$Limit 。
6 改变处理器模式
ARM处理器(V4架构以后的版本)一共有7种执行模式:
User: 用户模式
FIQ: 快速中断响应模式
IRQ: 一般中断响应模式
Supervisor: 超级模式
Abort: 出错处理模式
Undef: 未定义模式
System: 系统模式
除用户模式以外,其它6种模式都是特权模式。因为在初始化过程中,许多操作需要在特权模式下才能进行(比如CPSR的修改),所以要特别注意不能过早地进入用户模式。一般地,在初始化过程中会经历如图4所示的模式变化。
图4 处理器模式变换过程
在最后阶段才把模式转换到最终应用程序运行所需的模式,一般是用户模式。
内核级的中断使能(CPSR的I、F位状态)也可以考虑在这一步进行。如果系统中另外存在一个专门的中断控制器(多数情况下是这样的),这么做总是安全的,否则就需要考虑过早地打开中断可能带来的问题,比如在系统初始化完成之前就触发了有效中断,导致系统的死机。
7 呼叫主应用程序
当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序。最简单的一种情况是:
IMPORT main ; get the label main if ;main() is defined ;in other files
B main ; jump to main()
直接从启动代码跳转到应用程序的主函数入口,当然主函数名字可以由用户随便定义。
在ARM ADS环境中,还另外提供了一套系统级的呼叫机制。
IMPORT __main
B __main
如图5所示,__main()要插入在应用程序主函数之前。
图5 在应用程序主函数之前插入__main()
__main() 是编译系统提供的一个函数,负责完成库函数的初始化和第5节中所描述的功能,最后自动跳向main() 函数。这种情况下用户程序的主函数名必须是main。
用户可以根据需要选择是否使用__main()。如果想让系统自动完成系统调用(如库函数)的初始化过程,可以直接使用__main();如果所有的初始化步骤都是由用户自己显式地完成,则可以跳过__main()。
当然,使用__main() 的时候,可能会涉及到一些库函数的移植和重定向问题。在__main() 里面的程序执行流程如图6所示。
图6 有系统调用参与的程序执行流程
关于在__main() 里面调用到的库函数说明,可以参阅相关的编译器文档,库函数移植和重定向的方法,可以参考上一期文章里面的相关章节。
(更多关于ARM的详细技术资料,请访问:http://www.arm.com/arm/documentation?OpenDocument)
