原创 基于 ARM 的嵌入式系统程序开发要点(二)—— 系统的初始化过程

2008-10-9 12:04 1874 4 4 分类: MCU/ 嵌入式

<?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 内核的一部分,不同的系统其设计不尽相同,所以应该针对具体的要求来 完成这部分的程序设计。一般来说,下面这两个方面是比较通用的。


21.存储器类型和时序配置 一个复杂的系统可能存在多种存储器类型的接口需要根据实际的系统设计对此加以正确配置对同一种存储器类型来说也因为访问速度的差异需要不 同的时序设置。通常 Flash SRAM 同属于静态存储器类型,可以合用同一个存储器端口; 而 DRAM  因为动态刷新和地址线复用等特性,通常配有专用的存储器端口。存储器端口的接口时序优化是非常重要的影响到整个系统的性能因为一 般系统运行的速度瓶颈都存在于存储器访问,所以存储器访问时序应尽可能地 但同时又要考虑由此带来的稳定性问题只有根据具体选定的芯片进行多 次的测试之后,才能确定最佳的时序配置。


 


22.存储器地址分布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 IF 位状态也可以考虑在这一步进行如果 系统中另外存在一个专门的中断控制器(多数情况下是这样的,这么做总是安 全的否则就需要考虑过早地打开中断可能带来的问题比如在系统初始化完成 之前就触发了有效中断,导致系统的死机。


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   余焕丽

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
我要评论
0
4
关闭 站长推荐上一条 /1 下一条