发新帖本帖赏金 150.00元(功能说明)我要提问
返回列表
打印
[APM32F4]

APM32代码重定位--如何让整个程序在SDRAM运行

[复制链接]
1099|12
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 luobeihai 于 2023-8-9 09:13 编辑

#申请原创#  @21小跑堂

在有些情况下,我们想要把代码放到SDRAM运行。下面介绍在APM32的MCU中,如何把代码重定位到SDRAM运行。对于不同APM32系列的MCU,方法都是一样的。
1. APM32启动模式
熟悉STM32的MCU都知道,可以通过配置 BOOT0/1 两个引脚的高低电平,选择不同的启动方式。对于APM32来说也是一样的,可以配置 BOOT0/1 引脚电平选择不同的启动模式,下表是我从APM32的用户手册截图的启动模式配置表:
有内置SRAM、Flash、系统存储区启动3种模式。对于让程序重定位到SDRAM运行,我们选择Flash启动即可。
不同的启动模式,本质其实就是MCU上电时,从哪个地址处读取出数据然后赋值给PC寄存器,以及SP寄存器。比如选择从Flash启动,MCU上电时,0x08000000处起始的4个字节的数据,赋值给SP寄存器,08000004处起始的4个字节的数据赋值给PC寄存器。设置了SP和PC寄存器,程序就可以正常运行了。
MCU上电,PC寄存器从哪里开始取值,和后面要讲的把代码搬运到SDRAM运行,是有关联的,所以这里提一下APM32的启动模式。
2. 程序段概念的引入
一个程序的源码被编译之后,链接器会根据代码中的不同属性,把他们划分为一个个不同的段,比如 .text段、.rodata段、.data段、.bss/.zi段等等,还有用户也可以自定义一些段,比如把所有初始化的代码,自定义一个初始化段。
  • .text段:代码段或者文本段。我们编写的代码,链接器都是归类在这个段的。对于MCU来说就是存放在内部的Flash中。
  • .rodata段:只读数据段。比如我们使用const定义的变量,或者定义的字符串这些,都被链接到只读数据段。只读数据段和代码段,都是只能读不能写,所以都是存放在Flash中的。
  • .data段:可读可写的数据段。我们定义的初始化为非0的全局变量、非0的静态局部变量,都是存放在这个段的。
  • .bss/.zi段:.bss段或者.zi段,都是同一个段,只是叫法不一样。.bss段和.data段是一样的,都是可读可写的数据段的一种。.bss段存放的就是初始值为0的全局变量或者初始值为0的静态局部变量。
    既然.data段和.bss段存放的内容基本一样,为什么要把这两个段分开存放?这是因为.bss段的初值是0,不需要烧录到Flash里面存放,在程序使用之前,我们把.bss段的对应区域给清0就行了,这样不需要浪费Flash空间。
  • :一段空闲的内存空间,可以给程序员自由使用。可以通过一些内存管理接口函数进行申请和释放。
  • :也是一块内存空间,不过程序自行管理。C语言的运行需要栈,MCU上电时就需要把SP(栈)寄存器指向一片正常可用的RAM作为栈来使用。

用户如果有需要,也可以自定义自己的段,然后链接器会根据用户的要求,把指定的代码编译到自定义的段。
我们要把代码重定位到SDRAM运行,本质就是复制这些段的数据,把这些数据复制到它们应该位于的地方(代码应该要位于链接地址处运行)
3. keil散列文件语法分析
前面提到,代码的重定位,本质就是数据的复制。数据的复制有3个要点要知道:从那里复制(源地址)、复制到哪里去(目的地址)、复制多长(长度)。
  • 源地址:我们要从哪里开始复制数据呢?其实就是加载地址,我们程序烧录到哪个位置,那就是加载地址。比如程序烧写到内部的Flash中,那么加载地址就是0x08000000 。程序存放到外部的SPI Flash中,加载地址就是存放在外部SPI Flash的地址。
  • 目的地址:要把程序复制到这里的地址,就是程序的链接地址,程序的运行就是要位于它的链接地址处运行(当然如果写的所有代码都是位置无关码,那么可以不在链接地址处运行)。
  • 长度:复制多长。

