嵌入式之ARM指令系统(通用指令32位)
嵌入式之ARM指令系统(通用指令32位)
一、汇编指令类别
1. ARM汇编指令(ARM公司定)
一条汇编指令唯一对应一条机器指令 RISC
MOV R0, #5 => 010101...
MOV R0,R1
ADD R2,R1,R0
操作码: 表示是何种操作 "指令助记符"
MOV 表示 移动
ADD 表示 加法
....
操作数:
结果操作数: 用来保存计算的结果 目的操作数 前置
运算操作数:
第一操作数 R
第二操作数
指令是用来“运算”:运算符
操作数
2. 伪指令(由编译器厂商定的,keil环境下有keil的伪指令,
GNU环境有gnu的伪指令)
keil环境下常用的伪指令:
数据 C
int a = 5;
int b[10];
int c[10] = {2,3};
data_a //顶格写的 标号,标号表示一个地址,编译器可以读到
DCD 5 //DCD X : 分配4bytes空间,并且把X的值填入此处
data_b
SPACE 40 //SPACE X : 在此处开辟 X 个字节的空间,内容不定。
data_c
DCD 2
DCD 3
SPACE 8*4 //40-2*4=32
标号必须顶格,DCD是四个字节,DCW是两个字节。标号顶格、指令tab跳格



"段": 分区域
代码段 这个区域都是代码---ROM 0x08000000---
数据段 这个区域存放的是全局的数据
堆栈段 stack
...
那么在keil下面如何定义“段”呢?
AREA 段名1, 段的属性1, 段的属性2, ...
eg: AREA mstack, DATA, READWRITE
AREA 段名2 。。。
eg:AREA RESET , DATA, READONLY
END

段名:自己定义,规定的定义
段的属性1:
CODE 代码段
CODE32 thumb-2代码
DATA 数据段
NOINIT 不初始化
属性2:
READWRITE 只读可写 。。。
READONLY 只读
属性3
ALIGN=3 8字节对齐(2的3次幂对齐)
Cortex-M4要求代码必须是 8字节对齐,否则编译不会通过;
PRESERVE8 指令,表示后续如果没有特殊说明,都是采用 8字节对齐
当有C语言和汇编并存时,C语言的编译后的指令也需要8字节对齐,
这时这条指令可以实现。

3. 宏指令(同上,由编译器厂商指定)
由编译器厂商写的,keil环境下有keil的宏指令 。。。 在keil中需要顶格
stack_size EQU 0x200 ; // #define stack_size 0x200
4.keil环境下的汇编语句格式:
lable 指令 ;注释
lable:顶格写,标识这一行的地址
指令:ARM指令,伪指令
; 到行末,都是注释部分。
练习:
一个启动 .s文件(start.s),至少需要包含三个段:
;//堆栈段
; //中断向量表
; //代码段
①新建.s文件

②写入代码
stack_size EQU 0x200
; define stack
AREA mystack, DATA, READWRITE
stack_start
SPACE stack_size
stack_end
PRESERVE8
;define vectors
AREA RESET, DATA, READONLY
vectors
DCD stack_end ;stack-top
DCD test_start ;begin code
vectors_end
;define code
AREA mycode, CODE, READONLY,ALIGN=3
test_start
B . ;while(1) B表示跳转,B .表示原地循环
END
参考代码:(作业,带上注释)
二、ARM指令的寻址方式(自学)
三、ARM指令的格式
ARM指令的基本格式
<>内的必须要的 {} 可选的
opcode: operator code 操作码,指令助记符,表示哪条指令
如:
MOV
ADD
SUB
LDR/STR
...
cond: condition 条件。该指令执行的条件。如果省略不写,
则表示该条件指令无条件执行(必须执行)
if (r1 == r2)
{
r0 = 250; // MOV R0, #250
}
=>
CMP R1, R2; => if R1 == R2 , R1 - R2结果为0,
xPSR.Z == 1
//MOV(xPSR.Z == 1) R0, #250
//相当于MOVEQ R0, #250
EQ "equal" => 检查 xPSR.Z == 1
条件码 含义 测试的标志(xPSR中的标志位)
EQ Equal(相等) Z==1
NE Not Equal(不相等) Z==0
CS/HS Carry Set (C == 1)
unsigned Higher or Same C == 1
a>=b
CC/LO Carry Clear(C == 0) C == 0
a<b
MI MInous(负数) N == 1
PL Positive or Zero(非负数) N == 0
VS V Set(溢出) V == 1
VC V Clear(没溢出) V == 0
HI unsigned Higher (C == 1) && (Z == 0)
LS unsigned Lower or Same (C == 0) || (Z == 1)
GE signed Greater or Equal N == V
>= 1 1000 11000
LT Less Than N != V
<
GT Greater Than (N == V) && (Z == 0)
>
LE Less than or Equal (Z==1) ||(N != V)
CS/HS CC/LO 针对无符号数,但是有符号数,CMP 正,正或CMP负,负也满足。如果CMP 一正一负,则C标志位不满足



