Una explicación detallada del proceso de asignación de memoria y carga de arranque STM32.
Si bien todos los ejemplos a continuación son para STM32F446, los principios básicos se aplican a la mayoría de las MCU.
STM32CUBEMX produce stm32f446retx_flash.ld que veremos como referencia para todas nuestras exploraciones.
El enlazador toma los archivos de objeto producidos por el compilador y genera la salida de compilación final, que en nuestro caso es el .elf binario. El enlazador siempre usa un archivo de script de enlace; Incluso si no especifica uno, se usa un script predeterminado.
Es importante tener en cuenta que el script de enlazador solo describe la memoria en función de las especificaciones de MCU y no altera la promesa de memoria de hardware.
No podemos hablar sobre el proceso del gestor de arranque sin comprender la estructura de la memoria. En realidad, es el propósito del gestor de arranque tener la memoria en un estado listo para ejecutar el método main() de nuestra aplicación.
La memoria del programa, la memoria de datos, los registros y los puertos de E/S en STM32F4 se organizan dentro del mismo espacio de direcciones lineales.
...
+ -- -- 0x2001FFFF -- -- +
| |
| RAM |
| |
+ -- -- 0x20000000 -- -- +
| ... |
+ -- -- 0x1FFF7A0F -- -- +
| |
| System |
| |
+ -- -- 0x1FFF0000 -- -- +
| ... |
+ -- -- 0x081FFFFF -- -- +
| |
| Flash |
| |
+ -- -- 0x08000000 -- -- +
| ... |
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + Donde la memoria de alias apunta a Flash, System o RAM Memory dependiendo del pin BOOT0 . Por defecto es flash.
Dos espacios son de mayor interés para nosotros por ahora:
Sin embargo, en la práctica, la división funcional podría variar. Por ejemplo, es posible que deba cargar datos binarios del programa en RAM o viceversa.
La estructura de memoria se refleja en el archivo de enlace. Flash comienza en ORIGIN = 0x8000000 y RAM en ORIGIN = 0x20000000 .
// linker file
MEMORY
{
RAM ( xrw ) : ORIGIN = 0x20000000 , LENGTH = 128 K
FLASH ( rx ) : ORIGIN = 0x8000000 , LENGTH = 512 K
}Tenga en cuenta que esta es solo una implementación predeterminada, puede dividir fácilmente Flash y RAM y agregar sus propios bloques o secciones tan pronto como se adhieran a la especificación de memoria MCU.
Luego se divide la memoria en el archivo enlazador en secciones, cada una con su dirección y destino (por ejemplo, flash o RAM).
// linker file
SECTIONS
{
...
. text :
{
...
} > FLASH
...
}Para explorar la Tabla de símbolos del DelF Usaremos el comando Arm-None-EABI- OBJDUMP y ordenar todas las entradas por sus direcciones:
arm-none-eabi-objdump -t stm32-boot-explained.elf | sortEl programa produce la salida como se describe en detalles en el manual:
Address Flag_Bits Section Size NameEl código de programación compilado entra en esta sección en memoria flash .
La sección comienza desde la dirección ORIGIN del flash, y _etext apunta a la última dirección de la sección.
08000000 l d . text 00000000 . text
08000 c00 g . text 00000000 _etextPodemos ver la ubicación de las funciones, que usamos en nuestro archivo de gotador de arranque:
080005 c8 g F . text 00000048 __libc_init_array
08000610 g F . text 000000d 8 main
080006e8 g F . text 00000064 Reset_Handler
08000b c4 g F . text 00000024 SystemInit La sección reside en la memoria RAM y contiene todas las variables con valores definidos. El rango de direcciones abarca de _sdata a _edata .
20000000 g . data 00000000 _sdata
20000010 g . data 00000000 _edataVerifiquemos qué variables de Main.c reside en esta sección:
20000000 l O . data 00000004 static_data_int
20000008 g O . data 00000008 data_double Los valores reales deben llenarse mediante un script de gestor de arranque copiando datos de la memoria flash en la dirección _sidata :
08000 c08 g * ABS * 00000000 _sidata Bloquee la sección de datos de RAM del símbolo de inicio para todas las variables sin valor asignado. El gestor de arranque se encarga de establecer los datos de este bloque en 0 .
El rango de memoria abarca desde _sbss a _ebss .
20000010 g . bss 00000000 _sbss
20000044 g . bss 00000000 _ebssTodas las variables de main.c sin valor explícito residen en esta sección:
2000002 c l O . bss 00000004 static_bss_int
20000030 g O . bss 00000008 bss_double
20000038 g O . bss 00000008 bss_my_struct
20000040 g O . bss 00000004 bss_my_unionVamos a consultar con GDB :
(gdb) p bss_double
$1 = 0
(gdb) p & bss_double
$2 = (double * ) 0x20000030 < bss_double > Una vez que asigna algún valor a la variable, su dirección no cambia y aún permanece en .bss :
(gdb) p bss_double
$3 = 86
(gdb) p & bss_double
$4 = (double * ) 0x20000030 < bss_double > Toda la memoria RAM arriba _end y hasta que _estack esté dedicado a la memoria de acumulación y apilamiento.
20000048 g . _user_heap_stack 00000000 _end
20020000 g . isr_vector 00000000 _estack La dirección _estack se calcula como ORIGIN(RAM) + LENGTH(RAM) . Para que por 128 kb RAM:
_estack = 0x20000000 + 128 * 1024 # dec
= 0x20000000 + 0x20000 # hex
= 0x20020000 Stack es una estructura LIFO que comienza en _estack y crece hacia abajo. El tamaño mínimo de la pila se define en el archivo enlazador como _Min_Stack_Size . La memoria de la pila se libera automáticamente.
+ -- -- 0x20020000 -- -- + < -- _estack
| |
| Stack |
| |
+ - - 0x2001FC00 - - + < -- - _Min_Stack_Size
| |
+ -- -- 0x200 sssss -- -- + < -- $msp register
| |
| |
| Free space |
| |
| |
+ -- -- 0x200 hhhhh -- -- + < -- Actual heap end
| |
| Heap |
| |
+ - - 0x20000248 - - + < -- + _Min_Heap_Size
| |
+ -- -- 0x20000048 -- -- + < -- _end El montón a su vez comienza desde _end y crece hacia arriba hasta _estack - _Min_Stack_Size cuando lo solicita malloc .
Tenemos una variable stack_int definida en main.c. Verifiquemos la dirección con GDB después de que se haya inicializado:
(gdb) p & stack_int
$1 = (unsigned short * ) 0x2001ffd6
(gdb) p $msp
$2 = (void * ) 0x2001ffd0 Que coincide con nuestras expectativas, cuando la variable está por encima del $msp .
Vale la pena mencionar que la implementación de la biblioteca C estándar comúnmente utilizada en aplicaciones C integradas es NewLib. Esta biblioteca requiere la implementación de ciertas funciones específicas del sistema. STM32CUBEMX genera el archivo syscalls.c con las implementaciones predeterminadas necesarias.
También es el caso de sbrk Call que aumenta el espacio de datos del programa. malloc está utilizando esta función para asignar más memoria de montón. Puede encontrar una implementación generada por STM32CubEMX en Sysmem.C. Simplemente permite que el montón crezca desde _end hasta _estack - _Min_Stack_Size .
Ahora, cuando entendemos la memoria MCU, conectemos a nuestro programa con GDB , esto es lo que vemos como la primera salida:
...
Reading symbols from ./stm32-boot-explained.elf...
Remote debugging using localhost:61234
Reset_Handler () at %PATH%/bootloader.c:25
25 void Reset_Handler () {
(gdb) info registers
...
pc 0x80006e8 0x80006e8 < Reset_Handler >
... Reset_Handler se identificó de alguna manera como un punto de arranque para nuestra aplicación.
Es posible que haya notado que hay una instrucción ENTRY en el archivo de enlazador:
// linker file
ENTRY ( Reset_Handler ) En realidad, guarda una referencia a la dirección de función Reset_Handler en el encabezado del archivo .elf :
arm-none-eabi-objdump -t -f stm32-boot-explained.elf | grep " start address "
start address 0x080006e9 Mirando nuestra tabla de símbolos, Reset_Handler está presente en la sección Flash .text . Al igual que todas las demás funciones:
080006e8 g F . text 00000064 Reset_HandlerLas direcciones están alineadas en la página, por lo que existe la posibilidad de un desajuste entre el encabezado .elf y la dirección real.
Aunque esta información es utilizada principalmente por el enlazador para verificar la existencia del símbolo del punto de entrada dentro del código, no tiene un significado práctico para el MCU.
Según la especificación STM32, la CPU obtiene el valor de _estack superior de la dirección 0x00000000 , luego comienza la ejecución del código desde la memoria de arranque a partir de 0x00000004 .
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + Aquí es exactamente donde se define la memoria de alias mencionada anteriormente. Con la configuración predeterminada, cuando el valor PIN BOOT0 = 0 , se alias al bloqueo de memoria Flash a partir de 0x8000000 .
Otras opciones basadas en BOOT0 y BOOT1 incluyen la memoria del sistema con un gestor de arranque integrado o memoria RAM. El cargador de arranque integrado está programado por ST durante la producción y fuera del alcance de este manual.
Pero Reset_Handler tiene una dirección 0x08000524 que no es exactamente el comienzo de la memoria flash, ¿cómo se encuentra el MCU el método de bootstrap?
Aquí es donde la mesa Vector entra en juego.
08000000 g O . isr_vector 000001 c4 Vector_Table MCU trata el comienzo de la memoria como una tabla vectorial, que contiene punteros a varias rutinas de servicio de interrupción y funciones de inicio esenciales, incluido el Reset_Handler . Consulte la especificación para ver la estructura de la tabla exacta que MCU espera cargar de 0x00000000 . La tabla real debe ser llena por el gestor de arranque.
| DIRECCIÓN | Nombre |
|---|---|
| 0x00000000 | Reservado |
| 0x00000004 | Manipulador de reinicio |
| 0x00000008 | Interrupción no enmascarable |
| 0x00000012 | Falla dura |
| ... | Otras interrupciones |
Este Reset_Handler es una función de gestor de arranque que se puede utilizar para muchas aplicaciones, desde tareas específicas de seguridad hasta actualización automática de firmware. Aquí, exploraremos la implementación predeterminada básica para comprender su interacción con la memoria del MCU.
Por defecto, el método Reset_Handler se define en el archivo ASM STARTUP_STM32F436XX.S proporcionado por STM23CUBEMX. La implementación real de Bootloader.c en este proyecto está escrita en C para mayor claridad.
Tenga en cuenta que se pueden acceder a las variables definidas en el script de enlazador en el código C:
extern uint32_t _estack ;Para que sea fácil de replicar la versión ASM.
El proceso de carga mínimo podría dividirse en los siguientes pasos:
SystemInit() ).data de flash a ram.bss__libc_init_array() función)main() función)Para los pasos #1 y #4, STM32CUBEMX proporciona implementaciones de funciones, puede verificar los detalles en System_STM32F4XX.C.
El proyecto tiene un conjunto mínimo de archivos necesarios para iniciar el STM32. Es posible que desee probarlo usted mismo para verificar la salida de Arm-None-eabi- Objdump y atravesar GDB .
Se requiere una cadena de herramientas GNU ARM para construir el proyecto.
Se recomienda instalar un paquete de herramientas de comandos STM32CubeClt todo en uno con las herramientas Arm-None-Eabi-GCC , STM32_Programmer_cli y St-Link_GDBServer incluidas.
Construya el proyecto usando CMake :
mkdir build ; cd build
cmake ../ -DPROGRAMMER_CLI=/opt/ST/STM32CubeCLT_1.15.1/STM32CubeProgrammer/bin
-DGDB_SERVER=/opt/ST/STM32CubeCLT_1.15.1/STLink-gdb-server/bin
make VERBOSE=1Tenga en cuenta que el último comando es la etapa de enlace. Si eliminamos todas las otras banderas del compilador, el comando podría parecerse a los siguientes. Aquí es donde se instruye al enlazador para que use nuestro script de enlazador.
arm-none-eabi-gcc
...
-T " %SCRIPT_DIR%/STM32F446RETx_FLASH.ld "
...
" %OBJ_DIR%/%OBJECT_NAME%.c.obj "
...
-o stm32-boot-explained.elfSTM32_Programmer_cli está preconfigurado para SWD Procotol, solo ejecuta:
make flash Tome una nota en la salida del programador, que está utilizando la dirección 0x08000000 como punto de partida:
...
Memory Programming ...
Opening and parsing file: stm32-boot-explained.elf
File : stm32-boot-explained.elf
Size : 1,46 KB
Address : 0x08000000
...En realidad, es la dirección inicial de la memoria flash, que ya sabemos.
Hay un objetivo personalizado preconfigurado para ejecutar st-link_gdbserver :
# start ST-Link gdb server
make gdb-server
# connect with gdb debugger
gdb -ex ' target remote localhost:61234 ' ./stm32-boot-explained.elf