前面我们已经对IAR的icf链接文件进行了介绍,接下来我们再对MDK的scf链接文件进行相关介绍。(MDK的这个链接文件的名字叫做分散加载文件,其实两者的作用并没有什么不同,大同小异,都是为了将代码与地址一一对应)
相比较IAR的icf链接文件,section的定义是完全一样的,KEIL有自己默认的section段,区别就是名字起的不一样,在这里就不一一举例了,同样的我们也可以自定义setion。我们可以使用下面的语法在代码中定义自己的section:
__attribute__((section(".my_section_name"))) content
其中content可以是code和data。
在介绍scf文件之前,我们先了解一下执行域和加载域。我们只需要知道以下几点:
①执行域就是代码执行时所在的位置(换句话就是程序执行时跳转的地址)
②加载域就是代码存储的位置
③执行域可以等于加载域
④在代码执行前,会存在一个搬运工,这个搬运工就是MDK自己的初始化函数__main,它会将代码和数据从加载域拷贝到执行域,当然如果执行域等于加载域,则不需要拷贝。
说简单一点,KEIL的分散加载文件就决定了代码的执行域和加载域地址,我们可以非常方便的定义代码的组合。关于其语法这一篇文章《ARM Cortex-M底层技术(八)KEIL MDK 分散加载-2-语法》说的很细,大家可以自行去参考。
在这里我用非常土的语言简单描述一下常用到的东西。
从图中可以看到,一个加载域描述里可以放n个执行域描述,然后一个执行域描述里又可以放n个section,.o文件。其中LOAD_ROM1和EXEC_ROM1分别是加载域和执行域的名字,没有什么规范,随便起名字。
其中* 号就是放置的意思,举个例子*.o,意思就是把所有的.o文件放入,那么后面的括号是什么意思呢?后面的括号是限定条件,比如*.o(+RO) 的意思就是把所有.o文件里的RO属性的数据放入。比如*(.my_code_section)的意思就是把my_code_section这个段放入当前域。再比如*(RESET, +FIRST)的意思就是把RESET段放入且放在开头的地方。
如果我要指定放一个特定的.o文件,则可以直接写my_o_filename.o,不需要在前面加* 放置符号,同样的后面也可以加括号限定项。例如:
加载域名字 起始地址(FIXED) 大小 {
执行域名字 起始地址 大小 {
my_o_filename.o(+RO)
}
上面的意思就是把my_o_filename.o中的RO段放进去。中间有一个(FIXED),这个也是一个可选的修饰符,表示固定地址的意思,就是说我必须要把下面的这段数据放在这个地址处。
其中.ANY也是放入的意思,就是放其他还没有放的数据,后面的括号也是限定条件。具体后面再举例说明。
总结一下:非常简单,先定义一个加载域,在加载域里面定义执行域,在执行域里放东西。
下面我们以一个完整的scf文件对它进行一些解释,在对照着语法,你就非常明白了。
#!armclang --target=arm-arm-none-eabi -mcpu=cortex-m7 -E -x c
/*
** ###################################################################
** Processors: MIMXRT1052CVJ5B
** MIMXRT1052CVL5B
** MIMXRT1052DVJ6B
** MIMXRT1052DVL6B
**
** Compiler: Keil ARM C/C++ Compiler
** Reference manual: IMXRT1050RM Rev.2.1, 12/2018 | IMXRT1050SRM Rev.2
** Version: rev. 1.0, 2018-09-21
** Build: b191015
**
** Abstract:
** Linker file for the Keil ARM C/C++ Compiler
**
** Copyright 2016 Freescale Semiconductor, Inc.
** Copyright 2016-2019 NXP
** All rights reserved.
**
** SPDX-License-Identifier: BSD-3-Clause
**
** http: www.nxp.com
** mail: [email protected]
**
** ###################################################################
*/
#if (defined(__ram_vector_table__))
#define __ram_vector_table_size__ 0x00000400
#else
#define __ram_vector_table_size__ 0x00000000
#endif
#define m_flash_config_start 0x60000000
#define m_flash_config_size 0x00001000
#define m_ivt_start 0x60001000
#define m_ivt_size 0x00001000
#define m_interrupts_start 0x60002000
#define m_interrupts_size 0x00000400
#define m_text_start 0x60002400
#define m_text_size 0x03FFDC00
#define m_interrupts_ram_start 0x80000000
#define m_interrupts_ram_size __ram_vector_table_size__
#define m_data_start (m_interrupts_ram_start + m_interrupts_ram_size)
#define m_data_size (0x01E00000 - m_interrupts_ram_size)
#define m_ncache_start 0x81E00000
#define m_ncache_size 0x00200000
#define m_data2_start 0x20000000
#define m_data2_size 0x00020000
#define m_data3_start 0x20200000
#define m_data3_size 0x00040000
/* Sizes */
#if (defined(__stack_size__))
#define Stack_Size __stack_size__
#else
#define Stack_Size 0x0400
#endif
#if (defined(__heap_size__))
#define Heap_Size __heap_size__
#else
#define Heap_Size 0x0400
#endif
/*加载域定义, 还定义了属性和最大容量*/
LR_m_text m_flash_config_start m_text_start+m_text_size-m_flash_config_start { ; load region size_region
/*定义执行域地址大小,并定义属性 加载地址等于执行地址*/
RW_m_config_text m_flash_config_start FIXED m_flash_config_size { ; load address = execution address
/*把section .boot_hdr.conf放入,且放在最开始的地方*/
* (.boot_hdr.conf, +FIRST)
}
/*定义执行域地址大小,并定义属性 加载地址等于执行地址*/
RW_m_ivt_text m_ivt_start FIXED m_ivt_size { ; load address = execution address
* (.boot_hdr.ivt, +FIRST) //把section ..boot_hdr.ivt放入,且放在最开始的地方
* (.boot_hdr.boot_data) //把section .boot_hdr.boot_data紧跟着放入
* (.boot_hdr.dcd_data) //把section .boot_hdr.dcd_data紧跟着放入
}
/*定义执行域地址大小,并定义属性 加载地址等于执行地址*/
VECTOR_ROM m_interrupts_start FIXED m_interrupts_size { ; load address = execution address
* (.isr_vector,+FIRST) //把中断向量表放入当前执行域最开始的地方
}
/*定义执行域地址大小,并定义属性 加载地址等于执行地址*/
ER_m_text m_text_start FIXED m_text_size { ; load address = execution address
* (InRoot$$Sections) //把InRoot$$Sections放入当前执行域
.ANY (+RO) //将其他没有放置的只读RO段放入当前执行域
}
VECTOR_RAM m_interrupts_start EMPTY 0 {
}
RW_m_data2 m_data2_start m_data2_size {
* (RamFunction)
}
/*定义执行域地址大小,并定义属性 加载地址等于执行地址*/
RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size { ; RW data
.ANY (+RW +ZI) //把当前所有的RW和ZI段放入当前执行域
}
}
这样所生成的镜像文件,所以数据放置的顺序跟上面排布的顺序一致,如果两个加载域中间出现数据断层,则会直接将中间数据用0填充。
相比较IAR,用KEIL来链接代码至RAM,方法则是显得非常的单一,就是修改加载域和执行域的地址。接下来我们以NXP的RT1050举几个使用例子吧,(这里FLASH地址为0x6000000, RAM起始地址为0x20000000):
先在代码中使用下面代码自定义section:
__attribute__((section(".my_code_section")))
void my_code_fun(void)
{
printf("This is a test\r\n");
}
然后在分散加载文件中将自定义section的执行域放入RAM中,代码如下:
/*加载域名字 加载域大小 加载属性描述*/
LR_m_text m_flash_config_start m_text_start+m_text_size-m_flash_config_start { ; load region size_region
.......
.......
/*执行域名字 执行域大小 执行域属性描述为RW型*/
RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size { ; RW data
*(.my_code_section) //链接自定义的my_code_section
.ANY (+RW +ZI)
* (RamFunction)
* (NonCacheable.init)
* (*NonCacheable)
}
........
}
编译后查看map文件查看my_code_fun的链接地址:
运行代码,查看跳转地址:
(注意:如果我们想指定确定的代码和数据的执行域地址的话,比如我就想把这个代码或者数据放置在0x20001000处,这可以通过开辟多个执行域来实现,重点是在这个过程中需要通过对不同代码的大小分析,开辟的执行域不能存在重叠的情况。)
链接单个.o文件也是相对比较简单的,与链接单个section是一样的,这里我们使用下面代码将fsl_gpio.o链接至RAM:
/*加载域名字 加载域大小 加载属性描述*/
LR_m_text m_flash_config_start m_text_start+m_text_size-m_flash_config_start { ; load region size_region
.......
.......
/*执行域名字 执行域大小 执行域属性描述为RW型*/
RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size { ; RW data
*fsl_gpio.o(+RO) //链接指定的fsl_gpio.o文件
.ANY (+RW +ZI)
* (RamFunction)
* (NonCacheable.init)
* (*NonCacheable)
}
........
}
编译后查看map文件查看my_code_fun的链接地址:
运行代码,查看跳转地址:
链接整个代码至RAM,为了保证芯片正常启动,这里我将启动文件以及SystemInit函数所在的文件链接至了FLASH(因为入口函数在启动文件中以及__main函数之前调用了SystemInit),其他部分则链接至了RAM,代码如下:
/*加载域名字 加载域大小 加载属性描述*/
LR_m_text m_flash_config_start m_text_start+m_text_size-m_flash_config_start { ; load region size_region
.......
.......
ER_m_text m_text_start FIXED 0x4000 { ; load address = execution address
* (InRoot$$Sections)
*startup_mimxrt1052.o(+RO) //链接启动文件
*system_mimxrt1052.o(+RO) //链接SystemInit
}
/*执行域名字 执行域大小 执行域属性描述为RW型*/
RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size { ; RW data
.ANY (+RO)
.ANY (+RW +ZI)
* (RamFunction)
* (NonCacheable.init)
* (*NonCacheable)
}
........
}
查看文件发现基本上所有的代码都已经链接至了RAM,包括mian函数:
这里运行代码,查看跳转地址:
可以看到进入main函数,地址直接就跳到了RAM中。
(注意1:我们不能将所有的代码的执行域都放入至RAM中,换句话说就是entry入口地址必须得是FLASH,因为必须运行完MDK的初始化函数__main,而这个函数执行了代码拷贝至RAM的工作,所以需要在FLASH中执行完__main后,RAM中才会有代码,之后再跳转进入RAM中运行, 所以我们至少需要将__main函数之前的代码链接至FLASH中,不然是无法正常启动的。如果想完全将代码放入RAM中运行,则是可以直接将代码下载至RAM中,或者像RT1050这种支持non-XIP启动的MCU,则需要修改头信息,让BootROM实现所有代码的拷贝,类似于non-XIP启动。)
(注意2:如果想在正常启动过程中将中断向量表链接至RAM,则需要注意两个点,第一点就是将中断向量链接至RAM的同时还要保证芯片的正常启动,我们可以通过将与启动相关的部分中断向量表拷贝副本链接至FLASH保证正常启动,再将完整的中断向量链接至RAM。第二点则是需要通过修改SCB->VTOR寄存器改变中断向量映射地址。)
END