S: Status 表示该指令执行结果是否影响xPSR(程序状态寄存器)的标志位
如:
MOV R0, #0; -> 不会影响任何状态标志
MOVS R0, #0 ; ->会影响状态标志位
有一些指令如: CMP, CMN, TEQ ... 这些不加S也影响状态标志位
因为这些指令,不保存运算结果,只影响状态标志位
Rd: Register Desatiation 目标寄存器
用来保存运算的结果
operand1: 第一个操作数
operand2: 第2个操作数(有些指令没有第二个操作数).
操作数有如下形式:
(1) #immer_8r 立即数(常量表达式)
立即数 -> 常数
立即数的生成是有要求的:
by 0-255 shifted left by 0-23 or duplicated in all, odd or even bytes
ADD R0, R1, #250
ADD R0, R1, #0x10
ADD R0, R1, #(1 << 3) | (1 <<4)
为了避免立即数不合规
建议大家这样用:
LDR R0, =666666


(2) Rm 寄存器
操作数可以是一个寄存器。
如:
ADD R0, R1, R2;
R1 + R2 -> R0
(3) Rm,shift 寄存器移位方式
操作数可以是一个寄存器加移位方式
算术移位符号参与,逻辑移位无符号,符号位不动,由于计算机的巧妙设计,符号位就是最高数据位
算术右移:右边的低位直接干掉,左边全部补符号位。
逻辑移位:无论是左移还是右移全部补0.左移相当于乘2,右除2
C语言的中的移位是算术移位还是逻辑移位?
//有符号数:算术移位 无符号数:逻辑移位
int
-1 >> 30 ? //C语言中是算术移位,结果还很-1
-1u >> 30 ? //3
1 00001->1 11111
例如-8,补码1 1000,逻辑左移3位:1 1000 000[符号位不动,后面加3个0]
逻辑右移3位:1 000 1000[符号位不动,前面加3个0]
算术左移3位:1 0 111
算术右移3位:1 111 1
typeof(-1) => int
typeof(-1u) => unsigned int
LSL #n
Logic Shift Left 逻辑左移n位,
Logic逻辑移位,无论是左移还是右移,空出的位,都补0
LSR #n
Logic Shift Right 逻辑右移n位,
在移出的位为0的情况下,
LSL #n 就相当于在原寄存器值 乘以 2的n次方
LSR #n 就相当于在原寄存器值 除以 2的n次方
ASR #n 算术右移n位
算术移位,不应该改变它的符号。
最高n位补符号位的值.
ASL #n 没有。 => LSL逻辑左移
ROR #n Rotate Right 循环右移
把右边移的n位,补到最左边。
RRX 带扩展的循环右移1位
带C(xPSR.C) 那个位
type Rs
type为上述移位方式的一种,如: LSL, LSR,ASR, ROR, RRX...
Rs偏移量寄存器,低8位有效,要移的bit位数保存在Rs中。
代码测试:(1)MOV R1,#3 ;r13
MOV R2,#1 ;r21
LSL R2,R1 ;r2==8 1<<3

r13
r28
r0==5

写出代码!内卷 指令字节数少 2x12 2x4x3 2<<2


R=2
12R=4R+8R
=(R<<2)+8R //R左移两位后,此时等于4R
=4R+(4R)<<1
四、ARM指令(UAL)
1. ARM存储器访问指令
用来在存储器(Memory,) 《-》寄存器之间传递数据
把数据从存储器 -> 寄存器 加载 Loader LDR
把数据从寄存器 -> 存储器 存储 Store STR
没有一条指令可以实现存储器到存储器的交换、只能通过寄存器。
例如: MA1--3
MA2--6
①实现两者交换:LDR-LDR STR-STR
②实现MA1->MA2 LDR-STR
③实现数据加法:LDR-LDR-ADD-STR
(1)
LDR{cond} {S} {B/H} Rd, <地址> //从地址到Rd
STR{cond} {B/H} Rd, <地址> //从Rd到地址
LDR加载,把存储器<地址>中的内容加载到 寄存器Rd中
STR存储,把寄存器Rd中的内容,存储到存储器<地址>中去
//LDR先加载到寄存器,STR再从寄存器存到存储器。
问题二:存储器地址从哪里开始?
//从0x20000000开始,写代码时定义的比它大就行


