章節概述:
介紹什么是IAP、IAP的前置知識。
IAP
IAP(In Application Programming,在應用中編程);是用戶自己的程序在運行過程中對User Flash 的部分區域進行燒寫,目的是為了在產品發布后可以方便地通過預留的通信口對產品中的固件程序進行更新升級。
原理是:將代碼區划分為兩部分,兩部分區域各存放一個程序,通過觸發Bootloader對User application的擦除和重新寫入即可完成用戶應用的更換:
Bootloader
(引導加載程序),程序執行初始入口,根據定義好的條件(例如:按鍵是否被按下、串口是否接收到特定的數據、U盤是否插入等)選擇並執行分支;出廠以后固定下來,一般不隨便更改。(不建議開發人員提供更改Bootloader
的接口)User Application
(用戶應用程序),允許在需要變更時,對這一區域的程序進行更換。
站在用戶的角度來說,IAP做到了能讓用戶自己來更換設備里邊的代碼程序而廠家這邊只需要提供給用戶一個代碼文件。
代碼區:
BootLoader Application
┌─────┬─────────┐
└─────┴─────────┘
根據實際情況分配這兩塊空間的大小,BootLoader程序占用的空間越小越好。
通常實現IAP功能時,即用戶程序運行中作自身的更新操作,需要在設計固件程序時編寫兩個項目代碼:
- 第一個項目程序不執行正常的功能操作,而只是通過某種通信方式(如USB、USART)接收程序或數據,執行對第二部分代碼的更新
- 第二個項目代碼才是真正的功能代碼。
后面我們還會利用多個分區完成更高級的操作。
IAP准備
為了完成IAP功能,以及明確理解各個步驟的意義,我們先對有關知識進行說明:
- STM32啟動時程序執行順序(涉及分區與跳轉)
- 中斷機制(為了做到中斷重定向,最終的目的是使App程序的正確運行)
STM32啟動步驟
程序執行入口
STM32的啟動方式有3種(均是芯片內置的存儲介質):
- 內置FLASH(用戶閃存,一般默認是這個)
- 內置SRAM
- 系統存儲器ROM
通過BOOT0
和BOOT1
引腳的設置可以選擇啟動方式:
狀態 | 結果 |
---|---|
BOOT1=x BOOT0=0 |
從用戶閃存(內置FLASH)啟動,這是正常的工作模式。(x 代表任意) |
BOOT1=0 BOOT0=1 |
從系統存儲器啟動,這種模式啟動的程序功能由廠家設置。 |
BOOT1=1 BOOT0=1 |
從內置SRAM啟動,這種模式可以用於調試。 |
原理:決定哪個地址(
Main Flash
,System Flash
,SRAM
)映射到地址0x0000 0000
。
在《Cortex-M3權威指南》有講述:芯片復位后首先會從向量表里面取出兩個值:
- 從
0x0000 0000
地址取出MSP(主堆棧寄存器)
的值 - 從
0x0000 0004
地址取出PC(程序計數器)
的值,然后取出第一條指令執行
請注意,這與傳統的ARM架構不同(其實也和其它大多數的單片機不同)。傳統的ARM架構總是從0地址開始執行第一條指令,並且這是一條跳轉指令。在CM3中,在0地址提供的是MSP的初始值,然后緊跟着的是向量表(向量表在以后還可以轉移到其它位置)。向量表中的數值是32位的地址,而不是跳轉指令。向量表的第一個條目指向復位后應執行的第一條指令。
因為CM3使用的是向下生長的滿棧,所以MSP得初始值必須是堆棧內存的末地址加1,舉例來說:
如果堆棧區域在
0x20007C00
~0x20007FFF
之間,那么MSP的初始值就必須是0x20008000
。
所以我們知道,在嵌入式中,main函數並不是程序執行的入口。
一般來說,芯片執行代碼的入口地址是PC
指針在上電后的第一個值,也就是復位。通過復位后,指令在按順序往下執行。那么,由我們編寫的代碼鏈接順序就很重要了。
一般來說,執行順序是由鏈接文件告訴編譯器來決定的。當鏈接器進行鏈接的時候,首先決定各個目標文件在最終可執行文件里的位置。然后訪問所有目標文件的地址重定義表,對其中記錄的地址進行重定向(加上一個偏移量,即該編譯單元在可執行文件上的起始地址)。然后遍歷所有目標文件的未解決符號表,並且在所有的導出符號表里查找匹配的符號, 並在未解決符號表中所記錄的位置上填寫實現地址。最后把所有的目標文件的內容寫在各自的位置上,再作一些另的工作,就生成一個可執行文件。所以,程序的鏈接地址必須等於運行地址。
那么,一般在arm中,都是以 *.s
文件作為初始入口,例如stm32的startup_stm32f10x_md.s
文件寫得很清楚:
This module performs:
- Set the initial SP
- Set the initial PC == Reset_Handler
- Set the vector table entries with the exceptions ISR
- Branches to __main in the C library
也就是說,經過啟動文件,通過一些設置堆棧和中斷等最后引導至我們所說的main
函數,可以將main
函數視為應用層的入口。
啟動文件分析
分析
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標號用來定義棧的大小,這里是1 Kb
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的值是鏈接后,由鏈接器生成
; 上電后根據boot引腳來決定PC位置,比如boot設置為flash啟動,則啟動后PC跳到0x08000000。此時CPU會先取2個地址,第一個是棧頂地址,第二個是復位異常地址
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*****
啟動步驟總結
結合上述的所有知識以及匯編代碼分析,簡單來說,STM32系統的整個啟動流程:
- 系統復位,CPU在系統時鍾的第4個上升沿根據
BOOT0
,BOOT1
的配置確定寄存器SYSCFG_CFGR1
的MEM_MODE
的值。 MEM_MODE
進一步決定哪個地址(Main Flash
,System Flash
,SRAM
)映射到地址0x0000 0000
。- CPU從
0x0000 0000
地址取出棧頂地址賦給MSP寄存器(主堆棧寄存器),即MSP = __initial_sp
。這一步是由硬件自動完成的。 - 從
0x0000 0004
地址取出復位程序的地址給PC寄存器(程序計數器),即PC = Reset_Handler
。這一步也是由硬件自動完成。 映射地址+4
對應着復位中斷程序(如0x08000 0004
),也就是說,系統一開始就執行Reset_Handler
,進而運行SystemInit
。- 在
SystemInit
函數中初始化系統時鍾。 - 跳到C庫的
__main
函數初始化堆棧(初始化時是根據前面的分配的堆空間和棧空間來初始化的)和代碼重定位(初始RW 和ZI段),然后跳到main
函數執行應用程序。
就這樣,整個代碼啟動完成。接下來就是中斷產生於中斷響應了,中斷響應在IAP方案中也是需要注意的地方。
中斷機制
為了方便討論,以程序的映射地址0x00000000
而不是類似0x08000000
這樣的實際物理地址進行討論。
當以內置flash作為啟動,flash 的起始地址
0x0800 0000
被映射到0x0000 0000
。
沒有IAP時的中斷
在發生中斷的過程為:發生中斷(中斷請求),到中斷向量表查找中斷函數入口地址,跳轉到中斷函數,執行中斷函數,中斷返回。
也就是說在STM32的內置的Flash中有一個中斷向量表來存放各個中斷服務函數的入口地址,內置Flash的分配情況大致如下圖:
棧頂地址 | 中斷向量表 | 中斷服務函數 | 主程序 |
↑0x08000000,主程序 |
中斷向量表存放在代碼開始部分的后4個字節處(即0x00000004
);當發生中斷后程序通過查找該表得到相應的中斷服務程序入口地址,然后再跳到相應的中斷服務程序中執行。
代碼區開始的4個字節存放的是棧頂的地址(即0x00000000
中的內容是棧頂的位置)。
則程序的走向應為:
上電后從0x00000004
處取出復位中斷向量的地址,然后跳轉到復位中斷程序的入口,執行結束后跳轉到main函數中。
在執行main
函數的過程中發生中斷,則STM32強制將PC
指針指回中斷向量表處,從中斷向量表中找到相應的中斷函數入口地址,跳轉到相應的中斷服務函數,執行完中斷函數后再返回到main
函數中來。
允許IAP時的中斷
通過一開始的概述,我們知道IAP將分了2個區;了解了啟動步驟的具體流程,就不難看出實際上IAP中的2個分區的各個部分應該是什么樣子:
棧頂地址 | 中斷向量表 | 中斷服務函數 | IAP主程序 | 新棧頂地址 | 新中斷向量表 | 中斷服務函數 | 用戶程序 |
↑0x00000000,Bootloader | ↑0x00000000+N+M,User Application |
BootLoader
程序和User application
各有一個中斷向量表,假設BootLoader程序(IAP程序)占用的空間為N+M
字節(那么用戶程序的首地址就是0x00000000+N+M
)。
則程序的走向應為:
上電初始程序依然從0x00000004
處取出復位中斷向量地址,執行復位中斷函數后跳轉到IAP的main
。
在支持IAP模式的情況中,關於同時存在2個中斷向量表的理解:
實際上,在
BootLoader
跳轉到User
之前,需要對中斷向量表進行處理(具體是通過中斷向量表設置一個偏移量以設置向量表的重定義),改完偏移之后的中斷異常都用新表(復位中斷例外,APP通過軟件reset跳轉至BootLoader
)。簡單地理解就是,整個向量表地址向后偏移了(直接改變),再也回不去第一個向量表了(復位中斷例外);而不是像有些教程說的:先跳到舊的向量表,再跳到新的向量表。
在BootLoader
的main
函數執行完成后強制跳轉到0x00000004+N+M
處,執行Reset_handler
,跳轉到新的main
函數中來。
當發生中斷請求后,程序跳轉到新的中斷向量表中取出新的中斷函數入口地址,再跳轉到新的中斷服務函數中執行,執行完中斷函數后再返回到main
函數中來。
在User App
的main
函數的執行過程中,如果CPU得到一個中斷請求,PC指針本來應該跳轉到0x00000004
處的中斷向量表,由於我們設置了中斷向量表偏移量為N+M
,因此PC指針被強制跳轉到0x00000004+N+M
處的中斷向量表中得到相應的中斷函數地址,再跳轉到相應新的中斷服務函數,執行結束后返回到main
函數中來。
所以,如果沒有處理中斷向量表,那么在User App
下執行時,遇到中斷以后會重新跳轉IAP
中的中斷處理函數,顯然是不符合我們的期望的。
IAP流程總結
讀到這里,我相信大家對於IAP需要做的事情有很清晰的認識。
對於 BootLoader
流程大致應該如下:
1、初始化時鍾。
2、初始化中斷向量表地址。
3、初始化用於交互的接口(如按鍵,可設計為:上電時如果按鍵被按下則進行用戶程序更新操作)。
4、初始化通訊接口(如串口,用於讀取升級程序)。
5、判斷跳轉條件,是則執行步驟6,否則執行步驟10。
6、擦除用戶程序(例如:擦除0x08008000 ~ 0x0807ffff
地址空間Flash)。
7、從串口讀取新的用戶代碼數據,把代碼寫入用戶程序空間。
8、檢測數據接收完畢?是則執行步驟9,否則跳回步驟7。
9、用戶程序更新完畢,等待重新上電或硬件復位。
10、跳轉到用戶程序(強制將PC指針跳轉到0x08008000+4
處)。
對於 App
流程大致應該如下:
1、初始化時鍾。
2、修改中斷向量表地址。
3、正常執行。