Perfil de memória no STM32 e outros microcontroladores: análise de tamanho de pilha estática

Olá Habr!

No último artigo , eu mencionei e perguntei nos comentários - ok, bem, usando o método científico do puxão, selecionamos o tamanho da pilha, parece que nada está caindo, mas podemos avaliar de forma mais confiável o que é igual e quem comeu tanto?

Respondemos brevemente: sim, mas não.

Não, usando os métodos de análise estática, é impossível medir com precisão o tamanho da pilha necessária ao programa - mas, no entanto, esses métodos podem ser úteis.

A resposta é um pouco mais longa - sob o corte.

Como é amplamente conhecido por um círculo estreito de pessoas, o lugar na pilha é alocado, de fato, para variáveis ​​locais que a função que está executando atualmente usa - com exceção das variáveis ​​com o modificador estático, que são armazenadas na memória alocada estaticamente, na área bss, porque elas devem salvar seus significados entre chamadas de função.

Quando a função é executada, o compilador adiciona espaço na pilha para as variáveis ​​necessárias e, após a conclusão, libera esse espaço de volta. Parece que tudo é simples, mas - e isso é muito ousado, mas - temos vários problemas:

  1. funções chamam dentro de outras funções que também precisam de uma pilha
  2. às vezes, funções chamam outras funções não por referência direta, mas por um ponteiro para uma função
  3. em princípio, é possível - embora deva ser evitado por todos os meios - uma chamada de função recursiva quando A chama B, B chama C e C dentro de si chama A novamente
  4. a qualquer momento, pode ocorrer uma interrupção, cujo manipulador é a mesma função que deseja sua própria parte da pilha
  5. se você tiver uma hierarquia de interrupções, outra interrupção pode ocorrer dentro da interrupção!

Sem ambiguidade, as chamadas de função recursivas devem ser excluídas desta lista, porque a presença delas é uma desculpa para não considerar o tamanho da pilha, mas para expressar sua opinião ao autor do código. Tudo o resto, infelizmente, não pode ser riscado no caso geral (embora em particular possa haver nuances: por exemplo, todas as interrupções para você podem ter a mesma prioridade por design, por exemplo, como no RIOT OS, e não haverá interrupções aninhadas).

Agora imagine uma pintura a óleo:

  • A função A, que consome 100 bytes na pilha, chama a função B, que precisa de 50 bytes
  • no momento da execução de B, a própria A, obviamente, ainda não terminou, portanto, seus 100 bytes não são liberados, então já temos 150 bytes na pilha
  • A função B chama a função C e faz isso de acordo com um ponteiro que, dependendo da lógica do programa, pode apontar para meia dúzia de funções diferentes consumindo de 5 a 50 bytes de pilha
  • no tempo de execução C, ocorre uma interrupção com um manipulador pesado executando relativamente longo e consumindo 20 bytes de pilha
  • durante o processamento da interrupção, ocorre outra interrupção de prioridade mais alta, cujo manipulador deseja 10 bytes de pilha

Nesse design bonito, com uma coincidência particularmente bem-sucedida de todas as circunstâncias, você terá pelo menos cinco funções simultaneamente ativas - A, B, C e dois manipuladores de interrupção. Além disso, um deles não possui uma constante de consumo de pilha, porque pode ser apenas uma função diferente em passagens diferentes e, para entender a possibilidade ou impossibilidade de interromper um ao outro, você deve pelo menos saber se interrompeu de alguma forma com prioridades diferentes. e, no máximo - para entender se eles podem se sobrepor.

Obviamente, para qualquer analisador automático de código estático, essa tarefa é extremamente próxima da esmagadora e pode ser realizada apenas em uma aproximação aproximada da estimativa superior:

  • somar as pilhas de todos os manipuladores de interrupção
  • resumir pilhas de funções que são executadas no mesmo ramo de código
  • tente encontrar todos os ponteiros para funções e suas chamadas e use o tamanho máximo da pilha entre as funções que esses ponteiros apontam como tamanho da pilha

Na maioria dos casos, você terá, por um lado, uma estimativa muito alta e, por outro, a chance de pular algumas chamadas de funções particularmente complicadas através de ponteiros.

Portanto, no caso geral, podemos simplesmente dizer: esta tarefa não é resolvida automaticamente . Uma solução manual - uma pessoa que conhece a lógica deste programa - exige cavar alguns números.

No entanto, uma estimativa estática do tamanho da pilha pode ser muito útil para otimizar o software - pelo menos com o simples propósito de entender quem está comendo quanto, e não muito.

Existem duas ferramentas extremamente úteis para isso na cadeia de ferramentas GNU / gcc:

  • flag -fstack-use
  • utilitário cflow

Se você adicionar -fstack-use aos sinalizadores gcc (por exemplo, ao Makefile na linha com CFLAGS), para cada arquivo compilado% filename% .c, o compilador criará o arquivo% filename% .su, dentro do qual haverá texto simples e claro.

Tome, por exemplo, target.su para este sapato gigantesco :

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 

Aqui vemos o consumo real da pilha para cada função aparecendo nela, da qual podemos tirar algumas conclusões para nós mesmos - bem, por exemplo, o que vale a pena tentar otimizar antes de tudo, se tivermos falta de RAM.

Ao mesmo tempo, atenção, esse arquivo não fornece informações precisas sobre o consumo real da pilha para funções das quais outras funções são chamadas !

Para entender o consumo total, precisamos construir uma árvore de chamadas e resumir as pilhas de todas as funções incluídas em cada uma de suas ramificações. Isso pode ser feito, por exemplo, com o utilitário GNU cflow , configurando-o em um ou mais arquivos.

O escape aqui temos uma ordem de magnitude mais pesada, darei apenas parte dela para o mesmo objetivo.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() 

E isso não é nem metade da árvore.

Para entender o consumo real da pilha, precisamos pegar o consumo para cada uma das funções mencionadas nela e somar esses valores para cada uma das ramificações.

E, embora ainda não levemos em consideração as chamadas de função por ponteiros e interrupções, incl. aninhado (e especificamente neste código, eles podem ser aninhados).

Como você pode imaginar, fazer isso toda vez que você altera o código é, para dizer o mínimo, difícil - é por isso que ninguém costuma fazer.

No entanto, é necessário entender os princípios do preenchimento de pilha - isso pode levar a certas restrições no código do projeto, aumentando sua confiabilidade do ponto de vista de impedir o estouro de pilha (por exemplo, proibição de interrupções aninhadas ou chamadas de função por ponteiros) e especificamente o uso de pilha pode ajuda na otimização de código em sistemas com falta de RAM.

Source: https://habr.com/ru/post/pt443544/


All Articles