# ARM 架构

随着对 STM 的逐步了解,就会发现一些特别的地方,例如,为什么 ARM 可以像访问内存一样访问外设?

# 为什么 ARM 可以像访问内存一样访问外设?

不要觉得这是理所当然的
在 x86 上驱动程序需要有对应的驱动文件,驱动文件需要通过库函数或是内联汇编才能实现
虽然 ST 也需要驱动文件,但是底层的实现就很有意思,但 ST 系列的单片机可以使用 C 语言像访问内存一样来访问外设

从汇编的层面来看 ARM 架构下的处理器可以通过 ldr 来操作任意的内存和外设,而 x86 不同
他是通过 IN 指令和 MOV 指令分别实现对外设和内存的操作

其实本质是架构不同,形成的指令集不同,对于 ARM 架构来说,地址和内存统一编址,而 x86 则是将外设分别进行编址
CPU 依靠地址总线来访问对应的地址,32 位的单片机有 32 根地址线,有 4G 的寻址空间,ROM、RAM 和外设分布在 0~4G 的寻址空间之内
ARM 架构的内存分布大致如下图
image

只要是在 0~4G 之外的地址,CPU 想要访问就会和 x86 类似的方式来实现访问
例如,要访问 EEPROM, 那么对于处理器啊来说,会检查访问 eeprom 的驱动,然后再按 eeprom 的内部地址

# RISC & CISC

既然说到了架构,那就不得不说一下 两种指令集 CISC 和 RISC
由于个人能力有限,所以可能会出现错误和纰漏,就大致介绍一下两大指令集

  1. CISC
    Complex Instruction Set Computing, 其所用指令较为复杂,而复杂的指令采用 "微程序" 实现
    "微程序" 不代表会高效,该执行的步骤一部都不少,
    例如: call 和 ret 指令用于实现函数调用和返回,其事实上也是在调用的之前将 pc 或标签地址存入 LR 寄存器,然后在执行到 RET 语句时读取出来
    尽管逻辑一致
  2. RISC
    Reduced Instruction Set Computing, 其所用指令较为简单,特点如下:
  3. 对内存只有读、写指令
  4. 对于数据的运算是在 CPU 内部实现
  5. 使用 RISC 指令的 CPU 复杂度小一点,,易于设计

# 区别

  1. CISC 的指令能力强,但多数指令使用率低却增加了 CPU 的复杂度,指令是可变长格式
  2. RISC 的指令大部分为单周期指令,指令长度固定,操作寄存器,对于内存只有 Load/Store 操作
  3. CISC 支持多种寻址方式;RISC 支持的寻址方式
  4. CISC 通过微程序控制技术实现;
  5. RISC 增加了通用寄存器,硬布线逻辑控制为主,采用流水线
  6. CISC 的研制周期长
  7. RISC 优化编译,有效支持高级语言

# ARM 的 CPU 内部寄存器

解决了一个问题,又出现了新的问题~~(我那该死的探索欲啊)~~
ARM 架构的 CPU 的内部寄存器有哪些,具体代表什么作用

ARM 内部有 17 个寄存器,R0~R15 和一个 PSR 寄存器,大致如下图
image
其中 R0~R12 是通用寄存器
R13 是 SP 寄存器,SP 寄存器又分为 sp_processsp_main , 分别用于 main 的主栈和子函数的 sp_process 过程栈,启动文件中设置的 sp 就是这个文件
这里的两个寄存器在 keil 是可见的
image
R14 是 LR 寄存器,用来保存返回地址,函数调用,例如,在使用 BL 之前需要将地址存入到 LR 中
R15 是 PC 寄存器,程序寄存器,表示当前执行指令的下一条指令。例如,当前执行到 0x12 地址上的 mov R0, #8 , 那么 PC 的值则为 0x1A
PSR 寄存器的情况又有些不同,要分为 cortex-M3/M4 内核和 A7 核

# M 核

PSR 实际上是 xPSR
image
xPSR 等价于三个寄存器

  • IPSR: 中断 PSR
  • EPSR: 执行 PSR
  • APSR: 应用 PSR

这三个寄存器可以一起访问也可以分开访问

MRS R0, APSR  ; 读取APSR
MSR APSR, R0  ; 写入APSR
MRS R0, PSR   ;写入组合程序状态
MSR PSR, R0   ;写组合程序状态