上面提到的这些复制数据所需的信息,所有的这一切都可以从链接脚本中获取到,它是用来指导链接器如何进行链接的一种规则文件。对于Keil来说,链接脚本指的就是散列文件
3.1 Keil默认的散列文件示例
下面的代码是keil自动生成的APM32F407ZG型号的散列文件:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00100000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00100000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x20000000 0x00020000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

一个散列文件,是由多个加载域、执行域和输入段所组成。可以通过Keil帮助文档获得这些内容的讲解。
3.2 散列文件语法
上面的示例,每个数据、符号代表什么意义?这些内容我们可以通过Keil的帮助文档学习到,安装了Keil软件,就可以获取到帮助文档信息了。
打开链接相关的文档,找到散列文件的语法介绍。
下图就是帮助文档关于散列文件的组成结构:
根据上图:一个散列文件可以一个或多个加载域,每个加载域又可以包含多个可执行域,而可执行域是由各个段(如代码段、数据段等)组成的。
3.2.1 加载域语法
load_region_description ::=
load_region_name ( base_address | ("+"  offset )) [ attribute_list ] [ max_size ]
"{"
execution_region_description +
"}"


load_region_name就是加载域的名称,base_address加载域的基地址,加载域的长度(就是整个程序烧录的大小)。然后加载域里面包含一个或多个可执行域。
以上面的示例为例:
LR_IROM1 0x08000000 0x00100000  {    ; load region size_region

}

加载域名称就是 LR_IROM1,起始地址0x08000000,长度0x00100000。
3.2.2 可执行域语法
execution_region_description ::=exec_region_name ( base_address | "+"  offset ) [ attribute_list ] [ max_size |  length ]"{"input_section_description *"}"


和加载域的描述也是类似的。然后可执行域里面就是由各个段组成的。
3.2.3 输入段
输入段描述如下:
前面是选择代码的哪些区域空间链接进这个段,后面是段的名字。
比如:main.o(+RO) 就是说main.o文件链接到RO段。
常见段类型的解析:
  • *.o (RESET, +First) :指的是所有的.o文件的RESET段,+First就是要求RESET段要链接到程序最开始的地方。
  • *(InRoot$$Sections):链接器去链接Keil自带的一部分代码。这部分代码的作用主要是数据段的重定位和清除bss段
  • .ANY (+RO):.ANY作用和 * 一样,指的是所有的意思,但是优先级比 * 低。这里说是把所有文件的RO段(Read Only段)放到这里。
  • .ANY (+XO):所有的 execute-only 段。

3.3 如何通过散列文件获取源、目的、长度
我们学习散列文件的目的就是为了得到代码重定位的源(加载地址)、目的(链接地址)、和长度。那么我们如何通过散列文件获取这些信息?
Keil的链接器定义了各种符号,通过这些符号我们可以获取到这些信息。
3.3.1 可执行域区域信息
通过可执行域的这些符号,我们就知道把代码复制到哪里去了。
3.3.2 加载域区域信息
通过加载域的这些信息,可以获取到各个段(代码段、数据段、bss段)的起始地址,和长度信息。
4. 代码重定位到SDRAM的方法
我们为什么要把代码重定位到SDRAM运行?
一般有两种情况:
  • 内部Flash空间不足,不能存放下所有的代码。这个时候我们就只能把编译得到的bin文件烧录到外部的存储设备了,比如SPI Flash等。但是SPI Flash根本就不能运行代码,所以MCU上电后就需要把SPI Flash的bin文件,搬运到SDRAM或者其他可运行代码的存储设备。
  • 为了得到更快的执行速率,这个时候我们可以把代码搬运到SRAM或者SDRAM执行。(但是对于MCU来说,我不确定是内部的Flash执行代码更快还是SDRAM更快)

