文章

浅谈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 00000x0800 0000

● 从系统存储器启动:系统存储器被映射到启动空间(0x0000 0000),但仍然能够在它原有的地址(互联型产品原有地址为0x1FFF B000,其它产品原有地址为0x1FFF F000)访问它。

● 从内置SRAM启动:只能在0x2000 0000开始的地址区访问SRAM。

*注意: 当从内置SRAM启动,在应用程序的初始化代码中,必须使用NVIC的异常表和偏移寄存器,重新映射向量表至SRAM中。

内存映射

1、从主内存启动:主闪存存储器0x0800 0000被映射到启动空间(0x0000 0000)

2、从系统存储器启动:系统存储器0x1FFF F0000x1FFF 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文件,内容如下:

Address048C
0x0800 00002000 0D980800 01450800 14C70800 1403

其中0x0800 00000x0000 0000)的内容为2000 0D98这个值为SP栈顶指针,第二个数据0x0800 00040x0000 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 00040x0000 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文件:

Address048C
0x0800 80002000 04100800 81450800 8A530800 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跳转之前需要注意:

  1. 确认在跳转之前是否有关中断,中断需要把对应打开的外部中断都关掉,只关总中断在进入APP之后再打开总中断,打开总中断之后在boot中没有关闭的中断又打开了,但是app中对应的地址和boot,所以打开中断会导致死机。
  2. 跳转前把用到的外设全部释放。

参考:

[深入剖析STM32]

https://mcu.eetrend.com/content/2023/100571706.html

STM32的完整启动流程分析

stm32启动文件里面的__main和主函数main()

STM32启动详细流程之__main

本文由作者按照 CC BY 4.0 进行授权