计算机原理与应用第四章——ARM汇编语言程序设计

第四章——ARM汇编语言程序设计

一、ARM汇编程序设计

(一)语言程序格式

1、机器指令:计算机能识别的代码,机器指令是由二进制数组成的。
2、汇编语言:汇编语言是一种符号语言,用助记符表示操作码,用符号或符号地址表示操作数或操作数地址,它与机器指令是一一对应的。例如对Cortex-M4:
		汇编语言为:	ADD  r1, r0
		机器指令为:		0x4401
3 、高级语言:面向过程(如C, FORTRAN)或对象(如C++, Java)的语言,不依赖于机器,因而有很好的通用性和可移植性。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • ARM汇编程序以程序段为单位组织代码;
  • 段是相对独立的指令或数据序列,具有特定的名称;
  • 段可以分为代码段和数据段,代码段的内容为执行代码,数据段存放代码运行时需要用到的数据;
  • 一个汇编程序至少应该有一个代码段。当程序较长时,可以分割为多个代码段和数据段,多个段在程序编译链接时最终形成一个可执行映像文件。
  • 链接器将各个段安排在存储器中的相应位置。
  • 标号的本质是数据或指令的地址。

(二)常用的汇编指示命令

1.数据常量定义
//格式:
名称  EQU  表达式{,类型 } 
  • 其中EQU可用“*”代替
  • 当表达式为32位常量时,可指定表达式的数据类型,有三种类型:CODE16、CODE32和DATA 。
//例:
Data_in  EQU  100		;定义标号Data_in的值为100

Addr     EQU  0xFF, CODE32	   ;定义Addr值为0xFF, 32位的ARM指令
2.数据变量定义
  1. 全局变量 ——GBL(global)
GBLA  Test1    ;定义一个全局的数字变量,并初始化为 0,
                              ;变量名为Test1
GBLL  Test2    ;定义一个全局的逻辑变量,并初始化F(假)
GBLS  Test3    ;定义一个全局的字符串变量,并初始化为空

后缀:

类型英文简称初始值
数字ArithmaticA0
逻辑LogicL
字符串StringS
  1. 局部变量——LCL(local)

同上

LCLA  Test4	; 定义一个局部的数字变量,并初始化为 0
LCLL  Test5	; 定义一个局部的逻辑变量,并初始化为F(假)
LCLS  Test6	; 定义一个局部的字符串变量,并初始化为空
  1. 给全局变量、局部变量赋值——SET
Test1  SETA  0xaa			;将该变量赋值为0xaa
Test2  SETL  {TRUE}			;将该变量赋值为真
Test3  SETS  “Testing”	              ;将该变量赋值为“Testing”
  1. 寄存器列表变量
RegList  RLIST  {R0-R5,R8,R10}	;将寄存器列表名称定义为
                                    ;RegList,可在ARM指令
                                    ;LDM/STM中通过该名称
                                    ;访问寄存器列表。
  • 在LDM/STM指令中,列表中的寄存器访问次序为根据寄存器的编号由低到高,而与列表中的寄存器排列次序无关。
3.内存分配汇编指示命令

内存分配汇编指示命令一般用于为特定的数据分配存储单元,同时完成已分配存储单元的初始化。(下面带U的表示不严格对齐)

  1. DCB: 分配连续的字节存储单元,并进行初始化,可以用“=”代替
Array1  DCB  1,2,3,4,5		  ;数组
str1    DCB  "Your are welcome!"	  ;构造字符串并分配空间
  1. DCW(或DCWU): 分配连续的半字存储单元
  2. DCD(或DCDU):分配连续的存储单元
  3. DCQ(或DCQU):分配以8字节为单位的连续存储区域,不能给字符串分配空间
  4. DCFD(或DCFDU):为双精度的浮点数分配连续字存储单元 (double)
  5. DCFS(或DCFSU):为单精度的浮点数分配连续字存储单元 (single)
