ARM芯片的那些事
ARM架构
ARM的芯片都使用的同一种架构,这种架构可使CPU只与内部地址空间通信,而不用考虑与芯片外设直接的通信方式。
其通信架构图如下图所示:
图中的内存,UART,USB控制器等内部外设都是在同一片地址空间中的,CPU访问这些外设的方法都是一样的,即通过向内存控制器发出不同的地址选择指令,对接到不同的地址空间上。
ARM芯片术语精简指令集计算机(RISC),它具有以下特点:
- 对内存只有读、写指令
- 对于数据的运算是在CPU内部完成的
- 使用RISC指令的CPU复杂度小一些,便于设计
与之相对的是CISC,相比于RISC而言,该指令能力强,指令更加灵活,也更加复杂。
CPU内部构造
Cortex-M3 是一个 32 位处理器内核。内部的数据路径是 32 位的,寄存器是 32 位的,存储器 接口也是 32 位的。
下图是一个cortex-M3/M4内核在进行两个数据加法运算时的通信过程
CPU通过调取4调指令来完成一次加法运算,CPU从内存对应地址空间中读取a和b的值,并将其存入CPU内部的通用目的寄存器中(R0~R12),在ALU中可以直接访问这些内存目的寄存器,并将a和b相加,存回R0中,最终再将R0的数据写回a所在的地址空间,便完成了a = a+b的运算。
CPU内部构造详见Cortex-M3权威指南
关于内部存储情况,Cortex-M3 处理器拥有 R0-R15 的寄存器组。
其中R0~R12都是 32 位通用寄存器,用于数据操作
R13:作为堆栈指针SP。Cortex-M3 拥有两个堆栈指针,然而它们是联组工作的(banked),因此任一时刻只能使用其中的一个。
- 主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程
- 进程堆栈指针(PSP):由用户的应用程序代码使用
R14:连接寄存器LR,当呼叫一个子程序时,由 R14 存储返回地址。
R15:程序计数寄存器PC,指向当前的程序地址。如果修改它的值,就能改变程序的执行流。
此外,Cortex-M3/M4还在内核的基础上搭建了若干特殊寄存器,比如:
xPSR寄存器:程序状态寄存器,用来记录 ALU 标志(0 标志,进位标志,负数标志,溢出标志),执行状态,以及当前正服务的中断号。
我们关注高四位的位域代表的信息,当指令程序进行CMP运算时,高四位就会表示运算后的结果
CPU与访问内存的方式
CPU中执行的指令代码是用汇编语言编写的,其中主要包括下面几种指令代码:
- 内存R/W
- 运算
- 跳转/分支
- 比较
这些指令组合在一起就组成了CPU内部调用的指令集,常见的指令集有:ARM,Thumb,Thumb2等
ARM指令集是32位的指令集,每条指令都占用32位,我们在编程时利用CODE 32表示ARM指令集
Thumb指令集是16位的指令集,每条指令都占用16位,我们在编程时利用CODE 16表示为Thumb指令集
我们利用CODE 16和CODE 32来区分Thumb和ARM指令集,要节省空间时使用Thumb指令集,要高效时使用ARM指令集,但如果涉及到两种指令集中的代码互相调用时,就需要在调用时设置PC寄存器的bit0位来使能/失能Thumb指令集,使用起来比较繁琐。
于是,ARM引入了Thumb2指令集,该指令集会自动区分不同的指令,对不同的指令采用划分不同的指令位,我们编程时利用Thumb来表明使用的是Thumb2指令集
我们通过STR和LDR指令就可以从相应的内存地址中读取数据或写入数据,通过STM和LDM就可以一次写入或读取多个数据。STM和LDM指令有四种工作模式:
- IA:Increment After,每次传输后 增加待存放的数据地址位,从数据低地址位开始存
- IB:Increment Before,每次传输前 增加待存放的数据地址位,从数据低地址位为开始存储
- DA:Decrement After,每次传输后 减小待存放的数据地址位,从数据高地址位开始存储
- DB:Decrement Before,每次传输前 减小待存放的数据地址位,从数据高地址位为开始存储
注意:内存总是从最低位开始存储数据,也可以理解为数据存储是以小端对齐的方式进行的。
这种内存的方式与栈的访问方式是对应的:
- 根据栈指针的方向不同可分为满栈和空栈:
- 若为空栈,则先入栈(存数据)再调整SP
- 若为满栈,即栈内有数据,则先调整SP,再入栈(存数据)
- 根据压栈时SP的增长方向可以分为增/减:
- 增:SP变大
- 减:SP变小
- 访问栈的四种方式为:
- 满增FI,这就等于IB
- 满减FD,这就等于DB
- 空增EI,这就等于IA
- 空减ED,这就等于DA
但是注意:同一种访问方式对于压栈和出栈的表现不同,比如拿最常用的满减(FD)举例,对于压栈STM而言,是在内存中向下位移SP后存入数据,对于出栈LDM而言,是在内存先读取当前SP指向的内存在向上位移SP。
可以看到,对于同一种访问栈的方式,压栈和出栈时的操作是正好相反的,对于满减的栈,在入栈时需要先减SP后存数据,在出栈时就需要先读数据后加SP。
数据处理指令
由于ARM属于精简指令计算机,其对于内存只有读/写两种指令,而数据的运算都在CPU内部实现
其中主要的数据处理指令有:
- ADD R0,R1,R2:判断R0 == R1+R2
- SUB R0,R1,R2:判断R0 == R1-R2
- AND R0,R1,R2:判断R0 == R1&R2
- ORR R0,R1,R2:判断R0 == R1|R2
- BIC R0,R1,R2:判断R0 == R1&~R2,清除某位
- CMP R0,R1:得出比较的结果:R0-R1
- TST R0,R1:得出测试的结果:R0&R1
除此之外,写在CPU内部的汇编指令中也需要一些跳转指令,来与外部程序接轨。
在使用跳转指令之前要注意保存返回地址,使用的就是上面介绍到的R14:LR寄存器
常用的跳转指令有:
- B:Branch,只跳转
- BL:Branch with Link,跳转之前先把返回地址保存在LR寄存器中
- BX:Branch and eXchange,根据跳转地址的指令集类型切换BIT0状态
- BLX:Branch with Link and eXchange,根据跳转地址的指令集类型切换BIT0状态
- BIT0 = 0:ARM状态
- BIT0 = 1:Thumb状态
练习BL指令的代码如下:
BL DELAY ;BL执行后,跳转到DELAY地址段的指令,并且LR寄存器指向下一条指令的地址
MOV R1,#1
DELAY
MOV R0,#5
LOOP
SUB R0,R0,#1 ;R0=R0-1
BNE LOOP ;NE代表不相等,BNE代表仅在R0!=R0-1时跳转
MOV PC,LR ;将LR寄存器的值赋给PC
从上面代码可以看到BL执行后会跳转到对应标号的地址,并且会将BL指向下一条指令的地址
于是上面的代码还可以进一步简化
ADR LR,RET ;对LR赋值,当回调完成后跳转到对应标号地址处
ADR PC,DELAY ;对PC寄存器直接赋值,使代码直接跳转到对应标号地址处
RET
MOV R1,#1
DELAY
MOV R0,#5
LOOP
SUB R0,R0,#1
BNE LOOP
MOV PC,LR