DIY编程器网

 找回密码
 注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 287|回复: 0
打印 上一主题 下一主题

基于ARM的嵌入式程序开发要点

[复制链接]
跳转到指定楼层
楼主
发表于 2012-1-27 19:26:30 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

  
         
    基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
基于ARM的嵌入式系统程序开发要点(一)
—— 嵌入式程序开发过程
ARM 系列微处理器作为全球16/32位RISC处理器市场的领先者,在许多领
域内得到了成功的应用.近年来,ARM在国内的应用也得到了飞速的发展,越
来越多的公司和工程师在基于ARM的平台上面开发自己的产品.
与传统的4/8位单片机相比,ARM的性能和处理能力当然是遥遥领先的,但
与之相应,ARM的系统设计复杂度和难度,较之传统的设计方法也大大提升了.
本文旨在通过讨论系统程序设计中的几个基本方面,来说明基于ARM的嵌入式
系统程序开发的一些特点,并提出和解决了一些常见的问题.
文章分成几个相对独立的章节刊载.第一部分讨论基于ARM的嵌入式程序
开发和移植过程中的一些基本概念.
1.嵌入式程序开发过程
不同于通用计算机和工作站上的软件开发工程,一个嵌入式程序的开发过程
具有很多特点和不确定性.其中最重要的一点是软件跟硬件的紧密耦合特性.
(不带操作系统支持) (带操作系统支持)
图-1:两类不同的嵌入式系统结构模型
这是两类简化的嵌入式系统层次结构图.由于嵌入式系统的灵活性和多样
性,上面图中各个层次之间缺乏统一的标准,几乎每一个独立的系统都不一样.
这样就给上层的软件设计人员带来了极大地困难.第一,在软件设计过程中过多
地考虑硬件,给开发和调试都带来了很多不便;第二,如果所有的软件工作都需
要在硬件平台就绪之后进行,自然就延长了整个的系统开发周期.这些都是应该
从方法上加以改进和避免的问题.
为了解决这个问题,工程和设计人员提出了许多对策.首先在应用与驱动(或
API)这一层接口,可以设计成相对统一的一些接口函数,这对于具体的某一个
开发平台或在某个公司内部,是完全做得到的.这样一来,就大大提高了应用层
应用(Application)
驱动/板级支持包
(Driver/BSP)
硬件(Hardware)
应用(Application)
硬件抽象层(HAL)
硬件(Hardware)
操作系统(OS)
标准接口函数(API)
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
软件设计的标准化程度,方便了应用程序在跨平台之间的复用和移植.
对于驱动/硬件抽象这一层,因为直接驱动硬件,其标准化变得非常困难甚至
不太可能.但是为了简化程序的调试和缩短开发周期,我们可以在特定的EDA
工具环境下面进行开发,通过后再进行移植到硬件平台的工作.这样既可以保证
程序逻辑设计的正确性,同时使得软件开发可平行甚至超前于硬件开发进程.
我们把脱离于硬件的嵌入式软件开发阶段称之为"C软件"的开发,可以
用下面的图来示意一个嵌入式系统程序的开发过程.
"C软件"开发 移植,测试 产品发布
图-2:嵌入式系统产品的开发过程
在"C软件"开发阶段,可以用软件仿真,即指令集模拟的方法,来对用
户程序进行验证.在ARM公司的开发工具中,ADS 内嵌的ARMulator和
RealView 开发工具中的ISS,都提供了这项功能.在模拟环境下,用户可以设
置ARM处理器的型号,时钟频率等,同时还可以配置存储器访问接口的时序参
数.程序在模拟环境下运行,不但能够进行程序的运行流程和逻辑测试,还能够
统计系统运行的时钟周期数,存储器访问周期数,处理器运行时的流水线状态(有
效周期,等待周期,连续和非连续访问周期)等信息.这些宝贵的信息是在硬件
调试阶段都无法取得的,对于程序的性能评估非常有价值.
为了更加完整和真实地模拟一个目标系统,ARMulator和ISS还提供了一个
开放的API编程环境.用户可以用标准C来描述各种各样的硬件模块,连同工
具提供的内核模块一起,组成一个完整的"软"硬件环境.在这个环境下面开发
的软件,可以更大程度地接近最终的目标.
利用这种先进的EDA工具环境,极大地方便了程序开发人员进行嵌入式开
发的工作.当完成一个"C软件"的开发之后,只要进行正确的移植,一个真
正的嵌入式软件就开发成功了.而移植过程是相对比较容易形成一套规范的流程
的,其中三个最重要的方面是:
考虑硬件对库函数的支持
移植
移植
开发/实验/
测试平台
最终产品
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
符合目标系统上的存储器资源分布
应用程序运行环境的初始化
2.开发工具环境里面的库函数
如果用户程序里调用了跟目标相关的一些库函数,则在应用前需要裁剪这些
函数以适合在目标上允许的要求.主要需要考虑以下三类函数:
访问静态数据的函数
访问目标存储器的函数
使用semihosting(半主机)机制实现的函数
这里所指的C库函数,除了ISO C标准里面定义的函数以外,还包括由编
译工具提供的另外一些扩展函数和编译辅助函数.
2.1 裁剪访问静态数据的函数
库函数里面的静态数据,基本上都是在头文件里面加以定义的.比如CTYPE
类库函数,其返回值都是通过预定义好的CTYPE属性表来获得的.比如,想要
改变isalpha() 函数的缺省判断,则需要修改对应CTYPE属性表里对字符属性的
定义.
2.2 裁减访问目标存储器的函数
有一类动态内存管理函数,如malloc() 等,其本身是独立于目标系统而运行
的;但是它所使用的存储器空间需要根据目标来确定.所以malloc() 函数本身
并不需要裁剪或移植,但那些设置动态内存区(地址和空间)的函数则是跟目标
系统的存储器分布直接相关的,需要进行移植.例如堆栈的初始化函数
__user_initial_stackheap(),是用来设置堆(heap)和栈(stack)地址的函数,显
然针对每一个具体的目标平台,该函数都需要根据具体的目标存储器资源进行正
确移植.
下面是对示例函数__user_initial_stackheap() 进行移植的一个例子:
__value_in_regs struct __initial_stackheap __user_initial_stackheap(
unsigned R0, unsigned SP, unsigned R2, unsigned SL)
{
struct __initial_stackheap config;
config.heap_base = (unsigned int) 0x11110000;
// config.stack_base = SP; // optional
return config;
}
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
请注意上面的函数体并不完全遵循标准C的关键字和语法规范,使用了ARM
公司编译器(ADS 或RealView Compilation tool) 里的C语言扩展特性.关于编译
器特定的C语言扩展,请参考相关的编译器说明,这里简单介绍函数
__user_initial_stackheap() 的功能,它主要是返回堆和栈的基地址.上面的程序中
只对堆(heap) 的基地址进行了设置(设成了0x11110000),也就是说用户把
0x11110000开始的存储器地址用作了动态内存分配区(heap区).具体地址的确
定是要由用户根据自己的目标系统和应用情况来确定的,至少要满足以下条件:
0x11110000开始的地址空间有效且可写(是RAM)
该存储器空间不与其它功能区冲突(比如代码区,数据区,stack区等)
因为__user_initial_stackheap() 函数的全部执行效果就是返回一些数值,所
以只要符合接口的调用标准,直接用汇编来实现看起来更加直观一些:
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR r0,0x11110000
MOV pc,lr
如果不对这个函数进行移植,编译过程中将使用缺省的设置,这个设置适用
于ARM公司的Integrator系列平台.
(注意:ARM的编译/连接工具链也提供了绕过库函数来设置运行时存储器模型
的方法,请参阅ARM公司其他的相关文档.)
2.3 裁剪使用semihosting(半主机)机制实现的函数
库函数里有一大部分函数是涉及到输入/输出流设备的,比如文件操作函数需
要访问磁盘I/O,打印函数需要访问字符输出设备等.在嵌入式调试环境下,所
有的标准C库函数都是有效且有其缺省行为的,很多目标系统硬件不能支持的
操作,都通过调试工具来完成了.比如printf() 函数,缺省的输出设备是调试器
里面的信息输出窗口.
但是一个真实的系统是需要脱离调试工具而独立运行的,所以在程序的移植
过程当中,需先对这些库函数的运行机制作一了解.
下图说明了在ADS下面这类C库函数的结构.
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
图-3:C库函数实现过程中的层次调用
如图中例子所示,函数printf() 最终是调用了底层的输入/输出函数
_sys_write() 来实现输出操作的,而_sys_write() 使用了调试工具的内部机制来把
信息输出到调试器.
显然这样的函数调用过程在一个真实的嵌入式系统里是无法实现的,因为独
立运行的嵌入式系统将不会有调试器的参与.如果在最终系统中仍然要保留
printf() 函数,而且在系统硬件中具备正确的输出设备(如LCD等),则在移植
过程中,需要把printf() 调用的输出设备进行重新定向.
考察printf() 函数的完整调用过程:
图-4:printf() 的调用过程
单纯考虑printf() 的输出重新定向,可以有三种途径实现:
改写printf() 本身
改写 fput()
改写 _sys_write()
需要注意的是,越底层的函数,被其他上层函数调用的可能性越大,改变了
一个底层函数的实现,则所有调用该函数的上层函数的行为都被改变了.
以fputc() 的重新实现为例,下面是改变fputc() 输出设备到系统串行通信端
口的实例:
int fputc(int ch, FILE *f)
ANSI C
Input/
output
Error
handling
Stack &
heap setup
Other
Semihosting Support
应用程序调用的
函数,如printf()
设备驱动程序级
使用semihosting
机制
如_sys_write()
由调试系统执行
printf() fput() _sys_wite()输出设备
其他函数 其他函数
C 库函数
调试辅助环境
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
{ /* e.g. write a character to an UART */
char tempch = ch;
sendchar(&tempch); // UART driver
return ch;
}
代码中的函数sendchar() 假定是系统的串口设备驱动函数.只要新建函数
fput() 的接口符合标准,经过编译连接后,该函数实现就覆盖了原来缺省的函数
体,所有对该函数的调用,其行为都被新实现的函数所重新定向了.
具体哪些库函数是跟目标相关的,这些函数之间的相互调用关系等,请参考
具体的编译器说明.
3.Semihosting (半主机) 机制
上面提到许多库函数在调试环境下的实现都调用了一种叫semihosting的机
制.Semihosting具体来讲是指一种让代码在ARM 目标上运行,但使用运行了
ARM 调试器的主机上I/O 设备的方法;也就是让ARM 目标将输入/ 输出请求
从应用程序代码传递到运行调试器的主机的一种机制.通常这些输入/输出设备
包括键盘,屏幕和磁盘I/O.
半主机由一组已定义的SWI 操作来实现.库函数调用相应的SWI(软件中
断),然后调试代理程序处理SWI 异常,并提供所需的与主机之间的通讯.
图-5:Semihosting的实现过程
多数情况下,半主机SWI 是由库函数内的代码调用的.但是应用程序也可
以直接调用半主机SWI.半主机SWI 的接口函数是通用的.当半主机操作在硬
件仿真器,指令集仿真器,RealMonitor或Angel下执行时,不需要进行移植处
理.
使用单个SWI 编号请求半主机操作.其它的SWI 编号可供应用程序或操
printf()
printf("Hello world! ");
SWI
调试器
Hello world!
C 库代码
应用程序代码
与运行在主机上的调试器通信
主机屏幕上显示的文本
目标
主机
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
作系统使用.用于半主机的SWI号是:
在ARM 状态下:0x123456
在Thumb 状态下:0xAB
SWI 编号向调试代理程序指示该SWI 请求是半主机请求.要辨别具体的操
作类型,用寄存器r0 作为参数传递.r0 传递的可用半主机操作编号分配如下:
0x00-0x31:这些编号由ARM 公司使用,分别对应32个具体的执行函
数.
0x32-0xFF:这些编号由ARM 公司保留,以备将来用作函数扩展.
0x100-0x1FF:这些编号保留给用户应用程序.但是,如果编写自己的
SWI 操作,建议直接使用SWI指令和SWI编号,而不要使用半主机
SWI 编号加这些操作类型编号的方法.
0x200-0xFFFFFFFF:这些编号未定义.当前未使用并且不推荐使用这
些编号.
半主机SWI使用的软件中断编号也可以由用户自定义,但若是改变了缺省
的软中断编号,需要:
更改系统中所有代码(包括库代码)的半主机SWI 调用
重新配置调试器对半主机请求的捕捉与相应
这样才能使用新的SWI 编号.
有关半主机SWI处理函数实现的更详细信息,请参考ARM编译器的相关
文档.
4.应用环境的初始化和根据目标系统资源进行的移植
在下一期中介绍应用环境和目标系统的初始化.
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
基于ARM的嵌入式系统程序开发要点(二)
—— 系统的初始化过程
基于ARM的芯片多数为复杂的片上系统集成(SoC),这种复杂的系统里多
数的硬件模块都是可配置的,需要由软件来设置其需要的工作状态.因此在用户
的应用程序启动之前,需要有专门的一段启动代码来完成对系统的初始化.由于
这类代码直接面对处理器内核和硬件控制器进行编程,一般都使用汇编语言.系
统启动程序所执行的操作跟具体的目标系统和开发系统相关,一般通用的内容包
括:
中断向量表
初始化存储器系统
初始化堆栈
初始化有特殊要求的端口,设备
初始化应用程序执行环境
改变处理器模式
呼叫主应用程序
1.中断向量表
ARM要求中断向量表必须放置在从0地址开始,连续8×4字节的空间内
(ARM720T和ARM9/10及以后的ARM处理器也支持从0xFFFF0000开始的高
地址向量表,在本文的其他地方对此不再另加说明).各个中断矢量在向量表中
的位置分配如下图:
图1:中断向量表
每当一个中断发生以后,ARM处理器便强制把PC指针置为向量表中对应中
Reset 复位中断 0x00
Undef 未定义指令中断 0x04
Software Interrupt 软件中断 0x08
Prefetch Abort 指令预取异常 0x0C
Data Abort 数据异常 0x10
(Reserved) 保留 0x14
IRQ 普通外部中断 0x18
FIQ 外部快速中断 0x1C
… …
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
断类型的地址值.因为每个中断只占据向量表中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.存储器地址分布(memory map)
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
有些系统具有非常灵活的存储器地址分配特性,进行存储器初始化设计的时
候一定要根据应用程序的具体要求来完成地址分配.
一种典型的情况是启动ROM的地址重映射(remap).如前面第1节所述,
当一个系统上电后程序将自动从0地址处开始执行,因此在系统的初始状态,必
须保证在0地址处存在正确的代码,即要求0地址开始处的存储器是非易性的
ROM或Flash等.但是因为ROM或Flash的访问速度相对较慢,每次中断发生
后都要从读取ROM或Flash上面的向量表开始,影响了中断响应速度.因此有
的系统便提供一种灵活的地址重映射方法,可以把0地址重新指向到RAM中去.
在这种地址映射的变化过程当中,程序员需要仔细考虑的是程序的执行流程不能
被这种变化所打断.比如下面这种情况:
图2:启动ROM的地址重映射对程序执行流程的影响
系统上电后从Flash内的0地址开始执行,启动代码位于地址0x100开始的
空间,当执行到地址0x200时,完成了一次地址的重映射,把原来0开始的地址
空间由Flash转给了RAM.接下去执行的指令(这里为了简化起见,忽略流水
线指令预取的模型)将来自从0x204开始的RAM空间.如果预先没有对RAM
内容进行正确的设置,则里面的数据都是随机的,这样处理器在执行完0x200
地址处的指令之后,再往下取指执行就会出错.解决的方法就是要使RAM在使
用之前准备好正确的内容,包括开头的向量表部分.
有的系统不具备存储器地址重映射的功能,所有的空间地址就相对简单一
些,不需要考虑这方面的问题.
3.初始化堆栈
因为ARM处理器有7种执行状态,每一种状态的堆栈指针寄存器(SP)都
Flash
0x0100
(Reset_Handler)
B Reset_Handler
… …
.
.
.
(boot code)
.
.
(remap)
0x0000
0x0200
RAM
0x0200
remap 0x0204
Vector Table
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
是独立的(System和User模式使用相同的SP寄存器).因此对程序中需要用到
的每一种模式都要给SP寄存器定义一个堆栈地址.方法是改变状态寄存器CPSR
内的状态位,使处理器切换到不同的状态,然后给SP赋值.注意不要切换到User
模式进行User模式的堆栈设置,因为进入User模式后就不能再操作CPSR回到
别的模式了.可能会对接下去的程序执行造成影响.
一般堆栈的大小要根据需要而定,但是要尽可能给堆栈分配快速和高带宽的
存储器.堆栈性能的提高对系统整体性能的影响是非常明显的.
这是一段堆栈初始化的代码示例,其中只定义了三种模式的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 ; 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.初始化应用程序执行环境
一个简单的可执行程序的映像结构通常如下:
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
图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数据拷贝的目标地址.
ZI (Zero initialized R/W Data)
RW (R/W Data)
RO (Code + RO Data)
编译结果
定义时带初始值的全局变量
只定义了变量名的全局变量
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
|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:处理器模式变换过程
在最后阶段才把模式转换到最终应用程序运行所需的模式,一般是用户模
式.
内核级的中断使能(CPSR的I,F位状态)也可以考虑在这一步进行.如果
系统中另外存在一个专门的中断控制器(多数情况下是这样的),这么做总是安
全的,否则就需要考虑过早地打开中断可能带来的问题,比如在系统初始化完成
之前就触发了有效中断,导致系统的死机.
7.呼叫主应用程序
当所有的系统初始化工作完成之后,就需要把程序流程转入主应用程序.最
简单的一种情况是:
复位后的缺省模式 注意不要进入用户模式 用户选择
(堆栈初始化阶段)
超级模式
(Supervisor)
多种特权模式
变化
设置成用户程
序运行模式
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
IMPORT main ; get the label main if main() is defined in other files
B man ; jump to main()
直接从启动代码跳入应用程序主函数入口,主函数名字可由用户自己定义.
在ARM ADS环境中,还另外提供了一套系统级的呼叫机制.
IMPORT __main
B __main
__main()
图5:在应用程序主函数之前插入__main
__main() 是编译系统提供的一个函数,负责完成库函数的初始化和第5节中
所描述的功能,最后自动跳向main() 函数.这种情况下用户程序的主函数名字
必须得是main.
用户可以根据需要选择是否使用__main().如果想让系统自动完成系统调用
(如库函数)的初始化过程,可以直接使用__main();如果所有的初始化步骤都
是由用户自己显式地完成,则可以跳过__main().
当然,使用__main() 的时候,可能会涉及到一些库函数的移植和重定向问
题.在__main() 里面的程序执行流程如下图所示:
图6:有系统调用参与的程序执行流程
关于在__main() 里面调用到的库函数说明,可以参阅相关的编译器文档,
库函数移植和重定向的方法,可以参考上一期文章里面的相关章节.
Image Entry Point
__main
·copy code and data
·zero initialize
__rt_entry
·initialize library functions
·call top-level constructors
(C++)
·Exit from application
·
Reset handler
·user boot code
User application
·main
__User_initial_stackheap
·set up stack & heap
启动代码 应用程序初始化用户应用程序main()
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
基于ARM的嵌入式系统程序开发要点(三)
—— 如何满足嵌入式系统的灵活需求
因为嵌入式应用领域的多样性,每一个系统都具有各自的特点.在进行系统
程序设计的时候,一定要进行具体分析,充分利用这些特点,扬长避短.
结合ARM架构本身的一些特点,在这里讨论几个常见的要点.
1.ARM还是Thumb
在讨论ARM还是Thumb之前,先说明ARM内核型号和ARM结构体系之
间的区别和联系.
图-1 ARM结构体系和处理器家族的演变发展
如图-1所示,ARM的结构体系主要从版本4开始,发展到了现在的版本6,
结构体系的变化,对程序员而言最直接的影响就是指令集的变化.结构体系的演
变意味着指令集的不断扩展,值得庆幸的是ARM结构体系的发展一直保持了向
上兼容,不会造成老版本程序在新结构体系上的不兼容.
在图中的横坐标上,显示了每一个体系结构上都含有众多的处理器型号,这
是在同一体系结构下根据硬件配置和存储器系统的不同而作的进一步细分.需要
注意的是通常我们用来区分ARM处理器家族的ARM7,ARM9或ARM10,可
能跨越不同的体系结构.
在ARM的体系结构版本4与5中,还可以再细分出几个小的扩展版本:V4T,
V5TE和V5TEJ,其区别如图-2中所示,这些后缀名也反映在各自拥有的处理器
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
型号上面,可以进行直观的分辨.V6结构体系因为包含了以前版本的所有特性,
所以不需要再进行分类.
图-2 结构体系特征
上面介绍了整个ARM处理器家族的分布,主要是说明在一个特定的平台上
编写程序的时候,一定要先弄清楚目标的特性和一些细微的差别,特别是需要具
体优化特征的时候.
从ARM体系结构V4T以后,最大的变化是增加了一套16位的指令集——
Thumb.到底在一个具体应用中要否采用Thumb呢 首先我们来分析一下ARM
和Thumb各自的特点和优势.先看下面一张性能分析图:
图-3 ARM和Thumb指令集的比较
图中的纵坐标是测试向量Dhrystone在20MHz频率下运行1秒钟的结果,其
值越大表明性能越好;横坐标是系统存储器系统的数据总线宽度.结果表明:
(a) 当系统具有32位的数据总线宽度时,ARM比Thumb有更好的性能表现.
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
(b) 当系统的数据总线宽度小于32位时,Thumb比ARM的性能更好.
由此可见,并不是32位的ARM指令集性能一定强于16位的Thumb指令集,
要具体情况具体分析.考察个中的原因,其实不难发现,因为当在一个16位存
储器系统里面取1条32位指令的时候,需要耗费2个存储器访问周期;比之32
位的系统,其速度正好大概下降一半左右.而16位指令在32位存储器系统或
16位存储器系统里的表现基本相同.正是存储器造成的系统瓶颈导致了这个有
趣的差别.
除了在窄带宽系统里面的性能优势外,Thumb指令的另外一个好处是代码尺
寸.同样一段C代码,用Thumb指令编译的结果,其长度大约只占ARM编译
结果的65%左右,可以明显地节省存储器空间.在大多数情况下,紧凑的代码和
窄带宽的存储器系统,还会带来功耗上的优势.
当然,如果在32位的系统上面,并且对系统性能要求很高的情况下,ARM
是一个更好的选择.毕竟在这种情况下,只有32位的指令集才能完全发挥32
位处理器的优势来.
因此,选择ARM还是Thumb,需要从存储器开销和性能要求两方面加以权
衡考虑.
2.堆栈的分配
在图-3中,横坐标上还有一种情况,就是16位的存储器宽度,但是堆栈空
间是32位的.这种情况下无论ARM还是Thumb,其性能表现都比单纯的16位
存储器系统情况下要好.这是因为ARM和Thumb其指令集虽然分32位和16
位,但是堆栈全部是采用32位的.因此在16位堆栈和32位堆栈的不同环境下,
其性能当然都会相差很多.这种差别还跟具体的应用程序密切相关,如果一个程
序堆栈的使用频率相当高,则这种性能差异很大;反之则要小一些.
在基于ARM的系统中,堆栈不仅仅被用来进行诸如函数调用,中断响应等
时候的现场保护,还是程序局部变量和函数参数传递(如果大于4个)的存储空
间.所以出于系统整体性能考虑,要给堆栈分配相对访问速度最快,数据宽度最
大的存储器空间.
一个嵌入式系统通常存在多种多样的存储器类型.设计的时候一定要先清楚
每一种存储器的访问速度,地址分配和数据线宽度.然后根据不同程序和目标模
块对存储器的不同要求进行合理分配,以期达到最佳配置状态.
3.ROM还是RAM在0地址处
显然当系统刚启动的时候,0地址处肯定是某种类型的ROM,里面存储了系
统的启动代码.但是很多灵活的系统设计中,0地址处的存储器类型是可映射的.
也就是说,可以通过软件的方法,把别的存储器(主要是快速的RAM)分配以
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
0起始的地址.
这种做法的最主要目的之一是提高系统对中断的反应速度.因为每一个中断
发生的时候,ARM都需要从0地址处的中断向量表开始其中断响应流程,显然
把中断向量表放在RAM里,比放在ROM里有更快的访问速度.因此,如果系
统提供了这一类的地址重映射功能,软件设计者一定要加以利用.
下面是一个典型的经过0地址重映射之后的存储空间分布图,注意尽可能把
速度要求最高的部分放置在系统里面访问速度最快,带宽最宽的RAM里面.
图-4 系统存储器分布的实例
4.存储器地址重映射(memory remap)
存储器地址重映射是当前很多先进控制器所具有的功能.在上一节中已经提
到了0地址处存储器重映射的例子,简而言之,地址重映射就是可以通过软件配
置来改变一块存储器物理地址的一种机制或方法.
当一段程序对运行自己的存储器进行重映射的时候,需要特别注意保证程序
执行流程在重映射前后的承接关系.下面是一种典型的存储器地址重映射情况:
Peripherals
RO
Reset Handler
Heap
RW/ZI
Stack
Exception Handlers
Vector Table
Fast32-bit RAM
16-bit RAM
Flash
0x0000 0000
0x0000 4000
0x0001 0000
0x0001 8000
0x2400 0000
0x2800 0000
0x4000 0000
可以在ROM 里运行的代码
外设寄存器
变量区和动态内存分配区
需要快速响应的代码和数据
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
图-5 存储器重映射举例1
系统上电后的缺省状态是0地址上放有ROM,这块ROM有两个地址:从0
起始和从0x10000起始,里面存储了初始化代码.当进行地址remap以后,从0
起始的地址被定向到了RAM上,ROM则只保留有唯一的从0x10000起始的地
址了.
如果存储在ROM 里的Reset_Handler一直在0 - 0x4000的地址上运行,则
当执行完remap以后,下面的指令将从RAM里预取,必然会导致程序执行流程
的中断.根据系统特点,可以用下面的办法来解决这个问题:
(1) 上电后系统从0地址开始自动执行,设计跳转指令在remap发生前使PC
指针指向0x10000开始的ROM地址中去,因为不同地址指向的是同一块
ROM,所以程序能够顺利执行.
(2) 这时候0 - 0x4000的地址空间空闲,不被程序引用,执行remap后把RAM
引进.因为程序一直在0x10000起始的ROM空间里运行,remap对运行
流程没有任何影响.
(3) 通过在ROM里运行的程序,对RAM进行相应的代码和数据拷贝,完成
应用程序运行的初始化.
下面是一段实现上述步骤的例程:
-------------------------------------------------------------------------------------------------------
ENtrY
;启动时,从0开始,设法跳转到"真"的ROM地址(0x10000开始的空间里)
LDR pc, =start
;insert vector table here

