【汇编语言】ARM体系结构(含汇编开发)

ARM体系结构

一. 嵌入式系统基础1. 三极管2. 门电路3. 触发器4. 处理器1> 处理器介绍2> 处理器架构3> CISC和RISC

二. Contex-A和Contex-M核三. ARM汇编开发(条件执行与分支)1. ARM指令集类型2. 立即数和伪指令3. 内存访问指令4. 数据处理指令5. 跳转指令6. C语言中栈的变化1> 程序在内存分布区域2> 全局变量m赋值3> 保存进入main之前的栈底, fp-sp之间是当前函数栈4> 函数main的栈已经准备好了5> i入栈6> j入栈7> 准备函数fun的调用, 形参反向入栈 先形参b入栈8> 形参a入栈9> 留空一个地址作为fun返回值, 待后面返回时填入10> fun返回地址入栈, 通常是main函数当前pc指针的下一个11> main函数的栈底地址入栈12> pc指针跳转fun代码13> c入栈14> 可以看到函数fun的数据 形参a,b 在上一层函数的栈中. 一部分在自己的栈上. 此步取值到加法器中进行加法运算,再赋值给c15> c赋给返回值,填入上面的留空位置16> 栈底恢复上一层17> lr赋值给pc, 实现了跳转18> 返回值赋值给全局变量m19> 前面函数调用的形参已经无用,回滚sp20> 函数返回,清理main的栈空间

四. 纯汇编点亮LED灯汇编文件结构分解说明硬件初始化与主循环运行流程

五. 从零写STM32标准库六. HAL库工程创建1. Keil创建2. CubeMX生成Keil3. CubeIDE创建
七. FreeRTOS和RT-Thread1. FreeRTOS2. RT-Thread1> Keil创建2> RT-thread Studio创建

本文从历史沿革、指令集、执行状态、寄存器体系等全方面对 ARM(Advanced RISC Machines)体系结构进行系统梳理。

ARM 发展的历史沿革路线图

时间 关键节点 架构/产品线
1985 Acorn 发布 ARM1,首颗 ARM 处理器 ARMv1
1990 ARM Ltd. 成立,ARM6 量产 ARMv3
1993 ARM610 驱动 Apple Newton ARMv3
1994 ARM7 发布,Thumb 指令集问世 ARMv4T
1998 ARM9/ARM10 面向高性能嵌入式 ARMv5
2003 Cortex 系列推出(A/M/R 三线) ARMv7-A/R/M
2011 ARMv8 引入 AArch64 与 TrustZone-A Cortex-A50 系列
2016+ big.LITTLE、DynamIQ、SVE、Neoverse ARMv8.2+、ARMv9

Cortex 系列定位

Cortex-A:应用级(App)处理器,MMU + 虚拟内存,支持复杂 OS(Android、Linux)Cortex-R:实时(Real-Time)处理器,面向汽车、存储等领域Cortex-M:微控制器(MCU),面向低功耗物联网与控制场景

一. 嵌入式系统基础

理解计算机如何通过”0”, ”1”这样的高低电平的变化,实现对各种数据的处理(计算)的,计算机对所有数据(数字,图片,音频,视频等)的处理,最终反映在硬件上就是逻辑组合电路和时序电路的“0”,“1”一系列变化,没有硬件的软件就是空中楼阁,没有软件的硬件就是一堆铁。

1. 三极管


硅原子外层4个电子,比较稳定,硅单质每4个电子形成共价键,很稳定,不导电

【汇编语言】ARM体系结构(含汇编开发)


在硅单质中掺杂磷元素,磷元素最外层有5个电子,多出一个电子,很容易脱离束缚,从而导电,叫做N型半导体,自由电子叫载流子

【汇编语言】ARM体系结构(含汇编开发)


硅单质中掺杂了硼元素,外层3个电子,共价键缺少一个电子,叫做空穴,吸引其他电子来填补,从而形成电流,叫做P型半导体

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)