# xPSR 各个寄存器控制的位

image
其在 keil 中视图如下
image

APSR 各个位的含义
image
image
image

# A 核

PSR 实际上是 CPSR,Current Program Status Register
A7 充分的利用了每一位,每一位的 bit 都有自己的含义
image
A7 内部还有更多寄存器类型,例如 FIQ、IRQ、ABT 之类的, 更多的可以参考官方文档 B1-1145
image

# 一些想法

CPU 的内部的寄存器只有这些,而像 IIC, GPIO 等一系列的外设属于外设的寄存器,仅仅是因为他们在 CPU 的寻址范围内,所以可以实现像内存一样的控制

其实只有汇编层面上的寄存器才和 CPU 相关,C 语言层面的寄存器更多的是一个内存地址,转化到汇编就是将地址存入十二个寄存器之后进行操作而已

# ARM 的汇编到底是啥

说起这个问题,汇编的名字是 ASM , 但是很有意思,会发现 ATTIntel 的汇编都是 ASM, 但是差距还是很大的
前面也说了,RISC 和 CISC 的指令集不一样,而且 ARM 官方给出了推出了几次指令集,ARM、Thumb、Thumb2, 以及 UAL.

最开始的时候 ARM 公司只推出了 Thumb 和 ARM 两种汇编语言,两种指令都可以在 MCU 中执行,例如 STM32F103 系列
Thumb 是 16 位的指令集合,节省空间;而 ARM 是 32 位的指令集合,高效但浪费空间

# 如何区别两种指令集

ARM 公司给出了两个两个关键字 CODE16CODE32
当用 CODE16, 表示下面的代码工作于 Thumb 指令集
用 CODE32, 表示下面的代码工作于 ARM 指令集

指令集会影响到 CPU 的取指译码一连串的操作,所以 CPU 也必须实时的知道
所以在 PSR 寄存器中设有一个 T 标志位,用于区分当前处于哪一个指令集
为 0 的时候是 ARM 指令集,为 1 时时 Thumb 指令集

# CPU 如何调用函数

试想一个问题,函数 A 使用 Thumb 指令写的,函数 B 是使用 ARM 指令集编写,如何调用 A/B

事实上,可以直接将 A/B 的地址写入 PC 寄存器,来实现这个功能

但是再细想一下,如何让 CPU 运行再 Thumb 模式下运行 A, 而再运行 B 的时候处于 ARM 模式
做个手脚:
调用函数 A 时,让 PC 寄存器的 BITO 等于 1,即: PC = 函数 A 地址 +(1<<0);
调用函数 B 时,让 PC 寄存器的 BITO 等于 0:,即:PC = 函数 B 地址

再进一步,如何再执行 A 时调用 ARM 指令的函数 B, 在执行完毕后回到 Thumb 指令函数 A

Bx B_Addr
Bx A_Addr +1(+1之后会使能Bit0位)

但是这样子还是存在问题。例如,同一个程序中,有些地方需要高效的 ARM 指令集,而有些则需要 Thumb 低空间
为了解决这麻烦的问题,ARM 公司推出了 Thumb2 指令集,支持 CODE16 和 CODE32 混合编程

ARM, Thumb, Thumb2, 每一种汇编都有自己的语言风格,尽管相似,但是会有不同的特性,这样子就增加了程序员的学习成本
因此 ARM 公司就推出了统一的汇编语言 UAL, UAL 支持 CODE32,CODE16,Thumb 三种风格的语言,这样 ARM 的汇编语言的发展就算是基本成型了

# 基本的 UAL 汇编语法

汇编语言本身大差不差,大致会分为,内存操作指令,输出处理指令 以及 转跳指令三大类

# 内存操作指令

# 单寄存器操作

内存操作指令大致分为两大类:涉及内存读取和不涉及内存读取

所谓的涉不涉及内存,其实就是只涉及立即数和寄存器的操作,可以是寄存器到寄存器,也可以是立即数到寄存器
不过有一点需要注意,立即数是有一定限制的.
立即数必须满足,可以通过对一个 8 位数字进行向右旋转的操作得到一个在 32 位字中可以用作立即数的值
例如, mov r0, #10 是可以的但是 mov r0, #10086 就不行

MOV		R0, #0x20000
MOV		R1, #0x10
MOV		R2, #0x12

