章節概述:
以一個最簡單的例子示范IAP程序(沒有文件通訊,沒有跳轉判斷),需要借助IDE進行分區數據的划分以及下載。
准備
IDE:keil-MDK 5
MCU: STM32F103ZET6
為例(Flash地址為0x08000000—0x0807ffff,共512KB)。
BSP:STM32-HAL
啟動方式:FLASH
啟動
- 前32KB存放BootLoader程序(
0x08000000 ~ 0x08007fff
) - 剩余的空間存放APP程序(
0x08008000 ~ 0x0807ffff
)
假定跳轉的APP程序的實際物理地址為:
0x08008000
分別新建2個工程,名字可以為:bootLoader
與app
。
從Bootloader跳轉到APP
Bootloader
為了區分執行分區的不同,添加串口打印功能。(略)
例如:
printf("Hello Bootloader\r\n");
添加下面的代碼以實現跳轉功能:
節選自CubeMX的例程:
STM32Cube\Repository\xxx\Projects\Applications\IAP
#define NVIC_VectTab_RAM ((u32)0x20000000)
#define NVIC_VectTab_FLASH ((u32)0x08000000)
#define PHYSICAL_ADDRESS_Flash (0x08000000) // 程序燒寫的物理地址
#define APPLICATION_POSADDR (0x0000C000) // APP 偏移量
#define APPLICATION_ADDRESS ((PHYSICAL_ADDRESS_Flash) | (APPLICATION_POSADDR)) // 最終跳轉的地址,實際上就是0x08008000
typedef void (*pFunction)(void);
pFunction JumpToApplication;
uint32_t JumpAddress;
void NVIC_SetVectorTable(uint32_t base, uint32_t offset)
{
/* close interruption*/
__set_FAULTMASK(1);
/* set vector table*/
//NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0xffset);
SCB->VTOR = base | offset;
/* open interruption*/
__set_FAULTMASK(0);
}
int main(void)
{
// 在BootLoader程序的中斷向量表指向設置中應有這么一句:
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0); //設置中斷向量表指向
/* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000) //①
{
/* Jump to user application */
JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);// ②
JumpToApplication = (pFunction) JumpAddress;//③
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS); // ④
JumpToApplication(); // ⑤
}
while (1)
{}
}
解析:
①因為用戶程序開始位置(0x08008000
處)的前4個字節存放的是堆棧的地址,堆棧地址必定是指向RAM空間的,而STM32的RAM空間起始地址為0x20000000
,所以要進行判斷。
APPLICATION_ADDRESS
存放的是用戶程序Flash的首地址
(*(volatile u32*)APPLICATION_ADDRESS)
的意思是取用戶程序首地址里面的數據,這個數據就是用戶代碼的堆棧地址,堆棧地址指向RAM,而RAM的起始地址是0x20000000,因此上面的判斷語句執行:判斷用戶代碼的堆棧地址是否落在:0x20000000~0x2001ffff區間中,這個區間的大小為128K,筆者查閱STM32各型號的RAM大小,目前RAM最大的容量可以做到192K+4K,時鍾頻率為168MHZ。一般情況下,我們使用的芯片較多的落在<128K RAM的區間,因此上面的判斷語句是沒有太大問題的。
②程序跳轉地址的確認,前面已經說過0x08008004
處的4個字節存放的是復位函數的入口地址,該句的意思為獲得ApplicationAddress + 4
地址處的數據,即為獲得新的復位函數入口地址。
③令Jump_To_Application
這個函數指針指向復位函數入口地址。
④堆棧的初始化,調用__set_MSP
重新設定棧頂代地址,把棧頂地址設置為用戶代碼指向的棧頂地址。
⑤跳轉到新的復位函數。設置PC指針為復位地址。
App
為了區分執行分區的不同,添加串口打印功能。(略)
例如:
printf("Hello App\r\n");
添加下面的代碼以配合跳轉功能:
#define NVIC_VectTab_RAM ((u32)0x20000000)
#define NVIC_VectTab_FLASH ((u32)0x08000000)
#define APPLICATION_POSADDR (0x0000C000) // APP 偏移量
void NVIC_SetVectorTable(uint32_t base, uint32_t offset)
{
/* close interruption*/
__set_FAULTMASK(1);
/* set vector table*/
//NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0xffset);
SCB->VTOR = base | offset;
/* open interruption*/
__set_FAULTMASK(0);
}
int main(void)
{
// 在用戶程序的中斷向量表指向設置用應有這么一句:
NVIC_SetVectorTable(NVIC_VectTab_FLASH, APPLICATION_POSADDR); //設置中斷向量表指向
...;
}
工程設置
程序寫完了,但是編譯還需要進行設置。
BootLoader工程
為了確保
Address Range
不超過0x08008000
,超過會導致app分區被覆蓋。
點擊:MDK
-Options
-Debug
-Settings
:
Flash Download
,勾選Reset and Run
、Program
、Reset and Run
- 點擊列表中的Flash(如果沒有則需要點擊
Add
進行添加)- 修改
Size
為:0x08004000
(只要不超過0x8000
即可)
- 修改
編譯,燒寫。
APP 工程
為了修改鏈接地址,以及燒錄范圍。
MDK
-Options
-Target
-Read/Only Memory Areas
- 勾選
IROM1
:填入0x08008000
。
MDK
-Options
-Target
-Debug
-Settings
:
- 點擊
Flash Download
,勾選Reset and Run
、Program
、Reset and Run
- 點擊列表中的Flash(如果沒有則需要點擊
Add
進行添加)- 修改
Start
為:0x08008000
- 修改
Size
為:0x08004000
(只要不超過0x80000
即可)
- 修改
編譯,燒寫。
現象
觀察到下面的打印信息。
Hello Bootloader
Hello app
注意到2段程序都有NVIC_SetVectorTable
函數,可以設置將其注釋掉,然后做如下的實驗:
-
配置BootLoader中打開了定時器中斷,寫好對應的處理函數,例如在中斷響應中打印"Boot"。
-
配置User app中同樣打開了定時器中斷,寫好了對應的處理函數,例如在中斷響應中打印"User"。
觀察到,盡管兩邊都實現了中斷以及響應,但是都在執行着BootLoader
的中斷響應。
IAP實現總結
STM32的IAP方案實現需要在進行用戶程序之前加一段Bootloader
程序。
BootLoader
程序的作用就是:
- 要么:設置新的棧頂指針,直接跳轉到用戶程序。
- 要么:刪除原有的用戶程序,讀取
*.bin
文件數據並將數據重新寫入新的用戶程序。
對於用戶程序
相比普通的編程只需要做三步改動即可:
- 改變代碼存放的地址空間
- 改變中斷向量表
- 修改生成
*.bin
文件
實際上的IAP工程
本文介紹了簡單的IAP調整,沒有對文件的傳輸以及寫入。
傳輸有很多方法,可以是先把hex文件轉換為bin。再通過通訊方式完成文件傳輸、寫入;或者直接傳輸HEX文件,再在MCU進行解析(難)
我們討論對FLASH的讀寫。
對FLASH的讀寫
問:如何進行對STM32的Flash進行擦除和寫入操作。
答:STM32-HAL庫的讀寫非常方便,但是要注意讀寫地址與頁的關系
使用STM32的固件庫函數,只需調用幾個庫函數即可輕松解決,使用的固件庫為stm32f10x_flash.c文件,對Flash的操作過程簡要為:Flash解鎖Flash擦除Flash寫入Flash上鎖。(對Flash編程的更詳細操作參考STM32F10xxx閃存編程手冊)
//解鎖Flash
FLASH_Unlock();
FLASH_SetLatency(FLASH_Latency_2); //因為系統時鍾為72M所以要設置兩個時鍾周期的延時
// 擦除
for(i=0;i<240;i++)
{
if(FLASH_ErasePage(FLASH_ADDR+i*2048) != FLASH_COMPLETE) //一定要判斷是否擦除成功
return ERROR;
/*
說明:FLASH_ErasePage(uint32_t Page_Address)即為Flash擦除操作,按頁擦除,每頁2KB,Page_Address為頁的起始地址,如0x08000000是第一頁起始地址,0x08000800為第二頁起始地址,這里的操作擦除了0x08008000—0x0807ffff地址空間的Flash。
*/
}
#if 0
unsigned char buf[1024]; //假設待寫入的代碼數據
unsigned short temp; //臨時數據
for(i=0;i<512;i++)
{
temp = (buf[2*i+1]<<8) | buf[2*i]; //2個字節整合為1個半字
if(FLASH_ProgramHalfWord(ADDR,temp) != FLASH_COMPLETE) //判斷是否寫入成功
{
Return ERROR;
}
ADDR +=2; //地址要加2,因為每次寫入的是2個字節(1個半字)
}
/*
說明:因為STM32的Flash寫入為雙字節(1個半字)寫入,FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)函數即為對地址為Address寫入1個半字的Data,每次寫入完成后地址要加2。
*/
#endif
FLASH_Lock(); //Flash 上鎖,一個固件庫函數即可實現。
問:怎么進行PC指針的強制跳轉,跳轉時需要做些什么。
答:關閉中斷是可選的,設置棧頂指針是必須的。
- arm內核通用:
disable_irq(); //關閉所有中斷 enable_irq();//打開所有中斷
- 設置棧頂指針:
__set_MSP();
附錄:存儲容量與起止地址的關系式
以字節(Byte)為計算單位:存儲容量 = 儲存單元數 × 儲存字長(字節) 。
以Kb為計算單位:儲存單元數 × 儲存字長(字節)÷ 1024 。
首尾地址的差再加一即是儲存單元的個數,每一個儲存單元存放一個字節,可以得到總儲存單元一共有多少字節;
再根據1M = 1024 K = 1024 × 1024Byte
,那么就可以很容易地計算出三者的關系了。
例如:容量6K,首地址0x0000,末地址為?
解答:6K = 6 × 1024 Byte ,未地址 = 6 × 1024 + 000H = 1800H
附錄:確認代碼的鏈接地址
為什么在IDE中設置有關的地址呢,為了解答這個問題就需要我們知道:鏈接地址、運行地址、加載地址、存儲地址關系。
-
運行地址 與 鏈接地址:是類似的。
-
加載地址 與 存儲地址:是等價的。
鏈接地址
在程序編譯的時候,每個目標文件都是由源代碼編譯得到,最終多個目標文件鏈接生成一個最終的可執行文件,而鏈接地址就是指示鏈接器,各個目標文件的在可執行程序中的位置。
比如,一個可執行程序a.out由a.o、b.o、c.o組成,那么最終的a.out中誰在前、誰在中間、誰在結尾,都可以通過制定鏈接地址來決定。
鏈接地址是靜態的,在進行程序編譯的時候指定的。
注意,CPU不關心鏈接地址是物理地址還是虛擬地址。
總結:
1、鏈接地址是給編譯器用的,用來計算代碼中相關地址偏移的
2、只要和PC值相關的就是位置無關代碼(相對偏移),和PC無關的就是位置相關代碼(絕對值)
運行地址
程序實際在內存中運行時候的地址,比如CPU要執行一條指令,那么必然要通過給PC賦值,從對應的地址空間中去取出來,那么這個地址就是實際的運行地址。
運行地址是動態的,如果你將程序加載到內存中時,改變存放在內存的地址,那么運行地址也就隨之改變了。
注意,CPU同樣不關心運行地址是虛擬地址還是物理地址。
鏈接地址和運行地址區別
首先要區分開兩個概念:位置相關代碼和位置有關代碼。位置無關代碼:mov、b或bl等指令,在跳轉時,地址是PC+相對偏移量,這樣的指令沒有絕對地址,都是相對PC的偏移,這樣,及時運行地址和鏈接地址不一樣,也不影響實際代碼的執行。
位置相關代碼:ldr r0, =label
,這里的label
(標簽)實際就是鏈接地址,這條指令實質就是將標號的地址放到PC里實現跳轉,但是如果實際運行的地址和鏈接的地址不一樣,這樣就會由於跳轉的地址不是實際運行地址而出錯,這就叫做位置相關代碼。
條件相關代碼和條件無關代碼之間的本質區別就是:指令中相關地址是運行地址還是鏈接地址,如果是運行地址那么就是位置無關代碼,因為運行地址是變化的相對量;如果是加載地址,那么就是絕對量(鏈接時候指定好的),這就是位置相關代碼了。
總結,當鏈接指定的地址和實際運行的地址一樣的時候,鏈接地址==運行地址;當鏈接指定的地址和實際運行的地址不一樣的時候,如果整個代碼中的地址都是相對偏移量,那么整個程序仍然運行暢通無阻,否則,整個程序運行結果就會出錯(因為指定運行地址和實際運行地址不符)。
加載地址
每一個程序一開始都是存放在flash中的,而運行是在內存中,這個時候就需要從flash中將指令讀取到內存中(運行地址),flash的地址就是加載地址(存儲地址)。
指令在flash中存放的存儲地址,就是存儲地址。
所以,加載地址指的是將代碼從一個地址A搬移到地址B,這時候地址A就是加載地址。
附錄:STM32F0xx 實現中斷向量表重定義
在STM32F103等cortex-m3/m4內核的單片機上可以通過設置SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
該寄存器的值來實現中斷向量表的重定義。
在Coretext-M3與M4核中,在System Control Block中存在一個向量表偏移量寄存器 VTOR(
0xE000ED08
),系統產生中斷后,內核通過這個寄存器的值來找到中斷向量表的地址,進而執行中斷例程代碼。當然,此寄存器的值是可以修改的,它的默認值為0。實際上在大部分的M3和M4的工程中,一般都是在
SystemInit
函數中對此寄存器的值進行設置。
由於STM32F0XX采用的是M0核,它是沒有這個VTOR寄存器的,那么它又是怎么找到中斷向量表的地址的呢?
如何將中斷向量表的尋找位置從0x0800 0000
修改到0x0800 3000
(假設為APP的地址)? 我們重新回顧之前的分析,可以得出有2種方法:
- 修改寄存器VTOR的值(M3/M4 使用)
- 內存重映射(M0使用)
通過將SRAM重映射到地址0x0000 0000
,那么,M0系統產生中斷后,CPU還是從地址0x0000 0000
尋找中斷入口,但是,實際上不再是尋址0x0800 0000
,而是尋址0x2000 0000
,這么一來,接下來我們就只需要將中斷向量表整個拷貝到SRAM上,也就是0x2000 0000
上,就這樣,CPU就可以正常尋址中斷向量表了。
基於內存重映射實現的基本思想:
1、將中斷向量表放入到RAM的起始地址(只需要在應用程序中保留RAM其實地址的0x100大小不使用即可)。
2、在bootload中將應用程序的中斷向量表從Flash中拷貝到RAM中。
3、設置STM32F0xx中斷向量表位於RAM中,主要用到的寄存器如下:
具體實現代碼如下:
/*
* Function: void clock_init(void)
* Parameter: none
* Return: none
*/
int main(void)
{
memcpy((void*)0x20000000, (void*)APP_FLASHADDR, VECTOR_SIZE);
SYSCFG->CFGR1 |= 0x03; // SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_SRAM);
JumpToApp();
while (1);
}
memcpy((void*)0x20000000, (void*)APP_FLASHADDR, VECTOR_SIZE);
APP_FLASHADDR
是應用程序的起址地址,從這里開始的VECTOR_SIZE字節,存放是的應用程序的中斷向量表。VECTOR_SIZE
是指中斷向量表的大小,具體多大可以在startup.s
文件里計算得到。
VECTOR_SIZE 的計算:
根據startup.s
中的語句可以推導出來。
每一個DCD都代表一個中斷向量,例如:
DCD USART1_IRQHandler ; USART1
;這里的“USART1_IRQHandler"其實就是UART1中斷服務程序USART1_IRQHandler這個函數,同時,它也代表這個函數的入口地址。
這張表包括45個元素,每個元素是一個長度為4字節的地址。除了第一個地址是SP(堆棧指針)外,其它的地址都是某個中斷服務程序的入口地址。
針對本例,就應該是45*4=180(0xB4)個字節。
在執行完以上兩行代碼后,若發生中斷,CPU就會去SRAM(即0x2000 0000處)取中斷向量了,所以,以0x20000000作為起始地址之后的VECTOR_SIZE個字節就不能被改動了。為了達到這VECTOR_SIZE個字節不被修改的目的,如下兩種方法可以實現。
在工程文件內修改SRAM的起始地址及長度:
MDK
-Options
-Target
-Read/Write Memory Areas
- 勾選
IRAM1
:填入0x020000B4
(大於或等於0x020000B4
即可)
如果使用了分散加載文件,則在分散加載文件中修改SRAM的起始地址及長度也能達到目的。