当P型半导体和N型半导体结合在一起的时候,形成PN结,N区中的电子会发生移动,填入P区的空穴中(扩散运动),同时由于N少了电子,带正电,会形成从N->P的空间电场;因电场作用,N区的少子空穴会向P区移动,P区的少子电子会向N区移动(漂移运动);漂移与扩散同时存在,互相抵消,达到动态平衡后就会形成一个宽度一定的空间电荷区(PN结)
【汇编语言】ARM体系结构(含汇编开发)

把PN结接入电路,加上一个正向偏置电压,空间电荷区会逐渐变薄,当正偏电压超过0.7V时候,空间电荷区完全消失,PN结消失了,就可以导电了

【汇编语言】ARM体系结构(含汇编开发)

当加上反偏电压的时候,N->P电场变强,空间电荷区会变厚,形成一个由少子移动形成的漂移电流,由于PN结中少子很少,电流很弱,约等于0,PN结不导电了

【汇编语言】ARM体系结构(含汇编开发)

通过在3个极加上不同的电压,可以控制三极管在三个区工作,在制作工艺上,最下面的发射级掺杂浓度很高,有很多载流子,基区做的很薄,只能容纳很少的载流子,集电极做的比较大,可以容纳很多载流子的潜质

【汇编语言】ARM体系结构(含汇编开发)

当接入反偏电压的时候,两个PN节的厚度都会变大,N->P电场变强,N区中的大量载流子(电子)无法移动,只有P区少子移动形成的漂移电流,由于PN结中少子很少,电流很弱,约等于0,两个PN结不导电了

【汇编语言】ARM体系结构(含汇编开发)

当接入正向偏置电源,发射结正偏,集电极反偏,发射极的空间电荷区消失,发射极N的大量电子扩散进基极P,和P极中的空穴复合,形成一个从B->E的电流IB,然而基区比较薄,空穴很少,移动的过程中只有少部分的电子与空穴复合,大量的电子堆积在基区,由于集电极反偏,形成了很强的反向电场,漂移运动增强,基区堆积的电子漂移到C区,想成了一个C->E的电流IC,IB 越大,注入到基极的电子越多,漂移运动越厉害,IC 就越大

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)

2. 门电路

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)

有了简单的加法器,可以用电信号(高低电压–抽象为电平)实现人类熟悉的数字逻辑运算,在简单加法器的基础上经过拓展可以设计出更加复杂的加法器(乘法器),之后就可以进行用电路中的电信号实现各种数字运算了,而自然界的所有一切(数字,图片,音频,视频)都可以采样量化成具体的数字,那么都可以按照数字逻辑进行运算,这就是数字组合逻辑。

3. 触发器

有时候我们需要记录当前的输出状态,不管输入怎么发生变化,输出都不会发生变化,输出的结果被锁存,就像被”存储”下来了一样,除非断电,或者给定某个特定的信号(如写信号),这就是时序电路,有了时序电路,就可以实现对数据和状态的”暂存”,为后续的计算保存数据和各种状态。

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)


至此,有了组合逻辑,时序逻辑,计算机系统的邹型也就出现了。

4. 处理器

1> 处理器介绍

【汇编语言】ARM体系结构(含汇编开发)

2> 处理器架构

【汇编语言】ARM体系结构(含汇编开发)

3> CISC和RISC

CISC--复杂指令集计算集<Intel、AMD的X86为代表>
RISC--精减指令集计算集<ARM、Power PC、开源的RISC-V>

【汇编语言】ARM体系结构(含汇编开发)

上图是实现这样的乘法运算:a = a * b,它需要4个步骤:读出a的值、读出b的值、相乘、写结果到a

使用CISC提供的乘法指令,只需要一条指令即可完成这4步操作,当然,这一个指令需要多个CPU周期才可以完成。


而RISC不提供“一站式”的乘法指令,需调用四条单CPU周期指令完成两数相乘:内存a加载到寄存器,内存b加载到寄存器,两个寄存器中数相乘,寄存器结果存入内存a

