Definindo o tamanho da pilha e da pilha de um microcontrolador ARM Cortex-M4?

11

Tenho trabalhado dentro e fora de pequenos projetos de sistemas embarcados. Alguns desses projetos usaram um processador base ARM Cortex-M4. Na pasta do projeto, há um arquivo startup.s . Dentro desse arquivo, observei as duas linhas de comando a seguir.

;******************************************************************************
;
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Stack   EQU     0x00000400

;******************************************************************************
;
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Heap    EQU     0x00000000

Como se define o tamanho da pilha e pilha para um microcontrolador? Existe alguma informação específica na folha de dados para orientar a obtenção do valor correto? Se sim, o que procurar na folha de dados?


Referências:

Mahendra Gunawardena
fonte

Respostas:

12

Pilha e pilha são conceitos de software, não conceitos de hardware. O que o hardware fornece é memória. Definir zonas de memória, uma das quais é chamada de "pilha" e outra é chamada de "pilha", é uma opção do seu programa.

O hardware ajuda nas pilhas. A maioria das arquiteturas possui um registro dedicado chamado ponteiro da pilha. Seu uso pretendido é que, quando o programa faz uma chamada de função, os parâmetros da função e o endereço de retorno são enviados para a pilha e são exibidos quando a função termina e retorna ao chamador. Pressionar a pilha significa escrever no endereço fornecido pelo ponteiro da pilha e diminuir o ponteiro da pilha de acordo (ou incrementar, dependendo da direção em que a pilha cresce). Estalar significa aumentar (ou diminuir) o ponteiro da pilha; o endereço de retorno é lido a partir do endereço fornecido pelo ponteiro da pilha.

Algumas arquiteturas (embora não o ARM) possuem uma instrução de chamada de sub-rotina que combina um salto com a gravação no endereço fornecido pelo ponteiro da pilha e uma instrução de retorno de sub-rotina que combina a leitura do endereço fornecido pelo ponteiro da pilha e o salto para esse endereço. No ARM, o salvamento e a restauração de endereços são feitos no registro LR, as instruções de chamada e retorno não usam o ponteiro da pilha. No entanto, existem instruções para facilitar a gravação ou a leitura de vários registros no endereço fornecido pelo ponteiro da pilha, para pressionar e exibir argumentos da função.

Para escolher o tamanho da pilha e da pilha, a única informação relevante do hardware é a quantidade total de memória que você possui. Você faz sua escolha, dependendo do que deseja armazenar na memória (permitindo código, dados estáticos e outros programas).

Um programa normalmente usa essas constantes para inicializar alguns dados na memória que serão usados ​​pelo restante do código, como o endereço da parte superior da pilha, talvez um valor em algum lugar para verificar se há excesso de pilha, limites para o alocador de heap etc.

No código que você está visualizando , a Stack_Sizeconstante é usada para reservar um bloco de memória na área de código (por meio de uma SPACEdiretiva no assembly ARM). O endereço superior desse bloco recebe o rótulo __initial_spe é armazenado na tabela de vetores (o processador usa essa entrada para definir o SP após uma redefinição do software), além de ser exportado para uso em outros arquivos de origem. A Heap_Sizeconstante é similarmente usada para reservar um bloco de memória e os rótulos para seus limites ( __heap_basee __heap_limit) são exportados para uso em outros arquivos de origem.

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

…
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler

…

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
Gilles 'SO- parar de ser mau'
fonte
Você sabe como esses valores 0x00200 e 0x000400 são determinados
Mahendra Gunawardena
@MahendraGunawardena Cabe a você determiná-los, com base no que o seu programa precisa. A resposta de Niall dá algumas dicas.
Gilles 'SO- stop be evil' em
7

Os tamanhos da pilha e da pilha são definidos pelo seu aplicativo, e não em nenhum lugar da folha de dados do microcontrolador.

A pilha

A pilha é usada para armazenar os valores das variáveis ​​locais dentro das funções, os valores anteriores dos registradores da CPU usados ​​para as variáveis ​​locais (para que possam ser restaurados na saída da função), o endereço do programa para retornar ao sair dessas funções, além de algumas despesas gerais para o gerenciamento da própria pilha.

Ao desenvolver um sistema incorporado, você estima a profundidade máxima de chamadas que espera ter, soma os tamanhos de todas as variáveis ​​locais nas funções nessa hierarquia e, em seguida, adiciona algum preenchimento para permitir a sobrecarga mencionada acima e depois adiciona um pouco mais para quaisquer interrupções que possam ocorrer durante a execução do seu programa.

Um método de estimativa alternativo (onde a RAM não é restrita) é alocar muito mais espaço da pilha do que você precisará, preencher a pilha com um valor sentinela e monitorar o quanto você realmente usa durante a execução. Vi versões de depuração dos tempos de execução da linguagem C que farão isso automaticamente para você. Então, quando você terminar de desenvolver, poderá reduzir o tamanho da pilha, se desejar.

A pilha

Calcular o tamanho da pilha de que você precisa pode ser mais complicado. A pilha é usada para variáveis dinamicamente alocados, por isso, se você usa malloc()e free()em um programa em linguagem C, ou newe deleteem C ++, que é onde essas variáveis viver.

No entanto, especialmente em C ++, pode haver alguma alocação de memória dinâmica oculta em andamento. Por exemplo, se você tiver objetos alocados estaticamente, a linguagem exigirá que seus destruidores sejam chamados quando o programa sair. Estou ciente de pelo menos um tempo de execução em que os endereços dos destruidores são armazenados em uma lista vinculada alocada dinamicamente.

Portanto, para estimar o tamanho do heap necessário, observe toda a alocação dinâmica de memória em cada caminho na árvore de chamadas, calcule o máximo e adicione um pouco de preenchimento. O tempo de execução do idioma pode fornecer diagnósticos que você pode usar para monitorar o uso total de heap, fragmentação etc.

