# ARM 架构
随着对 STM 的逐步了解,就会发现一些特别的地方,例如,为什么 ARM 可以像访问内存一样访问外设?
# 为什么 ARM 可以像访问内存一样访问外设?
不要觉得这是理所当然的
在 x86 上驱动程序需要有对应的驱动文件,驱动文件需要通过库函数或是内联汇编才能实现
虽然 ST 也需要驱动文件,但是底层的实现就很有意思,但 ST 系列的单片机可以使用 C 语言像访问内存一样来访问外设
从汇编的层面来看 ARM 架构下的处理器可以通过 ldr
来操作任意的内存和外设,而 x86
不同
他是通过 IN
指令和 MOV
指令分别实现对外设和内存的操作
其实本质是架构不同,形成的指令集不同,对于 ARM 架构来说,地址和内存统一编址,而 x86 则是将外设分别进行编址
CPU 依靠地址总线来访问对应的地址,32 位的单片机有 32 根地址线,有 4G 的寻址空间,ROM、RAM 和外设分布在 0~4G 的寻址空间之内
ARM 架构的内存分布大致如下图
只要是在 0~4G 之外的地址,CPU 想要访问就会和 x86 类似的方式来实现访问
例如,要访问 EEPROM, 那么对于处理器啊来说,会检查访问 eeprom 的驱动,然后再按 eeprom 的内部地址
# RISC & CISC
既然说到了架构,那就不得不说一下 两种指令集 CISC 和 RISC
由于个人能力有限,所以可能会出现错误和纰漏,就大致介绍一下两大指令集
- CISC
Complex Instruction Set Computing, 其所用指令较为复杂,而复杂的指令采用 "微程序" 实现
"微程序" 不代表会高效,该执行的步骤一部都不少,
例如: call 和 ret 指令用于实现函数调用和返回,其事实上也是在调用的之前将 pc 或标签地址存入 LR 寄存器,然后在执行到 RET 语句时读取出来
尽管逻辑一致 - RISC
Reduced Instruction Set Computing, 其所用指令较为简单,特点如下: - 对内存只有读、写指令
- 对于数据的运算是在 CPU 内部实现
- 使用 RISC 指令的 CPU 复杂度小一点,,易于设计
# 区别
- CISC 的指令能力强,但多数指令使用率低却增加了 CPU 的复杂度,指令是可变长格式
- RISC 的指令大部分为单周期指令,指令长度固定,操作寄存器,对于内存只有 Load/Store 操作
- CISC 支持多种寻址方式;RISC 支持的寻址方式
- CISC 通过微程序控制技术实现;
- RISC 增加了通用寄存器,硬布线逻辑控制为主,采用流水线
- CISC 的研制周期长
- RISC 优化编译,有效支持高级语言
# ARM 的 CPU 内部寄存器
解决了一个问题,又出现了新的问题~~(我那该死的探索欲啊)~~
ARM 架构的 CPU 的内部寄存器有哪些,具体代表什么作用
ARM 内部有 17 个寄存器,R0~R15 和一个 PSR 寄存器,大致如下图
其中 R0~R12 是通用寄存器
R13 是 SP 寄存器,SP 寄存器又分为 sp_process
和 sp_main
, 分别用于 main
的主栈和子函数的 sp_process
过程栈,启动文件中设置的 sp
就是这个文件
这里的两个寄存器在 keil 是可见的
R14 是 LR 寄存器,用来保存返回地址,函数调用,例如,在使用 BL 之前需要将地址存入到 LR 中
R15 是 PC 寄存器,程序寄存器,表示当前执行指令的下一条指令。例如,当前执行到 0x12 地址上的 mov R0, #8
, 那么 PC 的值则为 0x1A
PSR 寄存器的情况又有些不同,要分为 cortex-M3/M4 内核和 A7 核
# M 核
PSR 实际上是 xPSR
xPSR 等价于三个寄存器
- IPSR: 中断 PSR
- EPSR: 执行 PSR
- APSR: 应用 PSR
这三个寄存器可以一起访问也可以分开访问
MRS R0, APSR ; 读取APSR
MSR APSR, R0 ; 写入APSR
MRS R0, PSR ;写入组合程序状态
MSR PSR, R0 ;写组合程序状态
# xPSR 各个寄存器控制的位
其在 keil 中视图如下
APSR 各个位的含义
# A 核
PSR 实际上是 CPSR,Current Program Status Register
A7 充分的利用了每一位,每一位的 bit 都有自己的含义
A7 内部还有更多寄存器类型,例如 FIQ、IRQ、ABT 之类的, 更多的可以参考官方文档 B1-1145
# 一些想法
CPU 的内部的寄存器只有这些,而像 IIC, GPIO 等一系列的外设属于外设的寄存器,仅仅是因为他们在 CPU 的寻址范围内,所以可以实现像内存一样的控制
其实只有汇编层面上的寄存器才和 CPU 相关,C 语言层面的寄存器更多的是一个内存地址,转化到汇编就是将地址存入十二个寄存器之后进行操作而已
# ARM 的汇编到底是啥
说起这个问题,汇编的名字是 ASM
, 但是很有意思,会发现 ATT
和 Intel
的汇编都是 ASM, 但是差距还是很大的
前面也说了,RISC 和 CISC 的指令集不一样,而且 ARM 官方给出了推出了几次指令集,ARM、Thumb、Thumb2, 以及 UAL.
最开始的时候 ARM 公司只推出了 Thumb 和 ARM 两种汇编语言,两种指令都可以在 MCU 中执行,例如 STM32F103 系列
Thumb 是 16 位的指令集合,节省空间;而 ARM 是 32 位的指令集合,高效但浪费空间
# 如何区别两种指令集
ARM 公司给出了两个两个关键字 CODE16
和 CODE32
当用 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
涉及内存的操作,也需要经过寄存器,通过 STR
和 LDR
来实现这个功能
例如,读取 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)所示地址上的值
# 多寄存器操作
当然除了读取单个寄存器还可以实现读个寄存器的读取
基本语法如下
对于多寄存器的操作来说,会有 LDM 和 STM 指令的 RegList 列表寄存器存储如何对应的问题
就记住一句话:低标号 Reg 对应低地址减法也如此
下面是一些 STMA
操作,LDM 也类似
# 实现栈操作
到这里,就会发现,这个操作非常的像栈,这里来模拟一下进出栈的操作
这里使用满减栈:
- 压栈,先减后存,STMFD
- 出栈,先出后减,LDMFD
前面说过栈指针是 SP, 咱们将 R1~R3 的数值存入压入栈中,然后,更改 R1~R3 的值之后存,再出栈,如果数据还原则表示入栈成功
# 数据处理指令
相较于 读取指令,数据操作的指令就更加多了,加减乘除,移位,清零等等
这里只介绍加减和为位操作以及简单的比较指令
# 加法指令: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 位,根据比较结果实现置位和
# 测试 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: 同上,但是转跳前保存返回地址
语法如图
由于模拟器不能模拟 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
大道五十,天衍四十九,人遁其一!