按照此思路,早期的设计出的RISC指令集,指令数是比CISC少些,后来,很多RISC的指令集中指令数反超了CISC,因此,应该根据指令的复杂度而非数量来区分两种指令集;


当然,CISC也是要通过操作内存、寄存器、运算器来完成复杂指令的。它在实现时,是将复杂指令转换成了一个微程序,微程序在制造CPU时就已存储于微服务存储器。一个微程序包含若干条微指令(也称微码),执行复杂指令时,实际上是在执行一个微程序。这也带来两种指令集的一个差别,微程序的执行是不可被打断的,而RISC指令之间可以被打断,所以理论上RISC可更快响应中断


ARM访问寄存器:
int   a;  
unsigned int *p  = &a;   // p等于“a的地址”
*p = val;   // 写这个地址,就是写a
val = *p;   // 读这个地址,就是读a

unsigned int *p  = 0x40010800; // p等于某个寄存器的地址
*p = val;   // 写这个地址,也就是写这个寄存器
val = *p;   // 读寄存器

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

二. Contex-A和Contex-M核


无论是cortex-M3/M4,
还是cortex-A7,
CPU内部都有R0、R1、……、R15寄存器;
它们可以用来“暂存”数据。

对于R13、R14、R15,还另有用途:
R13:别名SP(Stack Pointer),栈指针
R14:别名LR(Link Register),用来保存返回地址
R15:别名PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)


bin文件和hex文件的区别:
1> Hex
Hex文件包含地址信息。ISP下载过程中我们并没有选择要把程序下载到单片机的哪块内存中,即不需要设置地址。因为HEX文件内部的信息已经包括了地址

2> BIN
BIN文件格式只包括了数据本身,没有包含地址。烧写BIN文件的时候,用户是一定需要指定地址信息的。
所以在下载bin文件时需要选择内存的起始地址和终止地址,即要把bin文件下载到指定的内存空间。通常需要指定程序内存地址的芯片为ARM芯片和DSP芯片。
	
3> 文件大小
对于bin文件,通过右键属性查看到的文件的大小就是数据的实际大小。而对HEX文件而言,你看到的文件大小并不是实际的数据的大小。一是因为HEX文件是用ASCII来表示数据,二是因为HEX文件本身还包括别的附加信息

启动模式:
Main Flash memory
是STM32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。

System memory(ISP)
从系统存储器启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader, 也就是我们常说的ISP程序,这是一块ROM,出厂后无法修改。
一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。但是这个下载方式需要以下步骤:
Step1:将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader
Step2:最后在BootLoader的帮助下,通过串口下载程序到Flash中
Step3:程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动可以看到,利用串口下载程序还是比较的麻烦,需要跳帽跳来跳去的,非常的不注重用户体验。

Embedded Memory
内置SRAM,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的地方,然后就需要重新擦除整个Flash,比较的费时,
可以考虑从这个模式启动代码(也就是STM32的内存中),用于快速的程序调试,等程序调试完成后,在将程序下载到SRAM中

三. ARM汇编开发(条件执行与分支)

1. ARM指令集类型


一开始,ARM公司发布两类指令集:
① ARM指令集,这是32位的,每条指令占据32位,高效,但是太占空间
② Thumb指令集,这是16位的,每条指令占据16位,节省空间
要节省空间时用Thumb指令,要效率时用ARM指令。

一个CPU既可以运行Thumb指令,也能运行ARM指令。
怎么区分当前指令是Thumb还是ARM指令呢?
程序状态寄存器中有一位,名为“T”,它等于1时表示当前运行的是Thumb指令。

假设函数A是使用Thumb指令写的,函数B是使用ARM指令写的,怎么调用A/B?
我们可以往PC寄存器里写入函数A或B的地址,就可以调用A或B,
但是怎么让CPU在执行A函数是进入Thumb状态,在执行B函数时进入ARM状态?

