当前位置:首页 > 公众号精选 > 裸机思维

名场面]《让子弹飞》你不是欺负老实人吗?-娱乐圈-虎扑社区

【说在前面的话】


在操蛋的现实生活中,虽然防不胜防,但我们一定要时刻提醒自己:不要落入自证陷阱。但在嵌入式系统开发时,面对那种应用环境恶劣、量大且维护难度艰巨的产品,如何保证系统可靠性,避免陷入“发配客户现场坐牢”的危机——在开发阶段做好必要的质量自证,就成了必不可少的“自救手段”。沉浸式体验“发配边疆”!中国推出新奇旅游配套,穿囚服戴枷锁“流放宁古塔”!还原《甄嬛传》场景! – TTN 谈谈网

排除掉硬件设计缺陷带来的各类问题,单从固件开发的角度来说,由于缺乏有效的工具和相关的培训,很多小伙伴处理这类可靠性验证的方法就只有 7x24 小时烤机这一条路可走了——在这段如坐针毡的时间内,很多嵌入式工程师会陷入加班和担忧的双重煎熬——不出事还好,“万一测出点事情”,如果不能稳定复现问题,则会堕入生不如死的玄学修罗地狱。

虽然不是唯一的原因,但栈溢出就往往就是这类“难以稳定复现故障”的罪魁祸首。为了有效排查“偶发性故障是否由栈溢出导致”,有经验的老工程师会在 7x24 小时拷机时使用水印法对栈进行染色——比如,在Reset_Handler中对系统栈进行水印填充:

...#include "perf_counter.h"
/*----------------------------------------------------------------------------  Reset Handler called on controller reset *----------------------------------------------------------------------------*/__NO_RETURN void Reset_Handler(void){    extern uint32_t Image$$ARM_LIB_STACK$$ZI$$Base[];
    perfc_stack_fill(__perfc_port_get_sp(),                     (uintptr_t)Image$$ARM_LIB_STACK$$ZI$$Base);    SystemInit();                             /* CMSIS System Initialization */    __PROGRAM_START();                        /* Enter PreMain (C library entry point) */}...

或是在scatter script中对 ARM_LIB_STACK 直接进行填充:

如果在烤机的过程中发生了故障,则通过Memory View观察栈所在空间的水印情况来判断是否有发生过栈溢出的迹象:

当然,也可以通过从栈低向上搜索水印的方式来统计剩余空间:

#include "perf_counter.h"
...extern uint32_t Image$$ARM_LIB_STACK$$ZI$$Base[];
...printf("System Stack Remain: %"PRIu32 "\r\n",        perfc_stack_remain(           (uintptr_t)&Image$$ARM_LIB_STACK$$ZI$$Base));...

上述方法虽然有效,但只能证明:“在过去的7x24小时内”、“在当时的外部环境下”“没有发生栈溢出”——当然对很多人来说这就足够了。但对一些面巨额维护成本的项目来说,上述方法存在以下难以容忍的缺陷:

  • 无法以可信的工程数学模型获取系统对栈的最大消耗,换句话说:
  • 无法精确的测量出栈在极限情况下的最大消耗,又或者
  • 无法拍胸脯保证:在一切运行正常的情况下,从工程数学的角度来说,栈绝对不会溢出。

此外:

  • 烤机的方法是被动的,是产品测试阶段起到兜底作用的措施,且
  • 非常耗时——必须运行足够长的时间才有可能覆盖大部分运行场景;

那么,有没有一种方法,可以:在产品研发阶段,以有限的时间和资源消耗,简单可靠的计算出固件的最大栈用量呢?有的,宝子,有的!在新发布的 v2.5.2 中,perf_counter 就提供了这样一款简单好用的工具。

來吧!我等不及了!

为了快速进入正题,我把 perf_counter 的安装部署教程放到了最后,现在,就让我们马上发车吧!

【如何“活体”测量“任意”代码片段的“实时”栈消耗】


在部署了 perf_counter 后,我们“不用”包含任何头文件,就可以在系统任何(C语言可以触及的)地方,使用关键字 __stack_usage__() 来测量任意给定代码的栈消耗。它的语法原形为:

__stack_usage__(<“描述字符串”>,<栈底地址>[, {用户代码}]) {    /* 待测的任意代码片段 */}

这里:

  • 描述字符串是用户给定的任意字符串,一般在 __stack_usage__() 打印结果时起到描述和标识的作用。描述字符串不可以省略,如果不想提供具体内容,可以传递空串(“”)替代。
  • 栈底地址(stacklimit)在逻辑上是相对栈顶指针而存在的一个概念。对于Cortex-M这样向下生长(从值比较大的地址向值比较小的地址生长)的栈来说,栈底地址就是栈空间的基地址。