Arrayw1 DCW  0xa,-0xb,0xc,-0xd	;构造固定数组并分配半字存储单元
Arrayd1 DCD 1334234345435	;构造固定数组并分配字为单元的存储单元
Label   DCD  str1			;该字单元存放str1的地址
Arrayd1 DCQ  234234, 98765541	;构造固定数组并分配双字为单元的存储空间
Arrayf1 DCFD  6E2                          ;双精度浮点数   
Arrayf2 DCFD  1.23, 1.45                ;双精度浮点数
Arrayf1 DCFS  6E2, -9E-2, -.3         ;单精度浮点数
Arrayf2 DCFSU  1.23, 6.8E9           ;单精度浮点数,不严格字对齐
  1. SPACE:用于分配一片连续的存储区域并初始化为0。SPACE 可用“%”代替
 Freespace  SPACE  1000 ;分配1000字节的存储空间
  1. MAP:定义一个结构化的内存表的首地址。MAP 可用“^”代替
    MAP 表达式 {,基址寄存器}
  2. FIELD:定义一个结构化内存表中的数据域。FIELD 可用“#”代替
    FIELD常与MAP配合使用来定义结构化的内存表。MAP定义内存表的首地址,FIELD定义内存表中的各个数据域。MAP和FIELD仅用于定义数据结构,并不实际分配存储单元
MAP  0xF10000     ;定义结构化内存表首地址为0xF10000 
Count  FIELD  4     ;定义count的长度为4字节,位置为0xF1000+0 
x  FIELD  4 	     ;定义x的长度为4字节,位置为0xF1004 
y  FIELD  4 	     ;定义y的长度为4字节,位置为0xF1008
4.汇编控制指示命令
  1. IF、ELSE、ENDIF
IF   逻辑表达式
	      指令序列1
ELSE
	       指令序列2
ENDIF
  1. WHILE 、WEND
WHILE   逻辑表达式
	指令序列
WEND
  1. MACRO、MEND
    MACRO、MEND将一段代码定义为一个整体,程序通过宏指令多次调用该段代码。
    宏指令的使用与子程序相似,子程序节省存储空间并提高运行速度,但需要保护现场,在代码较短且需传递参数较多时,可使用宏指令。

  2. MEXIT:用于从宏定义中跳转出去

MACRO		;宏定义开始 
$label  jump $a1,$a2	;宏的名称为jump,有2个参数a1和a2 
$label.loop1		; $label.loop1 为宏体的内部标号,label在调用时被替换为用户定义的符号
… 
BGE $label.loop1 
$label.loop2 
BL $a1		;参数$a1为一个子程序的名称 
BGT $label.loop2 
… 
ADR $a2 
… 
MEND 		;宏定义结束
5. 其他
  1. AREA
    AREA 段名 属性1,属性2,……

AREA定义一个代码段或数据段。属性段表示该代码段(或数据段)的相关属性,常用的属性包括:

  • CODE属性:用于定义代码段,默认为READONLY;
  • DATA属性:用于定义数据段,默认为READWRITE;
  • READONLY属性:指定本段为只读,代码段默认为READONLY;
  • READWRITE属性:指定本段为可读可写,数据段的默认属性为READWRITE;
  • ALIGN属性:使用方式为ALIGN表达式。默认时,ELF(可执行连接文件)代码段和数据段按字对齐,表达式取值为0~31,相应的对齐方式为2表达式次方。
  • COMMON属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的COMMON段共享同一段存储单元。
  1. ALIGN
    ALIGN {表达式{,偏移量}}

