STM32学习【4】ARM汇编(够用)

目录

ARM汇编语言基础

对于学习汇编,我有话说,哈,我们实际工作中用到的并不多,我们学会常用的,遇见不会的直接大模型问就可以了,通过汇编,我们可以体会到数据底层的流动规律,写代码自然就会通透,更近一步,我们要彻底学会RTOS,底层的知识也是必不可少的。

写在前面

在CPU的世界里,一切外设都被抽象为寄存器或带有地址的内存数据。CPU只关心其内部的16个寄存器以及内存数据,外设寄存器虽然物理上与内存不同,但在CPU眼中并无区别,唯一的差异在于地址空间。


1. ARM汇编的分类

ARM汇编语言大致可以分为以下几类:

  1. 内存读写:用于访问内存数据。
  2. 运算类:执行加法、减法、逻辑运算等。
  3. 跳转/分支:实现程序流程控制。
  4. 比较 :类似于高级语言中的if语句。

这些指令组合成指令集,构成了ARM汇编语言的核心。


2. 关于指令集

ARM公司发布了两类主要的指令集:

  1. ARM指令集:32位指令,每条指令占用32位。优点是执行效率高,但占用空间较大。
  2. Thumb指令集:16位指令,每条指令占用16位。优点是节省空间,但执行效率稍低。

在实际应用中:

  • 如果需要节省空间,优先使用Thumb指令集。
  • 如果需要高效执行,优先使用ARM指令集。

一个CPU既可以运行Thumb指令,也可以运行ARM指令。如何区分当前指令是Thumb还是ARM指令呢?程序状态寄存器(CPSR)中有一位名为"T",当T=1时,表示当前运行的是Thumb指令。

指令集切换

指令集的切换通过设置PC寄存器的最低位(BIT0)来实现:

  • 调用函数A(Thumb指令)时,将PC寄存器的最低位设置为1,即PC = 函数A地址 + (1<<0)
  • 调用函数B(ARM指令)时,将PC寄存器的最低位设置为0,即PC = 函数B地址

这种切换方式是通过跳转指令(如BBL)实现的,目标地址的最低位决定了目标函数的指令集模式。在实际开发中,通常使用链接器或编译器工具来管理指令集切换,而不是手动设置PC寄存器的最低位。

Thumb2指令集

为了简化指令集切换,ARM公司引入了Thumb2指令集。Thumb2指令集支持16位和32位指令混合使用,允许在同一个指令集中无缝切换指令长度,而无需显式切换指令集。编译器通常会自动选择最优的指令长度,以平衡代码大小和执行效率。

统一汇编语言(UAL)

为了简化开发,ARM公司推出了统一汇编语言(UAL)。开发者无需区分ARM、Thumb或Thumb2指令集,只需在程序中使用CODE32CODE16THUMB指令来指定代码段的指令集类型。

常用汇编指令

在日常工作中,常用的ARM汇编指令包括:

  • 数据传输:MOVLDRSTRLDMSTM
  • 逻辑运算:ANDOR
  • 算术运算:ADDSUB
  • 跳转:BBL
  • 数据定义:DCD
  • 地址操作:ADRLDR
  • 比较:CMP

3. 汇编格式

以"数据处理"指令为例,UAL汇编格式为:

Operation{cond}{S} Rd, Rn, Operand2
  • Operation :表示各类汇编指令,如ADDMOV
  • cond :条件码,指定指令执行的条件。例如,EQ(相等)、NE(不相等)、CS(无符号大于等于)、CC(无符号小于)等。条件码可以应用于大多数指令,但某些指令(如LDMSTM)不支持条件码。
  • S :是否修改程序状态寄存器(CPSR)。如果指令带有S后缀,执行结果会影响CPSR中的条件标志。
  • Rd:目标寄存器,用于存储运算结果。
  • Rn:第一个源操作数。
  • Operand2:第二个操作数。

立即数与伪指令

在16位指令中,由于指令长度限制,立即数(直接给出的数值)的范围有限。例如,MOV R0, #VAL指令中,VAL不可能占用全部16位,毕竟MOV ,R0也需要一些位来表示其含义。为此,ARM提供了伪指令,让编译器自动处理复杂的立即数转换。

例如:

assembly 复制代码
MOV R0, #0x12345678  ; 伪指令,编译器会自动将其拆分为多条指令实现

在ARM指令集中,32位指令可以支持更大的立即数范围,但仍有限制。伪指令由编译器自动转换为多条指令,以实现复杂的立即数加载。


4. 操作内存的汇编指令

LDR:从内存加载数据到CPU寄存器

LDR指令用于从内存中加载数据到CPU寄存器。如果不指定数据类型,默认为32位数据。LDR支持多种寻址方式,包括偏移寻址、基址寻址和后索引寻址等,但是实际我们工作中用的的都是相对比较简单的。

例如:

assembly 复制代码
LDR R0, [R1]        ; 将R1指向的内存地址中的数据加载到R0
LDR R0, [R1, #4]    ; 将R1+4地址中的数据加载到R0
LDRB R0, [R1]       ; 加载8位数据到R0
LDRH R0, [R1]       ; 加载16位数据到R0

STR:将数据从寄存器存储到内存

STR指令用于将数据从寄存器存储到内存。STR也支持多种寻址方式。

例如:

assembly 复制代码
MOV R0, #0x20000
MOV R1, #0x10
MOV R2, #0x12
STR R2, [R0]        ; 将R2的值存储到R0指向的地址
STR R2, [R0, #4]    ; 将R2的值存储到R0+4地址
STR R2, [R0, #8]!   ; 将R2的值存储到R0+8地址,并将R0更新为R0+8
STR R2, [R0, R1]    ; 将R2的值存储到R0+R1地址
STR R2, [R0, R1, LSL #4]  ; 将R2的值存储到R0+(R1<<4)地址
STR R2, [R0], #0x20 ; 将R2的值存储到R0地址,并将R0更新为R0+0x20
STRB R2, [R0]       ; 存储8位数据到R0指向的地址

LDM:从存储器中加载多个数据到寄存器组

LDM指令用于批量加载多个寄存器,格式为:

LDM{addr_mode}{cond} Rn{!}, reglist{^}
  • addr_mode
    • IA:Increment After,每次传输后增加基地址(默认,可省略)。
    • IB:Increment Before,每次传输前增加基地址(仅ARM指令可用)。
    • DA:Decrement After,每次传输后减少基地址(仅ARM指令可用)。
    • DB:Decrement Before,每次传输前减少基地址。
  • ! :表示修改后的基地址会写回Rn寄存器。如果没有!,则Rn保持原值。
  • ^ :表示会影响程序状态寄存器(CPSR),通常在异常处理中使用。
    要注意:低位置寄存机对应低地址,比如R0寄存器属于最低的位置寄存器,R15是最高的寄存器。
    有个问题,为什么需要这么多的addr_mode?
    比如我们往栈中写数据的时候,有两种情况
    1.SP寄存器所指的地址有数据(满栈),这个时候我们需要先调整地址再写数据。
    2.SP寄存器所指的地址没有数据(空栈),这种情况是先写数据再调整地址。
    很明显我们需要不同的行为的需求,自然也就要有了多种addr_mode.

满增、满减, 空增,空减

根据栈指针指向,可分为满(Full)/空(Empty):

满SP指向最后一个入栈的数据,需要先修改SP再入栈

空SP指向下一个空位置,先入栈再修改SP

根据压栈时SP的增长方向,可分为增/减:

增(Ascending):SP变大

减(Descending):SP变小

组合后,就有4种方式: 满增、满减, 空增,空减。

常用的"满减": 入栈时用STMDB,也可以用STMFD,作用一样;注意是先减地址后存内容

出栈时用LDMIA,也可以用LDMFD,作用一样。先读内容后加地址

下面是几个例子,加黑的内容是汇编指令,方框内为内存结果,

5. 数据处理汇编指令

数据处理汇编指令有很多,我们并不需要全部掌握,只需要注意关键的几条即可。

加法指令ADD:

  ADD  R1, R2, R3         ; R1 = R2 + R3
  ADD  R1, R2, #0x12   ; R1 = R2 + 0x12

减法指令SUB:

  SUB  R1, R2, R3         ; R1 = R2 - R3
  SUB  R1, R2, #0x12   ; R1 = R2 - 0x12

位操作:

 ;  VisUAL里不支持(1<<4)这样的写法,写成:0x10
  AND R1, R2, #(1<<4)   ;  位与,R1 = R2 & (1<<4)
  AND  R1, R2, R3           ; 位与,R1 = R2 & R3
  BIC  R1, R2, #(1<<4)    ; 清除某位,R1 = R2 & ~(1<<4)
  BIC  R1, R2, R3             ; 清除某位,R1 = R2 & ~R3
  ORR   R1, R2, R3

比较:

关于比较要说明一下,CMP的对比结果会储存在状态寄存器里面,下面的语句会根据状态寄存器里面的值决定是否执行,比如MOVEQ,相等时才执行

CMP R0, R1 ; 比较R0-R1的结果

CMP R0, #0x12 ; 比较R0-0x12的结果

TST R0, R1 ; 测试 R0 & R1的结果

TST R0, #(1<<4) ; 测试 R0 & (1<<4)的结果

6.跳转指令

C程序中,函数A调用函数B的实质是什么?

void A()

{

int a = 10;

B(a);

printf("ok");

}
实质是:跳转去执行函数B的代码,函数B执行完后,还要回到函数A继续执行后面的代码。

对应的汇编指令就是跳转指令,要主要,凡事有正反,跳走了也要知道怎么跳回来,所以要保存返回地址

相关推荐
大橙子房27 分钟前
AI学习第六天-python的基础使用-趣味图形
前端·python·学习
码代码的小仙女1 小时前
学习笔记-07生产者-消费者模型4种实现方式
java·学习
Hetertopia3 小时前
STM32寄存器控制引脚高低电平
stm32·单片机·嵌入式硬件
因心,三人水3 小时前
【STM32F103ZET6——库函数】2.按键控制蜂鸣器
stm32·单片机·嵌入式硬件
乱次序_Chaos3 小时前
【监督学习】线性回归算法步骤及matlab实现
学习·算法·matlab·线性回归
小麦嵌入式3 小时前
Linux驱动开发实战(一):LED控制驱动详解
linux·c语言·驱动开发·stm32·单片机·嵌入式硬件·ubuntu
前端熊猫5 小时前
排序算法学习笔记
笔记·学习·排序算法
工匠Sola5 小时前
STM32G473VET6 在 Keil MDK 下手动移植 FreeRTOS 指南
stm32·单片机·嵌入式硬件
auspark5 小时前
macos下cocoapods的学习
学习·macos·cocoapods
道一235 小时前
STM32 微控制器库RCC_ClkInitTypeDef结构参数介绍
stm32·单片机·嵌入式硬件