JFlash 烧写之链接脚本介绍
在 RT-Thread 实时操作系统中,链接脚本(Linker Script)定义了如何将代码和数据映射到微控制器的内存中。链接脚本通常以 .ld
为扩展名。对于特定的微控制器,如 Renesas R7FA4M2AC3C,链接脚本中的 MEMORY
部分将详细说明内存的配置,包括不同区域的起始地址和大小。
以下是 MEMORY
部分的一个示例结构,它可能包含在 Renesas RX 系列微控制器的 RT-Thread 链接脚本中:
MEMORY
{
/* 定义内存区域 */
ROM (rx) : ORIGIN = 0x00000000, LENGTH = 512K /* 代码段,Flash内存 */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K /* 数据段,RAM内存 */
}
在这个 MEMORY
部分:
-
ROM
是代码存储区域的标签,rx
表示这部分内存是可读(read)和可执行(execute)的。ORIGIN
是区域的起始地址,LENGTH
是区域的大小。在这个示例中,ROM
被配置为从0x00000000
开始的 512KB 大小的空间,这通常对应于 Flash 存储器。 -
RAM
是数据存储区域的标签,rwx
表示这部分内存是可读(read)、可写(write)和可执行(execute)的。在这个示例中,RAM
被配置为从0x20000000
开始的 64KB 大小的空间,这通常对应于内部 SRAM。
请注意,以上地址和大小仅为示例。
在实际使用时,链接脚本可能包含更多的内存区域和复杂的配置,如堆(heap)和栈(stack)的定义、特殊功能区域(如存储配置参数的区域)等。开发者可能需要根据应用的需求和微控制器的内存资源来调整链接脚本。
如瑞萨的 4M2AC,有两块 RAM 空间,则需要在链接脚本中为每个 RAM 区域配置一个内存段。对于瑞萨的 RX 系列微控制器,通常会有内部 RAM 和可能的外部 RAM。 以下是一个可能的链接脚本配置,它定义了两个 RAM 区域:
MEMORY {
/* 定义第一块内存区域,例如内部 RAM */
RAM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 128K /* 例如,内部 RAM 的起始地址和大小 */
/* 定义第二块内存区域,例如外部 RAM */
RAM2 (rwx) : ORIGIN = 0x20020000, LENGTH = 128K /* 例如,外部 RAM 的起始地址和大小 */
}
在这个 MEMORY
部分:
RAM1
和RAM2
是用来标识不同 RAM 区域的标签,rwx
表示这些内存区域是可读(read)、可写(write)和可执行(execute)的。ORIGIN
是每个 RAM 区域的起始地址,LENGTH
是每个区域的大小。
通常情况下,内部 RAM 的地址较低,如果有外部 RAM,其地址将更高。所提供的起始地址 0x20000000
和 0x20020000
,以及长度 128K
,仅为示例。具体值需要根据你的 SoC 的实际内存映射来确定,这些信息可以在瑞萨 4M2AC 的数据手册或技术参考手册中找到。
在定义完 MEMORY
区域后,你需要在链接脚本中的任何相关部分(如堆区、栈区定义)指定应该使用哪一块 RAM。例如:
/* 定义堆和栈的大小 */
_estack = ORIGIN(RAM1) + LENGTH(RAM1); /* 假设栈顶位于 RAM1 的顶部 */
_heap_start = ORIGIN(RAM2); /* 假设堆起始位于 RAM2 的底部 */
_heap_end = ORIGIN(RAM2) + LENGTH(RAM2); /* 假设堆结束位于 RAM2 的顶部 */
确保在 RT-Thread 的系统初始化代码中正确初始化这两个 RAM 区域,使得操作系统可以管理和分配这两块内存。在启动 RT-Thread 之前,你可能需要调用微控制器的初始化函数来配置外部 RAM 的控制器(如果使用外部 RAM)。
当使用 ARM GCC 编译器和链接器编译 RT-Thread 项目时,链接脚本(通常是 .ld
文件)用于指导链接器如何将代码、数据、堆栈和其他段映射到目标微控制器的内存中。链接脚本定义了内存布局,包括各个内存段(如 .text
、.data
、.bss
等)的位置和大小。
在 RT-Thread 的链接脚本中,通常有以下几个关键部分:
- MEMORY 区段: 定义微控制器的内存布局,包括起始地址和长度。
- SECTIONS 区段: 定义如何将各个段映射到
MEMORY
区段中定义的内存区域。 - 符号定义: 如
_estack
(栈顶地址),_sdata
(已初始化数据段起始地址)等,这些符号会在链接脚本和 RT-Thread 的启动代码(通常是汇编写的启动文件)之间共享。
MEMORY 区段示例
MEMORY {
ROM (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 96K
}
在上述示例中:
ROM
是微控制器 Flash 的标识符,rx
表示可读和可执行。ROM
的起始地址是0x08000000
,长度是512K
。RAM
是微控制器 RAM 的标识符,rwx
表示可读、可写和可执行。RAM
的起始地址是0x20000000
,长度是96K
。
SECTIONS 区段示例
SECTIONS {
.text :
{
*(.vectors)
*(.text)
*(.rodata)
} > ROM
.data : AT(ADDR(.text) + SIZEOF(.text))
{
_sdata = .;
*(.data)
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss) *(COMMON)
_ebss = .;
} > RAM
}
在上述示例中:
.text
段包括向量表、代码段和只读数据,被放置在ROM
中。.data
段包括已初始化的数据,被放置在RAM
中,并且是从ROM
的.text
段之后的地址开始(使用AT()
关键字)。.bss
段包括未初始化的数据,被放置在RAM
中。
符号定义
链接脚本中还定义了一些符号,这些符号在启动代码中被引用来初始化全局数据。
例如:
_sdata = .; /* 定义全局数据的起始地址 */
_edata = .; /* 定义全局数据的结束地址 */
_sbss = .; /* 定义未初始化数据的起始地址 */
_ebss = .; /* 定义未初始化数据的结束地址 */
这些符号的值在启动代码中被用来执行数据复制(.data
从 Flash 到 RAM)和将 .bss
区域清零等操作。
启动代码
当 RT-Thread 启动时,启动代码(通常是汇编写的 startup.s
或 startup.S
文件)会使用这些链接脚本中定义的符号来准备运行环境。它会:
- 将
.data
段从 Flash 复制到 RAM。 - 将
.bss
段清零。 - 设置栈指针(使用
_estack
)。 - 跳转到 RT-Thread 的
main
函数或初始化函数。
实际使用
在实际编译过程中,链接脚本将被指定给链接器,通常在 Makefile
或构建系统配置文件中:
LDFLAGS += -T path/to/linker_script.ld
链接器会根据这个脚本来生成可执行文件,确保代码和数据正确地定位在微控制器的内存中。因此,正确配置链接脚本对于生成正确的固件至关重要。
在 RT-Thread 的链接脚本中,.data
段是已初始化数据的一部分,它需要从 Flash(非易失性存储)复制到 RAM(易失性存储)中,因为当程序运行时,它需要对这些数据进行读写操作。该操作是在启动代码(通常是汇编语言编写的启动文件)中完成的。
AT
关键字在链接脚本中用于指定虽然段在执行期间在 RAM 中,但在程序文件中这部分数据的位置应该是在 Flash 中的某个位置。ADDR
和 SIZEOF
是链接器的内建函数,用来分别获得段的起始地址和大小。
下面的链接脚本代码片段:
.data : AT (ADDR(.text) + SIZEOF(.text))
{
/* 数据段定义 */
_sdata = .; /* 已初始化数据段的起始地址 */
*(.data)
_edata = .; /* 已初始化数据段的结束地址 */
} > RAM
解释如下:
.data
:这指定了.data
段在链接脚本中的部分。AT (ADDR(.text) + SIZEOF(.text))
:该表达式告诉链接器将.data
段的内容放置在程序文件中的位置。它是紧接在.text
段之后的位置。ADDR(.text)
获取.text
段的起始地址,SIZEOF(.text)
获取.text
段的大小。这意味着.data
段在物理文件(例如:你的.bin
或.elf
文件)中紧随.text
段之后。_sdata = .;
:该行定义了一个全局符号_sdata
,它标记了.data
段在 RAM 中的起始地址。*(.data)
:这是一个通配符,它指示链接器包括所有.data
段的内容。_edata = .;
:该行定义了另一个全局符号_edata
,它标记了.data
段在 RAM 中的结束地址。> RAM
:这说明.data
段应该被加载到RAM
,即上文MEMORY
区段定义中的RAM
。
当微控制器启动时,启动代码会利用 _sdata
和 _edata
符号来确定需要将多少数据从 Flash 复制到 RAM 中,以及复制的目标地址。这个过程保证了在程序开始执行前,所有初始化的全局变量和静态变量都被设置为它们在编译时被赋予的值。
Reset_Handler:
ldr sp, =_estack /* set stack pointer */
/* copy data section from flash to SRAM */
movs r1, #0
b data_section_copy
CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4
data_section_copy:
ldr r0, =_sdata
ldr r3, =_edata
adds r2, r0, r1
cmp r2, r3
bcc CopyDataInit
ldr r2, =_sbss
b bss_section_init
/* Zero fill the bss segment. */
bss_clear:
movs r3, #0
str r3, [r2], #4
bss_section_init:
ldr r3, = _ebss
cmp r2, r3
bcc bss_clear
ARM BCC 指令介绍
在 ARM 架构的指令集中,BCC
是一个条件跳转指令,其中 CC
代表条件码(Condition Code)。这个指令会根据处理器状态寄存器(CPSR)中的标志位来决定是否跳转到指定的目标地址。如果条件满足,处理器将跳转到标签指定的地址执行指令;如果条件不满足,则继续执行下一条指令。
ARM 架构提供了一系列的条件码,每个条件码对应不同的状态寄存器标志位的组合。以下是一些常见的条件码及其代表的意义:
-
EQ
:相等(Zero flag is set, Z=1)
-
NE
:不相等(Zero flag is clear, Z=0)
-
CS
/HS
:进位置位/无符号数大于等于(Carry flag is set, C=1)
-
CC
/LO
:进位清除/无符号数小于(Carry flag is clear, C=0)
-
MI
:负数(Negative flag is set, N=1)
-
PL
:正数或零(Negative flag is clear, N=0)
-
VS
:溢出(Overflow flag is set, V=1)
-
VC
:无溢出(Overflow flag is clear, V=0)
-
HI
:无符号数大于(C=1 and Z=0)
-
LS
:无符号数小于或等于(C=0 or Z=1)
-
GE
:有符号数大于等于(N=V)
-
LT
:有符号数小于(N!=V)
-
GT
:有符号数大于(Z=0 and N=V)
-
LE
:有符号数小于或等于(Z=1 or N!=V)
BCC 指令使用举例
考虑下面的 ARM 汇编代码片段,我们使用 BCC
指令作为例子:
CMP R1, R2 ; 比较 R1 和 R2 的值
BCC target_label ; 如果不产生进位(即 R1 < R2),则跳转到标签 'target_label'
; 否则继续执行下面的指令
...
target_label:
; 如果满足条件跳转,则会执行到这里的代码
...
在这个例子中,CMP
指令用来比较寄存器 R1
和 R2
的值,并设置条件码。如果 R1
小于 R2
,则不会产生进位(C=0
),BCC
条件成立,处理器会跳转到 target_label
。如果 R1
大于等于 R2
,则会产生进位(C=1
),BCC
条件不成立,处理器会继续执行下一条指令。
记住,ARM 指令的条件跳转依赖于 CPSR 中的标志位,而这些标志位可能会被之前执行的指令如 CMP
、ADD
、SUB
等设置。因此,你总是需要仔细考虑代码的逻辑顺序和条件标志的状态。