通过添加填充字节的方式,使当前位置满足一定的对齐方式。

  • 表达式的值用于指定对齐方式,可能的取值为2的幂,如1、2、4、8、16等。若未指定表达式,则将当前位置对齐到下一个字的位置。
  • 偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为:2的表达式次幂+偏移量。
  1. CODE16和CODE32
  • CODE16伪指令通知编译器,其后的指令序列为16位的Thumb指令;
  • CODE32伪指令通知编译器,其后的指令序列为32位的ARM指令。
  1. ENTRY:
    指定汇编程序的人口点。在一个完整的汇编程序中至少要有一个ENTRY,也可以有多个。

  2. END:
    通知编译器已经到了源程序的结尾,通常放在程序最后一行。

  3. EXPORT:
    声明一个全局的标号,该标号可以在其他文件中引用。 [WEAK]选项声明其他的同名标号优先于该标号被引用。
    EXPORT 标号{[WEAK]}

  4. IMPORT:
    IMPORT 标号 {[WEAK]}
    用于通知编译器要使用的标号在其他的源文件中定义,要在当前源文件中引用,无论当前源文件是否引用该标号,该标号均会被加入到当前符号表中。

  5. EXTERN :
    EXTERN 标号 {[WEAK]}
    用于通知编译器要使用的标号在其他的源文件中定义,要在当前源文件中引用。如果当前源文件实际并未引用该标号,该标号就不会被加入到当前源文件的符号表中。

  6. GET 文件名
    将一个源文件包含到当前的源文件中,并将被包含的源文件在当前位置进行汇编处理。可使用INCLUDE代替GET。

  7. INCBIN 文件名
    将一个目标文件或数据文件包含到当前的源文件中,被包含的文件不作任何变动地存放在当前文件中,编译器从其后开始继续处理。

  8. RN
    给一个寄存器定义一个别名,方便程序员记忆该寄存器的功能。

  9. ROUT
    给一个局部变量定义作用范围。在程序中若未使用该伪指令,局部变量的作用范围为所在的AREA,而使用ROUT后,局部变量的作用范围在当前ROUT和下一个ROUT之间。

(三)复位后程序加载启动过程与调试验证

STM32F4xx复位过程:
0x00000000-0x03FFFFFF地址空间实际上是主Flash或系统存储器或SRAM1 三个地址空间之一的别名或映射,可以由用户通过硬件配置方式或软件配置方式选择具体映射哪个地址空间到地址0x00000000处。
在这里插入图片描述

三种存储器映射本质上是三种启动模式——

①用硬件配置
在这里插入图片描述
②用软件配置
在这里插入图片描述

(四)ARM汇编语言结构化程序设计方法

结构化程序设计方法是按照模块划分原则,以提高程序可读性和易维护性、可调性和可扩充性为目标的设计方法。

结构化程序是由若干基本结构组合而成,每一个基本结构可包含若干条语句和其他基本结构。

  • 顺序结构设计

  • 选择结构设计

//求3个数中的最大数,保存在R0中
; asm4-3.s
N1		EQU		456
N2		EQU		1278
N3		EQU		85
		……
Reset_Handler
		LDR  r0,  =N1		; 取第1个数
		LDR  r1,  =N2		; 取第2个数
		LDR  r2,  =N3		; 取第3个数
		CMP  r0,  r1		; 比较r0,  r1大小
		BHI  next			; 若r0大则跳转到next
		MOV  r0,  r1		; 若r1大则存入r0
next
		CMP  r0,  r2		; 比较r0,  r2大小
		BHI  deadloop		; 若r0大则跳转到deadloop
		MOV  r0,  r2		; 若r2大则存入r0
deadloop 
  • 循环结构设计
//求1加到N,存到r0
; asm4-4.s
N		EQU	0 ; 定义常量N值为0
		……
Reset_Handler
		LDR  r0,  =N		; 初始化循环计数值
		MOV  r1,  #0		; 初始化计算结果

; 将数值 N,N-1,...,2,1相加,计算结果在 R1 中
loop
		CMP  r0,  0			; 比较r0与0
		BEQ  deadloop       ;若r0 = 0,跳出循环
		ADD  r1,  r0		; R1 = R1 + R0
		SUB  r0,  #1		; 减小R0 
		B       loop		; 跳到 loop deadloop 
deadloop 	……
  • 子程序结构设计