//正确方法:0x20001000->Rn 【Rn】就是存储器0x20001000的地址空间 问题四:存储地址增加如何表示?直接+?(4个字节,相当于int,所以增4)



NOTE: (1)B: Byte一个字节, H: Half word半字(两个字节), 如果省略,默认为4个字节
B/H决定加载/存储多少个字节。



如果没有S就把地址的那个变量,当作是一个无符号的。
有符号来说,高位全部补符号位,
如果是无符号的,高位全部补0

(2)地址确定方式: 基址寄存器 + 偏移量
地址值肯定需要在一个寄存器中
即:
地址值
[Rn] Rn
[Rn, 偏移量] Rn + 偏移量 Rn值不变
[Rn, 偏移量]! Rn + 偏移量 Rn值+偏移量
[Rn], 偏移量 Rn Rn值=Rn + 偏移量
[]内表示存储器的地址值 ,
如果有!或,偏移量在[]外边,则表示做完后,基址值自动增加偏移量
偏移量有以下3种方式:
立即数:
如: LDR R1, [R0, #0x12]
[R0+0x12] -> R1
寄存器
如: LDR R1, [R0, R2]
[R0+R2] -> R1
寄存器及移位常数
如: LDR R1, [R0, R2, LSL #2]
[R0 + R2 << 2] -> R1
任务1:
给存储器0x20001000单元写入一个整数(-2).

任务2:将0x20001000单元的数值转到0x20001008。自己完成!

(1)
char ch =0x80; //编译时刻或运行时刻,为ch分配一个存储器空间 0x2000 1000,char是有符号
int a; //编译时刻或运行时刻,为a分配一个存储器空间 0x2000 1004,int a是给a一个地址
a = ch;


(2)
unsigned char ch =0x80; //编译时刻或运行时刻,为ch分配一个存储器空间 0x2000 1000
int a; //编译时刻或运行时刻,为a分配一个存储器空间 0x2000 1004
a = ch;

(2) 多寄存器存取
在一个连续的存储器地址上,进行多个寄存器的存取。
加载 LDR 多 Multi
多寄存器加载 LDM Loader Multi
多寄存器存储 STM Store Multi
LDM{cond}<模式> Rn{!}, reglist
STM{cond}<模式> Rn{!}, reglist
<1>这两条指令只是通过一个寄存器 Rn指定了一个存储器的地址,
存储器的多个地址,是连续增加,还是连续递减呢?
由<模式>来指定:
注意:无论是哪种模式,低地址都是对应编号低的寄存器(32位,所以+-4)
IA: Incrememt After() 每次传送后地址自动增加(+4) ,先放数后加地址<-----
DB: Decrement Before 每次传送前地址自动减少(-4) ,先减4,后取数<-----
IB: Increment Before 每次传送前地址先自动增加(+4)
DA:Decrement After 每次传送后地址自动减少(-4),先取数后减4
ARM Cortex M4只使用IA, DB
使用","隔开,寄存器由小到大排列(编译系统会自动按序号排)
如: {R1,R3,R4,R5}
{R1,R3-R5}
<3> ! 可加可不加
加: 表示最后存储器的地址写入到Rn中去。(保护现场)
不加: 最后Rn的值不变


任务:将R0-R3放到存储器单元0x20000200开始的递减连续单元存放,然后再恢复
参考代码:
Ⅰ、 空递增方式

【R3】=0x2000100c,0c原本没有东西,所以要先减4,再从存储器取数放到寄存器

恢复现场后,【R3】变为0x20001000

Ⅱ、满递减方式
(3) 堆栈操作:堆栈的低地址对应编号低的寄存器
压栈: PUSH
出栈: POP
"栈"就是一块内存,上面那两条指令,并没有指定内存地址,
PUSH, POP用的地址寄存器是SP
堆栈有四种类型:
A: add 递增堆栈 D: Dec递减堆栈
SP堆栈 栈顶指针,
栈顶指针可以保存元素 -> 满堆栈 Full
也可以指向空(不保存元素) ->空堆栈 Empty
EA: 空递增 // 相当于STMIA--LDMDB
PUSH X //先放数后加4,最后栈顶指向空。
X -> [SP]
sp ++
POP x //取数时先减4,后取数,因为一开始指向空。
sp--
[sp] -> x
FA: 满递增 // 相当于STMIB--LDMDA,
PUSH X //先加4,后放数,最后栈顶指向数,不为空
sp ++
x -> [SP]
POP x //先取数,再减4,因为开始栈顶指向满,指向数据
[sp] -> x
sp --
ED: 空递减 // 相当于STMDA--LDMIB
PUSH X //先放数,后减4,最后栈顶指向空
x -> [sp]
SP--
POP x //取数时,先加4再取,因为一开始栈顶指向空
sp++
[sp] -> x
PUSH X //先减4,再放数,最后栈顶指向满,指向数据
sp--
x -> [sp]
POP x //先取数,再加4
[sp] -> x
sp++
<----- ARM采用的堆栈形式是: FD满递减堆栈

例子: C函数的实现
“现场保护” :
“现场恢复”
PUSH X
sp-- //0x20000200
x -> [sp]
//PUSH {R0-R3}
0x20000200-4 0x200001FC R3
0x200001F8 R2
0x200001F4 R1
0x200001F0 R0
POP x
[sp] -> x
sp++
//POP {R0-R3}
//SP--0x200001F0 R0-->R1--->R2--->R3 SP 0x20000200
(堆栈的低地址对应编号低的寄存器,编号大的先入栈,而不是最后写的,后进先出,小编号R0最后进,最先出。所以写程序一般按顺序,小编号在前,大的在后。)
2. ARM数据处理指令 -> 对寄存器内容操作
//如果想对存储器的内容进行操作,将数据导出到寄存器中进行操作,最后再放回存储器
数据传送指令 算术运算指令
逻辑运算指令
比较指令
(1) 数据传送指令
MOV{cond}{S} Rd, operand2 ; Rd <-- operand2
MVN{cond}{S} Rd, operand2 ; Rd <--- ~operand2(取反)
S: 表示结果影响xPSR状态标志位
第二操作数 operand2 -> Rd
如:
MOV R0, #3
MOV R1, R0
MOV R2, R1, LSL #2
-----


ADD{cond}{S} Rd, Rn, operand2; Rd <--- Rn + operand2
ADC{cond}{S} Rd, Rn, operand2; Rd <--- Rn + operand2 + xPSR.C
例子:
32位的寄存器,如何实现64bits 加法 a + b -> c
//分成32+32
a : R1(高32位) R0(低32位)
b : R3 R2
c : R5 R4
ADDS R4, R2, R0 //低32位的和,一定要加S,带标志位
ADCS R5, R3, R1 //低32位和的进位结果以及高32位的和。带进位(第31位)的加法,实现4位相加
SUB{cond}{S} Rd, Rn, operand2; Rd <--- Rn - operand2
SBC{cond}{S} Rd, Rn, operand2; Rd <--- Rn - operand2 - !xPSR.C 带借位的减法
例:32位的寄存器,如何实现64bits的减法? a - b -> c
a : R1 R0
b : R3 R2
c : R5 R4
SUBS R4, R0, R2 ; 此时 if R0 - R2产生的借位 -> xPSR.C == 0
SBCS R5, R1, R3 ; R1 - R3 - 借位
借位: !xPSR.C(取反,因为减法有借位是c=0)
RSB 逆向减法指令
Reserve
RSB{cond}{S} Rd, Rn, operand2; operand2 - Rn -> Rd
RSC{cond}{S} Rd, Rn, operand2; operand2 - Rn - !xPSR.C -> Rd 带借位的逆向减法

(3) 逻辑运算指令 (按位)
AND{cond}{S} Rd, Rn, operand2; AND 与, Rn & operand2 -> Rd 按位与
ORR{cond}{S} Rd, Rn, operand2; OR 或, Rn | operand2 -> Rd 按位或
EOR{cond}{S} Rd, Rn, operand2; EOR 异或 Rn ^ operand2 -> Rd 按位异或 (相同为0,不同为1)
//Rd,目的寄存器,Rn表示寄存器,说明第一操作数只能是寄存器,operand2第二操作数可以是三种
Bit Clear 位清零,把一个指定寄存器的中,指定的bit位给清掉
BIC{cond}{S} Rd, Rn, operand2; Rn & (~operand2) -> Rd
把Rn中 operand2中的为1的哪些位置上的bit位清零。
BIC置零 OPP置1
练习: ①R0 低4位清零

②R0 清零
法一:


③低四位清零
法一:


//0xf是000000……1111(32位),取反变成111111…… 0000,相与后低四位变0 法三:

法一:

法二:

法一:


⑥把第26、21、1位清零




(4) 比较指令: 不需要加S,直接影响xPSR中的标志位。运算结果不保存。
CMP Rn, operand2; 比较Rn与operand2的大小 Rn - operand2
if Rn == operand2
CMP Rn, operand2
xPSR.Z == 1 => EQ
CMN Rn, operand2; 比较Rn与operand2的大小, Rn + operand2(负数比较)
(5)TST Rn, operand2 ; Rn & operand2 //用来测试Rn中特定的bit位是否为1,一般测定一位,测试多位的话,只要有一个位为1,z就为0
Rn & operand2 => xPSR.Z == 1
=> 说明operand2中为1的哪些bit的,在
Rn都为0
测试位是1,Z0
测试位是0,Z1
例子:
测试R0中的第2和第3bit位,是否为1
【注意:数据位一般是从0开始数的,所以最高位是31位,0位是最低位】

Rn == operand2 => Rn ^ operand2 == 0 => xPSR.Z == 1
3. 乘法指令
略
4.分支指令:用来实现代码的跳转
有两种方式可以实现程序的跳转
(1) 分支指令
B lable ; lable -> PC, 不带返回的跳转 ,跳转到label,去了就不再回来

BL lable ; 过程调用,函数调用 带返回的
把下一条指令的地址 -> LR
lable ->
(2) 直接向PC寄存器赋值
MOV PC, LR
MOV PC, #0x80000000
5、杂项指令
(1)MRS Rd, xPSR xPSR: APSR, IPSR, EPSR
程序状态寄存器的值 赋值给Rd
MOVS R0,#-1
MRS R0,APSR ;R0==0x80000000

将通用寄存器Rd的值,赋值给程序状态寄存器

伪指令,机器不识别,但是可以表示程序员或编译器的某种想要的操作。
编译器会把伪指令变成一条或多条合适的机器指令。
(1) NOP
No Operation
空操作,不产生任何实际的效果,但是占用机器周期。
(1)
NOP
NOP
(2)
NOP
<-> MOV R0, R0
MOV R1, R1
MOV R2, R2
.....

(2) LDR{cond} Rd, =expr
LDR伪指令用于加载表达式expr的值,到寄存器Rd中去。
expr可以为一个任意的常数,或标号,或常量表达式...
LDR Rd, =expr
=>
expr: (1<< 2)
<1> 如果expr是一个合法的立即数
LDR Rd, =expr
<=> MOV Rd, #expr
<2>解决立即数不合规的问题,可以直接给出存储器的地址
LDR R0,=0x12345678
LDR R0,=0x20001000
<3>标号(地址)
①LDR Rd, =data1
data1
DCD 0x12345678
(2)int i=4;
;先定义数据段
AREA mydata,DATA,READWRITE
data_i
SPACE 4
;标号指向数据空间的地址
MOV R0,#4
LDR R1,=data_i
STR R0,[R1]
任务:R0--2 R0*24--->data1(放到数据段1),并能查看
分析:最小的启动代码应该包括堆栈、中断向量、代码,今天的要求是定义数据段,如何定义段? AREA name, type, READWRITE,然后定义大小。最后在代码段写代码,LDR Rd, =data1



实现代码:
stack_size EQU 0x200
vector_size EQU 0x400
; define stack
AREA mystack, DATA, READWRITE
stack_start
SPACE stack_size
stack_end
PRESERVE8
;define vectors
AREA RESET, DATA, READONLY
vectors
DCD stack_end ;stack-top
DCD code_start ;begin code
vectors_end
;define data
AREA mydata, DATA,READONLY
data_s
SPACE 4
;define code
AREA mycode, CODE, READONLY,ALIGN=3
code_start PROC
MOV R0,#2 ;0000 0010 =0x0002
LSL R0,#3 ;8R0=0001 0000 =0x0010
ADD R0,R0,R0,LSL #1 ;8R0+16R0=24R0=0011 0000=0x30
LDR R1,=data_s
STR R0,[R1]
B .
END
扩展:如果定义了两个数据段

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通