做个手脚:
调用函数A时,让PC寄存器的BIT0等于1,即:PC=函数A地址+(1<<0);
调用函数B时,让PC寄存器的BIT0等于0:,即:PC=函数B地址

麻烦吧?麻烦!
引入Thumb2指令集,
它支持16位指令、32位指令混合编程

【汇编语言】ARM体系结构(含汇编开发)

2. 立即数和伪指令


MOV   R0, #VAL --- 意图是把VAL这个值存入R0寄存器。

VAL可以是任意值吗?不可以,必须是立即数。
假设VAL可以是任意数,”MOV  R0, #VAL”本身是16位或32位,哪来的空间保存任意数值的VAL?所以,VAL必须符合某些规定

【汇编语言】ARM体系结构(含汇编开发)


去判断一个VAL是否立即数,很麻烦
并且我就是想把任意数值赋给R0,怎么办?
可以使用伪指令:LDR   R0,  =VAL

“伪指令”,就是假的、不存在的指令。
注意LDR作为“伪指令”时,指令中有一个“=”,否则它就是真实的LDR(load regisgter)指令了。

编译器会把“伪指令”替换成真实的指令,比如:
LDR  R0,  =0x12    
0x12是立即数,那么替换为:MOV  R0,  #0x12

LDR  R0, =0x12345678
0x12345678不是立即数,那么替换为:
LDR  R0, [PC, #offset]    // 2. 使用Load Register读内存指令读出值,offset是链接程序时确定的
Label  DCD  0x12345678    // 1. 编译器在程序某个地方保存有这个值

ADR的意思是:address,用来读某个标号的地址
示例:
ADR  R0,  Loop

Loop
    ADD  R0, R0, #1
它是“伪指令”,会被转换成某条真实的指令,比如:
ADD R0, PC, #val   ; val在链接时确定
Loop
    ADD  R0, R0, #1

3. 内存访问指令


读内存指令LDR/LDM
写内存指令STR/STM

MOV		R0, #0x20000
MOV		R1, #0x10
MOV		R2, #0x12
MOV     R3, R2
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)所示地址上的值

IA - Increment After,  每次传输后才增加Rn的值(默认,可省)
IB - Increment Before, 每次传输前就增加Rn的值(ARM指令才能用)
DA – Decrement After,  每次传输后才减小Rn的值(ARM指令才能用)
DB – Decrement Before, 每次传输前就减小Rn的值

! : 表示修改后的Rn值会写入Rn寄存器, 
    如果没有"!", 指令执行完后Rn恢复/保持原值
^ : 会影响CPSR

MOV		R1, #1
MOV		R2, #2
MOV		R3, #3
MOV		R0, #0x20000
STMIA	R0,  {R1-R3}  ; R1,R2,R3分别存入R0,R0+4,R0+8地址处
ADD	R0, R0, #0x10
STMIA	R0!, {R1-R3}  ; R1,R2,R3分别存入R0,R0+4,R0+8地址处, R0=R0+3*4

//入栈和出栈操作
MOV		R1, #1
MOV		R2, #2
MOV		R3, #3
MOV		SP, #0x20000
STMDB	SP!,  {R1-R3}   ;STMFD  
MOV		R1, #0
MOV		R2, #0
MOV		R3, #0
LDMIA	SP!, {R1-R3}    ;LDMFD

【汇编语言】ARM体系结构(含汇编开发)

4. 数据处理指令


1> 加法指令ADD:
      ADD  R1, R2, R3         ; R1 = R2 + R3
      ADD  R1, R2, #0x12      ; R1 = R2 + 0x12

2> 减法指令SUB:
      SUB  R1, R2, R3         ; R1 = R2 - R3
      SUB  R1, R2, #0x12      ; R1 = R2 - 0x12

3> 位操作:
      ;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

4> 比较:
    CMP R0, R1                ; 比较R0-R1的结果
    CMP R0, #0x12             ; 比较R0-0x12的结果
    TST  R0, R1               ; 测试 R0 & R1的结果
    TST  R0, #(1<<4)          ; 测试 R0 & (1<<4)的结果

