Bootloader(引導加載程序
)的主要任務是引導加載并運行應用程序,我們的軟件升級邏輯也一般在BootLoader中實現(xiàn)。本文將詳細介紹BootLoader在單片機中的實現(xiàn),包括STM32、GD32、NXP Kinetis等等的所有單片機,因為無論是什么樣的芯片,它實現(xiàn)的邏輯都是一樣的。
注意,本篇文章主要是介紹實現(xiàn)一個嚴謹?shù)腂ootLoader需要掌握的基本知識和需要考慮的細節(jié),如果不注意一些細節(jié),應用層的代碼很可能會受到影響。
- 對于Linux的BootLoader來說其實也是一樣的,但它還需要初始化MMU、引導內核等等,這里我們不做過多的討論。有興趣的話,可以看我嵌入式Linux專欄中對于U-Boot源碼的分析。
1 基礎知識
1.1 NOR Flash和NAND Flash
NOR Flash和NAND Flash是兩種常見的非易失性存儲器(Flash Memory)類型,它們在內部結構、使用場景和性能方面存在一些顯著的區(qū)別。以下是它們之間的一些主要區(qū)別:
-
內部結構:
- NOR Flash: NOR Flash的內部結構類似于傳統(tǒng)的存儲器單元,支持隨機訪問。因此,它適用于需要快速隨機訪問的應用場景,例如執(zhí)行代碼(XIP,eXecute In Place)。
- NAND Flash: NAND Flash的內部結構更適合大容量、順序讀寫的應用場景。它采用頁和塊的結構,通常需要使用控制器來管理讀寫操作。
-
執(zhí)行方式(XIP - Execute In Place):
- NOR Flash: 由于其支持隨機訪問,NOR Flash 可以直接在存儲器中執(zhí)行代碼(XIP),無需將代碼加載到RAM中。
- NAND Flash: 通常需要將代碼從NAND Flash加載到RAM中才能執(zhí)行,因為它不太適合隨機訪問。
-
位反轉(Bit Inversion):
- NOR Flash: NOR Flash 通常不需要位反轉,即代碼可以直接在Flash中運行,無需進行位翻轉。
- NAND Flash: 由于NAND Flash內部的存儲單元是多級存儲,讀取時可能需要對數(shù)據(jù)進行位反轉,以確保正確的數(shù)據(jù)解析。
-
擦寫次數(shù):
- NOR Flash: NOR Flash 的擦寫次數(shù)相對較高,通常可以達到數(shù)百萬次,使其更適用于作為代碼存儲器。
- NAND Flash: NAND Flash 的擦寫次數(shù)相對較低,通常在幾千次到幾百萬次之間,取決于具體的 NAND Flash 類型。因此,對于頻繁擦寫的應用,可能需要考慮其他存儲器類型。
總體而言,選擇使用NOR Flash還是NAND Flash取決于具體的應用場景和需求。NOR Flash適用于需要隨機訪問和高擦寫次數(shù)的應用,例如嵌入式系統(tǒng)中的代碼存儲。NAND Flash適用于大容量存儲和順序訪問的應用,例如存儲大型文件和媒體內容。
注意事項:
對于STM32等單片機來說,它們都內置了NOR Flash,都是支持XIP的。但對于一些高端的單片機來說,如I.MX RT系列的MCU,在硬件上就需要自己接Flash,用戶可以接NOR也可以接NAND,對應了不同的引導方式,具體就需要查看芯片手冊了。
當然,對于單片機的絕大多數(shù)場景來說,代碼放在NOR Flash中跑的概率比較高,所以本篇文章介紹的也是基于NOR Flash的BootLoader的實現(xiàn)。
- 對于Linux來說,由于單單編譯出來的內核本身就很大,而NOR Flash的成本較高,所以更常見的是將程序存儲在一些NON-XIP的介質中,如EMMC、SD卡、NAND Flash,然后上電后將程序拷貝到SDRAM中運行。當然上電拷貝的程序也需要實現(xiàn),一般芯片會自帶一個很小的NOR Flash,里面存放一些固定的啟動代碼,當然不同廠商芯片的實現(xiàn)不同。
1.2 程序數(shù)據(jù)段
在程序中,通常會涉及到不同的段,這些段在內存中有著不同的屬性和用途。以下是一些常見的程序段及其作用:
-
代碼段(Text): 通常是只讀的
存儲程序的執(zhí)行代碼,包括可執(zhí)行指令和常量數(shù)據(jù)。在程序運行時,代碼段的內容會被加載到內存中,并且在執(zhí)行期間不可修改。
-
數(shù)據(jù)段(Data): 包括初始化數(shù)據(jù)(initialized data)和未初始化數(shù)據(jù)(uninitialized data)
存儲程序中的全局變量和靜態(tài)變量。初始化數(shù)據(jù)在程序啟動時會被初始化,而未初始化數(shù)據(jù)在程序啟動時不會被初始化,其初始值為零或未定義。
-
只讀數(shù)據(jù)段(Read-Only Data,rodata):
存儲常量數(shù)據(jù),如字符串常量、只讀常量等。在程序運行時,rodata段的內容不能被修改。
-
未初始化數(shù)據(jù)段(BSS):
存儲未初始化的全局變量和靜態(tài)變量。在程序啟動時,BSS段的內容被初始化為零或未定義的值。
-
棧(Stack):
存儲函數(shù)的局部變量和函數(shù)調用的狀態(tài)信息。棧是一個先進后出(FILO)的數(shù)據(jù)結構,用于支持函數(shù)調用和返回。
-
堆(Heap):
用于存儲動態(tài)分配的內存,例如通過
malloc()
或new
分配的內存。堆的管理通常由程序員負責,需要手動分配和釋放內存。
1.3 程序鏡像文件格式
對于不同的IDE來說,編譯后生成的程序的鏡像格式都不太一樣,常見的有以下幾種:
- AXF:用于基于ARM的微控制器。它包含可執(zhí)行代碼、數(shù)據(jù)和調試信息。AXF文件通常在開發(fā)和調試過程中使用
- HEX:由十六進制數(shù)及其對應的內存地址組成,可以將程序解析和編程到目標設備的內存中
- S19:以特定格式的ASCII字符表示二進制數(shù)據(jù)。S19文件包含數(shù)據(jù)和內存地址,常用于編程舊的微控制器和EEPROM
- ELF:包含可執(zhí)行代碼、數(shù)據(jù)和其他加載和執(zhí)行程序所需的信息,可用于調試、分析和部署到目標設備
- SREC:類似于S19的文件格式。它以ASCII字符表示二進制數(shù)據(jù),但遵循不同的格式
- BIN:BIN文件是直接包含可執(zhí)行機器代碼的二進制文件。它們通常用于以原始二進制格式存儲最終編譯的代碼。
不管什么格式,都是為不同下載器或者調試而服務的,經(jīng)過解析后下載進MCU內部FLASH的數(shù)據(jù)還是bin格式。
1.4 Flash相關函數(shù)需要放入RAM中執(zhí)行?
嵌入式Flash由多個塊(block
)組成,每個塊包含了在該塊內進行讀取、擦除和寫入時所需的電路。大多數(shù)閃存都存在一個限制:不允許在同一塊內在執(zhí)行擦/寫操作的同時,執(zhí)行讀取操作(比如CPU從Flash讀取指令運行代碼)。
舉個例子,如果有一段代碼在block1中執(zhí)行,那在這個代碼的執(zhí)行期間,不允許對block1中的任何部分進行擦/寫,這可能會導致讀寫沖突,進而引發(fā)錯誤。
以下是兩個解決辦法:
(1)從不同的Flash塊執(zhí)行命令
如果MCU有多個Flash塊,可以將擦/寫Flash的代碼放置在一個塊中,而將其它代碼或數(shù)據(jù)存儲在另一個塊中。
(2)從SRAM執(zhí)行Flash命令
如果MCU只有一個Flash塊,或用戶在每個可用塊內都要存放代碼和寫入,在這些場景中,可以將Flash命令移到SRAM中執(zhí)行。
- 總的來說就是大部分Flash都不支持
RWW
(Read while Writing
),注意Flash有RWW
特性表示支持一個塊擦寫的同時在另一個塊進行寫,我手上MK64的芯片的內置Flash是支持RWW
的。但大部分都是不支持RWW
的,所以最好還是把Flash相關函數(shù)鏈接到RAM中。
2 BootLoader實現(xiàn)實例
這里我將以NXP的Kinetis K系列芯片為例進行BootLoader的實現(xiàn),我使用的芯片為MK64FN1M0xxx12
,官方的開發(fā)板為FRDM-K64F
。
- 不同MCU的BootLoader實現(xiàn)原理都相同,希望大家能學到一些通用的知識,而不是特定于某個單片機的。
接下來我們就來在一個新的平臺中,如何一步一步地通過閱讀芯片手冊來實現(xiàn)BootLoader。
2.1 查看芯片的Flash映射
如下圖所示:
所以在我們使用的芯片中有自帶Flash,而且分為了兩個block,其中block 0的范圍是0x00000~0x7FFFF
;block 1的范圍是0x80000~0xFFFFF
,也就是兩個block各有512KB。另外,在上電后程序將從0地址取值運行。
2.2 Flash的擦寫
2.2.1 Flash擦寫的代碼
在前面的程序鏡像文件格式中,我們知道更新程序無非就是將原始的bin文件寫到Flash中,所以最重要的一步就是看看芯片內置Flash如何通過程序進行擦寫。
首先我們要知道,在寫Flash之前必須保證所有的內存為0xFF,這是因為寫操作只能將電平從1改為0,所以我們在寫入Flash之前,必須要先對Flash進行擦除(一般是以塊為單位進行)。
不同的芯片有不同的Flash控制器,這個一般在SDK中有提供相應的Flash驅動,這里不就做詳細地分析了。在MK64中,初始化完Flash后可以調用下面兩個函數(shù)來擦除和寫入Flash:
status_t mem_erase(uint32_t address, uint32_t length);
status_t mem_write(uint32_t address, uint32_t length, const uint8_t *buffer);
2.2.2 重定位Flash擦寫的代碼到SRAM中
在MK64內存映射中,我們知道MK64中有兩個block,每個block為512KB,就有前面所說的“Flash相關函數(shù)需要放入RAM中執(zhí)行”的問題,那么第一個解決方案(單獨將Flash函數(shù)放到第二個block上)其實不太實用,而且很麻煩。所以我們更多使用的是將Flash相關函數(shù)重定位到SRAM中執(zhí)行。
對于MK64的Flash來說,由于是內部的Flash,對于Flash的讀寫操作來說,只需要更改FTFE
寄存器即可。比如如果要擦除某個sector,只需要將這個sector的相關信息填充到FTFE
對應的寄存器中,然后將FTFE_FSTAT
寄存器的第7位CCIF
置1,即可根據(jù)我們填充的參數(shù)來啟動Flash操作。
所以我們實際上只需要填充好相應的Flash操作寄存器,然后將CCIF
位置為1,然后硬件會將CCIF
清零,然后我們再等待CCIF
置1即可。對于填充寄存器部分,由于沒有運行代碼,所以可以在Flash中運行,而對于操作CCIF
標志位的部分,我們需要將其重定位到SRAM中運行,以下是CCIF
位操作的代碼:
void flash_run_command(FTFx_REG_ACCESS_TYPE ftfx_fstat)
{
// clear CCIF bit
*ftfx_fstat = FTFx_FSTAT_CCIF_MASK;
// Check CCIF bit of the flash status register, wait till it is set.
// IP team indicates that this loop will always complete.
while (!((*ftfx_fstat) & FTFx_FSTAT_CCIF_MASK))
{
}
}
我們只要保證這個函數(shù)在SRAM中運行就行了,所以我們先將這個函數(shù)編譯出來,然后通過.map
內存映射文件,將去bin
文件反匯編objdump
,然后找到這個函數(shù)在匯編上的機器碼,我們這里保存為數(shù)組:
const static uint16_t s_flashRunCommandFunctionCode[] = {
0x2180, /* MOVS R1, #128 ; 0x80 */
0x7001, /* STRB R1, [R0] */
/* @4: */
0x7802, /* LDRB R2, [R0] */
0x420a, /* TST R2, R1 */
0xd0fc, /* BEQ.N @4 */
0x4770 /* BX LR */
};
然后我們再初始化Flash的時候,將這個機器碼拷貝到SRAM中即可,然后使用一個函數(shù)指針指向拷貝到的位置,就可以調用這個函數(shù)了:
// 聲明函數(shù)callFlashRunCommand(對應上面的flash_run_command)
static void (*callFlashRunCommand)(FTFx_REG_ACCESS_TYPE ftfx_fstat);
// 聲明保存二進制代碼的數(shù)組
#define kFLASH_ExecuteInRamFunctionMaxSizeInWords 16U
static uint32_t s_flashRunCommand[kFLASH_ExecuteInRamFunctionMaxSizeInWords];
// 拷貝二進制碼到數(shù)組中
memcpy((void *)&s_flashRunCommand, (void *)s_flashRunCommandFunctionCode, sizeof(s_flashRunCommandFunctionCode));
// 將callFlashRunCommand函數(shù)指針指向數(shù)組地址
callFlashRunCommand = (void (*)(FTFx_REG_ACCESS_TYPE ftfx_fstat))((uint32_t)s_flashRunCommand + 1);
這樣后續(xù)調用callFlashRunCommand
函數(shù),就和flash_run_command
函數(shù)是一個效果,但是callFlashRunCommand
就是在RAM中運行的了。前面說了Flash的所有操作,擦除、寫入等等函數(shù),最終都是會置CCIF
位來啟動Flash控制器進行操作,所以最后只要保證擦除、寫入等封裝好的函數(shù)最后調用的是callFlashRunCommand
函數(shù)啟動即可。
細心的人可能發(fā)現(xiàn)上面強制轉換時s_flashRunCommand
還加了1:
在ARM架構中,函數(shù)指針的值通常是奇數(shù)。這是因為ARM處理器使用Thumb指令集,而Thumb指令集中的指令是16位的,因此函數(shù)的地址通常是2的倍數(shù)。由于函數(shù)指針的最低位是用來指示Thumb指令集的狀態(tài)的,所以函數(shù)指針的值通常是奇數(shù)。
然而,實際上函數(shù)在內存中的存儲地址是偶數(shù)。因為Thumb指令集中的指令是16位的,而ARM處理器要求指令在內存中的地址是4的倍數(shù)。因此,當你想要獲取函數(shù)在內存中的真實地址時,你需要將函數(shù)指針的值加上1,以得到實際的偶數(shù)地址。
簡而言之,通過執(zhí)行 “+1” 操作,你可以將奇數(shù)的函數(shù)指針值調整為函數(shù)實際在內存中的偶數(shù)地址,以正確訪問函數(shù)的二進制代碼。這是在處理ARM函數(shù)指針時經(jīng)常需要考慮的一種調整。
當然,如果你的MCU還支持對Flash的數(shù)據(jù)進行緩存的話,那就還需要將清除緩存的函數(shù)重定位到SRAM中:
// 函數(shù)原型:這里不做詳細分析了,實際就是控制寄存器
void flash_cache_clear_command(FTFx_REG32_ACCESS_TYPE ftfx_reg)
{
*ftfx_reg = (*ftfx_reg & ~FMC_PFB01CR_CINV_WAY_MASK) | FMC_PFB01CR_CINV_WAY(~0);
*ftfx_reg |= FMC_PFB0CR_S_INV_MASK;
__ISB();
__DSB();
}
// 函數(shù)二進制
const static uint16_t s_flashCacheClearCommandFunctionCode[] = {
0x6801, /* LDR R1, [R0] */
0x22f0, /* MOVS R2, #240 ; 0xf0 */
0x0412, /* LSLS R2, R2, #16 */
0x430a, /* ORRS R2, R2, R1 */
0x6002, /* STR R2, [R0] */
0xf3bf, 0x8f6f, /* ISB */
0xf3bf, 0x8f4f, /* DSB */
0x4770 /* BX LR */
};
// 聲明函數(shù)指針
static void (*callFlashCacheClearCommand)(FTFx_REG32_ACCESS_TYPE ftfx_reg);
// 聲明數(shù)組
#define kFLASH_ExecuteInRamFunctionMaxSizeInWords 16
static uint32_t s_flashCacheClearCommand[kFLASH_ExecuteInRamFunctionMaxSizeInWords];
// 拷貝函數(shù)
memcpy((void *)s_flashCacheClearCommand, (void *)s_flashCacheClearCommandFunctionCode, sizeof(s_flashCacheClearCommandFunctionCode));
// 設置函數(shù)指針
callFlashCacheClearCommand = (void (*)(FTFx_REG32_ACCESS_TYPE ftfx_reg))((uint32_t)flashCacheClearCommand + 1);
// 調用例子
callFlashCacheClearCommand((FTFx_REG32_ACCESS_TYPE)&MCM->PLACR);
在每次擦除、寫完Flash之后,都需要調用這個函數(shù)flush一下cache。
- 注意:在我這個例子中使用機器碼的方式手動拷貝這些函數(shù)到SRAM中,實際上比較通用的做法是在鏈接腳本中定義一個鏈接在RAM中的段,同時把這個段鏈接到SRAM中,然后在函數(shù)聲明的地方加上
__attribute__((section("段名")))
,或者直接將整個Flash相關的函數(shù)所在的文件使用Exclude從Flash鏈接段中去除,然后在RAM中聲明。
2.3 MPU、低功耗和時鐘的操作
1、MPU
對于MPU來說,在我之前的文章中有詳細地介紹MPU內存保護單元詳解及例子,感興趣的可以看一下。
MPU是Cortex-M系列芯片都有的一個特性,它涉及到Cache的一些問題,如果使能的話,對于一些直接與硬件接觸的操作,如我們希望在BootLoader中實現(xiàn)通過USB獲取固件并升級,而USB一般使用了DMA,這樣的話數(shù)據(jù)的一致性會受到影響。當然我們可以使用CMSIS中的SCB_CleanDCache
等函數(shù)在執(zhí)行DMA之前清理一下D-Cache,但這些都太麻煩了,這里建議在BootLoader中直接關掉MPU。
2、低功耗
MK64芯片支持低功耗模式,為了防止在固件升級的過程中進入低功耗而引發(fā)Flash的未知狀態(tài),我們需要將低功耗模式關閉。當然有的芯片是自動開啟低功耗,有的則是沒有開啟低功耗,我的建議還是以防萬一,在上電時關閉一下低功耗。
MK64中通過SMC(System Mode Controller
,系統(tǒng)模式控制器)中的PMCTRL
中的RUNM
位控制低功耗模式:
我們在上電之后將這兩個位置為0即可,表示進入正常運行模式。
3、時鐘
我們在BootLoader中可能會使用到一些外設,我們可以在啟動時就將所有GPIO的時鐘打開。當然也可以在使用的時候再單獨打開,比如要使用串口,在串口初始化函數(shù)中初始化時鐘也行。
在MK64中通過SIM(System Integration Module
,系統(tǒng)集成模塊)的SCGC5
寄存器可以控制GPIOA~GPIOE
時鐘的使能。
2.4 BootLoader內存分配
首先我們要規(guī)定一下BootLoader的大小,假設我們給BootLoader留40KB的大小(需要保證編譯出來的BootLoader的bin文件小于40KB),那么在0~0xA000部分就存放BootLoader的代碼,從0xA000開始就存放應用程序的代碼。當然我們的程序大小不能超過block1,因為block1和block2的內存雖然在邏輯上是連續(xù)的,但是CPU無法從block1讀取一半指令,又從block2讀取一半指令執(zhí)行。如下圖所示:
2.5 鏈接腳本修改
對于APP來說,它的偏移現(xiàn)在在0xA000處,所以我們要在IDE中修改鏈接腳本,將程序鏈接到0xA000處,我這里使用的是IAR,只需要更改它的鏈接文件.icf
中的__ICFEDIT_intvec_start__
即可(變量名可能不同,具體參考你目錄下的鏈接腳本):
define symbol __ICFEDIT_intvec_start__ = 0x0000A000; /*-User Application Base-*/
對于Keil和IAR,我同樣寫過文章分析其鏈接腳本的格式,大家可以參考一下:
- IAR中ICF鏈接文件詳解和實例分析
- KEIL中SCF分散加載鏈接文件詳解和實例分析
2.7 上下文保持一致
我們必須保證程序在進BootLoader前是什么狀態(tài),在進APP前就應該是什么狀態(tài)。
我的真實經(jīng)歷是,同事在BootLoader中使用UART升級,打開了UART中斷,但退出BootLoader時沒有關閉這個中斷。于是在APP的初始化函數(shù)中,將數(shù)據(jù)段復制到RAM中的時候,這個中斷就會影響拷貝的值。比如你在程序中初始化了一個char *a = "123";
,但實際上a
的值可能為1a3
。
詳細的步驟如下:
1、清理Flash的緩存:一般Flash有一個flush
類似的函數(shù),保證之前的Flash操作都執(zhí)行完畢
2、清除所有中斷標志位:主要是控制NVIC寄存器,參考代碼如下:
__STATIC_INLINE void NVIC_ClearEnabledIRQs(void)
{
NVIC->ICER[0] = 0xFFFFFFFF;
NVIC->ICER[1] = 0xFFFFFFFF;
NVIC->ICER[2] = 0xFFFFFFFF;
NVIC->ICER[3] = 0xFFFFFFFF;
NVIC->ICER[4] = 0xFFFFFFFF;
NVIC->ICER[5] = 0xFFFFFFFF;
NVIC->ICER[6] = 0xFFFFFFFF;
NVIC->ICER[7] = 0xFFFFFFFF;
}
__STATIC_INLINE void NVIC_ClearAllPendingIRQs(void)
{
NVIC->ICPR[0] = 0xFFFFFFFF;
NVIC->ICPR[1] = 0xFFFFFFFF;
NVIC->ICPR[2] = 0xFFFFFFFF;
NVIC->ICPR[3] = 0xFFFFFFFF;
NVIC->ICPR[4] = 0xFFFFFFFF;
NVIC->ICPR[5] = 0xFFFFFFFF;
NVIC->ICPR[6] = 0xFFFFFFFF;
NVIC->ICPR[7] = 0xFFFFFFFF;
}
- 執(zhí)行上面兩個函數(shù)即可,但上面的代碼是基于Cortex-M4或Cortex-M7內核的,其它內核自行參考內核手冊的NVIC章節(jié)編寫。
3、設置VTOR為默認值
kDefaultVectorTableAddress = 0
SCB->VTOR = kDefaultVectorTableAddress;
4、恢復時鐘
比如程序中用到了USB的話,系統(tǒng)時鐘速率在之前應該配置地很高,這里需要恢復最初始的時鐘配置。同時如果前面開啟了所有GPIO的時鐘的話,這里也要全部關閉。比如使用了UART,打開了對應GPIO的時鐘的話,需要在此關閉。
對于MK64來說,如果打開USB的話,配置時鐘的時候還使能了這些位,都需要關閉。
5、使能中斷
這和我們剛剛清理的中斷標志位不一樣,在上電后默認總中斷的相應是使能的,為了進一步處理中斷請求并繼續(xù)系統(tǒng)的正常運行,需要重新使能系統(tǒng)對于中斷的相應。
__enable_irq()
6、內存屏障
最后我們確保指令和數(shù)據(jù)的一致性以及正確的執(zhí)行順序,這里是保證在APP跳轉之前我們的這些設置都起作用了。當然這里的__DSB
可以省略,因為我們前面更改的都是強有序內存(這些系統(tǒng)內存即使不使能MPU也是強有序的)。這里更多地考慮的是平臺之間的兼容,如代碼從Cortex-M4移動到Cortex-M7一樣可以使用。
__ISB();
__DSB();
2.8 獲取SP和PC
在更新完固件后,我們需要跳轉到位于0xA000處的APP,現(xiàn)在的問題是,APP的堆棧指針是什么,應該將PC指針設置為多少才能跳轉到APP中。
獲取SP和PC
如下圖所示,實際上固件的0地址存放的就是堆棧指針,在上電后硬件將設置MSP(主堆棧指針)的值為bin文件0偏移處的值。
我們再來看一下APP的.s
啟動文件:
可以看到第一個果然是堆棧指針,這里的CSTACK
可以在鏈接腳本中指定。同時我們發(fā)現(xiàn)第二個向量是Reset_Handler
函數(shù)的地址,我們將PC值設置為Reset_Handler
的值不就可以跳轉到APP了嗎?獲取這兩個值的函數(shù)如下:
#define APP_VECTOR_TABLE ((uint32_t *)0xA000)
static void get_user_application_entry(uint32_t *appEntry, uint32_t *appStack)
{
*appEntry = APP_VECTOR_TABLE[1];
*appStack = APP_VECTOR_TABLE[0];
}
2.9 跳轉APP
前面獲取了SP(appStack
)和PC(appEntry
),這里就派上用場了。但在跳轉APP之前,我們還需要做兩件事:
1、設置堆棧指針
因為前面說的是上電的時候硬件會設置SP,所以僅僅設置的是BootLoader中的SP,對于APP的堆棧指針需要我們自己設置:
__set_MSP(appStack);
__set_PSP(appStack);
實際上我們只需要設置MSP就行了,PSP如果使用了RTOS自然會設置。但這里我們還是給PSP一個默認值。
2、設置向量表地址
同樣地,上電后硬件設置的是BootLoader的向量表,我們要將其設置為APP的向量表位置:
#define APP_VECTOR_TABLE ((uint32_t *)0xA000)
SCB->VTOR = (uint32_t)APP_VECTOR_TABLE;
最后我們就可以跳轉到APP了,聲明一個函數(shù)指針,然后指向Reset_Handler
,然后執(zhí)行即可更改PC指針為Reset_Handler
:
static void (*farewellBootloader)(void) = 0;
farewellBootloader = (void (*)(void))appEntry;
farewellBootloader();
2.10 BootLoader完整流程
下面來列舉一下BootLoader的實現(xiàn)步驟:
1、退出低功耗:如果芯片支持的話,需要關閉
2、關閉MPU:建議關閉,否則代碼中需要兼容Cache
3、開啟所有GPIO的時鐘:非必要,可在用到具體某個外設時再打開
4、配置系統(tǒng)時鐘樹:建議使用芯片內部的時鐘作為主時鐘源
5、初始化Flash:包括Flash參數(shù)的配置、Flash時鐘的配置、拷貝代碼到SRAM
6、更新固件
實際上就是可以通過UART、SDCARD、USB等各種外設(記得初始化這些外設的引腳)獲取最新的固件,然后調用mem_erase
和mem_write
函數(shù)將固件寫入Flash中。
7、清理上下文:上下文保持一致
8、獲取SP和PC值,設置MSP/PSP/VTOR
9、跳轉APP
3 待優(yōu)化
另外,對于固件升級來說,還有兩點需要考慮。
1、可靠升級:如果在固件升級的過程中,已經(jīng)把0xA000處之前的APP擦掉了,準備寫入新的固件,但此時如果突然設備斷電,那么就沒有程序了,原來的程序也不能運行,所以我們還需要保證BootLoader的可靠。
2、加密:現(xiàn)在反匯編的技術已經(jīng)很成熟了。我最近使用的I.MX RT1170直接硬件自帶了OTFAD引擎,可以邊解密AES-128加密的代碼邊運行,可見加密的重要性。而對于這些普通的MCU來說,我們也可以自己設計加密算法。對于MK64來說,有AES解密的引擎,但沒有這個功能的MCU也沒關系,我們也可以自己解密??梢詤⒖嘉覍懙膬善P于AES的文章:
- AES加密(1):AES基礎知識和計算過程
- AES加密(2):AES代碼實現(xiàn)解析
我這里給大家提供一個思路,下圖是我在MK64平臺中實現(xiàn)的BootLoader:
這里我使用了AES-128加密,從串口/SDCARD直接邊讀取加密固件,邊解密原始固件到0x80000開始處的位置。同時我在APP的頭字段中包含一些字段(很多中斷向量表都是空的,可以用來存儲一些Boot信息),其中包括CRC字段,解密完后可以用于校驗固件的合法性。校驗完后將APP從0x80000處拷貝到0xA000處,這樣就也保證了可靠升級。最后再校驗一次0xA000處的CRC,就表示升級成功了。文章來源:http://www.zghlxwxcb.cn/news/detail-786836.html
4 總結
本文介紹了對于實現(xiàn)一個BootLoader需要考慮的方面,其實本文更多的是想傳遞一種嚴謹?shù)乃枷?,而不是從網(wǎng)上隨便復制一段代碼就去用。在你嚴謹?shù)刈鍪碌耐瑫r,就會考慮到更多的東西,比如這里你可能還會學到MPU、低功耗、內存屏障等知識,正是對這一個個知識的好奇、深入理解并積累,同時保持嚴謹?shù)膽B(tài)度,你才會在不知不覺中成為“高手”。文章來源地址http://www.zghlxwxcb.cn/news/detail-786836.html
到了這里,關于單片機(STM32,GD32,NXP等)中BootLoader的嚴謹實現(xiàn)詳解的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!