"Con experiencia, viene un enfoque científico estándar para calcular el tamaño correcto de la pila: tomar un número aleatorio y esperar lo mejor".
- Jack Ganssle, "El arte de diseñar sistemas embebidos"Hola Habr!
Por extraño que parezca, en la gran mayoría de los "cebadores STM32" que he visto en particular y en los microcontroladores en general, generalmente no hay nada sobre la asignación de memoria, la colocación de la pila y, lo más importante, la prevención del desbordamiento de la memoria, como resultado de lo cual un área deshilacha a otra y todo colapsa, generalmente con efectos encantadores.
Esto se debe en parte a la simplicidad de los proyectos de capacitación que se llevan a cabo en placas de depuración con microcontroladores relativamente grasientos, que pueden faltar en la memoria al parpadear un LED; sin embargo, recientemente, incluso para los principiantes aficionados, las referencias a, por ejemplo, los controladores de tipo STM32F030F4P6 son cada vez más comunes. , fácil de instalar, vale un centavo, pero también con una unidad de memoria de kilobytes.
Dichos controladores le permiten hacer cosas bastante serias por usted mismo (bueno, aquí, por ejemplo, se
realizó una medición tan adecuada para nosotros en STM32F042K6T6 con 6 KB de RAM, de la que quedan un poco más de 100 bytes libres), pero cuando se trata de memoria, necesita una cierta cantidad de memoria pulcritud
Quiero hablar sobre esta precisión. El artículo será breve, los profesionales no aprenderán nada nuevo, pero para los principiantes este conocimiento es muy recomendable.
En un proyecto típico en un microcontrolador basado en un núcleo Cortex-M, RAM tiene una división condicional en cuatro secciones:
- datos: datos inicializados por un valor específico
- bss: datos inicializados a cero
- heap - heap (área dinámica desde la cual se asigna memoria explícitamente usando malloc)
- stack: la pila (la región dinámica desde la cual el compilador asigna la memoria implícitamente)
El área noinit también puede ocurrir ocasionalmente (variables no inicializadas; son convenientes porque retienen el valor entre reinicios), incluso con menos frecuencia, algunas otras áreas asignadas para tareas específicas.
Están ubicados en la memoria física de una manera bastante específica: el hecho es que la pila en los microcontroladores en los núcleos ARM crece de arriba a abajo. Por lo tanto, se encuentra separado de los bloques de memoria restantes, al final de la RAM:

De manera predeterminada, su dirección suele ser igual a la última dirección RAM, y a partir de ahí disminuye a medida que crece, y una característica extremadamente desagradable de la pila crece: puede alcanzar bss y reescribir su parte superior, y no lo sabrá de ninguna manera explícita.
Áreas de memoria estática y dinámica
Toda la memoria se divide en dos categorías: estáticamente asignadas, es decir memoria, cuya cantidad total es obvia del texto del programa y no depende del orden de su ejecución, y se asigna dinámicamente, cuyo volumen requerido depende del progreso del programa.
Este último incluye un montón (del cual tomamos fragmentos usando malloc y regresamos usando gratis) y una pila que crece y se contrae por sí sola.
En términos generales, se
desaconseja usar malloc en microcontroladores a menos que sepa exactamente lo que está haciendo. El principal problema que traen es la fragmentación de la memoria: si asigna 10 unidades de 10 bytes cada una y luego se libera cada segundo, no obtendrá 50 bytes libres. Obtendrá 5 piezas gratuitas de 10 bytes cada una.
Además, en la etapa de compilación del programa, el compilador no podrá determinar automáticamente cuánta memoria requerirá su malloc (especialmente teniendo en cuenta la fragmentación, que depende no solo del tamaño de las piezas solicitadas, sino de la secuencia de su asignación y lanzamiento), y por lo tanto no podrá advertirle si al final no hay suficiente memoria.
Existen métodos para solucionar este problema: implementaciones especiales de malloc que funcionan dentro de un área asignada estáticamente, y no toda la RAM, uso cuidadoso de malloc teniendo en cuenta la posible fragmentación en el nivel lógico del programa, etc. - pero en general
malloc es mejor no tocar .
Todas las áreas de memoria con límites y direcciones se registran en un archivo con la extensión .LD, en la que se orienta el vinculador al crear el proyecto.
Memoria asignada estáticamente
Entonces, desde la memoria asignada estáticamente, tenemos dos áreas: bss y datos, que difieren solo formalmente. Cuando se inicializa el sistema, el bloque de datos se copia de la memoria flash, donde se almacenan los valores de inicialización necesarios, el bloque bss simplemente se llena con ceros (al menos llenarlo con ceros se considera una buena forma).
Ambas cosas, copiar desde un flash y rellenar con ceros, se realizan en el código del programa de
forma explícita , pero no en su main (), sino en un archivo separado que se ejecuta primero, se escribe una vez y simplemente se arrastra de un proyecto a otro.
Sin embargo, esto no es lo que nos interesa ahora, sino cómo entenderemos si nuestros datos incluso se ajustan a la RAM de nuestro controlador.
Se reconoce de manera muy simple, mediante la utilidad arm-none-eabi-size con un solo parámetro, el archivo ELF compilado de nuestro programa (a menudo su llamada se inserta al final del Makefile, porque es conveniente):