5. 跳转指令


1> B指令示例:
		B		Delay
Delay
		MOV		R0, #1000
Loop
		SUBS    R0, R0, #1
		BNE		Loop
		MOV		R1, #1

2> BL指令示例
		BL		Delay    ; 跳转前把返回地址保持在LR寄存器里
		MOV		R1, #1
Delay
		MOV		R0, #1000
Loop
		SUBS    R0, R0, #1
		BNE		Loop
		MOV		PC, LR    ; 把LR赋给PC,返回
		
3> 给PC直接赋值
		ADR		LR, Ret    ; 伪指令,读取Ret标号的地址赋给LR,这是返回地址
		ADR		PC, Delay  ; 伪指令,读取Delay标号的地址赋给PC,直接跳转
Ret
		MOV		R1, #1
Delay
		MOV		R0, #1000
Loop
		SUBS		R0, R0, #1
		BNE		Loop
		MOV		PC, LR    ; 把LR赋给PC,返回

6. C语言中栈的变化

1> 程序在内存分布区域

【汇编语言】ARM体系结构(含汇编开发)

2> 全局变量m赋值

【汇编语言】ARM体系结构(含汇编开发)

3> 保存进入main之前的栈底, fp-sp之间是当前函数栈

【汇编语言】ARM体系结构(含汇编开发)

4> 函数main的栈已经准备好了

【汇编语言】ARM体系结构(含汇编开发)

5> i入栈

【汇编语言】ARM体系结构(含汇编开发)

6> j入栈

【汇编语言】ARM体系结构(含汇编开发)

7> 准备函数fun的调用, 形参反向入栈 先形参b入栈

【汇编语言】ARM体系结构(含汇编开发)

8> 形参a入栈

【汇编语言】ARM体系结构(含汇编开发)

9> 留空一个地址作为fun返回值, 待后面返回时填入

【汇编语言】ARM体系结构(含汇编开发)

10> fun返回地址入栈, 通常是main函数当前pc指针的下一个

【汇编语言】ARM体系结构(含汇编开发)

11> main函数的栈底地址入栈

【汇编语言】ARM体系结构(含汇编开发)

12> pc指针跳转fun代码

【汇编语言】ARM体系结构(含汇编开发)

13> c入栈

【汇编语言】ARM体系结构(含汇编开发)

14> 可以看到函数fun的数据 形参a,b 在上一层函数的栈中. 一部分在自己的栈上. 此步取值到加法器中进行加法运算,再赋值给c

【汇编语言】ARM体系结构(含汇编开发)

15> c赋给返回值,填入上面的留空位置

【汇编语言】ARM体系结构(含汇编开发)

16> 栈底恢复上一层

【汇编语言】ARM体系结构(含汇编开发)

17> lr赋值给pc, 实现了跳转

【汇编语言】ARM体系结构(含汇编开发)

18> 返回值赋值给全局变量m

【汇编语言】ARM体系结构(含汇编开发)

19> 前面函数调用的形参已经无用,回滚sp

【汇编语言】ARM体系结构(含汇编开发)

20> 函数返回,清理main的栈空间

【汇编语言】ARM体系结构(含汇编开发)

四. 纯汇编点亮LED灯


单片机开发的两种工具链:
1.armcc  --- Keil IAR
2.GNU    --- 通用

启动代码分析:
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp

开辟栈的大小为 0X00000400(1KB),名字为 STACK, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐
EQU:宏定义的伪指令,相当于等于,类似于C 中的 define。
AREA:告诉汇编器汇编一个新的代码段或者数据段。STACK 表示段名,这个可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写, ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。
SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。
标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的


Heap_Size       EQU     0x00000200
AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit
开辟堆的大小为 0X00000200(512 字节),名字为 HEAP, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。__heap_base 表示对的起始地址, __heap_limit 表示堆的结束地址。堆是由低向高生长的,跟栈的生长方向相反。
堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆上面。这个在 STM32里面用的比较少


