下图是 STM32F103xCDE 型号的内存映射图。
由于 STM32 是 32 位,且其地址总线也为 32 根,所以其理论能够寻找的地址大小为 4GB。
从上图可以看出,左边的地址从 0x0000 0000 ~ 0xFFFF FFFF 的 4GB 是 STM32 理论分配的地址空间,STM32 实际上的空间大小 远远小于 4GB 的。4GB 中又划分出了 8 个块,一块占 512MB,分别作为 代码区、SRAM区、外设区、FSMC1区、FSMC2区、FSMC寄存器区、未使用区、Cortex-M3内部外设区。
映射其实就是对应的意思。事实上存储器本身并不具备地址,将芯片理论上的地址分配给存储器,这就是存储器映射。STM32 的所有片内外设其实都是存储器,所以所有的这些存储器都需要被映射。
理论上地址起始就是门牌号,存储中的每个字节就是房间,存储器生产出来后,这些房间是没有地址的(门牌号),映射的过程其实就是将这些门牌号分配给这些房间,分配好后,每个门牌号只能访问自己的房间,没有被分配的地址就是保留地址,所谓保留地址的意思就是,没有对应实际存储空间。
STM32 片内的 FLASH 分成两部分:主存储块、信息块。
STM32F103VET6 芯片的主Flash 的内存空间范围是 0x0800 0000 ~ 0x0807 FFFF,共 512KB。
在 block2 外设区,也就是地址从 0x4000000 ~ 0x5FFFFFF 这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,聪明的工程师就根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
自举(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 地址运行的。
IAP 的原理与上面两种有较大区别,这种方式将主存储区又分成了两个区域(根据实际需要由开发者自行分配),0x0800 0000 起始处的这部分,存储一个开发者自己设计的 Bootloader 程序,另一部分存储真正需要运行的 APP 程序。
单片机的 Bootloader 程序,其主要作用就是给单片机升级。在单片机启动时,首先从 Bootloader 程序启动,一般情况不需要升级,就会立即从 Bootloader 程序跳转到存储区另一部分的 APP 程序开始运行。
假如 Bootloader 程序时,需要进行升级(比如APP程序运行时,接收到升级指令,可以在 flash 中的特定位置设置一个标志,然后触发重启,重启后进入 Bootloader 程序,Bootloader 程序根据标志位就能判断是否需要升级),则会通过某种方式(比如通过 WIFI 接收升级包,或借助另一块单片机接收升级包,Bootloader 再通过串口或 SPI 等方式从另一块单片机获取升级包数据)先将接收到的程序写入存储区中存储 APP 程序的那个位置,写入完成后再跳转到该位置,即实现了程序的升级。
STM32 的片内 RAM 分为如下几个段:
启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作:
- 初始化堆栈指针 SP=_initial_sp
- 初始化 PC 指针=Reset_Handler
- 初始化中断向量表
- 配置系统时钟
- 调用 C 库函数 _main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界
// #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
// #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
// RESET:段名;DATA:包含数据,不包含指令;READONLY:只读
AREA RESET, DATA, READONLY
/* 声明 __Vectors、__Vectors_End 和 __Vectors_Size 这三个标号具有全局属性,
可供外部的文件调用 */
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
// __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 语言中的函数名就是一个地址。
// .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 的世界。
/* 初始化默认中断程序(无限循环) */
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
/* 用户栈和堆初始化, 由 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://zhuanlan.zhihu.com/p/511268958
https://zhuanlan.zhihu.com/p/367821312