章節說明
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!!!