在单片机开发中,有时需要将代码编译到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

上电复位后的代码流程如下:

  1. 设置栈指针SP=__initial_sp
  2. 设置PC指针,跳转执行Reset_Handler
  3. 调用配置系统时钟函数
  4. 跳转C库_main函数(后面会讲解该函数)
  5. 调用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用于执行环境和应用执行的初始化。主要完成以下三个动作:

  1. 把全局区变量从加载地址复制到运行地址(但我在实际实践中,只读全局变量并没有拷贝,可能跟嵌入式平台相关,有了解的同学麻烦在评论区帮忙解答一下);
  2. 将.bss段初始化为0;
  3. 跳转到__rt_entry;

__rt_entry主要完成以下四个动作:

  1. 建立堆和栈;
  2. 初始化引用的库函数,初始化语言环境,为main()设置argc和argv;
  3. 调用main()函数(这个就是咱们自己写的main函数了);
  4. 使用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)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