ARM汇编(适用于ARM realview工具链 DS-5 Keil微控制器开发套件)指令格式如下:
label
mnemonic operand1,operand2, … ;注释
label(标号)表示地址位置,是可选的。有些指令的前面可能会有标号,这样就可以通过这个标号得到指令的地址。标号也可以用于表示数据地址。例如,可以在程序内的查找表处放一个标号。mnemonic为助记符,也就是指令的名称,其后跟着的是多个操作数。
对于在ARM汇编器中编写的数据处理指令,第一个操作数为操作的目的。
对于存储器读指令(多加载指令除外),第一个操作数为数据被加载进去的寄存器
对于存储器写指令(多存储指令除外),第一个操作数为保存待写入寄存器的数据的寄存器。
处理多次加载和存储的指令具有不同的语法。
每条指令的操作数个数由指令的类型决定。
有些指令不需要任何操作,而有些则可能只需要一个。
处理多次加载和存储的指令具有不同的语法。
每条指令的操作数个数由指令类型决定。
有些指令不需要任何操作,而有些则可能只需要一个。
注意,助记符后可能会存在不同类型的操作数,这样可能会得到不同的指令编码。例如,MOV指令可以在两个寄存器间传输数据,也可以将立即数放到寄存器中。指令中操作数的个数取决于指令的种类,而操作数的语法也可能会各不相同。
对于GNU工具链,汇编语法一般为:
label
mnemonic operand1,operand2, /*注释 */
ARM汇编,定义常量的一个例子为:
NVIC_IRQ EQU 0xE00E100
NVIC_IRQ0_ENABLE EQU 0x1
GNU工具链的汇编语法:
.equ NVIC_IRQ, 0xE000E100
.equ NVIC_IRQ0_ENABLE, 0x1
多数汇编工具允许将数据插入程序中,这是另外一个典型特性。例如,可以在程序存储器中的特定位置定义数据,并用存储器读指令进行访问。
ARM 汇编器实现:
DCD用于插入字大小的数据,DCB则用于将字节大小的数据插入到程序中。ALGN后的数据决定了对齐的大小。
LDR R3, = MY_NUMBER ;获取MY_NUMBER的存储器位置
LDR R4, [R3] ;将数据值0x12345678读入R4
LDR R0, =HELLO_TEXT ;获取HELLO_TEXT的起始地址
BL PrintText
ALGN 4
MY_NUMBER DCD 0X12345678
HELLO_TEXT DCB"Hello\n", 0 ;以NULL结束的字符串
GNU工具链的汇编语法如下
LDR R3, = MY_NUMBER /*获取MY_NUMBER的存储器位置*/
LDR R4, [R3] /*将数据值0x12345678读入R4*/
LDR R0, =HELLO_TEXT /*获取HELLO_TEXT的起始地址*/
BL PrintText
.algn 4
MY_NUMBER:
.word 0x12345678
HELLO_TEXT:
.asciz"Hello\n" /*以NULL结尾的字符串*/
ARM汇编器与GNU汇编器中多个不同伪指令可将数据插入到程序中。如下表所示
插入的数据类型 | ARM汇编器(如keil MDK-ARM) | GNU汇编器 |
---|---|---|
字节 | DCB 如 DCB 0X12 | .byte 如.byte 0x12 |
半字 | DCW 如DCW 0X1234 | .hword/.2byte 如.hword 0xAA55, |
字 | DCD 如DCD 0X12345678 | .word/.4byte 如.word 0x12345678 |
双字 | DCQ 如DCQ 0X12345678ABCDEFAA | .quad/.octa 如 .quad 0x12345678ABCDEFAA |
浮点(单精度) | DCFS 如DCFS 1E3 | .float 如.float 1E3 |
浮点(半精度) | DCFD 如DCFD 3.14159 | .double 如.double 3.14159 |
字符串 | DCB 如DCB"Hello\n"0, | .ascii/.asciz(以NULL结束) 如.ascii “JNZ” @插入字节0x4A 0x4E 0x5A.asciz “JNZ” @插入字节0x4A 0x4E 0x5A 0x00 |
指令 | DCI 如DCI 0XBE00; | .word/.hword 如.hword 0XBE00 |
多数情况下,还可以在伪指令前加一个标号,以便利用这个标号确定数据的地址。
对于ARM处理器的汇编器,有些指令后会跟着后缀。CORTEX-M处理器可用的后缀如下:
CORTEX-M3和CORTEX-M4处理器的指令可以按功能分为以下几类:
存储器内传送数据、存储器访问、算术运算、逻辑运算、移位和循环移位运算、转换(展开和反转顺序)运算、位域处理指令、
程序流控制(跳转、条件跳转、条件执行和函数调用)、乘累加(MAC)指令、除法指令、存储器屏障指令、异常相关指令、休眠模式相关指令、其他指令。另外,cortex-m4处理器支持增强DSP指令:
SIMD运算和打包指令、快速乘法和MAC指令、饱和运算、浮点指令(前提是浮点单元存在)
微处理器中最基本的操作为在处理器内来回传送数据。例如,可能会
①将数据从一个寄存器送到另外一个寄存器
②在寄存器和特殊寄存器间(如CONTROL PRIMASK FAULTMASK BASEPRI)传送数据
③将立即数送到寄存器
对于具有浮点单元的CORTEX-M4处理器,还可以:
①在内核寄存器组中的寄存器和浮点单元寄存器组中的寄存器间传送数据。
②在浮点寄存器组中的寄存器间传送数据
③在浮点系统寄存器和内核寄存器间传送数据
④将立即数送入浮点寄存器。
处理器内传送数据的指令
指令 | 目的 | 源 | 操作 |
---|---|---|---|
MOV | R4, | R0 | 从R0复制数据到R4 |
MOVS | R4, | R0 | 从R0复制数据到R4,且更新APSR(标志) |
MRS | R7, | PRIMASK | 将数据从PRIMASK复制到R7 |
MSR | CONTROL, | R2 | 将数据从R2复制到CONTROL |
MOV | R3, | #0X34 | 设置R3为0x34 |
MOVS | R3, | 0X34 | 设置R3为0x34,且更新APSR(标志) |
MOVW | R6, | #0X1234 | 设置R6为16位常量0x1234 |
MOVT | R6, | #0x8765 | 设置R6高16位为0x8765 |
MVN | R3, | R7 | 将R7取反后送至R3 |
浮点单元和内核寄存器间传送数据的指令
指令 | 目的 | 源 | 操作 |
---|---|---|---|
VMOV | R0, | S0 | 将数据从浮点寄存器S0复制到通用目的寄存器R0 |
VMOV | S0, | R0 | 将数据从通用目的寄存器R0复制到浮点寄存器S0 |
VMOC | S0, | S1 | 将数据从浮点寄存器S1复制到S0(单精度) |
VMRS.F32 | R0, | FPSCR | 将数据从浮点单元系统寄存器FPSCR复制到R0 |
VMRS | APSR_nzcv, | FPSCR | 复制FPSCR中的标志到APSR中的标志 |
VMSR | FPSCR, | R3 | 复制R3到浮点单元系统寄存器FPSCR |
VMOV.F32 | S0, | #1.0 | 将单精度数据送到浮点单元寄存器S0 |
要将寄存器设置为一个较大的立即数(9-16位),可以使用MOVW指令。根据所使用的汇编器工具,若立即数位于9~16位,MOV或MOVS可能会被自动转换为MOVW.
若需要将寄存器设置为32位立即数,可以使用多种方法。最常见的方法为利用一个名为LDR的伪指令。例如:
LDR R0, =0x12345678 ;将R0设置为0x12345678
这不是一个实际的指令,汇编器会将其转换为存储器传输指令及存储在程序映像中的常量
LDR R0, [PC, #offset]
…
DCD 0x12345678
LDR读取[PC+offset]位置的数据,并存入R0。注意,由于处理器的流水线结构,PC的值并非LDR指令的地址,不过汇编器会计算偏移,因此,不必担心。
若需要将寄存器设置为程序代码中位于一定范围内的地址,则可以使用ADR伪指令,它会被转化为一个单独的指令;或者使用ADRL伪指令,它可以提供更大的地址范围,不过会被转换为两条指令。例如:
ADR R0, DataTable
...
ALIGN
DataTable
DCD 0, 245, 132, ...
ADR指令会被转换为基于程序计数器数值的加法或减法运算。
另外一种生成32位立即数的方法为组合使用MOVW和MOVT指令。例如:
MOVW R0, #0X789A ;设置R0为0x0000789a
MOVT R0, #0X3456 ;设置R0的高16位设置为0x3456,目前R0=0X3456789A
与使用LDR伪指令的方法相比,LDR方法的可读性更好,而且若同一个常量在程序代码中使用了多次,汇编器可能会重用相同的常量数据以降低代码大小。不过,取决于存储器系统的设计,若使用了系统级的缓存且LDR会带来数据缓存丢失,有些情况下MOVW+MOVT方法生成的代码可能会执行更快。
cortex-m3和cortex-m4处理器支持许多存储器访问指令,这是因为寻址模式及数据大小和数据传输方向具有多种组合方式。对于普通的数据传输,可用下表指令访问:
数据类型 | 加载(读存储器) | 存储(写存储器) |
---|---|---|
unsigned char | LDRB | STRB |
char | LDRSB | STRB |
unsigned short | LDRH | STRH |
short | LDRSH | STRH |
int | LDR | STR |
多个int | LDM | STM |
双字 | LDRD | STRD |
栈操作(32位) | POP | PUSH |
LDRSB和LDRSH会对被加载数据自动执行有符号展开运算,将其转换为有符号的32位数据。如LDRB 0X83,则数据被放到目的寄存器前会被转换为0xFFFFFF83.
对于浮点单元可用如下指令在浮点单元寄存器和存储器间数据传送。
数据类型 | 加载(读存储器) | 存储(写存储器) |
---|---|---|
单精度数据(32位) | VLDR.32 | VSTR.32 |
双精度数据(64位) | VLDR.64 | VSTR.64 |
多数据 | VLDM | VSTM |
栈操作 | VPOP | VPUSH |
寻址模式有多种,在有些模式下,还可以选择更新保存地址的寄存器(写回)。
1.立即数寻址(前序)
数据传输的存储器地址为寄存器中的数值和立即数常量(偏移)的加和,有时被称作前序寻址。如:
LDR R0, [R1,#0X03] ;从地址R1+0X3中读取一个字节并将其存入R0.
该寻址模式支持对存放地址的寄存器的写回,例如:
LDR R0, [R1,#0X03]! ;从地址R1+0X3中读取一个字节并将其存入R0, R1被更新为R1+0X3。
指令中的! 表示指令完成时是否更新存放地址的寄存器(写回)。
2.PC相关寻址(文本)
存储器访问可以产生相对于当前PC的地址值和偏移值。它常用于将立即数加载到寄存器中,也可被称作文本池访问。
如 LDRB R1, [PC, #offset]
3.寄存器便宜(前序)
用于所处理的数据数组的地址为基地址和从索引值计算出的偏移得到的情况。为了进一步提高地址计算的效率,在加到基地址寄存器前,索引值可以进行0~3位的移位。例如:
LDR R3, [R0, R2, LSS # 2] ;将存储器[R0+(R2<<2)]读入R3
STR R5, [R0,R7] ;将R5写入存储器[R0+R7]
4.后序
具有立即数寻址模式的存储器访问指令也有一个立即数偏移数值。不过在存储器访问期间是不会用到偏移的,它会在传输结束后更新地址寄存器。例如:
LDR R0, [R1], #offset ;读取存储器[R1],然后R1被更新为R1+offset
后序寻址模式在处理数组中的数据时非常有用,在访问数组中的元素时,地址寄存器可以自动调整,节省了代码大小和执行时间。
注意:后序指令中不能使用PC 和 SP,后序存储器访问指令都是32位的,offset可以为整数或附属。
5.多加载和多存储
ARM架构的一个重要优势在于,可以读或写存储器中多个连续数据,LDM(加载多个寄存器)和STM(存储多个寄存器)指令只支持32位数据,它们支持两种前序:
IA:在每次读/写后增加地址
DB:在每次读/写前减小地址
LDM 和STM指令在使用时可以不进行基地址写回,如下表所示:
多加载/存储实例 | 描述 |
---|---|
LDMIA Rn, | 从Rn指定的存储器位置读取多个字,地址在每次读取后增加(IA) |
LDMDB Rn, | 从Rn指定的存储器位置读取多个字,地址在每次读取前减小(DB) |
STMIA Rn, | 往Rn指定的存储器位置写入多个字,地址在每次写入后增加 |
STMDB Rn, | 往Rn指定的存储器位置写入多个字,地址在每次写入前减小 |
寄存器列表可以是不连续的,如{R1, R3, R5-R7,R9, R11-R12},其中包括R1、R3、R5、R6、R7、R9、R11、R12。
6.压栈和出栈
栈的push和pop为另外一种形式的多存储和多加载,它们利用当前选定的栈指针来生成地址。当前栈指针可以是主栈指针(msp),也可以是进程栈指针(PSP),实际选择是由处理器的当前模式和CONTROL寄存器决定的。
PUSH ;将寄存器存入栈中
POP ;从栈中恢复寄存器
7 SP相关寻址
除了用于函数或子例程的寄存器临时存储,栈空间还常用于局部变量,而访问这些变量需要SP相关的寻址。使用SP相关的寻址模式的一个例子如下:在函数开始处,为了给局部变量预留出空间,SP数值减小,这些局部变量可以用SP相关的寻址模式进行访问;在函数结束时,SP增大且恢复为初始值,这样在返回到调用代码前会将已分配的栈空间释放。
8 排他访问
排他访问指令为一组特殊的存储器访问指令,用于实现信号量或互斥量操作。它们常见于嵌入式OS中,其中的某个资源需为多个应用任务甚至多个处理器共用。
排他访问指令包括排他加载和排他存储,要监控排他访问,需要使用处理器内或总线中的特殊硬件。处理器内部存在一个仅有一位的寄存器,它可以记录排他访问流程,将其称为本地排他访问监控。在系统总线级可能也会存在排他访问监控,以确定排他访问使用的某个存储器位置(或存储器设备)是否 被另外一个处理器或总线主控访问。处理器在总线接口上存在额外的信号,以表明传输为排他访问以及接收系统总线级排他访问监控的回应。
排他访问指令列表
排他访问示例 | 描述 |
---|---|
LDREXB Rt, [Rn] | 从存储器位置Rn排他读取字节 |
LDREXH Rt, [Rn] | 从存储器位置Rn排他读取半字 |
LDREX Rt, [Rn, offset] | 从存储器位置Rn排他读取字 |
STREXB Rd,Rt, [Rn] | 往存储器位置Rt排他存储字节,返回状态位于Rd中 |
STREXB Rd,Rt, [Rn] | 往存储器位置Rt排他存储半字,返回状态位于Rd中 |
STREXB Rd,Rt, [Rn,#offset] | 往存储器位置Rt排他存储字,返回状态位于Rd中 |
CLREX | 强制本地排他访问监控清零,使得下一次排他存储失败。它并不是排他存储器访问指令,不过由于它的用法,在这里列出来。 |
CORTEX-M3和CORTEX-M4提供了用于算术运算的多个指令,下表列出了一些常用的算术指令。
常用算术指令(可选后缀未列出来) | 操作 |
---|---|
ADD Rd, Rn, Rm ;Rd = Rn + Rm | ADD运算 |
ADD Rd, Rn,#immed ;Rd = Rn + #immed | ADD运算 |
ADC Rd, Rn, Rm ;Rd = Rn + Rm +进位 | 带进位的ADD |
ADC Rd, Rn, #immed ;Rd = Rn + #immed+进位 | 带进位的ADD |
ADDW Rd, Rn, #immed ;Rd = Rn + #immed | 寄存器和12位立即数相加 |
SUB Rd,Rn, Rm ;Rd = Rn - Rm | 减法 |
SUB Rd, #immed ;Rd = Rd -#immed | 减法 |
SUB Rd, Rn, #immed ;Rd = Rn - #immed | 减法 |
SUBC Rd, Rn, #immed ;Rd = Rn - #immed -借位 | 带借位的减法 |
SUBC Rd, Rn,Rm ;Rd = Rn - Rm -借位 | 带借位的减法 |
SUBW Rd, Rn, #immed ;Rd = Rn - #immed | 寄存器和12位立即数相减 |
RSB Rd, Rn, #immed ;Rd = #immed - Rn | 减反转 |
RSB Rd, Rn,Rm ;Rd = Rm - Rn | 减反转 |
MUL Rd, Rn,Rm ;Rd = Rn * Rm | 乘法(32位) |
UDIV Rd, Rn,Rm ;Rd = Rn / Rm | 无符号除法 |
SDIV Rd, Rn,Rm ;Rd = Rn / Rm | 有符号除法 |
这些指令在使用时可以带着或不带S后缀以及指明APSR是否应更新。
若出现被零除的情况,UDIV和SDIV指令的结果默认为0.可以设置NVIC配置控制寄存器中的DIVBYZERO位,这样在出现被零除时就可以产生异常。M3 M4都支持具有32位和64位结果的32位乘法指令和乘累加(MAC)指令,这些指令支持有符号和无符号的形式,APSR标志不受这些指令影响。
指令(由于apsr不更新,因此无S后缀) | 操作 |
---|---|
MLA Rd,Rn, Rm, Ra ;Rd= Ra + Rn* Rm | 32位MAC 指令,32位结果 |
MLS Rd,Rn, Rm, Ra ;Rd= Ra - Rn* Rm | 32位乘减指令,32位结果 |
SMULL RdLo, RdHi, Rn, Rm ; {RdHi, RdLo} = Rn * Rm | 有符号数据的32位乘 64位结果 |
SMLAL RdLo, RdHi, Rn, Rm ; {RdHi, RdLo} += Rn * Rm | MAC指令,64位结果 |
UMULL RdLo, RdHi, Rn, Rm ; {RdHi, RdLo} = Rn * Rm | 无符号数据的32位乘 64位结果 |
UMLAL RdLo, RdHi, Rn, Rm ; {RdHi, RdLo} += Rn * Rm | MAC指令,64位结果 |
逻辑运算指令如下表所示:
指令(可选的S后缀未列出来) | 操作 |
---|---|
AND R1, R2 ;R1= R1 & R2 | 按位与 |
AND R1, R2 ,#immed ;R1= R2 & #immed | 按位与 |
AND R1, R2 ,R3 ;R1= R2 & R3 | 按位与 |
ORR R1, R2 ;R1= R1 | R2 | 按位或 |
ORR R1, R2 ,#immed ;R1= R2 | #immed | 按位或 |
ORR R1, R2 ,R3 ;R1= R2 | R3 | 按位或 |
BIC R1, R2 ;R1= R1 & (~R2) | 位清除 |
BIC R1, R2 ,#immed ;R1= R2 & (~#immed) | 位清除 |
BIC R1, R2 ,R3 ;R1= R2 & (~R3) | 位清除 |
ORN R1, R2 ,#immed ;R1= R2 | (w#immed) | 按位或非 |
ORN R1, R2 ,R3 ;R1= R2 | (wR3) | 按位或非 |
EOR R1, R2 ;R1= R1 ^ R2 | 按位异或 |
EOR R1, R2 ,#immed ;R1= R2 ^ #immed | 按位异或 |
EOR R1, R2 ,R3 ;R1= R2 ^ R3 | 按位异或 |
若使用这些指令的16位版本,则只能操作两个寄存器,且目的寄存器需要为源寄存器之一。另外,还必须是低寄存器(R0~R7),而且要使用S后缀。ORN指令没有16位的形式。
CORTEX-M3/M4处理器支持多种移位和循环移位指令,如下表所示
指令(可选的S后缀未列出来) | 操作 |
---|---|
ASR R1, R2 ;R1= R1 >> R2 | 算术右移 |
ASR R1, R2 ,#immed ;R1= R2 >> #immed | 算术右移 |
ASR R1, R2 ,R3 ;R1= R2 >> R3 | 算术右移 |
LSL R1, R2 ;R1= R1 << R2 | 逻辑左移 |
LSL R1, R2 ,#immed ;R1= R2 << #immed | 逻辑左移 |
LSL R1, R2 ,R3 ;R1= R2 << R3 | 逻辑左移 |
LSR R1, R2 ;R1= R1 >> R2 | 逻辑右移 |
LSR R1, R2 ,#immed ;R1= R2 >> #immed | 逻辑右移 |
LSR R1, R2 ,R3 ;R1= R2 >> R3 | 逻辑右移 |
ROR R1, R2 ,R3 ;R1= R2 右移 R3 | 循环右移 |
ROR R1, R2 ;R1= R1 右移 R2 | 循环右移 |
RRX R1, R2 ;{C,R1} ={R2, C} | 循环右移并展开 |
若使用了S后缀,这些循环和移位指令也会更新APSR中的进位标志。若移位运算移动了寄存器中的多个位,进位标志C的数据就会为移出寄存器的最后一位。
循环左移运算可以由循环右移一定数量代替,故没有循环左移指令。
要使用这些指令的16位版本,寄存器需要为低寄存器,而且应该使用S后缀。RRX指令没有16位形式。
对于cortex-m3和cortex-m4处理器,用于处理数据的有符号和无符号展开的指令有很多,如将8位数转换为32位或将16位转换为32位。有符号和无符号指令都有16位和32位的形式,如下表所示。这些指令的16位版本只能访问低寄存器。
指令 | 操作 |
---|---|
SXTB R1, R2 ;R1=有符号展开R2[7:0] | 有符号展开字节为字 |
SXTH R1,R2 ;R1= 有符号展开R2[15:0] | 有符号展开半字为字 |
UXTB R1, R2 ;R1=无符号展开R2[7:0] | 无符号展开字节为字 |
UXTH R1,R2 ;R1= 无符号展开R2[15:0] | 无符号展开半字为字 |
cortex-m3和cortex-m4处理器支持多种位域处理器运算。位域处理指令如下表:
指令 | 操作 |
---|---|
BFC R1, #, # | 清除寄存器中的位域 |
BFI R1,R2, #, # | 将位域插入寄存器 |
CLZ R1, R2 | 前导零计数 |
RBIT R1, R2 | 反转寄存器中的位顺序 |
SBFX R1,R2, #, # | 从源中复制位域并有符号展开 |
UBFX, R1, R2, #, # | 从源寄存器中复制位域 |
BFC(位域清除)清除寄存器任意相邻的1~31位。该指令的语法为:
BFC R1, #, #
例如:
LDR R0, =0X1234FFFF
BFC R0, #4, #8
得到结果为 0X1234F00F
BFI(位域插入)将一个寄存器的1~31位(#width)复制到另外一个寄存器的任意位置上。
LDR R0, =0X12345678
LDR R1, = 0X3355AACC
BFI R1,R0, #8, #16 ;将R0[15:0]插入 R1[23:8]
得到结果R1 = 0X335678CC
CLZ计算前导零的个数,若没有位为1则结果为32,所有位都为1而结果为0.其通常用于在对数据进行标准化处理时确定移位的个数,以便将第一个1移到第31位。在浮点计算中经常用到。
RBIT指令反转字数据中位顺序。该指令常在数据通信中用于串行位数据流的处理,如R0 为0XB4E10C23,二进制数据为10110100111000010000110000100011 执行RBIT R0, R1后R1 会变为:11000100001100001000011100101101,即0xC430872D
UBFX从寄存器的任意位置(由操作数<#lsb>指定)开始提取任意宽度(由操作数<#width>指定)的位域,将其零展开后放入目的寄存器。
LDR R0, =0X5678ABCD
UBFX R1, R0, #4, #8
得到的结果为R1 = 0X000000BC
类似地,SBFX提取出位域,不过会在放入目的寄存器前进行有符号展开,例如:
LDR R0, =0X5678ABCD
SBFX R1, R0, #4, #8
得到的结果为R1 = 0XFFFFFFBC
比较和测试指令用于更新APSR中的标志,这些标志随后可能会用于条件跳转或条件执行。
指令 | 操作 |
---|---|
CMP R1, R2 | 比较R1-R2,APSR更新但结果不会保存 |
CMP R1,#immed | 比较 计算R1-立即数 |
CMN R1, R2 | 负比较 计算R1+R2,APSR更新但结果不会保存 |
CMN R1, #immed | 负比较 计算R1+immed,APSR更新但结果不会保存 |
TST R1, R2 | 测试(按位与):计算Rn和立即数相与后的结果,APSR中的N位和Z位更新,但与运算的结果不会保存,若使用了桶形移位则更新C位 |
TST R1, #immed | 测试(按位与):计算Rn和立即数相与后的结果,APSR中的N位和Z位更新,但与运算的结果不会保存 |
TEQ R1, R2 | 测试(按位异或):计算Rn和立即数异或后的结果,APSR中的N位和Z位更新,但与运算的结果不会保存,若使用了桶形移位则更新C位 |
TEQ R1, #immed | 测试(按位异或):计算Rn和立即数相与后的结果,APSR中的N位和Z位更新,但与运算的结果不会保存 |
注意:由于APSR总是会更新,因此这些指令中不存在S后缀。
程序流控制的指令有多种:
跳转、函数调用、条件跳转、比较和条件跳转的组合、条件执行(IF-THEN指令)、表格跳转。
多个指令可以引发跳转操作:
①跳转指令(如B、BX)
②更新R15的数据处理指令(如MOV 、ADD)
③写入PC的读存储器指令(如LDR、LDM、POP)
一般来说,尽管可以使用任意一种操作来实现跳转,比较常用的还是B、BX以及POP指令(通常用于函数返回)。早期的ARM处理器有时还会在表格跳转时使用其他的方法,不过M3/M4就不需要了,它们有用于表格跳转的特殊指令。
无条件跳转指令
指令 | 操作 |
---|---|
B | 跳转到label。若跳转范围超过了+/- 2KB,则可以使用B.W使用32位版本的跳转指令,这样可以得到较大范围 |
B.W | |
BX | 间接跳转。跳转到存放于Rm中的地址值,并且基于Rm第0位设置处理器的执行状态(T位)。由于CORTEX-M处理器只支持Thumb状态,Rm第0位必须为1. |
要调用函数,可以使用链接跳转(BL)或带链接的间接跳转(BLX)指令,如下表所示。它们执行跳转并同时将返回地址保存到链接寄存器(LR),这样在函数调用结束后处理器还可以跳回之前的程序。
指令 | 操作 |
---|---|
BL | 跳转到标号地址并将返回地址保存在LR中 |
BLX | 跳转到Rm指定的地址,并将返回地址保存在LR中,以及更新EPSR中的T位为Rm的最低位 |
当执行这些指令时:
①程序计数器被置为跳转目标地址
②链接寄存器(LR/R14)被更新为返回地址,这也是已执行的BL/BLX后指令的地址。
③若指令为BLX,则EPSR中的Thumb位也会被更新为存放跳转目标地址的寄存器的最低位(M3/M4必然为1)。
条件跳转基于APSR的当前值条件执行(N、Z、C 和 V 标志),APSR受到以下情况的影响:
多数16位数据处理指令
带有S后缀的32位(Thumb-2)数据处理指令,如ADDS.W
比较和测试指令
直接写APSR/XPSR
bit[27]为另外一个标志,也就是Q标志,用于饱和算术运算而非条件跳转。
条件跳转发生时所需的条件由后缀指定表示为,条件跳转指令具有16位和32位的形式,它们的跳转范围不同,如下表所示:
指令 | 操作 |
---|---|
B B.W | 若条件为true则跳转到label,例如:CMP R0, #1 BEQ loop;若R0等于1则跳转到了loop若所需的跳转范围超过了+/-254字节,则可能需要指定使用32位版本的跳转指令,以增加跳转范围 |
为14个可能的条件后缀之一。
后缀 | 条件跳转 | 标志APSR |
---|---|---|
EQ | 相等 | Z置位 |
NE | 不相等 | Z清零 |
CS/HS | 进位置位/无符号大于或等于 | C置位 |
CC/LO | 进位清零/无符号小于 | C清零 |
MI | 减/负数 | N置位(减) |
PL | 加/正数或零 | N清零 |
VS | 溢出 | V置位 |
VC | 无溢出 | V清零 |
HI | 无符号大于 | C置位Z清零 |
LS | 无符号小于或等于 | C清零或Z置位 |
GE | 有符号大于或相等 | N置位V 置位,或N清零V清零(N==V) |
LT | 有符号小于 | N清零V清零,或N清零V置位(N!=V) |
GT | 有符号大于 | Z清零,且或者N置位V置位,或者N清零V清零(Z0, NV) |
LE | 有符号小于或相等 | Z置位,或者N置位V清零,或者N清零V置位(Z==1 或N!=V) |
ARMv7-M架构提供了两个新的指令,它们合并了和零比较以及条件跳转操作。这两个指令为CBZ(比较为零则跳转)和CBNZ(比较非零则跳转),它们只支持前向跳转,不支持向后跳转。CBZ和CBNZ常用于while等循环结构,例如:
i = 5;
while( i!=0 ){
func1();
i--;
}
这段语句可能会被编译为:
MOV R0, #5
loop1 CBZ R0, loopexit
BL func1
SUBS R0,#1
B loop1
loop1exit
CBNZ的用法和CBZ类似,只有Z标志未置位时才会发生跳转(结果非零)。 APSR的值不受CBZ和CBNZ指令的影响。
除了条件跳转,M3和M4处理器还支持条件执行。在IT(IF-THEN)指令执行后,接下来,最多4个指令可以根据IT指令指定的条件以及APSR数值条件执行。IT指令块中包含1个指明条件执行细节的IT指令,后面为1~4个条件执行指令。条件执行指令可以为数据处理指令或存储器访问指令。IT块中的最后一个条件执行指令也可以为条件跳转指令。IT指令语句中包含IT指令操作码并附加最多3个可选后缀T(then)以及E(else),后面是要检查的条件,与条件跳转中的条件符号一样。T/E表明IT指令块接下来还有几个指令,以及在符号条件时它们是否应该执行。
注意:当使用E后缀时,IT指令块中指令对应的执行条件必须要同IT指令指定的条件相反。
T和E具有不同的组合序列:
只有一个条件执行指令:IT
两个条件执行指令:ITT、 ITE
三个条件执行指令:ITTT、ITTE、ITET、ITEE
四个条件执行指令:ITTTT、ITTTE、ITTET、ITTEE、ITETT、ITETE、ITEET、ITEEE.
下表将列出IT指令序列的多种形式和实例,其中指定第二个指令的执行条件 指定第三个指令的执行条件 指定第四个指令的执行条件。指定指令块的基本条件,若为true,则执行IT后的第一条指令。
IT块[每个、、可以为T或E] | 例子 | |
---|---|---|
只有一个条件指令 | IT instr1 | IT EQ ADDEQ R0, R0, R1 |
两条件指令 | IT instr1 instr2 |
ITEGE ADDGE R0,R0,R1 ADDLT R0,R0,R3 |
三个条件指令 | IT instr1 instr2 |
ITTET GT ADDGT R0,R0,R1 ADDLE R0,R0,R3 ADDGT R2,R4,#1 |
四个条件指令 | IT instr1 instr2 |
ITETT NE ADDNE R0,R0,R1 ADDEQ R0,R0,R3 ADDNE R2,R4,#1 MOVNE R5, R3 |
若为AL,则在条件控制中不能使用E,因为它表示指令永远不会执行。
对于一些汇编工具,代码中无须使用IT指令。通过简单地给普通指令添加条件后缀,汇编工具会在前面自动插入所需的IT指令。IT指令块中的数据处理指令不应该修改APSR的数值,当有些16位的数据处理指令在IT指令块中使用时,APSR不会更新,这一点和它们正常操作更新APSR的情况不同。
M3、M4支持两个表格跳转指令:TBB(表格跳转字节)和TBH(表格跳转半字),它们和跳转表一起使用,通常用于实现C代码中的switch语句。它们和跳转表一起使用,通常用于实现C代码中的switch语句。由于程序计数器数值的第0位总是为0,利用表格跳转指令的跳转表也就无须保存这一位,因此,在目标地址计算中跳转偏移被乘以2.
TBB用于跳转表的所有入口被组织成字节数组的情形(相对于基地址的偏移小于2x2^8 =512字节),而当所有入口为半字数组时则使用TBH(相对于基地址的偏移小于2x2^16 =128K字节)。基地址可以为当前PC或另外一个寄存器的数值,由于Cortex-m处理器的流水线特性,当前PC值为TBB/TBH指令的地址加4,这一点在生成跳转表时必须要考虑到,TBB/TBH都只支持前向跳转。
TBB指令的语法为:
TBB [Rn, Rm]
其中,Rn中存放跳转表的基地址,Rm则为跳转表偏移。TBB偏移计算用的立即数位于存储器地址[Rn+Rm]。若PC用作Rn,则会看到下图操作:
TBH指令的情况非常类似,只是跳转表中的每个入口都是双字节大小,因此数组的索引不同,且偏移范围较大。为了表示索引的差异,TBH的语法稍微不同:
TBH [Rn, Rm, LSL #1]
TBB和TBH指令被C编译器用于switch 语句,而在汇编编程时直接使用这两条指令就没有那么容易了,因为跳转表中的数值和当前PC相关。若跳转目标地址未在同一个汇编程序文件中,汇编阶段无法确定地址偏移数值。
SVC指令用于产生SVC异常(异常类型为11)。SVC一般用于嵌入式OS/RTOS,其中运行在非特权执行状态的应用可以请求运行在特权状态的OS的服务。SVC异常机制提供了从非特权到特权的转换。另外SVC机制可以作为应用任务访问各种服务(包括OS服务或其他API函数)的入口,这样应用任务就可以在无须了解服务的实际存储器地址的情况下请求所需服务。它只需知道SVC服务编号、输入参数和返回结果。
SVC指令要求SVC异常的优先级高于当前的优先级,而且异常没有被PRIMASK等寄存器屏蔽,不然会触发错误异常。因此,由于NMI和hardfault 异常的优先级总是比SVC异常大,也就无法再这两个处理中使用SVC.
SVC指令的语法如下:SVC #
其中,立即数为8位,数值自身不会影响SVC异常的动作,不过SVC处理程序可以将其作为输入参数来确定应用任务所请求的服务。
另外一个和异常相关的指令为改变处理器状态(CPS)指令。对于Cortex-M处理器可以使用这条指令来设置或清除PRIMASK和FAULTMASK等中断屏蔽寄存器。注意,这些寄存器也可以用MSR和MRS指令访问。具体指令如下表所示:
指令 | 操作 |
---|---|
CPSIE I | 使能中断(清除PRIMASK) 和_enable_irq()相同 |
CPSID I | 禁止中断(设置PRIMASK),NMI和Hardfault不受影响 和_disable_irq()相同 |
CPSIE F | 使能中断(清除FAULTMASK) 和_enable_fault_irq()相同 |
CPSID F | 禁止错误中断(设置FAULTMASK),NMI不受影响 和_disable_fault_irq()相同 |
切换PRIMASK和FAULTMASK可以禁止或使能中断,经常用于确保时序关键的任务在不被打断的情况下快速完成。
进入休眠模式主要使用两条指令(注意:还有另外一种进入休眠模式的方法,也就是退出时休眠,处理器可以在异常退出时进入休眠)。
WFI ;等待中断(进入休眠)
或者在使用符合CMSIS 的设备驱动库进行C编程时:
_WFI(); //等待中断(进入休眠)
WFI(等待中断)指令会使处理器立即进入休眠模式,中断、复位或调试操作可以将处理器从休眠中唤醒。
另外一个指令为WFE(等待事件),会使处理器有条件地进入休眠:
WFE ;等待事件(条件进入休眠)
或者在使用符合CMSIS的设备驱动库进行C编程时:
_WFE(); //等待事件(条件进入休眠)
在Cortex-M3/M4处理器内部,一个只有一位的寄存器会记录事件。若该寄存器置位,WFE指令不会进入休眠而只是清除事件寄存器并继续执行下一条指令;若该寄存器清零,则处理器会进入休眠而且会被事件唤醒,事件可以是中断、调试操作、复位或外部事件输入的脉冲信号(例如,事件脉冲可由另一个处理器或外设产生)。
CORTEX-M处理器的接口信号包括一个事件输入和一个事件输出。处理器的事件输入可由多处理系统中其他处理器的事件输出产生,因此,处于WFE休眠(如等待自旋锁)的处理器可由其他的处理器唤醒。有些情况下,这些信号会被连接到CORTEX-M微控制器的I/O端口,而其他一些CORTEX-M微控制器的事件输入可能会被连接到低电平,而事件输出则可能会用不上。
事件输出可由SEV(发送事件)指令触发:
SEV ;发送事件
若利用符合CMSIS的设备驱动库进行C编程:
_SEV(); //发送事件
当执行SEV时,事件输出接口就会出现一个单周期的脉冲,SEV指令还会设置同一处理器的事件寄存器。
对于ARM架构,在不影响数据处理结果的情况下,存储器传输的顺序可以和程序代码不同。这种情况对于具有超矢量或乱序执行能力的高端处理器是很常见的。不过,在对存储器访问重新排序之后,若数据在多个处理器间共用,则另一个处理器看到的数据顺序可能和设定的不同,这样可能会引起错误。存储器屏障指令可用于:
①确保存储器访问的顺序。
②确保存储器访问和另一个处理器操作间的顺序。
③确保系统配置发生在后序操作之前。
CORTEX-M处理器支持三种存储器屏障指令
指令 | 描述 | CMSIS的设备驱动库函数 |
---|---|---|
DMB | 数据存储器屏障。确保在执行新的存储器访问前所有的存储器访问都已经完成 | void _DMB(void); |
DSB | 数据同步屏障。确保在下一条指令执行前所有的存储器访问都已经完成 | void _DSB(void); |
ISB | 指令同步屏障。清空流水线,确保在执行新的指令前,之前所有的指令都已完成 | void _ISB(void); |
由于cortex-m处理器具有相对简单的流水线,而且AHB Lite总线协议不允许对存储器系统中的传输重新排序,因此,即便没有存储器屏障指令,多数应用还是可以正常工作的。不过有些情况应该使用这些屏障指令,如下表所示:
应用场景(用于当前的Cortex-M3和Cortex-M4设计) | 所需的屏障指令 |
---|---|
利用MSR指令更新CONTROL寄存器后,应该使用ISB指令,确保接下来的操作使用更新后的配置 | ISB |
若系统控制寄存器中的SLEEPONEXIT位在异常处理内变化了,则应该在异常退出前使用DSB | DSB |
若挂起的异常是使能的且要确保挂起的异常在下面的操作抢执行 | DSB,后面跟着ISB |
在利用NVIC清除中断寄存器禁止中断时,若要确保在执行下一条指令前中断禁止立即产生效果 | DSB,后面跟着ISB |
自修复代码(下面的指令已经被取出且需要清空) | DSB,后面跟着ISB |
程序存储器被外设中的控制寄存器修改,且需要立即启用新的程序存储器 映射(假定存储器映射在写完成前立即更新) | DSB,后面跟着ISB |
数据存储器被外设中的控制寄存器修改,且需要立即启用新的数据存储器 映射(假定存储器映射在写完成前立即更新) | DSB |
存储器保护单元(MPU)配置更新,然后在MPU配置变化影响的存储器区域中取出并执行一条指令 | DSB,后面跟着ISB |
从架构角度来看,在有些情况下,两次操作间应该使用存储器屏障,不过即便不使用存储器屏障,当前的cortex-m处理器也不会有什么问题,如下表所示:
应用场景(基于架构推荐) | 所需的屏障指令 |
---|---|
存储器保护单元(MPU)配置更新,然后在MPU配置变化影响的存储器区域中取出并执行一条指令(只允许数据访问的区域,无取指) | DSB |
进入休眠前(WFI 或 WFE) | DSB |
信号量操作 | DMB或DSB |
修改异常(如SVC)的优先级后触发 | DSB |
利用向量表偏移寄存器(VTOR)将向量表重置于新的位置,然后触发新向量的异常 | DSB |
修改向量表中的向量入口(若在SRAM中)后立即触发同一个异常 | DSB |
恰好处于自复位前(可能会有正在进行的数据传输) | DSB |
备注:参考ARM Cortex-M3与Cortex-M4权威指南