如果你按照文章《这么好用?!99%开发者从未听说过的堆栈模型(加量增强版)》介绍的方法设置栈和堆,或者在 scatter script (*.sct)文件中通过 ARM_LIB_STACK 来设置栈空间,那么可以通过下面的方法获得系统栈的栈底地址:

extern uintptr_t     Image$$ARM_LIB_STACK$$ZI$$Base[];uintptr_t nStackLimit =     (uintptr_t)Image$$ARM_LIB_STACK$$ZI$$Base;

如果你的工程中,系统栈被定义在名为 STACK 的 Section 中,则可以通过类似的方法获得栈底地址:

extern uintptr STACK$$Base[];uintptr_t nStackLimit = (uintptr_t)STACK$$Base;

在某些芯片架构中,栈的生长方向是自下而上的(即:从值比较小的地址向值比较大的地址生长),那么逻辑上的栈底地址实际上是“栈空间最后一个字节的地址”。这么说也许有些抽象,我们不妨举个例子:假设我们分配一个栈空间,它的起始地址是 0x20000000,大小是 0x2000,那么“栈空间最后一个字节的地址” 应该是 (0x20002000 - 1),也就是 0x20001FFF。


说了这么多,我们不妨来看一个简单的例子:

void main(void){    ...    extern uintptr_t         Image$$ARM_LIB_STACK$$ZI$$Base[];    uintptr_t nStackLimit =         (uintptr_t)Image$$ARM_LIB_STACK$$ZI$$Base;
    while(1) {
        __stack_usage__("Task1", nStackLimit) {            task1();        }
        task2();             }}

如果你已经处理好了 printf(),或者通过在工程配置中定义宏 __perf_counter_printf__ 使用你自己的输出手段:

那么就可以看到类似如下的输出结果:

...-------------------------------------Task1 Stack Used: 32 Bytes...-------------------------------------Task1 Stack Used: 96 Bytes...-------------------------------------Task1 Stack Used: 96 Bytes...

聪明的你也许注意到了:__stack_usage__() 测量的结果每次都有可能不一样,这是因为它测量的是“本次”执行目标代码的“实时栈消耗”。如果你不关心“实时栈消耗”,而只想获得“历史最大栈消耗”,那么可以使用关键字 __stack_usage_max__() 来取代——输入参数相同,且后面有专门的章节介绍,这里就不在赘述。从优点上说,__stack_usage__():

  • 允许同时存在多个
  • 允许嵌套

那么代价是什么呢是什么梗魔兽经典台词- 弹幕秀

凡事皆有代价,从缺点上来说,由于__stack_usage__() 在本质上是对 perfc_stack_fill() 和 perfc_stack_remain() 这对组合的封装,因此会在执行被测代码前对“剩下”的栈空间进行染色,在退出被测代码后对栈进行统计——这一来一回,都是要消耗时间的——如果栈给的过大,那么上述过程所带来的代价就会大大增加。当然,如果栈的大小比较贴合,那么上述开销就会相应缩小,甚至感觉不到了。有时候,我们并不希望将测量的结果直接打印出去,而是想通过某种方法读取出来,以备后用。这就要借助 __stack_usage__() 原形中第三个可选的参数了:

__stack_usage__(<“描述字符串”>,<栈底地址>[, {用户代码}]) {    /* 待测的任意代码片段 */}

这里,“用户代码” 部分是可选的,当我们添加了任意代码后,__stack_usage__() 将不再打印结果。此时,我们可以借助局部变量 __stack_used__ 来读取结果:

size_t g_nTask1StackUsed = 0;...void main(void){    ...    extern uintptr_t         Image$$ARM_LIB_STACK$$ZI$$Base[];    uintptr_t nStackLimit =         (uintptr_t)Image$$ARM_LIB_STACK$$ZI$$Base;    while(1) {        __stack_usage__("Task1", nStackLimit, {                g_nTask1StackUsed = __stack_used__;            }) {            task1();        }        task2();          ...           }}

在这个例子中,我们通过插入代码

g_nTask1StackUsed = __stack_used__;

将测量结果保存在全局变量 g_nTask1StackUsed 中。

【卖家秀和买家秀】


看了前面的例子,你也许已经有点小心动了——不光因为这可能是你第一次看到有人能单独测量任意指定代码的栈消耗,还惊讶于,这样简单好用的关键字居然可以同时存在多个,甚至允许嵌套使用(彼此不影响结果)。但在你实际动手之前,请允许我讨论一个小问题。

