STM32內存映射和引導加載過程的詳細說明。
儘管下面的所有示例均適用於STM32F446,但基本原理適用於大多數MCU。
STM32Cubemx生產STM32F446RETX_FLASH.LD,我們將作為所有探索的參考。
鏈接器獲取編譯器生成的對象文件並生成最終的編譯輸出,在我們的情況下是.ELF二進製文件。鏈接器始終使用鏈接腳本文件;即使您不指定一個,也會使用默認腳本。
重要的是要注意,鏈接器腳本僅根據MCU規範描述內存,並且不會改變任何硬件存儲器。
如果不了解內存結構,我們無法談論引導加載程序過程。實際上,引導加載程序的目的是將內存準備在狀態下,準備執行我們的應用程序的main()方法。
STM32F4中的程序內存,數據存儲器,寄存器和I/O端口是在相同的線性地址空間內組織的。
...
+ -- -- 0x2001FFFF -- -- +
| |
| RAM |
| |
+ -- -- 0x20000000 -- -- +
| ... |
+ -- -- 0x1FFF7A0F -- -- +
| |
| System |
| |
+ -- -- 0x1FFF0000 -- -- +
| ... |
+ -- -- 0x081FFFFF -- -- +
| |
| Flash |
| |
+ -- -- 0x08000000 -- -- +
| ... |
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- +別名內存指向閃存,系統或RAM內存,具體取決於BOOT0引腳。默認情況下它是閃光燈。
目前,兩個空間對我們來說是最感興趣的空間:
但是,實際上,功能部門可能會有所不同。例如,您可能需要將程序數據加載到RAM中,反之亦然。
內存結構反映在鏈接文件中。 Flash從ORIGIN = 0x8000000開始,RAM在ORIGIN = 0x20000000 。
// linker file
MEMORY
{
RAM ( xrw ) : ORIGIN = 0x20000000 , LENGTH = 128 K
FLASH ( rx ) : ORIGIN = 0x8000000 , LENGTH = 512 K
}請注意,這只是一個默認的實現,您可以在遵守MCU內存規範後立即輕鬆拆分閃光燈和RAM並添加自己的塊或部分。
然後,內存在鏈接文件中將內存分為各節,每個都有其地址和目標(例如,閃存或RAM)。
// linker file
SECTIONS
{
...
. text :
{
...
} > FLASH
...
}要探索。 elf符號表,我們將使用無臂 - abi objdump命令,然後通過其地址對所有條目進行排序:
arm-none-eabi-objdump -t stm32-boot-explained.elf | sort該程序在手冊中的詳細信息中產生輸出:
Address Flag_Bits Section Size Name編譯的程序代碼在閃存中進入此部分。
本節從閃存的ORIGIN地址開始, _etext指向該部分的最後一個地址。
08000000 l d . text 00000000 . text
08000 c00 g . text 00000000 _etext我們可以看到我們在Bootloader文件中使用的功能的位置:
080005 c8 g F . text 00000048 __libc_init_array
08000610 g F . text 000000d 8 main
080006e8 g F . text 00000064 Reset_Handler
08000b c4 g F . text 00000024 SystemInit該部分位於RAM內存中,並保存所有具有定義值的變量。地址範圍從_sdata到_edata 。
20000000 g . data 00000000 _sdata
20000010 g . data 00000000 _edata讓我們檢查一下MAIN.C的哪些變量位於本節中:
20000000 l O . data 00000004 static_data_int
20000008 g O . data 00000008 data_double實際值必須通過bootloader腳本填充,通過在地址_sidata上複製閃存的數據:
08000 c08 g * ABS * 00000000 _sidata塊啟動符號RAM數據部分,用於所有變量,而沒有分配的值。引導加載程序會考慮將該塊的數據設置為0 。
內存範圍從_sbss到_ebss 。
20000010 g . bss 00000000 _sbss
20000044 g . bss 00000000 _ebss在本節中,來自main.c的所有變量都存在:
2000002 c l O . bss 00000004 static_bss_int
20000030 g O . bss 00000008 bss_double
20000038 g O . bss 00000008 bss_my_struct
20000040 g O . bss 00000004 bss_my_union讓我們檢查GDB :
(gdb) p bss_double
$1 = 0
(gdb) p & bss_double
$2 = (double * ) 0x20000030 < bss_double >一旦為變量分配了一些值,它的地址就不會更改,並且仍然保留在.bss中:
(gdb) p bss_double
$3 = 86
(gdb) p & bss_double
$4 = (double * ) 0x20000030 < bss_double > _end上方的所有RAM內存,直到_estack專用於堆和堆疊內存。
20000048 g . _user_heap_stack 00000000 _end
20020000 g . isr_vector 00000000 _estack _estack地址計算為ORIGIN(RAM) + LENGTH(RAM) 。因此,對於128KB RAM:
_estack = 0x20000000 + 128 * 1024 # dec
= 0x20000000 + 0x20000 # hex
= 0x20020000堆棧是一種LIFO結構,從_estack開始,然後向下生長。最小堆棧大小在鏈接文件中定義為_Min_Stack_Size 。堆棧內存將自動釋放。
+ -- -- 0x20020000 -- -- + < -- _estack
| |
| Stack |
| |
+ - - 0x2001FC00 - - + < -- - _Min_Stack_Size
| |
+ -- -- 0x200 sssss -- -- + < -- $msp register
| |
| |
| Free space |
| |
| |
+ -- -- 0x200 hhhhh -- -- + < -- Actual heap end
| |
| Heap |
| |
+ - - 0x20000248 - - + < -- + _Min_Heap_Size
| |
+ -- -- 0x20000048 -- -- + < -- _end堆又從_end開始,然後在malloc請求時向上增長到_estack - _Min_Stack_Size 。
我們有一個在main.c中定義的stack_int變量。初始化後,讓我們檢查GDB的地址:
(gdb) p & stack_int
$1 = (unsigned short * ) 0x2001ffd6
(gdb) p $msp
$2 = (void * ) 0x2001ffd0當變量高於$msp時,它與我們的期望匹配。
值得一提的是,在嵌入式C應用程序中常用的標準C庫實現是Newlib。該庫需要實施某些系統特定功能。 STM32Cubemx使用必要的默認實現生成syscalls.c文件。
sbrk調用也增加了程序數據空間。 malloc正在使用此功能分配更多的堆內存。您可能會在sysmem.c中找到STM32Cubemx生成的實現。它只允許堆從_end增長到_estack - _Min_Stack_Size 。
現在,當我們了解MCUS內存時,讓我們與GDB連接到我們的程序,這是我們認為的第一個輸出:
...
Reading symbols from ./stm32-boot-explained.elf...
Remote debugging using localhost:61234
Reset_Handler () at %PATH%/bootloader.c:25
25 void Reset_Handler () {
(gdb) info registers
...
pc 0x80006e8 0x80006e8 < Reset_Handler >
... Reset_Handler以某種方式被確定為我們應用程序的引導點。
您可能已經註意到鏈接文件中有一個ENTRY說明:
// linker file
ENTRY ( Reset_Handler )實際上,它在.elf文件標題中保存對Reset_Handler函數地址的引用:
arm-none-eabi-objdump -t -f stm32-boot-explained.elf | grep " start address "
start address 0x080006e9查看我們的符號表, Reset_Handler在flash .text部分中存在,就像所有其他功能一樣:
080006e8 g F . text 00000064 Reset_Handler地址是對頁面對齊的,這就是為什麼.Fell標頭和實際地址之間存在不匹配的原因。
儘管鏈接器主要使用此信息來驗證代碼中的入口點符號的存在,但它對MCU沒有實際含義。
根據STM32規範,CPU從地址0x00000000獲取了堆棧最高的_estack值,然後從啟動內存開始執行代碼,從0x00000004開始執行。
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- +這正是上面提到的別名內存的位置。使用默認配置,當PIN值BOOT0 = 0時,它與0x8000000的閃存塊相似。
基於BOOT0和BOOT1其他選項包括帶有嵌入式引導加載程序或RAM內存的系統內存。嵌入式引導加載程序在生產過程中由ST編程,並且不在本手冊的範圍內。
但是Reset_Handler有一個地址0x08000524它不是閃存的開始,MCU如何找到Bootstrap方法?
這是向量表發揮作用的地方。
08000000 g O . isr_vector 000001 c4 Vector_Table MCU將內存的開頭視為矢量表,其中包含指示各種中斷服務例程和基本啟動功能(包括Reset_Handler的指針。請諮詢規格,以查看MCU期望從0x00000000加載的確切表結構。實際表必須由引導程序填充。
| 地址 | 姓名 |
|---|---|
| 0x00000000 | 預訂的 |
| 0x00000004 | 重置處理程序 |
| 0x00000008 | 不可掩蓋的中斷 |
| 0x00000012 | 硬故障 |
| ... | 其他中斷 |
此Reset_Handler是一個引導加載程序功能,可用於許多應用程序,從特定於安全性的任務到固件自動更新。在這裡,我們將探索基本的默認實現,以了解其與MCU內存的交互。
默認情況下, Reset_Handler方法是在STM23Cubemx提供的startup_stm32f436xx.s ASM文件中定義的。為了清楚起見,該項目中的實際bootloader.c實現是在C中編寫的。
請注意,可以在C代碼中訪問鏈接器腳本中定義的變量:
extern uint32_t _estack ;這樣可以很容易地複制ASM版本。
然後,最小的加載過程可以分為以下步驟:
SystemInit()函數).data段初始化器從閃存複製到RAM.bss段__libc_init_array()函數)main()函數)對於步驟#1和#4,STM32Cubemx提供功能實現,您可以在System_stm32f4xx.c中查看詳細信息。
該項目具有啟動STM32所需的最小文件集。您可能想自己嘗試一下,以檢查無臂 - abi-objdump的輸出,然後使用GDB逐步進行。
構建項目需要ARM GNU工具鏈。
建議使用ARM-NONE-EABI-GCC , STM32_PROGRAGREMMER_CLI和ST-LINK_GDBSERVER工具安裝一個多合一的STM32CubeClt命令工具包。
使用CMAKE構建項目:
mkdir build ; cd build
cmake ../ -DPROGRAMMER_CLI=/opt/ST/STM32CubeCLT_1.15.1/STM32CubeProgrammer/bin
-DGDB_SERVER=/opt/ST/STM32CubeCLT_1.15.1/STLink-gdb-server/bin
make VERBOSE=1請注意,最後一個命令是鏈接階段。如果我們剝離所有其他編譯器標誌,則該命令看起來如下。這是指示鏈接器使用我們的鏈接腳本的地方。
arm-none-eabi-gcc
...
-T " %SCRIPT_DIR%/STM32F446RETx_FLASH.ld "
...
" %OBJ_DIR%/%OBJECT_NAME%.c.obj "
...
-o stm32-boot-explained.elfSTM32_PROGRAGMER_CLI已針對SWD procotol進行了預配置,只需運行:
make flash請記下程序員輸出,該輸出使用0x08000000地址作為起點:
...
Memory Programming ...
Opening and parsing file: stm32-boot-explained.elf
File : stm32-boot-explained.elf
Size : 1,46 KB
Address : 0x08000000
...這實際上是閃存的起始地址,我們已經知道。
有一個自定義目標預先配置以運行ST-Link_GDBServer :
# start ST-Link gdb server
make gdb-server
# connect with gdb debugger
gdb -ex ' target remote localhost:61234 ' ./stm32-boot-explained.elf