STM32地址分布、启动链接文件以及Boot跳转App简单记录

STM32地址分布、启动链接文件以及Boot跳转App简单记录

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开始执行,如果boot选择flash启动那么就是将FLASH(0x0800 0000)重映射到0 ,系统存储器启动将芯片出厂的系统代码(0x1FFF 0000)重映射到地址0,sram启动就是将0x20000000映射到0;参考野火库开发指南的SRAM中调试代码篇章

  • [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] 系统存储器

    当芯片上电后采样到BOOT0引脚为高电平,BOOT1为低电平时,内核将从系统存储器的0x1FFF0000及0x1FFF0004获取MSP及PC值进行自举。 系统存储器是一段特殊的空间,用户不能访问,ST公司在芯片出厂前就在系统存储器中固化了一段代码。固化的代码主要就是 Bootloader,ISP下载会用到;

  • [5] opt
    没用过加1 OTP 区域,即一次性可编程区域,共 528 字节,被分成两个部分,前面 512 字节(32 字节为 1 块,分成 16 块),可以用来存储一些用户数据(一次性的,写完一次,永远不可以擦除!!),后面 16 字节,用于锁定对应块。

  • [6] 选项字节
    flash写保护读保护,flash编程用到

    0x1FFF0000 - 1FFFFFFF这个地址范围通常也包含了一些特殊用途的存储器区域,主要用于存放一些系统配置信息、唯一设备标识号(Unique Device ID, UID)、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); /* 设置用户模式栈的最高地址 
   * 说明:
   * 1. 栈指针初始时指向RAM的最高地址(向下增长)
   * 2. 在STM32启动文件中,0x08000000处存放的就是这个值(向量表第一条)
   * 3. ORIGIN(RAM) + LENGTH(RAM) 计算RAM的结束地址
   */

/* 定义堆和栈的最小尺寸 */
_Min_Heap_Size = 0x200; /* 最小堆大小 (512 bytes) - 用于动态内存分配 */
_Min_Stack_Size = 0x400; /* 最小栈大小 (1024 bytes) - 用于函数调用和局部变量 */

/* 内存区域定义 */
MEMORY
{
  CCMRAM (xrw)    : ORIGIN = 0x10000000,   LENGTH = 64K    /* 核心耦合内存(CCM) - 只能被CPU访问,DMA不可用 */
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 128K   /* 主SRAM区域 - 通用内存 */
  FLASH  (rx)     : ORIGIN = 0x08000000,   LENGTH = 1024K  /* Flash存储器 - 存放程序代码和常量 */
}

