(1) 编译,MDK 软件使用的编译器是 armcc 和 armasm,它们根据每个 c/c++ 和汇编源文件编译成对应的以“.o”为后缀名的对象文件 (Object Code,也称目标文件),其内容主要是从源文件编译得到的机器码,包含了代码、数据以及调试使用的信息;
(2) 链接,链接器 armlink 把各个.o 文件及库文件链接成一个映像文件“.axf”或“.elf”;
(3) 格式转换,一般来说 Windows 或 Linux 系统使用链接器直接生成可执行映像文件 elf 后,内核根据该文件的信息加载后,就可以运行程序了,但在单片机平台上,需要把该文件的内容加载到芯片上,所以还需要对链接器生成的 elf 映像文件利用格式转换器 fromelf 转换成“.bin”或“.hex”文件,交给下载器下载到芯片的 FLASH 或 ROM 中。
构建工程的提示输出主要分 6 个部分,说明如下:
(1) 提示信息的第一部分说明构建过程调用的编译器。图中的编译器名字是“V5.06(build 20)”,后面附带了该编译器所在的文件夹。在电脑上打开该路径,可看到该编译器包含图编译工具 中的各个编译工具,如 armar、armasm、armcc、armlink 及 fromelf,后面四个工具已在图 MDK 编译过程 中已讲解,而 armar 是用于把.o 文件打包成 lib 文件的。
(2) 使用 armasm 编译汇编文件。图中列出了编译 startup 启动文件时的提示,编译后每个汇编源文件都对应有一个独立的.o 文件。
(3) 使用 armcc 编译 c/c++ 文件。图中列出了工程中所有的 c/c++ 文件的提示,同样地,编译后每个 c/c++ 源文件都对应有一个独立的.o 文件。
(4) 使用 armlink 链接对象文件,根据程序的调用把各个.o 文件的内容链接起来,最后生成程序的axf 映像文件,并附带程序各个域大小的说明,包括 Code、RO-data、RW-data 及 ZI-data 的大小。
(5) 使用 fromelf 生成下载格式文件,它根据 axf 映像文件转化成 hex 文件,并列出编译过程出现的错误 (Error) 和警告 (Warning) 数量。
(6) 最后一段提示给出了整个构建过程消耗的时间。构建完成后,可在工程的“Output”及“Listing”目录下找到由以上过程生成的各种文件,见图编译后 Output 及 Listing 文件夹中的内容 。
可以看到,每个 C 源文件都对应生成了.o、.d 及.crf 后缀的文件,还有一些额外的.dep、.hex、.axf、
.htm、.lnp、.sct、.lst 及.map 文件。
在工程的编译提示输出信息中有一个语句“Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx”,它说明了程序各个域的大小,编译后,应用程序中所有具有同一性质的数据 (包括代码) 被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状态,这些域的意义如下:
• Code:即代码域,它指的是编译器生成的机器指令,这些内容被存储到 ROM 区。
• RO-data:Read Only data,即只读数据域,它指程序中用到的只读数据,这些数据被存储在ROM 区,因而程序不能修改其内容。例如 C 语言中 const 关键字定义的变量就是典型的RO-data。
• RW-data:Read Write data,即可读写数据域,它指初始化为“非 0 值”的可读写数据,程序刚运行时,这些数据具有非 0 的初始值,且运行的时候它们会常驻在 RAM 区,因而应用程序可以修改其内容。例如 C 语言中使用定义的全局变量,且定义时赋予“非 0 值”给该变量进行初始化。
• ZI-data:Zero Initialie data,即 0 初始化数据,它指初始化为“0 值”的可读写数据域,它与RW-data 的区别是程序刚运行时这些数据初始值全都为 0,而后续运行过程与 RW-data 的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改其内容。例如 C 语言中使用定义的全局变量,且定义时赋予“0 值”给该变量进行初始化 (若定义该变量时没有赋予初始值,编译器会把它当 ZI-data 来对待,初始化为 0);
• ZI-data 的栈空间 (Stack) 及堆空间 (Heap):在 C 语言中,函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。而使用 malloc 动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于ZI-data 区域的,这些空间都会被初始值化为 0 值。编译器给出的 ZI-data 占用的空间值中包含了堆栈的大小 (经实际测试,若程序中完全没有使用 malloc 动态申请堆空间,编译器会优化,不把堆空间计算在内)。
以程序的组成构件为例,它们所属的区域类别见表程序组件所属的区域:
描述:应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如 STM32 的内部 FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中 (RAM),掉电后这些数据会丢失。
图中的左侧是应用程序的存储状态,右侧是运行状态,而上方是 RAM 存储器区域,下方是 ROM存储器区域。
注意:在 MDK 中,我们建立的工程一般会选择芯片型号,选择后就有确定的 FLASH 及 SRAM 大小,若代码超出了芯片的存储器的极限,编译器会提示错误,这时就需要裁剪程序了,裁剪时可针对超出的区域来优化。
调用这些编译工具,需要用到 Windows 的“命令行提示符工具”,为了让命令行方便地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。
例如上述文章中本机的路径为“D:work\keil5\ARM\ARMCC\bin”。
3.1.1 添加路径到 PATH 环境变量
本文以 Win7 系统为例添加工具链的路径到 PATH 环境变量,其它系统是类似的。
(1) 右键电脑系统的“计算机图标”,在弹出的菜单中选择“属性”,见图计算机属性页面 ;
(2) 在弹出的属性页面依次点击“高级系统设置”->“环境变量”,在用户变量一栏中找到名为“PATH”的变量,若没有该变量,则新建一个。编辑“PATH”变量,在它的变量值中输入工具链的路径,如本机的是“;D:\work\keil5\ARMARMCC\bin”,注意要使用“分号;”让它与其它路径分隔开,输入完毕后依次点确定,见图添加工具链路径到 PATH 变量 ;
(3)Windows+R打开快捷命令界面,输入cmd并确认即可打开命令行,见图打开命令行 ;
(4) 在弹出的命令行窗口中输入“fromelf”回车,若窗口打印出 formelf 的帮助说明,那么路径正常,就可以开始后面的工作了;若提示“不是内部名外部命令,也不是可运行的程序…”信息,说明路径不对,请重新配置环境变量,并确认该工作目录下有编译工具链。
总结:这个过程本质就是让命令行通过“PATH”路径找到“fromelf.exe”程序运行,默认运行“fromelf.exe”时它会输出自己的帮助信息,这就是工具链的调用过程,MDK 本质上也是如此调用工具链的,只是它集成为 GUI,相对于命令行对用户更友好。
armcc 用于把 c/c++ 文件编译成 ARM 指令代码,编译后会输出 ELF 格式的 O 文件 (对象、目标文件),在命令行中输入“armcc”回车可调用该工具,它会打印帮助说明,见图 armcc 的帮助提示
帮助提示中分三部分,第一部分是 armcc 版本信息,第二部分是命令的用法,第三部分是主要命令选项。
根据命令用法:armcc [options] fifile1 fifile2 …fifilen ,在 [option] 位置可输入下面的“–arm”、“–cpu list”选项,若选项带文件输入,则把文件名填充在 fifile1 fifile2…的位置,这些文件一般是 c/c++ 文件。例如根据它的帮助说明,“–cpu list”可列出编译器支持的所有 cpu,我们在命令行中输入“armcc–cpu list”,可查看图 cpulist 中的 cpu 列表。
打开 MDK 的 Options for Targe->c/c++ 菜单,可看到 MDK 对编译器的控制命令,见图 MDK 的ARMCC 编译选项 。
从该图中的命令可看到,它调用了-c、-cpu –D –g –O1 等编译选项,当我们修改 MDK 的编译配置时,可看到该控制命令也会有相应的变化。然而我们无法在该编译选项框中输入命令,只能通过 MDK 提供的选项修改。
了解这些,我们就可以查询具体的 MDK 编译选项的具体信息了,如 c/c++ 选项中的“Optimization:Leve 1(-O1)”是什么功能呢?首先可了解到它是“-O”命令,命令后还带个数字,在keil中点击"Help->uVision Help"查看 MDK 的帮助手册,在 armcc 编译器说明章节,可详细了解,如图编译器选项说明 。
帮助手册:
利用 MDK,我们一般不需要自己调用 armcc 工具,但经过这样的过程我们就会对 MDK 有更深入的认识,面对它的各种编译选项,就不会那么头疼了。
armasm 是汇编器,它把汇编文件编译成 O 文件。与 armcc 类似,MDK 对 armasm 的调用选项可在“Option for Target->Asm”页面进行配置,见图 armasm 与 MDK 的编译选项 。
armlink 是链接器,它把各个 O 文件链接组合在一起生成 ELF 格式的 AXF 文件,AXF 文件是可执行的,下载器把该文件中的指令代码下载到芯片后,该芯片就能运行程序了;利用 armlink 还可以控制程序存储到指定的 ROM 或 RAM 地址。在 MDK 中可在“Option for Target->Linker”页面配置 armlink 选项,见图 armlink 与 MDK 的配置选项 。
链接器默认是根据芯片类型的存储器分布来生成程序的,该存储器分布被记录在工程里的 sct 后缀的文件中,有特殊需要的话可自行编辑该文件,改变链接器的链接方式。
armar 工具用于把工程打包成库文件,fromelf 可根据 axf 文件生成 hex、bin 文件,hex 和 bin 文件是大多数下载器支持的下载文件格式。
在 MDK 中,针对 armar 和 fromelf 工具的选项几乎没有,仅集成了生成 HEX 或 Lib 的选项,见图控制 fromelf 生成 hex 及控制 armar 生成 lib 的配置 。
如果我们想利用 fromelf 生成 bin 文件,可以在 MDK 的“Option for Target->User”页中添加调用 fromelf 的指令,见图在 MDK 中添加指令 。
在 User 配置页面中,提供了三种类型的用户指令输入框,在不同组的框输入指令,可控制指令的执行时间,分别是编译前 (Before Compile c/c++ fifile)、构建前 (Before Build/Rebuild) 及构建后(AfterBuild/Rebuild) 执行。这些指令并没有限制必须是 arm 的编译工具链,例如如果您自己编写了 python 脚本,也可以在这里输入用户指令执行该脚本。
图中的生成 bin 文件指令调用了 fromelf 工具,紧跟后面的是工具的选项及输出文件名、输入文件名。由于 fromelf 是根据 axf 文件生成 bin 的,而 axf 文件又是构建 (build) 工程后才生成,所以我们把该指令放到“After Build/Rebuild”一栏。
除了上述编译过程生成的文件,MDK 工程中还包含了各种各样的文件,下面我们统一介绍,MDK工程的常见文件类型见表 MDK 常见的文件类型 。
这些文件主要分为 MDK 相关文件、源文件以及编译、链接器生成的文件。
在工程的“Project”目录下主要是 MDK 工程相关的文件,见图 Project 目录下文件
uvprojx 文件就是我们平时双击打开的工程文件,它记录了整个工程的结构,如芯片类型、工程包
含了哪些源文件等内容,见图工程包含的文件 _ 芯片类型等内容 。
uvoptx 文件记录了工程的配置选项,如下载器的类型、变量跟踪配置、断点位置以及当前已打开
的文件等等,见图工程中选择使用的下载器类型 。
uvguix 文件记录了 MDK 软件的 GUI 布局,如代码编辑区窗口的大小、编译输出提示窗口的位置
等等。
uvprojx、uvoptx 及 uvguix 都是使用 XML 格式记录的文件,若使用记事本打开可以看到 XML 代码,见图 XML 格式的记录 。而当使用 MDK 软件打开时,它根据这些文件的 XML 记录加载工程的各种参数,使得我们每次重新打开工程时,都能恢复上一次的工作环境。
这些工程参数都是当 MDK 正常退出时才会被写入保存,所以若 MDK 错误退出时 (如使用 Windows 的任务管理器强制关闭),工程配置参数的最新更改是不会被记录的,重新打开工程时要再次配置。根据这几个文件的记录类型,可以知道 uvprojx 文件是最重要的,删掉它我们就无法再正常打开工程了,而 uvoptx 及 uvguix 文件并不是必须的,可以删除,重新使用 MDK 打开 uvprojx工程文件后,会以默认参数重新创建 uvoptx 及 uvguix 文件。(所以当使用 Git/SVN 等代码管理的时候,往往只保留 uvprojx 文件)
源文件:就是我们编写的各种源代码,MDK 支持 c、cpp、h、s、inc 类型的源代码文件,其中 c、cpp 分别是 c/c++ 语言的源代码,h 是它们的头文件,s 是汇编文件,inc 是汇编文件的头文件,可使用“$include”语法包含。编译器根据工程中的源文件最终生成机器码。
点击 MDK 中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件,在工程的“Options for Targe->Output->Select Folder for Objects”和“Options for Targe->Listing->Select Folder for Listings”选项配置它们的输出路径
编译后 Output 和 Listing 目录下生成的文件见图编译后 Output 及 Listing 文件夹的内容 。
注意:下面讲解Output文件夹下文件
在某些场合下我们希望提供给第三方一个可用的代码库,但不希望对方看到源码,这个时候我们就可以把工程生成 lib 文件 (Library fifile) 提供给对方,在 MDK 中可配置“Options for Target->Create Library”选项把工程编译成库文件,见图生成库文件或可执行文件 。
工程中生成可执行文件或库文件只能二选一,默认编译是生成可执行文件的,可执行文件即我们下载到芯片上直接运行的机器码。
得到生成的 *.lib 文件后,可把它像 C 文件一样添加到其它工程中,并在该工程调用 lib 提供的函数接口,除了不能看到 *.lib 文件的源码,在应用方面它跟 C 源文件没有区别
*.dep 和 *.d 文件 (Dependency fifile) 记录的是工程或其它文件的依赖,主要记录了引用的头文件路径,其中 *.dep 是整个工程的依赖,它以工程名命名,而 *.d 是单个源文件的依赖,它们以对应的源文件名命名。这些记录使用文本格式存储,我们可直接使用记事本打开,见图工程的 dep 文件内容 和图 bsp_led_d 文件的内容 。
*.crf 是交叉引用文件 (Cross-Reference fifile),它主要包含了浏览信息 (browse information),即源代码中的宏定义、变量及函数的定义和声明的位置。
我们在代码编辑器中点击“Go To Defifinition Of ‘xxxx’”可实现浏览跳转,见图浏览信息 ,跳转的时候,MDK 就是通过 *.crf 文件查找出跳转位置的。
通过配置 MDK 中的“Option for Target->Output->Browse Information”选项可以设置编译时是否生成浏览信息,见图在 OptionsforTarget 中设置是否生成浏览信息 。只有勾选该选项并编译后,才能实现上面的浏览跳转功能。
*.crf 文件使用了特定的格式表示,直接用文本编辑器打开会看到大部分乱码,见图 crf 文件内容,我们不作深入研究。
总结:*.o、.elf、.axf、.bin 及.hex 文件都存储了编译器根据源代码生成的机器码,根据应用场合的不同,它们又有所区别。
ELF 文件说明
*.o、.elf、.axf 以及前面提到的 lib 文件都是属于目标文件,它们都是使用 ELF 格式来存储的,关于 ELF 格式的详细内容请参考配套资料里的《ELF 文件格式》文档了解,它讲解的是 Linux 下的ELF 格式,与 MDK 使用的格式有小区别,但大致相同。在本教程中,仅讲解 ELF 文件的核心概念。
ELF 是 Executable and Linking Format 的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。在 Linux 及 Windows 系统下都有使用该格式的文件 (或类似格式) 用于记录应用程序的内容,告诉操作系统如何链接、加载及执行该应用程序。
目标文件主要有如下三种类型:
(1) 可重定位的文件 (Relocatable File),包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。
例如 MDK 的 armcc 和 armasm 生成的 *.o 文件就是这一类,另外还有 Linux 的 *.o 文件,Windows的 *.obj 文件。
(2) 可执行文件 (Executable File) ,它包含适合于执行的程序,它内部组织的代码数据都有固定的地址 (或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。
例如 MDK 的 armlink 生成的 *.elf 及 *.axf 文件,(使用 gcc 编译工具可生成 *.elf 文件,用 armlink生成的是 *.axf 文件,.axf 文件在.elf 之外,增加了调试使用的信息,其余区别不大,后面我们仅讲解 *.axf 文件),另外还有 Linux 的/bin/bash 文件,Windows 的 *.exe 文件。
(3) 共享目标文件 (Shared Object File), 它的定义比较难理解,我们直接举例,MDK 生成的 *.lib文件就属于共享目标文件,它可以继续参与链接,加入到可执行文件之中。
另外,Linux 的.so,如/lib/ glibc-2.5.so,Windows 的 DLL 都属于这一类。
1.o 文件与 axf 文件的关系
根据上面的分类,我们了解到,.axf 文件是由多个.o 文件链接而成的,而 *.o 文件由相应的源文件编译而成,一个源文件对应一个 *.o 文件。它们的关系见图 axf 文件与 o 文件的关系 。
图中的中间代表的是 armlink 链接器,在它的右侧是输入链接器的 .o 文件,左侧是它输出的 axf文件。
由于都使用 ELF 文件格式,.o 与.axf 文件的结构是类似的,它们包含 ELF 文件头、程序头、节区 (section) 以及节区头部表。各个部分的功能说明如下:
• ELF 文件头用来描述整个文件的组织,例如数据的大小端格式,程序头、节区头在文件中的位置等。
• 程序头告诉系统如何加载程序,例如程序主体存储在本文件的哪个位置,程序的大小,程序要加载到内存什么地址等等。MDK 的可重定位文件 *.o 不包含这部分内容,因为它还不是可执行文件,而 armlink 输出的 *.axf 文件就包含该内容了。
• 节区是 *.o 文件的独立数据区域,它包含提供给链接视图使用的大量信息,如指令 (Code)、数据 (RO、RW、ZI-data)、符号表 (函数、变量名等)、重定位信息等,例如每个由 C 语言定义的函数在 *.o 文件中都会有一个独立的节区;
• 存储在最后的节区头则包含了本文件节区的信息,如节区名称、大小等等。
**总的来说,链接器把各个 .o 文件的节区归类、排列,根据目标器件的情况编排地址生成输出,汇总到 .axf 文件。
2.ELF 文件头
使用命令行,切换到文件所在的目录,输入“fromelf –text –v bsp_led.o”命令,可控制输出 bsp_led.o的详细信息,见图使用 fromelf 查看 o 文件信息 。利用“-c、-z”等选项还可输出反汇编指令文件、
3.程序头
4.节区头
5.节区主体及反汇编代码
6.分散加载代码
1.生成 hex 文件
在“Options for Target->Output->Create Hex File”中勾选该选项,然后编译工程即可。
2.生成 bin 文件
使用 MDK 生成 bin 文件需要使用 fromelf 命令,在 MDK 的“Options For Target->Users”中加入图
使用 fromelf 指令生成 bin 文件 中的命令。
图中的指令内容为:
“fromelf –bin –output …Output 流水灯.bin …Output 流水灯.axf”
注意:该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它,才能为自己的工程定制指令,首先看看 fromelf 的帮助,见图 fromelf 的帮助 。
MDK 输入的指令格式是遵守 fromelf 帮助里的指令格式说明的,其格式为:“fromelf [options] input_fifile”
设置完成生成 hex 的选项或添加了生成 bin 的用户指令后,点击工程的编译 (build) 按钮,重新编译工程,成功后可看到图 fromelf 生成 hxe 及 bin 文件的提示 中的输出。
其中 bin 文件是纯二进制数据,无特殊格式。
3.hex 文件格式
描述:hex 是 Intel 公司制定的一种使用 ASCII 文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到 ROM 中的数据,绝大多数下载器支持该格式。
一个 hex 文件由多条记录组成,而每条记录由五个部分组成,格式形如“:llaaaatt[dd…] cc”
(1) 02:表示这条记录数据区的长度为 2 字节;
(2) 0000:表示这条记录要存储到的地址;
(3) 04:表示这是一条扩展线性地址记录;
(4) 0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高 16 位,与前面的“0000”结合在一起,表示要扩展的线性地址为“0x0800 0000”,这正好是 STM32 内部 FLASH 的首地址;
(5) F2:表示校验和,它的值为 (0x02+0x00+0x00+0x04+0x08+0x00)%256 的值再取补码。再来看第二条记录:
(1) 10:表示这条记录数据区的长度为 2 字节;
(2) 0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的 FLASH 首地址为 (0x0800 0000+0x0000);
(3) 00:表示这是一条数据记录,数据区的是地址;
(4) 000400204501000829030008BF020008:这是要按地址存储的数据;
(5) 81: 校验和
4.hex、bin 及 axf 文件的区别与联系
bin、hex 及 axf 文件都包含了指令代码,但它们的信息丰富程度是不一样的。
• bin 文件是最直接的代码映像,它记录的内容就是要存储到 FLASH 的二进制数据 (机器码本质上就是二进制数据),在 FLASH 中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片 FLASH 平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);
• hex 文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到 FLASH 的哪个地址,下载器可以根据这些信息辅助下载;
• axf 文件在前文已经解释,它不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。
同一个工程生成的 bin、hex 及 axf 文件的大小见图文件大小 。
实际上,这个工程要烧写到 FLASH 的内容总大小为 1492 字节,然而在 Windows 中查看的 bin 文件却比它大 ( bin 文件是 FLASH 的代码映像,大小应一致),这是因为 Windows 文件显示单位的原因,使用右键查看文件的属性,可以查看它实际记录内容的大小,见图 bin 文件大小 。
使用 fromelf 工具输出的反汇编文件“流水灯 _axf_elfInfo_c.txt”文件,清晰地对比它们的差异,见图同一个工程的bin_hex 及 axf 文件对代码的记录 。
在 hex 文件中包含了地址信息以及地址中的内容,而在 bin 文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex 及 axf 文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三都之间的区别与联系。
由于文件中存储的都是机器码,见图 GPIO_Init 函数的代码数据在三个文件中的表示 ,该图是我根据 axf 文件的 GPIO_Init 函数的机器码,在 bin 及 hex 中找到的对应位置。所以经验丰富的人是有可能从 bin 或 hex 文件中恢复出汇编代码的,只是成本较高,但不是不可能。
注意:如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在 FLASH 中的数据,从而得到 bin 映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件 PCB,抄出一样的板子,再把 bin 映像下载芯片,直接山寨出目标产品,所以在实际的生产中,一定要注意做好加密措施。
在静态调用图文件中包含了整个工程各种函数之间互相调用的关系图,而且它还给出了静态占用最深的栈空间数量以及它对应的调用关系链。
该文件说明了本工程的静态栈空间最大占用 32 字节 (Maximum Stack Usage:32bytes),这个占用最深的静态调用为“main->LED_GPIO_Confifig->GPIO_Init”。注意这里给出的空间只是静态的栈使用统计,链接器无法统计动态使用情况,例如链接器无法知道递归函数的递归深度。在本文件的后面还可查询到其它函数的调用情况及其它细节。
利用这些信息,我们可以大致了解工程中应该分配多少空间给栈,有空间余量的情况下,一般会设置比这个静态最深栈使用量大一倍,在 STM32 中可修改启动文件改变堆栈的大小;如果空间不足,可从该文件中了解到调用深度的信息,然后优化该代码。
主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及
“映像组件大小”。
1.节区的跨文件引用
2.删除无用节区
3.符号映像表
4.存储器映像索引
5.映像组件大小
在默认的 sct 文件配置中仅分配了 Code、RO-data、RW-data 及 ZI-data 这些大区域的地址,链接时各个节区 (函数、变量等) 直接根据属性排列到具体的地址空间。
sct 文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号“{}”分隔开,最外层的是加载域,第二层“{}”内的是执行域,其整体结构见图分散加载文件的整体结构 。
1.加载域
2.执行域
执行域的格式与加载域是类似的,区别只是输入节区的描述有所不同,在代码清单:MDK-18 的例子中包含了 ER_IROM1 及 RW_IRAM 两个执行域,它们分别对应描述了 STM32 的内部 FLASH及内部 SRAM 的基地址及空间大小。而它们内部的“输入节区描述”说明了哪些节区要存储到这些空间,链接器会根据它来处理编排这些节区。
3.输入节区描述
各部分介绍如下:
• 模块选择样式:模块选择样式可用于选择 o 及 lib 目标文件作为输入节区,它可以直接使用目标文件名或“”通配符,也可以使用“.ANY”。例如,使用语句“bsp_led.o”可以选择bsp_led.o 文件,使用语句“.o”可以选择所有 o 文件,使用“.lib”可以选择所有 lib 文件,使用“”或“.ANY”可以选择所有的 o 文件及 lib 文件。其中“.ANY”选择语句的优先级是最低的,所有其它选择语句选择完剩下的数据才会被“.ANY”语句选中。
• 输入节区样式:我们知道在目标文件中会包含多个节区或符号,通过输入节区样式可以选择要控制的节区。
示例文件中“(RESET,+First)”语句的 RESET 就是输入节区样式,它选择了名为 RESET 的节区,并使用后面介绍的节区特性控制字“+First”表示它要存储到本区域的第一个地址。
示例文件中的“(InRoot$$Sections)”是一个链接器支持的特殊选择符号,它可以选择所有标
准库里要求存储到 root 区域的节区,如 __main.o、__scatter.o 等内容。
• 输入符号样式:同样地,使用输入符号样式可以选择要控制的符号,符号样式需要使用“:gdef:”来修饰。
例如可以使用“*(:gdef:Value_Test)”来控制选择符号“Value_Test”。
• 输入节区属性:通过在模块选择样式后面加入输入节区属性,可以选择样式中不同的内容,每个节区属性描述符前要写一个“+”号,使用空格或“,”号分隔开,可以使用的节区属性描述符见表属性描述符及其意义。
例如,示例文件中使用“.ANY(+RO)”选择剩余所有节区 RO 属性的内容都分配到执行域ER_IROM1 中,使用“.ANY(+RW +ZI)”选择剩余所有节区 RW 及 ZI 属性的内容都分配到执行域 RW_IRAM1 中。
• 节区特性:节区特性可以使用“+FIRST”或“+LAST”选项配置它要存储到的位置,FIRST存储到区域的头部,LAST 存储到尾部。通常重要的节区会放在头部,而 CheckSum(校验和)之类的数据会放在尾部。
例如示例文件中使用“(RESET,+First)”选择了 RESET 节区,并要求把它放置到本区域第一个位置,而RESET 是工程启动代码中定义的向量表,见代码清单:MDK-19 ,该向量表中定义的堆栈顶和复位向量指针必须要存储在内部 FLASH 的前两个地址,这样 STM32 才能正常启动,所以必须使用 FIRST 控制它们存储到首地址。
总的来说,我们的 sct 示例文件配置如下:程序的加载域为内部 FLASH 的 0x08000000,最大空间为 0x00080000;程序的执行基地址与加载基地址相同,其中 RESET 节区定义的向量表要存储在内部 FLASH 的首地址,且所有 o 文件及 lib 文件的 RO 属性内容都存储在内部 FLASH 中;程序执行时 RW 及 ZI 区域都存储在以 0x20000000 为基地址,大小为 0x00010000 的空间 (64KB),这部分正好是 STM32 内部主 SRAM 的大小。
链接器根据 sct 文件链接,链接后各个节区、符号的具体地址信息可以在 map 文件中查看。
1.选择 sct 文件的产生方式
首先需要选择 sct 文件产生的方式,选择使用 MDK 生成还是使用用户自定义的 sct 文件。在 MDK的“Options for Target->Linker-> Use Memory Layout from Target Dialog”选项即可配置该选择 。
该选项的译文为“是否使用 Target 对话框中的存储器分布配置”,勾选后,它会根据“Options for Target”对话框中的选项生成 sct 文件,这种情况下,即使我们手动打开它生成的 sct 文件编辑也是无效的,因为每次构建工程的时候,MDK 都会生成新的 sct 文件覆盖旧文件。该选项在 MDK中是默认勾选的,若希望 MDK 使用我们手动编辑的 sct 文件构建工程,需要取消勾选,并通过ScatterFile 框中指定 sct 文件的路径,见图使用指定的 sct 文件构建工程 。
2.通过 Target 对话框控制存储器分配
若我们在 Linker 中勾选了“使用 Target 对话框的存储器布局”选项,那么“Options for Target”对话框中的存储器配置就生效了。
主要配置是在 Device 标签页中选择芯片的类型,设定芯片基本的内部存储器信息以及在 Target 标签页中细化具体的存储器配置 (包括外部存储器)。
图中 Device 标签页中选定了芯片的型号为 STM32F103ZE,选中后,在 Target 标签页中的存储器信息会根据芯片更新。
在 Target 标签页中存储器信息分成只读存储器 (Read/Only Memory Areas) 和可读写存储器(Read/Write Memory Areas) 两类,即 ROM 和 RAM,而且它们又细分成了片外存储器 (offff-chip)和片内存储器 (on-chip) 两类。
可以发现,sct 文件根据 Target 标签页做出了相应的改变,除了这种修改外,在 Target 标签页上还控制同时使用 IRAM1 和 IRAM2、加入外部 RAM(如外接的 SRAM),外部 FLASH 等。
3.控制文件分配到指定的存储空间
可以看到在 sct 文件中的 RW_IRAM2 执行域中增加了一个选择 bsp_led.o 中 RW 内容的语句。
类似地,我们还可以设置某些文件的代码段被存储到特定的 ROM 中,或者设置某些文件使用的ZI-data 或 RW-data 存储到外部 SRAM 中 (控制 ZI-data 到 SDRAM 时注意还需要修改启动文件设置堆栈对应的地址,原启动文件中的地址是指向内部 SRAM 的)
取消了这个勾选后,在 MDK 的 Target 对话框及文件配置的存储器分布选项都会失效,仅以 sct文件中的为准,更改对话框及文件配置选项都不会影响 sct 文件的内容。
(1) 修改启动文件,在 __main 执行之前初始化“指定的存储空间”的硬件;
(2) 在 sct 文件中增加“指定的存储空间”对应的执行域;
(3) 使用节区选择语句选择要分配到“指定的存储空间”的内容;
(4) 编写测试程序,编译正常后,查看 map 文件的空间分配情况。
1.在 __main 之前初始化外部“指定的存储空间”的硬件
注意: 在本工程中,由于使用内部 SRAM 空间,不需要初始化,所以实际上不需要修改启动文件,保持与普通工程的一致即可
2.sct 文件初步应用
3.变量分配测试及结果
main.c
#include "stm32f10x.h"
#include "./usart/bsp_usart.h"
#include "./led/bsp_led.h"
#include "./sram/sram.h"
#include
void Delay(__IO u32 nCount);
//定义变量到“指定的存储空间”
uint32_t testValue =7 ;
//定义变量到“指定的存储空间”
uint32_t testValue2 =0;
//定义数组到“指定的存储空间”
uint8_t testGrup[100] ={0};
//定义数组到“指定的存储空间”
uint8_t testGrup2[100] ={1,2,3};
/*本实验中的sct配置,若使用外部存储器时,堆区工作可能不正常,
使用malloc无法得到正常的地址,不推荐在实际工程应用*/
/*另一种我们推荐的配置请参考教程中的说明*/
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
uint32_t inerTestValue =10;
/* LED 端口初始化 */
LED_GPIO_Config();
/* 初始化串口 */
USART_Config();
printf("\r\nSCT文件应用——自动分配变量到“指定的存储空间”实验\r\n");
printf("\r\n使用“ uint32_t inerTestValue =10; ”语句定义的局部变量:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d\r\n",(uint32_t)&inerTestValue,inerTestValue);
printf("\r\n使用“uint32_t testValue =7 ;”语句定义的全局变量:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d\r\n",(uint32_t)&testValue,testValue);
printf("\r\n使用“uint32_t testValue2 =0 ; ”语句定义的全局变量:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d\r\n",(uint32_t)&testValue2,testValue2);
printf("\r\n使用“uint8_t testGrup[100] ={0};”语句定义的全局数组:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n",(uint32_t)&testGrup,testGrup[0],testGrup[1],testGrup[2]);
printf("\r\n使用“uint8_t testGrup2[100] ={1,2,3};”语句定义的全局数组:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n",(uint32_t)&testGrup2,testGrup2[0],testGrup2[1],testGrup2[2]);
/*本实验中的sct配置,若使用外部存储器时,堆区工作可能不正常,
使用malloc无法得到正常的地址,不推荐在实际工程应用*/
/*另一种我们推荐的配置请参考教程中的说明*/
uint32_t * pointer = (uint32_t*)malloc(sizeof(uint32_t)*3);
if(pointer != NULL)
{
*(pointer)=1;
*(++pointer)=2;
*(++pointer)=3;
printf("\r\n使用“ uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3); ”动态分配的变量\r\n");
printf("\r\n定义后的操作为:\r\n*(pointer++)=1;\r\n*(pointer++)=2;\r\n*pointer=3;\r\n\r\n");
printf("结果:操作后它的地址为:0x%x,查看变量值操作:\r\n",(uint32_t)pointer);
printf("*(pointer--)=%d, \r\n",*(pointer--));
printf("*(pointer--)=%d, \r\n",*(pointer--));
printf("*(pointer)=%d, \r\n",*(pointer));
free(pointer);
}
else
{
printf("\r\n使用malloc动态分配变量出错!!!\r\n");
}
LED_BLUE;
while(1);
}
void Delay(__IO uint32_t nCount) //简单的延时函数
{
for(; nCount != 0; nCount--);
}
/*********************************************END OF FILE**********************/
SRAM.sct
;本文件用于备份sct文件的配置,要使用时把本文件名改为“SRAM.sct”
;然后复制到Output目录即可。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00010000 { ; RW data
*.o(STACK) ;选择STACK节区,栈
stm32f10x_rcc.o(+RW) ;选择stm32f10x_rcc的RW内容
.ANY (+RW +ZI)
}
RW_ERAM1 0x68000000 0x100000 { ; 外部SRAM
.ANY (+RW +ZI) ;其余的RW/ZI-data都分配到这里
}
}
其他配置:
串口输出结果:
取消了这个默认的“Use Memory Layout from Target Dialog”勾选后,在 MDK 的 Target 对话框及文件配置的存储器分布选项都会失效,以 sct 文件中的为准,更改对话框及文件配置选项都不会影响 sct 文件的内容。
(1) 修改启动文件,在 __main 执行之前初始化“指定的存储空间”的硬件;
(2) 在 sct 文件中增加“指定的存储空间”对应的执行域;
(3) 在“指定的存储空间”的执行域中选择一个自定义节区“EXRAM”;
(4) 使用 attribute 关键字指定变量分配到节区“EXRAM”;
(5) 使用宏封装 attribute 关键字,简化变量定义;
(6) 根据需要,把堆区分配到内部 SRAM 或“指定的存储空间”;
(7) 编写测试程序,编译正常后,查看 map 文件的空间分配情况。
1.在 __main 之前初始化外部“指定的存储空间”的硬件
2.sct 文件配置
3.指定变量分配到节区
4.变量分配测试及结果
main.c
/**
******************************************************************************
* @file 优先使用内部SRAM并把堆分配到外部SRAM空间
* @author fire
* @version V1.0
* @date 2015-xx-xx
* @brief SCT文件应用
******************************************************************************
* @attention
*
* 实验平台:野火 F103-指南者 STM32 开发板
* 论坛 :http://www.firebbs.cn
* 淘宝 :https://fire-stm32.taobao.com
*
******************************************************************************
*/
#include "stm32f10x.h"
#include "./usart/bsp_usart.h"
#include "./led/bsp_led.h"
#include "./sram/sram.h"
#include
void Delay(__IO u32 nCount);
//设置变量定义到“EXRAM”节区的宏
#define __EXRAM __attribute__ ((section ("EXRAM")))
//定义变量到“指定的存储空间”
uint32_t testValue __EXRAM =7 ;
//上述语句等效于:
//uint32_t testValue __attribute__ ((section ("EXRAM"))) =7 ;
//定义变量到SRAM
uint32_t testValue2 =7 ;
//定义数组到“指定的存储空间”
uint8_t testGrup[3] __EXRAM ={1,2,3};
//定义数组到SRAM
uint8_t testGrup2[3] ={1,2,3};
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
uint32_t inerTestValue =10;
/* LED 端口初始化 */
LED_GPIO_Config();
/* 初始化串口 */
USART_Config();
printf("\r\nSCT文件应用——自动分配变量到“指定的存储空间”实验\r\n");
printf("\r\n使用“ uint32_t inerTestValue =10; ”语句定义的局部变量:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d\r\n",(uint32_t)&inerTestValue,inerTestValue);
printf("\r\n使用“uint32_t testValue __EXRAM =7 ;”语句定义的全局变量:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d\r\n",(uint32_t)&testValue,testValue);
printf("\r\n使用“uint32_t testValue2 =7 ; ”语句定义的全局变量:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d\r\n",(uint32_t)&testValue2,testValue2);
printf("\r\n使用“uint8_t testGrup[3] __EXRAM ={1,2,3};”语句定义的全局数组:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n",(uint32_t)&testGrup,testGrup[0],testGrup[1],testGrup[2]);
printf("\r\n使用“uint8_t testGrup2[3] ={1,2,3};”语句定义的全局数组:\r\n");
printf("结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n",(uint32_t)&testGrup2,testGrup2[0],testGrup2[1],testGrup2[2]);
/*使用malloc从外部SRAM中分配空间*/
uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3);
if(pointer != NULL)
{
*(pointer)=1;
*(++pointer)=2;
*(++pointer)=3;
printf("\r\n使用“ uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3); ”动态分配的变量\r\n");
printf("\r\n定义后的操作为:\r\n*(pointer++)=1;\r\n*(pointer++)=2;\r\n*pointer=3;\r\n\r\n");
printf("结果:操作后它的地址为:0x%x,查看变量值操作:\r\n",(uint32_t)pointer);
printf("*(pointer--)=%d, \r\n",*(pointer--));
printf("*(pointer--)=%d, \r\n",*(pointer--));
printf("*(pointer)=%d, \r\n",*(pointer));
free(pointer);
}
else
{
printf("\r\n使用malloc动态分配变量出错!!!\r\n");
}
/*蓝灯亮*/
LED_BLUE;
while(1);
}
void Delay(__IO uint32_t nCount) //简单的延时函数
{
for(; nCount != 0; nCount--);
}
/*********************************************END OF FILE**********************/
SRAM.sct
;本文件用于备份sct文件的配置,要使用时把本文件名改为“SRAM.sct”
;然后复制到Output目录即可。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00005000 { ; RW data
;默认情况下以下属性的内容都会被分配到RW_IRAM1,所以可注释掉
;*.o(STACK) ;选择STACK节区,栈
;stm32f10x_rcc.o(+RW) ;选择stm32f10x_rcc的RW内容
.ANY (+RW +ZI)
}
RW_ERAM1 0x20005000 0x00007000 { ; 外部SRAM
*.o(HEAP) ;选择堆区
.ANY (EXRAM) ;选择EXRAM节区
}
}
其他配置:
串口输出结果:
5.把堆区分配到内部 SRAM 空间
若您希望堆区 (HEAP) 按照默认配置,使它还是分配到内部 SRAM 空间,只要把“*.o(HEAP)”选择语句从“指定的存储空间”的执行域删除掉即可,堆节区就会默认分配到内部 SRAM,“指定的存储空间”仅选择 EXRAM 节区的内容进行分配。
6.屏蔽链接过程的 warning