子程序进入:(子程序的返回地址放到LR寄存器中)

BL   funcname  

子程序返回:

BX  LR
或   	MOV PC, LR
//求1加到N,存到r0
Reset_Handler
		LDR  r0,  =N	; 初始化循环计数值
		BL  funcadd

; 子程序:入口参数R0,出口参数R1
funcadd
		MOV  r1,  #0	; 初始化计算结果
loop
		ADD    r1,  r0	; R1 = R1 + R0
		SUBS  r0,  #1	; 减小R0, 更新标志位 ('S' 后缀)
		BNE   loop	; 若上一条SUBS指令计算结果非0,则跳到 loop
		BX     LR	; 子程序返回

参数保护:

STMFD SP!, {R0-R7,LR}  
                                   ; 把要保护的寄存器内容压入堆栈
LDMFD SP!, {R0-R7,PC}  
                                   ; 恢复寄存器内容,原LR值传送到PC
//例:
LDR  r0,  =Number	; 待转换的4位十六进制数的地址
LDR  r1, [r0] 		; 待转换的4位十六进制数存入r1
LDR  r2,  =String	; 把存放字符串的首地址存入r2
STMFD  SP!,  {r1-r2 }   ; 把R1-R2压入堆栈,倒序压入(先R2后R1)
BL  Hex2String		; 调用子程序
LDMFD  SP!,  {r1-r2 }	; 参数出栈(释放保存R1-R2的栈空间)
                                       ; 此时String中是转换完的字符串

二、C与汇编混合程序设计

(一)AAPCS标准

C与汇编之间的相互调用的方式,主要有:

  • 把C语言代码封装为函数,在汇编代码中调用
  • 把汇编代码封装为函数(子程序),在C代码中调用
  • 在C代码中使用嵌入汇编(Embedded assembler)
  • 在C代码中使用内联汇编(inline assembler)

ARM公司推出的 AAPCS (ARM Architecture Procedure Call Standard) 标准,规定了一些函数间调用的规则:

  1. 父函数与子函数间的入口参数依次通过R0-R3这4个寄存器传递。父函数在调用子函数前先将参数存入到R0R3中,若只有一个参数则使用R0传递,2个则使用R0和R1传递,依次类推,**当超过4个参数时,其它参数通过栈传递**。当子函数运行时,根据自身参数个数自动从R0R3或者栈中读取参数。

  2. 子函数通过R0寄存器将返回值传递给父函数。子函数返回时,将返回值存入R0,当返回到父函数时,父函数读取R0获得返回值。

  3. 发生函数调用时,R0-R3是传递参数的寄存器,即使是父函数没有参数需要传递,子函数也可以任意更改R0-R3寄存器,无需考虑会破坏它们在父函数中保存的数值,返回父函数前无需恢复其值。AAPCS 规定,发生函数调用前,由父函数将R0-R3中有用的数据压栈,然后才能调用子函数 ,以防止父函数R0~R3中的有用数据被子函数破坏。

  4. R4-R11为普通的通用寄存器,若子函数需要使用这些寄存器,则需要将这些寄存器先压栈然后再使用,以免破坏了这些寄存器中保存的父函数的数值,子函数返回父函数前需要先出栈恢复其数值,然后再返回父函数。AAPCS规定,发生函数调用时,父函数无需对这些寄存器进行压栈处理,若子函数需要使用R4-R11 ,则由子函数负责压栈,以防止父函数R4~R11中的数据被破坏。

  5. 编译器在编译时就确定了函数间的调用关系,它会使函数间的调用遵守3、4条规定。但编译器无法预知中断函数的调用,被中断的函数无法提前对R0-R3进行压栈处理,因此需要在中断函数里对它所使用的R0-R11压栈。对于中断函数,不遵守第3条规定,遵守第5条规定

  6. R12寄存器在某些版本的编译器下另有它用,用户程序不能使用,因此我们在编写汇编函数时也必须对它进行压栈处理,确保它的数值不能被破坏。

  7. R13寄存器是堆栈寄存器(SP),用来保存堆栈的当前指针。

  8. R14寄存器是链接寄存器(LR),用来保存函数的返回地址。

  9. R15寄存器是程序寄存器(PC),指向程序当前的地址。