Niall C.
fonte
Obrigado pela resposta, eu gosto de como determinar o número específico, como 0x00400 e assim por diante
Mahendra Gunawardena
5

Além das outras respostas, gostaria de acrescentar que, ao acumular RAM entre a pilha e o espaço de pilha, você também precisa considerar o espaço para dados estáticos não constantes (por exemplo, arquivos globais, estática de funções e todo o programa globais da perspectiva C e provavelmente outros para C ++).

Como a alocação de pilha / heap funciona

Vale notar que o arquivo de montagem de inicialização é uma maneira de definir a região; a cadeia de ferramentas (seu ambiente de construção e ambiente de tempo de execução) se preocupa principalmente com os símbolos que definem o início do espaço de pilha (usado para armazenar o ponteiro inicial da pilha na Tabela de vetores) e o início e o fim do espaço de heap (usado pela dinâmica alocador de memória, normalmente fornecido pela libc)

No exemplo do OP, apenas 2 símbolos são definidos, um tamanho de pilha em 1kiB e um tamanho de heap em 0B. Esses valores são usados ​​em outros lugares para realmente produzir os espaços de pilha e heap

No exemplo do @Gilles, os tamanhos são definidos e usados no arquivo de montagem para definir um espaço de pilha iniciando em qualquer lugar e com duração do tamanho, identificado pelo símbolo Stack_Mem e define um rótulo __initial_sp no final. Da mesma forma para a pilha, onde o espaço é o símbolo Heap_Mem (tamanho de 0,5 kB), mas com rótulos no início e no fim (__heap_base e __heap_limit).

Eles são processados ​​pelo vinculador, que não alocará nada no espaço de pilha e no espaço de pilha porque essa memória está ocupada (pelos símbolos Stack_Mem e Heap_Mem), mas pode colocar essas memórias e todos os globais onde for necessário. As etiquetas acabam sendo símbolos sem tamanho nos endereços fornecidos. O __initial_sp é usado diretamente para a tabela de vetores no momento do link, e o __heap_base e __heap_limit pelo seu código de tempo de execução. Os endereços reais dos símbolos são atribuídos pelo vinculador com base em onde os colocou.

Como mencionei acima, esses símbolos não precisam vir de um arquivo startup.s. Eles podem vir da configuração do vinculador (arquivo Scatter Load no Keil, linkerscript no GNU), e naqueles você pode ter um controle mais refinado sobre o posicionamento. Por exemplo, você pode forçar a pilha a estar no início ou no final da RAM, ou manter seus globais afastados da pilha, ou o que quiser. Você pode até especificar que o HEAP ou STACK apenas ocupe a RAM restante após a colocação dos globais. OBSERVE, porém, que você deve ter cuidado para adicionar mais variáveis ​​estáticas que sua outra memória diminuirá.

No entanto, cada cadeia de ferramentas é diferente, e como gravar o arquivo de configuração e quais símbolos o seu alocador de memória dinâmico usará terão de vir da documentação do seu ambiente específico.

Dimensionamento da pilha

Quanto à forma de determinar o tamanho da pilha, muitas cadeias de ferramentas podem fornecer uma profundidade máxima da pilha analisando as árvores de chamadas de funções do seu programa, SE você não usa ponteiros de função ou recursão. Se você usá-los, estimando o tamanho de uma pilha e preenchendo-o previamente com valores cardinais (talvez através da função de entrada antes de main) e depois verificando depois que o programa foi executado por um tempo onde estava a profundidade máxima (onde estão os valores cardinais fim). Se você tiver exercitado seu programa totalmente até o limite, saberá com bastante precisão se pode reduzir a pilha ou, se o programa falhar ou se nenhum valor cardinal for deixado, é necessário aumentar a pilha e tentar novamente.

Dimensionamento da pilha

Determinar o tamanho do heap depende um pouco mais do aplicativo. Se você apenas fizer alocação dinâmica durante a inicialização, poderá adicionar o espaço necessário no seu código de inicialização (mais algumas despesas gerais para o gerenciamento de memória). Se você tiver acesso à fonte do seu gerenciador de memória, poderá saber exatamente qual é a sobrecarga e, possivelmente, até escrever código para percorrer a memória e fornecer informações de uso. Para aplicativos que precisam de memória de tempo de execução dinâmico (por exemplo, alocar buffers para quadros Ethernet de entrada), o melhor que posso sugerir é aprimorar com cuidado o tamanho da sua pilha e fornecer ao Heap tudo o que resta depois da pilha e das estáticas.

Nota final (RTOS)

A pergunta do OP foi marcada para bare-metal, mas quero adicionar uma observação para RTOSes. Freqüentemente (sempre?) Cada tarefa / processo / encadeamento (apenas escreverei aqui a tarefa por simplicidade) receberá um tamanho de pilha quando a tarefa for criada, além de pilhas de tarefas, provavelmente haverá um pequeno SO pilha (usada para interrupções e afins)

As estruturas de contabilidade de tarefas e as pilhas precisam ser alocadas de algum lugar, e isso geralmente ocorre no espaço de heap geral do seu aplicativo. Nesses casos, o tamanho inicial da pilha geralmente não importa, porque o sistema operacional o utilizará apenas durante a inicialização. Vi, por exemplo, especificar que TODO o espaço restante durante a vinculação seja alocado ao HEAP e colocar o ponteiro de pilha inicial no final do heap para crescer no heap, sabendo que o SO será alocado a partir do início do heap e alocará a pilha do SO pouco antes de abandonar a pilha initial_sp. Todo o espaço é usado para alocar pilhas de tarefas e outras memórias alocadas dinamicamente.

John O'M.
fonte