Start ;Begin of Reset_Handler
; 进行remap设置
remap
0x10000
0x4000
=
0x4000
0x0000
Reset Handler
Vectors
0x4000
0x0000
RAMROM
0x10000
0x10400
ROM ROM
0x10400
Vectors
Reset Handler
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
LDR r1, =Ctrl_reg ;假定控制remap的寄存器
LDR r0, [r1]
ORR r0, r0, #Remap_bit ;假定对控制寄存器进行remap设置
Str r0, [r1]
;接下去可以进行从ROM到RAM的代码和数据拷贝
-------------------------------------------------------------------------------------------------------
除此之外,还有另外一种常见的remap方式,如下图:
图-6 存储器重映射举例2
原来RAM和ROM各有自己的地址,进行重映射以后RAM和ROM的地址
都发生了变化,这种情况下,可以采用以下的方案:
(1) 上电后,从0地址的ROM开始往下执行.
(2) 根据映射前的地址,对RAM进行必要的代码和数据拷贝.
(3) 拷贝完成后,进行remap操作.
(4) 因为RAM在remap前准备好了内容,使得PC指针能继续在RAM里取
到正确的指令.
不同的系统可能会有多种灵活的remap方案,根据上面提到的两个例子,可
以总结出最根本的考虑是:要使程序指针在remap以后能继续往下得到正确的指
令.
5. 根据目标存储器系统分散加载映像(scatterloading)
Scatterloading文件是ARM的工具链里面的一个特性,作为程序编译过程中
给连接器使用的一个参数,用来指定最终生成的目标映像文件运行时的分布状
态.如果用户程序映像只是如图7所示的最简状态,所有的可执行代码都集合放
置在一起,那么可以不使用Scatterloading文件,直接用连接器的命令行选项就
remap
0x20000
0x4000
=
0x4000
0x0000
Reset Handler
Vectors
0x4000
0x0000
RAMROM
0x10000
0x10400
RAM ROM
0x20400
Vectors
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
能够完成设置:
RO = 0x00000:表示映像的第一条指令开始地址;
RW = 0x10000:表示变量区的起始地址,变量区一定要位于RAM区.
图-7 简单的映像分布举例
但是一个复杂的系统可能会把映像分割成几个部分.如图8,系统中存在多
种类型的存储器,不能的代码部分根据执行性能优化的考虑分布与不同的地方.
图-8 复杂的映像分布举例
这时候不能通过简单的RO,RW参数来完成实现上述配置,就要用到
scatterloading文件了.在scatterloading文件里,可以给编译出来的各个目标模块
RO
RW
ZI
Stack
Heap
RAM
Flash 代码区
变量区
0x00000
0x10000
Exception Handler
RO
Reset Handler
Heap
RW & ZI
Stack
Vector table
0x0000
0x4000
0x10000
0x18000
0x20000
0x28000
32-bit fast RAM
16-bit RAM
Flash
性能要求最苛刻的部分
变量区和动态内存分配区
普通程序区
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
指定运行地址,下面的例子是针对图8的.
FLASH 0x20000 0x8000
{
FLASH 0x20000 0x8000
{
init.o (Init, +First)
* (+RO)
}
32bitRAM 0x0000
{
vectors.o (Vect, +First)
handlers.o (+RO)
}
STACK 0x1000 UNINIT
{
stackheap.o (stack)
}
:
:
16bitRAM 0x10000
{
* (+RW,+ZI)
}
HEAP 0x15000 UNINIT
{
stackheap.o (heap)
}
}
关于scatterloading文件的详细语法,请参阅ARM公司的相关手册.
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
基于ARM的嵌入式系统程序开发要点(四)
—— 异常处理机制的设计
异常或中断是用户程序中最基本的一种执行流程或形态,这部分对ARM架
构下异常处理程序的编写作一个全面的介绍.
ARM一共有7种类型的异常,按优先级从高到低排列如下:
Reset
Data Abort
FIQ
IRQ
Prefetch Abort
SWI
Undefined instruction
请注意在ARM的文档中,使用术语exception 来描述异常.Exception主要
是从处理器被动接受异常的角度出发描述,而interrupt带有向处理器主动申请的
色彩.在本文中,对"异常"和"中断"不作严格区分,都是指请求处理器打断
正常的程序执行流程,进入特定程序循环的一种机制.
1.异常响应流程
如以前介绍异常向量表时所提到过的,每一个异常发生时,总是从异常向量
表开始起跳的,最简单的一种情况是:
图-1 异常向量表
B
B
(Reserved)
B B
B
B
B
B
0x1C
0x18
0x14
0x10
0x0C
0x08
0x04
0x00
FIQ_Handler()
IRQ_Handler()
DataAbt_Handler()
PreAbt_Handler()
SWI_Handler()
Undef_Handler()
Reset_Handler()
中断处理函数
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
向量表里面的每一条指令直接跳向对应的异常处理函数.其中FIQ_Handler()
可以直接从地址0x1C处开始,省下一条跳转指令.
但是当执行跳转的时候有2个问题需要讨论:跳转范围和异常分支.
1.1 跳转范围
我们知道ARM的跳转指令(B)是有范围限制的(±32MB),但很多情况
下不能保证所有的异常处理函数都定位在向量表的32MB范围内,需要大于
32MB的长跳转,而且因为向量表空间的限制只能由一条指令完成.这可以通过
下面二种方法实现.
(a) MOV PC, #imme_value
把目标地址直接赋给PC寄存器.
但是这条指令受格式限制并不能处理任意立即数,只有当这个立即数能够
表示为一个8-bit数值通过循环右移偶数位而得到,才是合法的.例如:
MOV PC, #0x30000000 是合法的,因为0x300000000可以通过0x03循
环右移4位而得到.
而 MOV PC, #30003000 就是非法指令.
(b) LDR PC, [PC+offset]
把目标地址先存储在某一个合适的地址空间,然后把这个存储器单元上的32
位数据传送给PC来实现跳转.
这种方法对目标地址值没有要求,可以是任意有效地址.但是存储目标地址
的存储器单元必须在当前指令的±4KB空间范围内.
注意在计算指令中引用的offset数值的时候,要考虑处理器流水线中指令预
取对PC值的影响,以图-2的情况为例:
offset = address location - vector address - pipeline effect
= 0xFFC - 0x4 - 0x8
= 0xFF0
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
图-2 利Literal pool实现跳转
1.2 异常分支
ARM内核只有二个外部中断输入信号nFIQ和nIRQ,但对于一个系统来说,
中断源可能多达几十个.为此,在系统集成的时候,一般都会有一个异常控制器
来处理异常信号.
图-3 中断系统
这时候,用户程序可能存在多个IRQ/FIQ的中断处理函数,为了从向量表
开始的跳转最终能找到正确的处理函数入口,需要设计一套处理机制和方法.
图-4 中断分支
LDR PC, [PC, 0xFF0]
0x30003000
Undef_Handler()
0x00
0x04
0xFFC
32MB
0x30003000
n
1
2 多
中断源
中断
控制器
ARM
内核
nIRQ
nFIQ
外设通信
配置/获取信息
IRQ 0x14
IRQ_Handler_1()
IRQ_Handler_2()
...
...
IRQ_Handler_n()

