文章目錄
- 二、Bootloader加載啟動App代碼講解
二、Bootloader加載啟動App代碼講解
代碼詳細解析:
typedef void (*pFunction)(void);static void DrvInit(void)
{RS485DrvInit();DelayInit();SystickInit();
}#define RAM_START_ADDRESS 0x20000000
#define RAM_SIZE 0x10000static void BootToApp(void)
{uint32_t stackTopAddr = *(volatile uint32_t*)APP_ADDR_IN_FLASH; if (stackTopAddr > RAM_START_ADDRESS && stackTopAddr < (RAM_START_ADDRESS + RAM_SIZE)) //判斷棧頂地址是否在合法范圍內{__disable_irq();__set_MSP(stackTopAddr);uint32_t resetHandlerAddr = *(volatile uint32_t*) (APP_ADDR_IN_FLASH + 4);/* Jump to user application */pFunction Jump_To_Application = (pFunction) resetHandlerAddr; // int *p = (int *)0x8003145/* Initialize user application's Stack Pointer */Jump_To_Application();}NVIC_SystemReset();
}
其中這里面設計到了指針以及函數指針需要詳細理解一下,不然這一段是看不懂的。
深入理解C語言內存空間、函數指針(三)(重點是函數指針)
首先就是一個函數指針:typedef void (*pFunction)(void);
先自己分析一下:
volatile uint32_t*
這里表示APP_ADDR_IN_FLASH
是一個int類型指針變量,但是前面又一個*
是什么意思,是指針的解引用嗎 去除這個地址里面存儲的內容,然后將存儲的地址在賦值給stackTopAddr ,volatile uint32_t*
這個地方最核心的意思是告訴編譯器,APP_ADDR_IN_FLASH
這只是首地址,還需要往后在找三個地址,因為這個表示的是4個字節32位。
執行步驟?:
-
?強制類型轉換?:
(volatile uint32_t*)APP_ADDR_IN_FLASH
→ 將常量地址APP_ADDR_IN_FLASH
轉換為 ?指向volatile uint32_t
的指針。-
?**
volatile
**?:告知編譯器該地址內容可能被外部因素(硬件、中斷等)異步修改,禁用優化(如緩存值到寄存器)。 -
?**
uint32_t*
?:指針類型為 ?32位無符號整數指針**,表示訪問從該地址開始的 ?連續4字節內存?(因uint32_t
占4字節)。
-
-
?解引用操作?:
*(...)
→ 讀取指針指向的 ?4字節數據?(即APP_ADDR_IN_FLASH
地址處的實際值),而非地址本身。 -
?賦值?:
將讀取到的32位值存入變量stackTopAddr
。
volatile
的核心作用:強制直接內存訪問。
volatile
的本質是禁用編譯器優化,確保每次對變量的讀寫都直接操作內存,而非使用寄存器緩存副本。
- 編譯器優化問題?:
編譯器默認會優化代碼,比如將頻繁訪問的變量緩存在寄存器中(減少內存讀寫開銷)。
但對硬件寄存器、中斷共享變量等,這種優化會導致程序無法感知外部實時變化。 - ?
volatile
的解決方案**?:
通過聲明volatile
,強制編譯器每次訪問變量時都從內存地址重新加載值(讀)或立即寫入內存(寫)
寄存器緩存原理?
編譯器(如GCC/Clang的-O2
/-O3
級別優化)會將頻繁訪問的變量緩存在CPU寄存器中,減少內存訪問次數。此時,代碼實際操作的是寄存器的副本而非內存中的原始變量。
優化失效場景?
當變量被外部異步修改時(如中斷、多線程、硬件寄存器),寄存器的副本不會同步更新,導致程序邏輯錯誤。
?場景? | ?后果? | ?案例? |
---|---|---|
?硬件中斷修改變量? | 主循環無法感知新值,死循環卡死 | OV5640攝像頭讀取卡死在while(a==0) |
?多線程共享變量? | 線程間數據不一致,邏輯錯誤 | 線程A緩存舊值,線程B更新無效 |
?內存映射硬件操作? | 讀取過時硬件狀態,驅動失效 | 傳感器數據寄存器讀取延遲 |
因此需要強制內存訪問
?volatile
關鍵字?
聲明變量為volatile
,強制每次訪問都從內存讀取/寫入
-
作用原理?:禁用寄存器緩存,生成直接訪問內存的指令
-
?適用場景?:中斷標志位、多線程共享變量、硬件寄存器映射變量
因此在之前開發過程中遇到的按鍵板和顯示板UART通信的時候,需要加上獲取按鍵值的變量加上關鍵字volatile
,這是因為這個變量是在中斷中直接解析的,導致頻繁的訪問,編譯器就會針對這個地方進行一次優化,就是上面說的“代碼實際操作的是寄存器的副本而非內存中的原始變量”,因此為了保證每次都是最新的按鍵值,所以我們要強制內存訪問。
繼續解析代碼:
uint32_t stackTopAddr = *(volatile uint32_t*)APP_ADDR_IN_FLASH;
?APP_ADDR_IN_FLASH
**?:APP 的起始地址(即向量表基地址)
這個地址是我們自己設定的,正常來說是從APP_ADDR_IN_FLASH 0x08000000,但是由于前面的空間我們預留給了BOOT,并且還是給了BOOT12KB的空間,因此APP的開始地址就是APP_ADDR_IN_FLASH 0x8003000。
在正常啟動的時候我們首先就是獲取棧頂地址,因此就是上面這段代碼,讀取出棧頂地址,這是ARM內核啟動的流程,必須也只能按照這樣執行。
得到的棧頂地址,什么是棧頂地址,就是這個工程有效使用棧空間最頂的地址,棧空間是什么空間?是運行我們函數的空間,存儲全局變量什么的,掉電易失。SRAM。
棧空間(Stack)是程序運行時的重要內存區域,主要用于存儲與函數調用、中斷處理等相關的臨時數據。
- ?函數調用上下文?
- ?返回地址(LR)??:子函數執行完畢后需返回父函數的位置,由鏈接寄存器(LR,X30)保存,調用子函數前會壓入棧中。
- ?幀指針(FP)??:指向當前函數棧幀的底部(高地址),用于界定函數棧邊界,通常由X29寄存器保存,入棧后形成函數調用鏈。
- ?調用者寄存器?:部分需跨函數保留的寄存器(如ARMv7的R4-R11,ARM64的X19-X28)會在子函數中壓棧保護,防止被覆蓋。
- ?局部變量?
- 函數內部定義的非靜態局部變量?(如
int a;
)存儲在棧幀中,生命周期僅限于函數執行期間,函數返回后自動釋放。 - 示例:函數內數組、結構體等臨時變量。
- ?函數參數?
- ?超出寄存器容量的參數?:ARM調用約定中,前幾個參數通過寄存器傳遞(如ARM64的X0-X7),超出部分會壓入調用者的棧空間。
- ?可變參數函數?:如
printf()
的多余參數需通過棧傳遞。
- ?中斷/異常上下文?
- 發生中斷或異常時,CPU自動將關鍵寄存器(PC、LR、CPSR等)?? 壓入當前模式棧(如IRQ模式棧),用于恢復現場。
- 中斷服務程序(ISR)中的局部變量也占用棧空間。
- ?臨時數據與中間結果?
- 編譯器生成的臨時計算結果?(如復雜表達式中間值)。
- ?寄存器溢出?:當寄存器不足時,部分中間變量暫存到棧中
ARM棧空間的核心作用是支撐函數調用鏈與臨時數據存儲,具體包括:函數返回地址(LR)、幀指針(FP)、局部變量、多余參數、中斷上下文及編譯器臨時數據。其設計遵循架構規范(如AAPCS),通過棧指針(SP)和幀指針(FP)協同管理棧幀邊界。開發中需警惕棧溢出風險,尤其在資源受限的嵌入式系統中。
棧頂地址(Stack Top Address)是計算機科學中棧(Stack)這一數據結構的關鍵概念,指棧中最后一個被插入元素的內存地址。棧是一種后進先出(LIFO)的線性表,所有操作(插入/刪除)僅在棧頂進行。
- 操作唯一性?:所有入棧(
PUSH
)和出棧(POP
)操作均通過修改棧頂地址完成:- ?入棧?:棧頂地址向低地址移動 → 新元素存入新地址。
- ?出棧?:棧頂地址向高地址移動 → 釋放當前元素。
- ?核心功能?:
- 存儲函數調用的返回地址、參數、局部變量;
- 實現遞歸和中斷處理時的上下文保護。
接著就是驗證 APP 棧頂指針合法性。
然后就是關閉中斷,防止跳轉過程被干擾
__set_MSP(stackTopAddr);
重設主棧指針(MSP)?
?**作用:將 APP 的初始棧頂地址保存到CPU的 SP 寄存器里面,確保 APP 從正確的棧空間啟動。
獲取復位處理函數地址
這一步也是CPU的基操,啟動的第二個流程,第一是獲取棧頂地址,第二就是復位函數地址,
uint32_t resetHandlerAddr = *(volatile uint32_t*) (APP_ADDR_IN_FLASH + 4);
同樣的思路,我們利用這一行代碼獲取到復位函數的入口地址了。
但是!!!! 警惕 警惕
現在我們只是自己知道resetHandlerAddr
這個里面存儲是復位函數的地址,但是編譯器不知道,現在這個里面只是存儲的復位函數入口地址。
并且沒有初始化原因是:
值由硬件預設,非軟件生成?
- Cortex-M 架構規定,應用程序的復位函數地址 ?由編譯器鏈接時確定,并固定在 Flash 的向量表偏移 4 字節處(即
APP_ADDR_IN_FLASH + 4
)。 - 該地址是只讀的硬件預設值,?非運行時動態生成,因此無需軟件初始化。
我們需要做的就是讓編譯器知道這個入口地址是函數的地址,
而復位函數的類型是 void ()(void);
因此我們前面聲明的函數指針就用上了。
typedef void (*pFunction)(void);
首先:
pFunction Jump_To_Application
表示的是我們定義一個函數,這個函數類型是pFunction
,也就是void ()(void);
符合復位函數的類型,那么我們后續我們就可以直接使用Jump_To_Application()
,
為什么能直接這樣使用Jump_To_Application()
?
是不是我可以理解成這樣就是對函數指針的解引用,區別于變量指針的解引用。
函數指針的調用 Jump_To_Application()
是 C 語言標準允許的語法糖?(Syntactic Sugar)。它等價于顯式解引用形式 (*Jump_To_Application)()
,但更簡潔直觀。
- 函數指針類型
pFunction
必須與Reset_Handler
的簽名完全匹配?(如void (*)(void)
),否則會因參數傳遞或棧布局錯誤導致硬件異常。 - ?錯誤示例?:若
Reset_Handler
實際需要參數,但pFunction
定義為無參數類型,調用時將破壞棧平衡。
但是現在還缺少一個地址,這個地址就是入口地址,前面我們獲取了入口地址是resetHandlerAddr。
但是我們還是需要將這個地址給強制轉換成函數類型也就是
(pFunction) resetHandlerAddr;
這樣做的目的是 原本我們只是知道這是一個0x8003145
,并不能說明這是一個地址,因此我們需要將他變成一個地址,但是指針又需要類型,而我們這個指針就是函數類型的,畢竟是函數入口地址,不是函數類型的指針是什么指針? 因此就是一個強轉。
最后綜合起來就是這行代碼
pFunction Jump_To_Application = (pFunction) resetHandlerAddr; // int *p = (int *)0x8003145/* Initialize user application's Stack Pointer */Jump_To_Application();
至此已經跳轉到APP。
文章源碼獲取方式:
如果您對本文的源碼感興趣,歡迎在評論區留下您的郵箱地址。我會在空閑時間整理相關代碼,并通過郵件發送給您。由于個人時間有限,發送可能會有一定延遲,請您耐心等待。同時,建議您在評論時注明具體的需求或問題,以便我更好地為您提供針對性的幫助。
【版權聲明】
本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議。這意味著您可以自由地共享(復制、分發)和改編(修改、轉換)本文內容,但必須遵守以下條件:
署名:您必須注明原作者(即本文博主)的姓名,并提供指向原文的鏈接。
相同方式共享:如果您基于本文創作了新的內容,必須使用相同的 CC 4.0 BY-SA 協議進行發布。
感謝您的理解與支持!如果您有任何疑問或需要進一步協助,請隨時在評論區留言。