作者: Sheldon He
摘要:实时控制器往往拥有十分有限的存储器资源特别是片内的随机存储器(RAM)资源。能否合理、高效的运用这些资源不仅关乎到整个嵌入式系统的实现成本与性能,更涉及到系统在运行时是否会出现致命且不易被发现的错误。本文将对C2000系列微控制器的栈 (亦习惯性的被称为堆栈,这里请注意堆与栈之间的区别)做简单的介绍,并提出四种方法来对应用程序运行所需的栈空间大小进行追踪或评估,以帮助开发者在开发过程中(尤其是使用C/C++高级语言进行开发时)优化内存资源的使用并避免嵌入式程序可能存在的风险。
在计算机中,栈作为一种数据结构可以存放一系列的成员并且通过“入栈”和“出栈”操作来从栈定加入新的数据或从栈顶拿走数据。从类别上来看堆栈通常又可以分为软件堆栈和硬件堆栈两类,前者时常经由数组和链表在程序中实现而后者则与计算机架构相关并被用于实现内存的分配及访问。
本文要讨论的是C2000系列微控制器C28x内核中的硬件堆。该栈的典型特点为有一个固定的起始地址,或者说是寄存器复位值,和一个由编译器指定的可变的栈空间大小。C28x内核的堆栈指针(stack point)寄存器SP是16位寄存器,且在使用时高16位保持为0,故可以访问64K大小的内存空间。在芯片复位时,SP的内容变为0x00000400,且在使用时栈由低地址向高地址生长。堆栈的合法使用空间通常由编译器命令--stack_size= size来设定,其中size是一个常数,指定了栈空间的大小(以16位字为单位),栈的空间不得超过实际非初始化物理内存区域.stack大小也不得超出0xFFFF范围,否则将产生溢出。
一个嵌入式系统软件常常会因为多种原因而需要使用堆栈,这些原因包括:存储数学表达式的中间计算结果、在函数递归时存储每一次调用的函数返回地址、存放函数内的局部变量、存放传递进函数的参数等。随着软件流程变得越来越庞大复杂,如何正确的评估所需的堆栈空间就显得十分重要。分配过多的堆栈空间会“浪费”内存,堆栈溢出则可能造成堆栈信息丢失或者修改到邻近内存区域的数据并最终导致系统出错。
本文总结了四种适用于TI C2000系列MCU的堆栈使用评估方法,同时建议读者在有条件的情况下使用多种方法交叉验证以弥补单一方法使用过程中的局限性。这些方法通常情况下也适用于TI的其他部分嵌入式产品,对于其他各类嵌入式系统的堆栈测试、评估也有一定的借鉴意义。
一、使用TI提供的XML文件处理脚本生成函数调用图并进行静态分析
通过函数的调用关系可以静态的分析堆栈的使用情况,TI提供了一套基于Perl的脚本工具可以用于分析工程build过程中产生的XML文件以提供程序空间使用相关的信息。这里笔者需要用到的是该工具包中的call_graph.pl脚本来生成函数调用图(Call Graph)。
首先需要在wiki页面中下载并安装该工具包,可以在搜索引擎中检索关键字“Code_Generation_Tools_XML_Processing_Scripts”
并找到对应的ti.com页面进行下载安装。对于不熟悉命令行操作的读者可以按照以下三个步骤来使用该脚本。
1. 新建一个文件夹并以英文命名,并从CCS对应的C2000编译器目录拷贝odf2000.exe到该新建的文件夹中。(ofd2000.exe在C:/ti/ccs901/ccs/tools/compiler/ti-cgt-c2000_18.12.1.LTS/bin,路径随CCS版本、CCS安装路径及编译器版本不同会有差异)同时还需要从cgxml工具路径C:/ti/cgxml/bin中拷贝call_graph.exe,从工程目录拷贝编译生成的.out文件到该文件夹中。
2. 打开命令行工具(可在windows开始菜单搜索“CMD”找到),在其中输入如下命令选取上一步中新建的文件夹为工作目录
cd C:/ti/cgxml/utils
3. 在命令行中运行如下脚本获取输出结果,用户需要自行修改.out文件的文件名使其与第一步中复制到文件夹中的.out同名。
ofd2000 -xg gpio_toggle_cpu01.out | call_graph --stack_max
此时用户可以在命令行的输出中看到最恶劣情况下的堆栈占用情况,此处,函数c_int00是函数调用图的根,其调用在最大情况下会占用48个16位字的堆栈空间。但是这样的结果有两点限制条件将在本节的末尾部分指出。
如果使用--stack_max参数则可以获得更多的细节信息,具体的数据解读方法请参阅安装目录下的文档《call_graph.pdf》。
该方法简单易用,但是对于非直接调用的函数以及相互嵌套的中断服务,该工具则无法直接将其在脚本输出结果中表现出来。此时需要使用者结合call_graph输出的详细信息,借助自己对于程序流程的理解,分析得到最终的堆栈评估结果。
二、使用回调函数在运行时抓取栈指针(SP)最大值
C2000较新版本的编译器支持在函数的进入和退出过程中插入回调函数。开发者可以使--entry_hook选项为每个函数的开头部分插入一段读取堆栈指针(SP)的代码并在一定周期的程序运行中对堆栈指针的最大值进行抽取与比较从而获取统计学的极限堆栈使用情况。
以TI v18.12.2LTS Coder generation tool为例做一个测试,首先右击打开工程属性,并在“Advanced Options”中找到--entry_hook设置栏目,在后方的空格处输入回调函数的函数名称(以名为“entry_hook”的函数为例)。
之后可以在c文件中定义函数entry_hook,其中使用的SP_current及SP_max为事先声明的int型全局变量。
void entry_hook(){
SP_current = getStackPointer();
SP_max = (SP_current > SP_max) ? SP_current : SP_max;
}
在该函数中使用了一小段汇编函数getStackPointer();用于获取堆栈指针(SP)寄存器的值,该函数的定义为:
_getStackPointer:
.asmfunc
MOV AL, SP
SUB AL, #2
LRETR
.endasmfunc
测试前还需要在头文件中对其做如下形式的函数声明:
extern int getStackPointer(void);
在完成设置后重新build工程,并点击CCS中的“Debug”按钮进入在线调试状态,此后可以进行全速运行。运行一段时间以后打开CCS的“View”,“Expressions”并点击绿色加号“Add new expression”输入变量名SP_max对最大堆栈占用情况进行观察。
通过回调函数做堆栈指针的采样统计不一定可以抓取到最极限的堆栈使用情况,实际的堆栈消耗会比用这种方法测量到的略大,因此笔者也提出了第三种测试方法。
三、在栈空间填充标识数据以检测栈空间使用情况
方法三的思路是在堆栈空间的特定内存区域中预先写入标志性数据如(0x5A)。经由程序的执行,使用过的堆栈空间内的数据会被其他数据覆盖掉,从栈尾开始向低地址走的标志性数据则因其内存空间未被使用而得以保留不变,当然这一切的前提是堆栈不发生溢出。通过寻找被修改数组的最大地址即可以判断出这一测试过程中程序实际所使用的最大堆栈规模。
该方法可以在连接仿真器的情况下进行堆栈占用情况的观察,也可以在芯片脱离仿真器运行之后再连接入仿真器(通过设置使目标芯片在连接时不被复位)并通过“Memory Browser”进行结果观察。该方法的准确性取决于软件执行的覆盖程度。
四、使用ERAD外设模块进行堆栈监测
ERAD(embedded real-time analysis and diagnostic)模块是F28004x系列MCU新增的外设,他独立于C28x内核之外,具有8个总线比较器和4个检测计数器子功能模块。由于该模块既可以被应用程序访问也可以被仿真工具访问因此能极大的增加调试的灵活性和便利性。
关于如何使用ERAD模块进行堆栈监控可以直接参考TI C2000ware软件包自带的范例程序,其参考位置为:C:/ti/c2000/C2000Ware_2_00_00_03/driverlib/f28004x/examples/erad/stack_overflow
其基本工作方式是对地址总线进行监控并根据HWBP_CNTL寄存器的配置,将地址总线内容与HWBP_REF寄存器中的参考值以指定方式进行对比(大于、大于等于、小于、小于等于),最终在比较事件发生时触发CPU的停止动作或生产RTOSINTn中断。通过这种方式的多次运用可以把堆栈空间的实际需求锁定在一个区间内便于参考。
总结:本文结合工程开发和调试的实际经验,对常用的四种C2000 MCU程序堆栈空间评估方法进行了总结,期待读者在阅读后能够结合实际情况选择一种或者多种方式确定出应用程序的堆栈需求并在工程属性中进行合理的配置,以实现最大程度的优化。
审核编辑 黄昊宇
全部0条评论
快来发表一下你的评论吧 !