文章目录
一、ARM伪操作
ARM伪操作一些特殊指令助记符,这些助记符与指令系统的真正的指令不同,它们并没有相对应的操作码,其作用是完成汇编程序作各种准备工作的,这些伪操作仅在编译过程中起作用,一旦编译结束,伪操作的使命就完成。
1、常用符号定义伪指令
(1)定义全局变量:
- GBLA伪指令用于定义一个全局的数字变量,并初始化为0;
- GBLL伪指令用于定义一个全局的逻辑变量,并初始化为F(假);
- GBLS伪指令用于定义一个全局的字符串变量,并初始化为空;
语法格式: GBLA ( GBLL 或 GBLS ) 全局变量名
GBLA Number ;定义一个全局的数字变量,变量名为Number
GBLL Flag ;定义一个全局的逻辑变量,变量名为Flag
(2)定义局部变量:
- LCLA伪指令用于定义一个局部的数字变量,并初始化为0;
- LCLL伪指令用于定义一个局部的逻辑变量,并初始化为F(假);
- LCLS伪指令用于定义一个局部的字符串变量,并初始化为空;
语法格式: LCLA ( LCLL 或 LCLS ) 局部变量名
LCLL Logic ;声明一个局部的逻辑变量,变量名为Logic
LCLS String ;定义一个局部的字符串变量,变量名为String
(3)变量赋值:
- SETA伪指令用于给一个数学变量赋值;
- SETL伪指令用于给一个逻辑变量赋值;
- SETS伪指令用于给一个字符串变量赋值;
语法格式: 变量名 SETA ( SETL 或 SETS ) 表达式
变量名为已经定义过的全局变量或局部变量,表达式为将要赋给变量的值。
Number SETA 0xaa ;将Number变量赋值为0xaa
Logic SETL {TRUE} ;将Logic变量赋值为真
String SETS “Testing” ;将String变量赋值为“Testing”
(4)定义通用寄存器列表名称:
语法格式: 名称 RLIST { 寄存器列表 }
RegList RLIST {R0-R5,R8,R10} ;将寄存器列表名
STMFD SP!, RegList ;保存寄存器列表RegList到堆栈
2、常用数据定义伪指令
用于为特定的数据分配存储单元,同时完成初始化。常见的有以下几种:
- DCB 用于分配一片连续的字节存储单元并用指定的数据初始化。
- DCW(DCWU)用于分配一片连续的半字存储单元并用指定的数据初始化。
- DCD(DCDU) 用于分配一片连续的字存储单元并用指定的数据初始化。
语法格式: { label } 伪指令 expr {,expr } …
String DCB “This is a test!”
Parameter DCB 0x33,0x44,0x55
Data DCW 0,1,2,3
Data DCD 3,4,5,6
注意事项
- DCB伪操作中表达式可以为0~255的数字或字符串,DCB也可用“=“代替;
- DCW分配的存储单元是半字对齐的,而用DCWU分配的存储单元并不严格半字对齐;
- DCD分配的存储单元是字对齐的,而用DCDU分配的存储单元并不严格字对齐,DCD也可用“&”代替。
3、常用汇编控制伪指令
控制伪操作用于控制汇编程序的执行流程。
(1)IF、ELSE、ENDIF:
IF、ELSE、ENDIF伪操作能根据条件的成立与否决定是否执行某个指令序列。当IF后面的逻辑表达式为真,则执行指令序列1,否则执行指令序列2。其中,ELSE及指令序列2可以没有,语法格式如下:
语法格式:
IF 逻辑表达式
指令序列 1
{ELSE
指令序列 2}
ENDIF
(2)WHILE、WEND:
WHILE、WEND伪操作能根据条件的成立与否决定是否循环执行某个指令序列。当WHILE后面的逻辑表达式为真,则执行指令序列,该指令序列执行完毕后,再判断逻辑表达式的值,若为真则继续执行,一直到逻辑表达式的值为假。
语法格式:
WHILE 逻辑表达式
指令序列
WEND
(3)MACRO、MEND:
MACRO、MEND伪操作可以将一段代码定义为一个整体,称为宏指令,然后就可以在程序中通过宏指令多次调用该段代码。
语法格式:
MACRO ;标志宏定义的开始
{$label} 宏名 {$参数1, {$参数2} … }
指令序列
MEND
(4)MEXIT:
MEXIT用于从宏定义中跳转出。
语法格式: MEXIT
4、其他常用伪指令
(1)AREA:
AREA伪操作用于定义一个代码段或数据段。
语法格式:
AREA 段名 属性 1 ,属性 2 ,……
常用的属性如下:
- CODE 属性:用于定义代码段,默认为 READONLY ;
- DATA 属性:用于定义数据段,默认为 READWRITE;
- READONLY 属性:指定本段为只读,代码段默认为 READONLY;
- READWRITE 属性:指定本段为可读可写,数据段的默认属性为 READWRITE ;
- COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的 COMMON 段共享同一段存储单元。
(2)ALIGN:
ALIGN 伪操作通常用于指定数据或代码的对齐方式,以便在编译时或汇编时确保所生成的机器代码满足对齐要求。
语法格式:
ALIGN alignment ;alignment 是一个整数值,表示要对齐的内存边界,值通常是 2 的幂
ALIGN 4
my_data:
; 数据定义
(3)CODE16、CODE32:
CODE16伪操作通知汇编器,其后的指令序列为16位的Thumb指令。CODE32伪操作通知汇编器,其后的指令序列为32位的ARM指令。
但是,CODE16和CODE32伪操作只告诉编译器后面的指令是16位或32位的类型,指令本身不能进行程序状态的切换,如果要进行状态的切换,可以使用BX指令进行操作。
……
CODE32 ;通知编译器其后的指令为32位的ARM指令
LDR R0,=NEXT+1 ;将跳转地址放入寄存器R0
BX R0 ;程序跳转到新的位置执行,并将处理器切换到Thumb工作状态
……
CODE16 ;通知编译器其后的指令为16位的Thumb指令
NEXT
LDR R3,=0x3FF
……
END ;程序结束
(4)ENTRY:
ENTRY伪操作用于指定程序的入口点,在一个完整的汇编语言程序中至少要有一个ENTRY(也可以有多个,当有多个ENTRY时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个ENTRY(可以没有)。
(5)END:
END伪操作用于通知汇编器已经到了源程序的结尾,每一个汇编源文件都需要使用一个END伪操作,指示本源程序结束。
(6)EQU:
EQU伪操作用于为程序中的常量、标号等定义一个等效的字符名称,类似于C语言中的 #define ,其中EQU可用“ * ”代替。
语法格式:
名称 EQU 表达式 { ,类型 }
其中类型有三种,分别为CODE16(16ARM指令)、CODE32和DATA。
(7)EXPORT(或GLOBAL):
EXPORT伪操作用于在程序中声明一个全局的标号,该标号可在其他的文件中引用。
(8)IMPORT:
IMPORT伪操作用于通知编译器要使用的标号在其他的源文件中定义,但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。
(9)EXTERN:
EXTERN伪操作用于通知编译器要使用的标号在其他的源文件中定义,但要在当前源文件中引用,如果当前源文件实际并未引用该标号,该标号就不会被加入到当前源文件的符号表中。
语法格式:
(10)GET(或INCLUDE):
GET伪操作用于将一个源文件包含到当前的源文件中,并将被包含的源文件在当前位置进行汇编处理。GET伪指令只能用于包含源文件,如果需要包含经过编译后的二进制目标文件,需要使用INCBIN伪指令。
(11)INCBIN:
INCBIN伪操作用于将一个二进制目标代码文件或任意格式的数据文件包含到当前的源文件中,
被包含的文件不作任何变动地存放在当前文件中,编译器从其后开始继续处理。
(12)RN:
RN 伪指令用于给一个寄存器定义一个别名。
语法格式:
名称 RN 表达式
二、混合语言编程
1、嵌入式C语言的预处理伪指令
(1)文件包含伪指令
文件包含伪指令可将头文件包含到程序中,头文件中定义的内容有符号常量,复合变量原型、用户定义的变量原型和函数的原型说明等。编译器编译预处理时,用文件包含的正文内容替换到实际程序中。
语法格式:
# include <头文件名.h> /*标准头文件*/
# include "头文件名.h " /*自定义头文件*/
(2)宏定义伪指令
-
简单宏
语法格式: # define 宏标识符 宏体
-
参数宏
预处理器在处理参数宏时使用2遍宏展开。第1遍展开宏体,第2遍对展开后的宏体用实参数替换形式参数。参数宏整体来看类似于函数的调用,但没有类型说明符。语法格式: # define 宏标识符(形式参数表) 宏体
#define ADC_WRITE_GETCH(data) (((data)>>16)&0x7) /*获得低三位,ADC通道号*/
-
条件宏
格式1是测试存在,格式2是测试不存在,else可有,也可没有。语法格式1(测试存在): # ifdef 宏标识符 //若标识符已定义 # undef 宏标识符 # define 宏标识符 宏体 # else //若标识符未定义 # define 宏标识符 宏体 # endif 语法格式2(测试不存在): # ifndef 宏标识符 //若标识符未定义 # define 宏标识符 宏体 # else //若标识符已定义 # undef 宏标识符 # define 宏标识符 宏体 # endif
-
预定义宏及宏释放
用于释放原先定义的宏标识符。经释放后的宏标识符可再次用于定义其他宏体。语法格式: # undef 宏标识符
(3)条件编译伪指令
语法格式:
# if(条件表达式1)
...
# elif (条件表达式2)
...
# else
...
# endif
2、嵌入式C语言的基本数据类型
(1)类型修饰符
不同的芯片可能会有所不同,一般都有的如下:
(2)访问修饰符
C语言用于控制访问和修改变量方式的修饰符,分别是常量(const)和易变量(volatile)。
带const修饰符定义出的常量在程序运行过程中始终保持不变。volatile修饰符用于提醒编译程序,该变量的值可以不通过程序中明确定义的方法来改变,每次从内存中读取最新数据,不要对它进行优化,抑制编译器优化,例如:在端口时经常使用,端口值不停的在变。
3、内联函数
与一般函数不同的是,它不是在调用时发生转移,而是在编译时将函数体嵌入在每一个调用语句处。
语法格式:
内联函数的定义形式为:
<inline> <类型标识符> <被调函数名>(含类型说明的形参表)
{
函数体
}
内联函数相当于宏,因此宏调用并不执行类型检查,甚至连正常参数也不检查,故使用需要谨慎。以下情况不适合使用内联:
- 如果函数体内的代码比较长(>10句),使用内联将导致内存消耗代价较高;
- 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大;
4、汇编语言与C/C++的混合编程
(1)汇编与C/C++程序的相互调用规则—ATPCS
-
寄存器的使用规则
- 程序间通过寄存器R0 ~ R3来传递参数, 记为A1 ~ A4(别名)。且被调用的子程序在返回前无须恢复寄存器R0~R3的内容;
- 程序中使用寄存器R4 ~ R11来保存局部变量,记为V1 ~ V8(别名);
如果在子程序中使用到了寄存器V1~V8中的某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值;对于子程序中没有用到的寄存器,则不必进行这些操作; - 寄存器R13用做数据栈指针,记作SP。在子程序中寄存器R13不能用做其他用途寄存器SP在进入子程序时的值和退出子程序时的值必须相等;
- 寄存器R14称为链接寄存器,记作LR。它用于保存子程序的返回地址。如果在子程序中保存了返回地址,则寄存器R14可作其他用途;
- 寄存器R15是程序计数器,记作PC。不能作其他用途;
-
数据栈的使用规则
ATPCS规定数据栈为FD(满递减)类型,并且对数据栈的操作是8字节对齐的。异常中断处理程序可使用中断程序的数据栈。 -
参数的传递规则
- 参数个数固定:第一个整数参数,通过寄存器R0~R3来传递。其他参数通过数据栈传递;
- 参数个数可变:当参数不超过4个,用R0~R3传递参数,当参数超过4个时,还可以使用数据栈来传递参数;
参数多于4个,将剩余的字数据传送到数据栈中时,入栈的顺序与参数顺序相反,即最后一个字数据先入栈。
- 子程序结果返回规则:结果为32位整数,通过R0返回;结果64位整数,通过R0,R1返回,依次类推;不够就通过数据栈返回;
(2)在C/C++代码中嵌入汇编指令
内嵌汇编指令表现为独立定义的函数体,并且可以直接引用C语言的变量定义,数据交换也必须通过ATPCS进行。
语法格式:
__asm{
instruction
...
instruction
} //ADS中支持
/* main.c */
void main()
{
int var=0xAA;
__asm //内嵌汇编标识
{
MOV R1,var
CMP R1,#0xAA
}
while(1);
}
内嵌SWI和BL指令时,3个可选寄存器列表的含义如下:
- 第1个寄存器列表用于存放输入的参数。
- 第2个寄存器列表用于存放返回的参数。
- 第3个寄存器列表的内容供被调用的子程序作为工作寄存器。
示例1:字符串复制
#include <stdio.h>
void my_strcpy(char* src, const char* dst)
{
int ch;
__asm{
loop
LDRB ch, [src], #1
STRB ch, [dst], #1
CMP ch, #0
BNE loop
};
}
int main(void){
const char* a = "Hello World!";
char b[20];
__asm{
MOV R0, a
MOV R1, b
BL my_strcpy, {R0, R1}
}
printf("Original String: %s\n",a);
printf(“Copied String: %s\n",b);
return 0;
}
示例2:修改CPSR寄存器中的bit7可使能和禁止中断
__inline void enable_IRQ(void)
{
int tmp;
__asm
{
MRS tmp,CPSR
BIC tmp, tmp, #0x80
MSR CPSR_c, tmp
}
}
__inline void disable_IRQ(void)
{
int tmp;
__asm
{
MRS tmp, CPSR
ORR tmp, tmp, 0x80
MSR CPSR_c, tmp
}
}
int main(void)
{
disable_IRQ( );
enable_IRQ( ) ;
}
注意事项
- 作为操作数的寄存器和常量可以是C/C++表达式,但必须是char、short、int类型,而且这些表达式都是作为无符号数进行操作;
- 不能直接向PC寄存器中赋值,程序的跳转只能通过B指令和BL指令实现,并且只有B指令可使用C/C++程序中的标号,指令BL不能使用C/C++程序中的标号;
- 除NOP外,不支持其它伪指令;
- LDM/STM指令中的寄存器列表只能使用物理寄存器,不能使用C表达式;
- 一般不要指定物理寄存器(会影响编译器分配寄存器);
- 编译器可能会使用R12寄存器或R13寄存器存放编译的中间结果;
- 内嵌汇编器不支持汇编语言用于内存分配的伪操作;
- 用户不用维护数据栈;
(3)从汇编程序中访问C程序全局变量
首先使用IMPORT伪操作声明该全局变量,然后使用LDR伪指令读取该全局变量的内存地址,通常该全局变量的内存地址值存放在程序的数据缓冲池中(literal pool),最后使用相应的LDR/STR指令读取或修改该全局变量的值。
示例:
AREA globals, CODE, READONLY
EXPORT asmsub
IMPORT globvl ;C程序中声明的全局变量
asmsub
LDR R1, =globvl ;获得变量地址
LDR R0, [R1] ;读入
ADD R0, R0, #2 ;修改变量的内容
STR R0, [R1] ;回存
MOV PC, LR
END
(4)汇编程序、C/C++程序间的相互调用
汇编语言程序的设计要遵守ATPCS。在汇编语言中需用EXPORT伪操作来说明,使得本程序可被其他程序调用。C语言程序调用该汇编语言程序之前,需要在C语言程序中使用EXTERN关键词来声明该汇编语言程序。
在调用时,按照各自语言函数的调用方法调用即可。
汇编语言程序调用C语言程序:
//C程序g()返回5个整数的和
int g(int a, int b, int c, int d, int e)
{
return a + b + c + d + e;
}
;汇编程序调用C程序g( )计算5个整数i,2*i,3*i, 4*i,5*i的和
EXPORT f ;输出代码段的名称
AREA f, CODE, READONLY
IMPORT g ;使用伪操作数IMPORT声明C程序g( )
STR LR, [SP,#-4]! ;保存返回地址
ADD R1, R0, R0 ;假设进入程序f时,R0中的值为i,R1值设为2*i
ADD R2, R1, R0 ;R2的值设为3*i
ADD R3, R1, R2 ;R3的值暂设为5*i
STR R3, [SP, #-4]! ;第五个参数5*i通过数据栈传递
ADD R3, R1, R1 ;R3值设为4*i
BL g ;调用C程序g(),栈取参数5
ADD SP, SP, #4 ;调整数据栈指针,准备返回
LDR PC, [SP], #4 ;返回
END