欢迎阅读《MDK的编译过程及文件类型全解》文档
本章参考资料:MDK的帮助手册《ARM DevelopmentTools》,点击MDK界面的“help->uVisionHelp”菜单可打开该文件。关于ELF文件格式,参考配套资料里的《ELF文件格式》文件。
在本章中讲解了非常多的文件类型,学习时请跟着教程的节奏,打开实际工程中的文件来了解。
相信您已经非常熟练地使用MDK创建应用程序了,平时使用MDK编写源代码,然后编译生成机器码,再把机器码下载到STM32芯片上运行,但是这个编译、下载的过程MDK究竟做了什么工作?它编译后生成的各种文件又有什么作用?本章节将对这些过程进行讲解,了解编译及下载过程有助于理解芯片的工作原理,这些知识对制作IAP(bootloader)以及读写控制器内部FLASH的应用时非常重要。
编译过程¶
编译过程简介¶
首先我们简单了解下MDK的编译过程,它与其它编译器的工作过程是类似的,该过程见图1‑1。
图 1‑1 MDK编译过程
编译过程生成的不同文件将在后面的小节详细说明,此处先抓住主要流程来理解。
编译,MDK软件使用的编译器是armcc和armasm,它们根据每个c/c++和汇编源文件编译成对应的以“.o”为后缀名的对象文件(ObjectCode,也称目标文件),其内容主要是从源文件编译得到的机器码,包含了代码、数据以及调试使用的信息;
链接,链接器armlink把各个.o文件及库文件链接成一个映像文件“.axf”或“.elf”;
格式转换,一般来说Windows或Linux系统使用链接器直接生成可执行映像文件elf后,内核根据该文件的信息加载后,就可以运行程序了,但在单片机平台上,需要把该文件的内容加载到芯片上,所以还需要对链接器生成的elf映像文件利用格式转换器fromelf转换成“.bin”或“.hex”文件,交给下载器下载到芯片的FLASH或ROM中。
具体工程中的编译过程¶
下面我们打开“多彩流水灯”的工程,以它为例进行讲解,其它工程的编译过程也是一样的,只是文件有差异。打开工程后,点击MDK的“rebuild”按钮,它会重新构建整个工程,构建的过程会在MDK下方的“BuildOutput”窗口输出提示信息,见图 1‑2。
图 1‑2 编译工程时的编译提示
构建工程的提示输出主要分6个部分,说明如下:
提示信息的第一部分说明构建过程调用的编译器。图中的编译器名字是“V5.06(build20)”,后面附带了该编译器所在的文件夹。在电脑上打开该路径,可看到该编译器包含图1‑3中的各个编译工具,如armar、armasm、armcc、armlink及fromelf,后面四个工具已在图1‑1中已讲解,而armar是用于把.o文件打包成lib文件的。
图 1‑3 编译工具
使用armasm编译汇编文件。图中列出了编译startup启动文件时的提示,编译后每个汇编源文件都对应有一个独立的.o文件。
使用armcc编译c/c++文件。图中列出了工程中所有的c/c++文件的提示,同样地,编译后每个c/c++源文件都对应有一个独立的.o文件。
使用armlink链接对象文件,根据程序的调用把各个.o文件的内容链接起来,最后生成程序的axf映像文件,并附带程序各个域大小的说明,包括Code、RO-data、RW-data及ZI-data的大小。
使用fromelf生成下载格式文件,它根据axf映像文件转化成hex文件,并列出编译过程出现的错误(Error)和警告(Warning)数量。
最后一段提示给出了整个构建过程消耗的时间。
构建完成后,可在工程的“Output”及“Listing”目录下找到由以上过程生成的各种文件,见图1‑4。
图 1‑4 编译后Output及Listing文件夹中的内容
可以看到,每个C源文件都对应生成了.o、.d及.crf后缀的文件,还有一些额外的.dep、.hex、.axf、.htm、.lnp、.sct、.lst及.map文件。
程序的组成、存储与运行¶
CODE、RO、RW、ZI Data域及堆栈空间¶
在工程的编译提示输出信息中有一个语句“Program Size:Code=xx RO-data=xxRW-data=xxZI-data=xx”,它说明了程序各个域的大小,编译后,应用程序中所有具有同一性质的数据(包括代码)被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状态,这些域的意义如下:
Code:即代码域,它指的是编译器生成的机器指令,这些内容被存储到ROM区。
RO-data:Read Onlydata,即只读数据域,它指程序中用到的只读数据,这些数据被存储在ROM区,因而程序不能修改其内容。例如C语言中const关键字定义的变量就是典型的RO-data。
RW-data:Read Writedata,即可读写数据域,它指初始化为“非0值”的可读写数据,程序刚运行时,这些数据具有非0的初始值,且运行的时候它们会常驻在RAM区,因而应用程序可以修改其内容。例如C语言中使用定义的全局变量,且定义时赋予“非0值”给该变量进行初始化。
ZI-data:Zero Initialiedata,即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动态申请堆空间,编译器会优化,不把堆空间计算在内)。
综上所述,以程序的组成构件为例,它们所属的区域类别见表 1‑1。
表 1‑1 程序组件所属的区域
程序组件 | 所属类别 |
---|---|
机器代码指令 | Code |
常量 | RO-data |
初值非0的全局变量 | RW-data |
初值为0的全局变量 | ZI-data |
局部变量 | ZI-data栈空间 |
使用malloc动态分配的空间 | ZI-data堆空间 |
程序的存储与运行¶
RW-data和ZI-data它们仅仅是初始值不一样而已,为什么编译器非要把它们区分开?这就涉及到程序的存储状态了,应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如STM32的内部FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的,见图1‑5。
图 1‑5 应用程序的加载视图与执行视图
图中的左侧是应用程序的存储状态,右侧是运行状态,而上方是RAM存储器区域,下方是ROM存储器区域。
程序在存储状态时,RO节(ROsection)及RW节都被保存在ROM区。当程序开始运行时,内核直接从ROM中读取代码,并且在执行主体代码前,会先执行一段加载代码,它把RW节数据从ROM复制到RAM,并且在RAM加入ZI节,ZI节的数据都被初始化为0。加载完后RAM区准备完毕,正式开始执行主体程序。
编译生成的RW-data的数据属于图中的RW节,ZI-data的数据属于图中的ZI节。是否需要掉电保存,这就是把RW-data与ZI-data区别开来的原因,因为在RAM创建数据的时候,默认值为0,但如果有的数据要求初值非0,那就需要使用ROM记录该初始值,运行时再复制到RAM。
STM32的RO区域不需要加载到SRAM,内核直接从FLASH读取指令运行。计算机系统的应用程序运行过程很类似,不过计算机系统的程序在存储状态时位于硬盘,执行的时候甚至会把上述的RO区域(代码、只读数据)加载到内存,加快运行速度,还有虚拟内存管理单元(MMU)辅助加载数据,使得可以运行比物理内存还大的应用程序。而STM32没有MMU,所以无法支持Linux和Windows系统。
当程序存储到STM32芯片的内部FLASH时(即ROM区),它占用的空间是Code、RO-data及RW-data的总和,所以如果这些内容比STM32芯片的FLASH空间大,程序就无法被正常保存了。当程序在执行的时候,需要占用内部SRAM空间(即RAM区),占用的空间包括RW-data和ZI-data。应用程序在各个状态时各区域的组成见表1‑2。
表 1‑2 程序状态区域的组成
程序状态与区域 | 组成 |
---|---|
程序执行时的只读区域(RO) | Code + RO data |
程序执行时的可读写区域(RW) | RW data + ZI data |
程序存储时占用的ROM区 | Code + RO data + RW data |
在MDK中,我们建立的工程一般会选择芯片型号,选择后就有确定的FLASH及SRAM大小,若代码超出了芯片的存储器的极限,编译器会提示错误,这时就需要裁剪程序了,裁剪时可针对超出的区域来优化。
编译工具链¶
在前面编译过程中,MDK调用了各种编译工具,平时我们直接配置MDK,不需要学习如何使用它们,但了解它们是非常有好处的。例如,若希望使用MDK编译生成bin文件的,需要在MDK中输入指令控制fromelf工具;在本章后面讲解AXF及O文件的时候,需要利用fromelf工具查看其文件信息,这都是无法直接通过MDK做到的。关于这些工具链的说明,在MDK的帮助手册《ARMDevelopment Tools》都有详细讲解,点击MDK界面的“help->uVisionHelp”菜单可打开该文件。
设置环境变量¶
调用这些编译工具,需要用到Windows的“命令行提示符工具”,为了让命令行方便地找到这些工具,我们先把工具链的目录添加到系统的环境变量中。查看本机工具链所在的具体目录可根据上一小节讲解的工程编译提示输出信息中找到,如本机的路径为“D:\work\keil5\ARM\ARMCC\bin”。
添加路径到PATH环境变量¶
本文以Win7系统为例添加工具链的路径到PATH环境变量,其它系统是类似的。
右键电脑系统的“计算机图标”,在弹出的菜单中选择“属性”,见图 1‑6;
图 1‑6 计算机属性页面
在弹出的属性页面依次点击“高级系统设置”->“环境变量”,在用户变量一栏中找到名为“PATH”的变量,若没有该变量,则新建一个。编辑“PATH”变量,在它的变量值中输入工具链的路径,如本机的是“;D:\work\keil5\ARM\ARMCC\bin”,注意要使用“分号;”让它与其它路径分隔开,输入完毕后依次点确定,见图1‑7;
图 1‑7 添加工具链路径到PATH变量
打开Windows的命令行,点击系统的“开始菜单”,在搜索框输入“cmd”,在搜索结果中点击“cmd.exe”即可打开命令行,见图1‑8;
图 1‑8 打开命令行
在弹出的命令行窗口中输入“fromelf”回车,若窗口打印出formelf的帮助说明,那么路径正常,就可以开始后面的工作了;若提示“不是内部名外部命令,也不是可运行的程序…”信息,说明路径不对,请重新配置环境变量,并确认该工作目录下有编译工具链。
这个过程本质就是让命令行通过“PATH”路径找到“fromelf.exe”程序运行,默认运行“fromelf.exe”时它会输出自己的帮助信息,这就是工具链的调用过程,MDK本质上也是如此调用工具链的,只是它集成为GUI,相对于命令行对用户更友好,毕竟上述配置环境变量的过程已经让新手烦躁了。
armcc、armasm及armlink¶
接下来我们看看各个工具链的具体用法,主要以armcc为例。
armcc¶
armcc用于把c/c++文件编译成ARM指令代码,编译后会输出ELF格式的O文件(对象、目标文件),在命令行中输入“armcc”回车可调用该工具,它会打印帮助说明,见图1‑9
图 1‑9 armcc的帮助提示
帮助提示中分三部分,第一部分是armcc版本信息,第二部分是命令的用法,第三部分是主要命令选项。
根据命令用法: armcc [options] file1 file2 ... filen,在[option]位置可输入下面的“–arm”、“–cpulist”选项,若选项带文件输入,则把文件名填充在file1file2…的位置,这些文件一般是c/c++文件。
例如根据它的帮助说明,“–cpulist”可列出编译器支持的所有cpu,我们在命令行中输入“armcc –cpulist”,可查看图 1‑10中的cpu列表。
图 1‑10 cpulist
打开MDK的Options for Targe->c/c++菜单,可看到MDK对编译器的控制命令,见图1‑11。
图 1‑11 MDK的ARMCC编译选项
从该图中的命令可看到,它调用了-c、-cpu –D –g–O1等编译选项,当我们修改MDK的编译配置时,可看到该控制命令也会有相应的变化。然而我们无法在该编译选项框中输入命令,只能通过MDK提供的选项修改。
了解这些,我们就可以查询具体的MDK编译选项的具体信息了,如c/c++选项中的“Optimization:Leve1(-O1)”是什么功能呢?首先可了解到它是“-O”命令,命令后还带个数字,查看MDK的帮助手册,在armcc编译器说明章节,可详细了解,如图1‑9。
图 1‑12 编译器选项说明
利用MDK,我们一般不需要自己调用armcc工具,但经过这样的过程我们就会对MDK有更深入的认识,面对它的各种编译选项,就不会那么头疼了。
armasm¶
armasm是汇编器,它把汇编文件编译成O文件。与armcc类似,MDK对armasm的调用选项可在“Optionfor Target->Asm”页面进行配置,见图 1‑13。
图 1‑13 armasm与MDK的编译选项
armlink¶
armlink是链接器,它把各个O文件链接组合在一起生成ELF格式的AXF文件,AXF文件是可执行的,下载器把该文件中的指令代码下载到芯片后,该芯片就能运行程序了;利用armlink还可以控制程序存储到指定的ROM或RAM地址。在MDK中可在“Optionfor Target->Linker”页面配置armlink选项,见图 1‑14。
图 1‑14 armlink与MDK的配置选项
链接器默认是根据芯片类型的存储器分布来生成程序的,该存储器分布被记录在工程里的sct后缀的文件中,有特殊需要的话可自行编辑该文件,改变链接器的链接方式,具体后面我们会详细讲解。
armar、fromelf及用户指令¶
armar工具用于把工程打包成库文件,fromelf可根据axf文件生成hex、bin文件,hex和bin文件是大多数下载器支持的下载文件格式。
在MDK中,针对armar和fromelf工具的选项几乎没有,仅集成了生成HEX或Lib的选项,见图1‑15。
图 1‑15 MDK中,控制fromelf生成hex及控制armar生成lib的配置
例如如果我们想利用fromelf生成bin文件,可以在MDK的“Option forTarget->User”页中添加调用fromelf的指令,见图 1‑16。
图 1‑16 在MDK中添加指令
在User配置页面中,提供了三种类型的用户指令输入框,在不同组的框输入指令,可控制指令的执行时间,分别是编译前(BeforeCompile c/c++ file)、构建前(Before Build/Rebuild)及构建后(AfterBuild/Rebuild)执行。这些指令并没有限制必须是arm的编译工具链,例如如果您自己编写了python脚本,也可以在这里输入用户指令执行该脚本。
图中的生成bin文件指令调用了fromelf工具,紧跟后面的是工具的选项及输出文件名、输入文件名。由于fromelf是根据axf文件生成bin的,而axf文件又是构建(build)工程后才生成,所以我们把该指令放到“AfterBuild/Rebuild”一栏。
MDK工程的文件类型¶
除了上述编译过程生成的文件,MDK工程中还包含了各种各样的文件,下面我们统一介绍,MDK工程的常见文件类型见表1‑3。
表 1‑3 MDK常见的文件类型(不分大小写)
这些文件主要分为MDK相关文件、源文件以及编译、链接器生成的文件。我们以“多彩流水灯”工程为例讲解各种文件的功能。
uvprojx、uvoptx、uvguix及ini工程文件¶
在工程的“Project”目录下主要是MDK工程相关的文件,见图 1‑17。
图 1‑17 Project目录下的uvprojx、uvoptx、uvguix及ini文件
uvprojx文件¶
uvprojx文件就是我们平时双击打开的工程文件,它记录了整个工程的结构,如芯片类型、工程包含了哪些源文件等内容,见图1‑18。
图 1‑18 工程包含的文件、芯片类型等内容
uvoptx文件¶
uvoptx文件记录了工程的配置选项,如下载器的类型、变量跟踪配置、断点位置以及当前已打开的文件等等,见图1‑19。
图 1‑19 代码编辑器中已打开的文件
uvprojx文件¶
uvguix文件记录了MDK软件的GUI布局,如代码编辑区窗口的大小、编译输出提示窗口的位置等等。
图 1‑20 记录MDK工作环境中各个窗口的大小
uvprojx、uvoptx及uvguix都是使用XML格式记录的文件,若使用记事本打开可以看到XML代码,见图1‑17。而当使用MDK软件打开时,它根据这些文件的XML记录加载工程的各种参数,使得我们每次重新打开工程时,都能恢复上一次的工作环境。
图 1‑21 使用记事本打开uvprojx、uvoptx及uvguix文件可看到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”语法包含。编译器根据工程中的源文件最终生成机器码。
Output目录下生成的文件¶
点击MDK中的编译按钮,它会根据工程的配置及工程中的源文件输出各种对象和列表文件,在工程的“Optionsfor Targe->Output->Select Folder for Objects”和“Options forTarge->Listing->Select Folder for Listings”选项配置它们的输出路径,见图1‑22和图 1‑23。
图 1‑22 设置Output输出路径
图 1‑23设置Listing输出路径
编译后Output和Listing目录下生成的文件见图 1‑24。
图 1‑24 编译后Output及Listing文件夹中的内容
接下来我们讲解Output路径下的文件。
lib库文件¶
在某些场合下我们不希望提供给第三方一个可用的代码库,但不希望对方看到源码,这个时候我们就可以把工程生成lib文件(Libraryfile)提供给对方,在MDK中可配置“Options for Target->CreateLibrary”选项把工程编译成库文件,见图 1‑25。
图 1‑25 生成库文件或可执行文件
工程中生成可执行文件或库文件只能二选一,默认编译是生成可执行文件的,可执行文件即我们下载到芯片上直接运行的机器码。
得到生成的*.lib文件后,可把它像C文件一样添加到其它工程中,并在该工程调用lib提供的函数接口,除了不能看到*.lib文件的源码,在应用方面它跟C源文件没有区别。
dep、d依赖文件¶
*.dep和*.d文件(Dependencyfile)记录的是工程或其它文件的依赖,主要记录了引用的头文件路径,其中*.dep是整个工程的依赖,它以工程名命名,而*.d是单个源文件的依赖,它们以对应的源文件名命名。这些记录使用文本格式存储,我们可直接使用记事本打开,见图1‑26和图 1‑27。
图 1‑26 工程的dep文件内容
图 1‑27 bsp_led.d文件的内容
crf交叉引用文件¶
*.crf是交叉引用文件(Cross-Reference file),它主要包含了浏览信息(browseinformation),即源代码中的宏定义、变量及函数的定义和声明的位置。
我们在代码编辑器中点击“Go To Definition Of ‘xxxx’”可实现浏览跳转,见图1‑28,跳转的时候,MDK就是通过*.crf文件查找出跳转位置的。
图 1‑28 浏览信息
通过配置MDK中的“Option for Target->Output->BrowseInformation”选项可以设置编译时是否生成浏览信息,见图1‑29。只有勾选该选项并编译后,才能实现上面的浏览跳转功能。
图 1‑29 在Options forTarget中设置是否生成浏览信息
*.crf文件使用了特定的格式表示,直接用文本编辑器打开会看到大部分乱码,见图1‑30,我们不作深入研究。
图 1‑30 crf文件内容
o、axf及elf文件¶
*.o、*.elf、*.axf、*.bin及*.hex文件都存储了编译器根据源代码生成的机器码,根据应用场合的不同,它们又有所区别。
ELF文件说明
*.o、*.elf、*.axf以及前面提到的lib文件都是属于目标文件,它们都是使用ELF格式来存储的,关于ELF格式的详细内容请参考配套资料里的《ELF文件格式》文档了解,它讲解的是Linux下的ELF格式,与MDK使用的格式有小区别,但大致相同。在本教程中,仅讲解ELF文件的核心概念。
ELF是Executable and LinkingFormat的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。在Linux及Windows系统下都有使用该格式的文件(或类似格式)用于记录应用程序的内容,告诉操作系统如何链接、加载及执行该应用程序。
目标文件主要有如下三种类型:
可重定位的文件(RelocatableFile),包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。
例如MDK的armcc和armasm生成的*.o文件就是这一类,另外还有Linux的*.o文件,Windows的 *.obj文件。
可执行文件(Executable File),它包含适合于执行的程序,它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。
例如MDK的armlink生成的*.elf及*.axf文件,(使用gcc编译工具可生成*.elf文件,用armlink生成的是*.axf文件,*.axf文件在*.elf之外,增加了调试使用的信息,其余区别不大,后面我们仅讲解*.axf文件),另外还有Linux的/bin/bash文件,Windows的*.exe文件。
共享目标文件(Shared ObjectFile), 它的定义比较难理解,我们直接举例,MDK生成的*.lib文件就属于共享目标文件,它可以继续参与链接,加入到可执行文件之中。另外,Linux的.so,如/lib/glibc-2.5.so,Windows的DLL都属于这一类。
o文件与axf文件的关系
根据上面的分类,我们了解到,*.axf文件是由多个*.o文件链接而成的,而*.o文件由相应的源文件编译而成,一个源文件对应一个*.o文件。它们的关系见图1‑31。
图 1‑31*.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文件。例如,见图1‑32,“多彩流水灯”工程中在“bsp_led.c”文件中有一个LED_GPIO_Config函数,而它内部调用了“stm32f4xx_gpio.c”的GPIO_Init函数,经过armcc编译后,LED_GPIO_Config及GPIO_Iint函数都成了指令代码,分别存储在bsp_led.o及stm32f4xx_gpio.o文件中,这些指令在*.o文件都没有指定地址,仅包含了内容、大小以及调用的链接信息,而经过链接器后,链接器给它们都分配了特定的地址,并且把地址根据调用指向链接起来。
图 1‑32 具体的链接过程
ELF文件头
接下来我们看看具体文件的内容,使用fromelf文件可以查看*.o、*.axf及*.lib文件的ELF信息。
使用命令行,切换到文件所在的目录,输入“fromelf –text –vbsp_led.o”命令,可控制输出bsp_led.o的详细信息,见图 1‑33。利用“-c、-z”等选项还可输出反汇编指令文件、代码及数据文件等信息,请亲手尝试一下。
图 1‑33 使用fromelf查看o文件信息
为了便于阅读,我已使用fromelf指令生成了“多彩流水灯.axf”、“bsp_led”及“多彩流水灯.lib”的ELF信息,并已把这些信息保存在独立的文件中,在配套资料的“elf信息输出”文件夹下可查看,见表1‑4。
表 1‑4 配套资料里使用fromelf生成的文件
fromelf选项 | 可查看的信息 | 生成到配套资料里相应的文件 |
---|---|---|
-v | 详细信息 | bsp_led_o_elfInfo_v.txt/多彩流水灯_axf_elfInfo_v.txt |
-a | 数据的地址 | bsp_led_o_elfInfo_a.txt/多彩流水灯_axf_elfInfo_a.txt |
-c | 反汇编代码 | bsp_led_o_elfInfo_c.txt/多彩流水灯_axf_elfInfo_c.txt |
-d | data section的内容 | bsp_led_o_elfInfo_d.txt/多彩流水灯_axf_elfInfo_d.txt |
-e | 异常表 | bsp_led_o_elfInfo_e.txt/多彩流水灯_axf_elfInfo_e.txt |
-g | 调试表 | bsp_led_o_elfInfo_g.txt/多彩流水灯_axf_elfInfo_g.txt |
-r | 重定位信息 | bsp_led_o_elfInfo_r.txt/多彩流水灯_axf_elfInfo_r.txt |
-s | 符号表 | bsp_led_o_elfInfo_s.txt/多彩流水灯_axf_elfInfo_s.txt |
-t | 字符串表 | bsp_led_o_elfInfo_t.txt/多彩流水灯_axf_elfInfo_t.txt |
-y | 动态段内容 | bsp_led_o_elfInfo_y.txt/多彩流水灯_axf_elfInfo_y.txt |
-z | 代码及数据的大小信息 | bsp_led_o_elfInfo_z.txt/多彩流水灯_axf_elfInfo_z.txt |
直接打开“elf信息输出”目录下的bsp_led_o_elfInfo_v.txt文件,可看到代码清单1‑1中的内容。
代码清单 1‑1bsp_led.o文件的ELF文件头(可到“bsp_led_o_elfInfo_v.txt”文件查看)
1¶
2
3 ** ELF Header Information
4
5 File Name:
6 .\bsp_led.o //bsp_led.o文件
7
8 Machine class: ELFCLASS32 (32-bit) //32位机
9 Data encoding: ELFDATA2LSB (Little endian) //小端格式
10 Header version: EV_CURRENT (Current version)
11 Operating System ABI: none
12 ABI Version: 0
13 File Type: ET_REL (Relocatable object) (1) //可重定位文件类型
14 Machine: EM_ARM (ARM)
15
16 Entry offset (in SHF_ENTRYSECT section): 0x00000000
17 Flags: None (0x05000000)
18
19 ARM ELF revision: 5 (ABI version 2)
20
21 Built with
22 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]
23 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]
24
25 Header size: 52 bytes (0x34)
26 Program header entry size: 0 bytes (0x0) //程序头大小
27 Section header entry size: 40 bytes (0x28)
28
29 Program header entries: 0
30 Section header entries: 246
31
32 Program header offset: 0 (0x00000000)//程序头在文件中的位置(没有程序头)
33 Section header offset: 507224 (0x0007bd58) //节区头在文件中的位置
34
35 Section header string table index: 243
36
37 =====================================================================
在上述代码中已加入了部分注释,解释了相应项的意义,值得一提的是在这个*.o文件中,它的ELF文件头中告诉我们它的程序头(Programheader)大小为“0bytes”,且程序头所在的文件位置偏移也为“0”,这说明它是没有程序头的。
程序头
接下来打开“多彩流水灯_axf_elfInfo_v.txt”文件,查看工程的*.axf文件的详细信息,见代码清单1‑2。
代码清单 1‑2*.axf文件中的elf文件头及程序头(可到“多彩流水灯_axf_elfInfo_v.txt”文件查看)
1 ===================================================================
2
3 ** ELF Header Information
4
5 File Name:
6 .\多彩流水灯.axf //多彩流水灯.axf 文件
7
8 Machine class: ELFCLASS32 (32-bit) //32位机
9 Data encoding: ELFDATA2LSB (Little endian) //小端格式
10 Header version: EV_CURRENT (Current version)
11 Operating System ABI: none
12 ABI Version: 0
13 File Type: ET_EXEC (Executable) (2) //可执行文件类型
14 Machine: EM_ARM (ARM)
15
16 Image Entry point: 0x080001ad
17 Flags: EF_ARM_HASENTRY + EF_ARM_ABI_FLOAT_SOFT (0x05000202)
18
19 ARM ELF revision: 5 (ABI version 2)
20
21 Conforms to Soft float procedure-call standard
22
23 Built with
24 Component: ARM Compiler 5.06 (build 20) Tool: armasm [4d35a2]
25 Component: ARM Compiler 5.06 (build 20) Tool: armlink [4d35a3]
26
27 Header size: 52 bytes (0x34)
28 Program header entry size: 32 bytes (0x20)
29 Section header entry size: 40 bytes (0x28)
30
31 Program header entries: 1
32 Section header entries: 15
33
34 Program header offset: 335252 (0x00051d94) //程序头在文件中的位置
35 Section header offset: 335284 (0x00051db4) //节区头在文件中的位置
36
37 Section header string table index: 14
38
39 =================================================================
40
41 ** Program header #0
42
43 Type : PT_LOAD (1) //表示这是可加载的内容
44 File Offset : 52 (0x34) //在文件中的偏移
45 Virtual Addr : 0x08000000 //虚拟地址(此处等于物理地址)
46 Physical Addr : 0x08000000 //物理地址
47 Size in file : 1456 bytes (0x5b0) //程序在文件中占据的大小
48 Size in memory: 2480 bytes (0x9b0) //若程序加载到内存,占据的内存空间
49 Flags : PF_X + PF_W + PF_R + PF_ARM_ENTRY (0x80000007)
50 Alignment : 8 //地址对齐
51
52
53 ===============================================================
对比之下,可发现*.axf文件的ELF文件头对程序头的大小说明为非0值,且给出了它在文件的偏移地址,在输出信息之中,包含了程序头的详细信息。可看到,程序头的“PhysicalAddr”描述了本程序要加载到的内存地址“0x08000000”,正好是STM32内部FLASH的首地址;“size infile”描述了本程序占据的空间大小为“1456bytes”,它正是程序烧录到FLASH中需要占据的空间。
节区头
在ELF的原文件中,紧接着程序头的一般是节区的主体信息,在节区主体信息之后是描述节区主体信息的节区头,我们先来看看节区头中的信息了解概况。通过对比*.o文件及*.axf文件的节区头部信息,可以清楚地看出这两种文件的区别,见代码清单1‑3。
代码清单 1‑3 *.o文件的节区信息(“bsp_led_o_elfInfo_v.txt”文件)
1 ====================================
2 ** Section #4
3
4 Name : i.LED_GPIO_Config //节区名
6
7 //此节区包含程序定义的信息,其格式和含义都由程序来解释。
8 Type : SHT_PROGBITS (0x00000001)
10
11 //此节区在进程执行过程中占用内存。 节区包含可执行的机器指令。
12 Flags :SHF_ALLOC + SHF_EXECINSTR (0x00000006)
14 Addr : 0x00000000 //地址
15 File Offset : 68 (0x44) //在文件中的偏移
16 Size : 116 bytes (0x74) //大小
17 Link : SHN_UNDEF
19 Info : 0
20 Alignment : 4 //字节对齐
21 Entry Size : 0
22 ====================================
这个节区的名称为LED_GPIO_Config,它正好是我们在bsp_led.c文件中定义的函数名,这个节区头描述的是该函数被编译后的节区信息,其中包含了节区的类型(指令类型)、节区应存储到的地址(0x00000000)、它主体信息在文件位置中的偏移(68)以及节区的大小(116bytes)。
由于*.o文件是可重定位文件,所以它的地址并没有被分配,是0x00000000(假如文件中还有其它函数,该函数生成的节区中,对应的地址描述也都是0)。当链接器链接时,根据这个节区头信息,在文件中找到它的主体内容,并根据它的类型,把它加入到主程序中,并分配实际地址,链接后生成的*.axf文件,我们再来看看它的内容,见代码清单1‑4。
代码清单 1‑4 *.axf文件的节区信息(“多彩流水灯_axf_elfInfo_v.txt”文件)
1¶
2 ** Section #1
3
4 Name : ER_IROM1 //节区名
5
6 //此节区包含程序定义的信息,其格式和含义都由程序来解释。
7 Type : SHT_PROGBITS (0x00000001)
8
9 //此节区在进程执行过程中占用内存。 节区包含可执行的机器指令
10 Flags : SHF_ALLOC + SHF_EXECINSTR (0x00000006)
11 Addr : 0x08000000 //地址
12 File Offset : 52 (0x34)
13 Size : 1456 bytes (0x5b0) //大小
14 Link : SHN_UNDEF
15 Info : 0
16 Alignment : 4
17 Entry Size : 0
18
19 ====================================
20 ** Section #2
21
22 Name : RW_IRAM1 //节区名
23
24 //包含将出现在程序的内存映像中的为初始
25 //化数据。 根据定义, 当程序开始执行, 系统
26 //将把这些数据初始化为 0。
27 Type : SHT_NOBITS (0x00000008)
28
29 //此节区在进程执行过程中占用内存。节区包含进程执行过程中将可写的数据。
30 Flags : SHF_ALLOC + SHF_WRITE (0x00000003)
31 Addr : 0x20000000 //地址
32 File Offset : 1508 (0x5e4)
33 Size : 1024 bytes (0x400) //大小
34 Link : SHN_UNDEF
35 Info : 0
36 Alignment : 8
37 Entry Size : 0
38 ====================================
在*.axf文件中,主要包含了两个节区,一个名为ER_IROM1,一个名为RW_IRAM1,这些节区头信息中除了具有*.o文件中节区头描述的节区类型、文件位置偏移、大小之外,更重要的是它们都有具体的地址描述,其中ER_IROM1的地址为0x08000000,而RW_IRAM1的地址为0x20000000,它们正好是内部FLASH及SRAM的首地址,对应节区的大小就是程序需要占用FLASH及SRAM空间的实际大小。
也就是说,经过链接器后,它生成的*.axf文件已经汇总了其它*.o文件的所有内容,生成的ER_IROM1节区内容可直接写入到STM32内部FLASH的具体位置。例如,前面*.o文件中的i.LED_GPIO_Config节区已经被加入到*.axf文件的ER_IROM1节区的某地址。
节区主体及反汇编代码
使用fromelf的-c选项可以查看部分节区的主体信息,对于指令节区,可根据其内容查看相应的反汇编代码,打开“bsp_led_o_elfInfo_c.txt”文件可查看这些信息,见代码清单1‑5。
代码清单 1‑5*.o文件的LED_GPIO_Config节区及反汇编代码(bsp_led_o_elfInfo_c.txt文件)
1 ** Section #4 ‘i.LED_GPIO_Config’ (SHT_PROGBITS) [SHF_ALLOC +SHF_EXECINSTR]
2 Size : 116 bytes (alignment 4)
3 Address: 0x00000000
4
5 $t
6 i.LED_GPIO_Config
7 LED_GPIO_Config
8 // 地址 内容 (ASCII码) 内容对应的代码
9 // (无意义)
10 0x00000000: e92d41fc -..A PUSH {r2-r8,lr}
11 0x00000004: 2101 .! MOVS r1,#1
12 0x00000006: 2088 . MOVS r0,#0x88
13 0x00000008: f7fffffe .... BL RCC_AHB1PeriphClockCmd
14 0x0000000c: f44f6580 O..e MOV r5,#0x400
15 0x00000010: 9500 .. STR r5,[sp,#0]
16 0x00000012: 2101 .! MOVS r1,#1
17 0x00000014: f88d1004 .... STRB r1,[sp,#4]
18 0x00000018: 2000 . MOVS r0,#0
19 0x0000001a: f88d0006 .... STRB r0,[sp,#6]
20 0x0000001e: f88d1007 .... STRB r1,[sp,#7]
21 0x00000022: f88d0005 .... STRB r0,[sp,#5]
22 0x00000026: 4f11 .O LDR r7,[pc,#68] ;
23 0x00000028: 4669 iF MOV r1,sp
24 0x0000002a: 4638 8F MOV r0,r7
25 0x0000002c: f7fffffe .... BL GPIO_Init
26 0x00000030: 006c l. LSLS r4,r5,#1
27 /*....以下省略**/
可看到,由于这是*.o文件,它的节区地址还是没有分配的,基地址为0x00000000,接着在LED_GPIO_Config标号之后,列出了一个表,表中包含了地址偏移、相应地址中的内容以及根据内容反汇编得到的指令。细看汇编指令,还可看到它包含了跳转到RCC_AHB1PeriphClockCmd及GPIO_Init标号的语句,而且这两个跳转语句原来的内容都是“f7fffffe”,这是因为还*.o文件中并没有RCC_AHB1PeriphClockCmd及GPIO_Init标号的具体地址索引,在*.axf文件中,这是不一样的。
接下来我们打开“多彩流水灯_axf_elfInfo_c.txt”文件,查看*.axf文件中,ER_IROM1节区中对应LED_GPIO_Config的内容,见代码清单1‑6。
代码清单1‑6*.axf文件的LED_GPIO_Config反汇编代码(多彩流水灯_axf_elfInfo_c.txt文件)
1 i.LED_GPIO_Config
2 LED_GPIO_Config
3 0x080002a4: e92d41fc -..A PUSH {r2-r8,lr}
4 0x080002a8: 2101 .! MOVS r1,#1
5 0x080002aa: 2088 . MOVS r0,#0x88
6 0x080002ac: f000f838 ..8. BL RCC_AHB1PeriphClockCmd ; 0x8000320
7 0x080002b0: f44f6580 O..e MOV r5,#0x400
8 0x080002b4: 9500 .. STR r5,[sp,#0]
9 0x080002b6: 2101 .! MOVS r1,#1
10 0x080002b8: f88d1004 .... STRB r1,[sp,#4]
11 0x080002bc: 2000 . MOVS r0,#0
12 0x080002be: f88d0006 .... STRB r0,[sp,#6]
13 0x080002c2: f88d1007 .... STRB r1,[sp,#7]
14 0x080002c6: f88d0005 .... STRB r0,[sp,#5]
15 0x080002ca: 4f11 .O LDR r7,[pc,#68] ; [0x8000310] = 0x40021c00
16 0x080002cc: 4669 iF MOV r1,sp
17 0x080002ce: 4638 8F MOV r0,r7
18 0x080002d0: f7ffffa5 .... BL GPIO_Init ; 0x800021e
19 0x080002d4: 006c l. LSLS r4,r5,#1
20 /*....以下省略**/
可看到,除了基地址以及跳转地址不同之外,LED_GPIO_Config中的内容跟*.o文件中的一样。另外,由于*.o是独立的文件,而*.axf是整个工程汇总的文件,所以在*.axf中包含了所有调用到*.o文件节区的内容。例如,在“bsp_led_o_elfInfo_c.txt”(bsp_led.o文件的反汇编信息)中不包含RCC_AHB1PeriphClockCmd及GPIO_Init的内容,而在“多彩流水灯_axf_elfInfo_c.txt”(多彩流水灯.axf文件的反汇编信息)中则可找到它们的具体信息,且它们也有具体的地址空间。
在*.axf文件中,跳转到RCC_AHB1PeriphClockCmd及GPIO_Init标号的这两个指令后都有注释,分别是“;0x8000320”及“;0x800021e”,它们是这两个标号所在的具体地址,而且这两个跳转语句的跟*.o中的也有区别,内容分别为“f000f838e”及“f7ffffa5”(*.o中的均为f7fffffe)。这就是链接器链接的含义,它把不同*.o中的内容链接起来了。
分散加载代码
学习至此,还有一个疑问,前面提到程序有存储态及运行态,它们之间应有一个转化过程,把存储在FLASH中的RW-data数据拷贝至SRAM。然而我们的工程中并没有编写这样的代码,在汇编文件中也查不到该过程,芯片是如何知道FLASH的哪些数据应拷贝到SRAM的哪些区域呢?
通过查看“多彩流水灯_axf_elfInfo_c.txt”的反汇编信息,了解到程序中具有一段名为“__scatterload”的分散加载代码,见代码清单1‑7,它是由armlink链接器自动生成的。
代码清单 1‑7 分散加载代码(多彩流水灯_axf_elfInfo_c.txt文件)
1
2 .text
3 __scatterload
4 __scatterload_rt2
5 0x080001e4: 4c06 .L LDR r4,[pc,#24] ; [0x8000200] = 0x80005a0
6 0x080001e6: 4d07 .M LDR r5,[pc,#28] ; [0x8000204] = 0x80005b0
7 0x080001e8: e006 .. B 0x80001f8 ; __scatterload + 20
8 0x080001ea: 68e0 .h LDR r0,[r4,#0xc]
9 0x080001ec: f0400301 @... ORR r3,r0,#1
10 0x080001f0: e8940007 .... LDM r4,{r0-r2}
11 0x080001f4: 4798 .G BLX r3
12 0x080001f6: 3410 .4 ADDS r4,r4,#0x10
13 0x080001f8: 42ac .B CMP r4,r5
14 0x080001fa: d3f6 .. BCC 0x80001ea ; __scatterload + 6
15 0x080001fc: f7ffffda .... BL __main_after_scatterload ; 0x80001b4
16 $d
17 0x08000200: 080005a0 .... DCD 134219168
18 0x08000204: 080005b0 .... DCD 134219184
这段分散加载代码包含了拷贝过程(LDM复制指令),而LDM指令的操作数中包含了加载的源地址,这些地址中包含了内部FLASH存储的RW-data数据。而“__scatterload ”的代码会被“__main”函数调用,见代码清单1‑8,__main在启动文件中的“Reset_Handler”会被调用,因而,在主体程序执行前,已经完成了分散加载过程。
代码清单 1‑8__main的反汇编代码(部分,多彩流水灯_axf_elfInfo_c.txt文件)
1 __main
2 _main_stk
3 0x080001ac: f8dfd00c .... LDR sp,__lit__00000000 ; [0x80001bc] =0x20000400
4 .ARM.Collect$$$$00000004
5 _main_scatterload
6 0x080001b0: f000f818 .... BL __scatterload ; 0x80001e4
若编译过程无误,即可把工程生成前面对应的*.axf文件,而在MDK中使用下载器(DAP/JLINK/ULINK等)下载程序或仿真的时候,MDK调用的就是*.axf文件,它解释该文件,然后控制下载器把*.axf中的代码内容下载到STM32芯片对应的存储空间,然后复位后芯片就开始执行代码了。
然而,脱离了MDK或IAR等工具,下载器就无法直接使用*.axf文件下载代码了,它们一般仅支持hex和bin格式的代码数据文件。默认情况下MDK都不会生成hex及bin文件,需要配置工程选项或使用fromelf命令。
生成hex文件
生成hex文件的配置比较简单,在“Options for Target->Output->Create HexFile”中勾选该选项,然后编译工程即可,见图 1‑34。
图 1‑34 生成hex文件的配置
生成bin文件
使用MDK生成bin文件需要使用fromelf命令,在MDK的“Options ForTarget->Users”中加入图 1‑35中的命令。
图 1‑35 使用fromelf指令生成bin文件
图中的指令内容为:
“fromelf –bin –output ..\..\Output\多彩流水灯.bin..\..\Output\多彩流水灯.axf”
该指令是根据本机及工程的配置而写的,在不同的系统环境或不同的工程中,指令内容都不一样,我们需要理解它,才能为自己的工程定制指令,首先看看fromelf的帮助,见图1‑36。
图 1‑36 fromelf的帮助
我们在MDK输入的指令格式是遵守fromelf帮助里的指令格式说明的,其格式为:
“fromelf [options] input_file”
其中optinos是指令选项,一个指令支持输入多个选项,每个选项之间使用空格隔开,我们的实例中使用“–bin”选项设置输出bin文件,使用“–outputfile”选项设置输出文件的名字为“..\..\Output\多彩流水灯.bin”,这个名字是一个相对路径格式,如果不了解如何使用“..\”表示路径,可使用MDK命令输入框后面的文件夹图标打开文件浏览器选择文件,在命令的最后使用“..\..\Output\多彩流水灯.axf”作为命令的输入文件。具体的格式分解见图1‑37。
图 1‑37 fromelf命令格式分解
fromelf需要根据工程的*.axf文件输入来转换得到bin文件,所以在命令的输入文件参数中要选择本工程对应的*.axf文件,在MDK命令输入栏中,我们把fromelf指令放置在“AfterBuild/Rebuild”(工程构建完成后执行)一栏也是基于这个考虑,这样设置后,工程构建完成生成了最新的*.axf文件,MDK再执行fromelf指令,从而得到最新的bin文件。
设置完成生成hex的选项或添加了生成bin的用户指令后,点击工程的编译(build)按钮,重新编译工程,成功后可看到图1‑38中的输出。打开相应的目录即可找到文件,若找不到bin文件,请查看提示输出栏执行指令的信息,根据信息改正fromelf指令。
图 1‑38 fromelf生成hxe及bin文件的提示
其中bin文件是纯二进制数据,无特殊格式,接下来我们了解一下hex文件格式。
hex文件格式
hex是Intel公司制定的一种使用ASCII文本记录机器码或常量数据的文件格式,这种文件常常用来记录将要存储到ROM中的数据,绝大多数下载器支持该格式。
一个hex文件由多条记录组成,而每条记录由五个部分组成,格式形如“:llaaaatt[dd…]cc”,例如本“多彩流水灯”工程生成的hex文件前几条记录见代码清单1‑9。
代码清单 1‑9 Hex文件实例(多彩流水灯.hex文件,可直接用记事本打开)
1 :020000040800F2
2 :1000000000040020C10100081B030008A30200082F
3 :100010001903000809020008690400080000000034
4 :100020000000000000000000000000003D03000888
5 :100030000B020008000000001D0300081504000862
6 :10004000DB010008DB010008DB010008DB01000820
记录的各个部分介绍如下:
“:” :每条记录的开头都使用冒号来表示一条记录的开始;
ll:以16进制数表示这条记录的主体数据区的长度(即后面[dd…]的长度);
aaaa:表示这条记录中的内容应存放到FLASH中的起始地址;
tt:表示这条记录的类型,它包含中的各种类型;
表 1‑5 tt值所代表的类型说明
tt的值 | 代表的类型 |
---|---|
00 | 数据记录 |
01 | 本文件结束记录 |
02 | 扩展地址记录 |
04 | 扩展线性地址记录(表示后面的记录按个这地址递增) |
05 | 表示一个线性地址记录的起始(只适用于ARM) |
dd:表示一个字节的数据,一条记录中可以有多个字节数据,ll区表示了它有多少个字节的数据;
cc:表示本条记录的校验和,它是前面所有16进制数据(除冒号外,两个为一组)的和对256取模运算的结果的补码。
例如,代码清单 1‑9中的第一条记录解释如下:
02:表示这条记录数据区的长度为2字节;
0000:表示这条记录要存储到的地址;
04:表示这是一条扩展线性地址记录;
0800:由于这是一条扩展线性地址记录,所以这部分表示地址的高16位,与前面的“0000”结合在一起,表示要扩展的线性地址为“0x08000000”,这正好是STM32内部FLASH的首地址;
F2:表示校验和,它的值为(0x02+0x00+0x00+0x04+0x08+0x00)%256的值再取补码。
再来看第二条记录:
10:表示这条记录数据区的长度为2字节;
0000:表示这条记录所在的地址,与前面的扩展记录结合,表示这条记录要存储的FLASH首地址为(0x08000000+0x0000);
00:表示这是一条数据记录,数据区的是地址;
00040020C10100081B030008A3020008:这是要按地址存储的数据;
2F:校验和
为了更清楚地对比bin、hex及axf文件的差异,我们来查看这些文件内部记录的信息来进行对比。
hex、bin及axf文件的区别与联系
bin、hex及axf文件都包含了指令代码,但它们的信息丰富程度是不一样的。
bin文件是最直接的代码映像,它记录的内容就是要存储到FLASH的二进制数据(机器码本质上就是二进制数据),在FLASH中是什么形式它就是什么形式,没有任何辅助信息,包括大小端格式也没有,因此下载器需要有针对芯片FLASH平台的辅助文件才能正常下载(一般下载器程序会有匹配的这些信息);
hex文件是一种使用十六进制符号表示的代码记录,记录了代码应该存储到FLASH的哪个地址,下载器可以根据这些信息辅助下载;
axf文件在前文已经解释,它不仅包含代码数据,还包含了工程的各种信息,因此它也是三个文件中最大的。
同一个工程生成的bin、hex及axf文件的大小见图 1‑39。
图 1‑39 同一个工程的bin、bex及axf文件大小
实际上,这个工程要烧写到FLASH的内容总大小为1456字节,然而在Windows中查看的bin文件却比它大(bin文件是FLASH的代码映像,大小应一致),这是因为Windows文件显示单位的原因,使用右键查看文件的属性,可以查看它实际记录内容的大小,见图1‑40。
图 1‑40 bin文件大小
接下来我们打开本工程的“多彩流水灯.bin”、“多彩流水灯.hex”及由“多彩流水灯.axf”使用fromelf工具输出的反汇编文件“多彩流水灯_axf_elfInfo_c.txt”文件,清晰地对比它们的差异,见图1‑41。如果您想要亲自阅读自己电脑上的bin文件,推荐使用sublime软件打开,它可以把二进制数以ASCII码呈现出来,便于阅读。
图 1‑41 同一个工程的bin、hex及axf文件对代码的记录
在“多彩流水灯_axf_elfInfo_c.txt”文件中不仅可以看到代码数据,还有具体的标号、地址以及反汇编得到的代码,虽然它不是*.axf文件的原始内容,但因为它是通过*.axf文件fromelf工具生成的,我们可认为*.axf文件本身记录了大量这些信息,它的内容非常丰富,熟悉汇编语言的人可轻松阅读。
在hex文件中包含了地址信息以及地址中的内容,而在bin文件中仅包含了内容,连存储的地址信息都没有。观察可知,bin、hex及axf文件中的数据内容都是相同的,它们存储的都是机器码。这就是它们三都之间的区别与联系。
由于文件中存储的都是机器码,见图1‑42,该图是我根据axf文件的GPIO_Init函数的机器码,在bin及hex中找到的对应位置。所以经验丰富的人是有可能从bin或hex文件中恢复出汇编代码的,只是成本较高,但不是不可能。
图 1‑42 GPIO_Init函数的代码数据在三个文件中的表示
如果芯片没有做任何加密措施,使用下载器可以直接从芯片读回它存储在FLASH中的数据,从而得到bin映像文件,根据芯片型号还原出部分代码即可进行修改,甚至不用修改代码,直接根据目标产品的硬件PCB,抄出一样的板子,再把bin映像下载芯片,直接山寨出目标产品,所以在实际的生产中,一定要注意做好加密措施。由于axf文件中含有大量的信息,且直接使用fromelf即可反汇编代码,所以更不要随便泄露axf文件。lib文件也能反使用fromelf文件反汇编代码,不过它不能还原出C代码,由于lib文件的主要目的是为了保护C源代码,也算是达到了它的要求。
在Output目录下,有一个以工程文件命名的后缀为*.bulid_log.htm及*.htm文件,如“多彩流水灯.bulid_log.htm”及“多彩流水灯.htm”,它们都可以使用浏览器打开。其中*.build_log.htm是工程的构建过程日志,而*.htm是链接器生成的静态调用图文件。
在静态调用图文件中包含了整个工程各种函数之间互相调用的关系图,而且它还给出了静态占用最深的栈空间数量以及它对应的调用关系链。
例如图 1‑43是“多彩流水灯.htm”文件顶部的说明。
图 1‑43“多彩流水灯.htm”中的静态占用最深的栈空间说明
该文件说明了本工程的静态栈空间最大占用56字节(Maximum StackUsage:56bytes),这个占用最深的静态调用为“main->LED_GPIO_Config->GPIO_Init”。注意这里给出的空间只是静态的栈使用统计,链接器无法统计动态使用情况,例如链接器无法知道递归函数的递归深度。在本文件的后面还可查询到其它函数的调用情况及其它细节。
利用这些信息,我们可以大致了解工程中应该分配多少空间给栈,有空间余量的情况下,一般会设置比这个静态最深栈使用量大一倍,在STM32中可修改启动文件改变堆栈的大小;如果空间不足,可从本文件中了解到调用深度的信息,然后优化该代码。
注意:
查看了各个工程的静态调用图文件统计后,我们发现本书提供的一些比较大规模的工程例子,静态栈调用最大深度都已超出STM32启动文件默认的栈空间大小0x00000400,即1024字节,但在当时的调试过程中却没有发现错误,因此我们也没有修改栈的默认大小(有一些工程调试时已发现问题,它们的栈空间就已经被我们改大了),虽然这些工程实际运行并没有错误,但这可能只是因为它使用的栈溢出RAM空间恰好没被程序其它部分修改而已。所以,建议您在实际的大型工程应用中(特别是使用了各种外部库时,如Lwip/emWin/Fatfs等),要查看本静态调用图文件,了解程序的栈使用情况,给程序分配合适的栈空间。
在Listing目录下包含了*.map及*.lst文件,它们都是文本格式的,可使用Windows的记事本软件打开。其中lst文件仅包含了一些汇编符号的链接信息,我们重点分析map文件。
map文件是由链接器生成的,它主要包含交叉链接信息,查看该文件可以了解工程中各种符号之间的引用以及整个工程的Code、RO-data、RW-data以及ZI-data的详细及汇总信息。它的内容中主要包含了“节区的跨文件引用”、“删除无用节区”、“符号映像表”、“存储器映像索引”以及“映像组件大小”,各部分介绍如下:
节区的跨文件引用
打开“多彩流水灯.map”文件,可看到它的第一部分——节区的跨文件引用(SectionCross References),见代码清单 1‑10。
代码清单 1‑10 节区的跨文件引用(部分,多彩流水灯.map文件)
1¶
2 Section Cross References
3
4 startup_stm32f429_439xx.o(RESET) refers tostartup_stm32f429_439xx.o(STACK) for __initial_sp
5 startup_stm32f429_439xx.o(RESET) refers tostartup_stm32f429_439xx.o(.text) for Reset_Handler
6 startup_stm32f429_439xx.o(RESET) refers tostm32f4xx_it.o(i.NMI_Handler) for NMI_Handler
7 startup_stm32f429_439xx.o(RESET) refers tostm32f4xx_it.o(i.HardFault_Handler) for HardFault_Handler
8 /**...以下部分省略****/
9
10 main.o(i.main) refers to bsp_led.o(i.LED_GPIO_Config) forLED_GPIO_Config
11 main.o(i.main) refers to stm32f4xx_gpio.o(i.GPIO_ResetBits) forGPIO_ResetBits
12 main.o(i.main) refers to main.o(i.Delay) for Delay
13 main.o(i.main) refers to stm32f4xx_gpio.o(i.GPIO_SetBits) forGPIO_SetBits
14 bsp_led.o(i.LED_GPIO_Config) refers tostm32f4xx_rcc.o(i.RCC_AHB1PeriphClockCmd) for RCC_AHB1PeriphClockCmd
15 bsp_led.o(i.LED_GPIO_Config) refers tostm32f4xx_gpio.o(i.GPIO_Init) for GPIO_Init
16 bsp_led.o(i.LED_GPIO_Config) refers tostm32f4xx_gpio.o(i.GPIO_ResetBits) for GPIO_ResetBits
17 /**...以下部分省略****/
18¶
19
在这部分中,详细列出了各个*.o文件之间的符号引用。由于*.o文件是由asm或c/c++源文件编译后生成的,各个文件及文件内的节区间互相独立,链接器根据它们之间的互相引用链接起来,链接的详细信息在这个“SectionCross References”一一列出。
例如,开头部分说明的是startup_stm32f429_439xx.o文件中的“RESET”节区分为它使用的“__initial_sp”符号引用了同文件“STACK”节区。
也许我们对启动文件不熟悉,不清楚这究竟是什么,那我们继续浏览,可看到main.o文件的引用说明,如说明main.o文件的i.main节区为它使用的LED_GPIO_Config符号引用了bsp_led.o文件的i.LED_GPIO_Config节区。
同样地,下面还有bsp_led.o文件的引用说明,如说明了bsp_led.o文件的i.LED_GPIO_Config节区为它使用的GPIO_Init符号引用了stm32f4xx_gpio.o文件的i.GPIO_Init节区。
可以了解到,这些跨文件引用的符号其实就是源文件中的函数名、变量名。有时在构建工程的时候,编译器会输出“Undefined symbol xxx (referred from xxx.o)”这样的提示,该提示的原因就是在链接过程中,某个文件无法在外部找到它引用的标号,因而产生链接错误。例如,见图1‑44,我们把bsp_led.c文件中定义的函数LED_GPIO_Config改名为LED_GPIO_ConfigABCD,而不修改main.c文件中的调用,就会出现main文件无法找到LED_GPIO_Config符号的提示。
图 1‑44 找不到符号的错误提示
删除无用节区
map文件的第二部分是删除无用节区的说明(Removing Unused input sectionsfrom the image.),见代码清单 1‑11。
代码清单 1‑11 删除无用节区(部分,多彩流水灯.map文件)
1 =================================================================
2 Removing Unused input sections from the image.
3
4 Removing startup_stm32f429_439xx.o(HEAP), (512 bytes).
5 Removing system_stm32f4xx.o(.rev16_text), (4 bytes).
6 Removing system_stm32f4xx.o(.revsh_text), (4 bytes).
7 Removing system_stm32f4xx.o(.rrx_text), (6 bytes).
8 Removing system_stm32f4xx.o(i.SystemCoreClockUpdate), (136 bytes).
9 Removing system_stm32f4xx.o(.data), (20 bytes).
10 Removing misc.o(.rev16_text), (4 bytes).
11 Removing misc.o(.revsh_text), (4 bytes).
12 Removing misc.o(.rrx_text), (6 bytes).
13 Removing misc.o(i.NVIC_Init), (104 bytes).
14 Removing misc.o(i.NVIC_PriorityGroupConfig), (20 bytes).
15 Removing misc.o(i.NVIC_SetVectorTable), (20 bytes).
16 Removing misc.o(i.NVIC_SystemLPConfig), (28 bytes).
17 Removing misc.o(i.SysTick_CLKSourceConfig), (28 bytes).
18 Removing stm32f4xx_adc.o(.rev16_text), (4 bytes).
19 Removing stm32f4xx_adc.o(.revsh_text), (4 bytes).
20 Removing stm32f4xx_adc.o(.rrx_text), (6 bytes).
21 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogCmd), (16 bytes).
22 Removingstm32f4xx_adc.o(i.ADC_AnalogWatchdogSingleChannelConfig), (12bytes).
23 Removing stm32f4xx_adc.o(i.ADC_AnalogWatchdogThresholdsConfig),(6 bytes).
24 Removing stm32f4xx_adc.o(i.ADC_AutoInjectedConvCmd), (24bytes).
25 /**...以下部分省略****/
26¶
这部分列出了在链接过程它发现工程中未被引用的节区,这些未被引用的节区将会被删除(指不加入到*.axf文件,不是指在*.o文件删除),这样可以防止这些无用数据占用程序空间。
例如,上面的信息中说明startup_stm32f429_439xx.o中的HEAP(在启动文件中定义的用于动态分配的“堆”区)以及stm32f4xx_adc.o的各个节区都被删除了,因为在我们这个工程中没有使用动态内存分配,也没有引用任何stm32f4xx_adc.c中的内容。由此也可以知道,虽然我们把STM32标准库的各个外设对应的c库文件都添加到了工程,但不必担心这会使工程变得臃肿,因为未被引用的节区内容不会被加入到最终的机器码文件中。
符号映像表
map文件的第三部分是符号映像表(Image Symbol Table),见代码清单 1‑12。
代码清单 1‑12 符号映像表(部分,多彩流水灯.map文件)
1¶
2 Image Symbol Table
3
4 Local Symbols
5
6 Symbol Name Value Ov Type Size Object(Section)
7 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry.o ABSOLUTE
8 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry9a.o ABSOLUTE
9 ../clib/microlib/init/entry.s 0x00000000 Number 0 entry9b.o ABSOLUTE
10 /*...省略部分*/
11 LED_GPIO_Config 0x080002a5 Thumb Code 106bsp_led.o(i.LED_GPIO_Config)
12 MemManage_Handler 0x08000319 Thumb Code 2stm32f4xx_it.o(i.MemManage_Handler)
13 NMI_Handler 0x0800031b Thumb Code 2 stm32f4xx_it.o(i.NMI_Handler)
14 PendSV_Handler 0x0800031d Thumb Code 2stm32f4xx_it.o(i.PendSV_Handler)
15 RCC_AHB1PeriphClockCmd 0x08000321 Thumb Code 22stm32f4xx_rcc.o(i.RCC_AHB1PeriphClockCmd)
16 SVC_Handler 0x0800033d Thumb Code 2 stm32f4xx_it.o(i.SVC_Handler)
17 SysTick_Handler 0x08000415 Thumb Code 2stm32f4xx_it.o(i.SysTick_Handler)
18 SystemInit 0x08000419 Thumb Code 62 system_stm32f4xx.o(i.SystemInit)
19 UsageFault_Handler 0x08000469 Thumb Code 2stm32f4xx_it.o(i.UsageFault_Handler)
20 __scatterload_copy 0x0800046b Thumb Code 14handlers.o(i.__scatterload_copy)
21 __scatterload_null 0x08000479 Thumb Code 2handlers.o(i.__scatterload_null)
22 __scatterload_zeroinit 0x0800047b Thumb Code 14handlers.o(i.__scatterload_zeroinit)
23 main 0x08000489 Thumb Code 270 main.o(i.main)
24 /*...省略部分*/
25¶
这个表列出了被引用的各个符号在存储器中的具体地址、占据的空间大小等信息。如我们可以查到LED_GPIO_Config符号存储在0x080002a5地址,它属于ThumbCode类型,大小为106字节,它所在的节区为bsp_led.o文件的i.LED_GPIO_Config节区。
存储器映像索引
map文件的第四部分是存储器映像索引(Memory Map of the image),见代码清单1‑13。
代码清单 1‑13 存储器映像索引(部分,多彩流水灯.map文件)
1¶
2 Memory Map of the image
3
4 Image Entry point : 0x080001ad
5 Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000005b0, Max:0x00100000, ABSOLUTE)
6
7 Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x000005b0, Max:0x00100000, ABSOLUTE)
8
9 Base Addr Size Type Attr Idx E Section Name Object
10
11 0x08000000 0x000001ac Data RO 3 RESET startup_stm32f429_439xx.o
12 /*..省略部分*/
13 0x0800020c 0x00000012 Code RO 5161 i.Delay main.o
14 0x0800021e 0x0000007c Code RO 2046 i.GPIO_Init stm32f4xx_gpio.o
15 0x0800029a 0x00000004 Code RO 2053 i.GPIO_ResetBitsstm32f4xx_gpio.o
16 0x0800029e 0x00000004 Code RO 2054 i.GPIO_SetBits stm32f4xx_gpio.o
17 0x080002a2 0x00000002 Code RO 5196 i.HardFault_Handlerstm32f4xx_it.o
18 0x080002a4 0x00000074 Code RO 5269 i.LED_GPIO_Config bsp_led.o
19 0x08000318 0x00000002 Code RO 5197 i.MemManage_Handlerstm32f4xx_it.o
20 /*..省略部分*/
21 0x08000488 0x00000118 Code RO 5162 i.main main.o
22 0x080005a0 0x00000010 Data RO 5309 Region$$Table anon$$obj.o
23
24 Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00000400,Max: 0x00030000, ABSOLUTE)
25
26 Base Addr Size Type Attr Idx E Section Name Object
27
28 0x20000000 0x00000400 Zero RW 1 STACK startup_stm32f429_439xx.o
29¶
本工程的存储器映像索引分为ER_IROM1及RW_IRAM1部分,它们分别对应STM32内部FLASH及SRAM的空间。相对于符号映像表,这个索引表描述的单位是节区,而且它描述的主要信息中包含了节区的类型及属性,由此可以区分Code、RO-data、RW-data及ZI-data。
例如,从上面的表中我们可以看到i.LED_GPIO_Config节区存储在内部FLASH的0x080002a4地址,大小为0x00000074,类型为Code,属性为RO。而程序的STACK节区(栈空间)存储在SRAM的0x20000000地址,大小为0x00000400,类型为Zero,属性为RW(即RW-data)。
映像组件大小
map文件的最后一部分是包含映像组件大小的信息(Image componentsizes),这也是最常查询的内容,见代码清单 1‑14。
代码清单 1‑14 映像组件大小(部分,多彩流水灯.map文件)
1¶
2 Image component sizes
3
4 Code (inc. data) RO Data RW Data ZI Data Debug Object Name
5
6 116 10 0 0 0 578 bsp_led.o
7 298 10 0 0 0 1459 main.o
8 36 8 428 0 1024 932 startup_stm32f429_439xx.o
9 132 0 0 0 0 2432 stm32f4xx_gpio.o
10 18 0 0 0 0 3946 stm32f4xx_it.o
11 28 6 0 0 0 645 stm32f4xx_rcc.o
12 292 34 0 0 0 253101 system_stm32f4xx.o
13
14¶
15 926 68 444 0 1024 263093 Object Totals
16 0 0 16 0 0 0 (incl. Generated)
17 6 0 0 0 0 0 (incl. Padding)
18
19 /*...省略部分*/
20¶
21 **Code (inc. data) RO Data RW Data ZI Data Debug **
22
23 1012 84 444 0 1024 262637 Grand Totals
24 1012 84 444 0 1024 262637 ELF Image Totals
25 1012 84 444 0 0 0 ROM Totals
26¶
27 Total RO Size (Code + RO Data) 1456 ( 1.42kB)
28 Total RW Size (RW Data + ZI Data) 1024 ( 1.00kB)
29 Total ROM Size (Code + RO Data + RW Data) 1456 ( 1.42kB)
30¶
这部分包含了各个使用到的*.o文件的空间汇总信息、整个工程的空间汇总信息以及占用不同类型存储器的空间汇总信息,它们分类描述了具体占据的Code、RO-data、RW-data及ZI-data的大小,并根据这些大小统计出占据的ROM总空间。
我们仅分析最后两部分信息,如GrandTotals一项,它表示整个代码占据的所有空间信息,其中Code类型的数据大小为1012字节,这部分包含了84字节的指令数据(inc.data)已算在内,另外RO-data占444字节,RW-data占0字节,ZI-data占1024字节。在它的下面两行有一项ROMTotals信息,它列出了各个段所占据的ROM空间,除了ZI-data不占ROM空间外,其余项都与GrandTotals中相等(RW-data也占据ROM空间,只是本工程中没有RW-data类型的数据而已)。
最后一部分列出了只读数据(RO)、可读写数据(RW)及占据的ROM大小。其中只读数据大小为1456字节,它包含Code段及RO-data段;可读写数据大小为1024字节,它包含RW-data及ZI-data段;占据的ROM大小为1456字节,它除了Code段和RO-data段,还包含了运行时需要从ROM加载到RAM的RW-data数据。
综合整个map文件的信息,可以分析出,当程序下载到STM32的内部FLASH时,需要使用的内部FLASH是从0x08000000地址开始的大小为1456字节的空间;当程序运行时,需要使用的内部SRAM是从0x20000000地址开始的大小为1024字节的空间。
粗略一看,发现这个小程序竟然需要1024字节的SRAM,实在说不过去,但仔细分析map文件后,可了解到这1024字节都是STACK节区的空间(即栈空间),栈空间大小是在启动文件中定义的,这1024字节是默认值(0x00000400)。它是提供给C语言程序局部变量申请使用的空间,若我们确认自己的应用程序不需要这么大的栈,完全可以修改启动文件,把它改小一点,查看前面讲解的htm静态调用图文件可了解静态的栈调用情况,可以用它作为参考。
当工程按默认配置构建时,MDK会根据我们选择的芯片型号,获知芯片的内部FLASH及内部SRAM存储器概况,生成一个以工程名命名的后缀为*.sct的分散加载文件(LinkerControl File,scatterloading),链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。
例如可以设置源文件中定义的所有变量自动按地址分配到外部SDRAM,这样就不需要再使用关键字“__attribute__”按具体地址来指定了;利用它还可以控制代码的加载区与执行区的位置,例如可以把程序代码存储到单位容量价格便宜的NAND-FLASH中,但在NAND-FLASH中的代码是不能像内部FLASH的代码那样直接提供给内核运行的,这时可通过修改分散加载文件,把代码加载区设定为NAND-FLASH的程序位置,而程序的执行区设定为SDRAM中的位置,这样链接器就会生成一个配套的分散加载代码,该代码会把NAND-FLASH中的代码加载到SDRAM中,内核再从SDRAM中运行主体代码,大部分运行Linux系统的代码都是这样加载的。
下面先来看看MDK默认使用的sct文件,在Output目录下可找到“多彩流水灯.sct”,该文件记录的内容见代码清单1‑15。
代码清单 1‑15 默认的分散加载文件内容(“多彩流水灯.sct”)
1 ;*************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ;*************************************************************
4
5 LR_IROM1 0x08000000 0x00100000 { ; 注释:加载域,基地址 空间大小
6 ER_IROM1 0x08000000 0x00100000 { ; 注释:加载地址 = 执行地址
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11 RW_IRAM1 0x20000000 0x00030000 { ; 注释:可读写数据
12 .ANY (+RW +ZI)
13 }
14 }
15
在默认的sct文件配置中仅分配了Code、RO-data、RW-data及ZI-data这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。
sct文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号“{}”分隔开,最外层的是加载域,第二层“{}”内的是执行域,其整体结构见图1‑45。
图 1‑45 分散加载文件的整体结构
加载域
sct文件的加载域格式见代码清单 1‑16。
代码清单 1‑16 加载域格式
1 //方括号中的为选填内容
2 加载域名 (基地址 | (“+” 地址偏移)) [属性列表] [最大容量]
3 “{“
4 执行区域描述+
5 “}”
配合前面代码清单 1‑15中的分散加载文件内容,各部分介绍如下:
加载域名:名称,在map文件中的描述会使用该名称来标识空间。如本例中只有一个加载域,该域名为LR_IROM1。
基地址+地址偏移:这部分说明了本加载域的基地址,可以使用+号连接一个地址偏移,算进基地址中,整个加载域以它们的结果为基地址。如本例中的加载域基地址为0x08000000,刚好是STM32内部FLASH的基地址。
属性列表:属性列表说明了加载域的是否为绝对地址、N字节对齐等属性,该配置是可选的。本例中没有描述加载域的属性。
最大容量:最大容量说明了这个加载域可使用的最大空间,该配置也是可选的,如果加上这个配置后,当链接器发现工程要分配到该区域的空间比容量还大,它会在工程构建过程给出提示。本例中的加载域最大容量为0x00100000,即1MB,正是本型号STM32内部FLASH的空间大小。
执行域
sct文件的执行域格式见代码清单 1‑17。
代码清单 1‑17 执行域格式
1 //方括号中的为选填内容
2 执行域名 (基地址 | “+” 地址偏移) [属性列表] [最大容量 ]
3 “{“
4 输入节区描述
5 “}”
执行域的格式与加载域是类似的,区别只是输入节区的描述有所不同,在代码清单1‑15的例子中包含了ER_IROM1及RW_IRAM两个执行域,它们分别对应描述了STM32的内部FLASH及内部SRAM的基地址及空间大小。而它们内部的“输入节区描述”说明了哪些节区要存储到这些空间,链接器会根据它来处理编排这些节区。
输入节区描述
配合加载域及执行域的配置,在相应的域配置“输入节区描述”即可控制该节区存储到域中,其格式见代码清单1‑18。
代码清单 1‑18 输入节区描述的几种格式
1 //除模块选择样式部分外,其余部分都可选选填
2 模块选择样式”(“输入节区样式”,”“+”输入节区属性”)”
3 模块选择样式”(“输入节区样式”,”“+”节区特性”)”
4
5 模块选择样式”(“输入符号样式”,”“+”节区特性”)”
6 模块选择样式”(“输入符号样式”,”“+”输入节区属性”)”
配合前面代码清单 1‑15中的分散加载文件内容,各部分介绍如下:
模块选择样式:模块选择样式可用于选择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”。
输入节区属性:通过在模块选择样式后面加入输入节区属性,可以选择样式中不同的内容,每个节区属性描述符前要写一个“+”号,使用空格或“,”号分隔开,可以使用的节区属性描述符见表1‑6。
表 1‑6 属性描述符及其意义
节区属性描述符 | 说明 |
---|---|
RO-CODE及CODE | 只读代码段 |
RO-DATA及CONST | 只读数据段 |
RO及TEXT | 包括RO-CODE及RO-DATA |
RW-DATA | 可读写数据段 |
RW-CODE | 可读写代码段 |
RW及DATA | 包括RW-DATA及RW-CODE |
ZI及BSS | 初始化为0的可读写数据段 |
XO | 只可执行的区域 |
ENTRY | 节区的入口点 |
例如,示例文件中使用“.ANY(+RO)”选择剩余所有节区RO属性的内容都分配到执行域ER_IROM1中,使用“.ANY(+RW+ZI)”选择剩余所有节区RW及ZI属性的内容都分配到执行域RW_IRAM1中。
节区特性:节区特性可以使用“+FIRST”或“+LAST”选项配置它要存储到的位置,FIRST存储到区域的头部,LAST存储到尾部。通常重要的节区会放在头部,而CheckSum(校验和)之类的数据会放在尾部。
例如示例文件中使用“(RESET,+First)”选择了RESET节区,并要求把它放置到本区域第一个位置,而RESET是工程启动代码中定义的向量表,见代码清单1‑19,该向量表中定义的堆栈顶和复位向量指针必须要存储在内部FLASH的前两个地址,这样STM32才能正常启动,所以必须使用FIRST控制它们存储到首地址。
代码清单 1‑19 startup_stm32f429_439xx.s文件中定义的RESET区(部分)
1 ; Vector Table Mapped to Address 0 at Reset
2 AREA RESET, DATA, READONLY
3 EXPORT __Vectors
4 EXPORT __Vectors_End
5 EXPORT __Vectors_Size
6
7 __Vectors DCD __initial_sp ; Top of Stack
8 DCD Reset_Handler ; Reset Handler
9 DCD NMI_Handler ; NMI Handler
总的来说,我们的sct示例文件配置如下:程序的加载域为内部FLASH的0x08000000,最大空间为0x00100000;程序的执行基地址与加载基地址相同,其中RESET节区定义的向量表要存储在内部FLASH的首地址,且所有o文件及lib文件的RO属性内容都存储在内部FLASH中;程序执行时RW及ZI区域都存储在以0x20000000为基地址,大小为0x00030000的空间(192KB),这部分正好是STM32内部主SRAM的大小。
链接器根据sct文件链接,链接后各个节区、符号的具体地址信息可以在map文件中查看。
了解sct文件的格式后,可以手动编辑该文件控制整个工程的分散加载配置,但sct文件格式比较复杂,所以MDK提供了相应的配置选项可以方便地修改该文件,这些选项配置能满足基本的使用需求,本小节将对这些选项进行说明。
选择sct文件的产生方式
首先需要选择sct文件产生的方式,选择使用MDK生成还是使用用户自定义的sct文件。在MDK的“Optionsfor Target->Linker->Use Memory Layout from TargetDialog”选项即可配置该选择,见图 1‑46。
图 1‑46 选择使用MDK生成的sct文件
该选项的译文为“是否使用Target对话框中的存储器分布配置”,勾选后,它会根据“OptionsforTarget”对话框中的选项生成sct文件,这种情况下,即使我们手动打开它生成的sct文件编辑也是无效的,因为每次构建工程的时候,MDK都会生成新的sct文件覆盖旧文件。该选项在MDK中是默认勾选的,若希望MDK使用我们手动编辑的sct文件构建工程,需要取消勾选,并通过ScatterFile框中指定sct文件的路径,见图 1‑47。
图 1‑47 使用指定的sct文件构建工程
通过Target对话框控制存储器分配
若我们在Linker中勾选了“使用Target对话框的存储器布局”选项,那么“OptionsforTarget”对话框中的存储器配置就生效了。主要配置是在Device标签页中选择芯片的类型,设定芯片基本的内部存储器信息以及在Target标签页中细化具体的存储器配置(包括外部存储器),见图1‑48及图 1‑49。
图 1‑48 选择芯片类型
图中Device标签页中选定了芯片的型号为STM32F429IGTx,选中后,在Target标签页中的存储器信息会根据芯片更新。
图 1‑49 Target对话框中的存储器分配
在Target标签页中存储器信息分成只读存储器(Read/Only MemoryAreas)和可读写存储器(Read/Write MemoryAreas)两类,即ROM和RAM,而且它们又细分成了片外存储器(off-chip)和片内存储器(on-chip)两类。
例如,由于我们已经选定了芯片的型号,MDK会自动根据芯片型号填充片内的ROM及RAM信息,其中的IROM1起始地址为0x80000000,大小为0x100000,正是该STM32型号的内部FLASH地址及大小;而IRAM1起始地址为0x20000000,大小为0x30000,正是该STM32内部主SRAM的地址及大小。图中的IROM1及IRAM1前面都打上了勾,表示这个配置信息会被采用,若取消勾选,则该存储配置信息是不会被使用的。
在标签页中的IRAM2一栏默认也填写了配置信息,它的地址为0x10000000,大小为0x10000,这是STM32F4系列特有的内部64KB高速SRAM(被称为CCM)。当我们希望使用这部分存储空间的时候需要勾选该配置,另外要注意这部分高速SRAM仅支持CPU总线的访问,不能通过外设访问。
下面我们尝试修改Target标签页中的这些存储信息,例如,按照图1‑50中的1配置,把IRAM1的基地址改为0x20001000,然后编译工程,查看到工程的sct文件如代码清单1‑20所示;当按照图1‑50中的2配置时,同时使用IRAM1和IRAM2,然后编译工程,可查看到工程的sct文件如代码清单1‑21所示。
图 1‑50 修改IRAM1的基地址及仅使用IRAM2的配置
代码清单 1‑20 修改了IRAM1基地址后的sct文件内容
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20001000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 }
代码清单 1‑21 仅使用IRAM2时的sct文件内容
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 RW_IRAM2 0x10000000 0x00010000 {
11 .ANY (+RW +ZI)
12 }
13 }
可以发现,sct文件都根据Target标签页做出了相应的改变,除了这种修改外,在Target标签页上还控制同时使用IRAM1和IRAM2、加入外部RAM(如外接的SDRAM),外部FLASH等。
控制文件分配到指定的存储空间
设定好存储器的信息后,可以控制各个源文件定制到哪个部分存储器,在MDK的工程文件栏中,选中要配置的文件,右键,并在弹出的菜单中选择“Optionsfor Filexxxx”即可弹出一个文件配置对话框,在该对话框中进行存储器定制,见图 1‑51。
图 1‑51 使用右键打开文件配置并把它的RW区配置成使用IRAM2
在弹出的对话框中有一个“MemoryAssignment”区域(存储器分配),在该区域中可以针对文件的各种属性内容进行分配,如Code/Const内容(RO)、ZeroInitialized Data内容(ZI-data)以及OtherData内容(RW-data),点击下拉菜单可以找到在前面Target页面配置的IROM1、IRAM1、IRAM2等存储器。例如图中我们把这个bsp_led.c文件的OtherData属性的内容分配到了IRAM2存储器(在Target标签页中我们勾选了IRAM1及IRAM2),当在bsp_led.c文件定义了一些RW-data内容时(如初值非0的全局变量),该变量将会被分配到IRAM2空间,配置完成后点击OK,然后编译工程,查看到的sct文件内容见代码清单1‑22。
代码清单 1‑22 修改bsp_led.c配置后的sct文件
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; RW data
8 .ANY (+RW +ZI)
9 }
10 RW_IRAM2 0x10000000 0x00010000 {
11 bsp_led.o (+RW)
12 .ANY (+RW +ZI)
13 }
14 }
可以看到在sct文件中的RW_IRAM2执行域中增加了一个选择bsp_led.o中RW内容的语句。
类似地,我们还可以设置某些文件的代码段被存储到特定的ROM中,或者设置某些文件使用的ZI-data或RW-data存储到外部SDRAM中(控制ZI-data到SDRAM时注意还需要修改启动文件设置堆栈对应的地址,原启动文件中的地址是指向内部SRAM的)。
虽然MDK的这些存储器配置选项很方便,但有很多高级的配置还是需要手动编写sct文件实现的,例如MDK选项中的内部ROM选项最多只可以填充两个选项位置,若想把内部ROM分成多片地址管理就无法实现了;另外MDK配置可控的最小粒度为文件,若想控制特定的节区也需要直接编辑sct文件。
接下来我们将讲解几个实验,通过编写sct文件定制存储空间。
由于内存管理对应用程序非常重要,若修改sct文件,不使用默认配置,对工程影响非常大,容易导致出错,所以我们使用两个实验配置来讲解sct文件的应用细节,希望您学习后不仅知其然而且知其所以然,清楚地了解修改后对应用程序的影响,还可以举一反三根据自己的需求进行存储器定制。
在本书前面的SDRAM实验中,当我们需要读写SDRAM存储的内容时,需要使用指针或者__attribute__((at(具体地址)))来指定变量的位置,当有多个这样的变量时,就需要手动计算地址空间了,非常麻烦。在本实验中我们将修改sct文件,让链接器自动分配全局变量到SDRAM的地址并进行管理,使得利用SDRAM的空间就跟内部SRAM一样简单。
本小节中使用到的硬件跟“扩展外部SDRAM”实验中的一致,若不了解,请参考该章节的原理图说明。
本小节中提供的例程名为“SCT文件应用—自动分配变量到SDRAM”,学习时请打开该工程来理解,该工程是基于“扩展外部SDRAM”实验改写而来的。
为方便讲解,本实验直接使用手动编写的sct文件,所以在MDK的“Options forTarget->Linker->Use Memory Layout from TargetDialog”选项被取消勾选,取消勾选后可直接点击“Edit”按钮编辑工程的sct文件,也可到工程目录下打开编辑,见图1‑52。
图 1‑52 使用手动编写的sct文件
取消了这个勾选后,在MDK的Target对话框及文件配置的存储器分布选项都会失效,仅以sct文件中的为准,更改对话框及文件配置选项都不会影响sct文件的内容。
修改启动文件,在__main执行之前初始化SDRAM;
在sct文件中增加外部SDRAM空间对应的执行域;
使用节区选择语句选择要分配到SDRAM的内容;
编写测试程序,编译正常后,查看map文件的空间分配情况。
在__main之前初始化SDRAM
在前面讲解ELF文件格式的小节中我们了解到,芯片启动后,会通过__main函数调用分散加载代码__scatterload,分散加载代码会把存储在FLASH中的RW-data复制到RAM中,然后在RAM区开辟一块ZI-data的空间,并将其初始化为0值。因此,为了保证在程序中定义到SDRAM中的变量能被正常初始化,我们需要在系统执行分散加载代码之前使SDRAM存储器正常运转,使它能够正常保存数据。
在本来的“扩展外部SDRAM”工程中,我们使用SDRAM_Init函数初始化SDRAM,且该函数在main函数里才被调用,所以在SDRAM正常运转之前,分散加载过程复制到SDRAM中的数据都丢失了,因而需要在初始化SDRAM之后,需要重新给变量赋值才能正常使用(即定义变量时的初值无效,在调用SDRAM_Init函数之后的赋值才有效)。
为了解决这个问题,可修改工程的startup_stm32f429_439xx.s启动文件,见代码清单1‑23。
代码清单 1‑23修改启动文件中的Reset_handler函数(startup_stm32f429_439xx.s文件)
1 ; Reset handler
2 Reset_Handler PROC
3 EXPORT Reset_Handler [WEAK]
4 IMPORT SystemInit
5 IMPORT __main
6
7 ;从外部文件引入声明
8 IMPORT SDRAM_Init
9
10 LDR R0, =SystemInit
11 BLX R0
12
13 ;在__main之前调用SDRAM_Init进行初始化
14 LDR R0, =SDRAM_Init
15 BLX R0
16
17 LDR R0, =__main
18 BX R0
19 ENDP
在原来的启动文件中我们增加了上述加粗表示的代码,增加的代码中使用到汇编语法的IMPOR引入在bsp_sdram.c文件中定义的SDRAM_Init函数,接着使用LDR指令加载函数的代码地址到寄存器R0,最后使用BLXR0指令跳转到SDRAM_Init的代码地址执行。
以上代码实现了Reset_handler在执行__main函数前先调用了我们自定义的SDRAM_Init函数,从而为分散加载代码准备好正常的硬件工作环境。
sct文件初步应用
接下来修改sct文件,控制使得在C源文件中定义的全局变量都自动由链接器分配到外部SDRAM中,见代码清单1‑24。
代码清单 1‑24 配置sct文件(SDRAM.sct文件)
1 ;*************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ;*************************************************************
4
5 LR_IROM1 0x08000000 0x00100000 { ; 加载域
6 ER_IROM1 0x08000000 0x00100000 { ; 加载地址 = 执行地址
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11
12
13 RW_IRAM1 0x20000000 0x00030000 { ; 内部SRAM
14 *.o(STACK) ;选择STACK节区,栈
15 stm32f4xx_rcc.o(+RW) ;选择stm32f4xx_rcc的RW内容
16 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到这里
17 }
18
19 RW_ERAM1 0xD0000000 0x00800000 { ; 外部SDRAM
20
21 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到这里
22 }
23 }
加粗部分是本例子中增加的代码,我们从后面开始,先分析比较简单的SDRAM执行域部分。
RW_ERAM1 0xD0000000 0x00800000{}
RW_ERAM1是我们配置的SDRAM执行域。该执行域的名字是可以随便取的,最重要的是它的基地址及空间大小,这两个值与我们实验板配置的SDRAM基地址及空间大小一致,所以该执行域会被映射到SDRAM的空间。在RW_ERAM1执行域内部,它使用“.ANY(+RW+ZI)”语句,选择了所有的RW/ZI类型的数据都分配到这个SDRAM区域,所以我们在工程中的C文件定义全局变量时,它都会被分配到这个SDRAM区域。
RW_IRAM1执行域
RW_IRAM1是STM32内部SRAM的执行域。我们在默认配置中增加了“*.o(STACK)及stm32f4xx_rcc.o(+RW)”语句。本来上面配置SDRAM执行域后已经达到使全局变量分配的目的,为何还要修改原内部SRAM的执行域呢?
这是由于我们在__main之前调用的SDRAM_Init函数调用了很多库函数,且这些函数内部定义了一些局部变量,而函数内的局部变量是需要分配到“栈”空间(STACK),见图1‑53,查看静态调用图文件“SDRAM.htm”可了解它使用了多少栈空间以及调用了哪些函数。
图 1‑53 SDRAM_Init的调用说明(SDRAM.htm文件)
从文件中可了解到SDRAM_Init使用的STACK的深度为148字节,它调用了FMC_SDRAMInit、RCC_AHB3PeriphClockCmd、SDRAM_InitSequence及SDRAM_GPIO_Config等函数。由于它使用了栈空间,所以在SDRAM_Init函数执行之前,栈空间必须要被准备好,然而在SDRAM_Init函数执行之前,SDRAM芯片却并未正常工作,这样的矛盾导致栈空间不能被分配到SDRAM。
虽然内部SRAM的执行域RW_IRAM1及SDRAM执行域RW_ERAM1中都使用“.ANY(+RW+ZI)”语句选择了所有RW及ZI属性的内容,但对于符合两个相同选择语句的内容,链接器会优先选择使用空间较大的执行域,即这种情况下只有当SDRAM执行域的空间使用完了,RW/ZI属性的内容才会被分配到内部SRAM。
所以在大部分情况下,内部SRAM执行域中的“.ANY(+RW+ZI)”语句是不起作用的(),而栈节区(STACK)又属于ZI-data类,如果我们的内部SRAM执行域还是按原来的默认配置的话,栈节区会被分配到外部SDRAM,导致出错。为了避免这个问题,我们把栈节区使用“*.o(STACK)”语句分配到内部SRAM的执行域。
增加“stm32f4xx_rcc.o(+RW)”语句是因为SDRAM_Init函数调用了stm32f4xx_rcc.c文件中的RCC_AHB3PeriphClockCmd函数,而查看map文件后了解到stm32f4xx_rcc.c定义了一些RW-data类型的变量,见图1‑54。不管这些数据是否在SDRAM_Init调用过程中使用到,保险起见,我们直接把这部分内容也分配到内部SRAM的执行区。
图 1‑54 SDRAM.map文件中查看到stm32f4xx_rcc.o文件的RW-data使用统计信息
变量分配测试及结果
接下来查看本工程中的main文件,它定义了各种变量测试空间分配,见代码清单1‑25。
代码清单 1‑25 main文件
1
2 //定义变量到SDRAM
3 uint32_t testValue =0 ;
4 //定义变量到SDRAM
5 uint32_t testValue2 =7;
6
7 //定义数组到SDRAM
8 uint8_t testGrup[100] ={0};
9 //定义数组到SDRAM
10 uint8_t testGrup2[100] ={1,2,3};
11
12 /**
13 * @brief 主函数
14 * @param 无
15 * @retval 无
16 */
17 int main(void)
18 {
19 uint32_t inerTestValue =10;
20/*SDRAM_Init已经在启动文件的Reset_handler中调用,进入main之前已经完成初始化*/
21 // SDRAM_Init();
22
23 /* LED 端口初始化 */
24 LED_GPIO_Config();
25
26 /* 初始化串口 */
27 Debug_USART_Config();
28
29 printf(“\r\nSCT文件应用——自动分配变量到SDRAM实验\r\n”);
30
31 printf(“\r\n使用“ uint32_t inerTestValue =10;”语句定义的局部变量:\r\n”);
32printf(“结果:它的地址为:0x%x,变量值为:%d\r\n”,(uint32_t)&inerTestValue,inerTestValue);
33
34 printf(“\r\n使用“uint32_t testValue =0;”语句定义的全局变量:\r\n”);
35printf(“结果:它的地址为:0x%x,变量值为:%d\r\n”,(uint32_t)&testValue,testValue);
36
37 printf(“\r\n使用“uint32_t testValue2 =7 ;”语句定义的全局变量:\r\n”);
38printf(“结果:它的地址为:0x%x,变量值为:%d\r\n”,(uint32_t)&testValue2,testValue2);
39
40 printf(“\r\n使用“uint8_t testGrup[100]={0};”语句定义的全局数组:\r\n”);
41printf(“结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n”,(uint32_t)&testGrup,testGrup[0],testGrup[1],testGrup[2]);
42
43 printf(“\r\n使用“uint8_t testGrup2[100]={1,2,3};”语句定义的全局数组:\r\n”);
44printf(“结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n”,(uint32_t)&testGrup2,testGrup2[0],testGrup2[1],testGrup2[2]);
45
46 uint32_t * pointer = (uint32_t*)malloc(sizeof(uint32_t)*3);
47 if(pointer != NULL)
48 {
49 *(pointer)=1;
50 *(++pointer)=2;
51 *(++pointer)=3;
52
53 printf(“\r\n使用“ uint32_t *pointer =(uint32_t*)malloc(sizeof(uint32_t)*3); ”动态分配的变量\r\n”);
54printf(“\r\n定义后的操作为:\r\n*(pointer++)=1;\r\n*(pointer++)=2;\r\n*pointer=3;”);
55printf(“结果:操作后它的地址为:0x%x,查看变量值操作:\r\n”,(uint32_t)pointer);
56 printf(“*(pointer–)=%d, \r\n”,*(pointer–));
57 printf(“*(pointer–)=%d, \r\n”,*(pointer–));
58 printf(“*(pointer)=%d, \r\n”,*(pointer));
59 }
60 else
61 {
62 printf(“\r\n使用malloc动态分配变量出错!!!\r\n”);
63 }
64 /*蓝灯亮*/
65 LED_BLUE;
66 while(1);
67 }
68
代码中定义了局部变量、初值非0的全局变量及数组、初值为0的全局变量及数组以及动态分配内存,并把它们的值和地址通过串口打印到上位机,通过这些变量,我们可以测试栈、ZI/RW-data及堆区的变量是否能正常分配。构建工程后,首先查看工程的map文件观察变量的分配情况,见图1‑55及图 1‑56。
图 1‑55在map文件中查看工程的存储分布1(SDRAM.map文件)
图 1‑56 在map文件中查看工程的存储分布2(SDRAM.map文件)
从map文件中,可看到stm32f4xx_rcc的RW-data及栈空间节区(STACK)都被分配到了RW_IRAM1区域,即STM32的内部SRAM空间中;而main文件中定义的RW-data、ZI-data以及堆空间节区(HEAP)都被分配到了RW_ERAM1区域,即我们扩展的SDRAM空间中,看起来一切都与我们的sct文件配置一致了。(堆空间属于ZI-data,由于没有像控制栈节区那样指定到内部SRAM,所以它被默认分配到SDRAM空间了;在main文件中我们定义了一个初值为0的全局变量testValue2及初值为0的数组testGrup[100],它们本应占用的是104字节的ZI-data空间,但在map文件中却查看到它仅使用了100字节的ZI-data空间,这是因为链接器把testValue2分配为RW-data类型的变量了,这是链接器本身的特性,它对像testGrup[100]这样的数组才优化作为ZI-data分配,这不是我们sct文件导致的空间分配错误。)
接下来把程序下载到实验板进行测试,串口打印的调试信息如图 1‑57。
图 1‑57 空间分配实验实测结果
从调试信息中可发现,除了对堆区使用malloc函数动态分配的空间不正常,其它变量都定义到了正确的位置,如内部变量定义在内部SRAM的栈区域,全局变量定义到了外部SDRAM的区域。
经过我的测试,即使在sct文件中使用“*.o(HEAP)”语句指定堆区到内部SRAM或外部SDRAM区域,都无法正常使用malloc分配空间。另外,由于外部SDRAM的读写速度比内部SRAM的速度慢,所以我们更希望默认定义的变量先使用内部SRAM,当它的空间使用完毕后再把变量分配到外部SDRAM。
在下一小节中我们将改进sct的文件配置,解决这两个问题。
本实验使用另一种方案配置sct文件,使得默认情况下优先使用内部SRAM空间,在需要的时候使用一个关键字指定变量存储到外部SDRAM,另外,我们还把系统默认的堆空间(HEAP)映射到外部SDRAM,从而可以使用C语言标准库的malloc函数动态从SDRAM中分配空间,利用标准库进行SDRAM的空间内存管理。
本小节中使用到的硬件跟“扩展外部SDRAM”实验中的一致,若不了解,请参考该章节的原理图说明。
本小节中提供的例程名为“SCT文件应用—优先使用内部SRAM并把堆分配到SDRAM空间”,学习时请打开该工程来理解,该工程从上一小节的实验改写而来的,同样地,本工程只使用手动编辑的sct文件配置,不使用MDK选项配置,在“Optionsfor Target->linker”的选项见图 1‑52。
图 1‑58 使用手动编写的sct文件
取消了这个默认的“Use Memory Layout from TargetDialog”勾选后,在MDK的Target对话框及文件配置的存储器分布选项都会失效,仅以sct文件中的为准,更改对话框及文件配置选项都不会影响sct文件的内容。
修改启动文件,在__main执行之前初始化SDRAM;
在sct文件中增加外部SDRAM空间对应的执行域;
在SDRAM中的执行域中选择一个自定义节区“EXRAM”;
使用__attribute__关键字指定变量分配到节区“EXRAM”;
使用宏封装__attribute__关键字,简化变量定义;
根据需要,把堆区分配到内部SRAM或外部SDRAM中;
编写测试程序,编译正常后,查看map文件的空间分配情况。
在__main之前初始化SDRAM
同样地,为了使定义到外部SDRAM的变量能被正常初始化,需要修改工程startup_stm32f429_439xx.s启动文件中的Reset_handler函数,在__main函数之前调用SDRAM_Init函数使SDRAM硬件正常运转,见代码清单1‑23。
代码清单 1‑26修改启动文件中的Reset_handler函数(startup_stm32f429_439xx.s文件)
1 ; Reset handler
2 Reset_Handler PROC
3 EXPORT Reset_Handler [WEAK]
4 IMPORT SystemInit
5 IMPORT __main
6
7 ;从外部文件引入声明
8 IMPORT SDRAM_Init
9
10 LDR R0, =SystemInit
11 BLX R0
12
13 ;在__main之前调用SDRAM_Init进行初始化
14 LDR R0, =SDRAM_Init
15 BLX R0
16
17 LDR R0, =__main
18 BX R0
19 ENDP
它与上一小节中的改动一样,当芯片上电运行Reset_handler函数时,在执行__main函数前先调用了我们自定义的SDRAM_Init函数,从而为分散加载代码准备好正常的硬件工作环境。
sct文件配置
接下来分析本实验中的sct文件配置与上一小节有什么差异,见代码清单 1‑27。
代码清单 1‑27 本实验的sct文件内容(SDRAM.sct)
1 ;*************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ;*************************************************************
4 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
5 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
6 *.o (RESET, +First)
7 *(InRoot$$Sections)
8 .ANY (+RO)
9 }
10
11
12 RW_IRAM1 0x20000000 0x00030000 { ; 内部SRAM
13 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到这里
14 }
15
16 RW_ERAM1 0xD0000000 0x00800000 { ; 外部SDRAM
17 *.o(HEAP) ;选择堆区
18 .ANY (EXRAM) ;选择EXRAM节区
19 }
20 }
本实验的sct文件中对内部SRAM的执行域保留了默认配置,没有作任何改动,新增了一个外部SDRAM的执行域,并且使用了“*.o(HEAP)”语句把堆区分配到了SDRAM空间,使用“.ANY(EXRAM)”语句把名为“EXRAM”的节区分配到SDRAM空间。
这个“EXRAM”节区是由我们自定义的,在语法上就跟在C文件中定义全局变量类似,只要它跟工程中的其它原有节区名不一样即可。有了这个节区选择配置,当我们需要定义变量到外部SDRAM时,只需要指定该变量分配到该节区,它就会被分配到SDRAM空间。
本实验中的sct配置就是这么简单,接下来直接使用就可以了。
指定变量分配到节区
当我们需要把变量分配到外部SDRAM时,需要使用__attribute__关键字指定节区,它的语法见代码清单1‑28。
代码清单 1‑28 指定变量定义到某节区的语法
1 //使用 __attribute__ 关键字定义指定变量定义到某节区
2 //语法: 变量定义 __attribute__ ((section (“节区名”))) = 变量值;
3 uint32_t testValue __attribute__ ((section (“EXRAM”))) =7 ;
4
5 //使用宏封装
6 //设置变量定义到“EXRAM”节区的宏
7 #define __EXRAM __attribute__ ((section (“EXRAM”)))
8
9 //使用该宏定义变量到SDRAM
10 uint32_t testValue __EXRAM =7 ;
11
上述代码介绍了基本的指定节区语法:“变量定义 __attribute__ ((section(“节区名”))) =变量值;”,它的主体跟普通的C语言变量定义语法无异,在赋值“=”号前(可以不赋初值),加了个“__attribute__((section(“节区名”)))”描述它要分配到的节区。本例中的节区名为“EXRAM”,即我们在sct文件中选择分配到SDRAM执行域的节区,所以该变量就被分配到SDRAM中了。
由于“__attribute__”关键字写起来比较繁琐,我们可以使用宏定义把它封装起来,简化代码。本例中我们把指定到“EXRAM”的描述语句“__attribute__((section(“EXRAM”)))”封装成了宏“__EXRAM”,应用时只需要使用宏的名字替换原来“__attribute__”关键字的位置即可,如“uint32_ttestValue __EXRAM =7;”。有51单片机使用经验的读者会发现,这种变量定义方法就跟使用keil51特有的关键字“xdata”定义变量到外部RAM空间差不多。
类似地,如果工程中还使用了其它存储器也可以用这样的方法实现变量分配,例如STM32的高速内部SRAM(CCM),可以在sct文件增加该高速SRAM的执行域,然后在执行域中选择一个自定义节区,在工程源文件中使用“__attribute__”关键字指定变量到该节区,就可以可把变量分配到高速内部SRAM了。
根据我们sct文件的配置,如果定义变量时没有指定节区,它会默认优先使用内部SRAM,把变量定义到内部SRAM空间,而且由于局部变量是属于栈节区(STACK),它不能使用“__attribute__”关键字指定节区。在本例中的栈节区被分配到内部SRAM空间。
变量分配测试及结果
接下来查看本工程中的main文件,它定义了各种变量测试空间分配,见代码清单1‑25。
代码清单 1‑29 main文件
1
2 //设置变量定义到“EXRAM”节区的宏
3 #define __EXRAM __attribute__ ((section (“EXRAM”)))
4
5 //定义变量到SDRAM
6 uint32_t testValue __EXRAM =7 ;
7 //上述语句等效于:
8 //uint32_t testValue __attribute__ ((section (“EXRAM”))) =7 ;
9
10 //定义变量到SRAM
11 uint32_t testValue2 =7 ;
12
13 //定义数组到SDRAM
14 uint8_t testGrup[3] __EXRAM ={1,2,3};
15 //定义数组到SRAM
16 uint8_t testGrup2[3] ={1,2,3};
17
18 /**
19 * @brief 主函数
20 * @param 无
21 * @retval 无
22 */
23 int main(void)
24 {
25 uint32_t inerTestValue =10;
26/*SDRAM_Init已经在启动文件的Reset_handler中调用,进入main之前已经完成初始化*/
27 // SDRAM_Init();
28
29 /* LED 端口初始化 */
30 LED_GPIO_Config();
31
32 /* 初始化串口 */
33 Debug_USART_Config();
34
35 printf(“\r\nSCT文件应用——自动分配变量到SDRAM实验\r\n”);
36
37 printf(“\r\n使用“ uint32_t inerTestValue =10;”语句定义的局部变量:\r\n”);
38printf(“结果:它的地址为:0x%x,变量值为:%d\r\n”,(uint32_t)&inerTestValue,inerTestValue);
39
40 printf(“\r\n使用“uint32_t testValue __EXRAM =7;”语句定义的全局变量:\r\n”);
41printf(“结果:它的地址为:0x%x,变量值为:%d\r\n”,(uint32_t)&testValue,testValue);
42
43 printf(“\r\n使用“uint32_t testValue2 =7 ;”语句定义的全局变量:\r\n”);
44printf(“结果:它的地址为:0x%x,变量值为:%d\r\n”,(uint32_t)&testValue2,testValue2);
45
46
47 printf(“\r\n使用“uint8_t testGrup[3] __EXRAM={1,2,3};”语句定义的全局数组:\r\n”);
48 printf(“结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n”,(uint32_t)&testGrup,testGrup[0],testGrup[1],testGrup[2]);
49
50 printf(“\r\n使用“uint8_t testGrup2[3]={1,2,3};”语句定义的全局数组:\r\n”);
51 printf(“结果:它的地址为:0x%x,变量值为:%d,%d,%d\r\n”,(uint32_t)&testGrup2,testGrup2[0],testGrup2[1],testGrup2[2]);
52
53 uint32_t *pointer = (uint32_t*)malloc(sizeof(uint32_t)*3);
54
55 if(pointer != NULL)
56 {
57 *(pointer)=1;
58 *(++pointer)=2;
59 *(++pointer)=3;
60
61 printf(“\r\n使用“ uint32_t *pointer =(uint32_t*)malloc(sizeof(uint32_t)*3); ”动态分配的变量\r\n”);
62printf(“\r\n定义后的操作为:\r\n*(pointer++)=1;\r\n*(pointer++)=2;\r\n*pointer=3;\r\n\r\n”);
63printf(“结果:操作后它的地址为:0x%x,查看变量值操作:\r\n”,(uint32_t)pointer);
64 printf(“*(pointer–)=%d, \r\n”,*(pointer–));
65 printf(“*(pointer–)=%d, \r\n”,*(pointer–));
66 printf(“*(pointer)=%d, \r\n”,*(pointer));
free(pointer);
67 }
68 else
69 {
70 printf(“\r\n使用malloc动态分配变量出错!!!\r\n”);
71 }
72 /*蓝灯亮*/
73 LED_BLUE;
74 while(1);
75 }
代码中定义了普通变量、指定到EXRAM节区的变量并使用动态分配内存,还把它们的值和地址通过串口打印到上位机,通过这些变量,我们可以检查变量是否能正常分配。
构建工程后,查看工程的map文件观察变量的分配情况,见图 1‑56。
图 1‑59 在map文件中查看工程的存储分布(SDRAM.map文件)
从map文件中可看到普通变量及栈节区都被分配到了内部SRAM的地址区域,而指定到EXRAM节区的变量及堆空间都被分配到了外部SDRAM的地址区域,与我们的要求一致。
再把程序下载到实验板进行测试,串口打印的调试信息如图 1‑60。
图 1‑60 空间分配实验实测结果
从调试信息中可发现,实际运行结果也完全正常,本实验中的sct文件配置达到了优先分配变量到内部SRAM的目的,而且堆区也能使用malloc函数正常分配空间。
本实验中的sct文件配置方案完全可以应用到您的实际工程项目中,下面再进一步强调其它应用细节。
使用malloc和free管理SDRAM的空间
SDRAM的内存空间非常大,为了管理这些空间,一些工程师会自己定义内存分配函数来管理SDRAM空间,这些分配过程本质上就是从SDRAM中动态分配内存。从本实验中可了解到我们完全可以直接使用C标准库的malloc从SDRAM中分配空间,只要在前面配置的基础上修改启动文件中的堆顶地址限制即可,见代码清单1‑30。
代码清单 1‑30 修改启动文件的堆顶地址(startup_stm32f429_439xx.s文件)
1 Heap_Size EQU 0x00000200
2
3 AREA HEAP, NOINIT, READWRITE, ALIGN=3
4
5
6 __heap_base
7 Heap_Mem SPACE Heap_Size
8 __heap_limit EQU 0xd0800000 ;设置堆空间的极限地址(SDRAM),
;0xd0000000+0x00800000
9
10 PRESERVE8
11 THUMB
C标准库的malloc函数是根据__heap_base及__heap_limit地址限制分配空间的,在以上的代码定义中,堆基地址__heap_base的由链接器自动分配未使用的基地址,而堆顶地址__heap_limit则被定义为外部SDRAM的最高地址0xD0000000+0x00800000(使用这种定义方式定义的__heap_limit值与Heap_Size定义的大小无关),经过这样配置之后,SDRAM内除EXRAM节区外的空间都被分配为堆区,所以malloc函数可以管理剩余的所有SDRAM空间。修改后,它生成的map文件信息见图1‑61。
图 1‑61 使用malloc管理剩余SDRAM空间
可看到__heap_base的地址紧跟在EXRAM之后,__heap_limit指向了SDRAM的最高地址,因此malloc函数就能利用所有SDRAM的剩余空间了。注意图中显示的HEAP节区大小为0x00000200字节,修改启动文件中的Heap_Size大小可以改变该值,它的大小是不会影响malloc函数使用的,malloc实际可用的就是__heap_base与__heap_limit之间的空间。至于如何使Heap_Size的值能自动根据__heap_limit与__heap_base的值自动生成,我还没找到方法,若您了解,请告知。
把堆区分配到内部SRAM空间。
若您希望堆区(HEAP)按照默认配置,使它还是分配到内部SRAM空间,只要把“*.o(HEAP)”选择语句从SDRAM的执行域删除掉即可,堆节区就会默认分配到内部SRAM,外部SDRAM仅选择EXRAM节区的内容进行分配,见代码清单1‑31,若您更改了启动文件中堆的默认配置,主注意空间地址的匹配。
代码清单 1‑31 按默认配置分配堆区到内部SRAM的sct文件范例
1 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
2 ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
3 *.o (RESET, +First)
4 *(InRoot$$Sections)
5 .ANY (+RO)
6 }
7 RW_IRAM1 0x20000000 0x00030000 { ; 内部SRAM
8 .ANY (+RW +ZI) ;其余的RW/ZI-data都分配到这里
9 }
10
11 RW_ERAM1 0xD0000000 0x00800000 { ; 外部SDRAM
12 .ANY (EXRAM) ;选择EXRAM节区
13 }
14 }
屏蔽链接过程的warning
在我们的实验配置的sct文件中使用了“*.o(HEAP)”语句选择堆区,但有时我们的工程完全没有使用堆(如整个工程都没有使用malloc),这时链接器会把堆占用的空间删除,构建工程后会输出警告提示该语句仅匹配到无用节区,见图1‑62。
图 1‑62 仅匹配到无用节区的warning
这并无什么大碍,但强迫症患者不希望看到warning,可以在“Options forTarget->Linker->disableWarnings”中输入warning号屏蔽它。warning号可在提示信息中找到,如上图提示信息中“warning:L6329W”表示它的warning号为6329,把它输入到图1‑63中的对话框中即可。
图 1‑63 屏蔽链接过程的warning
注意SDRAM用于显存的改变
根据本实验的sct文件配置,链接器会自动分配SDRAM的空间,而本书以前的一些章节讲解的实验使用SDRAM空间的方式非常简单粗暴,如果把这个sct文件配置直接应用到这些实验中可能会引起错误,例如我们的液晶驱动,见代码清单1‑32。
代码清单 1‑32 原液晶显示驱动使用的显存地址
1 /* LCD Size (Width and Height) */
2 #define LCD_PIXEL_WIDTH ((uint16_t)800)
3 #define LCD_PIXEL_HEIGHT ((uint16_t)480)
4
5 #define LCD_FRAME_BUFFER ((uint32_t)0xD0000000) //第一层首地址
6 #define BUFFER_OFFSET ((uint32_t)800*480*3) //一层液晶的数据量
7 #define LCD_PIXCELS ((uint32_t)800*480)
8
9 /**
10 * @brief 初始化LTD的 层 参数
11 * - 设置显存空间
12 * - 设置分辨率
13 * @param None
14 * @retval None
15 */
16 void LCD_LayerInit(void)
17 {
18 /*其它部分省略*/
19 /* 配置本层的显存首地址 */
20 LTDC_Layer_InitStruct.LTDC_CFBStartAdress =LCD_FRAME_BUFFER;
21 /*其它部分省略*/
22 }
在这段液晶驱动代码中,我们直接使用一个宏定义了SDRAM的地址,然后把它作为显存空间告诉LTDC外设(从0xD0000000地址开始的大小为800*480*3的内存空间),然而这样的内存配置链接器是无法跟踪的,链接器在自动分配变量到SDRAM时,极有可能使用这些空间,导致出错。
解决方案之一是使用__EXRAM定义一个数组空间作为显存,由链接器自动分配空间地址,最后把数组地址作为显存地址告诉LTDC外设即可,其它类似的应用都可以使用这种方案解决。
代码清单 1‑33 由链接器自动分配显存空间
1 #define BUFFER_OFFSET ((uint32_t)800*480*3) //一层液晶的数据量
2 #define LCD_PIXCELS ((uint32_t)800*480)
3
4 uint8_t LCD_FRAME_BUFFER[BUFFER_OFFSET] __EXRAM;
5
6 /**
7 * @brief 初始化LTD的 层 参数
8 * - 设置显存空间
9 * - 设置分辨率
10 * @param None
11 * @retval None
12 */
13 void LCD_LayerInit(void)
14 {
15 /*其它部分省略*/
16 /* 配置本层的显存首地址 */
17 LTDC_Layer_InitStruct.LTDC_CFBStartAdress = **&LCD_FRAME_BUFFER;**
18 /*其它部分省略*/
19 }
总而言之,当不再使用默认的sct文件配置时,一定要注意修改后会引起内存空间发生什么变化,小心这些变化导致的存储器问题。
如何把栈空间也分配到SDRAM
前面提到因为SDRAM_Init初始化函数本身使用了栈空间(STACK),而在执行SDRAM_Init函数之前SDRAM并未正常工作,这样的矛盾导致无法把栈分配到SDRAM。其实换一个思路,只要我们的SDRAM初始化过程不使用栈空间,SDRAM正常运行后栈才被分配到SDRAM空间,这样就没有问题了。
由于原来的SDRAM_Init实现的SDRAM初始化过程使用了STM32标准库函数,它不可避免地使用了栈空间(定义了局部变量),要完全不使用栈空间完成SDRAM的初始化,只能使用纯粹的寄存器方式配置。在STM32标准库的“system_stm32f4xx.c”文件已经给出了类似的解决方案,SystemInit_ExtMemCtl函数,见代码清单1‑34。
代码清单 1‑34 SystemInit_ExtMemCtl函数(system_stm32f4xx.c文件)
1 #ifdef DATA_IN_ExtSDRAM
2 /**
3 * @brief Setup the external memory controller.
4 * Called in startup_stm32f4xx.s before jump to main.
5 * This function configures the external SDRAM mounted onSTM324x9I_EVAL board
6 * This SDRAM will be used as program data memory (including heap andstack).
7 * @param None
8 * @retval None
9 */
10 void SystemInit_ExtMemCtl(void)
11 {
12 register uint32_t tmpreg = 0, timeout = 0xFFFF;
13 register uint32_t index;
14
15 /* Enable GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, GPIOH and GPIOIinterface
16 clock */
17 RCC->AHB1ENR |= 0x000001FC;
18
19 /* Connect PCx pins to FMC Alternate function */
20 GPIOC->AFR[0] = 0x0000000c;
21 GPIOC->AFR[1] = 0x00007700;
22 /* Configure PCx pins in Alternate function mode */
23 GPIOC->MODER = 0x00a00002;
24 /* Configure PCx pins speed to 50 MHz */
25 GPIOC->OSPEEDR = 0x00a00002;
26 /* Configure PCx pins Output type to push-pull */
27
28/**********************具体配置省略*************************/
29 }
30 #endif /* DATA_IN_ExtSDRAM */
该函数没有使用栈空间,仅使用register关键字定义了两个分配到内核寄存器的变量,其余配置均通过直接操作寄存器完成。这个函数针对ST的一个官方评估编写的,在其它硬件平台直接使用可能有错误,若有需要可仔细分析它的代码再根据自己的硬件平台进行修改。
这个函数是使用条件编译语句“#ifdefDATA_IN_ExtSDRAM”包装起来的,默认情况下这个函数并不会被编译,需要使用这个函数时只要定义这个宏即可。
定义了DATA_IN_ExtSDRAM宏之后,SystemInit_ExtMemCtl函数会被SystemInit函数调用,见代码清单1‑35,而我们知道SystemInit会在启动文件中的Reset_handler函数中执行。
代码清单 1‑35SystemInit函数对SystemInit_ExtMemCtl的调用(system_stm32f4xx.c文件)
1 /**
2 * @brief Setup the microcontroller system
3 * Initialize the Embedded Flash Interface, the PLL and update the
4 * SystemFrequency variable.
5 * @param None
6 * @retval None
7 */
8 void SystemInit(void)
9 {
10
11 /******部分代码省略******/
12 #if defined(DATA_IN_ExtSRAM) || defined(DATA_IN_ExtSDRAM)
13 SystemInit_ExtMemCtl();
14 #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
15 /******部分代码省略******/
16 }
所以,如果希望把栈空间分配到外部SDRAM,可按以下步骤操作:
修改sct文件,使用“*.o(STACK)”语句把栈空间分配到SDRAM的执行域;
根据自己的硬件平台,修改SystemInit_ExtMemCtl函数,该函数要实现SDRAM的初始化过程,且该函数不能使用栈空间;
定义DATA_IN_ExtSDRAM宏,从而使得SystemInit_ExtMemCtl函数被加进编译,并被SystemInit调用;
由于Reset_handler默认会调用SystemInit函数执行,所以不需要修改启动文件。
在工程中分别增加全局变量(0值及非0值)、局部变量,查看map文件,观察应用程序空间的变化。(定义的变量若不使用会被编译器优化;即使使用了变量,受编译器优化影响,空间变化也不一定完全与预计的一致,定义占用空间较大的变量观察,效果更明显,如uint32_ttestValue[50]={0};或uint32_t testValue[50]={0,1,2,3};)
查看MDK的对armcc编译器的命令输入(图1‑13),查看修改MDK配置后,命令会如何变化变化(如修改芯片类型、是否使用浮点单元、编译优化等)。
尝试修改分散加载文件,然后编译后在map文件中查看存储器索引表的变化。
参考图 1‑37,在MDK中加入fromelf指令,控制它生成工程对应的bin文件。
修改“自动分配变量到外部SDRAM”中的实验,把SDRAM的初始化过程放在main函数里(删掉启动文件中执行SDRAM_Init的两条语句),然后使用串口打印查看定义到SDRAM的变量内容是否正常。
在SRAM中调试代码¶
本章参考资料:《STM32F4xx中文参考手册》、《STM32F4xx规格书》、《Cortex-M3权威指南》、《Cortex-M4Technical ReferenceManual》(跟M3大部分是相同的,读英文不习惯可先参考《Cortex-M3权威指南》)。
学习本章时,配合《STM32F4xx中文参考手册》“存储器和总线结构”及“嵌入式FLASH接口”章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。
在RAM中调试代码¶
一般情况下,我们在MDK中编写工程应用后,调试时都是把程序下载到芯片的内部FLASH运行测试的,代码的CODE及RW-data的内容被写入到内部FLASH中存储。但在某些应用场合下却不希望或不能修改内部FLASH的内容,这时就可以使用RAM调试功能了,它的本质是把原来存储在内部FLASH的代码(CODE及RW-data的内容)改为存储到SRAM中(内部SRAM或外部SDRAM均可),芯片复位后从SRAM中加载代码并运行。
把代码下载到RAM中调试有如下优点:
下载程序非常快。RAM存储器的写入速度比在内部FLASH中要快得多,且没有擦除过程,因此在RAM上调试程序时程序几乎是秒下的,对于需要频繁改动代码的调试过程,能节约很多时间,省去了烦人的擦除与写入FLASH过程。另外,STM32的内部FLASH可擦除次数为1万次,虽然一般的调试过程都不会擦除这么多次导致FLASH失效,但这确实也是一个考虑使用RAM的因素。
不改写内部FLASH的原有程序。
对于内部FLASH被锁定的芯片,可以把解锁程序下载到RAM上,进行解锁。
相对地,把代码下载到RAM中调试有如下缺点:
存储在RAM上的程序掉电后会丢失,不能像FLASH那样保存。
若使用STM32的内部SRAM存储程序,程序的执行速度与在FLASH上执行速度无异,但SRAM空间较小。
若使用外部扩展的SDRAM存储程序,程序空间非常大,但STM32读取SDRAM的速度比读取内部FLASH慢,这会导致程序总执行时间增加,因此在SDRAM中调试的程序无法完美仿真在内部FLASH运行时的环境。另外,由于STM32无法直接从SDRAM中启动且应用程序复制到SDRAM的过程比较复杂(下载程序前需要使STM32能正常控制SDRAM),所以在很少会在STM32的SDRAM中调试程序。
STM32的启动方式¶
在前面讲解的STM32启动代码章节了解到CM-4内核在离开复位状态后的工作过程如下,见图2‑1:
从地址0x00000000处取出栈指针MSP的初始值,该值就是栈顶的地址。
从地址0x00000004处取出程序指针PC的初始值,该值指向复位后应执行的第一条指令。
图 ‑1 复位序列
上述过程由内核自动设置运行环境并执行主体程序,因此它被称为自举过程。
虽然内核是固定访问0x00000000和0x00000004地址的,但实际上这两个地址可以被重映射到其它地址空间。以STM32F429为例,根据芯片引出的BOOT0及BOOT1引脚的电平情况,这两个地址可以被映射到内部FLASH、内部SRAM以及系统存储器中,不同的映射配置见表2‑1。
表 2‑1 BOOT引脚的不同设置对0地址的映射
BOOT1 | BOOT0 | 映射到的存储器 |
0x00000000 地址映射到 |
0x00000004 地址映射到 |
---|---|---|---|---|
x | 0 | 内部FLASH | 0x08000000 | 0x08000004 |
1 | 1 | 内部SRAM | 0x20000000 | 0x20000004 |
0 | 1 | 系统存储器 | 0x1FFF0000 | 0x1FFF0004 |
内核在离开复位状态后会从映射的地址中取值给栈指针MSP及程序指针PC,然后执行指令,我们一般以存储器的类型来区分自举过程,例如内部FLASH启动方式、内部SRAM启动方式以及系统存储器启动方式。
内部FLASH启动方式
当芯片上电后采样到BOOT0引脚为低电平时,0x00000000和0x00000004地址被映射到内部FLASH的首地址0x08000000和0x08000004。因此,内核离开复位状态后,读取内部FLASH的0x08000000地址空间存储的内容,赋值给栈指针MSP,作为栈顶地址,再读取内部FLASH的0x08000004地址空间存储的内容,赋值给程序指针PC,作为将要执行的第一条指令所在的地址。具备这两个条件后,内核就可以开始从PC指向的地址中读取指令执行了。
内部SRAM启动方式
类似地,当芯片上电后采样到BOOT0和BOOT1引脚均为高电平时,0x00000000和0x00000004地址被映射到内部SRAM的首地址0x20000000和0x20000004,内核从SRAM空间获取内容进行自举。
在实际应用中,由启动文件starttup_stm32f429_439xx.s决定了0x00000000和0x00000004地址存储什么内容,链接时,由分散加载文件(sct)决定这些内容的绝对地址,即分配到内部FLASH还是内部SRAM。(下一小节将以实例讲解)
系统存储器启动方式
当芯片上电后采样到BOOT0引脚为高电平,BOOT1为低电平时,内核将从系统存储器的0x1FFF0000及0x1FFF0004获取MSP及PC值进行自举。系统存储器是一段特殊的空间,用户不能访问,ST公司在芯片出厂前就在系统存储器中固化了一段代码。因而使用系统存储器启动方式时,内核会执行该代码,该代码运行时,会为ISP提供支持(InSystemProgram),如检测USART1/3、CAN2及USB通讯接口传输过来的信息,并根据这些信息更新自己内部FLASH的内容,达到升级产品应用程序的目的,因此这种启动方式也称为ISP启动方式。
内部FLASH的启动过程¶
下面我们以最常规的内部FLASH启动方式来分析自举过程,主要理解MSP和PC内容是怎样被存储到0x08000000和0x08000004这两个地址的。
见图 2‑2,这是STM32F4默认的启动文件的代码,启动文件的开头定义了一个大小为0x400的栈空间,且栈顶的使用标号“__initial_sp”来表示;在图下方定义了一个名为“Reset_Handler”的子程序,它就是我们总是提到的在芯片启动后第一个执行的代码。在汇编语法中,程序的名字和标号都包含它所在的地址,因此,我们的目标是把“__initial_sp”和“Reset_Handler”赋值到0x08000000和0x08000004地址空间存储,这样内核自举的时候就可以获得栈顶地址以及第一条要执行的指令了。在启动代码的中间部分,使用了汇编关键字“DCD”把“__initial_sp”和“Reset_Handler”定义到了最前面的地址空间。
图 2‑2 启动代码中存储的MSP及PC指针内容
在启动文件中把设置栈顶及首条指令地址到了最前面的地址空间,但这并没有指定绝对地址,各种内容的绝对地址是由链接器根据分散加载文件(*.sct)分配的,STM32F429IGT6型号的默认分散加载文件配置见代码清单2‑1。
代码清单 2‑1 默认分散加载文件的空间配置
1 ;*************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ;*************************************************************
4
5 LR_IROM1 0x08000000 0x00100000 { ; load region size_region
6 ER_IROM1 0x08000000 0x00100000 { ; load address = executionaddress
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11 RW_IRAM1 0x20000000 UNINIT 0x00030000 { ; RW data
12 .ANY (+RW +ZI)
13 }
14 }
15
分散加载文件把加载区和执行区的首地址都设置为0x08000000,正好是内部FLASH的首地址,因此汇编文件中定义的栈顶及首条指令地址会被存储到0x08000000和0x08000004的地址空间。
类似地,如果我们修改分散加载文件,把加载区和执行区的首地址设置为内部SRAM的首地址0x20000000,那么栈顶和首条指令地址将会被存储到0x20000000和0x20000004的地址空间了。
为了进一步消除疑虑,我们可以查看反汇编代码及map文件信息来了解各个地址空间存储的内容,见图2‑3,这是多彩流水灯工程编译后的信息,它的启动文件及分散加载文件都按默认配置。其中反汇编代码是使用fromelf工具从axf文件生成的,具体过程可参考前面的章节了解。
图 2‑3 从反汇编代码及map文件查看存储器的内容
从反汇编代码可了解到,这个工程的0x08000000地址存储的值为0x20000400,0x08000004地址存储的值为0x080001C1,查看map文件,这两个值正好是栈顶地址__initial_sp以及首条指令Reset_Handler的地址。下载器会根据axf文件(bin、hex类似)存储相应的内容到内部FLASH中。
由此可知,BOOT0为低电平时,内核复位后,从0x08000000读取到栈顶地址为0x20000400,了解到子程序的栈空间范围,再从0x08000004读取到第一条指令的存储地址为0x080001C1,于是跳转到该地址执行代码,即从ResetHandler开始运行,运行SystemInit、__main(包含分散加载代码),最后跳转到C语言的main函数。
对比在内部FLASH中运行代码的过程,可了解到若希望在内部SRAM中调试代码,需要设置启动方式为从内部SRAM启动,修改分散加载文件控制代码空间到内部SRAM地址以及把生成程序下载到芯片的内部SRAM中。
实验:在内部SRAM中调试代码¶
本实验将演示如何设置工程选项实现在内部SRAM中调试代码,实验的示例代码名为“RAM调试—多彩流水灯”,学习以下内容时请打开该工程来理解,它是从普通的多彩流水灯例程改造而来的。
硬件设计¶
本小节中使用到的流水灯硬件不再介绍,主要讲解与SRAM调试相关的硬件配置。在SRAM上调试程序,需要修改STM32芯片的启动方式,见图2‑4。
图 2‑4 实验板的boot引脚配置
在我们的实验板左侧有引出STM32芯片的BOOT0和BOOT1引脚,可使用跳线帽设置它们的电平从而控制芯片的启动方式,它支持从内部FLASH启动、系统存储器启动以及内部SRAM启动方式。
本实验在SRAM中调试代码,因此把BOOT0和BOOT1引脚都使用跳线帽连接到3.3V,使芯片从SRAM中启动。
软件设计¶
本实验的工程从普通的多彩流水灯工程改写而来,主要修改了分散加载文件及一些程序的下载选项。
主要步骤¶
在原工程的基础上创建一个调试版本;
修改分散加载文件,使链接器把代码分配到内部SRAM空间;
添加宏修改STM32的向量表地址;
修改仿真器和下载器的配置,使程序能通过下载器存储到内部SRAM;
根据使用情况选择是否需要使用仿真器命令脚本文件*.ini;
尝试给SRAM下载程序或仿真调试。
创建工程的调试版本¶
由于在SRAM中运行的代码一般只是用于调试,调试完毕后,在实际生产环境中仍然使用在内部FLASH中运行的代码,因此我们希望能够便捷地在调试版和发布版代码之间切换。MDK的“ManageProject Items”可实现这样的功能,使用它可管理多个不同配置的工程,见图2‑5,点击“Manage Project Items”按钮,在弹出对话框左侧的“ProjectTarget”一栏包含了原工程的名字,如图中的原工程名为“多彩流水灯”,右侧是该工程包含的文件。为了便于调试,我们在左侧的“ProjectTarget”一栏添加一个工程名,如图中输入“SRAM_调试”,输入后点击OK即可,这个“SRAM_调试”版本的工程会复制原“多彩流水灯”工程的配置,后面我们再进行修改。
图 2‑5 使用Manage Project Items添加一个工程配置
当需要切换工程版本时,点击MDK工程名的下拉菜单可选择目标工程,在不同的工程中,所有配置都是独立的,例如芯片型号、下载配置等等,但如果两个工程共用了同一个文件,对该文件的修改会同时影响两个工程,例如这两个工程都使用同一个main文件,我们在main文件修改代码,两个工程都会被修改。
图 2‑6 切换工程
在下面的教程中我们将切换到“SRAM_调试”版本的工程,配置出一个代码会被存储到SRAM的多彩流水灯工程。
配置分散加载文件¶
为方便讲解,本工程的分散加载只使用手动编辑的sct文件配置,不使用MDK的对话框选项配置,在“Optionsfor Target->linker”的选项见图 2‑7。
图 2‑7 使用新建的“SRAM_调试.sct”文件
为了防止“多彩流水灯”工程的分散加载文件被影响,我们在工程的Output路径下新建了一个名为“SRAM_调试.sct”的文件,并在上图中把它配置为“SRAM_调试”工程专用的分散加载文件,该文件的内容见代码清单2‑2,若不了解分散加载文件的使用,请参考前面的章节。
代码清单 2‑2 分散加载文件配置(SRAM_调试.sct)
1 ;*************************************************************
2 ; *** Scatter-Loading Description File generated by uVision ***
3 ;*************************************************************
4
5 LR_IROM1 0x20000000 0x00010000 { ; load region size_region
6 ER_IROM1 0x20000000 0x00010000 { ; load address = executionaddress
7 *.o (RESET, +First)
8 *(InRoot$$Sections)
9 .ANY (+RO)
10 }
11 RW_IRAM1 0x20010000 0x00020000 { ; RW data
12 .ANY (+RW +ZI)
13 }
14 }
15
在这个分散加载文件配置中,把原本分配到内部FLASH空间的加载域和执行域改到了以地址0x20000000开始的64KB(0x00010000)空间,而RWdata空间改到了以地址0x20010000开始的128KB空间(0x00020000)。也就是说,它把STM32的内部SRAM分成了虚拟ROM区域以及RWdata数据区域,链接器会根据它的配置给工程中的各种内容分配到SRAM地址。
在具体的应用中,虚拟ROM及RW区域的大小可根据自己的程序定制,配置完毕编译工程后可在map文件中查看具体的空间地址分配。
配置中断向量表¶
由于startup_stm32f429_439xx.s文件中的启动代码不是指定到绝对地址的,经过它由链接器决定应存储到内部FLASH还是SRAM,所以SRAM版本工程中的启动文件不需要作任何修改。
重点在于启动文件定义的中断向量表被存储到内部FLASH和内部SRAM时,这两种情况对内核的影响是不同的,内核会根据它的“向量表偏移寄存器VTOR”配置来获取向量表,即中断服务函数的入口。VTOR寄存器是由启动文件中Reset_Handle中调用的库函数SystemInit配置的,见代码清单2‑3。
代码清单 2‑3 SystemInit函数(system_stm32f4xx.c文件)
1 /**
2 * @brief Setup the microcontroller system
3 * Initialize the Embedded Flash Interface, the PLL and update the
4 * SystemFrequency variable.
5 * @param None
6 * @retval None
7 */
8 void SystemInit(void)
9 {
10 /* ..其它代码部分省略 */
11
12 /* Configure the Vector Table location add offset address —-*/
13 #ifdef VECT_TAB_SRAM
14 SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* 向量表存储在SRAM */
15 #else
16 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /*向量表存储在内部FLASH */
17 #endif
18 }
代码中根据是否存储宏定义VECT_TAB_SRAM来决定VTOR的配置,默认情况下代码中没有定义宏VECT_TAB_SRAM,所以VTOR默认情况下指示向量表是存储在内部FLASH空间的。
由于本工程的分散加载文件配置,在启动文件中定义的中断向量表会被分配到SRAM空间,所以我们要定义这个宏,使得SystemInit函数修改VTOR寄存器,向内核指示向量表被存储到内部SRAM空间了,见图2‑8,在“Options for Target-> c/c++->Define”框中输入宏VECT_TAB_SRAM,注意它与其它宏之间要使用英文逗号分隔开。
图 2‑8 在c/c++编译选项中加入宏VECT_TAB_SRAM
配置完成后重新编译工程,即可生成存储到SRAM空间地址的代码指令。
修改FLASH下载配置¶
得到SRAM版本的代码指令后,为了把它下载到芯片的SRAM中,还需要修改下载器的配置,见图2‑9,“Options for Target->Utilities->Settings”中的选项。
图 2‑9 下载配置
这个配置对话框原本是用于设置芯片内部FLASH信息的,当我们点击MDK的
(下载、LOAD)按钮时,它会从此处加载配置然后下载程序到FLASH中,而在上图中我们把它的配置修改成下载到内部SRAM了,各个配置的解释如下:
把“Download Function”中的擦除选项配置为“Do notErase”。这是因为数据写入到内部SRAM中不需要像FLASH那样先擦除后写入。在本工程中,如果我们不选择“Donot Erase”的话,会因为擦除过程导致下载出错。
“RAM for Algorithm”一栏是指“编程算法”(ProgrammingAlgorithm)可使用的RAM空间,下载程序到FLASH时运行的编程算法需要使用RAM空间,在默认配置中它的首地址为0x20000000,即内部SRAM的首地址,但由于我们的分散加载文件配置,0x20000000地址开始的64KB实际为虚拟ROM空间,实际的RAM空间是从地址0x20010000开始的,所以这里把算法RAM首地址更改为本工程中实际作为RAM使用的地址。若编程算法使用的RAM地址与虚拟ROM空间地址重合的话,会导致下载出错。
“ProgrammingAlgorithm”一栏中是设置内部FLASH的编程算法,编程算法主要描述了FLASH的地址、大小以及扇区等信息,MDK根据这些信息把程序下载到芯片的FLASH中,不同的控制器芯片一般会有不同的编程算法。由于MDK没有内置SRAM的编程算法,所以我们直接在原来的基础上修改它的基地址和空间大小,把它改成虚拟ROM的空间信息。
从这个例子可了解到,这里的配置是跟我们的分散加载文件的实际RAM空间和虚拟ROM空间信息是一致的,若您的分散加载文件采用不同的配置,这个下载选项也要作出相应的修改,不能照抄本例子的空间信息。
这个配置是针对程序下载的,配置完成后点击MDK的
按钮(下载、LOAD),程序会被下载到STM32的内部SRAM中,复位后程序会正常运行(前提是BOOT0和BOOT要被设置为SRAM启动)。芯片掉电后这个存储在SRAM的程序会丢失,想恢复的话必须要重新下载程序。
仿真器的配置¶
上面的下载配置使得程序能够加载到SRAM中全速运行,但作为SRAM版本的程序,其功能更着重于调试,也就是说我们希望它能支持平时使用
按钮(调试、debug)时进行的硬件在线调试、单步运行等功能。
要实现调试功能,还要在“Options forTarget->Debug->Settings”中进行配置,见图 2‑10。
图 2‑10 设置仿真前检查代码并下载程序到FLASH中
在图中我们需要勾选“Verify Code Download”及“Download toFLASH”选项,也就是说点击调试按钮后,本工程的程序会被下载到内部SRAM中,只有勾选了这两个选项才能正常仿真。(至于为什么FLASH版本的程序不需要勾选,不太清楚)
经过这样的配置后,硬件仿真时与平时内部FLASH版本的程序无异,支持软件复位、单步运行、全速运行以及查看各种变量值等(同样地,前提是BOOT0和BOOT要被设置为SRAM启动) 。
不需要修改BOOT引脚的仿真配置¶
假如您使用的硬件平台中BOOT0和BOOT1引脚电平已被固定,设置为内部FLASH启动,不方便改成SRAM方式,可以使用如下方法配置调试选项实现在SRAM调试:
与上述步骤一样,勾选“Verify Code Download”及“Download toFLASH”选项;
见图 2‑11,在“Options for Target->Debug”对话框中取消勾选“LoadApplication at startup”选项。点击“InitializationFile”文本框右侧的文件浏览按钮,在弹出的对话框中新建一个名为“Debug_RAM.ini”的文件;
图 2‑11 新建一个ini文件
在Debug_RAM.ini文件中输入如代码清单 2‑4中的内容。
代码清单 2‑4 Debug_RAM.ini文件内容
1/***********************************************************/
2 /* Debug_RAM.ini: Initialization File for Debugging from InternalRAM */
3/******************************************************/
4 /* This file is part of the uVision/ARM development tools. */
5 /* Copyright (c) 2005-2014 Keil Software. All rights reserved. */
6 /* This software may only be used under the terms of a valid,current, */
7 /* end user licence from KEIL for a compatible version of KEILsoftware */
8 /*development tools. Nothing else gives you the right to use thissoftware */
9/***************************************************/
10
11 FUNC void Setup (void) {
12 SP = _RDWORD(0x20000000); //设置栈指针SP,把0x20000000地址中的内容赋值到SP。
13 PC = _RDWORD(0x20000004); //设置程序指针PC,把0x20000004地址中的内容赋值到PC。
14 XPSR = 0x01000000; // 设置状态寄存器指针xPSR
15 _WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table OffsetRegister
16 }
17
18 LOAD %L INCREMENTAL // 下载axf文件到RAM
19 Setup(); //调用上面定义的setup函数设置运行环境
20
21 //g, main//跳转到main函数,本示例调试时不需要从main函数执行,注释掉了,程序从启动代码开始执行
上述配置过程是控制MDK执行仿真器的脚本文件Debug_RAM.ini,而该脚本文件在下载了程序到SRAM后,初始化了SP指针(即MSP)和PC指针分别指向了0x20000000和0x20000004,这样的操作等效于从SRAM复位。
有了这样的配置,即使BOOT0和BOOT1引脚不设置为SRAM启动也能正常仿真了,但点击下载按钮把程序下载到SRAM然后按复位是不能全速运行的(这种运行方式脱离了仿真器的控制,SP和PC指针无法被初始化指向SRAM)。
上述Debug_RAM.ini文件是从STM32F4的MDK芯片包里复制过来的,若您感兴趣可到MDK安装目录搜索该文件名,该文件的语法可以从MDK的帮助手册的“µVisionUser’s Guide->Debug Commands”章节学习。
每课一问
在内部FLASH运行的程序与在SRAM运行的程序主要差异有哪些?
读写内部FLASH¶
本章参考资料:《STM32F4xx参考手册》、《STM32F4xx规格书》、库说明文档《stm32f4xx_dsp_stdperiph_lib_um.chm》。
STM32的内部FLASH简介¶
在STM32芯片内部有一个FLASH存储器,它主要用于存储代码,我们在电脑上编写好应用程序后,使用下载器把编译后的代码文件烧录到该内部FLASH中,由于FLASH存储器的内容在掉电后不会丢失,芯片重新上电复位后,内核可从内部FLASH中加载代码并运行,见图3‑1。
图 3‑1 STM32的内部框架图
除了使用外部的工具(如下载器)读写内部FLASH外,STM32芯片在运行的时候,也能对自身的内部FLASH进行读写,因此,若内部FLASH存储了应用程序后还有剩余的空间,我们可以把它像外部SPI-FLASH那样利用起来,存储一些程序运行时产生的需要掉电保存的数据。
由于访问内部FLASH的速度要比外部的SPI-FLASH快得多,所以在紧急状态下常常会使用内部FLASH存储关键记录;为了防止应用程序被抄袭,有的应用会禁止读写内部FLASH中的内容,或者在第一次运行时计算加密信息并记录到某些区域,然后删除自身的部分加密代码,这些应用都涉及到内部FLASH的操作。
STM32的内部FLASH包含主存储器、系统存储器、OTP区域以及选项字节区域,它们的地址分布及大小见表3‑1。
表 3‑1 STM32内部FLASH的构成
各个存储区域的说明如下:
主存储器
一般我们说STM32内部FLASH的时候,都是指这个主存储器区域,它是存储用户应用程序的空间,芯片型号说明中的1MFLASH、2MFLASH都是指这个区域的大小。主存储器分为两块,共2MB,每块内分12个扇区,其中包含4个16KB扇区、1个64KB扇区和7个128KB的扇区。如我们实验板中使用的STM32F429IGT6型号芯片,它的主存储区域大小为1MB,所以它只包含有表中的扇区0-扇区11。
与其它FLASH一样,在写入数据前,要先按扇区擦除,而有的时候我们希望能以小规格操纵存储单元,所以STM32针对1MBFLASH的产品还提供了一种双块的存储格式,见表 3‑2。(2M的产品按表3‑1的格式)
表 3‑2 1MB产品的双块存储格式
通过配置FLASH选项控制寄存器FLASH_OPTCR的DB1M位,可以切换这两种格式,切换成双块模式后,扇区8-11的空间被转移到扇区12-19中,扇区细分了,总容量不变。
注意如果您使用的是STM32F40x系列的芯片,它没有双块存储格式,也不存在扇区12-23,仅STM32F42x/43x系列产品才支持扇区12-23。
系统存储区
系统存储区是用户不能访问的区域,它在芯片出厂时已经固化了启动代码,它负责实现串口、USB以及CAN等ISP烧录功能。
OTP区域
OTP(One TimeProgram),指的是只能写入一次的存储区域,容量为512字节,写入后数据就无法再更改,OTP常用于存储应用程序的加密密钥。
选项字节
选项字节用于配置FLASH的读写保护、电源管理中的BOR级别、软件/硬件看门狗等功能,这部分共32字节。可以通过修改FLASH的选项控制寄存器修改。
对内部FLASH的写入过程¶
由于内部FLASH空间主要存储的是应用程序,是非常关键的数据,为了防止误操作修改了这些内容,芯片复位后默认会结FLASH上锁,这个时候不允许设置FLASH的控制寄存器,并且不能对修改FLASH中的内容。
所以对FLASH写入数据前,需要先给它解锁。解锁的操作步骤如下:
往Flash 密钥寄存器 FLASH_KEYR中写入 KEY1 = 0x45670123
再往Flash 密钥寄存器 FLASH_KEYR中写入 KEY2 = 0xCDEF89AB
在内部FLASH进行擦除及写入操作时,电源电压会影响数据的最大操作位数,该电源电压可通过配置FLASH_CR寄存器中的 PSIZE位改变,见表 3‑3。
表 3‑3 数据操作位数
电压范围 |
2.7 - 3.6 V (使用外部Vpp) |
2.7 - 3.6 V | 2.1 – 2.7 V | 1.8 – 2.1 V |
---|---|---|---|---|
位数 | 64 | 32 | 16 | 8 |
PSIZE(1:0)配置 | 11b | 10b | 01b | 00b |
最大操作位数会影响擦除和写入的速度,其中64位宽度的操作除了配置寄存器位外,还需要在Vpp引脚外加一个8-9V的电压源,且其供电时间不得超过一小时,否则FLASH可能损坏,所以64位宽度的操作一般是在量产时对FLASH写入应用程序时才使用,大部分应用场合都是用32位的宽度。
在写入新的数据前,需要先擦除存储区域,STM32提供了扇区擦除指令和整个FLASH擦除(批量擦除)的指令,批量擦除指令仅针对主存储区。
扇区擦除的过程如下:
检查 FLASH_SR 寄存器中的“忙碌寄存器位 BSY”,以确认当前未执行任何Flash 操作;
在 FLASH_CR 寄存器中,将“激活扇区擦除寄存器位SER ”置1,并设置“扇区编号寄存器位SNB”,选择要擦除的扇区;
将 FLASH_CR 寄存器中的“开始擦除寄存器位 STRT ”置 1,开始擦除;
等待 BSY 位被清零时,表示擦除完成。
擦除完毕后即可写入数据,写入数据的过程并不是仅仅使用指针向地址赋值,赋值前还还需要配置一系列的寄存器,步骤如下:
检查 FLASH_SR 中的 BSY 位,以确认当前未执行任何其它的内部 Flash操作;
将 FLASH_CR 寄存器中的 “激活编程寄存器位PG” 置 1;
针对所需存储器地址(主存储器块或 OTP 区域内)执行数据写入操作;
等待 BSY 位被清零时,表示写入完成。
查看工程的空间分布¶
由于内部FLASH本身存储有程序数据,若不是有意删除某段程序代码,一般不应修改程序空间的内容,所以在使用内部FLASH存储其它数据前需要了解哪一些空间已经写入了程序代码,存储了程序代码的扇区都不应作任何修改。通过查询应用程序编译时产生的“*.map”后缀文件,可以了解程序存储到了哪些区域,它在工程中的打开方式见图3‑2,也可以到工程目录中的“Listing”文件夹中找到。
图 3‑2 打开工程的.map文件
打开map文件后,查看文件最后部分的区域,可以看到一段以“Memory Map of theimage”开头的记录(若找不到可用查找功能定位),见代码清单 3‑1。
代码清单 3‑1 map文件中的存储映像分布说明
1¶
2 Memory Map of the image //存储分布映像
3
4 Image Entry point : 0x080001ad
5
6 /*程序ROM加载空间*/
7 Load Region LR_IROM1 (Base: 0x08000000, Size: 0x00000b50, Max:0x00100000, ABSOLUTE)
8
9 /*程序ROM执行空间*/
10 Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00000b3c,Max: 0x00100000, ABSOLUTE)
11
12 /*地址分布列表*/
13 Base Addr Size Type Attr Idx E Section Name Object
14
15 0x08000000 0x000001ac Data RO 3 RESET startup_stm32f429_439xx.o
16 0x080001ac 0x00000000 Code RO 5359 * .ARM.Collect$$$$00000000mc_w.l(entry.o)
17 0x080001ac 0x00000004 Code RO 5622 .ARM.Collect$$$$00000001mc_w.l(entry2.o)
18 0x080001b0 0x00000004 Code RO 5625 .ARM.Collect$$$$00000004mc_w.l(entry5.o)
19 0x080001b4 0x00000000 Code RO 5627 .ARM.Collect$$$$00000008mc_w.l(entry7b.o)
20 0x080001b4 0x00000000 Code RO 5629 .ARM.Collect$$$$0000000Amc_w.l(entry8b.o)
21 /*...此处省略大部分内容*/
22 0x08000948 0x0000000e Code RO 4910 i.USART_GetFlagStatusstm32f4xx_usart.o
23 0x08000956 0x00000002 PAD
24 0x08000958 0x000000bc Code RO 4914 i.USART_Init stm32f4xx_usart.o
25 0x08000a14 0x00000008 Code RO 4924 i.USART_SendDatastm32f4xx_usart.o
26 0x08000a1c 0x00000002 Code RO 5206 i.UsageFault_Handlerstm32f4xx_it.o
27 0x08000a1e 0x00000002 PAD
28 0x08000a20 0x00000010 Code RO 5363 i.__0printf$baremc_w.l(printfb.o)
29 0x08000a30 0x0000000e Code RO 5664 i.__scatterload_copymc_w.l(handlers.o)
30 0x08000a3e 0x00000002 Code RO 5665 i.__scatterload_nullmc_w.l(handlers.o)
31 0x08000a40 0x0000000e Code RO 5666 i.__scatterload_zeroinitmc_w.l(handlers.o)
32 0x08000a4e 0x00000022 Code RO 5370 i._printf_coremc_w.l(printfb.o)
33 0x08000a70 0x00000024 Code RO 5275 i.fputc bsp_debug_usart.o
34 0x08000a94 0x00000088 Code RO 5161 i.main main.o
35 0x08000b1c 0x00000020 Data RO 5662 Region$$Table anon$$obj.o
36
这一段是某工程的ROM存储器分布映像,在STM32芯片中,ROM区域的内容就是指存储到内部FLASH的代码。
上述说明中有两段分别以“Load Region LR_ROM1”及“Execution RegionER_IROM1”开头的内容,它们分别描述程序的加载及执行空间。在芯片刚上电运行时,会加载程序及数据,例如它会从程序的存储区域加载到程序的执行区域,还把一些已初始化的全局变量从ROM复制到RAM空间,以便程序运行时可以修改变量的内容。加载完成后,程序开始从执行区域开始执行。
在上面map文件的描述中,我们了解到加载及执行空间的基地址(Base)都是0x08000000,它正好是STM32内部FLASH的首地址,即STM32的程序存储空间就直接是执行空间;它们的大小(Size)分别为0x00000b50及0x00000b3c,执行空间的ROM比较小的原因就是因为部分RW-data类型的变量被拷贝到RAM空间了;它们的最大空间(Max)均为0x00100000,即1M字节,它指的是内部FLASH的最大空间。
计算程序占用的空间时,需要使用加载区域的大小进行计算,本例子中应用程序使用的内部FLASH是从0x08000000至(0x08000000+0x00000b50)地址的空间区域。
在加载及执行空间总体描述之后,紧接着一个ROM详细地址分布表,它列出了工程中的各个段(如函数、常量数据)所在的地址BaseAddr及占用的空间Size,列表中的Type说明了该段的类型,CODE表示代码,DATA表示数据,而PAD表示段之间的填充区域,它是无效的内容,PAD区域往往是为了解决地址对齐的问题。
观察表中的最后一项,它的基地址是0x08000b1c,大小为0x00000020,可知它占用的最高的地址空间为0x08000b3c,跟执行区域的最高地址0x00000b3c一样,但它们比加载区域说明中的最高地址0x8000b50要小,所以我们以加载区域的大小为准。对比表3‑1的内部FLASH扇区地址分布表,可知仅使用扇区0就可以完全存储本应用程序,所以从扇区1(地址0x08004000)后的存储空间都可以作其它用途,使用这些存储空间时不会篡改应用程序空间的数据。
为简化编程,STM32标准库提供了一些库函数,它们封装了对内部FLASH写入数据操作寄存器的过程。
对内部FLASH解锁、上锁的函数见代码清单 3‑2。
代码清单 3‑2 FLASH解锁、上锁
1
2 #define FLASH_KEY1 ((uint32_t)0x45670123)
3 #define FLASH_KEY2 ((uint32_t)0xCDEF89AB)
4 /**
5 * @brief Unlocks the FLASH control register access
6 * @param None
7 * @retval None
8 */
9 void FLASH_Unlock(void)
10 {
11 if ((FLASH->CR & FLASH_CR_LOCK) != RESET) {
12 /* Authorize the FLASH Registers access */
13 FLASH->KEYR = FLASH_KEY1;
14 FLASH->KEYR = FLASH_KEY2;
15 }
16 }
17
18 /**
19 * @brief Locks the FLASH control register access
20 * @param None
21 * @retval None
22 */
23 void FLASH_Lock(void)
24 {
25 /* Set the LOCK Bit to lock the FLASH Registers access */
26 FLASH->CR |= FLASH_CR_LOCK;
27 }
解锁的时候,它对FLASH_KEYR寄存器写入两个解锁参数,上锁的时候,对FLASH_CR寄存器的FLASH_CR_LOCK位置1。
解锁后擦除扇区时可调用FLASH_EraseSector完成,见代码清单 3‑3。
代码清单 3‑3 擦除扇区
1 /**
2 * @brief Erases a specified FLASH Sector.
3 *
4 * @note If an erase and a program operations are requestedsimultaneously,
5 * the erase operation is performed before the program one.
6 *
7 * @param FLASH_Sector: The Sector number to be erased.
8 *
9 * @note For STM32F42xxx/43xxx devices this parameter can be a valuebetween
10 * FLASH_Sector_0 and FLASH_Sector_23.
11 *
12 * @param VoltageRange: The device voltage range which defines theerase parallelism.
13 * This parameter can be one of the following values:
14 * @arg VoltageRange_1: when the device voltage range is 1.8V to2.1V,
15 * the operation will be done by byte (8-bit)
16 * @arg VoltageRange_2: when the device voltage range is 2.1V to2.7V,
17 * the operation will be done by half word (16-bit)
18 * @arg VoltageRange_3: when the device voltage range is 2.7V to3.6V,
19 * the operation will be done by word (32-bit)
20 * @arg VoltageRange_4: when the device voltage range is 2.7V to3.6V + External Vpp,
21 * the operation will be done by double word (64-bit)
22 *
23 * @retval FLASH Status: The returned value can be: FLASH_BUSY,FLASH_ERROR_PROGRAM,
24 * FLASH_ERROR_WRP, FLASH_ERROR_OPERATION or FLASH_COMPLETE.
25 */
26 FLASH_Status FLASH_EraseSector(uint32_t FLASH_Sector, uint8_tVoltageRange)
27 {
28 uint32_t tmp_psize = 0x0;
29 FLASH_Status status = FLASH_COMPLETE;
30
31 /* Check the parameters */
32 assert_param(IS_FLASH_SECTOR(FLASH_Sector));
33 assert_param(IS_VOLTAGERANGE(VoltageRange));
34
35 if (VoltageRange == VoltageRange_1) {
36 tmp_psize = FLASH_PSIZE_BYTE;
37 } else if (VoltageRange == VoltageRange_2) {
38 tmp_psize = FLASH_PSIZE_HALF_WORD;
39 } else if (VoltageRange == VoltageRange_3) {
40 tmp_psize = FLASH_PSIZE_WORD;
41 } else {
42 tmp_psize = FLASH_PSIZE_DOUBLE_WORD;
43 }
44 /* Wait for last operation to be completed */
45 status = FLASH_WaitForLastOperation();
46
47 if (status == FLASH_COMPLETE) {
48 /* if the previous operation is completed, proceed to erase thesector */
49 FLASH->CR &= CR_PSIZE_MASK;
50 FLASH->CR |= tmp_psize;
51 FLASH->CR &= SECTOR_MASK;
52 FLASH->CR |= FLASH_CR_SER | FLASH_Sector;
53 FLASH->CR |= FLASH_CR_STRT;
54
55 /* Wait for last operation to be completed */
56 status = FLASH_WaitForLastOperation();
57
58 /* if the erase operation is completed, disable the SER Bit */
59 FLASH->CR &= (~FLASH_CR_SER);
60 FLASH->CR &= SECTOR_MASK;
61 }
62 /* Return the Erase Status */
63 return status;
64 }
本函数包含两个输入参数,分别是要擦除的扇区号和工作电压范围,选择不同电压时实质是选择不同的数据操作位数,参数中可输入的宏在注释里已经给出。函数根据输入参数配置PSIZE位,然后擦除扇区,擦除扇区的时候需要等待一段时间,它使用FLASH_WaitForLastOperation等待,擦除完成的时候才会退出FLASH_EraseSector函数。
对内部FLASH写入数据不像对SDRAM操作那样直接指针操作就完成了,还要设置一系列的寄存器,利用FLASH_ProgramWord、FLASH_ProgramHalfWord和FLASH_ProgramByte函数可按字、半字及字节单位写入数据,见代码清单3‑4。
代码清单 3‑4 写入数据
1
2 /**
3 * @brief Programs a word (32-bit) at a specified address.
4 *
5 * @note This function must be used when the device voltage range isfrom 2.7V to 3.6V.
6 *
7 * @note If an erase and a program operations are requestedsimultaneously,
8 * the erase operation is performed before the program one.
9 *
10 * @param Address: specifies the address to be programmed.
11 * This parameter can be any address in Program memory zone or in OTPzone.
12 * @param Data: specifies the data to be programmed.
13 * @retval FLASH Status: The returned value can be: FLASH_BUSY,FLASH_ERROR_PROGRAM,
14 * FLASH_ERROR_WRP, FLASH_ERROR_OPERATION or FLASH_COMPLETE.
15 */
16 FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)
17 {
18 FLASH_Status status = FLASH_COMPLETE;
19
20 /* Check the parameters */
21 assert_param(IS_FLASH_ADDRESS(Address));
22
23 /* Wait for last operation to be completed */
24 status = FLASH_WaitForLastOperation();
25
26 if (status == FLASH_COMPLETE) {
27/* if the previous operation is completed, proceed to program the newdata */
28 FLASH->CR &= CR_PSIZE_MASK;
29 FLASH->CR |= FLASH_PSIZE_WORD;
30 FLASH->CR |= FLASH_CR_PG;
31
32 *(__IO uint32_t*)Address = Data;
33
34 /* Wait for last operation to be completed */
35 status = FLASH_WaitForLastOperation();
36
37 /* if the program operation is completed, disable the PG Bit */
38 FLASH->CR &= (~FLASH_CR_PG);
39 }
40 /* Return the Program Status */
41 return status;
42 }
看函数代码可了解到,使用指针进行赋值操作前设置了数据操作宽度,并设置了PG寄存器位,在赋值操作后,调用了FLASH_WaitForLastOperation函数等待写操作完毕。HalfWord和Byte操作宽度的函数执行过程类似。
在本小节中我们以实例讲解如何使用内部FLASH存储数据。
本实验仅操作了STM32芯片内部的FLASH空间,无需额外的硬件。
本小节讲解的是“内部FLASH编程”实验,请打开配套的代码工程阅读理解。为了方便展示及移植,我们把操作内部FLASH相关的代码都编写到“bsp_internalFlash.c”及“bsp_internalFlash.h”文件中,这些文件是我们自己编写的,不属于标准库的内容,可根据您的喜好命名文件。
对内部FLASH解锁;
找出空闲扇区,擦除目标扇区;
进行读写测试。
硬件定义
读写内部FLASH不需要用到任何外部硬件,不过在擦写时常常需要知道各个扇区的基地址,我们把这些基地址定义到bsp_internalFlash.h文件中,见代码清单3‑5。
代码清单 3‑5 各个扇区的基地址(bsp_internalFlash.h文件)
1
2 /* 各个扇区的基地址 */
3 #define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000)
4 #define ADDR_FLASH_SECTOR_1 ((uint32_t)0x08004000)
5 #define ADDR_FLASH_SECTOR_2 ((uint32_t)0x08008000)
6 #define ADDR_FLASH_SECTOR_3 ((uint32_t)0x0800C000)
7 #define ADDR_FLASH_SECTOR_4 ((uint32_t)0x08010000)
8 #define ADDR_FLASH_SECTOR_5 ((uint32_t)0x08020000)
9 #define ADDR_FLASH_SECTOR_6 ((uint32_t)0x08040000)
10 #define ADDR_FLASH_SECTOR_7 ((uint32_t)0x08060000)
11 #define ADDR_FLASH_SECTOR_8 ((uint32_t)0x08080000)
12 #define ADDR_FLASH_SECTOR_9 ((uint32_t)0x080A0000)
13 #define ADDR_FLASH_SECTOR_10 ((uint32_t)0x080C0000)
14 #define ADDR_FLASH_SECTOR_11 ((uint32_t)0x080E0000)
15
16 #define ADDR_FLASH_SECTOR_12 ((uint32_t)0x08100000)
17 #define ADDR_FLASH_SECTOR_13 ((uint32_t)0x08104000)
18 #define ADDR_FLASH_SECTOR_14 ((uint32_t)0x08108000)
19 #define ADDR_FLASH_SECTOR_15 ((uint32_t)0x0810C000)
20 #define ADDR_FLASH_SECTOR_16 ((uint32_t)0x08110000)
21 #define ADDR_FLASH_SECTOR_17 ((uint32_t)0x08120000)
22 #define ADDR_FLASH_SECTOR_18 ((uint32_t)0x08140000)
23 #define ADDR_FLASH_SECTOR_19 ((uint32_t)0x08160000)
24 #define ADDR_FLASH_SECTOR_20 ((uint32_t)0x08180000)
25 #define ADDR_FLASH_SECTOR_21 ((uint32_t)0x081A0000)
26 #define ADDR_FLASH_SECTOR_22 ((uint32_t)0x081C0000)
27 #define ADDR_FLASH_SECTOR_23 ((uint32_t)0x081E0000)
这些宏跟表 3‑1中的地址说明一致。
根据扇区地址计算SNB寄存器的值
在擦除操作时,需要向FLASH控制寄存器FLASH_CR的SNB位写入要擦除的扇区号,固件库把各个扇区对应的寄存器值使用宏定义到了stm32f4xx_flash.h文件。为了便于使用,我们自定义了一个GetSector函数,根据输入的内部FLASH地址,找出其所在的扇区,并返回该扇区对应的SNB位寄存器值,见代码清单3‑6。
代码清单 3‑6写入到SNB寄存器位的值(stm32f4xx_flash.h及bsp_internalFlash.c文件)
1/*固件库定义的用于扇区写入到SNB寄存器位的宏(stm32f4xx_flash.h文件)*/
2 #define FLASH_Sector_0 ((uint16_t)0x0000)
3 #define FLASH_Sector_1 ((uint16_t)0x0008)
4 #define FLASH_Sector_2 ((uint16_t)0x0010)
5 #define FLASH_Sector_3 ((uint16_t)0x0018)
6 #define FLASH_Sector_4 ((uint16_t)0x0020)
7 #define FLASH_Sector_5 ((uint16_t)0x0028)
8 #define FLASH_Sector_6 ((uint16_t)0x0030)
9 #define FLASH_Sector_7 ((uint16_t)0x0038)
10 #define FLASH_Sector_8 ((uint16_t)0x0040)
11 #define FLASH_Sector_9 ((uint16_t)0x0048)
12 #define FLASH_Sector_10 ((uint16_t)0x0050)
13 #define FLASH_Sector_11 ((uint16_t)0x0058)
14 #define FLASH_Sector_12 ((uint16_t)0x0080)
15 #define FLASH_Sector_13 ((uint16_t)0x0088)
16 #define FLASH_Sector_14 ((uint16_t)0x0090)
17 #define FLASH_Sector_15 ((uint16_t)0x0098)
18 #define FLASH_Sector_16 ((uint16_t)0x00A0)
19 #define FLASH_Sector_17 ((uint16_t)0x00A8)
20 #define FLASH_Sector_18 ((uint16_t)0x00B0)
21 #define FLASH_Sector_19 ((uint16_t)0x00B8)
22 #define FLASH_Sector_20 ((uint16_t)0x00C0)
23 #define FLASH_Sector_21 ((uint16_t)0x00C8)
24 #define FLASH_Sector_22 ((uint16_t)0x00D0)
25 #define FLASH_Sector_23 ((uint16_t)0x00D8)
26
27 /*定义在bsp_internalFlash.c文件中的函数*/
28 /**
29 * @brief 根据输入的地址给出它所在的sector
30 * 例如:
31 uwStartSector = GetSector(FLASH_USER_START_ADDR);
32 uwEndSector = GetSector(FLASH_USER_END_ADDR);
33 * @param Address:地址
34 * @retval 地址所在的sector
35 */
36 static uint32_t GetSector(uint32_t Address)
37 {
38 uint32_t sector = 0;
39
40 if ((Address < ADDR_FLASH_SECTOR_1) && (Address >=ADDR_FLASH_SECTOR_0)) {
41 sector = FLASH_Sector_0;
42 } else if ((Address < ADDR_FLASH_SECTOR_2) && (Address >=ADDR_FLASH_SECTOR_1)) {
43 sector = FLASH_Sector_1;
44 }
45
46 /*此处省略扇区2-扇区21的内容*/
47
48 else if ((Address < ADDR_FLASH_SECTOR_23) && (Address >=ADDR_FLASH_SECTOR_22)) {
49 sector = FLASH_Sector_22;
50 } else { /*(Address < FLASH_END_ADDR) && (Address >=ADDR_FLASH_SECTOR_23))*/
51 sector = FLASH_Sector_23;
52 }
53 return sector;
54 }
代码中固件库定义的宏FLASH_Sector_0-23对应的值是跟寄存器说明一致的,见图3‑3。
图 3‑3 FLASH_CR寄存器的SNB位的值
GetSector函数根据输入的地址与各个扇区的基地址进行比较,找出它所在的扇区,并使用固件库中的宏,返回扇区对应的SNB值。
读写内部FLASH
一切准备就绪,可以开始对内部FLASH进行擦写,这个过程不需要初始化任何外设,只要按解锁、擦除及写入的流程走就可以了,见代码清单3‑7。
代码清单 3‑7 对内部地FLASH进行读写测试(bsp_internalFlash.c文件)
1
2 /*准备写入的测试数据*/
3 #define DATA_32 ((uint32_t)0x00000000)
4 /* 要擦除内部FLASH的起始地址 */
5 #define FLASH_USER_START_ADDR ADDR_FLASH_SECTOR_8
6 /* 要擦除内部FLASH的结束地址 */
7 #define FLASH_USER_END_ADDR ADDR_FLASH_SECTOR_12
8
9 /**
10 * @brief InternalFlash_Test,对内部FLASH进行读写测试
11 * @param None
12 * @retval None
13 */
14 int InternalFlash_Test(void)
15 {
16/*要擦除的起始扇区(包含)及结束扇区(不包含),如8-12,表示擦除8、9、10、11扇区*/
17 uint32_t uwStartSector = 0;
18 uint32_t uwEndSector = 0;
19
20 uint32_t uwAddress = 0;
21 uint32_t uwSectorCounter = 0;
22
23 __IO uint32_t uwData32 = 0;
24 __IO uint32_t uwMemoryProgramStatus = 0;
25
26 /* FLASH 解锁********************************/
27 /* 使能访问FLASH控制寄存器 */
28 FLASH_Unlock();
29
30 /* 擦除用户区域 (用户区域指程序本身没有使用的空间,可以自定义)**/
31 /* 清除各种FLASH的标志位 */
32 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_OPERR |FLASH_FLAG_WRPERR |
33 FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR|FLASH_FLAG_PGSERR);
34
35
36 uwStartSector = GetSector(FLASH_USER_START_ADDR);
37 uwEndSector = GetSector(FLASH_USER_END_ADDR);
38
39 /* 开始擦除操作 */
40 uwSectorCounter = uwStartSector;
41 while (uwSectorCounter <= uwEndSector) {
42 /* VoltageRange_3 以“字”的大小进行操作 */
43 if (FLASH_EraseSector(uwSectorCounter, VoltageRange_3) !=FLASH_COMPLETE) {
44 /*擦除出错,返回,实际应用中可加入处理 */
45 return -1;
46 }
47 /* 计数器指向下一个扇区 */
48 if (uwSectorCounter == FLASH_Sector_11) {
49 uwSectorCounter += 40;
50 } else {
51 uwSectorCounter += 8;
52 }
53 }
54
55 /* 以“字”的大小为单位写入数据********************************/
56 uwAddress = FLASH_USER_START_ADDR;
57
58 while (uwAddress < FLASH_USER_END_ADDR) {
59 if (FLASH_ProgramWord(uwAddress, DATA_32) == FLASH_COMPLETE) {
60 uwAddress = uwAddress + 4;
61 } else {
62 /*写入出错,返回,实际应用中可加入处理 */
63 return -1;
64 }
65 }
66
67
68 /* 给FLASH上锁,防止内容被篡改*/
69 FLASH_Lock();
70
71
72 /*从FLASH中读取出数据进行校验***************************************/
73 /* MemoryProgramStatus = 0: 写入的数据正确
74 MemoryProgramStatus != 0: 写入的数据错误,其值为错误的个数 */
75 uwAddress = FLASH_USER_START_ADDR;
76 uwMemoryProgramStatus = 0;
77
78 while (uwAddress < FLASH_USER_END_ADDR) {
79 uwData32 = *(__IO uint32_t*)uwAddress;
80
81 if (uwData32 != DATA_32) {
82 uwMemoryProgramStatus++;
83 }
84
85 uwAddress = uwAddress + 4;
86 }
87 /* 数据校验不正确 */
88 if (uwMemoryProgramStatus) {
89 return -1;
90 } else { /*数据校验正确*/
91 return 0;
92 }
93 }
94
该函数的执行过程如下:
调用FLASH_Unlock解锁;
调用FLASH_ClearFlag清除各种标志位;
调用GetSector根据起始地址及结束地址计算要擦除的扇区;
调用FLASH_EraseSector擦除扇区,擦除时按字为单位进行操作;
调用FLASH_ProgramWord函数向起始地址至结束地址的存储区域都写入数值“DATA_32”;
调用FLASH_Lock上锁;
使用指针读取数据内容并校验。
main函数
最后我们来看看main函数的执行流程,见代码清单 3‑8。
代码清单 3‑8 main函数(main.c文件)
1 /**
2 * @brief 主函数
3 * @param 无
4 * @retval 无
5 */
6 int main(void)
7 {
8 /*初始化USART,配置模式为 115200 8-N-1*/
9 Debug_USART_Config();
10 LED_GPIO_Config();
11
12 LED_BLUE;
13 /*调用printf函数,因为重定向了fputc,printf的内容会输出到串口*/
14 printf(“this is a usart printf demo. \r\n”);
15 printf(“\r\n 欢迎使用秉火 STM32 F429 开发板。\r\n”);
16 printf(“正在进行读写内部FLASH实验,请耐心等待\r\n”);
17
18 if (InternalFlash_Test()==0) {
19 LED_GREEN;
20 printf(“读写内部FLASH测试成功\r\n”);
21
22 } else {
23 printf(“读写内部FLASH测试失败\r\n”);
24 LED_RED;
25 }
26 }
main函数中初始化了用于指示调试信息的LED及串口后,直接调用了InternalFlash_Test函数,进行读写测试并根据测试结果输出调试信息。
用USB线连接开发板“USB TOUART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到擦写内部FLASH的调试信息。
尝试擦除应用程序所在的内部FLASH扇区,观察实验现象。
使用C语言的“const uint8_t value;”和“volatile const uint8_tvalue”语句定义的变量value有什么区别?若定义后使用内部FLASH操作擦除value的存储空间,再读取value的值,哪种定义能正常读取?
设置FLASH的读写保护及解除¶
本章参考资料:《STM32F4xx参考手册》、《STM32F4xx规格书》、库说明文档《stm32f4xx_dsp_stdperiph_lib_um.chm》以及《Proprietarycode read-out protection on microcontrollers》。
选项字节与读写保护¶
在实际发布的产品中,在STM32芯片的内部FLASH存储了控制程序,如果不作任何保护措施的话,可以使用下载器直接把内部FLASH的内容读取回来,得到bin或hex文件格式的代码拷贝,别有用心的厂商即可利用该代码文件山寨产品。为此,STM32芯片提供了多种方式保护内部FLASH的程序不被非法读取,但在默认情况下该保护功能是不开启的,若要开启该功能,需要改写内部FLASH选项字节(OptionBytes)中的配置。
选项字节的内容¶
选项字节是一段特殊的FLASH空间,STM32芯片会根据它的内容进行读写保护、复位电压等配置,选项字节的构成见表4‑1。
表 4‑1 选项字节的构成
地址 | [63:16] | [15:0] |
---|---|---|
0x1FFF C000 | 保留 | ROP 和用户选项字节 (RDP & USER) |
0x1FFF C008 | 保留 | 扇区 0 到 11 的写保护 nWRP 位 |
0x1FFE C000 | 保留 | 保留 |
0x1FFE C008 | 保留 | 扇区 12 到 23 的写保护 nWRP 位 |
选项字节具体的数据位配置说明见表 4‑2。
表 4‑2 选项字节具体的数据位配置说明
我们主要讲解选项字节配置中的RDP位和PCROP位,它们分别用于配置读保护级别及代码读出保护。
RDP读保护级别¶
修改选项字节的RDP位的值可设置内部FLASH为以下保护级别:
0xAA:级别0,无保护
这是STM32的默认保护级别,它没有任何读保护,读取内部FLASH及“备份SRAM”的内容都没有任何限制。(注意这里说的“备份SRAM”是指STM32备份域的SRAM空间,不是指主SRAM,下同)
其它值:级别1,使能读保护
把RDP配置成除0xAA或0xCC外的任意数值,都会使能级别1的读保护。在这种保护下,若使用调试功能(使用下载器、仿真器)或者从内部SRAM自举时都不能对内部FLASH及备份SRAM作任何访问(读写、擦除都被禁止);而如果STM32是从内部FLASH自举时,它允许对内部FLASH及备份SRAM的任意访问。
也就是说,在级别1模式下,任何尝试从外部访问内部FLASH内容的操作都被禁止,例如无法通过下载器读取它的内容,或编写一个从内部SRAM启动的程序,若该程序读取内部FLASH,会被禁止。而如果是芯片自己访问内部FLASH,是完全没有问题的,例如前面的“读写内部FLASH”实验中的代码自己擦写内部FLASH空间的内容,即使处于级别1的读保护,也能正常擦写。
当芯片处于级别1的时候,可以把选项字节的RDP位重新设置为0xAA,恢复级别0。在恢复到级别0前,芯片会自动擦除内部FLASH及备份SRAM的内容,即降级后原内部FLASH的代码会丢失。在级别1时使用SRAM自举的程序也可以访问选项字节进行修改,所以如果原内部FLASH的代码没有解除读保护的操作时,可以给它加载一个SRAM自举的程序进行保护降级,后面我们将会进行这样的实验。
0xCC:级别2,禁止调试
把RDP配置成0xCC值时,会进入最高级别的读保护,且设置后无法再降级,它会永久禁止用于调试的JTAG接口(相当于熔断)。在该级别中,除了具有级别1的所有保护功能外,进一步禁止了从SRAM或系统存储器的自举(即平时使用的串口ISP下载功能也失效),JTAG调试相关的功能被禁止,选项字节也不能被修改。它仅支持从内部FLASH自举时对内部FLASH及SRAM的访问(读写、擦除)。
由于设置了级别2后无法降级,也无法通过JTAG、串口ISP等方式更新程序,所以使用这个级别的保护时一般会在程序中预留“后门”以更新应用程序,若程序中没有预留后门,芯片就无法再更新应用程序了。所谓的“后门”是一种IAP程序(InApplicationProgram),它通过某个通讯接口获取将要更新的程序内容,然后利用内部FLASH擦写操作把这些内容烧录到自己的内部FLASH中,实现应用程序的更新。
不同级别下的访问限制见图 4‑1。
图 4‑1 不同级别下的访问限制
不同保护级别之间的状态转换见图 4‑2。
图 4‑2 不同级别间的状态转换
PCROP代码读出保护¶
在STM32F42xx及STM32F43xx系列的芯片中,除了可使用RDP对整片FLASH进行读保护外,还有一个专用的代码读出保护功能(Proprietarycode readoutprotection,下面简称PCROP),它可以为内部FLASH的某几个指定扇区提供保护功能,所以它可以用于保护一些IP代码,方便提供给另一方进行二次开发,见图4‑3。
图 4‑3 PCROP保护功能
当SPMOD位设置为0时(默认值),nWRPi位用于指定要进行写保护的扇区,这可以防止错误的指针操作导致FLASH内容的改变,若扇区被写保护,通过调试器也无法擦除该扇区的内容;当SPMOD位设置为1时,nWRPi位用于指定要进行PCROP保护的扇区。其中PCROP功能可以防止指定扇区的FLASH内容被读出,而写保护仅可以防止误写操作,不能被防止读出。
当要关闭PCROP功能时,必须要使芯片从读保护级别1降为级别0,同时对SPMOD位置0,才能正常关闭;若芯片原来的读保护为级别0,且使能了PCROP保护,要关闭PCROP时也要先把读保护级别设置为级别1,再在降级的同时设置SPMOD为0。
修改选项字节的过程¶
修改选项字节的内容可修改各种配置,但是,当应用程序运行时,无法直接通过选项字节的地址改写它们的内容,例如,接使用指针操作地址0x1FFFC0000的修改是无效的。要改写其内容时必须设置寄存器FLASH_OPTCR及FLASH_OPTCR1中的对应数据位,寄存器的与选项字节对应位置见图4‑4及图 4‑5,详细说明请查阅《STM32参考手册》。
图 4‑4 FLASH_OPTCR寄存器说明(nWRP表示0-11扇区)
图 4‑5 FLASH_OPTCR1寄存器说明(nWRP表示12-23扇区)
默认情况下,FLASH_OPTCR寄存器中的第0位OPTLOCK值为1,它表示选项字节被上锁,需要解锁后才能进行修改,当寄存器的值设置完成后,对FLASH_OPTCR寄存器中的第1位OPTSTRT值设置为1,硬件就会擦除选项字节扇区的内容,并把FLASH_OPTCR/1寄存器中包含的值写入到选项字节。
所以,修改选项字节的配置步骤如下:
解锁,在 Flash 选项密钥寄存器 (FLASH_OPTKEYR) 中写入 OPTKEY1 =0x0819 2A3B;接着在 Flash 选项密钥寄存器 (FLASH_OPTKEYR) 中写入OPTKEY2 = 0x4C5D 6E7F。
检查 FLASH_SR 寄存器中的 BSY 位,以确认当前未执行其它Flash 操作。
在 FLASH_OPTCR 和/或 FLASH_OPTCR1 寄存器中写入选项字节值。
将 FLASH_OPTCR 寄存器中的选项启动位 (OPTSTRT) 置 1。
等待 BSY 位清零,即写入完成。
操作选项字节的库函数¶
为简化编程,STM32标准库提供了一些库函数,它们封装了修改选项字节时操作寄存器的过程。
对选项字节解锁、上锁的函数见代码清单 4‑1。
代码清单 4‑1选项字节解锁、上锁
1
2 #define FLASH_OPT_KEY1 ((uint32_t)0x08192A3B)
3 #define FLASH_OPT_KEY2 ((uint32_t)0x4C5D6E7F)
4
5 /**
6 * @brief Unlocks the FLASH Option Control Registers access.
7 * @param None
8 * @retval None
9 */
10 void FLASH_OB_Unlock(void)
11 {
12 if((FLASH->OPTCR & FLASH_OPTCR_OPTLOCK) != RESET)
13 {
14 /* Authorizes the Option Byte register programming */
15 FLASH->OPTKEYR = FLASH_OPT_KEY1;
16 FLASH->OPTKEYR = FLASH_OPT_KEY2;
17 }
18 }
19
20 /**
21 * @brief Locks the FLASH Option Control Registers access.
22 * @param None
23 * @retval None
24 */
25 void FLASH_OB_Lock(void)
26 {
27 /* Set the OPTLOCK Bit to lock the FLASH Option Byte Registersaccess */
28 FLASH->OPTCR |= FLASH_OPTCR_OPTLOCK;
29 }
解锁的时候,它对FLASH_OPTCR寄存器写入两个解锁参数,上锁的时候,对FLASH_OPTCR寄存器的FLASH_OPTCR_OPTLOCK位置1。
解锁后设置选项字节寄存器的RDP位可调用FLASH_OB_RDPConfig完成,见代码清单4‑2。
代码清单 4‑2 设置读保护级别
1 /**
2 * @brief Sets the read protection level.
3 * @param OB_RDP: specifies the read protection level.
4 * This parameter can be one of the following values:
5 * @arg OB_RDP_Level_0: No protection
6 * @arg OB_RDP_Level_1: Read protection of the memory
7 * @arg OB_RDP_Level_2: Full chip protection
8 *
9 * /!\ Warning /!\ When enabling OB_RDP level 2 it’s no morepossible to go back to level 1 or 0
10 *
11 * @retval None
12 */
13 void FLASH_OB_RDPConfig(uint8_t OB_RDP)
14 {
15 FLASH_Status status = FLASH_COMPLETE;
16
17 /* Check the parameters */
18 assert_param(IS_OB_RDP(OB_RDP));
19
20 status = FLASH_WaitForLastOperation();
21
22 if(status == FLASH_COMPLETE)
23 {
24 *(__IO uint8_t*)OPTCR_BYTE1_ADDRESS = OB_RDP;
25
26 }
27 }
该函数根据输入参数设置RDP寄存器位为相应的级别,其注释警告了若配置成OB_RDP_Level_2会无法恢复。类似地,配置其它选项时也有相应的库函数,如FLASH_OB_PCROP1Config、FLASH_OB_WRP1Config分别用于设置要进行PCROP保护或WRP保护(写保护)的扇区。
调用上一步骤中的函数配置寄存器后,还要调用代码清单4‑3中的FLASH_OB_Launch函数把寄存器的内容写入到选项字节中。
代码清单 4‑3 写入选项字节
1 /**
2 * @brief Launch the option byte loading.
3 * @param None
4 * @retval FLASH Status: The returned value can be: FLASH_BUSY,FLASH_ERROR_PROGRAM,
5 * FLASH_ERROR_WRP, FLASH_ERROR_OPERATION or FLASH_COMPLETE.
6 */
7 FLASH_Status FLASH_OB_Launch(void)
8 {
9 FLASH_Status status = FLASH_COMPLETE;
10
11 /* Set the OPTSTRT bit in OPTCR register */
12 *(__IO uint8_t *)OPTCR_BYTE0_ADDRESS |=FLASH_OPTCR_OPTSTRT;
13
14 /* Wait for last operation to be completed */
15 status = FLASH_WaitForLastOperation();
16
17 return status;
18 }
该函数设置FLASH_OPTCR_OPTSTRT位后调用了FLASH_WaitForLastOperation函数等待写入完成,并返回写入状态,若操作正常,它会返回FLASH_COMPLETE。
实验:设置读写保护及解除¶
在本实验中我们将以实例讲解如何修改选项字节的配置,更改读保护级别、设置PCROP或写保护,最后把选项字节恢复默认值。
本实验要进行的操作比较特殊,在开发和调试的过程中都是在SRAM上进行的(使用SRAM启动方式)。例如,直接使用FLASH版本的程序进行调试时,如果该程序在运行后对扇区进行了写保护而没有解除的操作或者该解除操作不正常,此时将无法再给芯片的内部FLASH下载新程序,最终还是要使用SRAM自举的方式进行解除操作。所以在本实验中为便于修改选项字节的参数,我们统一使用SRAM版本的程序进行开发和学习,当SRAM版本调试正常后再改为FLASH版本。
关于在SRAM中调试代码的相关配置,请参考前面的章节。
注意:
若您在学习的过程中想亲自修改代码进行测试,请注意备份原工程代码。当芯片的FLASH被保护导致无法下载程序到FLASH时,可以下载本工程到芯片,并使用SRAM启动运行,即可恢复芯片至默认配置。但如果修改了读保护为级别2,采用任何方法都无法恢复!(除了这个配置,其它选项都可以大胆地修改测试。)
硬件设计¶
本实验在SRAM中调试代码,因此把BOOT0和BOOT1引脚都使用跳线帽连接到3.3V,使芯片从SRAM中启动。
软件设计¶
本实验的工程名称为“设置读写保护与解除”,学习时请打开该工程配合阅读,它是从“RAM调试—多彩流水灯”工程改写而来的。为了方便展示及移植,我们把操作内部FLASH相关的代码都编写到“internalFlash_reset.c”及“internalFlash_reset.h”文件中,这些文件是我们自己编写的,不属于标准库的内容,可根据您的喜好命名文件。
主要实验¶
学习配置扇区写保护;
学习配置PCROP保护;
学习配置读保护级别;
学习如何恢复选项字节到默认配置;
代码分析¶
配置扇区写保护
我们先以代码清单 4‑4中的设置与解除写保护过程来学习如何配置选项字节。
代码清单 4‑4 配置扇区写保护
1
2 #define FLASH_WRP_SECTORS (OB_WRP_Sector_0|OB_WRP_Sector_1)
3 __IO uint32_t SectorsWRPStatus = 0xFFF;
4
5 /**
6 * @brief WriteProtect_Test,普通的写保护配置
7 * @param运行本函数后会给扇区FLASH_WRP_SECTORS进行写保护,再重复一次会进行解写保护
8 * @retval None
9 */
10 void WriteProtect_Test(void)
11 {
12 FLASH_Status status = FLASH_COMPLETE;
13 {
14 /* 获取扇区的写保护状态 */
15 SectorsWRPStatus = FLASH_OB_GetWRP() & FLASH_WRP_SECTORS;
16
17 if (SectorsWRPStatus == 0x00)
18 {
19 /* 扇区已被写保护,执行解保护过程*/
20
21 /* 使能访问OPTCR寄存器 */
22 FLASH_OB_Unlock();
23
24 /* 设置对应的nWRP位,解除写保护 */
25 FLASH_OB_WRPConfig(FLASH_WRP_SECTORS, DISABLE);
26 status=FLASH_OB_Launch();
27 /* 开始对选项字节进行编程 */
28 if (status != FLASH_COMPLETE)
29 {
30 FLASH_ERROR(“对选项字节编程出错,解除写保护失败,status =%x”,status);
31 /* User can add here some code to deal with this error */
32 while (1)
33 {
34 }
35 }
36 /* 禁止访问OPTCR寄存器 */
37 FLASH_OB_Lock();
38
39 /* 获取扇区的写保护状态 */
40 SectorsWRPStatus = FLASH_OB_GetWRP() & FLASH_WRP_SECTORS;
41
42 /* 检查是否配置成功 */
43 if (SectorsWRPStatus == FLASH_WRP_SECTORS)
44 {
45 FLASH_INFO(“解除写保护成功!”);
46 }
47 else
48 {
49 FLASH_ERROR(“未解除写保护!”);
50 }
51 }
52 else
53 { /* 若扇区未被写保护,开启写保护配置 */
54
55 /* 使能访问OPTCR寄存器 */
56 FLASH_OB_Unlock();
57
58 /*使能 FLASH_WRP_SECTORS 扇区写保护 */
59 FLASH_OB_WRPConfig(FLASH_WRP_SECTORS, ENABLE);
60
61 status=FLASH_OB_Launch();
62 /* 开始对选项字节进行编程 */
63 if (status != FLASH_COMPLETE)
64 {
65 FLASH_ERROR(“对选项字节编程出错,设置写保护失败,status =%x”,status);
66 while (1)
67 {
68 }
69 }
70 /* 禁止访问OPTCR寄存器 */
71 FLASH_OB_Lock();
72
73 /* 获取扇区的写保护状态 */
74 SectorsWRPStatus = FLASH_OB_GetWRP() & FLASH_WRP_SECTORS;
75
76 /* 检查是否配置成功 */
77 if (SectorsWRPStatus == 0x00)
78 {
79 FLASH_INFO(“设置写保护成功!”);
80 }
81 else
82 {
83 FLASH_ERROR(“设置写保护失败!”);
84 }
85 }
86 }
87 }
本函数分成了两个部分,它根据目标扇区的状态进行操作,若原来扇区为非保护状态时就进行写保护,若为保护状态就解除保护。其主要操作过程如下:
调用FLASH_OB_GetWRP函数获取目标扇区的保护状态若扇区被写保护,则开始解除保护过程,否则开始设置写保护过程;
调用FLASH_OB_Unlock解锁选项字节的编程;
调用FLASH_OB_WRPConfig函数配置目标扇区关闭或打开写保护;
调用FLASH_OB_Launch函数把寄存器的配置写入到选项字节;
调用FLASH_OB_GetWRP函数检查是否配置成功;
调用FLASH_OB_Lock禁止修改选项字节。
配置PCROP保护
配置PCROP保护的过程与配置写保护过程稍有区别,见代码清单 4‑5。
代码清单 4‑5 配置PCROP保护(internalFlash_reset.c文件)
1
2 /**
3 * @brief SetPCROP,设置PCROP位,用于测试解锁
4 * @note 使用有问题的串口ISP下载软件,可能会导致PCROP位置1,
5 导致无法给芯片下载程序到FLASH,本函数用于把PCROP位置1,
6 模拟出无法下载程序到FLASH的环境,以便用于解锁的程序调试。
7 若不了解PCROP位的作用,请不要执行此函数!!
8 * @param None
9 * @retval None
10 */
11 void SetPCROP(void)
12 {
13
14 FLASH_Status status = FLASH_COMPLETE;
15
17
18 FLASH_INFO();
19 FLASH_INFO(“正在设置PCROP保护,请耐心等待...”);
20 //选项字节解锁
21 FLASH_OB_Unlock();
22
23 //设置为PCROP模式
24 FLASH_OB_PCROPSelectionConfig(OB_PcROP_Enable);
25 //设置扇区0进行PCROP保护
26 FLASH_OB_PCROPConfig(OB_PCROP_Sector_10,ENABLE);
27 //把寄存器设置写入到选项字节
28 status =FLASH_OB_Launch();
29
30 if (status != FLASH_COMPLETE)
31 {
32 FLASH_INFO(“设置PCROP失败!”);
33 }
34 else
35 {
36 FLASH_INFO(“设置PCROP成功!”);
37
38 }
39 //选项字节上锁
40 FLASH_OB_Lock();
41 }
该代码在解锁选项字节后,调用FLASH_OB_PCROPSelectionConfig函数把SPMOD寄存器位配置为PCROP模式,接着调用FLASH_OB_PCROPConfig函数配置目标保护扇区。
恢复选项字节为默认值
当芯片被设置为读写保护或PCROP保护时,这时给芯片的内部FLASH下载程序时,可能会出现图4‑6和图 4‑7的擦除FLASH失败的错误提示。
图 4‑6 擦除失败提示
图 4‑7 擦除进度条卡在开始状态
只要不是把读保护配置成了级别2保护,都可以使用SRAM启动运行代码清单4‑6中的函数恢复选项字节为默认状态,使得FLASH下载能正常进行。
代码清单 4‑6 恢复选项字节为默认值
1 // @brief OPTCR register byte 0 (Bits[7:0]) base address
2 #define OPTCR_BYTE0_ADDRESS ((uint32_t)0x40023C14)
3 //@brief OPTCR register byte 1 (Bits[15:8]) base address
4 #define OPTCR_BYTE1_ADDRESS ((uint32_t)0x40023C15)
5 //@brief OPTCR register byte 2 (Bits[23:16]) base address
6 #define OPTCR_BYTE2_ADDRESS ((uint32_t)0x40023C16)
7 // @brief OPTCR register byte 3 (Bits[31:24]) base address
8 #define OPTCR_BYTE3_ADDRESS ((uint32_t)0x40023C17)
9 // @brief OPTCR1 register byte 0 (Bits[7:0]) base address
10 #define OPTCR1_BYTE2_ADDRESS ((uint32_t)0x40023C1A)
11
12 /**
13 * @brief InternalFlash_Reset,恢复内部FLASH的默认配置
14 * @param None
15 * @retval None
16 */
17 int InternalFlash_Reset(void)
18 {
19 FLASH_Status status = FLASH_COMPLETE;
20
21 /* 使能访问选项字节寄存器 */
22 FLASH_OB_Unlock();
23
24 /* 擦除用户区域 (用户区域指程序本身没有使用的空间,可以自定义)**/
25 /* 清除各种FLASH的标志位 */
26 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_OPERR |FLASH_FLAG_WRPERR |
27 FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR|FLASH_FLAG_PGSERR);
28
29 FLASH_INFO(“\r\n”);
30 FLASH_INFO(“正在准备恢复的条件,请耐心等待...”);
31
32 //确保把读保护级别设置为LEVEL1,以便恢复PCROP寄存器位
33//PCROP寄存器位从设置为0时,必须是读保护级别由LEVEL1转为LEVEL0时才有效,
34 //否则修改无效
35 FLASH_OB_RDPConfig(OB_RDP_Level_1);
36
37 status=FLASH_OB_Launch();
38
39 status = FLASH_WaitForLastOperation();
40
41 //设置为LEVEL0并恢复PCROP位
42
43 FLASH_INFO(“\r\n”);
44 FLASH_INFO(“正在擦除内部FLASH的内容,请耐心等待...”);
45
46 //关闭PCROP模式
47 FLASH_OB_PCROPSelectionConfig(OB_PcROP_Disable);
48 FLASH_OB_RDPConfig(OB_RDP_Level_0);
49
50 status =FLASH_OB_Launch();
51
52 //设置其它位为默认值
53 (*(__IO uint32_t *)(OPTCR_BYTE0_ADDRESS))=0x0FFFAAE9;
54 (*(__IO uint16_t *)(OPTCR1_BYTE2_ADDRESS))=0x0FFF;
55 status =FLASH_OB_Launch();
56 if (status != FLASH_COMPLETE)
57 {
58 FLASH_ERROR(“恢复选项字节默认值失败,错误代码:status=%X”,status);
59 }
60 else
61 {
62 FLASH_INFO(“恢复选项字节默认值成功!”);
63 }
64 //禁止访问
65 FLASH_OB_Lock();
66
67 return status;
68 }
这个函数进行了如下操作:
调用FLASH_OB_Unlock解锁选项字节的编程;
调用FLASH_ClearFlag函数清除所有FLASH异常状态标志;
调用FLASH_OB_RDPConfig函数设置为读保护级别1,以便后面能正常关闭PCROP模式;
调用FLASH_OB_Launch写入选项字节并等待读保护级别设置完毕;
调用FLASH_OB_PCROPSelectionConfig函数关闭PCROP模式;
调用FLASH_OB_RDPConfig函数把读保护级别降为0;
调用FLASH_OB_Launch定稿选项字节并等待降级完毕,由于这个过程需要擦除内部FLASH的内容,等待的时间会比较长;
直接操作寄存器,使用“(*(__IO uint32_t*)(OPTCR_BYTE0_ADDRESS))=0x0FFFAAE9;”和“(*(__IO uint16_t*)(OPTCR1_BYTE2_ADDRESS))=0x0FFF;”语句把OPTCR及OPTCR1寄存器与选项字节相关的位都恢复默认值;
调用FLASH_OB_Launch函数等待上述配置被写入到选项字节;
恢复选项字节为默认值操作完毕。
main函数
最后来看看本实验的main函数,见。代码清单 4‑7。
代码清单 4‑7 main函数
1 /**
2 * @brief 主函数
3 * @param 无
4 * @retval 无
5 */
6 int main(void)
7 {
8 /* LED 端口初始化 */
9 LED_GPIO_Config();
10 Debug_USART_Config();
11 LED_BLUE;
12
13 FLASH_INFO(“本程序将会被下载到STM32的内部SRAM运行。”);
14FLASH_INFO(“下载程序前,请确认把实验板的BOOT0和BOOT1引脚都接到3.3V电源处!!”);
15
16 FLASH_INFO(“\r\n”);
17 FLASH_INFO(“—-这是一个STM32芯片内部FLASH解锁程序—-”);
18 FLASH_INFO(“程序会把芯片的内部FLASH选项字节恢复为默认值”);
19
20
21 #if 0 //工程调试、演示时使用,正常解除时不需要运行此函数
22 SetPCROP(); //修改PCROP位,仿真芯片被锁无法下载程序到内部FLASH的环境
23 #endif
24
25 #if 0 //工程调试、演示时使用,正常解除时不需要运行此函数
26 WriteProtect_Test();//修改写保护位,仿真芯片扇区被设置成写保护的的环境
27 #endif
28
30
31 /*恢复选项字节到默认值,解除保护*/
32 if(InternalFlash_Reset()==FLASH_COMPLETE)
33 {
34 FLASH_INFO(“选项字节恢复成功,请把BOOT0和BOOT1引脚都连接到GND,”);
35FLASH_INFO(“然后随便找一个普通的程序,下载程序到芯片的内部FLASH进行测试”);
36 LED_GREEN;
37 }
38 else
39 {
40 FLASH_INFO(“选项字节恢复成功失败,请复位重试”);
41 LED_RED;
42 }
43
45
46 while (1);
47 }
在main函数中,主要是调用了InternalFlash_Reset函数把选项字节恢复成默认值,程序默认时没有调用SetPCROP和WriteProtect_Test函数设置写保护,若您想观察实验现象,可修改条件编译的宏,使它加入到编译中。
下载测试¶
把开发板的BOOT0和BOOT1引脚都使用跳线帽连接到3.3V电源处,使它以SRAM方式启动,然后用USB线连接开发板“USBTOUART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板并复位运行,在串口调试助手可看到调试信息。程序运行后,请耐心等待至开发板亮绿灯或串口调试信息提示恢复完毕再给开发板断电,否则由于恢复过程被中断,芯片内部FLASH会处于保护状态。
芯片内部FLASH处于保护状态时,可重新下载本程序到开发板以SRAM运行恢复默认配置。