R0~R12为通用目的寄存器,其中:R0-R7为低组寄存器,字长32位,由于指令中可用的空间有限,绝大多数16位指令只能访问低组寄存器;R8-R12为高组寄存器,字长32位,只有很少的16位Thumb指令可以访问它们。
注意:32位Thumb-2指令可用访问所有通用寄存器,R0~R12复位后的初始值未知。
CM3/4处理器内核中有两个堆栈指针:
裸机一般只用MSP,用到OS时才会使用PSP。
当引用R13时,引用到的是当前正在使用的那一个,另一个必须用MRS/MSR指令来访问。
堆栈指针的选择由特殊寄存器CONTROL
(后面会说到)决定。
堆栈指针访问堆栈时的操作:
堆栈由一块连续的内存和一个栈顶指针组成,用于实现LIFO的缓冲区。典型应用:在数据处理前先保存寄存器的值,任务处理完后再从堆栈中恢复先前保护的值。
OS中上下文切换时的保护现场与恢复现场,就是用堆栈来保存和恢复数据的。
PUSH
:把若干寄存器的值压入堆栈中POP
:从堆栈中弹出若干的寄存器的值在执行PUSH
和POP
时,SP地址寄存器的值由硬件自动调整,以避免后续操作破坏先前的数据。同时PUSH
和POP
操作必须是4
字节对齐的,而且R13的最低两位被硬件连接到0,因此总是读出0。
ARM堆栈是向下生长的满栈,在PUSH
新数据时,SP指针先减一个单元(弹栈时SP增加一个单元,即向高地址方向移动)。
通常在进入一个子程序后,第一件事就是把寄存器的值先PUSH
压栈,在程序退出后POP
弹出之前压入栈中的寄存器。此外,PUSH
和POP
还能一次操作多个寄存器:
PUSH {R0-R7, R12, R14} ; 保存寄存器列表(压入R0-R7,R12,R14)
… ; 执行处理
POP {R0-R7, R12, R14} ; 恢复寄存器列表(弹出R0-R7,R12,R14)
BX R14 ; 返回到主调函数
注意:在寄存器列表中,不管寄存器的序号是以什么顺序给出的,汇编器都将把它们升序排序。然后先push序号大的寄存器,所以也就先pop序号小的寄存器。(不按升序写,有些汇编器会报错)
LR
的值加载到PC
中,子程序即返回调用程序处并继续执行。汇编中使用BL
或BLX
指令调用子程序时,LR
值会自动被设置为该子程序返回地址。下面两种方式实现子程序返回操作:MOV PC, LR ; 把LR值传到PC,程序返回(这条语句相对于C语言return)
BX LR ; 跳转到LR保存的原函数地址处
注意:若某函数需要调用另外一个函数时(嵌套调用),它需要先将LR
的值保存到栈中,否则,当执行了函数调用后,LR
的当前值会丢失。例如:
main ;主程序
…
BL func1 ; 跳转到func1(跳转前PC=func1, LR=main的下一条指令地址)
…
Func1
… ; func1 的代码
BX LR ; 函数返回(如果func1要使用LR,必须在使用前PUSH,否则返回时程序就可能跑飞了)
LR
会自动更新为该异常返回地址,之后会在异常处理结束时触发异常返回。此外,LR
也可以当做通过寄存器来使用。
Cortex-M处理器的返回地址总是偶数(由于指令会对齐到半字上,因此第0位始终为0),但LR
的第0位为可读写,有些跳转/调用操作需要将LR
(或正使用的任何寄存器)的第0位(LSB)置1以表示Thumb
状态(0表示ARM
状态)。
这是历史遗留问题。现在的ARM处理器已经都采用
Thumb
指令集了,但为了兼容老的ARM
与Thumb
状态并存的处理器,需要允许LR
的bit[0]
可读写。
表示当前指令地址,写入新值即可跳转,汇编一般写成PC
,由于CM3内部使用了指令流水线,读PC时返回的值是当前指令的地址+4
。例如:
0x1000: MOV R0, PC ; R0 = 0x1004
因为指令至少半字(两个字节)对齐,所以PC
的最低位(LSB)总为0,但是使用跳转/读存储器的指令更新PC
时,需要将PC
的LSB置1表示Thumb
状态,否则就会由于试图使用不支持的ARM
指令而触发fault异常(LSB为0表示ARM
状态)。对于高级编程语言(C/C++等),编译器会自动将跳转目标的LSB置位。
特殊功能寄存器有预定义的功能,必须通过专用的指令MSR/MRS
来访问,而且它们也没有与之相关联的访问地址。
MRS
(Move to Register From Special Register):加载特殊功能寄存器的值到通用寄存器。MSR
(Move to Special Register From Register):存储通用寄存器的值到特殊功能寄存器。MRS <reg>, <special_reg> ; 读特殊功能寄存器的值到通用寄存器
MSR <special_reg>, <reg> ; 写通用寄存器的值到特殊功能寄存器
Cortex-A7中为CPSR(当前程序状态寄存器),与M3/4有所出入
注意:GE[3:0]
仅在Cortex-M4和CortexA/R系列存在,CM3中没有。
程序状态寄存器xPSR
包括以下三个状态寄存器:
ALU
(arithmetic and logic unit)标志ARM汇编器使用PSR
同时访问以上三个状态寄存器,同时它们也支持单独访问:
MRS R0 PSR ; 读组合程序状态寄存器值
MSR PSR R0 ; 写组合程序状态寄存器值
-----------------------------------
MRS R0, APSR ; 将状态标志读入到RO
MRS R0, IPSR ; 读取异常/中断状态
MSR APSR, R0 ; 写状态标志
汇编代码无法使用MRS/MSR
直接访问EPSR
,IPSR
为只读。
此外,还可以组合访问:
IAPSR = IPSR + APSR
IEPSR = IPSR + EPSR
EAPSR = EPSR + APSR
CMSIS-Core还提供了C函数接口(MDK的ACM6编译器可F12跳转查看,在cmsis-armclang
中可查看):
uint32_t __get_IPSR(void);
uint32_t __get_APSR(void);
uint32_t __get_xPSR(void);
PSR
组合状态寄存器中的位域:
APSR
位域N
N = 1
表示结果为负数,N = 0
为正数。Z
Z = 1
表示运算结果为0,Z = 0
表示运算结果不为0;对于CMP
比较指令,Z = 1
表示进行比较的两个数大小相等。C
1.
在加法指令中(包括比较指令CMN
),当结果产生了进位,则C = 1
,表示无符号数运算发生上溢出;其他情况C = 0
;2.
在减法指令中(包括比较指令CMP
),当结果发生错位,则C = 0
,表示无符号数运算发生下溢出;其他情况C = 1
;3.
对于包含移位操作的非加/减法运算指令,C
为最后一次被溢出的位的数值;4.
对于其他非加/减法运算指令,C
位的值通常不受影响。V
V = 1
表示符号位溢出,其他指令不影响V
位。ALU
标志运算示例:
此外,很多16位指令会影响这4个ALU
标志,对于32位指令,指令编码中的一个位定义了是否应该更新APSR
标志,注意:部分指令不会更新V
标志和C
标志,例如:MULS
(乘法)指令只会修改N
标志和Z
标志。
除条件跳转或条件执行代码,APSR
的进位标志也可以用于将加法和减法的运算扩大为超过32位,例如,将两个64位整数相加时,可以将低32位加法运算结果的进位标志作为高32位加法的一个输入:
// 计算 Z = X + Y,其中X, Y, Z均为64位
Z[31:O] = X[31:0] + Y[31:0]; //低字相加,更新进位标志
Z[63:32] = X[63:32] + Y[63:32] + Carry; //高字相加
用于指示增强的DSP指令是否发生溢出(饱和),该位被设置后,以及在软件写APSR
清除Q
位前,它会一直保持置位状态,饱和运算/调整运算不会清除该位。因此,可以在饱和运算/调整运算结束时,利用该位确定是否发生了饱和,而无须每步都检查饱和状态。
饱和算术运算对于数字信号处理非常有用,有些情况下,保存计算结果的目的寄存器的位宽度可能会不够,这样就会导致上溢或下溢。 若使用一般的数据运算指令,结果的MSB就会丢失,从而导致结果产生严重畸变。 饱和算术运算并非只是将MSB去掉,而是将结果强制置为最大值(上溢的情形)或最小值(下溢的情形),以降低信号畸变的影响。
实际触发饱和的最大值和最小值取决于所使用的指令。 多数情况下,饱和运算指令的助记符前都带有Q,如QADD16。 若产生了饱和,Q位就会置位,否则 Q位的数值 就不会改变。
Cortex-M3处理器提供了一些饱和调整指令,而除这些指令外,Cortex-M4还支持一整套饱和运算指令。
在Cortex-M4中,大于等于标志(GE
)在APSR
中占用4
位,而Cortex-M3处理器中则不存在。许多SIMD指令(single instruction multiple data,单指令流多数据流)都会更新该标志,其中,多数情况下,每个位表示SIMD
运算的每个字节为正或溢出。对于具有16位数据的SIMD
指令,第0和1位由低半字的 结果控制,第2和3位则由高半字的结果控制。
IPSR
位域EPSR
位域
三个寄存器常用于控制异常或中断的使能和除能。每个异常都有优先级,数值越小优先级越高,这些特殊寄存器可基于优先级屏蔽异常,只有在特权访问
模式才可以对它们操作,用户模式写操作会被忽略,读返回0。
可以用MRS/MSR
指令来读写它们。
PRIMASK
PRIMASK
只有1
位,缺省值为0,被置1后,它会阻止除NMI和HardFault异常之外的所有异常(包括中断),实际上,它是将当前异常优先级提升为0
(可编程中断/异常最高优先级)。
在RT-Thread中,就是通过其来开关中断以保护临界资源(中断锁)。
为了快速关中断,CM3还专门设置了一条CPS(修改处理器状态)指令,用法如下:
CPSID I ;PRIMASK=1 关中断
CPSIE I ;PRIMASK=0 开中断
MSR/MRS
指令操作:
MOVS R0, #1 ; 向PRIMASK写1禁止所有中断
MSR PRIMASK, R0
MOVS R0, #0 ; 向PRIMASK写0使能中断
MSR PRIMASK, R0
MRS R0, PRIMASK ; 将PRIMASK读入R0
CMSIS-Core提供的C函数接口
void __disable_irq(); // PRIMASK=1 关中断
void __enable_irq(); // PRIMASK=0 开中断
void __set_PRIMASK(uint32_t priMask); // 设置PRIMASK
uint32_t __get_PRIMASK(void); // 读取PRIMASK
当PRIMASK
被置位时,所有的错误事件都会触发HardFault
异常,而不论相应的可配置错误异常是否使能。
FAULTMASK
FAULTMASK
只有1
位,与PRIMASK
类似,缺省值为0,被置1后,只有NMI
能够响应,HardFault
异常也被屏蔽掉,实际上是将异常优先级提升到-1
,这样可以使用HardFault
的一些特殊特性:
这样FAULTMASK
可在配置错误处理执行期间,阻止其他异常或中断处理的执行。(不可以在NMI
和HardFault
中设置它)
与PRIMASK
不同,FAULTMASK
在异常返回时会被自动清除(从NMI
退出除外)。由于这个特点,FAULTMASK就有了 个很有趣的用法:若要在低优先级的异常处理中触发一个高优先级的异常(NMI
除外),但想在低优先级处理完成后再处理器高优先级,可以:
NMI
除外)由于在FAULTMASK
置位时,挂起的高优先级异常处理无法执行,高优先级的异常就会在FAULTMASK
被清除前继续保待挂起状态,低优先级处理完成后才会将其清除。 因此,可以强制让高优先级处理在低优先级处理结束后开始执行。
CPS
指令:
CPSID F ;FAULTMASK=1 关异常
CPSIE F ;FAULTMASK=0 开异常
MSR/MRS
指令:
MOVS R0, #1 ; 向FAULTMASK写1禁止所有中断
MSR FAULTMASK, R0
MOVS R0, #0 ; 向FAULTMASK写0使能中断
MSR FAULTMASK, R0
MRS R0, PRIMASK ; 将FAULTMASK读入R0
CMSIS-Core提供的C函数:
void __disable_fault_irq(); // FAULTMASK=1 关异常
void __enable_fault_irq(); // FAULTMASK=0 开异常
void __set_FAULTMASK(uint32_t faultMask); // 设置FAULTMASK
uint32_t __get_FAULTMASK(void); // 读取FAULTMASK
FAULTMASK
专门留给OS用
BASEPRI
BASEPRI
寄存器会根据优先级屏蔽异常或中断,最多有9
位,其位宽取决于设计表达优先级的位数,大多数CM3/4的MCU都有8/16个可编程的异常优先级,此时BASEPRI
位宽就为3/4位,如STM32F103
中,有4位用于表达优先级:
可以通过NVIC
与SCB
寄存器来配置优先级。
当需要禁止优先级低于某特定等级的中断时,将屏蔽优先级写入到BASEPRI
寄存器即可。例如,若要屏蔽优先级小于等于0x60的所有异常:
void __set_BASEPRI(uint32_t basePri);
__set_CONTROL(0x60); // 禁止优先级在0x60~0xFF的中断
// 读取BASEPRI
int32_t __get_BASEPRI(void);
// 取消屏蔽
__set_CONTROL(0); // 写0即可
MOVS RO, #0x60 ; 禁止优先级在0x60~0xFF的中断
MSR BASEPRI, R0
MRS R0, BASEPRI ; 读取BASEPRI
MOVS RO, #0 ;取消屏蔽
MSR BASEPRI, R0
此外,BASEPR
寄存器可用过别名BASEPR_MAX
访问,当使用这个名称时,会得到一个条件写操作,处理器会自动比较当前值与新的数值,只有新的优先级更高才会允许修改。(修改更低的优先级使用BASEPR
名称)
// 当BASEPRI被屏蔽/禁用或添加新值到BASEPRI时,可调用以下函数
void __set_BASEPRI_MAX(uint32_t basePri);
MOVS RO, #0x60 ; 禁止优先级在0x60~0xFF的中断
MSR BASEPR_MAX, R0
MOVS RO, #0xF0 ; 优先级低于上次0x60(越小越高), 写操作不起作用
MSR BASEPR_MAX, R0
MOVS RO, #0x40 ; 禁止优先级在0x40~0xFF的中断, 写操作修改成功
MSR BASEPR_MAX, R0
BASEPR
寄存器格式与优先级寄存器宽度有关,若表达优先级的位数为3位,则BASEPR
可被设为:0x00, 0x20, 0x40,… ,0xC0和0xE0。
CONTROL
仅在特权级下才允许写操作,而读操作特权和非特权访问都可以。
CONTROL[0]
:定义线程模式中的特权等级handler
模式永远都是特权级的)CONTROL[1]
:定义栈指针的选择handler
模式只允许使用MSP(此时不应该往该位写1)。因此,只有特权级的线程模式下才可以写此位。CONTROL[2]
:异常处理机制使用该位确定异常产生是浮点单元中的寄存器是否需要保存CONTROL
值全为0,即处于特权级的线程模式,使用主堆栈指针MSP。此时可以写CONTROL
寄存器,但是当bit[0]
被置1后,处于用户级的线程模式就不能访问CONTROL
了。此时需利用异常机制,在异常处理期间清除该位,返回特权级(异常服务函数永久处于特权级):CONTROL[0]
的bit[0]
与bit[1]
的组合:
一般不使用RTOS,则无须修改CONTROL
值,整个应用运行在特权级的线程模式,且只使用MSP:
操作CONTROL
寄存器:
uint32_t __get_CONTROL(void);
void __set_CONTROL(uint32_t control);
MRS RO, CONTROL ; 将CONTROL读入RO
MSR CONTROL, R0 ; 将RO写入CONTROL
可以通过检查IPSR
和CONTROL
寄存器数值确认是否为特权级:
int in_privileged(void)
{
if(__get_IPSR() != 0) // 判断是否在异常服务例程中(特权级)
return true;
else
if(__get_CONTROL() & 0x1 == 0) // 仅特权级可使用MSP
return true;
else
return false;
}
参考:计算机原理与应用 第二章——ARM处理器_南瑾与春风的博客-程序员秘密
END