STM32将变量定义到指定RAM地址
分类: 硬件 发布于: 2024-01-07

本实验基于STM32CubeIDE,ST 推出的免费集成编译环境,基于 Eclipse 开源框架,集成了 GCC、GDB 等免费的编译器、链接器。需要对内存进行细分时,比如指定变量/函数/文件到特殊地址等等,可以通过“*.ld”链接文件来实现。

ld即Linked Script,ST官网有一个官方文档 LAT0816 用来简单介绍ld如何编写。使用IDE创建好功能之后,项目根目录有两个文件STM32XXX_FLASH.ldSTM32XXX_RAM.ld,我们需要修改的是前者,后者是在线调试时使用,修改后对编译的结果没有作用。

ld文件分MEMORYSECTIONS两个章节,前者用来定义存储器的分段结构,后者可以视作一个逻辑层面的段,用来将变量标识到指定位置。

以下是STM32H753的MEMORY

MEMORY
{
  FLASH (rx)     : ORIGIN = 0x08000000, LENGTH = 2048K
  DTCMRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 128K
  RAM_D1 (xrw)   : ORIGIN = 0x24000000, LENGTH = 512K
  RAM_D2 (xrw)   : ORIGIN = 0x30000000, LENGTH = 288K
  RAM_D3 (xrw)   : ORIGIN = 0x38000000, LENGTH = 64K
  ITCMRAM (xrw)  : ORIGIN = 0x00000000, LENGTH = 64K
}

上面为默认的配置,也可以对其进行分割、指定自定义的标识。可以在SECTIONS章节中指定一个段,用来将变量保存在D2区域:

  .ram_d2 :
  {
    . = ALIGN(4);
    _ram_d2_start = .;
    *(.ram_d2)
    . = ALIGN(4);
    _ram_d2_end = .; 
  } >RAM_D2

然后按如下定义变量:

__attribute__((section(".ram_d2"))) uint8_t key[8] = {};

同时,要在IDE中清除掉之前编译的缓存,否则修改不会生效。 观察build analyzer,变量已经分配到RAM D2区域: RAM区域

未解决的问题

在某些时候,需要把变量指定到RAM的某位置,同时还带有初始值,比如带初始值的全局变量。针对这个应用场景,就需要将变量编译到Flash里,在运行阶段,再加载到RAM_D2。LAT0816有介绍:

  • VMA(the virtual memory address):这是运行输出文件时,该 section 的地址。VMA 是可选项,可以不设置。
  • LMA(load memory address):这是加载 section 时的地址。

在大多数情况下,这两个地址是相同的。当然也可以不相等,比如下面的例子就是 LMA 和 VMA 不同的案例: 数据段被加载到 ROM 中,然后在程序启动时复制到 RAM 中(通常用于初始化全局变量)。此时 ROM 地址就是LMA,RAM 地址就是 VMA。

同时,文档中给出了一个示例:

/* Initialized data sections into "RAM" Ram type memory */
 .data :
 {
  . = ALIGN(4);
  _sdata = .; /* create a global symbol at data start */
  *(.data) /* .data sections */
  *(.data*) /* .data* sections */
  . = ALIGN(4);
  _edata = .; /* define a global symbol at data end */
 } >RAM AT> FLASH

即“.data”因为有“>RAM AT> FLASH”的修饰,表示.data段的 VMA 为 RAM,LMA 为 FLASH。即.data 段的内容会放在 FLASH 中,但是运行时,会加载到 RAM 中。

基于上述RAM_D2的示例,同样加上修饰符:>RAM_D2 AT> FLASH,在build analyzer窗口中观察到变量的LMA已经分配到flash,但烧录到MCU中测试,赋予的初始值并没有生效。 RAM区域2

经过反复折腾均告失败,由此先在这里打个记号吧,等后续解决之后再来更新。现阶段只能手动拷贝了。

2025-04-25 补充:带初值的变量初始化

时隔一年多,无意中解决了这个问题。在STM32CubeIDE项目中,有一个startup_xxxx.s文件(例如startup_stm32f407xx.s)它是一个汇编语言编写的启动文件,是微控制器上电后执行的第一个代码,负责初始化硬件并引导进入主程序。也就是说,在进入main()函数之前,这个.s文件做了很多准备工作,包括但不限于设置栈指针、电源模式切换、时钟初始化、拷贝.data段、清零.bss段、调用构造函数、跳转到main()

思考一下,创建默认工程时,在默认RAM区定义一个带初值的全局变量,在运行时它的初始值总是能被正确的设置,这是怎么做到的呢?没错,.s文件默认做了这些工作,在Reset_Handler中,有以下几行关键代码:

ldr r0, =_sdata  /* SRAM中的.data段起始地址 */
ldr r1, =_edata  /* SRAM中的.data段结束地址 */
ldr r2, =_sidata /* Flash中.data段的初始值地址 */
movs r3, #0      /* 偏移量清零 */
b LoopCopyDataInit

CopyDataInit:
  ldr r4, [r2, r3]  /* 从Flash读取数据 */
  str r4, [r0, r3]  /* 写入SRAM */
  adds r3, r3, #4   /* 偏移量+4 */

LoopCopyDataInit:
  adds r4, r0, r3
  cmp r4, r1        /* 检查是否复制完毕 */
  bcc CopyDataInit  /* 若未完成,继续循环 */

需要特别注意的是,_sdata_edata_sidata都是ld文件中分别在各个段(SECTION)定义的符号,如果要自定义一个SECTION,这些标记不能与其他SECTION中定义的同名,否则将产生符号冲突。在上述中,RAM_D2的起止地址分别定义为_ram_d2_start_ram_d2_end,与RAM_D1中定义的_sdata_edata保持区别。

所有初始化的全局变量的初始值都保存在 Flash(AT> FLASH) 中,但会按照链接脚本中定义的顺序依次存放。如果要使用多个SECTION,也需要定义多个_sidata,用户来获取该SECTION中变量初值在FLASH中的地址,否则无法确定执行拷贝的参数。正确的示例:

.data_ram_d1 : { ... } >RAM_D1 AT> FLASH  /* 初始值在Flash的连续区域1 */
.data_ram_d2 : { ... } >RAM_D2 AT> FLASH  /* 初始值在Flash的连续区域2 */

/* 定义初始值在Flash中的加载地址 */
_sidata_ram_d1 = LOADADDR(.data_ram_d1);  /* D1变量初始值在Flash中的地址 */
_sidata_ram_d2 = LOADADDR(.data_ram_d2);  /* D2变量初始值在Flash中的地址 */

理解了以上背景,就知道之前给RAM_D2区域定义的全局变量赋予的初值,为什么没有生效了。解决办法很简单,就是在.s文件中,加上拷贝的逻辑:

/* 以下代码需要放在.s文件里的Reset_Handler代码块中,以使得复位后自动执行 */

/* 拷贝RAM_D2的.data段 */
ldr r0, =_sdata_ram_d2      /* 目标地址:RAM_D2起始 */
ldr r1, =_edata_ram_d2      /* 结束地址 */
ldr r2, =_sidata_ram_d2     /* Flash中的初始值地址 */
movs r3, #0                 /* 偏移量清零 */
b LoopCopyDataInit          /* 跳转到公共拷贝逻辑 */