对于第一种情况,是很常用的。比如嵌入式Linux的设备,就是这种启动方式,内核镜像存储在外部EMMC、SD卡等这种大容量设备中,但是他们都无法执行代码,所以上电后会把内核镜像搬运到内存中运行。
根据这两种情况,我们可以有两种方法把代码搬运到SDRAM运行。
  • 应用程序自己复制自己
  • 通过Bootloader程序,复制应用程序

4.1 程序自己复制自己
当整个应用程序都烧写在MCU的内部Flash时,这个时候我们可以使用这种方法,让应用程序自己复制自己到内存中,比如SDRAM运行。
当然在这种情况中,我好像想不到把程序复制到SDRAM运行的意义。是为了获得更快的代码运行速度?但是我也不确定程序在内部Flash运行更快还是SDRAM运行更快?
可能唯一的意义可以就是可以学习到代码重定位相关的知识了吧......
废话少说,程序它为什么可以自己复制自己?
我们前面介绍过,程序运行应该位于它的链接地址上,但是当这个程序的所有代码都是位置无关码的时候,它就可以不在链接地址上运行。位置无关码就是这段代码可以在任何地址上正常运行,它与执行的位置无关。
位置无关码编写要求
  • 汇编指令,不能使用绝对跳转。比如对PC指针赋值跳转,或者使用跳转指令,跳转到某个地址值。比如下面这些就是绝对跳转
    ; 下面这种跳转方式就是绝对跳转
    ldr PC, =main

    LDR R0, =SystemInit
    BLX R0

    ; BL指令是相对跳转指令
    BL main

  • C言语,不能使用函数指针调用函数。因为函数指针调用方式就是指向一个确定的地址值,而这个地址是链接时分配的地址,代码没有在链接地址处运行的时候,跳转过去程序只能崩溃
  • 对于访问数据的话,不要去访问全局变量、静态局部变量、字符串。

我们把应用程序烧写到MCU内部的Flash时,可以在应用程序的最前面一部分代码, 放置重定位相关的代码,这重定位相关的代码编写要求就是必须使用位置无关码编写。
因为当我们要把程序放到SDRAM运行时,就需要修改程序的链接地址指向SDRAM的内存空间,而MCU刚上电,是从Flash的空间开始取指令运行的,所以Flash最开始的那一部分代码必须是位置无关码,否则就无法正常运行。
4.2 Bootloader复制应用程序
当应用程序太大,无法烧录到内部Flash时,就只能烧写到外部Flash。
这个时候,就可以编写一个简单的程序(当然你也可以做得很复杂,做到适配各种外部存储设备,各种协议什么的),它的主要作用就是复制应用程序到链接地址处运行。
然后把这个程序烧写到内部的Flash上,MCU上电,可以先运行这段代码,然后把外部的应用程序复制到内存(SDRAM)运行,复制完成之后再跳转到内存运行即可。这个程序通常被叫做Bootloader。
嵌入式Linux设备就是使用这种方式启动的,当然启动过程比这里说的还要复杂,但是总体启动过程类似。
5. 代码重定位到SDRAM运行的过程
前面讲了很多内容,大家可以不用看,只看最后这章,看看代码怎么写就行了。重定位的代码其实也很简单,本质就是复制数据,而复制数据调用一个memcpy函数就足够了。
上面介绍了两种代码重定位到SDRAM运行的方法,下面我只讲第一种方法。其实大家也可以把前面这部分重定位的代码看作是Bootloader程序,只不过它比较简单,和应用程序链接在一起了。
重定位的这部分代码编写流程:
  • 初始化系统时钟
  • 初始化SDRAM,因为访问SDRAM需要设置SDRAM的时序参数
  • .text段重定位
  • .data段重定位
  • .bss/zi段清零
  • 重新设置中断向量表寄存器的基地址
  • 绝对跳转到SDRAM运行

