STM32 IAP固件升级实验系列文章

一、STM32F1XX的启动过程

在《Cortex-M3权威指南》有讲述:芯片复位后首先会从向量表里面取出两个值(下图来自Cortex-M3权威指南);

  • 从0x0000 0000地址取出MSP(主堆栈寄存器)的值
  • 从0x0000 0004地址取出PC(程序计数器)的值
  • 然后取出第一条指令执行

Cortex-M3权威指南 复位序列

二、STM32启动文件的分析

1、启动文件源代码分析

分析startup_stm32f10x_hd.s启动文件时会涉及到到一些汇编指令,如果不认识的指令可以到mdk集成开发工具 的 help -> μVision Help 里面搜索;如下图:

代码块如下:

;******************** (C) COPYRIGHT 2011 STMicroelectronics ********************
;* File Name          : startup_stm32f10x_hd.s
;* Author             : MCD Application Team
;* Version            : V3.5.0
;* Date               : 11-March-2011
;* Description        : STM32F10x High Density Devices vector table for MDK-ARM 
;*                      toolchain. 
;*                      This module performs:
;*                      (上电复位后会做下面的几件事情)
;*                      - Set the initial SP(设置堆栈,就是设置MSP的值)
;*                      - Set the initial PC == Reset_Handler(设置PC的值)
;*                      - Set the vector table entries with the exceptions ISR address(设置中断向量表的地址)
;*                      - Configure the clock system and also configure the external (设置系统时钟;如果芯片外部由挂载SRAM,还需要配置SRAM,默认是没有挂外部SRAM的)
;*                        SRAM mounted on STM3210E-EVAL board to be used as data 
;*                        memory (optional, to be enabled by user)
;*                      - Branches to __main in the C library (which eventually      (调用C库的__main函数,然后调用main函数执行用户的)
;*                        calls main()).
;*                      After Reset the CortexM3 processor is in Thread mode,
;*                      priority is Privileged, and the Stack is set to Main.
;* <<< Use Configuration Wizard in Context Menu >>>   
;*******************************************************************************
; THE PRESENT FIRMWARE WHICH IS FOR GUIDANCE ONLY AIMS AT PROVIDING CUSTOMERS
; WITH CODING INFORMATION REGARDING THEIR PRODUCTS IN ORDER FOR THEM TO SAVE TIME.
; AS A RESULT, STMICROELECTRONICS SHALL NOT BE HELD LIABLE FOR ANY DIRECT,
; INDIRECT OR CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING FROM THE
; CONTENT OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE CODING
; INFORMATION CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS.
;*******************************************************************************

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

