STM32链接脚本详解

程序的编译分为四个步骤:预处理、汇编、编译、链接。在开发STM32时,我们只要在IDE中点击编译就能一次性完成这4个步骤,实际上IDE也是要经过这些步骤的,只不过IDE为我们屏蔽了很多细节。

首先我们需要了解一个image文件的构成。image即编译的产物,我们编译STM32生成的bin文件此处称之为image。一个image文件由RO段和RW段组成,RO段包含只读的代码段和常量,RW段包含可读可写的全局变量和静态变量。因为程序刚运行时,RW段还在FLASH中,需要一段程序将这些变量复制到RAM中,STM32的启动文件的__main函数帮我们完成了这一动作。RW段中初始值为0的段为ZI段,image文件无需包含ZI段,因为ZI段包含的是全局或静态初始值为0的变量,只要在程序运行后,将对应的RAM区域清零即可。

这里又涉及到另一个概念:加载地址和运行地址。加载地址是指读取程序的地址,运行地址是指程序运行的入口地址。STM32因为有XIP(executed in place)技术,加载地址和运行地址是一样的,都是0x08000000。简而言之,如果程序在FLASH中运行,加载地址和运行地址是相同的,例如STM32等单片机;如果程序存放在FLASH里,而运行是在RAM里,那么加载地址指向FLASH,运行地址指向RAM,例如跑Linux系统的一些芯片。上面的RW段,其加载地址指向FLASH,而运行地址指向RAM,因此需要拷贝。

如何指定各个C文件的编译产物(.o格式)在RO段的顺序?又如何确定程序的加载地址和运行地址呢?这都是靠一个脚本来完成的,即链接脚本。在Linux下,链接脚本为lds文件;在KEIL中,链接脚本为sct文件;在IAR中,链接脚本为icf文件。本文以KEIL下的sct文件为例,讲解链接脚本结构。

我们可以通过编写一个分散加载文件来指定 ARM 连接器在生成映像文件时如何分配 Code、RO-Data, RW-Data, ZI-Data 等数据的存放地址。称为分散加载文件实际上就是链接脚本,如果不修改KEIL的链接脚本,那么会使用默认的链接脚本,我们按照下图的操作方式来查看默认的链接脚本,方法为点击工程设置,找到Link选项,去掉“Use Memory Layout from Target Dialog”前面的勾选,然后点击Edit。

不使用KEIL的默认链接脚本

 查看到的默认链接脚本如下,注:本例中使用的MCU型号为STM32F103RC,FLASH容量为256KB,RAM大小为64KB。

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00040000  {    ; 加载时域起始地址为0x08000000,大小为0x40000
  ER_IROM1 0x08000000 0x00040000  {  ; 第一个运行时域,运行地址为0x08000000,大小为0x40000
   *.o (RESET, +First)               ; RESET段最先链接,RESET段在启动文件中有声明
   *(InRoot$$Sections)               ; 链接__main函数,该函数用于RW段数据的拷贝和ZI段数据的清零
   .ANY (+RO)                        ; 剩余的code、RO数据随意链接
  }
  RW_IRAM1 0x20000000 0x0000C000  {  ; 第二个运行时域,运行地址为0x20000000,大小为0xC000
   .ANY (+RW +ZI)                    ; 存放所有的RW段数据和ZI段数据
  }
}

分散加载文件主要由一个加载时域和多个运行时域组成。

加载时域,顾名思义用于加载并存储数据,包括 Code、 RO-Data 和 RW-Data。

运行时域, 用于为运行时分配变量及代码映射空间, 包含 Code、 ZI-Data、 RW-Data。
 

分散加载文件的组成

 分散加载有3条规则需要特别注意:

1、第一个运行时域的基址必须与加载域基址相同。

2、第一个运行时域存放的代码不会进行额外拷贝。

3、一个加载时域,有且仅有一个不拷贝的运行时域,FIXED关键字修饰除外。
 

分散加载的用途有很多,例如:

1、我们可以通过修改分散加载文件将部分代码或整个代码放到RAM中运行以提高运行速度。

2、可以将一组函数放在特定地址上,作为Firmware供app程序调用。

3、将程序分成boot和app,实现升级功能。实现这个功能可以不修改分散加载文件,直接在keil里设置即可。

4、定义section来灵活地存放特定的数据。

 

关于分散加载的更多知识,可以参考周立功写的一篇文档《keil分散加载文件浅释》,我已经传到百度网盘:

链接:https://pan.baidu.com/s/1heC-pLmi_eeqS_SU19dmIA      提取码:iguq 

 

有时候我们需要在程序运行时知道各个段的起始地址、结束地址、大小等信息,这些信息链接器已经帮我们导出了,下面给出了一个使用的例子,这个例子实际上完成了__main的部分功能,即把FLASH中的RW段数据拷贝到RAM的运行地址上,并将RAM中的ZI段数据清零。

void RW_And_ZI_Init (void)
{
    extern unsigned char Image$$ER_IROM1$$Limit;       // 获取RW段在FLASH中的加载地址
    extern unsigned char Image$$RW_IRAM1$$Base;        // 获取RW段在RAM中的运行地址    
    extern unsigned char Image$$RW_IRAM1$$RW$$Limit;   // 获取RW段在RAM中的结束地址
    extern unsigned char Image$$RW_IRAM1$$ZI$$Limit;   // 获取ZI段在RAM中的结束地址
    unsigned char * psrc, *pdst, *plimt;
		
    psrc  = (unsigned char *)&Image$$ER_IROM1$$Limit;
    pdst  = (unsigned char *)&Image$$RW_IRAM1$$Base;
    plimt = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
    while(pdst < plimt)     // 将FLASH中的RW段拷贝到RAM的RW段运行地址上
    {
        *pdst++ = *psrc++;
    }

    psrc  = (unsigned char *)&Image$$RW_IRAM1$$RW$$Limit;
    plimt = (unsigned char *)&Image$$RW_IRAM1$$ZI$$Limit;
    while(psrc < plimt)     // 将RAM中的ZI段清零
    {
        *psrc++ = 0;
    }
}  

 

发布了6 篇原创文章 · 获赞 3 · 访问量 234

猜你喜欢

转载自blog.csdn.net/qq_27575841/article/details/104373417