RAM运行flash代码详解
在单片机开发中,有时需要将代码编译到RAM中运行,比如flash资源紧张时利用RAM的IAP、放在RAM中提高代码运行速度、或者芯片平台要求部分代码必须编译到指定区域,比如小华的HC32L17系列。以小华的HC32L170为例,分享一下如何把代码编译到RAM中运行,以及实际是如何运行跳转的。
在单片机开发中,有时需要将代码编译到RAM中运行,比如flash资源紧张时利用RAM的IAP、放在RAM中提高代码运行速度、或者芯片平台要求部分代码必须编译到指定区域,比如小华的HC32L17系列。
以小华的HC32L170为例,分享一下如何把代码编译到RAM中运行。
基础知识
前几节主要介绍一些基础知识,帮助了解其中的原理,已了解的同学可以直接看【实操】章节
startup
先简单解析一下startup启动文件:
开辟栈
Stack_Size EQU 0x00000800
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
开辟堆
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
中断向量表
PRESERVE8
THUMB
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line0
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY
复位服务函数
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
假如需要重定向向量表的话,则将新向量表的地址装载进VTOR寄存器,注意,不是所有mcu都支持重定向向量表。
例:VTOR寄存器地址0xE000ED08,新向量表的地址0x0000A000
VTOR_RES EQU 0xE000ED08
new_vect_table EQU 0x0000A000
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =VTOR_RES
LDR R2, =new_vect_table
STR R2, [R0]
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
上电复位后的代码流程如下:
- 设置栈指针SP=__initial_sp
- 设置PC指针,跳转执行Reset_Handler
- 调用配置系统时钟函数
- 跳转C库_main函数(后面会讲解该函数)
- 调用main函数
map文件
下面再通过map文件来介绍一下flash、ram的结构
先看一下flash的整体分区:
OK,继续看下map文件。
中断向量表:
代码区,还可以看到代码区有PAD类型,padding,主要用来4字节对齐,可以用于提高cpu取指效率:
代码常量区,即const类型的全局变量,注意const类型的局部变量不是在这个区域而是在栈中,所以const类型的局部变量是可以通过指针来篡改的,这并不安全,所以建议const类型变量定义成全局
读写数据区,非0的变量初始化值就保存在这个区域,不过有的map文件中找不到这个区。
RAM的整体分区如下:
map文件中有Exec Addr和Load Addr,分别指运行地址和加载地址,几种地址的基本概念如下:
- 加载地址:将指令或数据从地址A拷贝到地址B,那地址A就是加载地址
- 链接地址:由链接脚本文件指出,链接的时候确定,静态的
- 运行地址:程序在内存中运行时候的地址,动态的
- 存储地址:指令或数据在flash中存放的地址,其实就是加载地址
- 代码重定向:将用户程序或者数据从存储地址拷贝到运行地址
在startup章节中上电复位后的代码流程的第四个步骤是调用__main函数,
而__main用于执行环境和应用执行的初始化。主要完成以下三个动作:
- 把全局区变量从加载地址复制到运行地址(但我在实际实践中,只读全局变量并没有拷贝,可能跟嵌入式平台相关,有了解的同学麻烦在评论区帮忙解答一下);
- 将.bss段初始化为0;
- 跳转到__rt_entry;
__rt_entry主要完成以下四个动作:
- 建立堆和栈;
- 初始化引用的库函数,初始化语言环境,为main()设置argc和argv;
- 调用main()函数(这个就是咱们自己写的main函数了);
- 使用main()函数返回值调用exit();
实操
以上是基础知识,以下是实践环节:
有两种实现方法,任选其一即可:
方式一 修改keil设置


然后选择运行地址区域就可以了。
方式二 修改sct文件
首先在需要重定向的函数定义和声明之前加:attribute((section(“RAMCODE”)))
其中,RAMCODE也可以用其他名字;
然后修改编译设置
最后修改sct文件,在sct文件中增加*.o (RAMCODE):
在此,顺便学习一下sct文件的语法:
LR_IROM1【加载域名】 0x00000000【起始地址】 0x00020000【大小】 { ; load region size_region
ER_IROM1【运行域名】 0x00000000【起始地址】 0x00020000【大小】 { ; load address = execution address
*.o (RESET, +First)【中断向量表,+First表示强制放在首地址】
*(InRoot$$Sections)【ARM相关库,InRoot$$Sections是ARM库的链接器标号】
.ANY (+RO)【只读区域】
.ANY (+XO)【只执行区域,即只有指令访问】
}
RW_IRAM1 0x20000000 0x00004000 { ; RW data
*.o (RAMCODE)
.ANY (+RW +ZI)【可读可写区域】
}
}
实例
ok,接着我们来看下效果,以Flash_Write32函数为例,该接口已用上述其中一个方法重定向到了RAM,
首先,查看map文件,可以看到,加载地址是flash中的0x0001d820,执行地址是RAM中的0x20000380
那程序具体是怎么跳转的呢?上面已说到,在__main函数中会先将加载地址的代码拷贝到运行地址。再看调用处,调用Flash_Write32函数,先跳转到0x00001ae8
而0x00001ae8处的程序是一个长跳转语句
其作用是要进一步跳转到Flash_Write32
而Symbol【Flash_Write32】则指向0x20000381,即最开始的运行地址。
(可能会有同学疑问,为什么有的程序地址和装载地址会相差1,这是因为ARM处理器有ARM指令和Thumb指令,执行不同的指令时需要切换到对应的ARM状态和Thumb状态,而不管是ARM指令还是Thumb指令,都至少是2字节对齐的,即最低位必定是0,设计师们就利用这个空闲的最低位用来区分ARM状态和Thumb状态。所以执行Thumb code,pc装载程序地址时最低位置1,0x20000381=0x20000380+1)
更多推荐



所有评论(0)