基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
(a) 硬件处理
有的系统在ARM的异常向量表之外,又增加了一张由中断控制器控制的特
殊向量表.当由外设触发一个中断以后,PC能够自动跳到这张特殊向量表中去,
特殊向量表中的每个向量空间对应一个具体的中断源.
举例来说,下面的系统一共有20个外设中断源,特殊向量表被直接放置在
普通向量表后面.
图-5 额外的硬件异常向量表
当某个外部中断触发之后,首先触发ARM的内核异常,中断控制器检测到
ARM的这种状态变化,再通过识别具体的中断源,使PC自动跳转到特殊向量
表中的对应地址,从而开始一次异常响应.需要检查具体的芯片说明,是否支持
这类特性.
(b) 软件处理
多数情况下是用软件来处理异常分支.因为软件可以通过读取中断控制器来
获得中断源的详细信息.
图-6 软件控制中断分支
Int_20
.
.
.
Int_2
Int_1
FIQ
IRQ
.
.
Reset
0x70
0x6C
.
.
.
0x24
0x20
0x1C
0x18
.
.
0x00
Int_20_Handler()



Int_2_Handler()
Int_1_Handler()
(获取状态信息)
IRQ
… …

中断控制器
IRQ_Handler:
Switch(int_source)
{
case 1:
case 2:

}
Int_1_Handler()
Int_2_Handler()
… …
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
因为软件设计的灵活性,用户可以设计出比上图更好的流程控制方法来.下
面是一个例子:
图-7 灵活的软件分支设计
Int_vector_table是用户自己开辟的一块存储器空间,里面按次序存放异常处
理函数的地址.IRQ_Handler()从中断控制器获取中断源信息,然后再从
Int_verctor_table中的对应地址单元得到异常处理函数的入口地址,完成一次异
常响应的跳转.这种方法的好处是用户程序在运行过程中,能够很方便地动态改
变异常服务内容.
2.异常处理函数的设计
2.1 异常发生时处理器的动作
当任何一个异常发生并得到响应时,ARM内核自动完成以下动作:
拷贝 CPSR 到 SPSR_
Address of Int_n_Handler()
.
.
.
Address of Int_2_Handler()
Address of Int_1_Handler()
(获取状态信息)
IRQ
… …