Aquí el texto es la cantidad de datos del programa que se encuentran en la memoria flash, y bss y los datos son nuestras áreas estáticamente asignadas en la RAM. Las dos últimas columnas no nos molestan: esta es la suma de las tres primeras, no tiene ningún significado práctico.
Total, estáticamente en RAM necesitamos bss + bytes de datos, en este caso - 5324 bytes. El controlador tiene 6144 bytes de RAM, no usamos malloc, quedan 820 bytes.
Lo cual debería ser suficiente para nosotros en la pila.
Pero suficiente? Porque si no, nuestra pila crecerá a nuestros propios datos, y luego primero sobrescribirá los datos, luego los sobrescribirá y luego todo se bloqueará. Además, entre el primer y el segundo punto, el programa puede continuar funcionando sin darse cuenta de que hay basura en los datos que procesa. En el peor de los casos, serán los datos que anotó cuando todo estaba en orden con la pila, y ahora solo lee, por ejemplo, los parámetros de calibración de algún sensor, y luego no tiene ninguna forma obvia de entender que todo está mal con ellos, Este programa continuará ejecutándose, como si nada hubiera pasado, dándote basura en la salida.
Memoria asignada dinámicamente
Y aquí comienza la parte más interesante: si reduce el cuento a una frase, entonces
es casi imposible determinar el tamaño de la pila de antemano .
Teóricamente , puede pedirle al compilador que le proporcione el tamaño de pila utilizado por cada función individual, luego pedirle que devuelva el árbol de ejecución de su programa, y para cada rama en él calcule la suma de las pilas de todas las funciones presentes en este árbol. Esto solo para cualquier programa más o menos complejo le llevará una cantidad considerable de tiempo.
Entonces recuerda que en cualquier momento puede ocurrir una interrupción, cuyo procesador también necesita memoria.
Entonces, que pueden ocurrir dos o tres interrupciones anidadas, cuyos manejadores ...
En general, lo entiendes. Intentar contar la pila para un programa específico es una actividad emocionante y generalmente útil, pero a menudo no lo hará.
Por lo tanto, en la práctica, se utiliza una técnica que le permite al menos comprender de alguna manera si todo en nuestra vida se desarrolla bien: la llamada "pintura de la memoria" (pintura de la memoria).
Lo que es conveniente en este método es que no depende de las herramientas de depuración que use, y si el sistema tiene al menos algún medio para generar información, puede hacerlo sin herramientas de depuración.
Su esencia es que llenamos toda la matriz desde el final de bss hasta el comienzo de la pila en algún lugar de la etapa inicial de la ejecución del programa, cuando la pila todavía es exactamente pequeña, con el mismo valor.
Además, comprobando en qué dirección este valor ya ha desaparecido, entendemos dónde cayó la pila. Dado que una vez que el color borrado no se restablezca, la verificación se puede hacer esporádicamente: mostrará el tamaño máximo de pila alcanzado.
Defina el color de la pintura: el valor específico no importa, a continuación solo toqué con dos dedos de mi mano izquierda. Lo principal es no elegir 0 y FF:
#define STACK_CANARY_WORD (0xCACACACAUL)
- , -, :
volatile unsigned *top, *start;
__asm__ volatile ("mov %[top], sp" : [top] "=r" (top) : : );
start = &_ebss;
while (start < top) {
*(start++) = STACK_CANARY_WORD;
}
? top , — ; start — bss (, ,
*.ld — libopencm3). bss .
:
unsigned check_stack_size(void) {
/* top of data section */
unsigned *addr = &_ebss;
/* look for the canary word till the end of RAM */
while ((addr < &_stack) && (*addr == STACK_CANARY_WORD)) {
addr++;
}
return ((unsigned)&_stack - (unsigned)addr);
}
_ebss , _stack —
, , , , .
.
— - check_stack_size() , , , .
.
712 — 6 108 .
Word of caution
— , , 100-% . ,
, , , , . , , -, 10-20 %, 108 .
, , , .
P.S. RTOS — MSP, , PSP. , — .