一、内存映射
1、内存映射图
下图是 STM32F103xCDE 型号的内存映射图。
2、内存划分
由于 STM32 是 32 位,且其地址总线也为 32 根,所以其理论能够寻找的地址大小为 4GB。
从上图可以看出,左边的地址从 0x0000 0000 ~ 0xFFFF FFFF 的 4GB 是 STM32 理论分配的地址空间,STM32 实际上的空间大小 远远小于 4GB 的。4GB 中又划分出了 8 个块,一块占 512MB,分别作为 代码区、SRAM区、外设区、FSMC1区、FSMC2区、FSMC寄存器区、未使用区、Cortex-M3内部外设区。
3、存储器映射
映射其实就是对应的意思。事实上存储器本身并不具备地址,将芯片理论上的地址分配给存储器,这就是存储器映射。STM32 的所有片内外设其实都是存储器,所以所有的这些存储器都需要被映射。
理论上地址起始就是门牌号,存储中的每个字节就是房间,存储器生产出来后,这些房间是没有地址的(门牌号),映射的过程其实就是将这些门牌号分配给这些房间,分配好后,每个门牌号只能访问自己的房间,没有被分配的地址就是保留地址,所谓保留地址的意思就是,没有对应实际存储空间。
STM32 片内的 FLASH 分成两部分:主存储块、信息块。
- 主存储块
- 主Flash:用于存储程序,我们写的程序一般存储在这里。
- 信息块
- 系统存储器(系统FLASH):存放在系统存储器自举模式下的启动程序(BootLoader),当使用 ISP 方式加载程序时,就是由这个程序执行。这个区域由芯片厂写入 BootLoader,然后锁死,用户是无法改变这个区域的。
- 选项字节:存储芯片的配置信息及对主存储块(主Flash)的保护信息。
STM32F103VET6 芯片的主Flash 的内存空间范围是 0x0800 0000 ~ 0x0807 FFFF,共 512KB。
4、寄存器映射
在 block2 外设区,也就是地址从 0x4000000 ~ 0x5FFFFFF 这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,聪明的工程师就根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
5、地址重映射
自举(bootstrap)计算机设备使用硬件加载的程序,用于初始化足够的软件来查找并加载功能完整的操作系统。也用来描述加载自举程序的过程。什么是单片机的自举,单片机的自举就是单片机的启动。
而众所周知,单片机在每次上电时都是从 0 地址开始执行,那么这就存在一个问题,我们下载程序时是将代码放在 主Flash ,其地址为 0x0800 0000 ~ 0x0807 FFFF,起始地址并不在 0 地址,那单片机要如何找到代码并执行呢?
在地址划分的区域可以看出,0x0000 0000 ~ 0x0007 FFFF 这块区域的功能是专门进行地址重映射的,而要进行重映射的区域取决于 BOOT 引脚,通过 BOOT1 和 BOOT0 引脚的电平值,可以选择将0x0000 0000 ~ 0x0007 FFFF 映射到不同的存储器上。
这就解释了为什么我们在 keil 中设置好程序的下载地址为 0x8000000,但是单片机上电是确实从 0 开始执行。是因为我们在硬件上设置了 BOOT0=0,BOOT1=X,从而导致了主FLASH 区被映射到了0x0000 0000 ~ 0x0007 FFFF(512KB),故而代码是下载到 0x80000000 往后的存储空间中,却说运行又是从 0x00000000 地址运行的。
5.1 STM32启动模式
- 从主FLASH启动:将主 Flash 地址 0x0800 0000 映射到 0x0000 0000,这样代码启动之后就相当于从 0x08000000 开始。一般使用 JTAG 或者 SWD 模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。
- 从系统存储器启动:从系统存储器地址 0x1FFF F000 开始执行代码。系统存储器是芯片内部一块特定的区域,芯片出厂时在这个区域预置了一段 Bootloader,就是通常说的 ISP 程序。这个区域的内容在芯片出厂后没有人能够修改或擦除,即它是一个 ROM 区。启动的程序功能由厂家设置。系统存储器存储的其实就是 STM32 自带的 bootloader 代码。
- 从内置SRAM启动:将 SRAM 地址 0x20000000 映射到 0x00000000,这样代码启动之后就相当于从 0x20000000 开始。内置 SRAM,也就是STM32的内存,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的地方,然后就需要重新擦除整个Flash,比较的费时,可以考虑从这个模式启动代码,用于快速的程序调试,等程序调试完成后,在将程序下载到SRAM中。
6、程序烧录方式
6.1 ISP(串口烧录)
- BOOT0 = 1,BOOT1 = 0
- 启动地址:0x1FFF F000
- 使用串口下载程序
- 系统存储器(系统Flash)启动方式运行内置的 Bootloader,将程序写入主存储区(主Flash)
- 重启后,需要再将 BOOT0 拉低,从主存储区(主Flash)启动程序
6.2 ICP(SWD/JTAG接口烧录)
- BOOT0 = 0,BOOT1 = x
- 启动地址:0x0800 0000
- 使用 JTAG 或者 SWD 模式下载程序
- 主闪存存储器(主Flash)启动方式,将程序在主存储区写入
- 重启后也直接从这启动程序
6.3 IAP
IAP 的原理与上面两种有较大区别,这种方式将主存储区又分成了两个区域(根据实际需要由开发者自行分配),0x0800 0000 起始处的这部分,存储一个开发者自己设计的 Bootloader 程序,另一部分存储真正需要运行的 APP 程序。
单片机的 Bootloader 程序,其主要作用就是给单片机升级。在单片机启动时,首先从 Bootloader 程序启动,一般情况不需要升级,就会立即从 Bootloader 程序跳转到存储区另一部分的 APP 程序开始运行。
假如 Bootloader 程序时,需要进行升级(比如APP程序运行时,接收到升级指令,可以在 flash 中的特定位置设置一个标志,然后触发重启,重启后进入 Bootloader 程序,Bootloader 程序根据标志位就能判断是否需要升级),则会通过某种方式(比如通过 WIFI 接收升级包,或借助另一块单片机接收升级包,Bootloader 再通过串口或 SPI 等方式从另一块单片机获取升级包数据)先将接收到的程序写入存储区中存储 APP 程序的那个位置,写入完成后再跳转到该位置,即实现了程序的升级。
二、启动过程
STM32 的片内 RAM 分为如下几个段:
1、概括
启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作:
- 初始化堆栈指针 SP=_initial_sp
- 初始化 PC 指针=Reset_Handler
- 初始化中断向量表
- 配置系统时钟
- 调用 C 库函数 _main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界
2、栈初始化
// #define Stack_Size 0x00000400
Stack_Size EQU 0x00000400
// STACK:段名;NOINIT:不初始化;READWRITE:可读可写;ALIGN=3:2^3,即 8 字节对齐
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
// 栈的结束地址,即栈顶地址,需要保存栈顶的地址
__initial_sp
- EQU:宏定义的伪指令,相当于等于,类似与 C 中的 define。
- AREA:告诉汇编器汇编一个新的代码段或者数据段。
- SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。
3、堆初始化
// #define Heap_Size 0x00000200
Heap_Size EQU 0x00000200
// HEAP:段名;NOINIT:不初始化;READWRITE:可读可写;ALIGN=3:2^3,即 8 字节对齐
AREA HEAP, NOINIT, READWRITE, ALIGN=3
// 堆的起始地址
__heap_base
Heap_Mem SPACE Heap_Size
// 堆的结束地址
__heap_limit
PRESERVE8
THUMB
- PRESERVE8:指定当前文件的堆栈按照 8 字节对齐。
- THUMB:表示后面指令兼容 THUMB 指令。THUBM 是 ARM 以前的指令集,16bit,现在 Cortex-M 系列的都使用 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超集。
4、初始化向量表
4.1 开辟向量表空间
// RESET:段名;DATA:包含数据,不包含指令;READONLY:只读
AREA RESET, DATA, READONLY
/* 声明 __Vectors、__Vectors_End 和 __Vectors_Size 这三个标号具有全局属性,
可供外部的文件调用 */
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
- EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。
4.2 初始化向量表
// __Vectors:向量表起始地址
__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位程序地址
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
// 外部中断开始
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
// 限于篇幅,中间代码省略
DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2
DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3
DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
// __Vectors_End:向量表结束地址
__Vectors_End
// 获得向量表大小
__Vectors_Size EQU __Vectors_End - __Vectors
向量表从 FLASH 的 0 地址(0x0800 0000)开始放置,以 4 个字节为一个单位,地址 0(0x0800 0000)存放的是栈顶地址,0x04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。
- DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表 中,DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。
4.3 复位中断程序初始化
// .text:段名;DATA:包含机器指令;READONLY:只读
AREA |.text|, CODE, READONLY
// 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 函数的原因。
4.4 其他中断程序初始化
/* 初始化默认中断程序(无限循环) */
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
// 限于篇幅,中间代码省略
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
/* 外部中断 */
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMPER_IRQHandler [WEAK]
EXPORT RTC_IRQHandler [WEAK]
// 限于篇幅,中间代码省略
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
B .
ENDP
ALIGN
- B:跳转到一个标号。这里跳转到一个‘.’,即表示无限循环。
- PROC:过程(子程序)的开始。
- ENDP:过程(子程序)的结束。
- ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。
4.5 用户堆栈初始化
/* 用户栈和堆初始化, 由 C 库函数 _main 来完成 */
// 这个宏在 KEIL 里面开启
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(堆结束地址)全局属性,可供外部文件调用。然后堆栈的初始化就由 C 库函数 _main 来完成。
如果没有定义 __MICROLIB,则才用双段存储器模式,且声明标号 __user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。
文章来源:https://www.toymoban.com/news/detail-516541.html
- IF,ELSE,ENDIF:汇编的条件分支语句,跟C 语言的if ,else 类似。
- END:文件结束
参考资料:
https://zhuanlan.zhihu.com/p/511268958
https://zhuanlan.zhihu.com/p/367821312文章来源地址https://www.toymoban.com/news/detail-516541.html
到了这里,关于【STM32】STM32内存映射以及启动过程(超详细过程)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!