PRESERVE8
THUMB
PRESERVE8:指定当前文件的堆栈按照8字节对齐。
THUMB:表示后面指令兼容 THUMB 指令

AREA    RESET, DATA, READONLY
EXPORT  __Vectors
EXPORT  __Vectors_End
EXPORT  __Vectors_Size
定义一个数据段,名字为 RESET,只读。并声明 __Vectors、 __Vectors_End 和__Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。
EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令

__Vectors DCD __initial_sp ;栈顶地址
DCD Reset_Handler ;复位程序地址
DCD NMI_Handler
DCD HardFault_Handler
DCD MemManage_Handler
DCD BusFault_Handler
DCD UsageFault_Handler
DCD 0 ; 0 表示保留
DCD 0
DCD 0
DCD 0
DCD SVC_Handler
DCD DebugMon_Handler
DCD 0
DCD PendSV_Handler
DCD SysTick_Handler
;外部中断开始
DCD WWDG_IRQHandler
DCD PVD_IRQHandler
DCD TAMPER_IRQHandler
;限于篇幅,中间代码省略
DCD DMA2_Channel2_IRQHandler
DCD DMA2_Channel3_IRQHandler
DCD DMA2_Channel4_5_IRQHandler
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors

__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,两个相减即可算出向量表大小。
向量表从 FLASH 的 0 地址开始放置,以 4 个字节为一个单位,地址 0 存放的是栈顶地址,0X04 存放的是复位程序的地址,以此类推。
从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中, DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。


; Reset handler
Reset_Handler   PROC
EXPORT  Reset_Handler             [WEAK]
IMPORT  __main
IMPORT  SystemInit
LDR     R0, =SystemInit
BLX     R0               
LDR     R0, =__main
BX      R0
ENDP
复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界。
WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表示 SystemInit 和__main 这两个函数均来自外部的文件。
SystemInit()是一个标准的库函数,在 system_stm32f10x.c 这个库文件中定义。主要作用是配置系统时钟,这里调用这个函数之后,单片机的系统时钟配被配置为 72M。
__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,并在函数的最后调用main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因


ALIGN
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF      :DEF:__MICROLIB

EXPORT  __initial_sp
EXPORT  __heap_base
EXPORT  __heap_limit

ELSE

IMPORT  __use_two_region_memory
EXPORT  __user_initial_stackheap

__user_initial_stackheap

LDR     R0, =  Heap_Mem
LDR     R1, =(Stack_Mem + Stack_Size)
LDR     R2, = (Heap_Mem +  Heap_Size)
LDR     R3, = Stack_Mem
BX      LR
ALIGN
ENDIF
END
首先判断是否定义了__MICROLIB ,如果定义了这个宏则赋予标号__initial_sp(栈顶地址)、 __heap_base(堆起始地址)、 
__heap_limit(堆结束地址)全局属性,可供外部文件调用

如果没有定义__MICROLIB,则才用双段存储器模式,且声明标号__user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。
前文的汇编代码,需要注意:
IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似
END:文件结束

【汇编语言】ARM体系结构(含汇编开发)


                PRESERVE8
                THUMB

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors

__Vectors       DCD     0               ; Top of Stack
                DCD     Reset_Handler   ; Reset Handler


                AREA    |.text|, CODE, READONLY
                
; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                
                ;使能GPIOC
				;0x40021018 RCC 
				LDR R0, =0x40021018
				LDR R1, [R0]
				ORR R1, R1, #(1 << 4)
				STR R1, [R0]
				
				;设置模式
				;0x40011004 GPIOC_CRH
                LDR R0, =0x40011004
				LDR R1, [R0]
				BIC R1, R1, #0xf
				ORR R1, R1, #2
				STR R1, [R0]
				