(二)在汇编代码中调用C函数

c:
int my_add_c(int x1, int x2, int x3, int x4, int x5, int x6)
{
	return  (x1 + x2 + x3 + x4 + x5 + x6);
}

汇编中:
在这里插入图片描述

(三)在C代码中调用汇编函数

汇编:
		EXPORT  my_add_asm
my_add_asm	FUNCTION
		ADD  R0, R0, R1	;R0 = x1 + x2
		ADD  R0, R0, R2	;R0 = x1 + x2 + x3
		ADD  R0, R0, R3	;R0 = x1 + x2 + x3 + x4
		LDR  R1,  [SP]		;从 栈顶 位置取出x5到R1
		ADD  R0, R0, R1	;R0 = x1 + x2 + x3 + x4 + x5
		LDR  R2,  [SP,  #4]	;从 栈顶+4 位置取出x6到R2
		ADD  R0, R0, R2	;R0 = x1 + x2 + x3 + x4 + x5 + x6
		BX     LR		;返回结果位于R0中
		ENDFUNC
c:
	extern int my_add_asm(int x1, int x2, int x3, int x4, int x5, int x6);int y = 0;
	y = my_add_asm(1,2,3,4,5,6);

(四)嵌入汇编

c:  定义了一个函数,之后调用和c相同
__asm int my_add_embedded_asm(int x1,int x2,int x3,int x4,int x5,int x6)
{
	ADD  R0, R0, R1	//R0 = x1 + x2
	ADD  R0, R0, R2	//R0 = x1 + x2 + x3
	ADD  R0, R0, R3	//R0 = x1 + x2 + x3 + x4
	LDR  R1,  [SP]		//从 栈顶 位置取出x5到R1
	ADD  R0, R0, R1	//R0 = x1 + x2 + x3 + x4 + x5
	LDR  R2,  [SP,  #4]	//从 栈顶+4 位置取出x6到R2
	ADD  R0, R0, R2	//R0 = x1 + x2 + x3 + x4 + x5 + x6
	BX  LR			//返回结果位于R0中
}

(五)内联汇编

c:同上,定义了一个函数,之后调用和c相同
int my_add_inline_asm (int x1, int x2, int x3, int x4, int x5, int x6)
{
__asm
{
	ADD x1, x2 	// x1+x2
	ADD x1, x3 	// x1+x2+x3
	ADD x1, x4 	// x1+x2+x3+x4
	ADD x1, x5 	// x1+x2+x3+x4+x5
	ADD x1, x6 	// x1+x2+x3+x4+x5+x6
}
	return (x1);
}

(六)内部函数

有些情况下,需要使用无法用普通C代码实现的特殊功能操作。

除了使用汇编函数(子程序)、内联汇编、嵌入汇编来进行汇编语言编程外,实现特殊功能操作的另一个方法是使用内部函数(Intrinsic functions)。

内部函数有两种:

  • CMSIS-Core内部函数。
    CMSIS文件包含在微控制器厂商提供的设备驱动程序包中,当使用CMSIS兼容的设备驱动程序库时,实际上已经使用了CMSIS。具体来说,在MDK v5开发平台下,用户应用程序需要如下的文件:
    1、Startup_.s——设备启动代码,包括复位处理程序和异常向量。
    2、System_.c——设备的基本配置文件,包括时钟和总线的配置。
    3、.h用户代码需要的包含文件,用于访问设备。

  • 编译器相关的内部函数。
    特殊功能操作包括系统初始化、外设访问等,比如,“RW段和ZI段初始化代码”是用汇编语言编程实现的,实现复杂。而在CMSIS-Core内部函数中提供了具备该功能的C函数,使用方便。

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值