本实验基于STM32CubeIDE,ST 推出的免费集成编译环境,基于 Eclipse 开源框架,集成了 GCC、GDB 等免费的编译器、链接器。需要对内存进行细分时,比如指定变量/函数/文件到特殊地址等等,可以通过“*.ld”链接文件来实现。
ld即Linked Script,ST官网有一个官方文档 LAT0816 用来简单介绍ld如何编写。使用IDE创建好功能之后,项目根目录有两个文件STM32XXX_FLASH.ld
和 STM32XXX_RAM.ld
,我们需要修改的是前者,后者是在线调试时使用,修改后对编译的结果没有作用。
ld文件分MEMORY
和SECTIONS
两个章节,前者用来定义存储器的分段结构,后者可以视作一个逻辑层面的段,用来将变量标识到指定位置。
以下是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的某位置,同时还带有初始值,比如带初始值的全局变量。针对这个应用场景,就需要将变量编译到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中测试,赋予的初始值并没有生效。
经过反复折腾均告失败,由此先在这里打个记号吧,等后续解决之后再来更新。现阶段只能手动拷贝了。
时隔一年多,无意中解决了这个问题。在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 /* 跳转到公共拷贝逻辑 */