然后我的开发板使用的是APM32F407ZG型号,下面的代码是适配这个型号的。当然APM32系列的其他型号,重定位的思路方法都是一样的,参考着来就行。
5.1 修改散列文件
我们的目的是要把程序放到SDRAM运行,所以我们必须修改链接地址在SDRAM的内存空间,让链接器根据这段内存空间分配地址。而修改链接地址,对于Keil来说就要修改散列文件。
我使用的芯片型号是APM32F407ZG,而且外面接的SDRAM的起始地址是0x60000000,大小一共是2MB。所以修改出来的散列文件如下:
LR_IROM1 0x08000000 0x00100000  {    ; load region size_region
  ER_IROM1 0x60000000 0x00200000  {  ; load address = execution address
   *.o (RESET, +First)
   ;*(InRoot$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x20000000 0x00020000  {  ; RW data
   .ANY (+RW +ZI)
  }
  
  ARM_LIB_HEAP +0 EMPTY 0x0200 { ; Heap region growing up
  }

  ARM_LIB_STACK +0 EMPTY 0x0400 { ; Stack region growing down
  }
}

加载域的地址不变,因为我们还是要下载到Flash中存储程序的。但是执行域修改为了SDRAM的起始地址和大小。然后可读可写的数据域地址和大小没改,依旧是使用MCU内部的SRAM作为数据存储区。
但是下面我新增了两个执行域,那就是堆和栈,这两个于的地址是紧接着 RW_IRAM1域进行编排地址的,大小分别是 0x200 和 0x400。为什么要加上这两个域下面讲解。
5.2 初始化系统时钟和SDRAM
初始化时钟相关的代码,直接调用APM32 SDK提供的SystemInit函数即可,但是要更改一下调用方式为相对跳转。
我们是要把代码从MCU内部的Flash复制到SDRAM运行,那么复制之前,必须能正确的读写SDRAM才能正常复制,而SDRAM的读写需要先初始化它的时序参数才行。
我的板子使用的SDRAM型号是:EM638165TS-7IG,然后关于这个SDRAM型号的初始化代码,可以从官方的 APM32F4xx_EVAL_SDK_V1.0 这个SDK中,稍微修改下就可以拿过来使用了。
然后我们在汇编代码的 Reset_Handler 函数(标号)中调用这几个函数:
BL SystemInit
BL SDRAM_GPIOConfig
BL SDRAM_Init

5.3 .text段重定位
各个段的重定位,就是数据的复制。我们必须要知道,源、目的、长度。而这些信息,都在散列文件中获取到。前面花了很大篇幅讲了散列文件作用就在这里。
代码如下:
IMPORT |Image$ER_IROM1$Base|
IMPORT |Image$ER_IROM1$Length|
IMPORT |Load$ER_IROM1$Base|

; relocate text section
LDR R0, = |Image$ER_IROM1$Base|    ; destination
LDR R1, = |Load$ER_IROM1$Base|     ; source
LDR R2, = |Image$ER_IROM1$Length|  ; lenth
BL mymemcpy

ER_IROM1 就段名,这个段就是 .text 代码段的意思。
  • |Load$$ER_IROM1$$Base| :.text段加载地址,就是复制数据的源地址
  • |Image$$ER_IROM1$$Base|  :.text段执行域地址,也就是链接地址,就是复制数据的目的地址
  • |Image$$ER_IROM1$$Length| :.text段的长度

我们通过上面那几个奇奇怪怪的符号,就可以获得.text段的加载地址,链接地址,和长度了。知道这些信息,然后复制数据调用一个memcpy就可以把Flash的.text段复制到SDRAM了。
5.4 .data段重定位
.data段,存放的就是各种初值不是0的全局变量、静态局部变量,我们也需要把这部分数据的初始值,从Flash空间中复制到对应的内存中去。
我们在散列文件,数据段的链接地址是设置在0x20000000地址处的。
重定位代码如下:
IMPORT |Image$RW_IRAM1$Base|
IMPORT |Image$RW_IRAM1$Length|
IMPORT |Load$RW_IRAM1$Base|

; relocate data section
LDR R0, = |Image$RW_IRAM1$Base|    ; destination
LDR R1, = |Load$RW_IRAM1$Base|     ; source
LDR R2, = |Image$RW_IRAM1$Length|  ; lenth
BL mymemcpy

代码基本和.text段重定位差不多的,就是那几个符号不一样,是获取.data段的加载地址、链接地址和长度的符号。
5.5 .bss/zi段清零
.bss/zi段 是初始值为0的全局变量和静态局部变量存放的地址,但是这个段的内容不会存放在Flash中,因为初值为0,我们在使用之前把这个段的内存空间清0即可,无需浪费空间把编译进bin文件里面。
IMPORT |Image$RW_IRAM1$ZI$Base|
IMPORT |Image$RW_IRAM1$ZI$Length|

; clear bss/zi
LDR R0, = |Image$RW_IRAM1$ZI$Base|    ; destination
MOV R1, #0                                                            ; Value
LDR R2, = |Image$RW_IRAM1$ZI$Length|  ; lenth
BL bss_section_clear

  • |Image$$RW_IRAM1$$ZI$$Base|  : 是bss/zi段的链接地址
  • |Image$$RW_IRAM1$$ZI$$Length|  : bss/zi段的长度

我们调用一个memset函数就可以把对应的内存段清0了。
5.6 重新设置中断向量表寄存器基地址
中断向量表的基地址默认是0x08000000处的,当中断发生时,MCU会自动到这个地址处找到对应的中断处理函数,然后跳转过去。
但是我们需要把代码搬运到了SDRAM运行,如果还想要正常使用中断处理函数的话,就必须修改中断向量表的基地址到链接的起始地址,也就是0x60000000。
修改方法也很简单,SCB->VTOR 修改这个寄存器的值就行了,这个寄存器是内核相关的寄存器,它的地址是0xE000ED08.
代码如下:
; set interrupt vector base address to 0x60000000
ldr r0, =__Vectors   
ldr r1, =0xE000ED08   ; SCB->VTOR register address is 0xE000ED08
str r0, [r1]

ldr指令,把__Vectors的链接地址(其实就是0x60000000)加载到r0寄存器中,然后再使用str指令,把这个值写到0xE000ED08地址处。
5.7 一些问题
做完前面的过程,基本就完成了把代码重定位到SDRAM了,这个时候就可以使用绝对跳转指令,跳转到用户应用程序运行代码了。
LDR     R0, =mymain
BX      R0

当执行了上面两条指令,那么就已经是跳转到SDRAM运行代码了,因为mymain的函数链接地址,就是位于SDRAM那边的。
但是我在这个过程中遇到了一些问题。
5.7.1 Reset_Handler 中断问题

file://E:/%E5%8D%9A%E5%AE%A2%E6%96%87%E7%AB%A0/picture/image-20230808233404427.png?lastModify=1691512138
在中断向量表的第二个位置,会放这个 Reset_Handler 的函数在那里。这个是MCU上电的时候,就会从这个位置,然后把这个 Reset_Handler 的函数地址赋值给PC指针,然后让MCU从 Reset_Handler 开始不断的执行代码。
程序被烧录到内部的Flash中,然后0x08000004地址开始存放的,就是 Reset_Handler 的地址了。
但是问题是 Reset_Handler 这个函数名,链接器是使用链接地址给它分配地址值的,也就是它的地址是 0x60000000 开始的某个地址。然后MCU上电,就把这个 0x60000000 开始的某个地址,赋值给了PC指针,这个时候会怎么样?
因为这时还没有把代码复制到SDRAM,那MCU从那里取不到正确的指令,只能是无法执行下去。
所以我们不能使用 Reset_Handler 函数名放在那个位置,而是要把 0x08000000 开始的某个地址放在那里,因为我们把程序烧录到了 0x08000000 处。
然后这个数值怎么得到?通过反汇编文件

file://E:/%E5%8D%9A%E5%AE%A2%E6%96%87%E7%AB%A0/picture/image-20230808234433096.png?lastModify=1691512138
这里的 +1 是因为ARM公司有两种指令集,ARM 指令集和 Thumb 指令集,其中指令的 bit0 位为1,那就代表是Thumb指令。而Cortex-M3/M4 内核使用的就是Thumb指令集,所以会 +1.
所以才会看到中断向量表的第二个位置放了个奇怪的数据: 0x080003f5,就是这么来的。
5.7.2 清除bss/zi段时把堆栈也给清除了
在运行bss段清0的代码时,我发现代码就死掉了。通过 .map 文件分析,发现Keil把堆和栈的大小,也归类为了bss里面了,这可能是 .s 文件定义堆栈的方式导致的,我没有去深究。
如果把堆栈都归类了bss段了,那么清除bss段时,就把堆栈给干掉了,堆可能关系还不大,因为还没有用到堆内存。但是把栈清0了,肯定不行,因我们前面的代码有C语言写的代码,C函数的调用,局部变量都用到了栈。
解决方式其实很简单,既然把堆栈都归类为bss段,那么我们想办法不让Keil把堆栈归类到bss段即可。
我是直接在散列文件那里定义堆栈的大小了,然后通过链接器的符号:|Image$$ARM_LIB_STACK$$ZI$$Limit| 获取到栈顶指针的地址值,然后在中断向量表的第一个位置填这个符号即可。

file://E:/%E5%8D%9A%E5%AE%A2%E6%96%87%E7%AB%A0/picture/image-20230809000650638.png?lastModify=1691512138
当然,不使用散列文件的话,也有很多其他取巧的解决办法,比如:
  • 调用 bss 段清0函数之前,重新设置 SP 的值
  • bss 段清0函数内部,长度信息减去堆栈的空间长度

以上就是程序自己复制自己,把自己重定位到SDRAM运行的介绍。

整个demo程序也上传这里了,以供大家学习参考。
APM32F4xx_SDK_V1.4 - ExecuteInSDRAM.zip (1.34 MB)



  

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 150.00 元 2023-08-15
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
21小跑堂 2023-8-15 10:40 回复TA
基于APM32系列的代码重定位讲解,从原理到实现逐步阐述,讲解细致,结构完整。 
沙发
Fanexs168| | 2023-8-14 09:10 | 只看该作者
点赞~

使用特权

评论回复
板凳
tpgf| | 2023-9-5 12:07 | 只看该作者
全部在sdram中运行的话 是提高速度还是降低速度了呢

使用特权

评论回复
地板
aoyi| | 2023-9-5 13:23 | 只看该作者
程序运行过程会不会受到数据传输速度的影响呢

使用特权

评论回复
5
gwsan| | 2023-9-5 16:16 | 只看该作者
所有的数据也在sdram里边处理吗

使用特权

评论回复
评论
luobeihai 2023-9-11 21:58 回复TA
数据存储的话,我这里还是使用的内部的RAM。想让数据段也全部都放在SDRAM,修改散列文件的RAM地址就行。需要主要的是,刚上电时要等到SDRAM初始化完成之后,才能使用SDRAM。 
6
tfqi| | 2023-9-5 20:23 | 只看该作者
如果突然掉电了的话 就会产生异常吧

使用特权

评论回复
评论
luobeihai 2023-9-11 21:51 回复TA
突然掉电当然不能正常运行了。但是再次上电之后就没有影响,还是可以正常再次运行。无论是把代码下载到内部的Flash,还是外部的Flash、SD卡等设备,再次上电,都应该要能正常运行的。 
7
zljiu| | 2023-9-5 21:01 | 只看该作者
这样做会很大程度提高代码的运算速度?

使用特权

评论回复
8
nawu| | 2023-9-5 21:58 | 只看该作者
所有的数据也在sdram中传输吗

使用特权

评论回复
评论
luobeihai 2023-9-11 21:49 回复TA
数据段我没有让它链接到SDRAM那边。想让所有数据都在SDRAM那边,修改下散列文件的数据域(也就是RAM)的地址,这样链接器就会把定义的变量全部链接到SDRAM的地址空间了。 但是要注意的是,一定要等SDRAM初始化完成之后才能使用SDRAM,也就是说刚上电的时候,设置栈指针还是得指向内部的RAM地址。 
发新帖 本帖赏金 150.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

11

主题

50

帖子

1

粉丝