前言:
本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM(MX6U)裸机篇”视频的学习笔记,在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。
引用:
正点原子IMX6U仓库 (GuangzhouXingyi) - Gitee.com
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》第 8.1 章
《正点原子资料_A盘/02开发板原理图/IMX6ULL_MINI_V2.2(Mini底板原理图).pdf》
-
资料盘
开发板资料链接: https://pan.baidu.com/s/1j5Jzbdx9i-g0cWIi3wf2XA 提取码:ag1u
正文:
本文是 “正点原子[第二期]Linux之ARM(MX6U)裸机篇--第6.3讲” 的读书笔记。
1. Contex-A7 寄存器介绍
本节介绍 ARM Contex-A 的内核寄存器,注意不是芯片的外设寄存器,本节主要参考 《ARM Contex-A(armV7)编程手册V4.0.pdf》第三章 ARM Processor Modes And Registers。
ARM构架提供了 16 个 32 位通用寄存器(R0~R15)供软件使用,前 15 个寄存器(R0~R14)可以用作通用寄存器,R15 是程序计数器 PC (Program Counter),用来保存将要执行的指令。ARM 还提供了一个当前程序状态寄存器 CPSR (Current Program Status Register) 和一个备份程序状态寄存器 SPSR (S Program Status Register),SPSR 寄存器就是 CPSR 寄存器的备份。这18个寄存器如图 6.3.1 所示。
上一小节我们讲到 ARM Contex-A7 有9种运行模式,每一种运行模式都有一组与之对应的寄存器组。每一种模式可见的寄存器包括15个通用寄存器(R0~R14),一两个状态寄存器,和一个程序计数器PC。在这些寄存器中,有些是所有模式公用的同一个物理寄存器,有一些是个模式自己拥有的,各个模式所拥有的寄存器如下表所示。
在上图中浅色字体的是与 User 模式所共有的寄存器,蓝绿色背景的是各个模式所独有的寄存器。可以看出,在所有模式总,低寄存器组(R0~R7)是共享同一组物理寄存器的,只是一些高寄存器组在不同的模式下有自己独有的寄存器,比如 FIQ 模式下 R8~R14 是独立的物理寄存器。假如某个程序在 FIQ 模式下访问 R13 寄存器,那它实际访问的是寄存器 R13_fiq,如果程序在 SVC 模式下访问 R13 寄存器,那它实际访问的是寄存器R13_svc 。总结一下,ARM Contex-A 内核的寄存器组组成如下:
- 34个通用寄存器,包括R15 程序计数器(PC),这些寄存器是32位的。
- 8个状态寄存器
- HYP模式下一个独有的 ELR_Hyp寄存器
1.1 通用寄存器
R0-R15 就是通用寄存器,通用寄存器可以分为一下3类
- 未备份寄存器,即 R0~R7
- 备份寄存器,即 R8~R14
- 程序计数器PC,即 R15
1.1.1 未备份寄存器
未备份寄存器指的是 R0~R7 这8个寄存器,因为在所有的处理器模式下这个8个寄存器都是同一个物理寄存器,在不同模式下下,这8个寄存器的数据就会被破坏。所以这8个寄存器没有被用作特殊用途。
1.1.2. 备份寄存器
备份寄存器中的 R8 ~ R12 这 5 个寄存器有两种物理寄存器,在快速中断模式下 FIQ 它们对应着 Rx_irq(x=8~12)物理寄存器,其它模式下对应着 Rx(8~12)物理寄存器。FIQ是快速中断模式,看名字就知道这个中断模式要求快速执行。FIQ模式下中断处理程序可以使用 R8 ~ R12 寄存器,因为 FIQ 模式下下 R8~R12 是独立的,因此中断可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。
备份寄存器 R13 一共8个物理寄存器,其中一个是用户模式(User)和系统模式(Sys)公用的,剩下一个分别对应7种不同的模式。R13 也叫做 SP (Stack Pointer),用来做栈指针。基本上每种模式都有一个自己的R13寄存器,应用程序会初始化R13,使其指向该模式专用的栈地址,这就是常说的初始化SP指针。
备份寄存器R14一共有7个物理寄存器,其中一个是用户模式(User)和系统模式(Sys)和超级监视模式(Hyp)所共有的,剩下的6中分别对应着6中不同的模式。R14也称为连接寄存器(LR),LR寄存器在ARM中的主要用途有以下2种:
- 每种处理器模式使用R14(LR)来存放当前子程序的返回地址,如果使用 BL 或则 BLX 来调用子函数的话,R14(LR)用来存放当前函数的返回地址,在子函数中,将R14(LR)的值赋值给R15(PC)即可完成子函数的返回,比如在子程序中使用如下代码:
MOV PC, LR @寄存器LR中的值赋值给PC,实现跳转
或者可以在子程序入口将LR入栈:
PUSH {LR} @将LR寄存器入栈
在子程序的最后出栈即可
POP {PC} @将上面压栈的LR寄存器出栈给PC寄存器,严格意义上来讲应该是将@LR-4 赋
@赋值给PC,应为是3级流水线,这里只是演示代码 - 当异常发生时,该异常模式对应的R14寄存器被设置成该异常模式将要返回的地址,R14也可以当做普通寄存器使用。
1.1.3. 程序计数器R15
程序计数器R15也叫做PC,R15保存着当前正在执行的指令地址加8字节,这是因为ARM的流水线机制导致的。ARM处理器3级流水线:取指->译码->执行,这3级流水香循环执行,比如当前正在执行第一条指定的同时,也对第二条指令译码,第三条指令也同时被去除存放到 R15 (PC)中。我们喜欢以当前你正在执行的指令作为参考点,也就是以第一条执行为参考点,那么 R15(PC)中存放就是第三条指令,换句话说R15(PC)总是指向当前正在执行的指令地址再加上2条指令的地址。对于32位的ARM处理器,每条指令时4个字节,所以
R15(PC)值 = 当前执行的程序位置 + 8 个字节
1.2 程序状态寄存器
所有的处理器模式都公用一个 CPSR (Current Program State Register)物理寄存器,因此 CPSR 可以在任何模式下访问。CPSR 是当前程序状态寄存器,该寄存器包含了条件标志位,当前处理器模式标志等一些状态为以及一些控制位。所有的处理器模式都公用一个CPSR必然会导致冲突,为此,除了 User 和 Sys 这两个模式以外,其它7个模式都配备了一个专用的物理状态寄存器,叫做 SPSR (备份程序状态寄存器),当特定的异常中断发生时,SPSR寄存器用来保存当前策划给你续状态寄存器(CPSR)的值,当异常退出以后可以用SPSR中保存的值来回复CPSR。
因为User和Sys两个模式不是异常模式,所以并没有配置SPSR,因此不能再User和Sys模式下访问SPSR,会导致不可预知的结果。由于SRSR是CPSR的备份,因此SPSR和CPSR的寄存器结构相同,如下图所示
位位置 | 功能 |
N(bit31) | 当两个补码表示的有符号整数运算的时候,N=1表示运算结果为负数,N=0表示结果为正数。 |
Z(bit30) | Z=1表示运算结果为零,Z=0表示运算结果不为0,对于CMP指令,Z=1表示比较的两个数大小相等。 |
C(bit29) | 在加法指令中,当结果产生了进位,则C=1,表示无符号数运算发生了上溢,其它结果C=0。 在加法指令中,当运算中发生借位,则C=0,表示无符号运算发生下溢,其它情况c=1。 对于包含一位操作的非加/减法运算指令,C中包含最后一次溢出的位的数值,对于其它非加/减法运算指令,C位的值通常不收影响。 |
V(bit28) | 对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时,V=1表示符号位溢出,通常其它不会影响V位。 |
Q(bit27) | 仅ARM V5TE_j 构架支持,表示饱和状态,Q=1表示累积饱和,Q=0表示累积不饱和。 |
IT[1:0](bit26:25) | 和 IT[7:2] (bit15:bit10)一起表示IT[7:0],作为IF_THEN指令状态。 |
J(bit24) | 仅ARM V5TE_j 构架支持,J=1 表示处于 Jazelle 状态,此位通常和 T(bit5)位一起表示当前所使用的指令集,如表 6.3.2.1 所示: |
GE[3:0](bit19:16) | SIMD指令有效,大于或等于 |
IT[7:2](bit15:10) | 参考IT[1:0] |
E(bit9) | 大小端控制位,E=1表示大端,E=0 表示小端 |
A(bit8) | 禁止异步中断为,A=1表示禁止异步中断 |
I(bit7) | I=1 禁止IRQ,I=0使能IRQ |
F(bit6) | F=1 禁止FIRQ,F=0 使能 FIRQ |
T(bit5) | 控制指令执行状态,表名指令时ARM指令还是Thumb指令,通常和J(bit24)一起表示指令类型,参考J(bit24)位。 |
M[4:0] | 处理器模式位,含义如表 6.3.2.2 所示 |
M[4:0] | 处理器模式 |
10000 | User模式 |
10001 | FIQ模式 |
10010 | IRQ模式 |
10011 | Supervisor (SVC)模式 |
10110 | Monitor(MON)模式 |
10111 | Abort(ABT)模式 |
11010 | Hyper(HYP)模式 |
11011 | Undef(UND)模式 |
11111 | System(SYS)模式 |
表 6.3.2.2
2. ARM 汇编语言简介
在进行嵌入式Linux开发的时候绝对要掌握基本的ARM汇编,因为 Cortex-A 芯片一上电 SP 指针还没有初始化,C环境还没准备好,所以肯定不能运行C代码,必须先用汇编语言设置好C环境,比如初始化DDR,设置SP指针等等,当汇编把C环境设置好了以后才可以运行C代码。所以Contex-A一开始肯定是汇编代码。本章我们只讲解最常用的一些指令,满足我们后续学习即可。
I.MX6U-ALPHA/Mini 使用的是NXP的I.MX6UL芯片,这是一款 Contex-A7 内核的芯片,所以我们要讲的是 Cortex-A 汇编指令。为此我们需要参考两份 Contex-A 内核有关的文档:
- 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》
- 《ARM CortexA(armV7)编程手册 V4.0.pdf》
第一份文档主要讲解 ARMv7-A 和 ARMv7-R 指令集的开发,Contex-A7 使用的是 ARMv7-A 指令集。第二份文档主要讲解 Cortex-A(armV7)编程的,这两份文档是学习Cortex-A不可或缺的文档。在《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》的 A4 章详细的讲解了 Cortex-A 的汇编指令,要想系统的学习 Cortex-A的指令就要认真的阅读 A4 章节。
对于Cortex-A芯片来讲,大部分芯片在上电以后C语言环境还没有准备好,所以第一行程序肯定是汇编,至于要写多少汇编程序,那就看一能在哪一步把C语言环境准备好。所谓的C语言环境就是保证C语言能够正常运行。C语言的函数调用涉及到出栈入栈,出栈入栈就要对堆栈进行操作,所谓的堆栈其实就是一段内存,这段内存比较特殊,由SP指针访问,SP指针指向栈顶。芯片一上电SP指针还没有初始化,所以C语言没法执行,对于有些芯片还需要初始化DDR,因为芯片本身没有RAM,或者内部RAM不开放给用户使用,用户代码需要再DDR中运行,因此一开始要用汇编来初始化DDR控制器。后面学习Uboot和Linux内核的时候汇编是必须要会的,是不是觉得好难啊?还要会汇编!前面都说了只是在芯片上电以后用汇编来初始化一个外设,不会涉及到复杂的代码,而且用到的指令都是很简单的,用到就是那么十几个指令。所以,不要看到汇编就觉得复杂,打击学习信心。
2.1 GNU汇编语法
不同的汇编器其汇编的语法就有一些小的差别。我们要编写的是ARM汇编,编译器使用的是GCC交叉编译器,所以我们的汇编代码要符合 GNU语法。
GNU汇编语法适用于所有的架构,并不是ARM独享的,GNU汇编由一系列的语句组成,每行一条语句,每条语句有三个可选的部分:
label: instruction @comment
label: 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的 “:”,任何以“:” 结尾的标识符都会被识别为一个标号。
instruction: 即指令,也就是汇编指令或伪指令。
@符号,表示后面是注释,就和C语言中的的"/*"和"*/"一样,其实GNU汇编文件中也可以使用"/*"和"*/" 来进行注释。
comment: 就是注释内容
比如如下代码:
add:
MOVS R0, #0X12 @设置R0=0x12
上面代码中的“add”就是标号,“MOVRS R0,#0x12”就是指令,最后的“ @设置R0=0x12”就是注释。
注意!ARM中的指令,伪指令,伪操作,寄存器等可以全部使用大写,也可以全部使用小写,但不能大小写混用。
用户也可以使用 .section 伪操作来定义一个段,汇编系统预留了一些段名:
- .text 表示代码段
- .data 初始化的数据段
- .bss 未初始化数据段
- .rodata 只读数据
我们当然可以自己使用 .section 来定义一个段,每个段以段名开始,以下一段或者文件结尾结束,比如:
.section .testsection @定义一个 .testsectjion 段
汇编程序的默认入口标志是 _start,不过我们可以在链接脚本中使用 ENTRY 来指明其它的入口点,西面就是使用 _start 作为入口标号。
.global _start
_start:
ldr r0, =0x12 @r0=0x12
上面的代码中 .global 是伪操作,表示 _start 是一个全局符号,类似C语言里面的全局变量一样,常见的伪操作有:
- .byte 定义单字节数据,比如 .byte 0x12
- .short 定义双字节数据,比如 .short 0x1234
- .long 定义一个4字节数据,比如 .long 0x12345678
- .equ 赋值语句,格式为: .equ 变量名,表达式,比如 .equ num, 0x12,表示 num=0x12
- .align 数据字节对齐,比如 .align 4 表示 4字节对齐
- .end 表示源文件结束
- .global 定义一个全局符号,格式为 .global symbol,比如,.global _start
GNU 汇编还有其他的伪操作,但是最常见的就是上面这些,如果想详细了解全部的伪操作,可以参考《ARM Cortex-A(armV7)编程手册V4.0.pdf》的第57页。
GNU汇编同样支持函数,函数个数如下:
函数名:
函数体
返回语句
GNU 汇编函数的返回语句不是必须得,如下代码就是用GNU汇编编写的 Cortex-A7 中断服务函数:
//示例代码 7.1.1.1 汇编函数定义
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC 中断 */
SVN_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PerfAbort_Handler:
ldr r0, =PerfAbort_Handler
bx r0
- 上述代码中定义了3个汇编函数: Undefined_Handler, SVC_Handler, 和 PerfAbort_Handler。以函数 Underfined_Handler 为例我们来看一下汇编函数组成,Underfined_Handler 就是函数名,"ldr r0, =Undefined_Handler" 是函数体,"bx r0" 是函数返回语句,"bx" 是返回指令,函数返回语句不是必须得。
2.2 Cortex-A7 常用汇编指令
本节我们将介绍一些常用的 Corex-A7 汇编指令,如果想系统的了解 Corex-A7 的所有汇编指令请参考《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition.pdf》的A4章节。
2.2.1 处理器内部数据传送指令
使用处理器做的最多的事情就是在处理器内部来回的传送数据,常见的操作有:
- 将数据从一个寄存器传送到另一个寄存器
- 将数据从一个寄存器传送到特殊寄存器,如 CPSR 和 SPSR 寄存器
- 将立即数传送到寄存器
数据传送常用的指令有三个: MOV,MRS 和 MSR,这三个指令的用法如下表所示
指令 | 目的 | 源 | 描述 |
MOV | R0 | R1 | 将R1里面的数据复制到R0中 |
MRS | R0 | CPSR | 将特殊寄存器CPSR里面的数据复制到R0 |
MSR | CPSR | R1 | 将R1里面的数据复制到特殊寄存器CPSR里 |
分别来详细介绍一下如何使用这3个指令:
1. MOV 指令
MOV指令用来将一个数据从给一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面,使用示例如下:
MOV R0, R1 @将寄存器R1里的数据传送到R0,即 R0=R1
MOV R0, #0x12 @将立即数0x12 传递给R0寄存器,即R0=0x12
2.MRS 指令
MRS指令用来将特殊寄存器(如CPSR和SPSR)中的数据传送给通用寄存器,要读取特殊寄存器的数据智能使用MRS指令,使用示例如下
MRS R0,CPSR @将特殊寄存器CPSR里面的数据传送给R0,即R0=CPSR
3.MSR指令
MSR指令和MRS指令正好相反,MSR指令用来将普通寄存器里的数据传递给特殊寄存器,也就是写特殊寄存器,写特殊寄存器智能使用MSR,使用示例如下
MSR CPSR, R0 @将R0中的数据复制到CPSR中,即CPSR=R0
2.2.2 存储器访问指令
ARM不能直接访问存储器,比如RAM中的数据,I.MX6Ul 中的寄存器就是RAM类型的,我们用汇编配置 I.MX6Ul 寄存器的时候需要借助存储器访问指令,一般先将要配置的值写入到 Rx(0~12)寄存器中,然后借助存储器访问指令将Rx中的数据写到 I.MX6U 寄存器中。读I.MX6U 寄存器也是一样的,只是过程相反。常用的存储器访问指令有两种 LDR 和 STR, 用法如下表:
指令 | 描述 |
LDR Rd, [Rn,#offset] | 从存储器 Rn+offset 的位置读取数据存放到Rd中 |
STD Rd, [Rn,#offset] | 将Rd中的数据写入到存储器Rn+offset位置 |
分别来详细的介绍一些如何使用这两个指令,
1. LDR指令
LDR主要用户从存储器加载数据到寄存器Rx中,LDR也可以将一个立即数加载到寄存器Rx中,LDR加载立即数的时候要使用 "=",而不是"#"。在嵌入式开发中,LDR最常用的就是读取CPU的寄存器值,比如 I.MX6U 有个寄存器 GPIO1_GDIR ,其地址为0x0209_c004 ,我们要读取这个寄存器中的数据,示例代码如下:
LDR R0, =0x0209C004 @将寄存器地址0x0209C004 加载到R0中,即R0=0x0209C004
LDR R1, [R0] @读地址0x0209c004中的数据到R1寄存器
2. STR指令
LDR从存储器读取数据,STR就是将数据写入到寄存器,同样以 I.MX6U 寄存器 GPIO1_GDIR 为例,现在我们要配置 GPIO1_GDIR 的值为 0x20000002 ,示例代码如下:
LDR R0, =0x0209C004 @将寄存器地址0x0209C004加载到R0,即R0=0x0209C004
LDR R1, =0x20000002 @将R1要写入到寄存器的值,即R1=R0==0x20000002
STR R1, [R0] @将R1的值写入到R0中所保存的地址中
LDR和STR都是按照字(4个字节)进行读取和写入的,也就是操作32位数据,如果按照字节,半字进行操作的话可以在执行“LDR”后面加上B或H,比如按字节操作的指令就是 LDRB 和 STRB,按照半字进行操作的就是 LDRH 和 STRH。
2.2.3 压栈和出栈指令
我们通常会再 A 函数总调用 B 函数,当B函数执行完以后再回到A函数继续执行。要想再调回A函数以后代码能够接着正常运行,那么必选在调到B函数之前将当前处理器状态保存起来(就是保存R0~R15这些寄存器的值),当 B 函数执行完成以后再用前面保存的寄存器值恢复 R0~R15 即可。保存R0~R15寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要压栈(入栈)操作,恢复现场的时候要进行出栈操作。压栈的指令为PUSH,出栈的指令为POP,PUSH和POP是一种多存储和多加载指令,即可以一次操作多个寄存器数据,它们利用当前栈指针 SP 来生成地址,PUSH 和 POP 的用法如下表所示:
指令 | 描述 |
PUSH <reg lisg> | 将寄存器列表入栈 |
POP <reg list> | 从栈中恢复寄存器列表 |
假如我们现在要将 R0~R3 和 R12 这5个寄存器压栈,当前 SP 指针指向 0x8000_0000 ,处理器的堆栈是向下增长的,使用汇编代码如下:
PUSH {R0~R3,R12} @将R0~R3和R12寄存器压栈
压栈完成后的堆栈如图 7.2.3.1 所示
图 7.2.3.1就是R0~R3,R12进行压栈以后得堆栈示意图,此时的SP指向 0x7FFF_FFEC,假如我们现在要再将 LR 进行压栈,汇编代码如下:
PUSH {LR} @将LR进行压栈
对 LR 进行压栈完成以后的堆栈模型如图 7.2.3.2 所示
图 7.2.3.2 就是分两步对 R0~R3,R12和 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 Decending 的缩写,即满递减的意思。根据 ATPCS (ARM-Thumb Procedure Call Standard)规则,ARM使用的是FD类型的堆栈,SP指向的是最后一个入栈的数值,堆栈是由高地址向下增长的,也就是前面说的向下增长的堆栈,因此最常用的指令就是 STMFD(STM Full Decending)和 LDMFD(LDM Full Decending)。STM 和 LDM 的指令寄存器列表编号小的对应低地址,编号高的对应高地址。(即PUSH/POP, LDMFD/STMFD 入栈顺序是从右往左入栈)。
2.2.4 跳转指令
有多种跳转指令,比如:
- 直接跳转指令,B, BL, BX
- 直接向 PC 寄存器里面写数据
上述两种方法都可以完成跳转,但是一般常用的还是 B, BL, BX ,用法如下表
指令 | 描述 |
B <lable> | 跳转到 label,如果跳转范围超过了 +/- 2KB 可以指定 B.W <lable>使用32位版本的跳转指令,这样可以得到较大范围的跳转 |
Bx <Rm> | 间接跳转指令,跳转到存放于 Rm 中的地址处,并且切换指令集 |
BL <label> | 跳转到标号地址,并将访问地址保存在LR中 |
BLX <Rm> | 结合 BX 和 BL 的特点,跳转到 Rm 指定的地址,并将返回地址保存在LR中,切换指令集。 |
我们重点来看一下B和BL指令,因为这两个是我们用的最多的,如果要再汇编中进行函数调用使用的就是B和BL指令。
1. B 指令
这是最简单的跳转指令,B指令会将 PC 寄存器的值设置为跳转目标地址,一旦执行B指令,ARM处理器就会立即跳转到指定的目标地址。如果要调用的函数不会再返回到原来的执行处,那就可以使用 B 指令,如下例所示:
.global _start
_start:
LDR SP, =0x80200000 @设置栈指针 SP=0x8020_0000
B main @跳转到 main 函数,main是一个label(标签)
上述代码就是典型的在汇编中初始化C运行环境,然后跳转到C文件的 main 函数执行,上述代码只是初始化了 SP 指针,有些处理器还需要做其他初始化,比如初始化 DDR 等等。因为跳转到 C 文件以后再也不会回到汇编了,所以在第4行直接使用了B指令来完成跳转。
2. BL 指令
BL 指令相比于 B 指令,在跳转之前会再寄存器 LR(R14) 中保存当前 PC 寄存器的值,所以可以通过 LR 寄存器的值重新加载到 PC 来继续从跳转之前的代码处运行,这是子程序调用的一个基本但常用的手段。比如 Cortex-A 处理器的 irq 中断服务函数都是汇编编写的,主要用汇编来实现现场保护和恢复,获取中断号等。但是具体的中断处理过程都是C函数,所以就会存在在汇编中调用C的问题。而且C语言版本的中断处理函数执行万策划给你以后是需要返回到 irq 汇编服务函数,因此还要处理其他的工作,一般是恢复现场。这个时候就不能直接使用 B 指令了,因为B指令一旦跳转就再也不会回来了,这个时候需要使用 BL 指令,示例代码如下:
push {r0,r1} @保存r0,r1, 压栈
cps #0x13 @进入SVC模式,允许其它中断再次进去
bl system_irqhandler @加载C语言中断处理函数到r2寄存器中
cps #0x12 @进入IRQ模式
pop {r0,r1} @恢复r0,r1,出栈
str r0 [r1, #0x10] @中断执行完成,写EOIR
上述代码第5行就是执行C语言版本的中断处理函数,当处理完成以后是需要返回来继续执行下面的程序,所以使用了BL指令。
2.2.5 算术运算指令
汇编也可以进行算术运算,比如加减乘除,常用的运算指令如下表所示
指令 | 计算公式 | 备注 |
ADD Rd, Rn, Rm | Rd = Rn + Rm | 加法运算,指令为ADD |
ADD Rd, Rn, #immed | Rd = Rn + #immed | 加法运算,指令为ADD |
ADC Rd, Rn, Rm | Rd = Rn + Rm + 进位 | 带进位的加法运算,指令为ADC |
ADC Rd, Rn, #immed | Rd = Rn + #immed + 进位 | 带进位的加法运算,指令为ADC |
SUB Rd, Rn, Rm | Rd = Rn - Rm | 减法 |
SUB Rd, #immed | Rd = Rd - #immed | 减法 |
SUB Rd, Rn, #immed | Rd = Rn - #immed | 减法 |
SBC Rd, Rn, #immed | Rd = Rn - #immed - 借位 | 带借位的减法 |
SBC Rd, Rn, Rm | Rd = Rn - Rm - 借位 | 带借位的减法 |
MUL Rd, Rn, Rm | Rd = Rn * Rm | 乘法(32位) |
UDIV Rd, Rn, Rm | Rd = Rn/Rm | 无符号除法 |
SID Rd, Rn, Rm | Rd = Rn/Rm | 有符号除法 |
在嵌入式开发中最常会用的就是加减指令,乘除基本用不到。
2.2.6 逻辑运算指令
我们用 C 语言进行CPU寄存器配置的时候常常需要用到逻辑运算符号,比如 “&” , “| ” 等 逻辑运算符。使用汇编语言的时候也可以使用逻辑运算指令,常用的运算指令如下表所示。
指令 | 计算公式 | 备注 |
AND Rd, Rn | Rd = Rd & Rn | 按位与 |
AND Rd, Rn, #immed | Rd = Rn & #immed | |
AND Rd, Rn, Rm | Rd = Rn & Rm | |
ORR Rd, Rn | Rd = Rd | Rn | 按位或 |
ORR Rd, Rn, #immed | Rd = Rn | #immed | |
ORR Rd, Rn, Rm | Rd = Rn | Rm | |
BIC Rd, Rn | Rd = Rd & (~Rn) | 位清除 |
BIC Rd, Rn, #immed | Rd = Rn & (~#immed) | |
BIC Rd, Rn, Rm | Rd = Rn & (~Rm) | |
ORN Rd, Rn, #immed | Rd = Rn | (#immed) | 按位或非 |
ORN, Rd, Rn, Rm | Rd = Rn | (Rm) | |
EOR Rd, Rn | Rd = Rd ^ Rn | 按位异或 |
EOR Rd, Rn, #immed | Rd = Rn ^ #immed | |
EOR Rd, Rn, Rm | Rd = Rn ^ Rm |
逻辑运算指令都很好理解,后面时候汇编配置 I.MX6Ul 的外设的时候可能会用到,ARM 汇编就讲到这里,本节主要讲解了一些最常用的汇编指令,还有很多不常用的指令没有讲解,但是够我们后续学习使用了。要想详细学习 ARM 的所有指令请参考 《ARM Cortex-A(armV7) 编程手册V4.opdf》和 《ARm Architecutre Refercen Manual ARMv7-A and ARMv7-R edition.pdf》。