/* 段(SECTION)定义 - 控制代码和数据在内存中的布局 */
SECTIONS
{
  /* ==================== FLASH 区域 ==================== */
  
  /* 中断向量表 - 必须放在Flash起始位置 */
  .isr_vector :
  {
    . = ALIGN(4); /* 4字节对齐 */
    KEEP(*(.isr_vector)) /* 保留中断向量表 - 芯片复位后首先读取此处 */
    . = ALIGN(4); /* 保持对齐 */
  } >FLASH /* 指定到FLASH区域 */

  /* 程序代码段 */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* 主程序代码 */
    *(.text*)          /* 其他程序代码 */
    *(.glue_7)         /* ARM/Thumb模式切换胶合代码(ARM→Thumb) */
    *(.glue_7t)        /* ARM/Thumb模式切换胶合代码(Thumb→ARM) */
    *(.eh_frame)       /* 异常处理框架信息(用于栈展开) */
    /* 大概是C++构造函数?,我直接用__attribute__((constructor)) 指定一个函数开main始前确实执行了这个函数,但函数运行结束显示“No source available for "__libc_init_array() at 0x8006744"*/
    KEEP (*(.init))    /* 程序初始化代码(C++构造函数等) */
    KEEP (*(.fini))    /* 程序终止代码(C++析构函数等) */

    . = ALIGN(4);
    _etext = .;        /* 定义代码段结束符号(在启动代码中用于数据初始化) */
  } >FLASH

  /* 只读数据段(常量、字符串等) */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* 只读数据 */
    *(.rodata*)        /* 其他只读数据 */
    . = ALIGN(4);
  } >FLASH

  /* ARM异常处理扩展表 个人观点是不用管*/
  .ARM.extab :
  {
    . = ALIGN(4);
    *(.ARM.extab* .gnu.linkonce.armextab.*) /* 异常处理相关数据 */
    . = ALIGN(4);
  } >FLASH

  /* ARM异常索引表 个人观点是不用管*/
  .ARM :
  {
    . = ALIGN(4);
    __exidx_start = .; /* 异常索引表起始 */
    *(.ARM.exidx*)     /* 异常处理索引 */
    __exidx_end = .;   /* 异常索引表结束 */
    . = ALIGN(4);
  } >FLASH

  /* ==================== 全局构造函数/析构函数表 ==================== */
  
  /* 预初始化数组 个人观点是不用管*/
  .preinit_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__preinit_array_start = .); /* 隐藏符号 - 链接器内部使用 */
    KEEP (*(.preinit_array*)) /* C++全局对象预初始化函数指针 */
    PROVIDE_HIDDEN (__preinit_array_end = .);
    . = ALIGN(4);
  } >FLASH

  /* 主初始化数组 个人观点是不用管*/
  .init_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*))) /* 按优先级排序的初始化函数 */
    KEEP (*(.init_array*))        /* C++全局对象构造函数指针 */
    PROVIDE_HIDDEN (__init_array_end = .);
    . = ALIGN(4);
  } >FLASH

  /* 终止处理数组 个人观点是不用管*/
  .fini_array :
  {
    . = ALIGN(4);
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*))) /* 按优先级排序的终止函数 */
    KEEP (*(.fini_array*))        /* C++全局对象析构函数指针 */
    PROVIDE_HIDDEN (__fini_array_end = .);
    . = ALIGN(4);
  } >FLASH

  /* ==================== RAM 初始化相关 ==================== */
  
  /* 定义.data段初始值在Flash中的加载地址 启动文件会用到这个变量*/
  _sidata = LOADADDR(.data); /* 用于启动代码中复制初始化数据 */

  /* 已初始化数据段(全局/静态变量) */
  .data :
  {
    . = ALIGN(4);
    _sdata = .;        /* RAM中.data段的起始地址(符号导出) */
    *(.data)           /* 已初始化数据 */
    *(.data*)          /* 其他已初始化数据 */
    *(.RamFunc)        /* 需要在RAM中执行的函数 */
    *(.RamFunc*)       /* 其他RAM函数 */

    . = ALIGN(4);
    _edata = .;        /* RAM中.data段的结束地址 */
  } >RAM AT> FLASH     /* >RAM: 运行时位置; AT> FLASH: 加载时位置 */

  /* ==================== CCMRAM 段 ==================== */
  
  _siccmram = LOADADDR(.ccmram); /* CCMRAM初始值在Flash中的地址 */

  .ccmram :
  {
    . = ALIGN(4);
    _sccmram = .;      /* CCMRAM段起始地址 */
    *(.ccmram)         /* 指定到CCMRAM的数据 */
    *(.ccmram*)        /* 其他CCMRAM数据 */

    . = ALIGN(4);
    _eccmram = .;      /* CCMRAM段结束地址 */
  } >CCMRAM AT> FLASH  /* 运行时在CCMRAM,初始值在FLASH */

  /* ==================== 未初始化数据段 ==================== */
  
  .bss :
  {
    _sbss = .;         /* .bss段起始地址 */
    __bss_start__ = _sbss; /* 兼容性定义 */
    *(.bss)            /* 未初始化数据 */
    *(.bss*)           /* 其他未初始化数据 */
    *(COMMON)          /* 公共块数据(未初始化的全局变量) */
    
    . = ALIGN(4);
    _ebss = .;         /* .bss段结束地址 */
    __bss_end__ = _ebss; /* 兼容性定义 */
  } >RAM

  /* ==================== 堆栈分配 ==================== */
  
  ._user_heap_stack :
  {
    . = ALIGN(8);      /* 8字节对齐(ARM AAPCS要求) */
    PROVIDE ( end = . ); /* 定义程序数据结束位置 */
    PROVIDE ( _end = . ); /* 同上(兼容性) */
    
    . = . + _Min_Heap_Size;  /* 保留堆区域 */
    . = . + _Min_Stack_Size; /* 保留栈区域 */
    
    . = ALIGN(8);      /* 最终对齐 */
  } >RAM

  /* ==================== 其他设置 ==================== */
  
  /* 删除不必要的库部分以减少体积 */
  /DISCARD/ :
  {
    libc.a ( * )      /* 丢弃标准C库冗余部分 */
    libm.a ( * )      /* 丢弃数学库冗余部分 */
    libgcc.a ( * )    /* 丢弃GCC支持库冗余部分 */
  }

  /* ARM属性段(调试信息) */
  .ARM.attributes 0 : { *(.ARM.attributes) } /* 不占用实际内存 */
}