实际上,如何设定“栈底地址”是一个稍微有点复杂的问题。以Cortex-M处理器为例:

  • 在最理想的裸机环境中,运行在Handler模式下的中断处理程序与运行在Thread模式下的超级循环,使用的都是系统栈,栈顶指针保存在 MSP 中——这是一种简单的情况,此时,“栈底指针”说的就是“系统栈”的栈底指针。
  • 在稍微复杂点的裸机环境中,Thread模式下运行的超级循环使用的栈由PSP来维护,而中断处理程序使用的则固定是MSP——他们二者所指向的栈空间显然不可能是同一个。此时,超级循环和中断处理程序都有可能用到我们索要测量的代码片段,此时,如何设定“栈底地址”就没那么简单了。
  • 在RTOS环境中,每个线程都有自己独立的任务栈,因此在测量目标代码的栈消耗时,显然不能使用系统栈的栈底地址,而应该使用任务栈所对应的栈底地址。假设被测代码同样有可能被多个任务所调用,这意味着每次调用时,目标代码使用的栈都是不同的,那么如何正确设置“栈底地址”就是一个头疼的问题。

此时的你多半已经被绕晕了,不光打消了要试一试 __stack_usage__ 的念头,甚至还为自己被“骗的满怀期待”有些许愤怒。

我知道你很急但是你先别急》文字表情包表情包:lay了表情包图片gif动图- 求表情网,斗图从此不求人!

我这里有一个完美且无脑的解决方案,可以让你完全无视上述问题,且让你“简简单单”就写出一个能“自适应”的测量方案。

不卖关子,对向下生长的栈来说,新的方法如下:

uintptr_t nStackLimit = __perfc_port_get_sp()                      - <被测代码预计的最大栈消耗>;
__stack_usage__(<“描述字符串”>,nStackLimit[, {用户代码}]) {    /* 待测的任意代码片段 */}

比如:

void some_function(void){    ...
    uintptr_t nStackLimit = __perfc_port_get_sp()                          - 512;    __stack_usage__("",nStackLimit) {            ...        }    ...
}

在这个例子中,由于不确定 some_function() 所使用的栈会是哪个,因此,我们通过 __perfc_port_get_sp() 无脑获取了当前的栈顶指针;接着根据我们的预估——觉得被测代码不可能使用超过512字节的栈空间——因此在当前栈顶指针的基础上减小了 512 (栈向下生长)。通过这种方法:

  • 我们不需要关心当前代码使用的是哪个栈;
  • 不需要关心栈的基地址
  • 通过预估待测代码的栈消耗,可以通过一个非常“贴合”的预测深度,降低 __stack_usage__() 的时间消耗

如果测量出来的结果与我们的预测值相同或者相近,说明实际栈用量超过了我们的预测。此时,我们只要适当的增加预测值,就可以获得正确的栈用量信息——如果追加一次搞不定,就多追加几次嘛。


如果你使用的栈生长方向是自下而上(从值比较小的地址向值比较大的地址生长),那么上述方法就要做对应的调整——把原本的减号修改为加号,这里就不再赘述。

uintptr_t nStackLimit = __perfc_port_get_sp()                      + <被测代码预计的最大栈消耗>;__stack_usage__(<“描述字符串”>,nStackLimit[, {用户代码}]) {    /* 待测的任意代码片段 */}

【如何测量“任意”代码片段的历史最大栈消耗】


正如前面所说,如果你只关心目标代码的历史最大栈消耗,则可以使用 __stack_usage_max__() 来取代 __stack_usage__()。他的语法原形如下:

uintptr_t nStackLimit = __perfc_port_get_sp()                      - <被测代码预计的最大栈消耗>;__stack_usage_max__(<“描述字符串”>,nStackLimit[, {用户代码}]) {    /* 待测的任意代码片段 */}

这里,所有的输入参数和用法与 __stack_usage__() 几乎一样。需要注意的是,在插入用户代码读取结果时,测量值保存在 __stack_used_max__ 中而不是此前的 __stack_used__。

【如何测量任意中断处理程序的最大栈消耗】


回到最初的问题,对一个产品来说,最大栈用量往往发生在极端中断嵌套发生的情况下。虽然很少见,或者说很难复现,但:

  • 如果一个系统中使用了 n 个可抢占优先级;
  • 虽然同一个优先级下存在多个中断处理程序,但考虑到他们彼此无法打断,因此,每一个优先级下只取所有中断处理程序中栈消耗的最大值,标记为 En;
  • 假设运行在Thread模式下的用户任务X也使用 MSP,且该用户任务的最大栈消耗为 X0;

那么,该用户任务X的极端理论最大栈消耗为:

翻译成人话就是:把该任务的最大栈消耗和所有可抢占优先级的最大栈消耗加起来。任务的最大栈消耗可以通过 __stack_usage_max__() 来测量获得,那么每个中断处理程序的最大栈消耗又如何测量呢?针对Cortex-M处理器,perf_counter 专门提供了另外一个关键字 ISR() 来实现对目标中断处理程序最大栈消耗的测量。它的语法原形是:

ISR(<中断处理程序名称>, <预测的最大栈用量>){    /* 中断处理程序的代码 */}
extern uint32_t g_w<中断处理程序名称>_StackUsage;

其中:

  • 中断处理程序名称就是大家所熟悉的,诸如 SysTick_Handler, Hardfault_Handler 这类在向量表中定义的处理程序名称;
  • ISR() 不需要用户提供栈底地址,而是直接让用户提供“预测的最大栈用量”;
  • ISR() 不会将测量结果打印出来,而是保存在一个根据目标中断处理程序名称而明明的全局变量中——这意味着,我们随时可以读取该变量来获取结果。

举个栗子戴着奇怪帽子的卡通人物,一脸坏笑配文“举个栗子”表情包图片gif动图- 求表情网,斗图从此不求人!

假设我们的SysTick_Handler原本写成如下的样子:

static volatile uint32_t s_wSystemMS = 0;
...
void SysTick_Handler(void){    s_wSystemMS++;    communication_insert_1ms_isr();    ...}

那么,我们只需要改写成如下的形式:

static volatile uint32_t s_wSystemMS = 0;...ISR(SysTick_Handler, 512){    s_wSystemMS++;    communication_insert_1ms_isr();    ...}

此时,我们可以通过全局变量 g_wSysTick_Handler_StackUsage来观察 SysTick_Handler的最大栈消耗:

是不是特别简单?

值得强调的是,ISR() 的栈统计功能平时是“默认关闭”的——不会消耗任何时间或者空间上的资源——因此,我推荐将所有的中断处理程序都加上 ISR()。当且仅当我们在工程配置中定义了宏 __PERFC_STACK_CHECK_IN_ISR__,ISR() 的统计功能才会被开启。

当我们通过 ISR() 测量获得了工程中所有实际用到的中断处理程序的最大栈消耗以后,需要记住以下几点:

  • Cortex-M架构下,并非每一个优先级都是能抢占的优先级。需要厂商的手册来知道具体有多少可抢占的优先级。
  • 对每一个可抢占的优先级来说,旗下可能存在多个不可抢占的子优先级——这里,我们要一视同仁,按照他们中最大的栈消耗作为当前可抢占优先级的最大栈消耗。
  • 对运行在Thread模式下的任务来说,当且仅当它也是用同一个MSP作为栈顶指针时,才需要把任务的最大栈消耗与中断处理程序的极限嵌套消耗相加。如果Thread模式使用了不同的栈顶指针,比如PSP(或者在Armv8-M下,使用了不同安全域的MSP),那么只需要给Thread模式在最大栈消耗的基础上预留Stack Frame的大小作为余量即可。
  • 在Armv8-M架构下,安全域和非安全与内的中断处理程序要分开计算。如果觉得复杂,就记住一个关键点:使用同一个MSP的Thread和异常处理程序放在一起计算。

至此,借助 perf_counter 所提供的三个关键字 __stack_usage__, __stack_usage_max__ 和 针对Cortex-M处理器的 ISR(),我们已经可以在产品的开发阶段,借助对中断模型的理解,从工程数学模型的角度出发计算出系统中每一个栈的理论最大消耗——通过适当的配置和调整,我们就能拍胸脯说:只要代码执行正确,就不会出现因为极端中断嵌套而导致的栈溢出了!

【如何部署 perf_counter】


你可以打开MDK自带的Pack Installer,在GorgonMeducer下找到perf_counter:

或者,你也从下面的链接找到最新版本的cmsis-pack进行下载:

也可以向公众号发送 perf_counter 获得最新的网盘链接。

下载后,双击pack文件进行无脑安装

:

在确认了开原许可Apache 2.0的条款后,一路Next,直到单击Finish,完成整个安装过程。


一般来说,部署会非常顺利,但如果出现了安装错误,比如下面这种:

图片


则很可能是您所使用的MDK版本太低导致的——是时候更新下MDK啦。关注【裸机思维】公众号后发送关键字"MDK",即可获得最新的MDK网盘链接。

当我们想在任何已有工程中部署perf_counter时,只需要单击MDK工具栏上如下图所示的图标:

图片

打开RTE配置窗口:

我们会注意到,在列表的末端出现了一个Utilities条目,依次展开后勾选Performance下的perf_counter——默认情况下系统会自动选择以库的形式来实现模块的部署——这也是我吐血推荐的方式,因为它省去了不必要的编译麻烦。

单击OK按钮后,我们会发现Utilities已经被加入到了工程管理器中:

图片



看过我

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