中断控制器
IRQ_Handler():
Switch(int_source)
{
case 1:
case 2:

case n:
}
Int_1_Handler()
Int_2_Handler()
… …
Int_vector_table
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
设置适当的 CPSR 位:
改变处理器状态进入 ARM 状态
改变处理器模式进入相应的异常模式
设置中断禁止位禁止相应中断
更新 LR_
设置 PC 到相应的异常向量
注意当响应异常后,不管异常发生在ARM还是Thumb状态下,处理器都将
自动进入ARM状态.另一个需要注意的地方是中断使能被自动关闭,也就是说
缺省情况下中断是不可重入的.单纯的把中断使能位打开接受重入的中断会带来
新的问题,在第3部分中对此会有详细介绍.
除这些自动完成的动作之外,如果在汇编级进行手动编程,还需要注意保存
必要的通用寄存器.
2.2 进入异常处理循环后软件的任务
进入异常处理程序以后,用户可以完全按照自己的意愿来进行程序设计,包
括调用Thumb状态的函数,等等.但是对于绝大多数的系统来说,有一个步骤
必须处理,就是要把中断控制器中对应的中断状态标识清掉,表明该中断请求已
经得到响应.否则等退出中断函数以后,又马上会被再一次触发,从而进入周而
复始的死循环.
2.3 异常的返回
当一个异常处理返回时,一共有3件事情需要处理:通用寄存器的恢复,状
态寄存器的恢复以及PC指针的恢复.
通用寄存器的恢复采用一般的堆栈操作指令,而PC和CPSR的恢复可以通
过一条指令来实现,下面是3个例子:
MOVS pc, lr 或 SUBS pc, lr, #4 或LDMFD sp!, {pc}^
这几条指令都是普通的数据处理指令,特殊之处就是把PC寄存器作为了目
标寄存器,并且带了特殊的后缀"S"或"^",在特权模式下,"S"或"^"的作
用就是使指令在执行时,同时完成从SPSR到CPSR的拷贝,达到恢复状态寄存
器的目的.
异常返回时另一个非常重要的问题是返回地址的确定.在2.1节中提到进入
异常时处理器会有一个保存LR的动作,但是该保存值并不一定是正确中断的返
回地址.下面以一个简单的指令执行流水状态图来对此加以说明.
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
图-8 ARM状态下3级指令流水线执行示例
我们知道在ARM架构里,PC值指向当前执行指令的地址加8处.也就是说,
当执行指令A(地址0x8000)时,PC等于指令C的地址(0x8008).假如指令
A是"BL"指令,则当执行时,会把PC(=0x8008)保存到LR寄存器里面,但
是接下去处理器会马上对LR进行一个自动的调整动作R=LR-0x4.这样,最
终保存在LR里面的是B指令的地址,所以当从BL返回时,LR里面正好是正
确的返回地址.
同样的调整机制在所有LR自动保存操作中都存在,比如进入中断响应时处
理器所做的LR保存中,也进行了一次自动调整,并且调整动作都是LR=LR-0x4.
由此我们来对不同异常类型的返回地址进行依次比较:
假设在指令B处(地址0x8004)发生了中断响应,进入中断响应后LR上经
过调整保存的地址值应该是C的地址0x8008.
(a) 如果发生的是软件中断,即B是"SWI"指令
从SWI中断返回后下一条执行指令就是C,正好是LR寄存器保存的地址,
所以只要直接把LR恢复给PC.
(b) 如果发生的是"IRQ"或"FIQ"等指令
因为外部中断请求中断了B指令的执行,当中断返回后,需要重新回到B
指令的执行,也就是返回地址应该是B(0x8004),需要把LR减4.
(c) 如果发生的是"Data Abort"
在B上进入数据异常的响应,但导致数据异常的原因却应该是上一条指令A.
当中断处理程序修复数据异常以后,要回到A上重新执行导致数据异常的指令,
因此返回地址应该是LR减8.
如果原来的指令执行状态是Thumb,异常返回地址的分析与此类似,对LR
的调整正好与ARM状态完全一致.
2.4 ARM编译器对异常处理函数编写的扩展
F D E
F D E
F D E
F D E
0x8000 A
0x8004 B
0x8008 C
0x800C D
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
考虑到异常处理函数在现场保护和返回地址的处理上与普通函数的不同之
处,不能直接把普通函数体连接到异常向量表上,需要在上面加一层封装,下面
是一个例子:
IRQ_Handler ;中断响应,从向量表直接跳来
STMFD SP!, {R0-R12, LR} ;保护现场,一般只需保护{r0-r3,lr}即可
BL IrqHandler ;进入普通处理函数,C或汇编均可
LDMFD SP!, {R0-R12, LR} ;恢复现场
SUBS PC, LR, #4 ;中断返回,注意返回地址
为了方便使用高级语言直接编写异常处理函数,ARM编译器对此作了特定
的扩展,可以使用函数声明关键字__irq,这样编译出来的函数就满足异常响应
对现场保护和恢复的需要,并且自动加入对LR进行减4的处理,符合IRQ和
FIQ中断处理的要求.
__irq void IRQ_Handler (void)
{…}
2.5 软件中断处理
软件中断由专门的软中断指令SWI触发,SWI指令后面跟一个中断编号,
以标识可能共存的多个软件中断程序.
图-9 软件中断处理流程
在C程序中调用软件中断需要用到编译器的扩展功能,使用关键字"__swi"
来声明中断函数.注意软中断号码同时在函数定义时指定.
__swi(0x24) void my_swi (void);
这样当调用函数my_swi的时候,就会用"SWI 0x24"来代替普通的函数调
用"BL my_swi".
分析图9的流程,可以发现软件中断同样存在着中断分支的问题,即需要根
据中断号码来决定调用不同的处理程序.软中断号码只存在于SWI指令码当中,
因此需要在中断处理程序中读取触发中断的指令代码,然后提取中断号信息,再