这里可以有一个很好用的方法,我们可以使用 SECTION(xxx)或者__attribute__ ((section (xxx)))指定存放位置(不同的编译器可能不太一样)例如 __attribute__ ((section (".freertos_heap"))) static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] ;是把freertos的堆栈放到了CCRAM中,另外也可以知道这个位置的起始地址和结束地址,那么我们就可以将同一种类的结构体放到同一个位置
例如一个结构体里面有

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;  //结束位置
__attribute__((section(shellCommand))) “定义的变量”   
//在stm32ide中 uint32_t test[10] = {1} __attribute__((section(".ccmram")))不可行;  __attribute__((section(".ccmram"))) uint32_t test[10] = {1}可行;  uint32_t test[10]  __attribute__((section(".ccmram")))可行;

根据结构体大小就可以从初始位置开始到结束位置存放了多少个变量,然后从初始位置遍历每个结构体,根据event的值去匹配不同的函数指针去执行,可以参考RT-thread的自动初始化机制,对于理解函数指针、链接脚本很有帮助;

如果要实现boot升级,首先要注意FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* 定义Flash区域 */的分配,这是代码的起始地址;
boot程序放在0x08000000 然后可以把APP设置成0x08008000,这个数值正常来说设置成某个页或块的起始地址,flash写入前要先擦除,擦除单位是页或者块;
另外链接文件中的._user_heap_stack是为了代码检查,防止所占用内存大于RAM大小,栈其实还是从高地址到低地址;

空闲空间
.bss
.data
.text
---- ---- ----

[!NOTE]

//突然想到正常来说我们的代码是下载到flash中的,但是定义的各种变量最终是在ram中的,尤其是有初始值的变量;曾经是有这个疑问的,后来才发现这些初始值确实是在flash中的,但是在启动文件中会把这些初始值复制到ram中变量对应的地址;所以此时又有一个疑问,启动文件中并没有管CCRAM的初始化,那这里的CCRAM区域变量的初始化是否还会生效呢?flash中又是否会存储它的初始值呢?

首先注意uint32_t test[10] = {1}只有第一个变量是1其余变量0

定义两个测试变量 (没用可能会被优化掉)

__attribute__((section(".ccmram"))) uint32_t lpajsj[10] = {0x741852};
uint32_t lpajsj1[10] = {0x741851};

先查看map _siccmram的值为0x803a934也就是变量初始值会在0x803a934

image

lpajsj数组最终会在0x10000000;长度0x28字节

image

_sidata的值为0x803a444

image

lpajsj1数组会在0x200000c相对0x20000000偏移0xc;

image

查看bin文件0x3a934位置是有数据 0x741852;0x3a444+0xc = 3a450有数据0x741851

image

image

所以可以看出有初始值的变量它的初始值确实在flash中是有存储的,普通变量没有初始值就会被归类到.bss段

结合启动文件的内容,也就是在启动文件中完成了各个变量的初始赋值

这会身边并没有板子,不过基本可以确定 如果启动文件中没有CCRAM变量的初始化,位于ccram的变量即使定义了初始值大概率也是不会生效的

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呢,一般就是用APP编译好的bin文件,上位机或者其他通信设备打开文件后一个字节一个字节发送,boot程序在接收到数据后要写到Flash对应的地址。要注意的商定好通信协议,例如帧头、帧尾、数据总长度,本次包长度、CRC校验等,一般程序不会一次性发完,商定好每次传输的长度,分包发送,双方都要进行校验,可以boot程序申请一次其他设备回复一次,每个包都要标上序号,如果发现校验出错重新申请当前的包或者停止升级;如果已经在运行在app中在收到外部的升级请求后可以复位或者跳转回boot中进行升级。
另外也可以使用文件系统,例如插入u盘后通过按键或者其他方式主动触发升级,在文件系统查找其中的APP文件,将其写入Flash中;

 
  .section  .text.Reset_Handler  /* 定义代码段,存放复位处理程序 */
  .weak  Reset_Handler       /* 声明弱符号,允许在其他文件中重定义 */
  .type  Reset_Handler, %function /* 指定Reset_Handler为函数类型 */
