คำอธิบายโดยละเอียดของการแมปหน่วยความจำ STM32 และกระบวนการบูต
ในขณะที่ตัวอย่างทั้งหมดด้านล่างนี้มีไว้สำหรับ STM32F446 แต่หลักการพื้นฐานใช้กับ MCUs ส่วนใหญ่
STM32CUBEMX ผลิต STM32F446RETX_FLASH.LD ซึ่งเราจะดูเป็นข้อมูลอ้างอิงสำหรับการสำรวจทั้งหมดของเรา
Linker ใช้ไฟล์วัตถุที่ผลิตโดยคอมไพเลอร์และสร้างเอาต์พุตการรวบรวมสุดท้ายซึ่งในกรณีของเราคือ . Linker ใช้ไฟล์สคริปต์ Linker เสมอ แม้ว่าคุณจะไม่ได้ระบุหนึ่งสคริปต์เริ่มต้น
สิ่งสำคัญที่ควรทราบว่าสคริปต์ Linker จะอธิบายเฉพาะหน่วยความจำตามข้อกำหนดของ MCU และไม่ได้เปลี่ยนหน่วยความจำฮาร์ดแวร์ใด ๆ
เราไม่สามารถพูดคุยเกี่ยวกับกระบวนการ bootloader โดยไม่เข้าใจโครงสร้างหน่วยความจำ ที่จริงแล้วมันเป็นจุดประสงค์ของ bootloader ที่จะมีหน่วยความจำในสถานะพร้อมที่จะดำเนินการวิธีการ main() ของแอปพลิเคชันของเรา
หน่วยความจำโปรแกรมหน่วยความจำข้อมูลการลงทะเบียนและพอร์ต I/O ใน STM32F4 ถูกจัดระเบียบภายในพื้นที่ที่อยู่เชิงเส้นเดียวกัน
...
+ -- -- 0x2001FFFF -- -- +
| |
| RAM |
| |
+ -- -- 0x20000000 -- -- +
| ... |
+ -- -- 0x1FFF7A0F -- -- +
| |
| System |
| |
+ -- -- 0x1FFF0000 -- -- +
| ... |
+ -- -- 0x081FFFFF -- -- +
| |
| Flash |
| |
+ -- -- 0x08000000 -- -- +
| ... |
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + โดยที่หน่วยความจำนามแฝงชี้ไปที่แฟลชระบบหรือหน่วยความจำ RAM ขึ้นอยู่กับพิน BOOT0 โดยค่าเริ่มต้นมันเป็นแฟลช
สองช่องว่างเป็นสิ่งที่น่าสนใจที่สุดสำหรับเราในตอนนี้:
อย่างไรก็ตามในทางปฏิบัติแผนกการทำงานอาจแตกต่างกันไป ตัวอย่างเช่นคุณอาจต้องโหลดข้อมูลไบนารีของโปรแกรมลงใน RAM หรือในทางกลับกัน
โครงสร้างหน่วยความจำสะท้อนให้เห็นในไฟล์ Linker Flash เริ่มต้นที่ ORIGIN = 0x8000000 และ RAM ที่ ORIGIN = 0x20000000
// linker file
MEMORY
{
RAM ( xrw ) : ORIGIN = 0x20000000 , LENGTH = 128 K
FLASH ( rx ) : ORIGIN = 0x8000000 , LENGTH = 512 K
}โปรดทราบว่านี่เป็นเพียงการใช้งานเริ่มต้นคุณสามารถแยกแฟลชและ RAM ได้อย่างง่ายดายและเพิ่มบล็อกหรือส่วนของคุณเองทันทีที่พวกเขาปฏิบัติตามข้อกำหนดของหน่วยความจำ MCU
จากนั้นหน่วยความจำจะถูกแบ่งในไฟล์ linker เป็นส่วนแต่ละรายการมีที่อยู่และปลายทาง (ตัวอย่างเช่นแฟลชหรือ RAM)
// linker file
SECTIONS
{
...
. text :
{
...
} > FLASH
...
}ในการสำรวจตารางสัญลักษณ์ ตัวตน เราจะใช้คำสั่ง arm-none-ebi-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 ข้อมูลสำหรับตัวแปรทั้งหมดโดยไม่มีค่าที่กำหนด bootloader ดูแลการตั้งค่าข้อมูลของบล็อกนี้เป็น 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 > หน่วยความจำ RAM ทั้งหมดด้านบน _end และจนกว่า _estack จะทุ่มเทให้กับกองและหน่วยความจำสแต็ก
20000048 g . _user_heap_stack 00000000 _end
20020000 g . isr_vector 00000000 _estack ที่อยู่ _estack คำนวณเป็น ORIGIN(RAM) + LENGTH(RAM) ดังนั้นสำหรับ RAM 128KB:
_estack = 0x20000000 + 128 * 1024 # dec
= 0x20000000 + 0x20000 # hex
= 0x20020000 สแต็คเป็นโครงสร้าง LIFO ที่เริ่มต้นที่ _estack และเติบโตลง ขนาดสแต็กขั้นต่ำถูกกำหนดในไฟล์ linker เป็น _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 และเพิ่มขึ้นเป็น _estack - _Min_Stack_Size เมื่อ malloc ร้องขอ
เรามีตัวแปร stack_int ที่กำหนดไว้ใน main.c ลองตรวจสอบที่อยู่ด้วย GDB หลังจากเริ่มต้น:
(gdb) p & stack_int
$1 = (unsigned short * ) 0x2001ffd6
(gdb) p $msp
$2 = (void * ) 0x2001ffd0 ซึ่งตรงกับความคาดหวังของเราเมื่อตัวแปรสูงกว่า $msp
เป็นมูลค่าการกล่าวขวัญว่าการใช้ห้องสมุด C มาตรฐานที่ใช้กันทั่วไปในแอปพลิเคชัน C ฝังคือ newlib ไลบรารีนี้ต้องการการใช้งานฟังก์ชั่นเฉพาะระบบบางอย่าง STM32CUBEMX สร้างไฟล์ syscalls.c ด้วยการใช้งานเริ่มต้นที่จำเป็น
นอกจากนี้ยังเป็นกรณีของการโทร sbrk ที่เพิ่มพื้นที่ข้อมูลโปรแกรม malloc ใช้ฟังก์ชั่นนี้เพื่อจัดสรรหน่วยความจำฮีปมากขึ้น คุณอาจพบการใช้งานที่สร้างโดย STM32Cubemx ใน Sysmem.c มันช่วยให้กองเติบโตจาก _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:
// linker file
ENTRY ( Reset_Handler ) ที่จริงแล้วมันบันทึกการอ้างอิงไปยังที่อยู่ฟังก์ชัน Reset_Handler ในส่วนหัวไฟล์. ELLF :
arm-none-eabi-objdump -t -f stm32-boot-explained.elf | grep " start address "
start address 0x080006e9 ดูที่ตารางสัญลักษณ์ของเรา Reset_Handler มีอยู่ในส่วนแฟลช .text ข้อความเช่นเดียวกับฟังก์ชั่นอื่น ๆ ทั้งหมด:
080006e8 g F . text 00000064 Reset_Handlerที่อยู่คือการจัดแนวหน้าซึ่งเป็นเหตุผลว่าทำไมจึงมีความเป็นไปได้สำหรับความไม่ตรงกันระหว่างส่วนหัว . และที่อยู่จริง
แม้ว่าข้อมูลนี้ส่วนใหญ่จะถูกใช้โดย Linker เพื่อตรวจสอบการมีอยู่ของสัญลักษณ์จุดเริ่มต้นภายในรหัส แต่ก็ไม่มีความหมายในทางปฏิบัติสำหรับ MCU
ตามข้อกำหนด STM32 CPU จะดึงค่า _estack จากที่อยู่สูงสุดจากที่อยู่ 0x00000000 จากนั้นเริ่มการดำเนินการรหัสจากหน่วยความจำบูตเริ่มต้นที่ 0x00000004
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + นี่คือที่กำหนดหน่วยความจำนามแฝงที่กล่าวถึงข้างต้น ด้วยการกำหนดค่าเริ่มต้นเมื่อค่า PIN BOOT0 = 0 จะมีนามแฝงกับบล็อกหน่วยความจำแฟลชเริ่มต้นที่ 0x8000000
ตัวเลือกอื่น ๆ ตาม BOOT0 และ BOOT1 รวมหน่วยความจำระบบพร้อมหน่วยความจำ bootloader หรือ RAM แบบฝังตัว bootloader ฝังตัวถูกตั้งโปรแกรมโดย ST ในระหว่างการผลิตและนอกขอบเขตของคู่มือนี้
แต่ Reset_Handler มีที่อยู่ 0x08000524 ซึ่งไม่ได้เป็นจุดเริ่มต้นของหน่วยความจำแฟลชอย่างแน่นอน MCU จะหาวิธี Bootstrap ได้อย่างไร
นี่คือที่ที่โต๊ะเวกเตอร์เข้ามาเล่น
08000000 g O . isr_vector 000001 c4 Vector_Table MCU ปฏิบัติต่อจุดเริ่มต้นของหน่วยความจำเป็นตารางเวกเตอร์ที่มีพอยน์เตอร์ไปยังรูทีนบริการขัดจังหวะและฟังก์ชั่นเริ่มต้นที่จำเป็นรวมถึง Reset_Handler ปรึกษาข้อมูลจำเพาะเพื่อดูโครงสร้างตารางที่แน่นอนที่ MCU คาดว่าจะโหลดจาก 0x00000000 ตารางจริงจะต้องเต็มไปด้วย bootloader
| ที่อยู่ | ชื่อ |
|---|---|
| 0x00000000 | ที่สงวนไว้ |
| 0x00000004 | รีเซ็ตตัวจัดการ |
| 0x00000008 | ไม่ขัดจังหวะการสวมหน้ากาก |
| 0x00000012 | ความผิดยาก |
| - | การขัดจังหวะอื่น ๆ |
Reset_Handler นี้เป็นฟังก์ชั่น bootloader ที่สามารถใช้สำหรับแอปพลิเคชันจำนวนมากตั้งแต่งานเฉพาะด้านความปลอดภัยไปจนถึงการอัปเดตเฟิร์มแวร์อัตโนมัติ ที่นี่เราจะสำรวจการใช้งานเริ่มต้นพื้นฐานเพื่อทำความเข้าใจการโต้ตอบกับหน่วยความจำของ MCU
โดยเริ่มต้นเมธอด Reset_Handler เริ่มต้นในไฟล์ startup_stm32f436xx.s ASM ที่จัดทำโดย STM23Cubemx การใช้งาน bootloader.c จริงในโครงการนี้เขียนเป็น C เพื่อความชัดเจน
โปรดทราบว่าตัวแปรที่กำหนดไว้ในสคริปต์ Linker สามารถเข้าถึงได้ในรหัส C:
extern uint32_t _estack ;เพื่อให้สามารถทำซ้ำเวอร์ชัน ASM ได้อย่างง่ายดาย
กระบวนการโหลดน้อยที่สุด อาจถูกแบ่งออกเป็นขั้นตอนต่อไปนี้:
SystemInit() ).data segment initializers จาก Flash ถึง RAM.bss__libc_init_array() ฟังก์ชั่น)main() )สำหรับขั้นตอน #1 และ #4, STM32Cubemx มีการใช้งานฟังก์ชั่นคุณสามารถตรวจสอบรายละเอียดใน System_STM32F4XX.C
โครงการมีชุดไฟล์น้อยที่สุดที่จำเป็นในการบูต STM32 คุณอาจต้องการลองด้วยตัวคุณเองเพื่อตรวจสอบผลลัพธ์ของ แขนไม่มี -Eabi-Objdump และก้าวผ่านด้วย GDB
จำเป็นต้องใช้ ARM GNU Toolchain เพื่อสร้างโครงการ
ขอแนะนำให้ติดตั้งแพ็คเกจเครื่องมือคำสั่ง all-in-one stm32cubeclt พร้อม ARM-None-Eabi-GCC , STM32_Programmer_Cli และเครื่องมือ ST-LINK_GDBSERVER
สร้างโครงการโดยใช้ 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โปรดทราบว่าคำสั่งสุดท้ายคือขั้นตอนการเชื่อมโยง หากเราตัดธงคอมไพเลอร์อื่นทั้งหมดคำสั่งอาจมีลักษณะดังต่อไปนี้ นี่คือที่ linker ได้รับคำสั่งให้ใช้สคริปต์ linker ของเรา
arm-none-eabi-gcc
...
-T " %SCRIPT_DIR%/STM32F446RETx_FLASH.ld "
...
" %OBJ_DIR%/%OBJECT_NAME%.c.obj "
...
-o stm32-boot-explained.elfstm32_programmer_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