💘ARM Embedded System,欢迎关注我的Gitee
文章目录
💦两种常见的ARM编译开发环境
ADS/SDT IDE开发环境:它由ARM公司开发,使用了CodeWarrior公司的编译器;
集成了GNU开发工具的IDE开发环境:它由GNU的汇编器as、交叉编译器gcc、和链接器ld等组成。
💦ARM汇编的语句格式
汇编语言都具有一些相同的基本特征。
① 一条指令一行。
② 使用标号(label)给内存单元提供名称,从第1列开始书写。
③ 指令必须从第2列或能区分标号的地方开始书写。
④ 注释跟在指定的注释字符后面(ARM使用的是“;”/”@”),一直书写到行尾。
ARM汇编语言基本的的语句格式如下:
{symbol} {instruction |directive | pseudo-instruction} {;comment}
符号 指令、伪指令或伪操作 [;/@ 注释]
💦符号命名规则
① 符号由大小写字母、数字及下画线组成,符号不能用数字开头。
② 符号区分大小写,同名的大、小写符号会被编译器认为是两个不同的符号。
③ 符号在其作用范围内必须唯一。
④ 自定义的符号名不能与系统的保留字相同。
⑤ 符号名不应与指令或伪指令同名。
💦ARM汇编语言伪操作
伪操作(Directive)是ARM汇编语言程序里的一些特殊的指令助记符,其作用主要是为完成汇编程序做各种准备工作,对源程序运行汇编程序处理,而不是在计算机运行期间由处理器执行。不同的编译程序所使用的伪操作有所不同。
💦GNU ARM汇编伪操作
Linux汇编行结构 任何汇编行都是如下结构:
[:] [} @ comment
[:] [} @注释
Linux ARM 汇编中,任何以冒号结尾的标识符都被认为是一个标号,而不一定非要在一行的开始。
【例1】定义一个"add”的函数,返回两个参数的和。
.section .text, “x”
.global add @ give the symbol add external linkage
add:
ADD r0, r0, r1 @ add input arguments
MOV pc, lr @ return from subroutine
@ end of program
💦Linux汇编程序中的标号
Linux 汇编程序中的标号 标号只能由a~z,A~Z,0~9,“.”,”_”等字符组成。当标号为0~9的数字时为局部标号,局部标号可以重复出现,使用方法如下:
标号f: 在引用的地方向前的标号
标号b: 在引用的地方向后的标号
【例2】使用局部符号的例子,一段循环程序
1: subs r0,r0,#1 @每次循环使r0=r0-1
bne 1f @跳转到1标号去执行
局部标号代表它所在的地址,因此也可以当作变量或者函数来使用。
💦Linux汇编程序中的分段
💦.section
用户可以通过.section伪操作来自定义一个段,
格式如下:
.section section_name [, "flags"[, %type[,flag_specific_arguments]]]
每一个段以段名为开始, 以下一个段名或者文件结尾为结束。这些段都有缺省的标志(flags),连接器可以识别这些标志。(与armasm中的AREA相同)。
下面是ELF格式允许的段标志
a 允许段, w 可写段 x:执行段
例子:
.section .mysection
@自定义数据段,段名为“.mysection”
💦预定义段.text、.data、.bss
语法格式
.text {subsection}
.data {subsection} @初始化数据
.bss {subsection}
作用
.text、.data和.bss将汇编系统预定义的段名编译到相应的代码段、数据段和bss段。注意:源程序中.bss段应该在.text之前。
说明
bss段通常是指用来存放程序中未初始化的全局变量的一块内存区域 数据段通常是指用来存放程序中已初始化的全局变量的一块内存区域
举例
.section .data
<initialize data here>
.section .bss
<uninitialized data here>
.section .text
.global _start
_start:
<instruction code goes here>
💦.end
语法格式
.end
作用
表明源文件的结束,如果该标号之后还有代码,不会被编译到执行文件中
💦.include
语法格式
.include “filename”
作用
可以将指定的文件在使用位置处展开,一般是头文件
💦.incbin
语法格式
.incbin “file”[,skip[,count]]
作用
可以将原封不动的一个二进制文件编译到当前文件中。其中,skip表明是从文件开始跳过skip个字节开始读取文件,count是读取的字数
💦.if、.else/.endif
语法格式
.if 条件表达式
代码段1
.else
代码段2
.endif
💦.macro、.exitm和.endm
语法格式
.macro 宏名 参数名列表 @伪操作.macro定义一个宏 宏体
.endm @.endm表示宏结束
说明
如果宏使用参数,那么在宏体中使用该参数时添加前缀“\”。宏定义时的参数还可以使用默认值,可以使用.exitm伪指令来退出宏
举例
.macro SHIFTLEFT a,b
.if \b<0
MOV \a,\a,ASR #-\b
.exitm
.endif
MOV \a,\a,LSL #\b
.endm
💦.string/.asciz/.ascii
语法格式
.string/.asciz/.ascii 表达式{,表达式}…
作用
.string/.asciz/.ascii定义多个字符串。
注意:ascii伪操作定义的字符串需要自动添加结尾字符’\0’
举例
.string "abcd","hello"
💦.equ、.set
语法格式
.equ(.set)常量名,表达式
作用
.equ和.set用于为程序中标号定义名称
举例
.equ abc 3 @让abc=3
💦.global/.globl
语法格式
.global/.globl symbol
作用
.global和.globl用来定义一个全局的符号
💦.extern
语法格式
.extern label
作用
.extern用于声明一个外部标号
💦.ltorg、.pool
语法格式
.ltorg/.pool
作用
.ltorg和.pool用于声明一个数据缓冲池的开始,它可以分配很大的空间
💦ARM汇编语言伪指令
伪指令是ARM处理器支持的汇编语言程序里的特殊助记符,它不在处理器运行期间由机器执行,只是在汇编时将被合适的机器指令代替成ARM或Thumb指令,从而实现真正的指令操作。ARM汇编语言伪指令如表所示。
💦小范围的地址读取
在汇编编译器编译源程序时,ADR伪指令被编译器替换成一条合适的指令。通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能,若不能用一条指令实现,则产生错误,编译失败。ADR伪指令中的地址是基于PC或寄存器的,当ADR伪指令中的地址是基于PC时,该地址与ADR伪指令必须在同一个代码段中。
地址表达式expr的取值范围如下:
当地址值是字节对齐时,其取指范围为−255B~255B;
当地址值是字对齐时,其取指范围为−1020B~1020B。
ADR伪指令将基于PC相对偏移的地址值或基于寄存器相对偏移的地址值读取到寄存器中。在汇编编译器编译源程序时,ADR伪指令被编译器替换成一条合适的指令。通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能,若不能用一条指令实现,则产生错误,编译失败。
应用示例2(查表):
ADR R0,DISP_TAB @加载转换表地址
LDRB R1,[R0,R2] @ 使用R2作为参数,进行查表
…
DISP_TAB:
.word 0xC0,0xF9,0xA4,0xB0,0x99, 0x92,0x82,0xF8
💦大范围的地址读取
在汇编编译源程序时,LDR伪指令被编译器替换成一条合适的指令。若加载的常数未超出MOV或MVN的范围,则使用MOV或MVN指令代替该LDR伪指令,否则汇编器将常量放入文字池,并使用一条程序相对偏移的LDR指令从文字池读出常量。
示例:
LDR r1,=0xff @ 将0xff读取到r1中
@ 编译后得到MOV r1,0xff
示例:
LDR r1, =ADDR @ 将外部地址ADDR读取到R1中
汇编后将得到:
; LDR r1,[PC,OFFSET_TO_LPOOL]
;…
; LPOOL .word ADDR
💦ARM汇编的程序结构
.text
.global _start
_start:
mov r0,#9
mov r1,#15
loop:
cmp r0,r1 @比较r0 r1
sublt r1,r1,r0 @若r0 < r1 则 r1 = r1 – r0
subgt r0,r0,r1 @若r1 < r0 则 r0 = r0 – r1
bne loop @若 r0 r1不相等则继续循环
stop:
b stop
.end
💦循环程序设计
用预先设定的行标与B、BL结合可以设计各种循环结构。
例如:
LOOP ADD R0,R0,R1 @ R0=R0+R1
CMP R0,#3 @ 比较R0和#3
BLS LOOP @if R0<3 then 跳转到LOOP循环
.end
💦子程序
在ARM汇编语言程序中,子程序的调用一般是通过BL指令来实现的。在程序中,使用指令:
BL 子程序名
该指令在执行时完成如下操作:将子程序的返回地址存放在连接寄存器LR中,同时将程序计数器PC指向子程序的入口点,当子程序执行完毕需要返回调用处时,只需要将存放在LR中的返回地址重新复制给程序计数器PC即可。
以下是使用BL指令调用子程序的汇编语言源程序的基本结构:
……
BL PRINT_TEXT @ 跳转到子程序 PRINT_TEXT,并保存PC至LR
……
PRINT_TEXT: @子程序入口
……
MOV PC,LR @子程序运行完毕将PC置为LR,准备返回
.end
💦堆栈指令初始化
INITSTACK:
MOV R0, LR ;保存返回地址
;设置管理模式堆栈
MSR CPSR_C,#0xD3
LDR SP,StackSvc
;设置中断模式堆栈
MSR CPSR_C,#0xD2
LDR SP,StackIrq
💦ARM C语言基础及混合编程
C语言的优点是运行速度快、编译效率高、移植性好和可读性强。C语言支持模块化程序设计,支持自顶向下的结构化程序设计方法。因此,在嵌入式程序设计中经常会用到C语言程序设计。
嵌入式C语言程序设计是利用基本的C语言知识,面向嵌入式工程实际应用进行程序设计。也就是说它首先是C语言程序设计,因此必须符合C语言基本语法,只是它是面向嵌入式的应用而设计的程序。
为了使单独编译的C语言程序和汇编程序之间能够相互调用,必须为子程序之间的调用规定一定的规则。ATPCS就是ARM程序和Thumb程序中子程序调用的基本规则。
💦ATPCS概述
PCS即Procedure Call Standard(过程调用规范),ATPCS即ARM-Thumb Procedure Call Standard。ATPCS规定了一些子程序之间调用的基本规则,这些基本规则包括子程序调用过程中寄存器的使用规则,数据栈的使用规则,参数的传递规则。
💦基本ATPCS
基本ATPCS规定了在子程序调用时的一些基本规则,包括以下3个方面的内容:各寄存器的使用规则及其相应的名字,数据栈的使用规则,参数传递的规则。相对于其他类型的ATPCS,满足基本ATPCS的程序的执行速度更快,所占用的内存更少。但是它不能提供以下的支持:ARM程序和Thumb程序相互调用,数据以及代码的位置无关的支持,子程序的可重入性,数据栈检查的支持。
💦寄存器的使用规则
ATPCS中定义的寄存器如下。
其中:R0~R3:用于传参,r0用于返回值。
R4~R11:通用变量寄存器。
R12:用作过程调用中间临时过渡寄存器IP。
R13:堆栈指针。
R14:连接寄存器。
R15:PC。
另外,R9、R10和R11还有一个特殊作用,分别记为:静态基址寄存器SB,数据栈限制指针SL和桢指针FP。
① 子程序通过寄存器R0~R3来传递参数,这时寄存器可以记作A0~A3,被调用的子程序在返回前无须恢复寄存器R0~R3的内容。
② 在子程序中,使用R4~R11来保存局部变量,这时寄存器R4~R11可以记作V1~V8。如果在子程序中使用到V1~V8的某些寄存器,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值,对于子程序中没有用到的寄存器则不必执行这些操作。在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量。
③ 寄存器R12用作子程序间临时过渡寄存器,记作IP,在子程序的连接代码段中经常会有这种使用规则。
④ 寄存器R13用作数据栈指针,记做SP,在子程序中寄存器R13不能用作其他用途。寄存器SP在进入子程序时的值和退出子程序时的值必须相等。
⑤ 寄存器R14用作连接寄存器,记作LR。它用于保存子程序的返回地址,如果在子程序中保存了返回地址,则R14可用作其他的用途。
⑥ 寄存器R15是程序计数器,记作PC,它不能用作其他用途。
⑦ ATPCS中的各寄存器在ARM编译器和汇编器中都是预定义的。
💦数据栈的使用规则
栈指针通常可以指向不同的位置。当栈指针指向栈顶元素(即最后一个入栈的数据元素)时,称为FULL栈。当栈指针指向与栈顶元素相邻的一个元素时,称为Empty栈。数据栈的增长方向也可以不同,当数据栈向内存减小的地址方向增长时,称为Descending栈;当数据栈向着内存地址增加的方向增长时,称为Ascending栈。综合这两种特点可以有以下4种数据栈,即FD、ED、FA、EA。ATPCS规定数据栈为FD类型,并对数据栈的操作是8字节对齐的,下面是一个数据栈的示例及相关的名词。
① 数据栈栈指针(stack pointer):指向最后一个写入栈的数据的内存地址。
② 数据栈的基地址(stack base):指数据栈的最高地址。由于ATPCS中的数据栈是FD类型的,实际上数据栈中最早入栈数据占据的内存单元是基地址的下一个内存单元。
③ 数据栈界限(stack limit):数据栈中可以使用的最低的内存单元地址。
④ 已占用的数据栈(used stack):数据栈的基地址和数据栈栈指针之间的区域,其中包括数据栈栈指针对应的内存单元。
⑤ 数据栈中的数据帧(stack frames):在数据栈中,为子程序分配的用来保存寄存器和局部变量的区域。
💦参数的传递规则
(1)参数个数可变的子程序参数传递规则
对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递;当参数超过4个时,还可以使用数据栈来传递参数。在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3;如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈。按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递。
(2)参数个数固定的子程序参数传递规则
对于参数个数固定的子程序,参数传递与参数个数可变的子程序参数传递规则不同,如果系统包含浮点运算的硬件部件,浮点参数将按照下面的规则传递:各个浮点参数按顺序处理;为每个浮点参数分配FP寄存器;分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器。第1个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。
(3)子程序结果返回规则
① 结果为一个32位的整数时,可以通过寄存器R0返回。
② 结果为一个64位整数时,可以通过R0和R1返回,依此类推。
③ 结果为一个浮点数时,可以通过浮点运算部件的寄存器f0、d0或者s0来返回。
④ 结果为一个复合的浮点数时,可以通过寄存器f0~fN或者d0~dN来返回。
⑤ 对于位数更多的结果,需要通过调用内存来传递。
💦支持ARM程序和Thumb程序混合使用的ATPCS
在编译或汇编时,使用/intework告诉编译器或汇编器生成的目标代码遵守支持ARM程序和Thumb程序混合使用的ATPCS,它用在以下场合:程序中存在ARM程序调用Thumb程序的情况;程序中存在THUMB程序调用ARM程序的情况;需要连接器来进行ARM状态和Thumb状态切换的情况;在下述情况下使用选项nointerwork:程序中不包含Thumb程序;用户自己进行ARM程序和Thumb程序切换。需要注意的是:在同一个C/C++程序中不能同时有ARM指令和Thumb指令。
💦C语言及汇编语言混合编程
在嵌入式系统开发中,目前使用的主要编程语言是C和汇编,C++已经有相应的编译器,但是现在使用还是比较少的。在稍大规模的嵌入式软件中,如含有OS,大部分的代码都是用C语言编写的,主要是因为C语言的结构比较好,便于人的理解,而且有大量的支持库。尽管如此,很多地方还是要用到汇编语言,如开机时硬件系统的初始化,包括CPU状态的设定,中断的使能,主频的设定,以及RAM的控制参数及初始化,一些中断处理方面也可能涉及汇编。另外一个使用汇编的地方就是一些对性能非常敏感的代码块,这是不能依靠C编译器的生成代码,而要手工编写汇编,达到优化的目的。而且,汇编语言是和CPU的指令集紧密相连的,作为涉及底层的嵌入式系统开发,熟练对应汇编语言的使用也是必须的。
💦在C语言中内嵌汇编
在C语言中内嵌的汇编指令包含大部分的ARM和Thumb指令,不过其使用与汇编文件中的指令有些不同,存在一些限制,主要有下面几个方面。
① 不能直接向PC寄存器赋值,程序跳转要使用B或者BL指令。
② 在使用物理寄存器时,不要使用过于复杂的C表达式,避免物理寄存器冲突。
③ R12和R13可能被编译器用来存放中间编译结果,计算表达式值时可能将R0到R3、R12及R14用于子程序调用,因此,要避免直接使用这些物理寄存器。
④ 一般不要直接指定物理寄存器,而让编译器进行分配。
💦在汇编中使用C定义的全局变量
内嵌汇编不用单独编辑汇编语言文件,比较简洁,但是有诸多限制,当汇编的代码较多时一般放在单独的汇编文件中,这时就需要在汇编和C之间进行一些数据的传递,最简便的办法就是使用全局变量。汇编中使用C定义的全局变量。
💦在C中调用汇编的函数
在C中调用汇编文件中的函数,要做的主要工作有两个,一是在C中声明函数原型,并加extern关键字;二是在汇编中用EXPORT导出函数名,并用该函数名作为汇编代码段的标识,最后用mov pc, lr返回。然后,就可以在C中使用该函数了。从C的角度,并不知道该函数的实现是用C还是汇编。更深的原因是因为C的函数名起到表明函数代码起始地址的作用,这个和汇编的label是一致的。
【例3-6】 在C中调用汇编的函数(函数不多于4个参数)。
#include <stdio.h>
extern void strcopy(char *d, const char *s);
int main()
{
const char *srcstr = "First string - source ";
char dststr[] = "Second string - destination ";
/* 下面将 dststr 作为数组进行操作 */
printf("Before copying:\n");
printf(" %s\n %s\n",srcstr,dststr);
strcopy(dststr,srcstr);
printf("After copying:\n");
printf(" %s\n %s\n",srcstr,dststr);
return(0);
}
.global strcopy
strcopy: @R0 指向目的字符串
@R1 指向源字符串
LDRB R2, [R1],#1 @加载字节并更新源字符串指针地址
STRB R2, [R0],#1 @存储字节并更新目的字符串指针地址
CMP R2, #0 @判断是否为字符串结尾
BNE strcopy @如果不是,程序跳转到 strcopy 继续复制
MOV pc,lr @程序返回
💦在汇编中调用C的函数
【例3-8】 在汇编语言中调用C语言的函数(参数不多于4个)。
;prog1_asm.asm
.extern prog1_c @ 声明prog1_c函数
.text
_start:
.global prog1_asm
prog1_asm:
STR lr, [sp, #-4]! @ 保存当前lr
ldr r0,=0x1 @ 参数1
ldr r1,=0x2 @ 参数2
ldr r2,=0x3 @ 参数3
bl prog1_c @ 调用C函数
LDR pc, [sp], #4 @ 将lr装进pc(返回main函数)
.end
//prog1_c.c
void prog1_c(int p1,int p2,int p3)
{
printk("%0x %0x %0x\r\n",p1,p2,p3); // 输出参数值
}
//main.c
int main()
{ prog1_asm();
while(1);
}
【例3-9】 在汇编语言中调用C语言的函数(参数多于4个)。
;prog2_asm.asm
.extern prog2_c @ 声明prog2_c函数
.section .text
_start:
.global prog2_asm
prog2_asm:
STR lr, [sp, #-4]! @ 保存当前lr
ldr r0,=0x1 @ 参数1
ldr r1,=0x2 @ 参数2
ldr r2,=0x3 @ 参数3
ldr r3,=0x4 @ 参数4
ldr r4,=0x6
str r4,[sp,#-4]! @ 参数6入栈
ldr r4,=0x5
str r4,[sp,#-4]! @ 参数5入栈
bl prog2_c
ADD sp, sp, #4 @清除栈中参数5,本语句执行完后sp指向参数6
ADD sp, sp, #4 @ 清除栈中参数6,本语句执行完后sp指向lr
LDR pc, [sp],#4 @将lr装进pc(返回main函数)
.end
//prog2_c.c
void prog2_c(int p1,int p2,int p3,int p4,int p5,int p6)
{
printk("%0x %0x %0x %0x %0x %0x\r\n",p1,p2,p3,p4,p5,p6); // 输出参数值
}
//main.c
int main()
{
prog2_asm();
while (1);
}
💦启动代码
💦启动流程图
💦启动代码内容
💦启动代码简介
(1)startup.s-异常向量表定义、各模式堆栈初始化、跳转到C程序main入口等。
(2)target.c-目标板初始化,如时钟分频、PLL设置、VIC设置等。
(3)irq.s-用于管理中断嵌套。
💦启动代码工作流程
复位后,启动代码工作及内容:
(1)复位后,PC=0,根据异常向量表,跳转到复位处理程序。
(2)执行复位程序。
-堆栈初始化
-跳到C程序main入。
(3)执行用户程序。
💦scatter文件分析 .scf
ROM_LOAD 为加载区的名称,其后面的0x00000000 表示加载区的起始地址(存放程序代码的起始地址)
ROM_LOAD 0x0
{
;ROM_EXEC 描述了执行区的地址,放在第一块位置定义
ROM_EXEC 0x00000000
{
;从起始地址开始放置向量表(即Startup.o(vectors, +First),其中Startup.o 为Startup.s 的目标文件)
;+First表示Vector段放在最前面
Startup.o (vectors, +First)
接着放置其它代码(即* (+RO)),* 是通配符,类似WINDOW下搜索用的通配符
* (+RO)
}
;变量区IRAM 的起始地址为0x40000000
IRAM 0x40000000
{
;放置Startup.o (MyStacks)
Startup.o (MyStacks)
}
+0表示接着上一段,UNINIT 表示不初始化
STACKS_BOTTOM +0 UNINIT
{
放置AREA StackBottom, DATA, NOINIT
Startup.o (StackBottom)
}