第10章 ARM程序设计基础final.docx
- 文档编号:15724568
- 上传时间:2023-07-07
- 格式:DOCX
- 页数:30
- 大小:32.31KB
第10章 ARM程序设计基础final.docx
《第10章 ARM程序设计基础final.docx》由会员分享,可在线阅读,更多相关《第10章 ARM程序设计基础final.docx(30页珍藏版)》请在冰点文库上搜索。
第10章ARM程序设计基础final
第10章ARM高级语言程序设计基础
10.1C与汇编的混合编程
在应用系统的程序设计中,如果所有的编程任务都用汇编语言来完成,其工作量是很大的,同时不利于系统设计或应用软件移植,因此人们倾向于用C来编写设计任务。
那么对于一个ARM嵌入式应用系统来说,能否只用C语言进行ARM的开发呢?
答案是否定的,一般来说启动代码和中断的初始化需要用汇编语言来编写。
ARM体系结构支持C/C++以及与汇编语言的混合编程。
汇编语言与C/C++混合编程通常有以下几种方式:
(1)在C/C++代码中嵌入汇编指令;
(2)在汇编程序和C/C++的程序之间进行变量的互访;
(3)汇编程序、C/C++程序间的相互调用。
混合编程中,必须遵守一定的调用规则,如物理寄存器的使用、参数的传递等,ARM专门为此制定了一个标准,这就是下面要介绍的ARM过程调用标准ATPCS。
10.1.1ARM过程调用标准ATPCS
1ATPCS概念
ATPCS,即ARM、Thumb过程调用标准(ARM-Thumb Procedure Call Standard),这个标准定义了相互独立的汇编模块或编译模块间调用时必须遵守的一些约定,诸如在内存使用上的约定,包括寄存器、数据栈等的使用。
描述了子程序是如何被单独编写、编译、汇编并组合在一起工作的,通过使用ATPCS可以方便地将各种不同语言编写的程序组合成一个完整的文件。
通过使用EmbestIDE的C语言编译器的“-apcs/interwork”编译选项,可以使编译的C语言程序满足ATPCS规则。
而对于汇编语言程序来说,除了需要对汇编器使用“-apcs”选项外,在程序编写时还要注意必须遵守相应的ATPCS规则。
基本ATPCS规定了在子程序调用时的一些基本规则,包括:
各寄存器的使用规则及其相应的名称、堆栈的使用规则和参数传送的规则。
2寄存器的使用规则
ATPCS对ARM和Thumb指令集中的16个32位寄存器(R0~R15)定义了另一个名称,具体如表9.1所示。
表9.1ATPCS寄存器
寄存器
R0
R1
R2
R3
R4
R5
R6
R7
R8
R9
R10
R11
R12
R13
R14
R15
ATPCS名称
a1
a2
a3
a4
v1
v2
v3
v4
WR
v5
v6
SB
v7
SL
v8
FP
IP
SP
LR
PC
ATPCS寄存器的使用规则及功能如下。
●子程序间通过寄存器R0~R3来传递参数。
这时,寄存器R0~R3可记作a0~a3。
被调用的子程序在返回前无须恢复寄存器R0~R3的内容。
●在子程序中,使用寄存器R4~R11来保存局部变量。
这时,寄存器R4~R11可以记作v1~v8。
如果在子程序中使用了寄存器v1~v8中某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。
在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量。
另外R9、R10和R11还有一个特殊的作用,分别记为:
静态基址寄存器SB、数据栈限制指针SL和桢指针。
●寄存器R12用作过程调用中间临时寄存器,记作IP。
●寄存器R13用作堆栈指针,记作SP。
在子程序中寄存器R13不能用作其他用途。
寄存器SP在进入子程序时的值和退出子程序的值必须相等。
●寄存器R14称为链接寄存器,记作LR,它用于保存子程序的返回地址。
如果在子程序中保存了返回地址,寄存器R14则可以用作其他用途。
●寄存器R15为程序计数器,记作PC,它不能用作其他用途。
注意,只有寄存器R0~R7、SP、LR和PC可以在Thumb状态下使用,其中R7常常作为Thumb状态的工作寄存器,记为WR。
3数据栈使用规则
ATPCS规定的数据栈为满降序栈(FD),即栈指针指向栈顶元素,且数据栈的增长方向为向低地址增长,并且对数据栈的操作是8字节对齐。
因此要求包含外部调用的程序必须满足下列条件:
(1)外部接口程序的数据栈必须是8字节对齐的。
(2)本程序生成的数据栈也必须是8字节对齐的。
在汇编程序中可以使用PRESERVE8伪指令告诉连接器,本汇编程序数据栈是8字节对齐的。
4参数传递规则
根据被调用程序是否接受可变数量的参数,可以有两种参数传递方式。
(1)可变数量参数传递规则(variadicroutines)
前4个字依次放入寄存器R0~R3中(最低地址的数进入R0中);剩余的字数据以逆序的方式放入数据栈中,即入栈的顺序与参数的顺序相反,最后一个字数据先入栈。
(2)固定数量参数传递规则
每个浮点参数顺序处理,为每个参数分配浮点寄存器;整型参数通过寄存器R0~R3传递;剩下的参数值以逆序的方式压入数据栈中。
对于函数的返回结果,若为整型数,则返回规则如下:
若结果的位数不超过4个字时,通过R0~R3返回;若结果的位数超过4个字时,通过内存来返回。
对于浮点类型数据的函数的返回,其结果通过相应的浮点寄存器返回,在此不做介绍。
10.1.2内嵌汇编器
ADS编译器和GCC编译器中的内嵌汇编器都支持汇编语言与C/C++之间进行非常灵活的混合编程,例如允许寄存器的操作数是一个C或C++的表达式。
另外,内嵌汇编器还扩展了复杂指令、优化了汇编语言代码。
通过内嵌的汇编器,可以完成C语言不能完成的目标处理器的功能,同时获得更高效率的代码。
作为一种高级汇编器,内嵌汇编器与ARM汇编器armasm有些区别,这将在下面加以介绍。
注意,内嵌汇编语言受编译器优化级别的支配。
1ADS内嵌汇编器
(1)内嵌汇编指令的调用
ADS内嵌汇编器支持两种方式的内嵌汇编语言:
一种是用__asm标识的内嵌汇编指令;另一种是用关键字asm标识的内嵌汇编指令。
它们的用法如下。
1)__asm标识的内嵌汇编指令。
__asm
{
指令[;指令]/*注释*/
...
[指令]
}
例如包含内嵌汇编指令的字符串拷贝程序my_strcpy如例10.1所示。
例10.1:
字符串拷贝。
voidmy_strcpy(char*src,char*dst)
{
intch;
__asm
{
loop:
LDRBch,[src],#1
STRBch,[dst],#1
CMPch,#0
BNEloop
}
}
2)关键字asm标识的汇编指令。
asm("指令[;指令]");
例如:
asm("movR0,R1");
asm语句应放在C或C++函数中,且字符串中不能包含注释语句。
不论使用__asm还是使用asm关键字,同一行中的两条指令要用分号分开,若一条指令占用多行,则必须用续行符(\),且在汇编指令段中可以使用C或C++的注释语句。
(2)内嵌汇编指令的使用及其注意事项
1)在内嵌汇编指令中,作为操作数的寄存器或常数可以是C或C++的表达式,但要注意表达式必须是可以分配的char、short或int型的无符号数,在编译过程中编译器会通过寄存器计算这些表达式。
如果操作数作为地址使用,则表达式也必须是可分配的。
不建议混合使用寄存器和C/C++表达式,否则容易出现寄存器冲突。
2)内嵌汇编器对物理寄存器的操作有所限制,主要有以下几点:
●禁止对PC寄存器的写操作,程序的跳转只能通过B或BL指令实现。
●编译器使用R12(IP)存放中间结果,使用R0~R3、R12(IP)、R14(LR)进行函数调用,并且结果会影响到CPSR的NZCV标志位,因此这些寄存器最好不要再在指令中使用。
●通常内嵌汇编指令中不要指定物理寄存器,因为这可能会影响编译器分配寄存器,从而影响代码效率,甚至产生错误。
例如:
__asm
{
MOVR0,x
ADDy,R0,x/y//(x/y)的结果将覆盖R0的值
}
由于编译器在计算x/y的值时,会破坏R0中的值,而用x/y的商来代替,导致程序执行错误。
可以通过用C变量代替R0来解决这个问题:
MOVvar,x
ADDy,var,x/y
●LDM和STM指令中的寄存器列表只允许使用物理寄存器。
●可以通过指令改变处理器的模式,或者改变协处理器的状态等,但是编译器不知道这些改变。
因此一旦做了上述改变后,就不能再使用C或C++表达式,除非恢复原状态。
3)在内嵌汇编指令中,常数前的#符号可以省略,若在#后为一常数表达式,必须保证表达式的值为一常数。
4)内嵌汇编指令支持指令的扩展(协处理器指令除外),因此内嵌汇编指令中使用的常数没有限制,例如指令:
ADDR0,R0,#1023,编译后实际被替换成下面两条指令来完成其功能。
ADDR0,R0,#1024
SUBR0,R0,#1
5)标号的使用:
只能通过B指令使用C程序中的标号:
B{cond}label。
6)内存的分配:
内嵌汇编器不支持汇编语言中用于内存分配的伪操作,所有的内存单元的分配都是通过C编译器完成的,分配的内存单元通过变量供内嵌汇编器使用。
7)SWI和BL指令的使用:
在这些指令的后面必须增加3个可选的寄存器列表,依次为:
输入参数寄存器列表、输出参数寄存器列表和工作寄存器列表。
例如:
SWI{cond}swi_num,{input_regs},{output_regs},{corrupted_regs}
BL{cond}function,{input_regs},{output_regs},{corrupted_regs}
忽略某一列表表示此列表为空,BL指令总是使用R0~R3、IP和lR寄存器作为工作寄存器。
除此之外还要注意以下几点:
●汇编语言中用逗号代表分隔符,因此带有逗号分隔符的C表达式应放在括号内。
例如:
__asm{ADDx,y,(f(),z)}
●混合汇编时,要尽量避免使用物理寄存器去寻址变量。
如果编译器检测到这种情况,它会产生错误信息或者将变量放到另一个寄存器中来避免冲突。
例如:
intbad_f(intx)//x放入寄存器R0中
{
__asm
{ADDR0,R0,#1}//错误的认为R0中的值仍为x
returnx;//x放入寄存器R0中
}
这段代码会将x的值原封不动的返回,因为编译器认为x和R0是两个不同的变量,尽管在程序入口和出口处,x确实放在寄存器R0中。
实际上编译器会认为这段代码没有做任何有意义的工作而将其优化掉了。
因此这段代码应改为:
ADDx,x,#1
(3)不要试图保存和恢复内嵌汇编器用到的物理寄存器,编译器会自动做这些工作。
另外,除CPSR和SPSR之外的物理寄存器必须在赋值之后才能够读取,否则编译器将报错。
例如:
intf(intx)
{
__asm
{
STMFDsp!
{R0}//保存R0的值——错误:
赋值前读取
ADDR0,x,1
EORx,R0,x
LDMFDsp!
{R0}//恢复R0的值——没有必要
}
returnx;
}
这里的某些规则也适用于下面介绍的GCC内嵌汇编语言。
2GCC内嵌汇编器
GCC内嵌汇编器支持的内嵌汇编语言调用的基本的格式如下:
__asm__("asm指令");
其中__asm__可以用__asm或asm代替。
例如:
__asm__("nop");
如果需要同时执行多条汇编语句,则应该用分号将各个语句分隔开,例如:
__asm__("movR1,R2;
movR3,R4"
);
这样的内嵌汇编有些缺陷,因为GCC在进行处理时,首先是形成汇编文件,再交给GNU汇编器gas去汇编,所以gas并不知道R1和R3的值已经改变,如果程序上下文中的C代码需要R1或R3做暂存,这样就会产生错误。
为了解决这个问题,更多时候是使用扩展的内嵌汇编格式,其语法如下:
__asm__(汇编语句模板:
输出部分:
输入部分:
破坏描述部分)
内嵌汇编语言共四个部分:
汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:
”隔开,其中汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:
”隔开,相应部分内容为空。
例如:
__asm__ __violate__ ("mov R3,%0" :
:
"r" (input));
下面依次介绍内嵌汇编语言的各个部分。
(1)汇编语句模板
汇编语句模板由汇编语句序列组成,语句之间使用";"、"\n"或"\n\t"分开。
指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:
%0、%1、...、%9。
指令中使用占位符表示的操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节,其中对字节操作可以显式的指明是低字节还是次字节。
方法是在%和序号之间插入一个字母,"b"代表低字节,"h"代表高字节,例如:
%h1。
GCC在处理时,根据汇编语言模板,以及后面的输入/输出部分的约束条件,自动决定如何将寄存器或内存与变量相结合。
2、输出部分
输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C 语言变量组成。
每个输出操作数的限定字符串必须包含"=",表示它是一个输出操作数。
限定字符串表示对该变量的限制条件,这样GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。
3、输入部分
输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成,其与输出操作数的唯一区别在于限定字符串不包含"="。
4、限制字符
限制字符有很多种,并且与特定的体系结构相关,作用是指示编译器如何处理其后的C语言变量与指令操作数之间的关系。
例如"r"表示将变量放入通用寄存器中,"m"表示将变量放入内存中,"X"表示操作数可以是任何类型,"I"表示操作数是0~31之间的立即数。
详细内容可查阅相关资料。
5、破坏描述部分
破坏描述部分用于通知编译器使用了哪些寄存器或内存,由逗号隔开的字符串组成,一般是寄存器名,另外还有"memory"。
例10.2:
intexam1(void)
{
inta=10,b=0;
__asm____volatile__("movR0,%1;"
"mov%0,R0"
:
"=r"(b)
:
"r"(a)
:
"R0");
}
此程序完成将变量a的值赋值给变量b,其中"=r"(b)为输出部分,作为最先出现的变量与占位符%0对应,第二个占位符%1与输入部分的变量a对应,并且编译器使用了寄存器R0,GCC在处理时不应使用该寄存器来存储任何其它的值,程序所示破坏描述部分为“R0”。
"__volatile__"表示告知编译器不要优化代码,后面的指令保留原样,"volatile"是它的别名。
因此上面的汇编语句等价于:
movR0,a
movb,R0
例10.3:
对于例10.1,通过如下改动,可以使其适用于GCC编译器。
__asm__
("movR0,%0;"
"movR1,%1;"
"movR2,%2;"
"loop:
LDRBR0,[R1],#1;"
"STRBR0,[R2],#1;"
"CMPR0,#0;"
"BNEloop"
:
:
"r"(ch),"r"(src),"r"(dst)
:
"R0","R1","R2"
);
3内嵌汇编器和armasm的区别
内嵌汇编器支持的汇编语言和ARM汇编器支持的汇编语言有些不同。
对内嵌汇编器来说:
●不能够通过使用符号“.”或PC获得当前指令的地址;
●不支持伪指令LDRRn,=expression,可以使用指令MOVRn,expression来代替;
●不支持标号表达式;
●不支持ADR和ADRL伪指令;
●不支持BX指令;
●不能用&来表示16进制数,而应使用前缀0x。
10.1.3C和ARM汇编语言间相互调用
由前述可知,只要遵守ATPCS规则,就可以进行C和ARM汇编语言间的相互调用。
1汇编程序访问C全局变量
全局变量只能通过地址间接调用。
为了访问C中的全局变量,首先通过.extern伪指令引入全局变量,然后将其地址装入寄存器中。
根据变量的类型,可以通过下面的装载和存储指令访问。
•对unsignedchar类型变量,使用LDRB/STRB访问
•对unsignedshort类型变量,使用LDRH/STRH访问(对体系结构3使用LDRB/STRB访问)
•对unsignedint类型变量,使用LDR/STR访问
•对char类型变量,使用LDRSB/STRSB访问
•对short类型变量,使用LDRSH/STRSH访问
小于8个字的结构体可以通过LDM/STM指令来访问整个变量,结构体中的变量也可以通过相应类型的装载和存储指令来访问,此时必须知道此成员与结构体起始地址的偏移量。
例10.4:
.text
.globalasmsubroutine
.externglobvar@引入C中的全局变量globvar
asmsubroutine:
LDRR1,=globvar@读取globvar的地址到R1中
LDRR0,[R1]@读取变量值
ADDR0,R0,#2
STRR0,[R1]
MOVPC,LR
.end
2C程序调用汇编程序的例子
C程序调用汇编程序应首先通过extern声明要调用的汇编程序模块,声明中形参个数要与汇编模块中需要的变量个数一致,且参数传递要满足ATPCS规则;然后再在C程序正文中调用。
例10.5:
下例是一个C程序调用汇编程序的串拷贝例子。
C程序为:
#include
externvoidstrcopy(char*d,char*s);
intmain()
{char*srcstr="Firststring-source";
char*dststr="Secondstring-destination";
printf("Beforecopying:
\n");
printf("%s\n%s\n",srcstr,dststr);
strcopy(dststr,srcstr);
printf("Aftercopying:
\n");
printf("%s\n%s\n",srcstr,dststr);
return(0);
}
ARM汇编语言模块:
.text
.globalstrcopy
Strcopy:
@R0指向目的数据串,R1指向源数据串
LDRBR2,[R1],#1@取字节并修改地址
STRBR2,[R0],#1@存储字节并修改地址
CMPR2,#0@检查是否到串尾
BNEstrcopy@否,则继续
MOVPC,LR@返回
.end
其中,externvoidstrcopy(char*d,char*s)为汇编模块声明,strcopy(dststr,srcstr)为汇编模块调用。
3汇编程序调用C程序的例子
汇编程序调用C语言模块,在调用之前首先必须根据C语言模块中需要的参数个数以及ATPCS参数传递规则,完成参数的传递,即前4个参数通过R0~R3传递,后面的参数通过堆栈传递;然后再利用B/BL指令进行调用。
例10.6:
定义汇编语言将要调用的C函数功能为返回5个参数之和,其函数原型为:
intg(inta,intb,intc,intd,inte)
{returna+b+c+d+e;}
汇编语言要完成的功能是求取i+2i+3i+4i+5i的结果,程序如下:
.global_start
.text
_start:
.externg@引入C函数g
STRLR,[sp,#-4]!
@保存LR
ADDR1,R0,R0@R1=2*i(第2个参数)
ADDR2,R1,R0@R2=3*i(第3个参数)
ADDR3,R1,R2@R3=5*i
STRR3,[sp,#-4]!
@第5个参数入栈
ADDR3,R1,R1@R3=4*i(第4个参数)
BLg@调用C函数g
ADDsp,sp,#4@第5个参数出栈
LDRPC,[sp],#4@返回
.end
程序执行结束,结果保存于寄存器R0中。
10.2基本IO程序(含启动代码)
下面我们通过基本IO程序来介绍完整的ARM程序的编写过程。
10.2.1启动代码
启动代码是用汇编语言编写的一段程序,一般完成堆栈初始化、系统变量初始化、中断系统初始化、地址重映射、I/O初始化以及外围初始化等操作,并引导程序进入C语言编写的主程序。
设计时可以根据需要进行适当的删减。
通常来说,启动代码的大致流程如下:
●中断向量表
●初始化中断向量控制器
●REMAP
●初始化各模式栈指针
●初始化C程序变量
●转到C入口地址
1中断向量表
由于ARM7处理器的中断向量位于地址0x00000000~0x0000001C处,因此应将中断向量表置于此处。
当程序工作于用户RAM模式,或用户外部存储器模式时,需要进行中断向量的重映射。
中断向量表的程序如下:
Vectors:
LDRPC,Reset_Addr
LDRPC,Undef_Addr
LDRPC,SWI_Addr
LDRPC,PAbt_Addr
LDRPC,DAbt_Addr
DCD0xb9205f80@保留向量
LDRPC,[PC,#-0xff0]
LDRPC,FIQ_Addr
其中DCD0xb9205f80为保留向量,这个向量在ARM文件中标识为保留,该位置被Boot装载程序用作有效的用户程序关键字。
LPC2000芯片规定,当向量表中所有的数据累加和为0,且ISP外部硬件条件不满足时,Boot装载程序将执行用户程序。
可以注意到IRQ向量使用的指令(LDRPC,[PC,#-0xff0])与其他向量不同,正常情况下,这条指令所在的地址为0x00000018,当CPU执行这条指令但还没有跳转时,PC的值为0x00000020,0x00000020-0x00000ff0为0xfffff030,这正是向量中断控制器VIC的向量中断地址寄存器VICVectAddr,这个寄存器保存当前将要服务的IRQ中断服务程序的入口地址,这样就可直接跳转到需要的中断服务程序中。
2REMA
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第10章 ARM程序设计基础final 10 ARM 程序设计 基础 final