浅谈STM32的启动过程
BOOT配置
STM32单片机的启动过程首先需要了解该款单片机的硬件启动配置,根据数据手册可以知道,在STM32F10xxx里,可以通过BOOT[1:0]引脚选择三种不同启动模式。
表6 启动模式
1
2
3
4
5
启动模式选择引脚 | 启动模式 | 说明
BOOT1 BOOT0 | |
X 0 | 主闪存存储器 | 主闪存存储器被选为启动区域
0 1 | 系统存储器 | 系统存储器被选为启动区域
1 1 | 内置SRAM | 内置SRAM被选为启动区域
在系统复位后,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式。
在从待机模式退出时,BOOT引脚的值将被被重新锁存;因此,在待机模式下BOOT引脚应保持为需要的启动配置。在启动延迟之后,CPU从地址0x0000 0000
获取堆栈顶的地址,并从启动存储器的0x0000 0004
指示的地址开始执行代码。
因为固定的存储器映像,代码区始终从地址0x0000 0000
开始(通过ICode和DCode总线访问),而数据区(SRAM)始终从地址0x2000 0000
开始(通过系统总线访问)。Cortex-M3的CPU始终从ICode总线获取复位向量,即启动仅适合于从代码区开始(典型地从Flash启动)。STM32F10xxx微控制器实现了一个特殊的机制,系统可以不仅仅从Flash存储器或系统存储器启动,还可以从内置SRAM启动。
根据选定的启动模式,主闪存存储器、系统存储器或SRAM可以按照以下方式访问:
● 从主闪存存储器启动:主闪存存储器被映射到启动空间(0x0000 0000
),但仍然能够在它原有的地址(0x0800 0000
)访问它,即闪存存储器的内容可以在两个地址区域访问,0x0000 0000
或0x0800 0000
。
● 从系统存储器启动:系统存储器被映射到启动空间(0x0000 0000
),但仍然能够在它原有的地址(互联型产品原有地址为0x1FFF B000
,其它产品原有地址为0x1FFF F000
)访问它。
● 从内置SRAM启动:只能在0x2000 0000
开始的地址区访问SRAM。
*注意: 当从内置SRAM启动,在应用程序的初始化代码中,必须使用NVIC的异常表和偏移寄存器,重新映射向量表至SRAM中。
内存映射
1、从主内存启动:主闪存存储器0x0800 0000
被映射到启动空间(0x0000 0000
)
2、从系统存储器启动:系统存储器0x1FFF F000
或0x1FFF B000
被映射到启动空间(0x0000 0000
)
注意:这里是厂商的bootload程序,可以通过串口或者其他方式将程序下载到主内存(flash0x0800 0000
)内。
3、从SRAM中启动,这里没有内存映射(???),而是通过修改中断向量表和启动的偏移地址来启动。
启动代码分析
这里以f103zet6芯片为例,在HAL库的MDK-ARM软件中,启动文件为startup_stm32f103xe.s
,根据文件的描述文件
1
2
3
4
5
6
7
8
9
10
;* Description : STM32F103xE Devices vector table for MDK-ARM toolchain.
;* This module performs:
;* - Set the initial SP
;* - Set the initial PC == Reset_Handler
;* - Set the vector table entries with the exceptions ISR address
;* - Configure the clock system
;* - Branches to __main in the C library (which eventually
;* calls main()).
;* After Reset the Cortex-M3 processor is in Thread mode,
;* priority is Privileged, and the Stack is set to Main.
可以知道,该文件主要做了以下事情:
1、初始化SP(Stack Point)指针,这里为栈顶地址(栈向下增长)
2、初始化PC(Program Counter)指针==Reset_Handler(复位回调函数),PC指针存放下一条运行指令的地址,这里表示下一步运行复位回调函数
3、设置中断向量表,向量表是一个WORD( 32 位整数,4bytes)数组
__Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; 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
复位回调函数
; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP
4、配置系统时钟,通过执行
SystemInit
函数来进行配置5、进入main函数,
__main
是一个标准的 C 库函数,主要作用是初始化用户堆栈,这个是由编译器完成的,该函数最终会调用我们自己写的main
函数,从而进入C世界中。
当然,该文件还配置了堆栈的大小,堆栈内容初始化,多少字节对齐等等
__main函数内容
在Reset_Handler
函数中,执行了两个函数,一个为SystemInit
,该函数为系统初始化,是用户自己编写的代码,另一个是__main
函数,这个函数为c库函数,由编译器直接完成,那么该函数具体执行了哪些内容呢,最终又怎样进入用户的main
函数呢,该函数的执行过程如下:
1
2
3
4
5
6
7
__main ---> 初始化rw区
初始化zi区
__rt_entry() ---> __user_initial_stackheap()
__rt_stackheap_init()
__rt_lib_init()
main()
exit()
首先初始化RW区,这里需要从FLASH中复制参数到RAM区,再初始化ZI区,这里只需要给0就可以了,进而执行__rt_entry()
函数,该函数主要执行了初始化堆栈和进入main
函数的操作,到此,程序进入C语言的世界。
启动内容分析
单片机从0x0000 0000
这个默认地址开始运行,假设boot是从主存储空间启动的,那么0x0000 0000
这里的内容实际上是映射了0x0800 0000
这个地址上的内容,而0x0800 0000
这个地址实际上为下载的hex/bin文件,以一个工程的hex文件和map文件来进行分析。
打开hex文件,内容如下:
Address | 0 | 4 | 8 | C |
---|---|---|---|---|
0x0800 0000 | 2000 0D98 | 0800 0145 | 0800 14C7 | 0800 1403 |
其中0x0800 0000
(0x0000 0000
)的内容为2000 0D98
这个值为SP栈顶指针,第二个数据0x0800 0004
(0x0000 0004
)的内容为0800 0145
这个值为PC指针,表示下一条运行指令的地址。
查阅map文件:
1
2
3
__initial_sp 0x20000d98 Data 0 startup_stm32f103xe.o(STACK)
0x20000598 - 0x00000800 Zero RW 1 STACK startup_stm32f103xe.o
从这里可以看到,在初始化的时候SP指针确实为2000 0D98
,同时栈区的起始地址为0x20000598
,大小为0x00000800
,同样可以计算出栈顶SP指针的地址为0x20000d98
。
1
Reset_Handler 0x08000145 Thumb Code 8 startup_stm32f103xe.o(.text)
在文件进行链接的时候,复位回调函数(Reset_Handler)的地址为0x08000145
,这里对应了0x0800 0004
(0x0000 0004
)的内容,表示开机就执行该函数(这里没有做内存对齐,在实际中是对齐到了0x0800 0144
的)。在执行完该函数(Reset_Handler)之后,就会跳转到main函数内了。
1
2
3
4
5
6
149: LDR R0, =SystemInit
0x08000144 4806 LDR r0,[pc,#24] ; @0x08000160
150: BLX R0
0x08000146 4780 BLX r0
151: LDR R0, =__main
0x08000148 4806 LDR r0,[pc,#24] ; @0x08000164
IAP程序偏移
本小节摘自 https://blog.csdn.net/zhuimeng_ruili/article/details/119709888。
1)ICP(In Circuit Programing)。在电路编程,可通过CPU的Debug Access Port 烧录代码,比如ARM Cortex的Debug Interface主要是SWD(Serial Wire Debug)或JTAG(Joint Test Action Group);
2)ISP(In System Programing)。在系统编程,可借助MCU厂商预置的Bootloader 实现通过板载UART或USB接口烧录代码。
3)IAP(In Applicating Programing)。在应用编程,由开发者实现Bootloader功能,比如STM32存储映射Code分区中的Flash本是存储用户应用程序的区间(上电从此处执行用户代码),开发者可以将自己实现的Bootloader存放到Flash区间,MCU上电启动先执行用户的Bootloader代码,该代码可为用户应用程序的下载、校验、增量/补丁更新、升级、恢复等提供支持,如果用户代码提供了网络访问功能,IAP 还能通过无线网络下载更新代码,实现OTA空中升级功能。
4)IAP和ISP 的区别。
a、ISP程序一般是芯片厂家提供的。IAP一般是用户自己编写的
b、ISP一般支持的烧录方式有限,只有串口等。IAP就比较灵活,可以灵活的使用各种通信协议烧录
c、isp一般需要芯片进行一些硬件上的操作才行,IAP全部工作由程序完成,不需要去现场
d、isp一般只需要按格式将升级文件通过串口发送就可以。IAP的话控制相对麻烦,如果是OTA的话还需要编写后台的。
e、注意,这里介绍的bootloader功能显然跟之前介绍的启动文件bootloader有所区别,其目的是为了能接受外部镜像进行烧录,而不是为了运行普通用户程序。
通过以上可以知道,IAP可以烧录程序去进行升级,更新版本的操作,那么烧录进去的程序和直接运行的程序有什么区别呢?这里就需要了解IAP烧录程序的偏移地址了,首先它的程序FLASH起始地址需要进行偏移,如:直接运行的程序起始地址为0x0800 0000
,这个地址在从主存储空间启动的时候会直接映射到0x0000 0000
上去,但是IAP烧录的APP程序不能这样,否则会覆盖掉IAP的bootloader程序,假设偏移到0x0800 8000
这个地址区,MDK-ARM中的IROM地址需要修改为这个偏移之后的地址。
查看代码生成的hex文件:
Address | 0 | 4 | 8 | C |
---|---|---|---|---|
0x0800 8000 | 2000 0410 | 0800 8145 | 0800 8A53 | 0800 89ED |
可以看到,hex文件的起始地址已经为0x0800 8000
了,再查看0x0800 0000
地址处的内容,全部都是FF
,可知直接将IAP的APP程序下载到芯片内是无法正常运行的。
查看map文件
1
Reset_Handler 0x08008145 Thumb Code 8 startup_stm32f103xe.o(.text)
复位回调函数(PC指针)的地址也变成了0800 8145
。
再看IAP中执行APP程序的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
void UserJumpToApplication(uint32_t address)
{
/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
if (((*(__IO uint32_t *)address) & 0x2FFE0000) == 0x20000000)
{
/* Jump to user application */
JumpAddress = *(__IO uint32_t *)(address + 4);
JumpToApplication = (pFunction)JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t *)address);
JumpToApplication();
}
}
假设函数的入口参数是上一个进行偏移地址的程序,那么address=0x08008000
,第一句(*(__IO uint32_t *)address)
的内容为2000 0410
,这里&0x2FFE0000
的原因为ZET6
这款单片机RAM大小为0x10000
,第二句JumpAddress = *(__IO uint32_t *)(address + 4);
为取出0x0800 8000+4
这个地址的值,这个值的内容为PC指针指向的内容0800 8145
(内存对齐为0x0800 8144
),后面将这个地址转换成函数指针去执行实际上就是去执行APP程序的Reset_Handler ,第四句__set_MSP(*(__IO uint32_t *)*address*);
为设置栈顶指针为APP程序的栈顶指针,这时就从IAP的bootloader程序跳转到APP程序去执行了。
1
2
__Vectors 0x08008000 Data 4 startup_stm32f103xe.o(RESET)
__Vectors_End 0x08008130 Data 0 startup_stm32f103xe.o(RESET)
因为APP程序进行了地址偏移,如上所示中断向量表的位置也进行了偏移,在APP程序的Reset_Handler函数中需要对中断向量表进行重映射,这时就需要调整SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
的值(SCB代表系统控制块(System Control Block),VTOR全称为“向量表偏移寄存器”(Vector Table Offset Register)),通过设置SCB->VTOR
的值,将中断向量表从0x0800 8000
这个地址映射到0x0000 0000
上去。
在执行IAP跳转之前需要注意:
- 确认在跳转之前是否有关中断,中断需要把对应打开的外部中断都关掉,只关总中断在进入APP之后再打开总中断,打开总中断之后在boot中没有关闭的中断又打开了,但是app中对应的地址和boot,所以打开中断会导致死机。
- 跳转前把用到的外设全部释放。
参考:
https://mcu.eetrend.com/content/2023/100571706.html