Reset_Handler:               /* 复位处理程序入口点 */
  
  /* 1. 初始化栈指针 */
  ldr   sp, =_estack         /* 将栈顶地址(_estack)加载到栈指针(sp)
                                _estack通常在链接脚本中定义,指向RAM最高地址 */
  
  /* 2. 调用系统时钟初始化 */
  bl  SystemInit             /* 调用SystemInit函数初始化时钟系统
                                注意:有些实现中此步骤可能在数据初始化之后 */
  
  /* 3. 初始化.data段(已初始化的全局/静态变量) */
  ldr r0, =_sdata            /* r0 = .data段在RAM中的起始地址 */
  ldr r1, =_edata            /* r1 = .data段在RAM中的结束地址 */
  ldr r2, =_sidata           /* r2 = .data段初始值在Flash中的地址 */
  movs r3, #0                /* r3 = 当前复制偏移量(初始为0) */
  b LoopCopyDataInit         /* 跳转到循环检查点 */

CopyDataInit:                /* 数据复制子程序 */
  ldr r4, [r2, r3]           /* 从Flash(sidata + r3)加载初始值到r4 */
  str r4, [r0, r3]           /* 将初始值存储到RAM(sdata + r3) */
  adds r3, r3, #4            /* 偏移量增加4字节(32位系统) */

LoopCopyDataInit:            /* 数据复制循环控制 */
  adds r4, r0, r3            /* 计算当前RAM地址(sdata + r3) */
  cmp r4, r1                 /* 比较当前地址与结束地址(edata) */
  bcc CopyDataInit           /* 如果当前地址 < 结束地址,继续复制 */
  
  /* 4. 清零.bss段(未初始化的全局/静态变量) */
  ldr r2, =_sbss             /* r2 = .bss段在RAM中的起始地址 */
  ldr r4, =_ebss             /* r4 = .bss段在RAM中的结束地址 */
  movs r3, #0                /* r3 = 0(用于清零的值) */
  b LoopFillZerobss          /* 跳转到循环检查点 */

FillZerobss:                 /* 清零子程序 */
  str  r3, [r2]              /* 将0存储到当前地址 */
  adds r2, r2, #4            /* 地址指针增加4字节 */

LoopFillZerobss:             /* 清零循环控制 */
  cmp r2, r4                 /* 比较当前地址与结束地址(ebss) */
  bcc FillZerobss            /* 如果当前地址 < 结束地址,继续清零 */
  /*搜的解释:调用静态构造函数 一般不用C语言没有构造函数的概念,程序进入 main 函数之前执行一些初始化代码。
  GCC提供了constructor和destructor属性,允许你定义在main函数之前和程序退出时运行的函数。
  /* 5. 调用C++静态构造函数/C库初始化 */
  bl __libc_init_array       /* 初始化全局对象和C运行时库
                                对于纯C项目可能是空操作 */
  
  /* 6. 进入主程序 */
  bl  main                  /* 调用用户主函数main() */
  
  /* 7. 主程序返回处理(理论上不应返回) */
  bx  lr                    /* 如果main返回,回到调用者(通常进入死循环) */
    
.size  Reset_Handler, .-Reset_Handler /* 设置函数大小信息 可在map文件中查看*/

[!NOTE]

函数大小信息查看

image
中断向量表大小查看
image

4.程序跳转

程序如何实现跳转呢,简单来说就是将app程序的地址,转换成函数指针指向的地址,直接当成函数跳转;
但是需要重新完成主堆栈地址的设置,和中断向量表的设置,__set_MSP设置sp,但其实启动文件也有sp的设置,向量表偏移就是VTOR寄存器;
提醒stm32f0并没有VTOR寄存器,跳转APP后要将向量表复制到RAM并改变向量表映射为ram,可参考 stm32f0中断向量表映射,H7有两个BANK,可以直接切换BANK

	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权威指南

posted @ 2024-06-24 21:57  lpajsj  阅读(1188)  评论(0)    收藏  举报