<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" /><?xml:namespace prefix = w ns = "urn:schemas-microsoft-com:office:word" />基于 ARM 的芯片多数为复杂的片上系统集成(SoC),这种复杂的系统里多 数的硬件模块都是可配置的,需要由软件来设置其需要的工作状态。因此在用户 的应用程序启动之前,需要有专门的一段启动代码来完成对系统的初始化。由于 这类代码直接面对处理器内核和硬件控制器进行编程,一般都使用汇编语言。系 统启动程序所执行的操作跟具体的目标系统和开发系统相关,一般通用的内容包括:
z 中断向量表
z 初始化存储器系统
z 初始化堆栈
z 初始化有特殊要求的端口、设备
z 初始化应用程序执行环境
z 改变处理器模式
z 呼叫主应用程序
1.中断向量表
ARM 要求中断向量表必须放置在从 0 地址开始,连续 8×4 字节的空间内
(ARM720T 和 ARM9/10 及以后的 ARM 处理器也支持从 0xFFFF0000 开始的高 地址向量表,在本文的其他地方对此不再另加说明)。
2.初始化存储器系统 初始化存储器系统的编程对象是系统的存储器控制器。存储器控制器并不是
ARM 内核的一部分,不同的系统其设计不尽相同,所以应该针对具体的要求来 完成这部分的程序设计。一般来说,下面这两个方面是比较通用的。
2.1.存储器类型和时序配置 一个复杂的系统可能存在多种存储器类型的接口,需要根据实际的系统设计对此加以正确配置。对同一种存储器类型来说,也因为访问速度的差异,需要不 同的时序设置。通常 Flash 和 SRAM 同属于静态存储器类型,可以合用同一个存储器端口; 而 DRAM 因为动态刷新和地址线复用等特性,通常配有专用的存储器端口。存储器端口的接口时序优化是非常重要的,影响到整个系统的性能。因为一 般系统运行的速度瓶颈都存在于存储器访问,所以存储器访问时序应尽可能地 快;但同时又要考虑由此带来的稳定性问题。只有根据具体选定的芯片,进行多 次的测试之后,才能确定最佳的时序配置。
2.2.存储器地址分布(memory map)
有些系统具有非常灵活的存储器地址分配特性,进行存储器初始化设计的时 候一定要根据应用程序的具体要求来完成地址分配。
一种典型的情况是启动 ROM 的地址重映射(remap)。如前面第 1 节所述, 当一个系统上电后程序将自动从 0 地址处开始执行,因此在系统的初始状态,必 须保证在 0 地址处存在正确的代码,即要求 0 地址开始处的存储器是非易性的 ROM 或 Flash 等。但是因为 ROM 或 Flash 的访问速度相对较慢,每次中断发生 后都要从读取 ROM 或 Flash 上面的向量表开始,影响了中断响应速度。因此有 的系统便提供一种灵活的地址重映射方法,可以把 0 地址重新指向到 RAM 中去。 在这种地址映射的变化过程当中,程序员需要仔细考虑的是程序的执行流程不能 被这种变化所打断。比如下面这种情况:
Flash
RAM
remap 0x0204
0x0200
0x0100 (Reset_Handler)
(remap)
.
.
(boot code)
.
.
.
0x0200
0x0000
… …
B Reset_Handler
Vector Table
图 2:启动 ROM 的地址重映射对程序执行流程的影响
系统上电后从 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 回到 别的模式了。可能会对接下去的程序执行造成影响。
一般堆栈的大小要根据需要而定,但是要尽可能给堆栈分配快速和高带宽的 存储器。堆栈性能的提高对系统整体性能的影响是非常明显的。
这是一段堆栈初始化的代码示例,其中只定义了三种模式的 SP 指针:
MRS R0, CPSR ; CPSR -> R0
BIC ORR MSR | R0, R0, #MODEMASK R1, R0, #IRQMODE CPSR_cxsf, R1 | ; ; ; | 安全起见,屏蔽模式位以外的其它位 把设置模式位设置成需要的模式 转到 IRQ 模式 |
LDR | SP, =UndefStack | ; | 设置 SP_irq |
ORR |
R1,R0,#FIQMODE |
|
|
MSR | CPSR_cxsf, R1 |
| ; FIQMode |
LDR | SP, =FIQStack |
|
|
ORR |
R1, R0, #SVCMODE |
|
|
MSR | CPSR_cxsf, R1 |
| ; SVCMode |
LDR | SP, =SVCStack |
|
|
注意上面的程序中使用到的 3 个 SP 寄存器是不同的物理寄存器:SP_irq,
SP_fiq 和 SP_svc。引用的几个标号假设已经正确定义。
4.初始化有特殊要求的端口、设备 这要由具体的系统和用户需求而定。一般的外设初始化可以在系统初始化之
后进行。
比较典型的应用是驱动一些简单的输出设备,如 LED 等,来指示系统启动 的进程和状态。
5.初始化应用程序执行环境 一个简单的可执行程序的映像结构通常如下:
ZI (Zero initialized R/W Data)
RW (R/W Data)
只定义了变量名的全局变量
定义时带初始值的全局变量
RO (Code + RO Data) 编译结果
图 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 initialised 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 的修改),所以要特别注意不能过早 地进入用户模式。一般地,在初始化过程中会经历以下一些模式变化:
(堆栈初始化阶段)
超级模式
(Supervisor)
多种特权模式
变化
设置成用户程
序运行模式
复位后的缺省模式 注意不要进入用户模式 用户选择
图 4:处理器模式变换过程
在最后阶段才把模式转换到最终应用程序运行所需的模式,一般是用户模 式。
内核级的中断使能(CPSR 的 I、F 位状态)也可以考虑在这一步进行。如果 系统中另外存在一个专门的中断控制器(多数情况下是这样的),这么做总是安 全的,否则就需要考虑过早地打开中断可能带来的问题,比如在系统初始化完成 之前就触发了有效中断,导致系统的死机。
7.呼叫主应用程序 当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序。最
简单的一种情况是:
IMPORT | main | ; get the label main if main() is defined in other files |
B | man | ; jump to main() |
直接从启动代码跳入应用程序主函数入口,主函数名字可由用户自己定义。
在 ARM ADS 环境中,还另外提供了一套系统级的呼叫机制。
IMPORT
B
main
main
main()
启动代码 应用程序初始化 用户应用程序 main()
图 5:在应用程序主函数之前插入 main
main() 是编译系统提供的一个函数,负责完成库函数的初始化和第 5 节中
所描述的功能,最后自动跳向 main() 函数。这种情况下用户程序的主函数名字 必须得是 main。
用户可以根据需要选择是否使用 main()。如果想让系统自动完成系统调用
(如库函数)的初始化过程,可以直接使用 main();如果所有的初始化步骤都 是由用户自己显式地完成,则可以跳过 main()。
当然,使用 main() 的时候,可能会涉及到一些库函数的移植和重定向问 题。在 main() 里面的程序执行流程如下图所示:
main
·copy code and data
·zero initialize
Image Entry Point
·
Reset handler
·user boot code
rt_entry
·initialize library functions
·call top-level constructors
(C++)
User_initial_stackheap
·set up stack & heap
·Exit from application
User application
·main
关于在 main() 里面调用到的库函数说明,可以参阅相关的编译器文档, 库函数移植和重定向的方法,可以参考上一期文章里面的相关章节。
技术支持:13148818895 0755-83690800/075583662100 余焕丽
文章评论(0条评论)
登录后参与讨论