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