STM32和其他微控制器上的存储器性能分析:静态堆栈大小分析

哈Ha!

上一篇文章中 ,我自己提到了它,并在评论中提出了问题-好的,使用科学的戳方法,我们选择了堆栈的大小,似乎没有东西掉落,但是我们可以以某种方式更可靠地评估它等于什么以及谁吃了这么多东西吗?

我们简短地回答:是的,但是没有。

不可以,使用静态分析方法无法准确测量程序所需的堆栈大小,但是,这些方法可以派上用场。

答案要长一些-削减了。

正如一小撮人所众所周知的,实际上,栈上的位置是为当前执行函数使用的局部变量分配的-带有static修饰符的变量除外,这些变量存储在bss区域的静态分配的内存中,因为它们必须保存它们在函数调用之间的含义。

执行该函数时,编译器会在堆栈上为其所需的变量添加空间,并在完成后释放此空间。 看起来一切都很简单,但是-这非常大胆- 但是我们有几个问题:

  1. 函数调用其他也需要堆栈的函数
  2. 有时函数不是通过直接引用而是通过指向函数的指针来调用其他函数
  3. 原则上,尽管A一定要避免,但也可以-尽管A调用了B,B调用了C,而C本身内部又调用了A时进行了递归函数调用
  4. 在任何时候都可能发生中断,其处理程序与需要其自身堆栈的函数相同
  5. 如果您具有中断层次结构,则该中断内部可能会发生另一个中断!

毫无疑问,应该从此列表中删除递归函数调用,因为它们的存在是没有考虑堆栈大小的借口,而是要向代码作者表达您的意见。 遗憾的是,其他所有情况在一般情况下都无法消除(尽管特别可能存在细微差别:例如,您的所有中断在设计上都可以具有相同的优先级,例如在RIOT OS中,并且不会有嵌套的中断)。

现在想象一下一幅油画:

  • 函数A,在堆栈上占用100个字节,调用函数B,该函数需要50个字节
  • 在执行B时,A本身显然尚未完成,因此尚未释放其100个字节,因此我们在堆栈上已经有150个字节
  • 函数B调用函数C,它根据一个指针执行此操作,根据程序逻辑,该指针可以指向6个不同的函数,这些函数占用5到50个字节的堆栈
  • 在运行时C,一个重的处理程序运行相对较长的时间并占用20字节的堆栈时发生中断
  • 在中断处理期间,发生另一个更高优先级的中断,该中断的处理程序需要10个字节的堆栈

在这种精美的设计中,在所有情况下都取得了特别成功的巧合,您将至少拥有五个同时活动的功能 -A,B,C和两个中断处理程序。 此外,它们中的一个没有栈消耗常量,因为它可能在不同的遍中只是一个不同的函数,并且要了解相互中断的可能性或不可能,您至少必须知道您是否完全拥有具有不同优先级的中断,并且最大程度地了解它们是否可以彼此重叠。

显然,对于任何自动静态代码分析器而言,此任务几乎都是压倒性的,并且只能在近似上限的情况下才能执行:

  • 对所有中断处理程序的堆栈求和
  • 总结在同一代码分支中运行的函数堆栈
  • 尝试找到所有指向函数及其调用的指针,并将这些指针指向的函数中的最大堆栈大小作为堆栈大小

在大多数情况下,您一方面会得到很高的估计,另一方面,您有机会跳过通过指针进行的一些特别棘手的函数调用。

因此,在一般情况下,我们可以简单地说: 此任务不会自动解决 。 手动解决方案-知道该程序逻辑的人-需要挖掘很多数字。

不过,对堆栈大小的静态估计对于优化软件非常有用-至少出于简单的目的,即了解谁吃了很多而不是太多。

GNU / gcc工具链中有两个非常有用的工具:

  • 标志-fstack-usage
  • cflow实用程序

如果将-fstack-usage添加到gcc标志(例如,添加到CFLAGS行中的Makefile),则对于每个已编译文件%filename%.c,编译器将创建文件%filename%.su,其中将包含简单明了的文本。

这个巨大的鞋垫为例,例如target.su

target.c:159:13:save_settings 8 static target.c:172:13:disable_power 8 static target.c:291:13:adc_measure_vdda 32 static target.c:255:13:adc_measure_current 24 static target.c:76:6:cpu_setup 0 static target.c:81:6:clock_setup 8 static target.c:404:6:dma1_channel1_isr 24 static target.c:434:6:adc_comp_isr 40 static target.c:767:6:systick_activity 56 static target.c:1045:6:user_activity 104 static target.c:1215:6:gpio_setup 24 static target.c:1323:6:target_console_init 8 static target.c:1332:6:led_bit 8 static target.c:1362:6:led_num 8 static 

