本帖最后由 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程序,只不过它比较简单,和应用程序链接在一起了。 重定位的这部分代码编写流程: 然后我的开发板使用的是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
我们调用一个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了,这个时候就可以使用绝对跳转指令,跳转到用户应用程序运行代码了。
当执行了上面两条指令,那么就已经是跳转到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 当然,不使用散列文件的话,也有很多其他取巧的解决办法,比如: 以上就是程序自己复制自己,把自己重定位到SDRAM运行的介绍。
整个demo程序也上传这里了,以供大家学习参考。
|
基于APM32系列的代码重定位讲解,从原理到实现逐步阐述,讲解细致,结构完整。