SWI 0x01


用户程序(C或汇编)

CMP swi_num
BEQ …
(Optional)
异常向量表 SWI处理程序(汇编)
SWI处理程序(C)
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
进行进一步处理.下面是软中断指令的编码格式:
ARM状态下的SWI指令编码格式,32位长度,其中低24位是中断编号.
Thumb状态下的SWI指令编码格式,16位长度,其中低8位是中断编号.
图-10 SWI指令编码格式
为了在中断处理程序里面得到SWI 指令的地址,可以利用LR寄存器.每
当响应一次SWI的时候,处理器都会自动保存并调整LR寄存器,使里面的内
容指向SWI下一条指令的地址,所以把LR里面的地址内容上溯一条指令就是
所需的SWI指令地址.需要注意的一点是当SWI指令的执行状态不同时,其指
令地址间隔不一样,如果进入SWI执行前是在ARM状态下,需要通过LR-4来
获得SWI指令地址,如果是在Thumb状态下进入,则只要LR-2就可以了.
下面是一段提取SWI中断号码的例程:
MRS R0, SPSR ;检查进入SWI响应前的状态
TST R0, #T_bit ;是ARM还是Thumb #T_bit=0x20
LDRNEH R0, [LR, #-2] ;是Thumb,读回SWI指令码
BICNE R0, R0, #0xff00 ;提取低8位
LDREQ R0, [LR, #-4] ;是ARM,读回SWI指令码
BICEQ R0, R0, #0xff000000 ;提取低24位
;寄存器R0中的内容是正确的软中断编号了
3.可重入中断设计
如2.1节所述,缺省情况下ARM中断是不可重入的,因为一旦进入异常响
应状态,ARM自动关闭中断使能.如果在异常处理过程中简单地打开中断使能
而发生中断嵌套,显然新的异常处理将破坏原来的中断现场而导致出错.但有时
候中断的可重入又是需要的,因此要能够通过程序设计来解决这个问题.其中有
二点是这个问题的关键:
(a) 新中断使能之前必须要保护好前一个中断的现场信息,比如LR_irq和
SPSR_irq等,这一点容易想到也容易做到.
(b) 中断处理过程中对BL的保护
28 24 27
SWI number
23
15 8 7 0
1 1 0 1 1 1 1 1 SWI number
31
Cond 1 1 1 1
0
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
在中断处理函数中发生函数调用(BL)是很常见的,假设有下面一种情况:
IRQ_Handler:

BL Foo -----------> Foo:
ADD … STMFD SP!, {R0-R3, LR}
… …
LDMFD SP!, {R0-R3, PC}
上述程序,在IRQ处理函数IRQ_Handler() 中调用了函数Foo(),这是一个
普通的异常处理函数.但若是在IRQ_Handler() 里面中断可重入的话,则可能发
生问题,考察下面的情况:
当新的中断请求恰好在"BL Foo"指令执行完成后发生.
这时候LR寄存器(因在IRQ模式下,是LR_irq)的值将调整为BL指令的
下一条指令(ADD)地址,以期能从Foo() 正确返回;但是因为这时候发生了
中断请求,接下去要进行新中断的响应,处理器为了能使新中断处理完成后能正
确返回,也将进行LR_irq保存.因为新中断是在指令流
BL Foo --> STMFD SP!, {R0-R3, LR}
执行过程中插入的,完成跳转操作后,进行流水线刷新,最后LR_irq保存的是
STMFD后面一条指令的地址;这样当新中断利用(PC = LR - 4)操作返回时,
正好可以继续原来的流程执行STMFD指令.这二次对LR_irq的操作发生了冲
突,当新中断返回后往下执行STMFD指令,这时候压栈的LR已不是原来需要
的ADD指令的地址,从而使子程序Foo() 无法正确返回.
这个问题无法通过增加额外的现场保护指令来解决.一个巧妙的办法是在重
新使能中断之前改变处理器的模式,也就是使上面程序中的"BL Foo"指令不
要运行在IRQ模式下.这样当新中断发生时就不会造成LR寄存器的冲突了.考
虑ARM的所有运行模式,采用System模式是最恰当的,因为它既是特权模式,
又与中断响应无关.
所以一个完整的可重入中断应该遵循以下流程:
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
图-11 可重入中断处理流程
下面是一段实现的例程:
保护寄存器R,SPSR等
与中断控制器通信(需要的话)
切换到System状态,开中断使能
中断处理(现在中断可重入)
关闭中断使能,切换回IRQ状态
恢复寄存器C,CPSR等
进入普通不可重入中断处理
结束一次可重入中断处理
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
基于ARM的嵌入式系统程序开发要点(五)
—— ARM/Thumb的交互工作
在前面的文章中提到过,很多情况下应用程序需要在ARM跟Thumb状态之
间相互切换,这部分就讨论交互工作的实现方法和一些注意问题.
1. 需要交互的原因
前面提到过Thumb指令在某些特殊情况下具有比ARM指令更为出色的表
现,主要是在代码长度和窄带宽存储器系统性能两方面.正因为Thumb指令在
特定环境下面的优势,它在很多方面得到了广泛的应用.但是因为下面一些原因,
Thumb又不可能独立地组成一个应用系统,所以不可避免地会产生ARM与
Thumb之间交互的问题.
Thumb指令集在功能上只是ARM指令集的一个子集,某些功能只能在
ARM状态下执行,如CPSR和协处理器的访问.
进行异常响应时,处理器会自动进入ARM状态.
从系统优化考虑,在宽带存储器上不应该放置Thumb代码,很多窄带
系统具有宽带的内部存储器.
即使是一个单纯的Thumb应用系统,也必须加一个汇编的交互头程序,
因为系统总是自动从ARM开始启动.
2. 状态切换的实现
处理器在ARM/Thumb之间的状态切换是通过一条专用的跳转交换指令BX
来实现的.BX指令以通用寄存器(R0-R15)为操作数,通过拷贝Rn到PC来
实现4GB空间范围内的一个绝对跳转. BX利用Rn寄存器中存储的目标地址值
的最后一位来判断跳转后的状态.
图-1 BX指令实现状态切换
0 31
Rn
PC
BX
ARM/Thumb选择位:
0 - ARM
1 - Thumb
BX Rn
当前状态是Thumb时
BX{Cond.} Rn
当前状态是ARM时
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
无论ARM还是Thumb,其指令存储在存储器中都是边界对齐的(4-Byte或
2-Byte对齐),所以在执行跳转过程中,PC寄存器中的最低位肯定被舍弃,不起
作用.在BX指令的执行过程中,最低位正好被用作状态判断的标识,不会造成
存储器访问不对齐的错误.
图2中是一段直接进行状态切换的例程:
图-2 ARM/Thumb交互工作的例子
我们知道ARM的状态寄存器CPSR中,bit-5是状态控制位T-bit,决定当前
处理器的运行状态.如果直接修改CPSR的状态位,也能够达到改变处理器运行
状态的目的,但是会带来一个问题.因为ARM采用了多级流水线的结构,所以
在程序执行过程中指令流水线上会存在几条预取指令(具体数目视流水线级数而
不同).当修改CPSR的T-bit以后,状态的转变会造成流水线上预取指令执行的
错误.而如果用BX指令,则执行后会进行流水线刷新动作,清除流水线上的残
余指令,在新的状态下重新开始指令预取,从而保证状态转变时候指令流的正确
衔接.
3. ARM/Thumb之间的函数调用
在无交互的子程序调用中,其过程比较简单.实现调用通常只需要一条指
令:
BL function
实现返回也只需要从LR恢复PC即可:
MOV PC, LR
;从ARM状态开始
CODE32 ;汇编关键字
ADR R0, Into_Thumb+1 ;得到目标地址,末位置1,转向Thumb
BX R0 ;执行
… ;其他代码
CODE16 ;汇编关键字
Into_Thumb ;Thumb代码段起始地址
… ;Thumb代码
ADR R5, Back_to_ARM ;得到目标地址,末位缺省为0,转向ARM
BX R5 ;执行
… ;其他代码
CODE32 ;汇编关键字
Back_to_ARM ;ARM代码段起始地址

基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
如下图所示:
图-3 普通函数调用
如果子函数和父函数不是在同一种状态下执行的,因为状态切换,需要对
函数调用作更多的考虑.
(a) BL不能完成状态切换,需要由BX来切换状态.
(b) BX不能自动保存返回地址到LR,需要在BX之前先保存好LR.
(c) 用"BX LR"来返回,不能使用"MOV PC, LR",因为这条指令同
样不能实现状态切换.返回时要仔细考虑保存的LR中最低位内容是否
正确.
假如用户直接使用汇编进行状态交互跳转,上述的几个问题都需要用手工
编码加以处理.如果用户使用高级语言进行开发,不需要为ARM/Thumb之间的
相互调用增加额外的编码,但是最好要对其调用过程加以了解.下面以ARM ADS
中的编译工具为例进行说明(图4).
(a) 两个函数func1()和func2()被编译成了不同的指令集(ARM或Thumb).
注意func1()和func2()在这里位于二个不同的源文件.
(b) 编译时必须告诉编译器和连接器足够的信息,一方面让编译器能够使用
正确的指令码进行编译,另一方面这样当在不同的状态之间发生函数调
用时,连接器将插入一段连接代码(veneers)来实现状态转换.
图-4 不同状态间函数调用的示例
func1
连接器生成
连接代码
File2.c File1.c
Void func1(void)
{

func2();

}
.
.
.
BL
.
.
.
.
.
BX
func2
. .
.
BX
Void func2(void)
{


}
func2
func1
Void func1(void)
{

func2();

}
.
.
.
BL func2
.
.
.
.
.
MOV PC, LR
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
上述过程中的一个特点是func1还是使用通常的BL指令来进行子程序调用,
而func2返回时则直接使用"BX LR",没有对LR进行判断和最低位的设置.
这是因为当执行BL指令对LR进行保存时,其最低位会被自动设置,以满足返
回时状态切换的需要,可直接使用"BX LR".
在上面的例子中,为了让编译器在编译函数func2时使用BX而不是BL进
行返回,必须告诉编译器要按照满足交互工作要求的方式进行编译.在ARM的
编译器选项设置中,是"-apcs /interwork".这样,函数的返回指令会被正确设置,
并且当连接器进行目标代码的连接时,能够在需要的地方插入正确的连接代码实
现状态切换.
当然,插入了连接代码会相应地增加代码长度,通常一段veneer包含3条指
令,即12B字节长度.可以用"-info veneers"选项使连接器输出所有veneers
的位置和长度信息.
4. 交互程序之间的兼容性
正因为在指定交互选项后编译及连接后的输出代码跟在无交互情况下不同,
所以当多个源文件如果使用了不同的设置进行编译,相互之间的调用可能产生兼
容性问题.下面这张图说明了这些关系:
图-5 不同编译方式下函数调用的兼容性
在一个使用交互工作的项目工程管理中,对此要加以仔细考虑.
5. V5架构的扩展
ARM在V5版本的架构中,对ARM/Thumb的交互增加了新的支持.针对前
面第3节中提到的函数调用和返回问题,V5版本中专门对指令做了扩展.
(a) 增加了新指令BLX,解决了原来BX和BL指令各自的欠缺.使交互的
函数调用可以由一条指令实现,省略了跳转代码的开销.
(b) 扩展了以PC为目标地址的数据传输指令功能.PC加载值的最低位将被
非交互的
Thumb 代码
交互的
Thumb 代码
交互的
ARM 代码
非交互的
ARM 代码
允许非交互的调用交互的
不可调用
允许交互调用
允许ARM-ARM调用 允许Thumb-Thumb调用
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
自动送到状态寄存器CPSR的T状态位.也就是说通过给PC赋值的方法也能实
现状态的切换了,这样就使习惯的函数返回方法——从堆栈中恢复寄存器的方
法,也能实现交互调用函数的正确返回了.
所以,V5架构以后的代码,不再需要额外的连接代码(veneers)了,帮助
缩小代码长度,提高状态切换时候的执行效率.当然,在V5及以后架构中,继
续保持对以前代码的良好兼容性.
6. Thumb-2
ARM和Thumb因为各自的优势都得到了极为广泛的应用,在一个应用程序
中,用户要根据系统的具体情况灵活分配,使用不同的编译器,把不同的代码编
译成ARM或Thumb,以希望得到最优的代码长度和性能平衡.这样做能够达到
系统优化的目的,但是也给设计人员带来了额外的交互处理工作.最近ARM公
司公布了一项新的发明——Thumb-2指令集,该指令集同时包含32位和16位指
令,在代码长度和性能之间作了最佳的平衡;这样,以后用户就可以用一个统一
的Thumb-2编译器来解决现在面临的很多问题了.
下图是Thumb-2指令集跟ARM和Thumb之间的比较.
0
20
40
60
80
100
Code Size
ARM
Thumb-2
Thumb
0
20
40
60
80
100
Performance
ARM
Thumb-2
Thumb
图-6 ARM,Thumb和Thumb-2的比较
新的指令集将在最新的ARM授权中发布,更多信息请访问www.arm.com.
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
基于ARM的嵌入式系统程序开发要点(六)
—— 开发高效程序的技巧
开发高效率的程序涉及很多方面,包括编程风格,算法实现,针对目标的特
殊优化等等.这部分主要从ARM的体系结构特点出发,介绍几个程序开发中的
注意点.如何根据目标硬件的存储器配置,对运行程序的映像文件进行优化布局
的方法,在前面的专题中已经有过介绍.
1. 变量定义
变量定义虽然很简单,但是也有很多值得注意的地方.先看下面一个例子:
这里定义的4个变量形式都一样,只是次序不同,却导致了在最终的映像中不同
的数据布局,见图1所示.显然,第二种方式节约了更多的存储器空间.
图1:变量在数据区里的布局
由此可见在变量声明的时候,需要考虑怎样最佳地控制存储器布局.当然,
编译器在一定程度上能够优化这类问题,但是最好的方法还是在编程的时候,把
所有相同类型的变量放在一起定义.
第二个问题是局部变量的类型定义.一般情况下人们总是设法使用short或
char来定义变量以节省存储器空间;但是,当一个函数的局部变量数目有限的情
况下,编译器会把局部变量分配给内部寄存器,每个变量占用一个寄存器.这样,
使用short和char型变量不但起不到节省空间的作用,还会带来其他的副作用,
请看图2.假定a1是任意可能的寄存器,存储函数的局部变量.同样完成加一
的操作,32位的int型变量最快,只用一条加法指令.而8位和16位变量,完
成加法操作后,还需要在32位的寄存器中进行符号扩展,其中带符号的变量,
要用逻辑左移(LSL)接算术右移(ASR)两条指令才能完成符号扩展;无符号
的变量,要使用一条逻辑与(AND)指令对符号位进行清零.所以,使用32位
的int或unsigned int局部变量最有效率.某些情况下,函数从外部存储器读入局
部变量进行计算,这时候,往往值得先把不是32位的变量转换成32位(至于把
8位或16位变量扩展成32位后,隐藏了原来可能的溢出异常这个问题,需要进
char a;
short b;
char c;
int d;
char a;
char c;
short b;
int d;
a pad b
pad c
d
d
a c b
(Pad: 无意义的填充数据)
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
一步的仔细考虑).
图2:不同类型局部变量的编译结果
变量定义中还有一个与习惯思维相悖的地方是冗于局部变量的使用.一般情
况下程序员总是竭力避免使用冗余变量,以精简程序.通常情况下这是正确的,
但是也有例外,请看下面一个例子:
int f(void);
int g(void); // f()和 g()不访问全局变量errs
int errs; // 全局变量
void test1(void)
{ errs += f();
errs += g();
}
void test2(void)
{ int localerrs = errs; // 定义冗余的局部变量
localerrs += f();
localerrs += g();
errs = localerrs;
}
在第一种情况test1()里,每次访问全局变量errs时都要先从相应的存储器
load到寄存器里,经f()或g()函数调用后再store回原来的存储器里面,在
这个例子里一共要进行两次这样的load/store操作.而在第二种情况test2()里,
局部变量localerrs被分配以寄存器,这样一来,整个函数就只需要一次load/store
全局变量存储器了;能够节省存储器访问的次数对于系统性能的提高是非常有好
处的.
int wordinc (int a)
{ return a + 1;
}
short shortinc (short a)
{ return a + 1;
}
char charinc (char a)
{ return a + 1;
}
wordinc
ADD a1,a1,#1
MOV PC,LR
shortinc
ADD a1,a1,#1
MOV a1,a1,LSL #16
MOV a1,a1,ASR #16
MOV PC,LR
charinc
ADD a1,a1,#1
AND a1,a1,#&ff
MOV PC,LR
基于ARM的嵌入式程序开发要点
ARM-CHINA-040415A
这个例子说明了增加局部变量可以减少存储器的访问.
2. 参数传递
在ARM的工具链里,定义了统一的函数过程调用标准AT P C S(ARM-Thumb
Procedure Call Standard).AT P C S定义了寄存器组中的{R0 - R3}作为参数传递和
结果返回寄存器,如果参数数目超过四个,则使用堆栈进行传递.我们知道内部
寄存器的访问速度是远远大于存储器的,所以要尽量使参数传递在寄存器里面进
行,即应尽量控制函数的参数在四个以下.这是理解AT P C S后应该实现的一种
编程风格.但是,利用AT P C S我们还可以得到更多,我们可以用它来实现C与
汇编之间直接的函数调用.见图3中的例子:
图3:利用AT P C S编写汇编函数
这个例子中的函数strcopy(dest, src)用汇编来实现,根据AT P C S的定义,
函数参数从左到右由寄存器进行传递,所以在汇编中可以直接由R0和R1进行
引用.有了这条途径,在C和汇编之间进行相互调用就容易实现了.
3. 循环条件
记数循环是程序中十分常用的流程控制结构.在C中,类似下面的for循环
比比皆是:
for (loop = 1; loop

         
          [/td]
        [/tr]
      
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
收藏收藏 分享分享 支持支持 反对反对
您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|文字版|手机版|DIY编程器网 ( 桂ICP备14005565号-1 )

GMT+8, 2025-7-28 00:25 , 耗时 0.098872 秒, 18 个查询请求 , Gzip 开启.

各位嘉宾言论仅代表个人观点,非属DIY编程器网立场。

桂公网安备 45031202000115号

DIY编程器群(超员):41210778 DIY编程器

DIY编程器群1(满员):3044634 DIY编程器1

diy编程器群2:551025008 diy编程器群2

QQ:28000622;Email:libyoufer@sina.com

本站由桂林市临桂区技兴电子商务经营部独家赞助。旨在技术交流,请自觉遵守国家法律法规,一旦发现将做封号删号处理。

快速回复 返回顶部 返回列表