目录
- Cortex-M 处理器的指令集
- 寄存器组详解
- 指令集详解(Cortex-M3 和 Cortex-M4 都支持的)
- 1. 处理器内传送数据
- 2. 存储器访问指令
- 2.1 各种数据大小的存储器访问指令
- 2.2 浮点单元的存储器访问指令
- 2.3 具有立即数偏移的存储器访问指令
LDR Rd,[Rn,#offset] !
- 2.4 浮点单元的存储器访问指令
VLDR.32 Sd,[Rn,#offset]
- 2.5 PC 相关寻址的存储器访问指令
LDR Rt,[PC,#offset]
- 2.6 PC 相关寻址到浮点单元存储器访问指令
VLDR.32 Sd,[PC,#offset]
- 2.7 寄存器偏移的存储器访问指令
LDR Rd,[Rn,Rm{,LSL #n}]
- 2.8 后序存储器访问指令
LDR Rd,[Rn],#offset
- 2.9 多加载/存储存储器访问指令
LDMIA Rn!,<reg list>
- 2.10 浮点单元多加载/存储存储器访问指令
VLDMIA.32 Rn!,<s_reg list>
- 2.11 内核寄存器的压栈和出栈指令
PUSH <reg_list>
- 2.12 浮点单元寄存器的压栈和出栈指令
VPUSH.32 <s_reg lit>
- 2.13 非特权访问等级的存储器访问指令
- 2.14 排他访问指令
- 3. 算术运算
- 4. 逻辑运算指令
- 5. 移位和循环移位指令
- 6. 数据转换运算(展开和返序)
- 7. 位域处理指令
- <lsb> 是位段的最低有效位(Least Significant Bit)的位置。
- <width> 是要清除的位段的宽度,即要清除的位的数量。
- <lsb> 是目标寄存器中要插入位字段的最低有效位(Least Significant Bit)的位置。
- <width> 是要插入的位字段的宽度,即要从源寄存器中插入的位数。
- immed 是一个 16 位的立即数,表示断点的标识符。通常情况下,这个立即数是用来标识不同的断点,但在实际的使用中,可能会忽略这个值,或者使用特定的约定来进行设置。
Cortex-M 处理器的指令集
Cortex-M 处理器支持的指令集
正如上图中所见,从 Cortex-M0 到 Cortex-M3 以及后面的 Cortex-M4,Cortex-M 处理器的指令集设计是向上兼容的,因此,为Cortex-MO/M0+/M1 处理器编译的代码在 Cortex-M3 或 Cortex-M4 处理器上也能运行,而为 Cortex-M3 编译的代码也可以在 CortexM4 上运行。
ARMv6-M的多数指令都是16位的,有些则具有16位和32位两种格式。当操作能以16位执行时,编译器通常会选择使用16位版本的指令这样会降低代码大小。32位的指令使用更大寄存器范围(如高寄存器)、更大的立即数、更宽的地址区域以及更多的寻址模式。不过,对应同一个操作,16 位版本和 32 位版本的指令所需的执行时间是相同的。
Cortex-M 处理器指令集的选择
对于一般的数据处理和 I/O 控制任务,Cortex-M0 和 Cortex-M0+ 处理器完全能够应付。例如,Cortex-M0 和 Cortex-M0+ 性能可以达到2.15CoreMark/MHz,这已经是其他 16 位微控制器在相同运行频率下性能的两倍。若应用需要处理更多的复杂数据、执行快速乘法运算或需要较快完成数据处理,则可能需要升级到 Cortex-M3 或 Cortex-M4 处理器。若需要在DSP应用或浮点运算中达到最优的效果,Cortex-M4 将是一个更好的选。
尽管 Cortex-M 处理器支持的指令不少,不过由于 C 编译器可以生成相当高效的代码,也不用了解全部指令的细节。另外,免费的CMSIS-DSP 库和各种中间件(如软件库)有助于软件开发人员实现高性能的DSP应用,而无须深人了解每条指令的细节。
寄存器组详解
1. 通用寄存器 R0~R12
R0~ R12 是最具“通用目的”的 32 位通用寄存器,用于数据缓冲操作。该类寄存器分为两组:一组被称为低位寄存器, R0~R7,它们能够被所有通用寄存器指令访问;另一组被称为高位寄存器, R8~R12,它们能够被所有 32 位通用寄存器指令访问,而不能被所有 16 位通用寄存器指令访问。
2. 栈指针
寄存器 R13 被用作栈指针,用于访问 RAM 中的栈区。在 Arm 架构中, SP 的最低两位被忽略,相当于 SP 的最低两位永远是 0,所以 SP 的值是 4 的整数倍,那么 SP 指向的 RAM 地址也是 4 的整数倍,即是按照 4 字节对齐的。 Arm 是 32 位机,机器字长为 32 位, 4 字节对齐表示栈中的数据存储是按照字对齐的。在图中, SP( R13)右侧的箭头 “→” 指向了SP的两个名字: PSP、 MSP。主栈指针MSP是复位后缺省使用的栈指针,用于操作系统内核以及异常处理例程(包括中断处理程序)。“Handler”模式通常使用主栈指针( Main Stack Pointer, MSP),但是也可以使用进程栈指针( Processor Stack Pointer, PSP)。
3. 连接寄存器
寄存器 R14 也称作连接寄存器( Link Rigister, LR),用 于保存函数或子程序调用时的返回地址。 LR 也被用于异常返回。在其他情况下,可以将 R14 作为通用寄存器来使用。
4. 程序计数寄存器
寄存器R15 是程序计数寄存器,指示将要执行的指令在存储器中的位置。复位的时候,处理器的硬件机制自动将复位向量值放入PC。 如果修改它的值,就能改变程序的执行流。该寄存器的第 0 位若为 0,则指令总是按照字对齐或者半字对齐。 PC能以特权或者非特权模式进行访问。
5. 程序状态字寄存器( xPSR)
程序状态字寄存器在内部分为 3 个子寄存器: APSR、 IPSR、 EPSR。 3 个子寄存器既可以被单独访问,也可以将两个或三个组合到一起访问。使用三合一方式访问时,把该寄存器称为xPSR,各个寄存器组合名称与读写属性如下表所示。其中 xPSR、 IPSR 和 EPSR 寄存器只能够在特权模式下被访问,而 APSR 寄存器能够在特权或者非特权模式下被访问,具体描述详见《CM4 用户指南》。
**Arm Cortex-M4 程序状态寄存器( xPSR) **
寄存器 | 类型 | 结合 |
---|---|---|
xPSR | RW | APSR、 IPSR、和 EPSR |
IEPSR | RO | IPSR 和 EPSR |
IAPSR | RW | APSR 和 IPSR |
EAPSR | RW | APSR 和 EPSR |
-
应用程序状态寄存器(APSR):显示算术运算单元 ALU 状态位的一些信息。
负标志 N:若结果最高位为 1,相当于有符号运算中结果为负,则置 1,否则清 0。零标志 Z:若结果为 0,则置 1,否则清 0。进位标志 C:若有最高位的进位(减法为借位),则置 1,否则清 0。溢出标志 V:若溢出,则置 1,否则清 0。以上各位在 Cortex-M 系列处理器中同 M0、 M0+、 M3、 M4 的定义是一样的。这些位会在条件转移指令中被用到,复位之后是随机的。
饱和标志位Q:在实现DSP扩展的处理器中,如果在运算中出现饱和,处理器就会将该位置 1,此即为饱和。该位只在Cortex-M3、 Cortex-M4 中存在。
大于或等于标志位GE:仅用于DSP扩展, SIMD指令更新这些标志用以指明结果来自操作的单个字节或半字。该位只在Cortex-M4 中存在。更多信息请参考《Armv7-M参考手册》。
-
中断程序状态寄存器(IPSR):每次异常处理完成之后,处理器会实时更新 IPSR 内的异常号, 相关值只能被 MRS 指令读写。进程模式下(可以理解为处于无操作系统的主循环中,或者有操作系统情况下的某一任务程序中),值为 0。 Handler 模式(处理异常的模式, 可简单地理解为中断状态)下,存放当前异常的异常号。复位之后,寄存器被自动清零。复位异常号是一个暂时值,复位时, 其是不可见的。 在 Cortex-M 系列处理器中 M0 和 M0+ 的异常号占用 0~5 位, M3、 M4 使用 0~8 位,这与处理器所能支持的异常或中断数量有关。
-
执行程序状态寄存器(EPSR): T 标志位指示当前运行的是否为 Thumb 指令,该位是不能被软件读取的, 运行复位向量对应的代码时置 1。如果该位为 0,会发生硬件异常,并进入硬件中断处理程序。在 Cortex-M 系列处理器中这一位的定义相同。
ICI/IT 标志位:指示异常可继续指令状态或保存的 IT 状态,该位存在于 Cortex-M3 与 Cortex-M4 中。该位的更多信息请参考《Armv7-M 参考手册》。
6. 特殊功能寄存器
- 中断屏蔽寄存器(PRIMASK)
使用特殊指令(MSR, MRS)可以访问中断屏蔽寄存器,当其某些位被置位时,除不可屏蔽中断和硬件错误外,其余所有中断都会被屏蔽。 - 错误屏蔽寄存器(FAULTMASK)
FAULTMASK 与 PRIMASK 的区别在于 FAULTMASK 能够屏蔽掉优先级更高的硬件错误(HardFault)异常。 - 基本优先级屏蔽寄存器(BASEPRI)
BASEPRI 提供了一种更加灵活的中断屏蔽机制,通过设置该寄存器可以屏蔽特定优先级的中断,当该寄存器设置为一个非零值时,所有优先级值(中断的优先级数值越大优先级越低)大于或等于该值的中断都会被屏蔽,当该寄存器为零时不起作用。寄存器只能在特权模式下访问。复位时,基本优先级屏蔽寄存器被清除。 - 控制寄存器(CONTROL)
CONTROL 用于控制和确定处理器的操作模式以及当前执行任务的特性。
7. 浮点控制寄存器
浮点控制寄存器只在 Cortex-M4F 处理器中存在,其中包含了用于浮点数据处理与控制的寄存器,在这里只进行简单的介绍,详细内容请参考《CM3/4 权威指南》《 Armv7-M 参考手册》。此外,浮点单元中还引入一些经过存储器映射的寄存器, 如协处理器访问控制寄存器(CPACR)等。需要注意的是,为降低功耗,浮点单元默认是被禁用的,如须使用浮点运算就要通过设置 CPACR 来启用浮点单元。
指令集详解(Cortex-M3 和 Cortex-M4 都支持的)
1. 处理器内传送数据
- 将数据从一个寄存器送到另外一个寄存器。
- 在寄存器和特殊寄存器(如 CONTROL、PRIMASK、FAULTMASK 和 BASEPRI)间传送数据。
- 将立即数送到寄存器。
对于具有浮点单元的 Cortex-M4 处理器,还可以:
- 在内核寄存器组中的寄存器和浮点单元寄存器组中的寄存器间传送数据,在浮点寄存器组中的寄存器间传送数据。
- 在浮点系统寄存器(如FPSCR——浮点状态和控制寄存器)和内核寄存器间传送数据。
- 将立即数送入浮点寄存器
1.1 处理器内传送数据的指令
指令 | 目的 | 源 | 操作 |
---|---|---|---|
MOV | R4, | R0 | ;从 R0 复制数据到 R4 |
MOVS | R4, | R0 | ;从 R0 复制数据到 R4,且更新 APSR(标志) |
MRS | R7, | PRIMASK | ;将数据中 RPIMASK(特殊寄存器)复制到 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 |
1.2 浮点单位和内核寄存器间传送数据的指令
指令 | 目的 | 源 | 操作 |
---|---|---|---|
VMOV | R0, | S0 | ;将数据从浮点寄存器 S0 复制到通用目的寄存器 R0 |
VMOV | S0, | R0 | ;将数据从通用目的寄存器 R0 复制到浮点寄存器 S0 |
VMOV | S0, | S1 | ;将数据从浮点寄存器 S1 复制到 S0 (单精度) |
VMRS.F32 | R0, | FPSCR | ;将数据从浮点单元系统寄存器 FPSCR 复制到 R0 |
VMRS | APSR_nzcv | FPSCR | ;复制 FPSCR 中的标志到 APSR 中标志 |
VMRS | 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 + 偏移] 位置的数据,并将其存人 R0。注意,由于处理器的流水线结构,PC 的值并非 LDR 指令的地址。不过,汇编器会计算偏移,因此也不必担心。
文字池
汇编器通常会将字符数据(如上面例子中的 0x12345678)组成名为文字池的数据块。由于 LDR 指令中的偏移值有限,程序通常需要多个文字池,才能使 LDR 访问到文字数据。因此,需要插入 LTORG(或.poo0)之类的汇编伪指令,以告知汇编器何处可以插人文字池。否则,汇编器会尝试将所有的字符数据放到程序代码的末尾,这样可能会超出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 = 0x3456789
与使用 LDR 伪指令的方法相比,LDR 方法的可读性更好,而且若同一个常量在程序代码中使用了多次,汇编器可能会重用相同的常量数据以降低代码大小。不过,取决于存储器系统的设计,若使用了系统级的缓存目 LDR 会带来数据缓存丢失,有些情况下 MOVW + MOVI 方法生成的代码可能会执行更快。
2. 存储器访问指令
2.1 各种数据大小的存储器访问指令
数据类型 | 加载(读存储器) | 存储(写存储器) |
---|---|---|
8位无符号 | LDRB | STRB |
8为有符号 | LDRSB | STRB |
16位无符号 | LDRH | STRH |
16为有符号 | LDRSH | STRH |
32位 | LDR | STR |
多个32位 | LDR | STR |
双字(64位) | LDRD | STRD |
栈操作(32位) | POP | PUSH |
注意:LDRSB 和 LDRSH 会对被加载数据自动执行有符号展开运算(符号扩展),将其转换为有符号的32位数据。例如,若 LDRB 指令读取的是 0x83,则数据在被放到目的寄存器前会被转换为 0xFFFFFF83。
2.2 浮点单元的存储器访问指令
数据类型 | 读存储器(加载) | 写存储器(存储) |
---|---|---|
单精度数据(32位) | VLDR.32 | VSTR.32 |
双精度数据(64位) | VLDR.64 | VSTR.64 |
多数据 | VLDM | VSTM |
栈操作 | VPOP | VPUSH |
2.3 具有立即数偏移的存储器访问指令LDR Rd,[Rn,#offset] !
前序访问实例(#offset 域为可选的) | 描述 |
---|---|
LDRB Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 读取字节(并写回【带"!"】) |
LDTSB Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 读取有符号展开的字节(并写回) |
LDRH Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 读取半字(并写回) |
LDRSH Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 读取有符号展开的半字(并写回) |
LDR Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 读取字(并写回) |
LDRD Rd1,Rd2,[Rn,#offset] ! | 从存储器位置 Rn+offset 读取双字(并写回) |
STRB Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 存储字节(并写回) |
STRH Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 存储半字(并写回) |
STR Rd,[Rn,#offset] ! | 从存储器位置 Rn+offset 存储字(并写回) |
STRD Rd1,Rd2,[Rn,#offset] ! | 从存储器位置 Rn+offset 存储双字(并写回) |
指令中的感叹号( ! )表示指令完成时是否更新存放地址的寄存器(写回:更新地址)。不管是否使用了感叹号( ! ),数据传输的地址都使用 R1+R8 的和。
2.4 浮点单元的存储器访问指令VLDR.32 Sd,[Rn,#offset]
例子(#offset 域为可选的) | 描述 |
---|---|
VLDR.32 Sd,[Rn,#offset] | 读取存储器中的单精度数据到单精度寄存器 Sd |
VLDR.64 Dc,[Rn,#offset] | 读取存储器中的双精度数据到双精度寄存器 Dd |
VSTR.32 Sd,[Rn,#offset] | 写单精度寄存器 Sd 中的单精度数据到存储器 |
VSTR.64 Dd,[Rn,#offset] | 写双精度寄存器 Dd 中的双精度数据到存储器 |
2.5 PC 相关寻址的存储器访问指令LDR Rt,[PC,#offset]
存储器访问可以产生相对于当前PC的地址值和偏移值。它常用于将立即数加载到寄存器中,也可被称作文本池访问。
例子(#offset 域为可选的) | 描述 |
---|---|
LDRB Rt,[PC,#offset] | 利用 PC 偏移加载无符号字节到 Rt |
LDRSB Rt,[PC,#offset] | 对字节数据进行有符号展开并利用 PC 偏移加载到 Rt |
LDRH Rt,[PC,#offset] | 利用 PC 偏移加载无符号半字到 Rt |
LDRSH Rt,[PC,#offset] | 对半字数据进行有符号展开并利用 PC 偏移加载到 Rt |
LDR Rt,[PC,#offset] | 利用 PC 偏移加载字数据到 Rt |
LDRD Rt,Rt2,[PC,#offset] | 利用 PC 偏移加载双字数据到 Rt 和 Rt2 |
2.6 PC 相关寻址到浮点单元存储器访问指令VLDR.32 Sd,[PC,#offset]
例子(#offset 域为可选的) | 描述 |
---|---|
VLDR.32 Sd,[PC,#offset] | 利用 PC 偏移加载单精度数据到单精度寄存器 Sd |
VLDR.64 Dd,[PC,#offset] | 利用 PC 偏移加载双精度数据到双精度寄存器 Dd |
2.7 寄存器偏移的存储器访问指令LDR Rd,[Rn,Rm{,LSL #n}]
处理的数据数组的地址为基地址和从索值计算出的偏移得到的情况。为了进一步提高地址计算的效率,在加到基地址寄存器前,索值可以进行 0~3 位的移位。
寄存器偏移访问实例 | 描述 |
---|---|
LDRB Rd,[Rn,Rm{,LSL #n}] | 从存储器位置 Rn+(Rm<<n) 处读取字节 |
LDRSB Rd,[Rn,Rm{,LSL #n}] | 从存储器位置 Rn+(Rm<<n) 处读取字节并进行有符号展开 |
LDRH Rd,[Rn,Rm{,LSL #n}] | 从存储器位置 Rn+(Rm<<n) 处读取半字 |
LDRSH Rd,[Rn,Rm{,LSL #n}] | 从存储器位置 Rn+(Rm<<n) 处读取半字并进行有符号展开 |
LDR Rd,[Rn,Rm{,LSL #n}] | 从存储器位置 Rn+(Rm<<n) 处读取字 |
STRB Rd,[Rn,Rm{,LSL #n}] | 往存储器位置 Rn+(Rm<<n) 存储字节 |
STRH Rd,[Rn,Rm{,LSL #n}] | 往存储器位置 Rn+(Rm<<n) 存储半字 |
STR Rd,[Rn,Rm{,LSL #n}] | 往存储器位置 Rn+(Rm<<n) 存储字 |
2.8 后序存储器访问指令LDR Rd,[Rn],#offset
具有立即数寻址模式的存储器访问指令也有一个立即数偏移数值。不过,在存储器访问期间是不会用到偏移的,它会在数据传输结束后更新地址寄存器。
若使用后序存储器寻址模式,由于在数据传输成功完成时,基地址寄存器总会得到更新因此无须使用感叹号( ! )。
后序寻址模式在处理数组中的数据时非常有用,在访问数组中的元素时,地址寄存器可以自动调整,节省了代码大小和执行时间。
注意:后序指令中不能使用 R15(PC) 或 R14(SP),后序存储器访问指令都是 32 位的,偏移数值可以为整数或负数。
后序访问实例 | 描述 |
---|---|
LDRB Rd,[Rn],#offset | 读取存储器 [Rn] 处的字节到Rd,然后更新 Rn 到 Rn+offset |
LDRSB Rd,[Rn],#offset | 读取存储器 [Rn] 处的字节到 Rd 并进行有符号展开,然后更新 Rn 到 Rn+offset |
LDRH Rd,[Rn],#offset | 读取存储器 [Rn] 处的半字到Rd,然后更新 Rn 到 Rn+offset |
LDRSH Rd,[Rn],#offset | 读取存储器 [Rn] 处的半字到 Rd 并进行有符号展开,然后更新 Rn 到 Rn+offset |
LDR Rd,[Rn],#offset | 读取存储器 [Rn] 处的字到Rd,然后更新 Rn 到 Rn+offset |
LDRD Rd1,Rd2,[Rn],#offset | 读取存储器 [Rn] 处的双字到 Rd1、Rd2,然后更新 Rn 到 Rn+offset |
STRB Rd,[Rn],#offset | 存储字节到存储器 [Rn],然后更新 Rn 到 Rn+offset |
STRH Rd,[Rn],#offset | 存储半字到存储器 [Rn],然后更新 Rn 到 Rn+offset |
STR Rd,[Rn],#offset | 存储字到存储器 [Rn],然后更新 Rn 到 Rn+offset |
STRD Rd1,Rd2,[Rn],#offset | 存储双字到存储器 [Rn],然后更新 Rn 到 Rn+offset |
2.9 多加载/存储存储器访问指令LDMIA Rn!,<reg list>
ARM架构的一个重要优势在于,可以读或写存储器中多个连续数据,LDM(加载多个寄存器)和 STM(存储多个寄存器)指令只支持32位数据,它们支持两种前序:
- IA:在每次读/写后增加地址。
- DB:在每次读/写前减小地址。
LDM 和 STM 指令在使用时可以不进行基地址写回。
多加载/存储实例 | 描述 |
---|---|
LDMIA Rn!,<reg list> | 从 Rn 指定的存储器位置读取多个字,地址在每次读取后增加(IA)(Rn 在传输完成后写回【带"!"】) |
LDMDB Rn!,<reg list> | 从 Rn 指定的存储器位置读取多个字,地址在每次读取前减小(DB)(Rn 在传输完成后写回) |
STMIA Rn!,<reg list> | 往 Rn 指定的存储器位置写人多个字,地址在每次写入后增加(Rn 在传输完成后写回) |
STMDB Rn!,<reg list> | 往 Rn 指定的存储器位置写人多个字,地址在每次写人前减小(Rn 在传输完成后写回) |
<reg list>为寄存器列表,其中至少包括一个寄存器,以及:
- 开始为 “{”,结束为 “}”。
- 使用 “-”(连字符)表示范围。例如,R0-R4 表示 R0、R1、R2、R3 以及 R4。
- 使用 “,”(逗号)隔开每个寄存器。
与其他的加载/存储指令类似,可以在 STM 和 LDM 中使用写回 ( ! ) 。
2.10 浮点单元多加载/存储存储器访问指令VLDMIA.32 Rn!,<s_reg list>
多加载/存储实例 | 描述 |
---|---|
VLDMIA.32 Rn!,<s_reg list> | 读取多个单精度数据,地址在每次读取后增加(Rn在传输完成后写回【带"!"】) |
VLDMDB.32 Rn!,<s_reg list> | 读取多个单精度数据,地址在每次读取前减小(Rn在传输完成后写回【带"!"】) |
VLDMIA.64 Rn!,<d_reg list> | 读取多个双精度数据,地址在每次读取后增加(Rn在传输完成后写回【带"!"】) |
VLDMDB.64 Rn!,<d_reg list> | 读取多个双精度数据,地址在每次读取前减小(Rn在传输完成后写回【带"!"】) |
VSTMIA.32 Rn!,<s_reg list> | 写人多个单精度数据,地址在每次写入后增加(Rn在传输完成后写回【带"!"】) |
VSTMDB.32 Rn!,<s_reg list> | 写人多个单精度数据,地址在每次写人前减小(Rn在传输完成后写回【带"!"】) |
VSTMIA.64 Rn!,<d_reg list> | 写人多个双精度数据,地址在每次写人后增加(Rn在传输完成后写回【带"!"】) |
VSTMDB.64 Rn!,<d_reg list> | 写人多个双精度数据,地址在每次写人前减小(Rn在传输完成后写回【带"!"】) |
2.11 内核寄存器的压栈和出栈指令PUSH <reg_list>
栈的 push 和 pop 为另外一种形式的多存储和多加载,它们利用当前选定的栈指针来生成地址。当前栈指针可以是主栈指针(MSP),也可以是进程栈指针(PSP)实际选择是由处理器的当前模式和 CONTROL 特殊寄存器的数值决定。
栈操作 | 描述 |
---|---|
PUSH <reg_list> | 将寄存器存入栈中 |
POP <reg_list> | 从栈中恢复寄存器 |
寄存器列表的语法和 LDM 与 STM 相同。
对于PUSH指令,通常会对应一个具有相同寄存器列表的 POP,不过这也并不是必然的。例如,异常中就有使用 POP 作为函数返回的情形:
PUSH {R4-R6,LR} ;在子程序开始处保存 R4-R6 和 LR(链接寄存器)
;LR 中包含返回地址
... ;子程序中的处理
POP {R4-R6,PC} ;从栈中恢复 R4-R6 和返回地址,返回地址直接存入 PC
;这样会触发跳转(子程序返回)
除了将返回地址恢复到 IR 然后写入程序计数器(PC)外,可以将返回地址直接写人 PC 以减少指令数和周期数。
16 位的 PUSH 和 POP 只能使用低寄存器(RO~R7)、LR(用于PUSH)和 PC(用于POP)。因此,若在函数中某个高寄存器被修改时要保存寄存器的内容,则需要使用 32 位的 PUSH 和 POP 指令对。
2.12 浮点单元寄存器的压栈和出栈指令VPUSH.32 <s_reg lit>
若浮点单元存在,VPUSH 和 VPOP 指令也可用于执行针对浮点单元中寄存器的栈操作与 PUSH 和 POP 不同,VPUSH 和 VPOP 指令需要:
- 寄存器列表中寄存器是连续的。
- 每次 VPUSH 或 VPOP 压栈/出栈的寄存器的最大数量为16。
若需要保存超过 16 个的单精度浮点寄存器,可以使用双精度指令或者使用两组 VPUSH 和 VPOP。
栈操作示例 | 描述 |
---|---|
VPUSH.32 <s_reg lit> | 将单精度寄存器存入栈中(如 S0~S31) |
VPUSH.64 <d_reg lit> | 将双精度寄存器存入栈中(如 d0~d15) |
VPOP.32 <s_reg lit> | 从栈中恢复单精度寄存器 |
VPOP.64 <d_reg lit> | 从栈中恢复双精度寄存器 |
2.13 非特权访问等级的存储器访问指令
利用一组加载和存储指令,外于特权访问等级的程序代码可以访问非特权访问权限的有存储器。
有些 OS 环境可能会需要这些指令,因为非特权应用程序可以访问以数据指针作为输入参数的 API 函数(运行在特权访问等级),而 API 操作的存储器数据由指针决定。若数据访问由普通的加载和存储指令完成,非特权应用任务则可以利用这些 API 修改被其他任务或 OS 内核使用的数据。而将这些 API 编码为非特权访问等级的特殊的加载和存储指令后,它们只能访问应用任务可以访问的数据。
非特权访问等级 LDR/STR 示例(#offset城可选) | 描述 |
---|---|
LDRBT Rd,[Rn,#offset] | 从存储器位置 Rn+offset 读取字节 |
LDRSBT Rd,[Rn,#offset] | 从存储器位置 Rn+offset 读取有符号展开的字节 |
LDRHT Rd,[Rn,#offset] | 从存储器位置 Rn+offset 读取半字 |
LDRSHT Rd,[Rn,#offset] | 从存储器位置 Rn+offset 读取有符号展开的半字 |
LDRT Rd,[Rn,#offset] | 从存储器位置 Rn+offset 读取字 |
STRBT Rd,[Rn,#offset] | 往存储器位置 Rn+offset 存储字节 |
STRHT Rd,[Rn,#offset] | 往存储器位置 Rn+offset 存储半字 |
STRT Rd,[Rn,#offset] | 往存储器位置 Rn+offset 存储字 |
2.14 排他访问指令
排他访问示例 | 描述 |
---|---|
LDREXB RT,[Rn] | 从存储器位置 Rn 排他读取字节 |
LDREXH RT,[Rn] | 从存储器位置 Rn 排他读取半字 |
LDREX Rt,[Rn,#offset] | 从存储器位置 Rn 排他读取字 |
STREXB Rd,Rt,[Rn] | 往存储器位置 Rt 排他存储字节,返回状态位于 Rd 中 |
STREXH Rd,Rt,[Rn] | 往存储器位置 Rt 排他存储半字,返回状态位于 Rd 中 |
STREX Rd,Rt,[Rn,#offset] | 往存储器位置 Rt 排他存储字,返回状态位于 Rd 中 |
CLREX | 强制本地排他访问监控清零,使得下一次排他存储失败。它并不是排他存储器访问指令,不过由于它的用法,在这里列了出来 |
3. 算术运算
按照传统的 Thumb 语法(UAL之前),在使用 16 位的 Thumb 代码时,ADD 指令会修改 PSR 中的标志。不过,32 位 Thumb-2 指令可以修改这些标志,也可以不修改。为了区分这两种操作,根据统一汇编语言(UAL)语法,若后面的操作需要标志,则应该使用 S 后缀:
ADD R0,R1,R2 ;标志未变
ADDS R0,R1,R2 ;标志改变
算术数据运算指令
常用算术指令(可选后缀未列出来) | 操作 |
---|---|
ADD Rd,Rn,Rm ; Rd = Rn + Rm | ADD 运算 |
ADD Rd,Rn,#immed ; Rd = Rn + #immde | ADD 运算 |
ADC Rd,Rn,Rm ; Rd = Rn + Rm + 进位 | 带进位的 ADD |
ADC Rd,#immed ; Rd = Rd + #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 | 减法 |
SBC Rd,Rn,#immed ; Rd = Rn - #immed - 借位 | 带借位的减法 |
SBC 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 位,这样,在出现被零除时就可以产生异常(使用错误)。
乘法和 MAC(乘累加)指令
指令(由于 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 位乘指令 |
SMLAL RdLo,RdHi,Rn,Rm ; {RdHi,RdLo} += Rn * Rm | 有符号数据 32 位 MAC 指令 |
UMULL RdLo,RdHi,Rn,Rm ; {RdHi,RdLo} = Rn * Rm | 无符号数据 32 位乘指令 |
UMLAL RdLo,RdHi,Rn,Rm ; {RdHi,RdLo} += Rn * Rm | 无符号数据 32 位 MAC 指令 |
4. 逻辑运算指令
与算术指令类似,这些指令的 16 位版本会更新 APSR 中的标志。若未指定 S 后缀,汇编器会将它们转换为 32 位指令。
若使用这些指令的 16 位版本,则只能操作两个寄存器,且目的寄存器需要为源寄存器之另外,还必须是低寄存器(RO~R7),而且要使用 S 后缀(APSR更新)。ORN 指令没有 16 位的形式。
指令(可选 S 后缀未列出来) | 操作 |
---|---|
ADD 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 = Rd | #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 | (w#immed) | 按位或非 |
ORN Rd,Rn,Rm ; Rd = Rn | (wRm) | 按位或非 |
EOR Rd,Rn ; Rd = Rd ^ Rn | 按位异或 |
EOR Rd,Rn,#immed ; Rd = Rn |#immed | 按位异或 |
EOR Rd,Rn,Rm ; Rd = Rn | Rm | 按位异或 |
5. 移位和循环移位指令
若使用了 S 后缀,这些循环和移位指令也会更新 APSR 中的进位标志。若移位运算移动了寄存器中的多个位,进位标志 C 的数据就会为移出寄存器的最后一位。
要使用这些指令的 16 位版本,寄存器需要为低寄存器,而且应该使用 S 后缀(更新APSR)。RRX 指令没有 16 位的形式。
指令(可选 S 后缀未列出) | 操作 |
---|---|
ASR Rd,Rn,#immed ; Rd = Rn>>immed | 算术右移 |
ASR Rd,Rn ; Rd = Rd>>Rn | 算术右移 |
ASR Rd,Rn,Rm ; Rd = Rn>>Rm | 算术右移 |
LSL Rd,Rn,#immed ; Rd = Rn<<immed | 逻辑左移 |
LSL Rd,Rn ; Rd = Rd<<Rn | 逻辑左移 |
LSL Rd,Rn,Rm ; Rd = Rn<<Rm | 逻辑左移 |
LSR Rd,Rn,#immed ; Rd = Rn>>immed | 逻辑右移 |
LSR Rd,Rn ; Rd = Rd>>Rn | 逻辑右移 |
LSR Rd,Rn,Rm ; Rn = Rd>>Rm | 逻辑右移 |
ROR Rd,Rn ; Rd 右移 Rn | 循环右移 |
ROR Rd,Rn,Rm ; Rd = Rn 右移 Rm | 循环右移 |
RRX Rd,Rn ; {C,Rd} = | 循环右移并展开 |
6. 数据转换运算(展开和返序)
有符号和无符号展开
指令 | 操作 |
---|---|
SXTB Rd,Rm ; Rd = 有符号展开(Rn[7:0]) | 有符号展开字节为字 |
SXTH Rd,Rm ; Rd = 有符号展开(Rn[15:0]) | 有符号展开半字为字 |
UXTB Rd,Rm ; Rd = 无符号展开(Rn[7:0]) | 无符号展开字节为字 |
UXTH Rd,Rm ; Rd = 无符号展开(Rn[15:0]) | 无符号展开半字为字 |
这些指令的 32 位形式可以访问高寄存器,而且可以选择在进行有符号展开运算前将输入数据循环右移。
具有可选循环移位的有符号和无符号展开
指令 | 操作 |
---|---|
SXTB Rd,Rm {,ROR #n} ;n = 8/16/24 | 有符号展开字节为字 |
SXTH Rd,Rm {,ROR #n} ;n = 8/16/24 | 有符号展开半字为字 |
UXTB Rd,Rm {,ROR #n} ;n = 8/16/24 | 无符号展开字节为字 |
UXTH Rd,Rm {,ROR #n} ;n = 8/16/24 | 无符号展开半字为字 |
SXTB/SXTH 使用 Rn 的 bit[7]/bit[15] 进行有符号展开,而 UXTB 和 UXTH 则将数据以零展开的方式扩展为 32 位。
例如,R0 为 0x55AA8765
SXTB R1,R0 ; R1 = 0x00000065
SXTH R1,R0 ; R1 = 0xFFFF8765
UXTB R1,R0 ; R1 = 0x00000065
UXTH R1,R0 ; R1 = 0x00008765
这些指令可以用于不同数据类型间的转换,在从存储器中加载数据时,可能会同时产生有符号展开和无符号展开(如 IDRB 用于无符号数据,LDRSB用于有符号数据)。
数据反转指令
指令 | 操作 |
---|---|
REV Rd,Rn ; Rd = rev(Rn) | 反转字中的字节 |
REV16 Rd,Rn ; Rd = rev16(Rn) | 反转每个半字中的字节 |
REVSH Rd,Rn ; Rd = revsh(Rn) | 反转低半字中的字节并将结果有符号展开 |
7. 位域处理指令
指令 | 操作 |
---|---|
BFC Rd,#lsb,#width | 清除寄存器中的位域 |
BFI Rd,Rn,#lsb,#width | 将位域插入寄存器 |
CLZ Rd,Rm | 前导零计数 |
RBIT Rd,Rn | 反转寄存器中的位顺序 |
SBFX Rd,Rn,#lsb,#width | 从源中复制位域并有符号展开 |
UBFX Rd,Rn,#lsb,#width | 从源寄存器中复制位域 |
BFC 用于清除寄存器中的指定位段(Bit Field)。这种指令通常用于将寄存器中的特定位段设置为0,而不影响其他位。
-
<lsb> 是位段的最低有效位(Least Significant Bit)的位置。
-
<width> 是要清除的位段的宽度,即要清除的位的数量。
LDR R0,=0x1234FFFF
BFC R0,#4,#8
结果:R0 = 0x1234F00F
BFI 用于将一个位字段(Bit Field)插入到另一个寄存器的指定位置。该指令通常用于需要在一个寄存器中插入一段数据的情况,而不影响其他位的值。
- Rd 是目标寄存器,表示将要被修改的寄存器。
- Rn 是源寄存器,表示提供要插入的位字段的寄存器。
-
<lsb> 是目标寄存器中要插入位字段的最低有效位(Least Significant Bit)的位置。
-
<width> 是要插入的位字段的宽度,即要从源寄存器中插入的位数。
LDR R0,=0x12345678
LDR R1,=0x3355AACC
BFI R1,R0,#8,#16
结果:R0 = 0x335678CC
CLZ 用于计算寄存器中从最高位(MSB)开始的连续零位的个数。若没有位为 1 则结果为 32,而所有位都为 1 则结果为 0。
- Rd 是目标寄存器,用于存储从最高位开始的连续零位的个数。
- Rm 是源寄存器,从其中读取要进行前导零位计数的数据。
RBIT 用于反转寄存器中的位顺序。
- Rd 是目标寄存器,用于存储反转位顺序后的结果。
- Rm 是源寄存器,从其中读取要进行位反转的数据。
RBIT 指令的作用是将源寄存器 Rm 中的位顺序进行反转,并将结果存储到目标寄存 Rd 中。例如,如果源寄存器 Rm 的二进制表示是 1101,则目标寄存器 Rd 中存储的值将是 1011。
该指令常在数据通信中用于串行位数据流的处理。RBIT 指令对于一些位操作和加密算法非常有用。例如,在某些情况下,可以使用RBIT指令来实现位级别的混淆和反混淆,从而增强数据的安全性。此外,它还可以用于一些编码和解码操作,或者在通信协议中进行数据的转换和处理。
UBFX 用于从无符号数中提取指定的位字段(bit field)。该指令允许从寄存器中的任意位置(由操作数<#lsb>指定)开始提取任意宽度(由操作#<width>指定)的位域,将其零展开后放入目的寄存器。
- Rd 是目标寄存器,用于存储从源寄存器中提取的位字段。
- Rn 是源寄存器,从中提取位字段的无符号整数。
- lsb 是位字段的起始位索引(Least Significant Bit),即要提取的位段的最低有效位的位置。
- width 是要提取的位字段的宽度,即要从源寄存器中提取的位数。
LDR R0,=0x5678ABCD
UBFX R1,R0,#4,#8
结果:R1 = 0x000000BC
SBFX 指令的作用是从源寄存器 Rn 中的有符号整数中提取位字段,从给定的起始位索引<#lsb> 开始,提取指定数量的位数 #<width>,并将结果存储到目标寄存器 Rd 中。
- Rd 是目标寄存器,用于存储从源寄存器中提取的位字段。
- Rn 是源寄存器,从中提取位字段的有符号整数。
- lsb 是位字段的起始位索引(Least Significant Bit),即要提取的位段的最低有效位的位置。
- width 是要提取的位字段的宽度,即要从源寄存器中提取的位数。
LDR R0,=0x5678ABCD
SBFX R1,R0,#4,#8
结果:R1 = 0xFFFFFFBC
8. 比较和测试指令
指令 | 操作 |
---|---|
CMP Rn,Rm | 比较:计算 Rn-Rm,APSR 更新但结果不会保存 |
CMP Rn,#immed | 比较:计算 Rn-立即数 |
CMN Rn,Rm | 负比较:计算 Rn+Rm,APSR 更新但结果不会保存 |
CMN Rn,#immed | 负比较:计算 Rn+立即数,APSR 更新但结果不会保存 |
TST Rn,Rm | 测试(按位与):计算 Rn 和 Rm 相与后的结果,APSR 中的 N 位和 Z 位更新,但与运算的结果不会保存,若使用了桶形移位则更新 C 位 |
TST Rn,#immed | 测试(按位与):计算 Rn 和立即数相与后的结果,APSR 中的 N 位和 Z 位更新,但与运算的结果不会保存 |
TEQ Rn,Rm | 测试(按位异或):计算 Rn 和 Rm 异或后的结果,APSR 中的 N 位和 Z 位更新,但运算的结果不会保存,若使用了桶形移位则更新 C 位 |
TEQ Rn,#immed | 测试(按位异或):计算 Rn 和立即数异或后的结果,APSR 中的 N 位和 Z 位更新,但运算的结果不会保存 |
由于 APSR 总是会更新,因此这些指令中不存在 S 后缀。
9. 程序流控制
9.1 跳转
可以引发跳转的指令:
- 跳转指令(如 B、BX)
- 更新 R15(PC)的数据处理指令(如 MOV、ADD)。
- 写人 PC 的读存储器指令(如LDR、LDM、POP)。
一般来说,尽管可以使用任意一种操作来实现跳转,比较常用的还是 B(跳转)、BX(间接跳转)以及 POP 指令(通常用于函数返回)。
无条件跳转指令
指令 | 操作 |
---|---|
B<label> / B.W<label> | 跳转到 label。若跳转范围超过了 +/-2KB,则可以指定 B.W<label> 使用 32 位版本的跳转指令,这样可以得到较大的范围 |
BX<Rm> | 间接跳转。跳转到存放于 Rm 中的地址值,并且基于 Rm 第 0 位设置处理器的执行状态(T位)(由于 Cortex-M 处理器只支持 Thumb 状态,Rm 的第 0 位必须为 1) |
9.2 函数调用
执行跳转并同时将返回地址保存到链接寄存器(LR),这样在函数调用结束后处理器还
以跳回之前的程序。
函数调用指令
指令 | 操作 |
---|---|
BL<label> | 跳转到标号地址并将返回地址保存在 LR 中 |
BLX<Rm> | 跳转到 Rm 指定的地址,并将返回地址保存在 LR 中,以及更新 EPSR 中的 T 为 Rm 的最低位 |
当执行这些指令时:
- 程序计数器被置为跳转目标地址。
- 链接寄存器(LR/R14)被更新为返回地址,这也是已执行的 BL/BLX 后指令的地址。
- 若指令为 BLX,则 EPSR 中的 Thumb 位也会被更新为存放跳转目标地址的寄存器的最低位。
由于 Cortex-M3 和 M4 处理器只支持 Thumb 状态,BLX 操作中使用的寄存器的最低位必须要置为 1,要不然,它就表示试图切换至 ARM 状态,这样会引发错误异常。
若需要调用子程序则保存 LR,BL 指令会破坏 LR 寄存器的当前内容。因此,若程序代码稍后需要 LR 寄存器,则应该在执行 BL 前保存 LR。最常用的方法为在子程序开头处将 LR 压人栈中。
9.3 条件跳转
条件跳转基于 APSR 的当前值条件执行(N、Z、C、和 V 标志)。
APSR 中的标志(状态位),可用于条件跳转控制
标志 | FSR 位 | 描述 |
---|---|---|
N | 31 | 负标志(上一次运算结果为负值) |
Z | 30 | 零(上一次运算结果得到零值,例如,比较两个数值相同的寄存器) |
C | 29 | 进位(上一次执行的运算有进位或没有借位,还可以是移位或循环移位操作中移出的最后一位) |
V | 28 | 溢出(上一次运算的结果溢出) |
APSR 受到以下情况的影响:
- 多数 16 位数据处理指令。
- 带有 S 后缀的 32 位(Thumb-2)数据处理指令,如ADDS.W。
- 比较(如CMP)和测试(如TST、TEQ)。
- 直接写 APSR/xPSR。
bit[27] 为另外一个标志,也就是 Q 标志,用于饱和算术运算而非条件跳转。
条件跳转发生时所需的条件由后缀指定(在表中表示为<cond>)。条件跳转指令具有 16 位和 32 位的形式,它们的跳转范围不同。
条件跳转指令
指令 | 操作 |
---|---|
B<cond><label> B<cond>.W<ladel> |
若条件为 true 则跳转到label, 例如:CMP R0,#1 BEQ loop ;若 R0 等于 1 则跳转到 "loop" 若所需的跳转范围超过了 ±254 字节,则可能需要指定使用 32 位版本的跳转指令以增加跳转范围 |
条件执行和条件跳转用的后缀
后缀 | 条件跳转 | 标志(APSR) |
---|---|---|
EQ | 相等 | Z 置位 |
NE | 不相等 | Z 清零 |
CS/HS | 进位置位/无符号大于或相等 | C 置位 |
CC/LO | 进位清零/无符号小于(有BLO的写法) | 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 清零(Z == 0,N == V) |
LE | 有符号小于或者相等 | Z 置位,或者 N 置位 V 清零,或者 N 清零 V 置位(Z == 1 或 N != V) |
简单的条件跳转
CMP R0,#1 ; 比较R0和1
BEQ p2 ; 若相等则跳转p2
MOVS R3,#1 ; R3 = 1
B p3 ; 跳转到p3
p2
MOVS R3,#2
p3 ; 标号p3
...
9.4 比较和跳转
指令 | 操作 |
---|---|
CBZ <寄存器>, <目标地址> | 比较为零则跳转 |
CBNZ <寄存器>, <目标地址> | 比较非零则跳转 |
上面两个指令只支持向前跳转,不支持向后跳转。
CBZ:
<寄存器>
是一个寄存器标识符,用于存储需要检查是否为零的值。<目标地址>
是跳转到的目标地址,如果<寄存器>
中的值为零,则执行跳转到该地址。
CBZ R0, zero_detected ; 如果 R0 为零,则跳转到 zero_detected 标签处
; 继续执行其他指令
...
zero_detected:
; 在 R0 为零时执行的代码
例子:
i = 5;
while(i != 0)
{
funcl();
i--;
}
编译:
MOV R0,#5 ; 设置环境变量
loop1 CBZ R0,looplexit ; 若循环变量 0 则跳出循环
BL funcl ; 调用函数
SUBS R0,#1 ; 循环变量减小
B loopl ; 下一个循环
looplexit
CBNZ:
<寄存器>
是一个寄存器标识符,用于存储需要检查是否为零的值。<目标地址>
是跳转到的目标地址,如果<寄存器>
中的值不为零,则执行跳转到该地址。
CBNZ R0, nonzero_detected ; 如果 R0 不为零,则跳转到 nonzero_detected 标签处
; 继续执行其他指令
...
nonzero_detected:
; 在 R0 不为零时执行的代码
例子:
status = strchr(email_address, "@");
if(staus == 0) // 若 email 地址中无 @ 则 status 为 0
{
show_error_message();
exit(1);
}
编译:
...
BL strchr
CBNZ R0,email_looks_okay ;结果非零则跳转
BL show_error_message
BL exit
email_looks_okay
...
APSR 的值不受 CBZ 和 CBNZ 指令的影响。
9.5 条件执行
在 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 指令指定的条件
相反。
各种大小的 IT 指令块
表例:
- <x>指定第二个指令的执行条件
- <y>指定第三个指令的执行条件
- <z>指定第四个指令的执行条件
- <cond>指定指令块的基本条件,若<cond>为 true,则执行 IT 后的第一条指令。
IT 块[每个<x>、<y>和<z>可以为 T 或 E] | 例子 | |
---|---|---|
只有一个条件指令 | IT <cond> instr1<cond> |
IT EQ ADDEQ R0,R0,R1 |
两个条件指令 | IT <x> <cond> instr1<cond> instr2<cond or ~(cond)> |
ITE GE ADDGE R0,R0,R1 ADDLT R0,R0,R3 |
三个条件指令 | IT <x><y> <cond> instr1<cond> instr2<cond or ~(cond)> instr3<cond or ~(cond)> |
ITET GT ADDGT R0,R0,R1 ADDLE R0,R0,R3 ADDGT R2,R4,#1 |
四个条件指令 | IT <x><y><z> <cond> instr1<cond> instr2<cond or ~(cond)> instr3<cond or ~(cond)> instr4<cond or ~(cond)> |
ITETT NE ADDNE R0,R0,R1 ADDEQ R0,R0,R3 ADDNE R2,R4,#1 MOVNE R5,R3 |
若<cond>为 AL,则在条件控制中不能使用 E,因为它表示指令永远不会执行。
IT 指令块中的数据处理指令不应修改 APSR 的数值,当有些 16 位的数据处理指令在 IT 指令块中使用时,APSR不会更新,这一点和它们正常操作更新 APSR 的情况不同。这样,就可以在 IT 指令块中使用 16 位的数据处理指令以降低代码大小。
对于一些汇编工具,代码中无须使用 IT 指令。通过简单地给普通指令添加条件后缀,汇编工具(如 DS-5 Professional 或 Keil MDK-ARM 的 ARM 汇编器)会在前面自动插入所需的 IT 指令。由于无须手动插人 IT 指令,因此这样有助于经典 ARM 处理器(如ARM7TDMI)往 Cortex-M3/M4 的移植。
初始汇编代码:
...
CMP R1,#2
ADDEQ R0,R1,#1
...
生成的目标文件中的反汇编代码
...
CMP R1,#2
IT
ADDEQ R0,R1,#1
...
9.6 表格跳转
TBB(表格跳转字节)和 TBH(表格跳转半字),它们和跳转表一起使用,通常用于实现 C 代码中的 switch 语句。由于程序计数器数值的第 0 位总是为 0,利用表格跳转指令的跳转表也就无须保存这一位,因此,在目标地址计算中跳转偏移被乘以 2。
10. 饱和运算
Cortex-M3 处理器支持两个用于有符号和无符号数据饱和调整的指令:SSAT(用于有符号数据)和USAT(用于无符号数据)。Cortex-M4 处理器同样支持这两条指令,而且还支持用于饱和算法的其他指令。
饱和多用于信号处理:
饱和运算通过将数据强制置为最大允许值,减小了数据畸变。畸变仍然是存在的,不过若数据没有超过最大范围太多,就不会有太大的问题。
饱和运算指令
指令 | 描述 |
---|---|
SSAT <Rd>,#<immed>,<Rn>, | 有符号数据的饱和 |
USAT <Rd>,#<immed>,<Rn>, | 有符号数据转换为无符号数据的饱和 |
<Rn>为输入值;
<shift>为饱和前可选的移位操作,可以为 #LSL N 或 #ASR N;
<immed>为执行饱和的位的位置;
<Rd>为目的寄存器
除了目的寄存器,APSR 中的 Q 位也会受结果的影响。若在运算中出现饱和 Q 标志就会置位,它可以通过写 APSR 清除。
SSAT指令例子:(32 位有符号数值要被饱和为16 位有符号数)
SSAT R1,#16,R0
有符号饱和结果实例
输入(R0) | 输出(R1) | Q位 |
---|---|---|
0x00020000 | 0x00007FFF | 置位 |
0x00008000 | 0x00007FFF | 置位 |
0x00007FFF | 0x00007FFF | 不变 |
0x00000000 | 0x00000000 | 不变 |
0xFFFF8000 | 0xFFFF8000 | 不变 |
0xFFFF7FFF | 0xFFFF8000 | 置位 |
0xFFFE0000 | 0xFFFF8000 | 置位 |
USAT指令例子:
USAT R1,#16,R0
无符号饱和结果示例
输入(R0) | 输出(R1) | Q位 |
---|---|---|
0x00020000 | 0x0000FFFF | 置位 |
0x00008000 | 0x00008000 | 不变 |
0x00007FFF | 0x00007FFF | 不变 |
0x00000000 | 0x00000000 | 不变 |
0xFFFF8000 | 0x00000000 | 置位 |
0xFFFF8001 | 0x00000000 | 置位 |
0xFFFFFFFF | 0x00000000 | 置位 |
11. 异常相关指令
管理调用(SVC)指令:SVC #immed
管理调用(SVC)指令用于产生 SVC 异常(异常类型为11)。SVC一般用于嵌入式OS/实时OS(RTOS),其中,运行在非特权执行状态的应用可以请求运行在特权状态的 OS 的服务。SVC 异常机制提供了从非特权到特权的转换。
SVC 机制可以作为应用任务访问各种服务(包括 OS 服务或其他 API 函数)的入口,这样应用任务就可以在无须了解服务的实际存储器地址的情况下请求所需服务。它只需知道SVC服务编号、输入参数和返回结果。
SVC 指令要求 SVC 异常的优先级高于当前的优先级,而且常没有被 PRIMASK 等存器屏蔽,不然就会触发错误异常。因此,由于 NMI 和 HardFaut 异常的优先级总是比 SVC 异常大,也就无法在这两个处理中使用 SVC。
其中的立即数为 8 位,数值自身不会影响 SVC 异常的动作,不过 SVC 处理可以在程序中提取出这个数值并将其用作输入参数,这样可以确定应用任务所请求的服务。
改变处理器状态(CPS)指令
对于 Cortex-M 处理器,可以使用这条指令来设置或清除 PRIMASK 和 FAULTMASK 等中断屏蔽寄存器。注意,这些寄存器也可以用 MSR 和 MRS 指令访问。
CPS 指令在使用时必须要带一个后缀:IE(中断使能)或 ID(中断禁止)。由于Cortex-M3 和 Cortex-M4 处理器具有多个中断屏蔽寄存器,因此,还得指定要设置/清除的寄存器。
RRIMASK 和 FAULTMASK 的设置及清除指令
指令 | 操作 |
---|---|
CPSIE I | 使能中断(清除 PRIMASK) 和 _enable_irq() 相同 |
CPSID I | 禁止中断(设置PRIMASK),NMI 和 HardFault 不受影响 和 _disable_irg()相同 |
CPSIE F | 使能中断(清除 FAULTMASK) 和 _enable_fault_irq() 相同 |
CPSID F | 禁止错误中断(设置FAULTMASK),NMI不受影响 和 _disable_fault_irg() 相同 |
切换 PRIMASK 和 FAULTMASK 可以禁止或使能中断,经常用于确保时序关键的任务在不被打断的情况下快速完成。
12. 休眠模式相关指令
进入休眠模式
WFI ;等待中断(进入休眠)
WFE ;等待事件(条件进入休眠)
C 编程(符合 CMSIS 的设备驱动库)
_WFI(); //等待中断(进入休眠)
_WFE(); //等待事件(条件进入休眠)
WFI(等待中断)指令会使处理器立即进入休眠模式,中断、复位或调试操作可以将处理器从休眠中唤醒。
WFE(等待事件),会使处理器有条件地进入休眠。
在 Cortex-M3/M4 处理器内部,一个只有一位的寄存器会记录事件。若该寄存器置位,WFE 指令不会进入休眠而只是清除事件寄存器并继续执行下一条指令;若该寄存器清零,则处理器会进入休眠而且会被事件唤醒,事件可以是中断、调试操作、复位或外部事件输入的脉冲信号(例如,事件脉冲可由另一个处理器或外设产生)。
Cortex-M 处理器的接口信号包括一个事件输人和一个事件输出。处理器的事件输人可由多处理器系统中其他处理器的事件输出产生,因此,处于 WFE 休眠(如等待自旋锁)的处理器可由其他的处理器唤醒。有些情况下,这些信号会被连接到 Cortex-M 微控制器的 I/O 端口,而其他一些 Cortex-M 微控制器的事件输入可能会被连接到低电平,而事件输出则可能会用不上。
事件输出
SEV ;发送事件
C 编程(符合 CMSIS 的设备驱动库)
_SEV(); //发送事件
当执行 SEV 时,事件输出接口就会出现一个单周期的脉冲,SEV 指令还会设置同一处理器的事件寄存器。
13. 存储器屏障指令
存储器屏障指令可用于:
- 确保存储器访问的顺序。
- 确保存储器访问和另一个处理器操作间的顺序。
- 确保系统配置发生在后序操作之前。
存储器屏障指令
指令 | 描述 |
---|---|
DMB | 数据存储器屏障。确保在执行新的存储器访问前所有的存储器访问都已经完成 |
DSB | 数据同步屏障。确保在下一条指令执行前所有的存储器访问都已经完成 |
ISB | 指令同步屏障。清空流水线,确保在执行新的指令前,之前所有的指令都已完成 |
对于 C 编程,若使用符合 CMSIS 的设备驱动,可以利用下面的函数来实现这些指令:
void_DMB(void); //数据存储器屏障
void_DSB(void); //数据同步屏障
void_ISB(void); //指令同步屏障
由于 Cortex-M 处理器具有相对简单的流水线,而且 AHBLite 总线协议不允许对存储器系统中的传输重新排序,因此,即便没有存储器屏障指令,多数应用还是可以正常工作的。
存储器屏障指令应用场景示例
应用场景(用于当前的 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 |
架构定义存储器屏障指令应用场景示例
应用场景(基于架构推荐) | 所需的屏障指令 |
---|---|
存储器保护单元(MPU)配置更新,然后在 MPU 配置变化影响的存储器区域中取出并执行一条指令(只允许数据访问的区域,无取指) | DSB |
进入休眠前(WFI 或 WFE) | DSB |
信号量操作 | DMB 或 DSB |
修改异常(如 SVC)的优先级后触发 | DSB |
利用向量表偏移寄存器(VTOR)将向量表重置与新的位置,然后触发新向量的异常 | DSB |
修改向量表中的向量人口(若在 SRAM 中)后立即触发同一个异常 | DSB |
恰好处于自复位前(可能会有正在进行的数据传输) | DSB |
14. 其他指令
NOP 指令:产生指令对齐或延时
NOP ;什么都不做
C 编程
_NOP(); //什么都不做
有一点需要注意,NOP 指令产生的延时一般是精确的(简单地占用了一个时钟周期),不同系统间可能会存在差异(如存储器等待状态和处理器类型)。若延时需要非常精确,就应该使用硬件定时器。
断点(BKPT)指令
在软件开发/调试过程中,断点(BKPT)指令用于实现应用程序中的软件断点。若程序在 SRAM 中执行,则该指令一般由调试器插入以替换原有的指令。当到达断点时,处理器会被暂停,然后调试器就会恢复原有的指令,用户也可以通过调试器执行调试任务。BKPT 指令也可以用于产生调试监控异常,它具有一个8位立即数,调试器或调试监控异常可以将该数据提取出来,并根据该信息确定要执行的动作。例如,可用某些特殊数值表示半主机请求(同工具链相关)。
BKPT #immed ;断点
immed 是一个 16 位的立即数,表示断点的标识符。通常情况下,这个立即数是用来标识不同的断点,但在实际的使用中,可能会忽略这个值,或者使用特定的约定来进行设置。
C 编程
_BKPT(immed);
除了 BKPT,Cortex-M3 和 Cortex-M4 处理器中还存在一个断点单元,它具有最多 8 个硬件断点,而且不用覆盖原有的程序映像。