涉及内存的操作,也需要经过寄存器,通过 STRLDR 来实现这个功能
例如,读取 0x20000 的数据到 R1 中,读取 0x40000 中的数据到 R3 中

mov r0, #0x20000  ; 0x20000 地址放入r0
str r0, [r2]        ; 将数据放入r1的地址
mov r0, #0x40000  ; 将地址写入r0
ldr r3, [r0]        ; 将地址中的数据读入r3

除了这些基本的操作,还有一些更复杂的操作

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
MOV		R2, #0x34
STR		R2, [R0]              ; R2的值存到R0所示地址
LDR		R3, [R0], +R1, LSL #1 ; R3的值等于R0+(R1<<1)所示地址上的值

# 多寄存器操作

当然除了读取单个寄存器还可以实现读个寄存器的读取
基本语法如下
image

对于多寄存器的操作来说,会有 LDM 和 STM 指令的 RegList 列表寄存器存储如何对应的问题
就记住一句话:低标号 Reg 对应低地址减法也如此
下面是一些 STMA 操作,LDM 也类似
image

# 实现栈操作

到这里,就会发现,这个操作非常的像栈,这里来模拟一下进出栈的操作

这里使用满减栈:

  1. 压栈,先减后存,STMFD
  2. 出栈,先出后减,LDMFD

前面说过栈指针是 SP, 咱们将 R1~R3 的数值存入压入栈中,然后,更改 R1~R3 的值之后存,再出栈,如果数据还原则表示入栈成功

image
image

# 数据处理指令

相较于 读取指令,数据操作的指令就更加多了,加减乘除,移位,清零等等
这里只介绍加减和为位操作以及简单的比较指令

# 加法指令:ADD

语法: add 目的寄存器, 加数1, 加数2

		mov		r0, #1
		mov		r1, #2
		add		r1, r1, r0

# 减法指令:SUB

语法: sub 目的寄存器, 被减数数1, 减数2

		mov		r0, #2
		mov		r1, #1
		sub		r1, r1, r0

# 位操作

# AND

语法: and 目的寄存器, 数值1, 数值2

		mov		r0, #2
		mov		r1, #1
		and		r1, r1, r0 ; r1变成0
# BIC

语法: bic 目的寄存器, 数值1, 数值2

		mov		r0, #1
		mov		r1, #3
		bic		r1, r1, r0 ; r1 变成2
# ORR

语法: orr 目的寄存器, 数值1, 数值2

		mov		r0, #1
		mov		r1, #2
		orr		r1, r1, r0 ;r1变成3

# 比较

# 比较指令 CMP

语法: cmp r1, r2
cmp 指令执行后会影响 psr 的 C 位,根据比较结果实现置位和
image

# 测试 TST

不修改数据结果来进行 and 操作,来测试数据

语法: tst 寄存器1, 寄存器

		mov		r0, #1
		mov		r1, #2
		tst		r1, r0

# 转跳指令

转跳指令其实不止这两个,但是这两个是函数调用的接口

B: Branch 转跳
BL:Branch with Link, 转跳前将返回地址存入 LR
BX: Branch and exechange, 根据转跳地址的 BIT0, 自动切换 ARM 指令集和 Thumb 指令集
BXL: 同上,但是转跳前保存返回地址

语法如图
image

由于模拟器不能模拟 BX 系列指令,这里只有 B 指令的例子

		mov		r0, #10
		BL		Delay
		
		mov		r2, #0x1
		
		
Delay
		SUBS	r0, r0, #1	;等价于下面两句话
		;SUB		r0, r0, #1
		;CMP		r0, #0
		BNE		Delay
		mov		PC, LR	
		; BL 的返回地址在LR中, 要通过mov来实现
		; 没有类似x86的 ret 指令

# 小结

由于个人知识储备有限,文章可能有些虎头蛇尾,后期等查阅完毕相关的文档后在更新和修正
文章如有不恰当之处,望诸位大佬斧正

# 参考文献

[1] https://www.100ask.net/p/t_pc/course_pc_detail/column/p_5f85677be4b06aff1a03cecb?content_app_id=
[2] STM32MP157 裸机开发完全手册
[3] DM00327659
[4] ARM Cortex-M3 与 Cortex-M4 权威指南
[5] DDI0406C_d_armv7ar_arm


大道五十,天衍四十九,人遁其一!

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

黑羊 支付宝

支付宝