¿Definir el montón y el tamaño de pila para un microcontrolador ARM Cortex-M4?

11

He estado trabajando de vez en cuando en pequeños proyectos de sistemas integrados. Algunos de estos proyectos utilizaron un procesador base ARM Cortex-M4. En la carpeta del proyecto hay un archivo startup.s . Dentro de ese archivo, noté las siguientes dos líneas de comando.

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

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

¿Cómo se define el tamaño del montón y la pila para un microcontrolador? ¿Hay alguna información específica en la hoja de datos para guiar a llegar al valor correcto? Si es así, ¿qué se debe buscar en la hoja de datos?


Referencias

Mahendra Gunawardena
fuente

Respuestas:

12

La pila y el montón son conceptos de software, no conceptos de hardware. Lo que proporciona el hardware es memoria. La definición de zonas de memoria, una de las cuales se llama "pila" y una de las cuales se llama "montón", es una elección de su programa.

El hardware ayuda con las pilas. La mayoría de las arquitecturas tienen un registro dedicado llamado puntero de pila. Su uso previsto es que cuando el programa realiza una llamada a la función, los parámetros de la función y la dirección de retorno se envían a la pila, y se abren cuando la función finaliza y vuelve a su llamador. Empujar sobre la pila significa escribir en la dirección dada por el puntero de la pila y disminuir el puntero de la pila en consecuencia (o aumentar, dependiendo de en qué dirección crezca la pila). Hacer estallar significa incrementar (o disminuir) el puntero de la pila; la dirección de retorno se lee de la dirección dada por el puntero de la pila.

Algunas arquitecturas (aunque no ARM) tienen una instrucción de llamada de subrutina que combina un salto con la escritura en la dirección dada por el puntero de pila, y una instrucción de retorno de subrutina que combina la lectura de la dirección dada por el puntero de pila y saltar a esta dirección. En ARM, la dirección de guardar y restaurar se realiza en el registro LR, las instrucciones de llamada y devolución no utilizan el puntero de la pila. Sin embargo, hay instrucciones para facilitar la escritura o lectura de múltiples registros en la dirección dada por el puntero de la pila, para empujar y hacer estallar los argumentos de la función.

Para elegir el tamaño del montón y la pila, la única información relevante del hardware es la cantidad de memoria total que tiene. A continuación, elige según lo que quieras almacenar en la memoria (permitiendo código, datos estáticos y otros programas).

Un programa normalmente usaría estas constantes para inicializar algunos datos en la memoria que serán utilizados por el resto del código, como la dirección de la parte superior de la pila, tal vez un valor en algún lugar para verificar si hay desbordamientos de la pila, límites para el asignador del montón etc.

En el código que está viendo , la Stack_Sizeconstante se usa para reservar un bloque de memoria en el área de código (a través de una SPACEdirectiva en el ensamblaje ARM). La dirección superior de este bloque recibe la etiqueta __initial_spy se almacena en la tabla de vectores (el procesador usa esta entrada para configurar el SP después de un reinicio del software) y se exporta para su uso en otros archivos de origen. La Heap_Sizeconstante se usa de manera similar para reservar un bloque de memoria y las etiquetas a sus límites ( __heap_basey __heap_limit) se exportan para su uso en otros archivos de origen.

; 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- deja de ser malvado'
fuente
¿Sabes cómo se determinan esos valores 0x00200 y 0x000400
Mahendra Gunawardena
@MahendraGunawardena Depende de usted determinarlos, en función de lo que necesita su programa. La respuesta de Niall da algunos consejos.
Gilles 'SO- deja de ser malvado'
7

Los tamaños de la pila y el montón están definidos por su aplicación, no en ninguna parte de la hoja de datos del microcontrolador.

La pila

La pila se utiliza para almacenar los valores de las variables locales dentro de las funciones, los valores anteriores de los registros de la CPU utilizados para las variables locales (para que puedan restaurarse al salir de la función), la dirección del programa a la que debe regresar cuando abandone esas funciones, más algunos gastos generales para la gestión de la pila en sí.

Cuando desarrolle un sistema integrado, calcule la profundidad máxima de llamada que espera tener, sume los tamaños de todas las variables locales en las funciones de esa jerarquía, luego agregue algo de relleno para permitir la sobrecarga mencionada anteriormente, luego agregue un poco más para cualquier interrupción que pueda ocurrir durante la ejecución de su programa.

Un método de estimación alternativo (donde la RAM no está restringida) es asignar mucho más espacio de pila del que necesitará, llenar la pila con un valor centinela y luego monitorear cuánto usa realmente durante la ejecución. He visto versiones de depuración de tiempos de ejecución de lenguaje C que lo harán automáticamente. Luego, cuando haya terminado de desarrollar, puede reducir el tamaño de la pila si lo desea.

El montón

Calcular el tamaño del montón que necesita puede ser más complicado. El montón se utiliza para las variables asignados dinámicamente, por lo que si se utiliza malloc()y free()en un programa de lenguaje C, o newy deleteen C ++, que es donde viven esas variables.

Sin embargo, especialmente en C ++, puede haber alguna asignación de memoria dinámica oculta. Por ejemplo, si tiene objetos asignados estáticamente, el lenguaje requiere que se llame a sus destructores cuando el programa salga. Soy consciente de al menos un tiempo de ejecución donde las direcciones de los destructores se almacenan en una lista vinculada asignada dinámicamente.

Entonces, para estimar el tamaño del montón que necesita, observe toda la asignación de memoria dinámica en cada ruta a través de su árbol de llamadas, calcule el máximo y agregue algo de relleno. El tiempo de ejecución del lenguaje puede proporcionar diagnósticos que puede usar para monitorear el uso total del montón, la fragmentación, etc.