MainLoop	
				;设置GPIOC8输出高电平
				LDR R0, =0x4001100C
				LDR R1,[R0]
				ORR R1, R1, #0x100
				STR R1, [R0]
				BL Delay
				;设置GPIOC8输出低电平
				LDR R0, =0x4001100C
				LDR R1,[R0]
				BIC R1, R1, #0x100
				STR R1, [R0]
				BL Delay
				B MainLoop
				
Delay
			    LDR R0, =500000
DelayLoop
			    SUBS R0, #1
				BNE DelayLoop
				BX LR
				NOP
				ENDP
                END
汇编文件结构分解说明

这段代码是为 STM32F103RCT6(Cortex‑M3 内核) 编写的裸机启动程序,核心任务是点亮并反复闪烁一颗连接在 GPIOC.8(推测)上的 LED。程序从复位向量开始执行,在
Reset_Handler
中完成外设时钟开启、引脚配置、GPIO 输出控制以及软件延时。

逐段解析:

汇编伪指令


				PRESERVE8
                THUMB


PRESERVE8
:提示汇编器/链接器保持 8 字节对齐(
Cortex-M
的栈要求 8 字节对齐)。
THUMB
:指定后续代码生成 Thumb 指令集
Cortex-M
仅运行
Thumb/Thumb-2
指令)。
中断向量表区域


				AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     0                          ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors


AREA RESET, DATA, READONLY
:定义名为
RESET
的只读数据段,通常放在
Flash
起始处(地址

0x0800_0000
)。


__Vectors
:中断向量表起始标签。

第 0 项 (
DCD 0
) 应填入栈顶地址(MSP 初值),此处写 0 仅作示范;实际工程中要换成
SRAM
末地址,如
0x2000 FFFF

第 1 项是复位处理函数地址
Reset_Handler


__Vectors_Size
记录向量表大小,便于链接器引用。

代码段与
Reset_Handler


				AREA    |.text|, CODE, READONLY
                
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]


AREA |.text|
:定义指令存放段。
PROC
/
ENDP
用于过程(函数)标记。
[WEAK]
:允许链接时被其他同名强符号覆盖。


硬件初始化与主循环

4.1. 开启 GPIOC 外设时钟


				LDR R0, =0x40021018
				LDR R1, [R0]
				ORR R1, R1, #(1 << 4)
				STR R1, [R0]

地址
0x40021018
RCC_APB2ENR(重置与时钟控制器 APB2 外设时钟使能寄存器)。
ORR R1, R1, #(1 << 4)
设置位 4 (
IOPCEN
),为 GPIOC 模块 供时。步骤:


LDR R0, =...
把寄存器地址装入 R0。

LDR R1, [R0]
读出旧值。

ORR
置位。

STR
写回,完成时钟开启。

4.2. 配置 GPIOC.8 为推挽输出


				LDR R0, =0x40011004
				LDR R1, [R0]
				BIC R1, R1, #(0x0F)
				ORR R1, R1, #(1 << 1)
				STR R1, [R0]


0x40011000
是 GPIOC 基地址;偏移
0x04
对应 GPIOC_CRx 中的
CRH
(管脚 8~15)。需要配置 PC8 的 4 个控制位:

MODE8[1:0]
:输出速度。
10
表示 2 MHz。
CNF8[1:0]
:输出模式。
00
为通用推挽输出。 操作过程:

BIC R1, R1, #(0x0F)
清空 PC8 的 4 位配置(低 4 位)。

ORR R1, R1, #(1 << 1)
仅设置
MODE8
的位 1,使其成为
0010
,即
MODE=10

CNF=00
。 写回后,PC8 成为 2MHz 推挽输出。

提示:若要更直观,可以先
BIC

ORR
设置具体位模式,如
0b0010
。若需 50MHz 输出,可写
0b0011
等。

4.3. 主循环:GPIO 输出电平翻转


