STM32内存分布,启动过程及bootloader
STM32怎么说也用了好几年了,但是对于它的内存分布,启动过程,总是模棱两可;所以说决定写这篇文章做下梳理,水平有限,欢迎指正;
以下以F407ZGT6为例
1. STM32地址空间分布
| Address Range | Description
| 0x00000000 - 0x03FFFFFF | 内存别名映射区域
| 0x08000000 - 0x080FFFFF | 内部Flash存储器
| 0x10000000 - 0x1000FFFF | CCMRAM
| 0x1FFF0000 - 0x1FFF77FF | 系统存储器
| 0x1FFF7800 - 0x1FFF7A0f | opt
| 0x1FFFC000 - 0x1FFFC00F | 选项字节
| 0x20000000 - 0x2001BFFF | SRAM1 112KB
| 0x20001C00 - 0x2001FFFF | SRAM2 16KB
| 0x20020000 - 0x2003FFFF | SRAM3 64kB 407不存在
| 0x40000000 - 0x5FFFFFFF | 外设寄存器
| 0xE0000000 - 0xE00FFFFF | 系统控制空间
- [1] 内存别名映射区域
搜了一大堆,不太清楚,可以理解为,程序其实还是从地址0开始执行,如果选择flash启动那么就是将FLASH(0x0800 0000)重映射或者芯片出厂自带的Bootloader(0x1FFF 0000)重映射到地址0,故而代码是下载到 0x80000000 往后的存储空间中,却说运行又是从 0x00000000地址运行的 - [2] 内部Flash存储器
程序这就不用说了,开始时是中断向量表 - [3] CCMRAM
CCMRAM由内核直接控制,可以使用__attribute__((section(".ccmram")))
指定变量位置,总之就也是RAM,速度还更快,例如在使用Freertos时可以指定堆栈在此区域(static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] attribute ((section (".freertos_heap"))););注意此区域DMA无法访问;所以说在使用DMA时要注意不要把地址指定到这里,STM32其他型号也会有所不同 - [4] 系统存储器
这个主要用来存放 STM32F4 的 bootloader 代码用于下载代码;这个地址范围包含了一些特殊用途的存储器区域,主要用于存放一些系统配置信息、唯一设备标识号(Unique Device ID, UID)、Flash管理和保护设置等重要数据 - [5] opt
没用过加1 OTP 区域,即一次性可编程区域,共 528 字节,被分成两个部分,前面 512 字节(32 字节为 1 块,分成 16 块),可以用来存储一些用户数据(一次性的,写完一次,永远不可以擦除!!),后面 16 字节,用于锁定对应块。 - [6] 选项字节
flash写保护读保护,flash编程用到 - [7] SRAM
内存区域,407ram大小为128KB,也就是上边的SRAM1和SRAM2,手册中把这里分为了两个区域,但是根据STM32407链接文件定义来看,是把这里SRAM1和SRAM2,还是统一作为整个RAM处理;直接作为128KB处理
其中22000000->23FFFFFF的32MB大小是20000000-20100000的位带映射
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}
- [8] 外设寄存器
各种外设寄存器区域
这还是看参考手册,存储器映射吧,其中42000000->43FFFFFF的32MB大小是40000000-40100000的位带映射,我们用来操控IO很方便;提醒一点并不是所有32都有位带操作,F7就不存在 - [9] 系统存储器
没了解过,以后有机会再去看吧
2.关于链接文件
点击查看代码
/* Entry Point */
ENTRY(Reset_Handler) /* 定义程序的入口点为 Reset_Handler */
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* 设置用户模式栈的最高地址 这个比那辆在启动文件中会用到,正常情况下是在0x08000000存放,也就是复位中断的前一个字节,表示初始堆栈指针向下增长*/
/* 定义堆和栈的最小尺寸 */
_Min_Heap_Size = 0x200; /* 最小堆大小 (512 bytes) */
_Min_Stack_Size = 0x400; /* 最小栈大小 (1024 bytes) */
/* 内存区域定义 */
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K /* 定义CCMRAM区域 */
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K /* 定义SRAM区域 */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* 定义Flash区域 */
}
/* 段的定义 */
SECTIONS
{
/* 中断向量表存放在Flash中 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 中断向量表 启动代码 */
. = ALIGN(4);
} >FLASH
/* 程序代码和只读数据存放在Flash中 */
.text :
{
. = ALIGN(4);
*(.text) /* .text段 (代码) */
*(.text*) /* .text*段 (代码) */
*(.glue_7) /* 从 ARM 模式 切换到 Thumb 模式 的代码段 */
*(.glue_7t) /* 从 Thumb 模式 切换到 ARM 模式 的代码段 */
/*在使用bx blx指令时,地址最低位是0 还是1 就决定使用何种指令*/
*(.eh_frame)
KEEP (*(.init))
/* 大概是C++构造函数?,我直接用__attribute__((constructor)) 指定一个函数开main始前确实执行了这个函数,但函数运行结束显示“No source available for "__libc_init_array() at 0x8006744" ”无法理解*/
KEEP (*(.fini)) /* 程序后代码 */
. = ALIGN(4);
_etext = .; /* 在代码段结束处定义全局符号 */
} >FLASH
/* 常量数据存放在Flash中 */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata段 (常量, 字符串等) */
*(.rodata*) /* .rodata*段 (常量, 字符串等) */
. = ALIGN(4);
} >FLASH
/* ARM特定段 ,无法理解,有谁知道呀*/
.ARM.extab :
{
. = ALIGN(4);
*(.ARM.extab* .gnu.linkonce.armextab.*)
. = ALIGN(4);
} >FLASH
.ARM :
{
. = ALIGN(4);
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
. = ALIGN(4);
} >FLASH
/* 构造函数表 大概和上边的.init ,fini也有关*/
.preinit_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
. = ALIGN(4);
} >FLASH
.init_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
. = ALIGN(4);
} >FLASH
.fini_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
. = ALIGN(4);
} >FLASH
/*_sidata 其实就代表.data段在ram中的地址*/
_sidata = LOADADDR(.data);
/* 初始化数据段存放在RAM中,其初始值在Flash中 */
.data :
{
. = ALIGN(4);
_sdata = .; /* 在数据段开始处定义全局符号 */
*(.data) /* .data段 */
*(.data*) /* .data*段 */
*(.RamFunc) /* .RamFunc段 */
*(.RamFunc*) /* .RamFunc*段 */
. = ALIGN(4);
_edata = .; /* 在数据段结束处定义全局符号 */
} >RAM AT> FLASH
/* CCMRAM段 */
_siccmram = LOADADDR(.ccmram);
.ccmram :
{
. = ALIGN(4);
_sccmram = .; /* 在CCMRAM段开始处定义全局符号 */
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
_eccmram = .; /* 在CCMRAM段结束处定义全局符号 */
} >CCMRAM AT> FLASH
/* 未初始化数据段存放在RAM中 */
.bss :
{
_sbss = .; /* 在bss段开始处定义全局符号 */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .; /* 在bss段结束处定义全局符号 */
__bss_end__ = _ebss;
} >RAM
/* 用户堆栈段,用于检查剩余的RAM */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/* 删除编译器库信息 */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
/* ARM属性段 */
.ARM.attributes 0 : { *(.ARM.attributes) }
}
typedef struct
{
const tEventCallback func; /** 回调函数 */
const unsigned char paramNum; /**< 参数数量 */
const unsigned short event; /**< 名字或事件*/
} CEvent;
链接文件中是
.rodata :
{
. = ALIGN(4);
PROVIDE_HIDDEN(_cevent_start= .);
KEEP (*(shellCommand))
PROVIDE_HIDDEN(_cevent_end= .);
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >FLASH
在其他文件中使用
extern const unsigned int _cevent_start; //初始位置
extern const unsigned int _cevent_end; //结束位置
根据结构体大小就可以从初始位置开始到结束位置存放了多少个变量,然后从初始位置遍历每个结构体,根据event的值去匹配不同的函数指针去执行(后面有空在详细说下吧);
如果要实现boot升级,首先要注意FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* 定义Flash区域 */的分配,这是代码的起始地址;
boot程序放在0x08000000 然后可以把APP设置成0x08008000,这个数值正常来说设置成某个页或块的起始地址,flash写入前要先擦除,擦除单位是页或者块;
._user_heap_stack是为了代码检查,防止所占用内存大于RAM大小,栈其实还是从高地址到低地址;
栈 | ||
空闲空间 | ||
堆 | ||
.bss | ||
.data | ||
.text | ||
---- | ---- | ---- |
3. 启动文件
在启动文件的中断向量表中,开头如下所示,在我们程序地址默认0x08000000开始时,0x08000000地址的数据也就是_estack
,对应链接文件中的值_estack = ORIGIN(RAM) + LENGTH(RAM)
也就是ram最高地址;
0x08000004是复位中断,程序开始也就是进入这里;
.word _estack
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0x12345678
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler
复位中断完成什么工作呢,第一步就是将栈地址_estack放到SP中,在完成ram中变量初始化后,调用SystemInit,然后进入主函数;
SystemInit,函数在system_stm32f4xx.c定义,里面有个寄存器值就是我们常说的中断向量表偏移,SCB->VTOR
,这里指向中断向量表位置,正常程序等于0x0800000,但是app程序要指向app的中断向量表位置,程序在0x08008000那么就需要SCB->VTOR=0x08008000;一般来说刚开始是只有boot程序执行的,我们可以将APP程序向量表的一些保留位设置成特殊值或者使用其他存储方式去判断APP程序是否存在,例如上边我把向量表其中一个没用的设置成了0x12345678;正常上电先执行boot程序然后就可以看对应0x08008000+14的位置是不是0x12345678来判断App程序是否存在;
如何写入APP呢,其实就是boot程序在接收到数据后要写Flash,与上位机或者其他通信设备商定好通信协议,例如帧头、帧尾、数据总长度,本次包长度、CRC校验等,一般程序不会一次性发完,商定好每次传输的长度,分包发送,双方都要进行校验,可以boot程序申请一次其他设备回复一次,每个包都要标上序号,如果发现校验出错重新申请发送当前的包;
也可以使用文件系统,插入u盘使用文件系统查找其中的APP文件,将其写入Flash中;
(这些有时间再上代码吧)
提醒stm32f0并没有VTOR寄存器,跳转APP后要将向量表复制到RAM并改变向量表映射为ram,H7有两个BANK,可以直接切换BANK
.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr sp, =_estack /* 设置堆栈指针 */
/* 将数据段的初始值从Flash复制到SRAM */
movs r1, #0 /* 初始化偏移量 */
b LoopCopyDataInit /*跳转*/
/*完成将有初始化数据的变量,复制到ram,也就是.data段*/
/*r1是偏移量,每次复制后偏移量加4*/
CopyDataInit:
ldr r3, =_sidata /* 数据段初始值在Flash中的起始地址 */
ldr r3, [r3, r1] /* 从Flash中读取数据 */
str r3, [r0, r1] /* 将数据写入SRAM */
adds r1, r1, #4 /* 更新偏移量 */
b LoopCopyDataInit
/*判断是否复制结束*/
LoopCopyDataInit:
ldr r0, =_sdata /* 数据段起始地址 */
ldr r3, =_edata /* 数据段结束地址 */
adds r2, r0, r1 /* 计算当前地址 */
cmp r2, r3 /* 比较当前地址和数据段结束地址 */
bcc CopyDataInit /* 如果当前地址小于结束地址,继续复制 */
ldr r2, =_sbss /* .bss段起始地址 */
b LoopFillZerobss
/*完成将有未初始化数据的变量,复制到ram,也就是.bss段*/
/* 清零.bss段 */
FillZerobss:
movs r3, #0 /* 初始化值为0 */
str r3, [r2], #4 /* 将0写入.bss段 */
adds r2, r2, #4 /* 更新地址 */
b LoopFillZerobss
LoopFillZerobss:
ldr r3, =_ebss /* .bss段结束地址 */
cmp r2, r3 /* 比较当前地址和.bss段结束地址 */
bcc FillZerobss /* 如果当前地址小于结束地址,继续清零 */
/* 调用系统初始化函数 */
bl SystemInit
/*搜的解释:调用静态构造函数 一般不用C语言没有构造函数的概念,程序进入 main 函数之前执行一些初始化代码。
GCC提供了constructor和destructor属性,允许你定义在main函数之前和程序退出时运行的函数。
/*完成C库的初始化 确保在程序开始执行其主要逻辑之前正确初始化了 C/C++ 对象*/*/
bl __libc_init_array
/* 调用应用程序的入口点 */
bl main
bx lr /* 返回 */
**4.程序跳转 **
程序如何实现跳转呢,简单来说就是将app程序的地址,转换成函数指针指向的地址,直接当成函数跳转;
但是需要重新完成主堆栈地址的设置,和中断向量表的设置,__set_MSP设置sp,但其实启动文件也有sp的设置,向量表偏移就是VTOR寄存器;
注意STM32F0并不一样需要把向量表复制到RAM中,然后把向量表指向RAM
pFunction Jump_To_Application;//定义一个函数指针
uint32_t JumpAddress;
__disable_irq(); //关闭中断
JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 0x00000004);//得到复位程序地址
Jump_To_Application = (pFunction) JumpAddress; //强制转换为函数
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS); //设置栈顶地址,ram完全交给app使用
Jump_To_Application();
图片出自M3与M4权威指南
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具