Niall C.
fuente
Gracias por la respuesta, me gusta cómo determinar el número específico como 0x00400 y así sucesivamente
Mahendra Gunawardena
5

Además de las otras respuestas, me gustaría agregar que al dividir la RAM entre la pila y el espacio de almacenamiento dinámico, también debe tener en cuenta el espacio para datos estáticos no constantes (por ejemplo, archivos globales, estadísticas de funciones y todo el programa). globales desde una perspectiva C, y probablemente otros para C ++).

Cómo funciona la asignación de pila / montón

Vale la pena señalar que el archivo de ensamblaje de inicio es una forma de definir la región; la cadena de herramientas (tanto su entorno de compilación como el entorno de tiempo de ejecución) se preocupan principalmente por los símbolos que definen el inicio del espacio de apilamiento (utilizado para almacenar el puntero de pila inicial en la Tabla de vectores) y el inicio y el final del espacio de almacenamiento dinámico (utilizado por la dinámica asignador de memoria, generalmente proporcionado por su libc)

En el ejemplo de OP, solo se definen 2 símbolos, un tamaño de pila en 1 kB y un tamaño de montón en 0 B. Estos valores se utilizan en otros lugares para producir realmente los espacios de pila y montón

En el ejemplo de @Gilles, los tamaños se definen y usan en el archivo de ensamblaje para establecer un espacio de pila comenzando donde sea y durando el tamaño, identificado por el símbolo Stack_Mem y establece una etiqueta __initial_sp al final. Del mismo modo para el montón, donde el espacio es el símbolo Heap_Mem (0.5 kB de tamaño), pero con etiquetas al principio y al final (__heap_base y __heap_limit).

Estos son procesados ​​por el enlazador, que no asignará nada dentro del espacio de pila y espacio de almacenamiento dinámico porque esa memoria está ocupada (por los símbolos Stack_Mem y Heap_Mem), pero puede colocar esas memorias y todos los globales donde sea necesario. Las etiquetas terminan siendo símbolos sin longitud en las direcciones dadas. __Initial_sp se usa directamente para la tabla de vectores en el momento del enlace, y __heap_base y __heap_limit por su código de tiempo de ejecución. El vinculador asigna las direcciones reales de los símbolos en función de dónde las colocó.

Como mencioné anteriormente, estos símbolos en realidad no tienen que venir de un archivo startup.s. Pueden provenir de la configuración de su enlazador (Scatter Load file en Keil, linkerscript en GNU), y en aquellos puede tener un control más fino sobre la ubicación. Por ejemplo, puede forzar que la pila esté al principio o al final de la RAM, o mantener sus glóbulos alejados del montón, o lo que quiera. Incluso puede especificar que HEAP o STACK solo ocupen la RAM restante después de colocar los globals. Sin embargo, tenga en cuenta que debe tener cuidado de que al agregar más variables estáticas que disminuya su otra memoria.

Sin embargo, cada cadena de herramientas es diferente, y la forma de escribir el archivo de configuración y los símbolos que usará su asignador de memoria dinámica deberán provenir de la documentación de su entorno particular.

Tamaño de pila

En cuanto a cómo determinar el tamaño de la pila, muchas cadenas de herramientas pueden darle una profundidad máxima de la pila analizando los árboles de llamadas de función de su programa, SI no usa punteros de recursión o función. Si los usa, estimando un tamaño de pila y llenándolo previamente con valores cardinales (quizás a través de la función de entrada antes de main) y luego verificando después de que su programa se haya ejecutado durante un tiempo donde estaba la profundidad máxima (que es donde están los valores cardinales final). Si ha ejercido completamente su programa hasta sus límites, sabrá con bastante precisión si puede reducir la pila o, si su programa falla o no quedan valores cardinales, necesita aumentar la pila e intentar nuevamente.

Tamaño del montón

Determinar el tamaño del almacenamiento dinámico depende un poco más de la aplicación. Si solo realiza una asignación dinámica durante el inicio, puede agregar el espacio requerido en su código de inicio (más algunos gastos generales para la administración de la memoria). Si tiene acceso a la fuente de su administrador de memoria, puede saber exactamente cuál es la sobrecarga y posiblemente incluso escribir código para recorrer la memoria y darle información de uso. Para las aplicaciones que necesitan memoria dinámica de tiempo de ejecución (p. Ej., Asignando buffers para tramas de ethernet entrantes), lo mejor que puedo sugerir es que afine cuidadosamente su tamaño de pila y le dé al montón todo lo que queda después de la pila y las estadísticas.

Nota final (RTOS)

La pregunta de OP fue etiquetada para metal desnudo, pero quiero agregar una nota para RTOS. A menudo (¿siempre?) A cada tarea / proceso / hilo (simplemente escribiré la tarea aquí por simplicidad) se le asignará un tamaño de pila cuando se crea la tarea, y además de las pilas de tareas, es probable que haya un sistema operativo pequeño stack (usado para interrupciones y tal)

Las estructuras de contabilidad de tareas y las pilas deben asignarse desde algún lugar, y esto a menudo será desde el espacio de almacenamiento dinámico general de su aplicación. En estos casos, su tamaño inicial de pila a menudo no importará, porque el sistema operativo solo lo usará durante la inicialización. He visto, por ejemplo, especificar TODO el espacio restante durante la vinculación se asignará al HEAP y colocar el puntero de la pila inicial al final del montón para crecer en el montón, sabiendo que el sistema operativo asignará comenzando desde el principio del montón y asignará la pila del sistema operativo justo antes de abandonar la pila initial_sp. Luego, todo el espacio se utiliza para asignar pilas de tareas y otra memoria asignada dinámicamente.

John O'M.
fuente