在这里,我们看到堆栈中每个函数的实际消耗量,可以据此为自己得出一些结论-举例来说,如果我们遇到RAM不足的情况,那么首先应该进行优化。

同时,请注意, 此文件实际上并未提供有关调用其他函数的函数实际消耗堆栈的准确信息

要了解总消耗量,我们需要构建一个调用树并总结其每个分支中包含的所有功能的堆栈。 例如,可以通过在一个或多个文件上设置GNU cflow实用程序来完成此操作。

在这里,我们将得到的权重要高一个数量级,对于相同的目标,我只给出其中的一部分。c:

 olegart@oleg-npc /mnt/c/Users/oleg/Documents/Git/dap42 (umdk-emb) $ cflow src/stm32f042/umdk-emb/target.c adc_comp_isr() <void adc_comp_isr (void) at src/stm32f042/umdk-emb/target.c:434>: TIM_CR1() ADC_DR() ADC_ISR() DMA_CCR() GPIO_BSRR() GPIO_BRR() ADC_TR1() ADC_TR1_HT_VAL() ADC_TR1_LT_VAL() TIM_CNT() DMA_CNDTR() DIV_ROUND_CLOSEST() NVIC_ICPR() clock_setup() <void clock_setup (void) at src/stm32f042/umdk-emb/target.c:81>: rcc_clock_setup_in_hsi48_out_48mhz() crs_autotrim_usb_enable() rcc_set_usbclk_source() dma1_channel1_isr() <void dma1_channel1_isr (void) at src/stm32f042/umdk-emb/target.c:404>: DIV_ROUND_CLOSEST() gpio_setup() <void gpio_setup (void) at src/stm32f042/umdk-emb/target.c:1215>: rcc_periph_clock_enable() button_setup() <void button_setup (void) at src/stm32f042/umdk-emb/target.c:1208>: gpio_mode_setup() gpio_set_output_options() gpio_mode_setup() gpio_set() gpio_clear() rcc_peripheral_enable_clock() tim2_setup() <void tim2_setup (void) at src/stm32f042/umdk-emb/target.c:1194>: rcc_periph_clock_enable() rcc_periph_reset_pulse() timer_set_mode() timer_set_period() timer_set_prescaler() timer_set_clock_division() timer_set_master_mode() adc_setup_common() <void adc_setup_common (void) at src/stm32f042/umdk-emb/target.c:198>: rcc_periph_clock_enable() gpio_mode_setup() adc_set_clk_source() adc_calibrate() adc_set_operation_mode() adc_disable_discontinuous_mode() adc_enable_external_trigger_regular() ADC_CFGR1_EXTSEL_VAL() adc_set_right_aligned() adc_disable_temperature_sensor() adc_disable_dma() adc_set_resolution() adc_disable_eoc_interrupt() nvic_set_priority() nvic_enable_irq() dma_channel_reset() dma_set_priority() dma_set_memory_size() dma_set_peripheral_size() dma_enable_memory_increment_mode() dma_disable_peripheral_increment_mode() dma_enable_transfer_complete_interrupt() dma_enable_half_transfer_interrupt() dma_set_read_from_peripheral() dma_set_peripheral_address() dma_set_memory_address() dma_enable_circular_mode() ADC_CFGR1() memcpy() console_reconfigure() tic33m_init() strlen() tic33m_display_string() 

甚至还不到一半。

要了解堆栈的实际消耗量,我们需要计算其中提到的每个函数的消耗量,并对每个分支的这些值求和。

虽然我们仍然不考虑指针和中断对函数的调用,包括。 嵌套(特别是在此代码中,它们可以嵌套)。

您可能会猜到,每次更改代码时都要这样做,说起来有点困难,这就是为什么没人通常这样做。

不过,有必要了解堆栈填充的原理-这可能会导致对项目代码的某些限制,从防止堆栈溢出的角度(例如,禁止嵌套中断或指针调用函数)的角度来看,这会增加其可靠性,特别是-fstack-usage可以极大地提高在缺少RAM的系统上帮助代码优化。

Source: https://habr.com/ru/post/zh-CN443544/


All Articles