; ------------------分配栈空间----------------
Stack_Size      EQU     0x00000400      ;EQU指令是定义一个标号;标号名是Stack_Size; 值是0x00000400(有点类似于C语言的#define)。Stack_Size标号用来定义栈的大小
                AREA    STACK, NOINIT, READWRITE, ALIGN=3  ;AREA指令是定义一个段;这里定义一个 段名是STACK,不初始化,数据可读可写,2^3=8字节对齐的段(详细的说明可以查看指导手册)
Stack_Mem       SPACE   Stack_Size   ;SPACE汇编指令用来分配一块内存;这里开辟内存的大小是Stack_Size;这里是1K,用户也可以自己修改
__initial_sp      ;在内存块后面声明一个标号__initial_sp,这个标号就是栈顶的地址;在向量表里面会使用到

; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
; ------------------分配堆空间----------------
;和分配栈空间一样不过大小只是512字节
Heap_Size       EQU     0x00000200
                AREA    HEAP, NOINIT, READWRITE, ALIGN=3

__heap_base        ;__heap_base堆的起始地址
Heap_Mem        SPACE   Heap_Size      ;分配一个空间作为堆空间,如果函数里面有调用malloc等这系列的函数,都是从这里分配空间的
__heap_limit       ;__heap_base堆的结束地址

                PRESERVE8 ;PRESERVE8 指令作用是将堆栈按8字节对齐
                THUMB;THUMB作用是后面的指令使用Thumb指令集

; ------------------设置中断向量表----------------
; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY      ;定义一个段,段名是RESET的只读数据段
                ;EXPORT声明一个标号可被外部的文件使用,使标号具有全局属性
                EXPORT  __Vectors          ;声明一个__Vectors标号允许其他文件引用          
                EXPORT  __Vectors_End      ;声明一个__Vectors_End标号允许其他文件引用
                EXPORT  __Vectors_Size     ;声明一个__Vectors_Size标号允许其他文件引用

;DCD 指令是分配一个或者多个以字为单位的内存,并且按四字节对齐,并且要求初始化

;__Vectors 标号是 0x0000 0000 地址的入口,也是向量表的起始地址
__Vectors       DCD     __initial_sp               ;* Top of Stack     定义栈顶地址;单片机复位后会从这里取出值给MSP寄存器,
                                                   ;* 也就是从0x0000 0000 地址取出第一个值给MSP寄存器 (MSP = __initial_sp) 
                                                   ;* __initial_sp的值是链接后,由链接器生成

                DCD     Reset_Handler              ;* Reset Handler    定义程序入口的值;单片机复位后会从这里取出值给PC寄存器,
                                                   ;* 也就是从0x0000 0004 地址取出第一个值给PC程序计数器(pc = Reset_Handler)
                                                   ;* Reset_Handler是一个函数,在下面定义
                ;后面的定义是中断向量表的入口地址了这里就不多介绍了,想要了解的可以参考《STM32中文手册》和《Cortex-M3权威指南》
                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

                ; External Interrupts
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
                DCD     FLASH_IRQHandler           ; Flash
                DCD     RCC_IRQHandler             ; RCC

                .....由于文件太长这里省略了部分向量表的定义,完整的可以查看工程里的启动文件

                DCD     DMA2_Channel1_IRQHandler   ; DMA2 Channel1
                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   ;定义__Vectors_Size标号,值是向量表的大小

                AREA    |.text|, CODE, READONLY  ;定义一个代码段,段名是|.text|,属性是只读

;PROC指令是定义一个函数,通常和ENDP成对出现(标记程序的结束)               
; Reset handler
Reset_Handler   PROC                                      ;定义 Reset_Handler函数;复位后赋给PC寄存器的值就是Reset_Handler函数的入口地址值。也是系统上电后第一个执行的程序

                EXPORT  Reset_Handler             [WEAK]  ;*[WEAK]指令是将函数定义为弱定义。所谓的弱定义就是如果其他地方有定义这个函数,
                                                          ;*编译时使用另一个地方的函数,否则使用这个函数

                                                          ;*IMPORT   表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似
                IMPORT  __main                            ;*__main 和 SystemInit 函数都是外部文件的标号
                IMPORT  SystemInit                        ;* SystemInit 是STM32函数库的函数,作用是初始化系统时钟
                LDR     R0, =SystemInit
                BLX     R0              
                LDR     R0, =__main                       ;* __main是C库的函数,主要是初始化堆栈和代码重定位,然后跳到main函数执行用户编写的代码
                BX      R0
                ENDP

; Dummy Exception Handlers (infinite loops which can be modified)
;下面定义的都是异常服务函中断服务函数
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  DMA2_Channel2_IRQHandler   [WEAK]
                EXPORT  DMA2_Channel3_IRQHandler   [WEAK]
                EXPORT  DMA2_Channel4_5_IRQHandler [WEAK]

WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
.....由于文件太长这里省略了部分标号的定义,完整的可以查看工程里的启动文件
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
                B       .

                ENDP

                ALIGN    ;四字节对齐

;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
;下面函数是初始化堆栈的代码
                 IF      :DEF:__MICROLIB     
                 ;如果定义了__MICROLIB宏编译下面这部分代码,__MICROLIB在MDK工具里面定义
                 ;这种方式初始化堆栈是由 __main 初始化的
                 EXPORT  __initial_sp   ;栈顶地址 (EXPORT将标号声明为全局标号,供其他文件引用)
                 EXPORT  __heap_base    ;堆的起始地址
                 EXPORT  __heap_limit   ;堆的结束地址

                 ELSE
                 ;由用户初始化堆
                 ;否则编译下面的
                 IMPORT  __use_two_region_memory      ;__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

;******************* (C) COPYRIGHT 2011 STMicroelectronics *****END OF FILE*****

2、小结

STM32的启动步骤如下:

  • 上电复位后,从 0x0000 0000 地址取出栈顶地址赋给MSP寄存器(主堆栈寄存器),即MSP = __initial_sp。这一步是由硬件自动完成的
  • 0x0000 0004 地址取出复位程序的地址给PC寄存器(程序计数器),即PC = Reset_Handler。这一步也是由硬件自动完成
  • 调用SystemInit函数初始化系统时钟
  • 跳到C库的__main函数初始化堆栈(初始化时是根据前面的分配的堆空间和栈空间来初始化的)和代码重定位(初始RW 和ZI段),然后跳到main函数执行应用程序

三、bootloader分析

1.bootloader分析

由于芯片复位启动后执行的是bootloader程序,而bootloader的主要作用之一就是可以启动application。又因为application的空间和bootloader的空间是独立的。所以要顺利启动application。bootloader还要做以下处理:

  1. 设置application的MSP的值; 从0x0800 C000地址取出栈顶地址的值赋给MSP。(0x0800 C000 是application的起始地址 )
  2. 从 0x0800 C004 地址取出application第一个执行的函数地址赋值给函数指针fun_point
  3. 使用fun_point函数针跳到application执行
  4. 跳到application后需要设置中断向量表的偏移;也就是设置VTOR寄存器的值(VTOR寄存器可以到《Cortex-M3权威指南》查看)。为什么要设置向量表偏移?原因也很简单。因为发生中断后,cpu要到application的空间找到中断服务程序的;如果不设置偏移,默认是从bootloader空间找到中断服务程序的。

简单总结一下

其实从bootloader跳到application执行和上电复位启动bootloader差不多。两者的差别就是前者需要开发者自己设置,后者是由硬件自动完成的。

2.bootloader 程序

typedef void(*App_Fun_t)(void);
typedef void(*Firm_Reload_Fun_t)(void);

/* 定义一个函数指针,主要用于跳到app */
App_Fun_t app_main ;

/* 定义一个函数指针,主要用于调用firmware区的重定位 */
Firm_Reload_Fun_t firm_fun;
int main(void)
{
    /* 初始化串口和滴答定时器 */
    Sys_Init();

    firm_fun = (Firm_Reload_Fun_t)*(vu32*)(0x0807c000);
    /* frimware的RW和ZI段重定位 */
    firm_fun();
    /* 串口输出一些信息 */
    Sys_Printf("hello bootloader\r\n");

    Sys_Delay_ms(10);
        /* app的起始地址是0x0800C000 */
        /* 简单的判断一下app区域的向量表的数据正不正确 */
    if(((*(vu32*)(0x0800C000+4))&0xFF000000)==0x08000000){

            /* 设置应用程序的堆栈 */
            /* 
             * MSR_MSP 是一个函数,原型如下:
             * __asm void MSR_MSP(u32 addr)  //__asm的作用是将告诉编译器,函数体内部的代码是汇编指令 
             * {   
             *     // 由 ATPCS 规则,汇编跟C语言传参使用的是R0-R3寄存器(参数小于等于4个的情况下),所以r0的值就是addr    
             *     MSR MSP, r0 //MSR指令是将r0寄存器的值赋给MSP(主堆栈寄存器)    
             *     BX r14 //r14是lr寄存器(连接寄存器),保存返回地址的。这条指令的作用是函数执行完返回
             * }
             */
        MSR_MSP(*(vu32*)(0x0800C000));

            /* 从0x0800C004地址取出app第一个执行的函数入口 */
            app_main = (App_Fun_t)*(vu32*)(0x0800C004);

            /* 设置中断向量表偏移 */
            /* 其实中断向量的偏移值在这里设置也可以,不过需要注意的是在app的SystemInit函数又重新设置为0了,
             * 所以可以注释掉app的 SystemInit 的 SCB-&gt;VTOR = FLASH_BASE | VECT_TAB_OFFSET;语句,
             * 或者将VECT_TAB_OFFSET宏定义为: #define VECT_TAB_OFFSET  0xC000 
             */
            SCB-&gt;VTOR = (FLASH_BASE | 0xC000);

            /* 跳转到应用程序执行 */
            /* 这里跳过去就不会回来了 */
        app_main();

    }else{
        Sys_Printf("boot addr error\r\n");
    }
    while(1);

三、application

从bootloader跳到application后,application的程序也会做下面的几件事情:

执行SystemInit函数(这个是application空间的函数了)
执行__main(这个也是application空间的函数),然后跳到main函数执行业务程序
application 需要注意的就是,如果使用官方的启动文件,需要注释 SystemInit 函数的 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;语句。或者修改VECT_TAB_OFFSET的值。否者在bootloader设置的向量表偏移又会被设置为0了。然后app程序根据业务编写程序就好。

四、firmware

1、firmware程序的分和实现

通过上面分析启动文件后,编写firmware也是比较简单了。firmware跟application和bootloader都不在一个同一个工程,所以不能通过函数名调用firmware的区域的函数。要调用firmware区的函数时只能通过函数指针调用。所以在编写完firmware区域的函数后,还需要将函数的入口地址暴露出来。怎么暴露出来才能让其他的地方能正确的调用到呢? 其实我们也可以仿照中断向量表的方式,将函数firmware的所有函数定义成一张表,这张表放在firmware区域的前面地址。函数表的定义如下代码块:

                PRESERVE8
                THUMB

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

;__Vectors 是firmware的起始地址
;使用DCD指令将函数的入口地址存放在表中。
;下面是我自己实现的函数,感兴趣的可以自己定义或者修改

__Vectors       DCD     RW_And_ZI_Init              ;初始化RW段和ZI段
                DCD     Num_Inc                     ;数字自增
                DCD     Num_Dec                     ;数字自减
                DCD     Get_Num                     ;获取数字的值
                DCD     Get_Num1
                DCD     Get_Num_Addr
                DCD     Get_Flag
                DCD     Swap_Num                    ;交换数据
                DCD     My_Men_Copy                 ;内存copy                 
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

;使用AREA指令定义一个代码段。然后定义所有的函数在下面。

                AREA    |.text|, CODE, READONLY
                EXPORT  RW_And_ZI_Init            [WEAK]
                EXPORT  Num_Inc                   [WEAK]
                EXPORT  Num_Dec                   [WEAK]
                EXPORT  Get_Num                   [WEAK]
                EXPORT  Get_Num1                  [WEAK]
                EXPORT  Get_Num_Addr              [WEAK]
                EXPORT  Get_Flag                  [WEAK]
                EXPORT  Swap_Num                  [WEAK]
                EXPORT  My_Men_Copy               [WEAK]

RW_And_ZI_Init      
Num_Inc
Num_Dec
Get_Num
Get_Num1
Get_Num_Addr
Get_Flag
Swap_Num
My_Men_Copy

                B   .
                END

2、firmware注意事项

a.注意事项一

由于firmware没有初始化堆所以firmware区域的函数不能使用malloc和calloc这一系列的函数。同时firmware区域的函数是由application或者bootloader调用执行的,firmware区域不需要初始化和设置栈。

b.注意事项二

因为firmware区域不会调用C库__main,如果有使用全局变量,则需要自己实现代码重定位函数。并且需要在application或者bootloader调用执行一次。重定位的程序如下代码块:

unsigned int flag;
void RW_And_ZI_Init (void)
{
    /* flag是一个全局变量,但是在执行if判断的时候并没有进行重定位,所以这个值是一个随机值(不确定的) */
    /* flag的作用是防止 RW_And_ZI_Init函数 被调用 */ 

    if(flag!=0xf55faa55){   /* 一般第一次执行的时候不会等于0xf55faa55。如果你执行的时候等于0xf55faa55,那么恭喜你,这运气你可以去买彩票了 */
        /**********这些变量都是是由链接器链接的时候生成确定的***********/
        extern unsigned char Image$$ER_IROM1$$Limit;      //&Image$$ER_IROM1$$Limit;只读段的末尾地址,也是可读可写数据段的起始地址
        extern unsigned char Image$$RW_IRAM1$$Base;       //&Image$$RW_IRAM1$$Base是可读可写数据段的重定位的起始地址
        extern unsigned char Image$$RW_IRAM1$$RW$$Limit;  //&Image$$RW_IRAM1$$Base是RW数据段的重定位的结束地址,也是ZI数据段的重定位的起始地址
        extern unsigned char Image$$RW_IRAM1$$ZI$$Limit;  //&Image$$RW_IRAM1$$ZI$$Limit 是ZI数据段的重定位的结束地址
        /**********这些变量都是是由链接器链接的时候生成确定的***********/

        unsigned char * psrc, *pdst, *plimt;

        psrc  = (unsigned char *)&Image$$ER_IROM1$$Limit;

        pdst  = (unsigned char *)&Image$$RW_IRAM1$$Base;
        plimt = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
        /* 数据copy,也就是将存在flash空间的数据copy到RAM空间 */
        while(pdst &lt; plimt){
            *pdst++ = *psrc++;
        }
        psrc  = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
        plimt = (unsigned char *)&Image$$RW_IRAM1$$ZI$$Limit;
        /* 初始化ZI段 */
        while(psrc &lt; plimt){
            *psrc++ = 0;
        }
        /* 执行到这里flag的值和一些未初始化全局变量的值都是0 */

        flag=0xf55faa55; //将flag的值设置0xf55faa55防止再次被重定位
    }
} 

c.注意事项三

在调用firware区域的函数时,需要定义的函数指针的格式(即参数和返回值)一定要一致,否则可能发生一些不可预判的错误

工程源码

从码云下载: https://gitee.com/gu_lan/gl_stm32_partition

文章来源:https://www.cnblogs.com/gulan-zmc/p/12248509.html