MainLoop
				;设置为低电平
				LDR R0, =0x4001100C
				LDR R1, [R0]
				BIC R1, R1, #(1 << 8)
				STR R1, [R0]
				BL Delay
				;设置为高电平
   	LDR R0, =0x4001100C
				LDR R1, [R0]
				ORR R1, R1, #(1 << 8)
				STR R1, [R0]
				BL Delay
				B MainLoop


0x4001100C
GPIOC_ODR(输出数据寄存器)。
BIC
清除 bit8 → 输出低电平,点灯或灭灯取决于 LED 接线(常见:PC8 输出低点亮)。
ORR
置位 bit8 → 输出高电平。每次设置后调用
Delay
软件延时,制造可见闪烁。

也可以使用
GPIO_BSRR (0x40011010)
实现原子置位/复位,避免 读-改-写 带来的干扰。

4.4. 延时子程序


Delay
			    LDR R0, =50000
DelayLoop
			    SUBS R0, #1
					BNE DelayLoop
					BX LR
					NOP

粗略的忙等待(busy wait):把常数
50000
写入 R0,
SUBS
递减并更新标志位,直到为 0 跳出。
BX LR
返回调用点。
NOP
占位,可用于调试或对齐。


运行流程

复位后
Reset_Handler
开始执行。开启 GPIOC 时钟,否则后续写入寄存器无效。配置 PC8 输出模式,保证可以驱动 LED。进入循环:
输出低电平 → 延时。输出高电平 → 延时。永久重复,实现 LED 闪烁。

这段汇编直接操作 RCC 与 GPIOC 寄存器,通过简单的延时循环实现 LED 闪烁。由于完全控制硬件寄存器,可以更好理解 STM32 启动流程、GPIO 配置和 Cortex‑M 汇编编程。

五. 从零写STM32标准库


1> 在KEIL中实现反汇编
在KEIL的User选项中,如下图添加这两项:
//生成bin文件
fromelf  --bin  --output=led.bin  Objectsled.axf
//生成反汇编文件
fromelf  --text  -a -c  --output=led.dis  Objectsled.axf

2> volatile的使用:
① 编译器很聪明,会帮我们做些优化,比如:
int   a;
a = 0;   // 这句话可以优化掉,不影响a的结果
a = 1;

② 有时候编译器会自作聪明,比如:
int *p = ioremap(xxxx, 4);  // GPIO寄存器的地址
*p = 0;   // 点灯,但是这句话被优化掉了
*p = 1;   // 灭灯

③ 对于上面的情况,为了避免编译器自动优化,需要加上volatile,告诉它“这是容易出错的,别乱优化”:
volatile  int *p = ioremap(xxxx, 4);  // GPIO寄存器的地址
*p = 0;   // 点灯,这句话不会被优化掉
*p = 1;   // 灭灯

【汇编语言】ARM体系结构(含汇编开发)

六. HAL库工程创建

1. Keil创建

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

2. CubeMX生成Keil


我的电脑-->右键-->高级系统设置-->环境变量-->系统环境变量设置
1> 新建JAVA_HOME
JAVA_HOME      C:Program FilesJavajdk1.8.0_102

2> 修改Path,增加如下:
%JAVA_HOME%in
%JAVA_HOME%jrein

3> 新建CLASSPATH
CLASSPATH   .;%JAVA_HOME%libdt.jar;%JAVA_HOME%lib	ools.jar

测试:
Win + R --->cmd进入DOS
javac 
java -version

3. CubeIDE创建

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

七. FreeRTOS和RT-Thread

1. FreeRTOS

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)

2. RT-Thread
1> Keil创建

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

【汇编语言】ARM体系结构(含汇编开发)

2> RT-thread Studio创建

【汇编语言】ARM体系结构(含汇编开发)
【汇编语言】ARM体系结构(含汇编开发)
以上,欢迎有从事同行业的电子信息工程、互联网通信、嵌入式开发的朋友共同探讨与提问,我可以提供实战演示或模板库。希望内容能够对你产生帮助!

© 版权声明

相关文章

暂无评论

none
暂无评论...