前期准备——2.基本汇编语法
在做裸机开发前,我们要掌握一些基础的ARM汇编语法,因为即使后面我们用C去写驱动,也要用汇编去执行配置指针、中断、清除session等操作。我们使用的芯片是I.MX6UL,这是款Cortex-A7的内核芯片,所以使用的就是Cortex-A的汇编指令,这里有两份资料可以参考点击下载(提取码:l1rg)。还好我们主要目的就是进行系统的初始化,用到的都是比较简单的指令,也不会涉及到复杂的代码结构。
GNU汇编语法
语法结构
GNU汇编语法是适用于所有的架构,并不是ARM独享的,GNU汇编由一系列的语句组成,每条语句结构如下:
label: instruction @ comment
label:标号,用来表示地址位置,有些指令前面可能会有标号,这样可以通过标号来找到指令的地址注意标号后面有个冒号,在汇报中,任何以冒号结尾的表示符都会被识别为标号
instruction:指令,包括汇编指令或伪指令
@:注释符,也可以用/**/来包裹注释内容
comment:注释内容
比如我们在第一个用汇编点亮LED的试验中的一段汇编代码
第二行的_start就是就是标号,第6行的
ldr r0,=0x020c4068
就是指令。注意指令中的指令、伪指令、寄存器、寄存器名可以全部使用大写,也可以全部使用小写,但是切记不能大小写混用!
后面的@CCGR0就是表明注释内容。
常用的伪操作
在定义标号时,要知道有些标号是常用的伪操作
- .byte 定义单字节数据
- .short 定义双字节数据
- .long 定义4字节数据
- .equ 赋值语句 比如.equ num,0x12,表示num=0x12
- .align 字节对齐,比如.align 4 表示4字节对齐,这个在后面定义session会经常用到
- .end 表示源文件结束
- .global 定义全局符号,比如上面的.global _start。
并且在在使用.session伪操作定义段的时候,汇编已经预定义了一些段名:
- .text——代码段
- .data——初始化的数据段
- .bss——未初始化的数据段
- .rodata——只读数据段
函数
GNU也是支持函数的,函数的格式如下:
函数名: 函数体 返回语句
可以发现,函数声明的方法和前面的基础语法一样,就是把label换成了函数名。返回值在函数里不是必要的。比如下面这段代码就是我们在讲中断时定义的函数
/*复位中断服务函数 */ Reset_Handler: ldr r0,=Reset_Handler bx r0 /*未定义指令中断服务函数 */ Undefined_Handler: ldr r0,=Undefined_Handler bx r0 /*SVC中断服务函数 */ SVC_Handler: ldr r0,=SVC_Handler bx r0
常用汇编语句
下面看看我们常用的汇编语句
处理器内部数据传输指令
处理器做的最多的事情就是在处理器内部来回传递数据,这些数据操作基本为
- 通用寄存器之间数据相互传递
- 通用寄存器与特殊寄存器(如CPSR、SPSR等)之间数据相互传递
- 寄存器存入立即数
数据传输常用的指令有三个:MOV、MRS和MSR,这三个指令的用法如下:
指令 | 目的 | 数据源 | 说明 |
MOV | R0 | R1 | 将寄存器R1内数据复制到寄存器R0中 |
MRS | R0 | CPSR | 将特殊寄存器CPSR内的数据复制到R0中 |
MSR | CPSR | R0 | 将R0内数据复制到特殊寄存器CPSR内 |
下面分别介绍下3个指令的具体用法
MOV指令
MOV指令用来将一个寄存器里的数据拷贝到另一个寄存器内,也可以将一个立即数拷贝到寄存器内,用法如下
MOV R0,R1 @将R1内的数据传递给R0 MOV R0,#0xFF @将立即数0xFFC传递给R0
MRS指令
MRS是将数据从特殊寄存器传递个通用寄存器,也就是用于读取特殊寄存器的值,用法如下:
MRS R0,CPSR @将CPSR的数据传递给R0
MSR指令
MSR指令是将数据从通用寄存器传递给特殊寄存器,也就是写通用寄存器
MSR CPSR,R0 @将R0的数据传递给CPSR
MSR和MRS两个指令非常容易混淆,有个简单的方法记忆:因为指令后面两个寄存器方向是固定的,前面的是目标,后面的是源,R可以记成通用寄存器,S刚好和特殊的头文字相符,就记成特殊寄存器,MRS就是从特殊到通用,MSR就是从通用到特殊。
存储器访问指令
ARM处理器是不能直接访问存储器的,这里的存储器不是SD卡或者NANDFlash,而是类似RAM中的数据,它必须借助内核寄存器组,也就是R0~R15来实现功能,我们需要用汇编来配置I.MX6UL寄存器的时候需要借助存储器访问指令,一般就是将要配置的参数写入到通用寄存器(R0~R12)中,然后借助存储器访问指令将R存储器中的数据写入到I.MX6UL寄存器中,读取寄存器也是一样的,只是过程相反。常用的访问存储器的指令有两种:LDR和STR:用法如下表
指令 | 说明 |
LDR Rd,[Rn,#offset] |
从存储器Rn+offset的位置读取数据放到Rd中 |
STR Rd,[Rn,#offset] | 将Rd里的数据写入到Rn+offset的位置 |
下面来看看这两条指令是怎么用的:
LDR指令
LDR用于从RAM里读取寄存器的值,比如我们要读取地址为0x0209C004这个寄存器(GPIO1_GDIR,控制GPIO1输入输出功能的寄存器,点灯时候会用到)的值,就要这么做
LDR R1,=0x0209C004
LDR R0,[R1]
就是现在R1中保存要读取寄存器的地址,再把R1地址中的数据传给R0。注意这里传递立即数的时候和MOV指令有区别,一个用的是#,一个用的是=。
STR指令
STR指令和LDR相反,用来设置寄存器的值,比如我们要讲GPIO1_GDIR的值设置为0x00000001,代码如下:
LDR R1,=0x0209C004 LDR R0,=0x00000001 STR R0,[R1]
LDR和STR是按照字节进行读取和写入到,也就是操作得都是32位的数据,如果要按照字节或板子姐进行操作得话可以来指令后加上B(Byte)或者H(Half)。同样我们找个方便记忆的方法:LDR可以记忆成LosdR,也就是读取寄存器,STR是setR,设置,也就是写入寄存器。
压栈和出栈
我们通常在中断等操作中需要在A函数中调用B函数,当B函数执行完成后继续执行A函数,要想在跳回A函数时代码能够正常运行,就需要在跳转B函数前讲当前处理器状态保存(也就是保存R0~R15的值),在B函数执行完毕后将保存的值恢复值R0~R15即可。所以保存R0~R15的过程就叫现场保护,恢复的过程就叫恢复现场。在现场保护的时候要进行压栈操作,恢复现场就是进行出栈操作。我们有两个指令来进行该擦操作:PUSH和POP
指令 | 说明 |
PUSH |
将寄存器列表存入栈中 |
POP |
从栈中恢复到寄存器列表 |
假如我们现在要把R0~R3和R12这5个寄存器压栈,当前的SP指针指向0x80000000,处理器的堆栈为向下增长,使用的代码就是这样的
PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈
一定要注意堆栈指针的增长方向,入栈以后的堆栈如下图所示
现在的指针就指向0x7FFFFFEC,假如此刻我们需要对LR寄存器入栈,那么执行完下面代码
PUSH {LR} @将LR压栈
堆栈模型如下:
在这种情况下我们如果要出栈,就要执行下面的代码
POP {LR} @先恢复 LR POP {R0~R3,R12} @在恢复 R0~R3,R12
出入栈的另一种方法
出栈就是从栈盯,也就是SP当前执行的开始位置,地址依次减小来提取堆栈中的数据到要恢复的寄存器列表中,PUSH和POP的另一种方法就是 STMFD SP!和LDMFD SP!所以上面的代码也可以写成这样:
STMFD SP!,{R0~R3, R12} @R0~R3,R12 入栈
STMFD SP!,{LR} @LR 入栈
LDMFD SP!, {LR} @先恢复 LR
LDMFD SP!, {R0~R3, R12} @再恢复 R0~R3, R12
STMFD可以被分解为两部分:STM和FD,同样,LDMFD也可以被分解成LDM和FD这个STM和LDM就跟前面我们讲过LDR和STR,但是这两个指令只能读取存储器中一个数据,而这两个STM和LDM可以多存储和多加载,可以操作存储器内多个连续数据。FD是Full Descending的缩写,即满递减的意思,根据ATPCS规则,ARM使用的FD类型的堆栈,SP指向最后一个入栈的数值,堆栈是由高地址向下增长的,所以用的是STMFD的指令。STM和LDM的指令寄存器列表中标号小的对应低地址,编号高度对应小地址。
跳转指令
在代码中我们经常需要程序跳转至别的代码段,这就是跳转操作。有两种方法我们比较常用:
- 使用跳转指令:B、BL、BX
- 直接向PC寄存器写入数据
第一种方法我们是最常用的,指令用法如下:
指令 | 说明 |
B |
跳转到label处,如果跳转范围超过了±2KB,可以指定B.W来使用32位版本的跳转指令。 |
BX |
间接跳转,Rm内存放数据为跳转到目的地址,并且切换指令集 |
BL |
跳转到Label处,并将返回地址保存在LR(Link Register)里,可以回到跳转前的位置 |
BLX |
结合了BX和BL的特点,间接跳转至Rm保存的地址,并保存返回地址再LR内,切换指令集。 |
B指令
最简单的跳转指令,跳转后不考虑返回,一旦执行B指令,ARM处理器直接跳转到指定目标地址
_start: ldr sp,=0x80200000 @设置堆栈指针 b main @跳转到main函数
上面的代码就是用来初始化C语言的运行环境,然后跳转到C底main函数处。这里跳转到main函数后不会再回到汇编,所以使用了B指令。
BL指令
BL指令和B指令相比,就是在LR(R14)中保存PC(R15)的值,看下图,处理器在各种模式下R0~R15各个内核寄存器作用,后面会大致提到。主要是这个R15,也就是PC,保存了当前指令地址加8个字节。也就是说R15记录了当前程序的位置,我们需要回到跳转前位置只需要读取LR到值就可以了。这个后面有机会可以详细讲讲。
上面就是我们常用的指令,除此之外还有算数运算指令、逻辑运算指令等, 需要用到话我们再来补充。