章节说明
STM32 IAP固件升级实验分为一下的章节(加粗的字体是本章节的内容):
一、Flash和RAM的区域划分、工程建立、程序分散加载、程序烧写
二、Stm32 bootloader、application、firmware 程序的分析和编写
三、使用DMA来读取串口的不定长数据
四、通信协议的设计
五、STM32 IAP程序的设计
六、上位机的程序的编写
一、STM32F1XX的启动过程
在《Cortex-M3权威指南》有讲述:芯片复位后首先会从向量表里面取出两个值(下图来自Cortex-M3权威指南);
- 从
0x0000 0000
地址取出MSP(主堆栈寄存器)
的值- 从
0x0000 0004
地址取出PC(程序计数器)
的值- 然后取出第一条指令执行
二、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
还要做以下处理:
- 设置
application
的MSP
的值; 从0x0800 C000
地址取出栈顶地址的值赋给MSP
。(0x0800 C000
是application
的起始地址 )- 从
0x0800 C004
地址取出application
第一个执行的函数地址赋值给函数指针fun_point
- 使用
fun_point
函数针跳到application
执行- 跳到
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->VTOR = FLASH_BASE | VECT_TAB_OFFSET;语句,
* 或者将VECT_TAB_OFFSET宏定义为: #define VECT_TAB_OFFSET 0xC000
*/
SCB->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 < plimt){
*pdst++ = *psrc++;
}
psrc = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
plimt = (unsigned char *)&Image$$RW_IRAM1$$ZI$$Limit;
/* 初始化ZI段 */
while(psrc < plimt){
*psrc++ = 0;
}
/* 执行到这里flag的值和一些未初始化全局变量的值都是0 */
flag=0xf55faa55; //将flag的值设置0xf55faa55防止再次被重定位
}
}
c.注意事项三
在调用
firware
区域的函数时,需要定义的函数指针的格式(即参数和返回值)一定要一致,否则可能发生一些不可预判的错误
五、
最后将工程的链接附上--->点我project!!!