Une explication détaillée du processus de cartographie et de chargement de démarrage de la mémoire STM32.
Bien que tous les exemples ci-dessous soient pour STM32F446, les principes de base s'appliquent à la plupart des MCU.
STM32cubemx produit STM32F446ReTx_flash.ld que nous considérerons comme une référence pour toutes nos explorations.
Le linker prend les fichiers d'objet produits par le compilateur et génère la sortie de compilation finale, qui dans notre cas est le binaire .ell . Le linker utilise toujours un fichier de script de linker; Même si vous n'en spécifiez pas, un script par défaut est utilisé.
Il est important de noter que le script de liaison ne décrit que la mémoire en fonction des spécifications MCU et ne modifie aucune addition de mémoire matérielle.
Nous ne pouvons pas parler du processus de chargeur de démarrage sans comprendre la structure de mémoire. En fait, c'est le but du chargeur de démarrage d'avoir la mémoire dans un état prêt à exécuter la méthode main() de notre application.
La mémoire du programme, la mémoire des données, les registres et les ports d'E / S dans STM32F4 sont organisés dans le même espace d'adressage linéaire.
...
+ -- -- 0x2001FFFF -- -- +
| |
| RAM |
| |
+ -- -- 0x20000000 -- -- +
| ... |
+ -- -- 0x1FFF7A0F -- -- +
| |
| System |
| |
+ -- -- 0x1FFF0000 -- -- +
| ... |
+ -- -- 0x081FFFFF -- -- +
| |
| Flash |
| |
+ -- -- 0x08000000 -- -- +
| ... |
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + Où l'alias de la mémoire pointe vers la mémoire flash, système ou RAM en fonction de la broche BOOT0 . Par défaut, c'est Flash.
Deux espaces sont le plus intéressant pour nous pour l'instant:
Cependant, dans la pratique, la division fonctionnelle pourrait varier. Par exemple, vous devrez peut-être charger des données binaires du programme dans RAM ou vice versa.
La structure de la mémoire est reflétée dans le fichier de liaison. Le flash commence à ORIGIN = 0x8000000 et RAM à ORIGIN = 0x20000000 .
// linker file
MEMORY
{
RAM ( xrw ) : ORIGIN = 0x20000000 , LENGTH = 128 K
FLASH ( rx ) : ORIGIN = 0x8000000 , LENGTH = 512 K
}Notez qu'il s'agit d'une implémentation par défaut, vous pouvez facilement diviser Flash et RAM et ajouter vos propres blocs ou sections dès qu'ils adhèrent à la spécification de mémoire MCU.
La mémoire est ensuite divisée dans le fichier de linker en sections, chacune ayant son adresse et sa destination (par exemple, Flash ou RAM).
// linker file
SECTIONS
{
...
. text :
{
...
} > FLASH
...
}Pour explorer la table de symboles .
arm-none-eabi-objdump -t stm32-boot-explained.elf | sortLe programme produit la sortie comme décrit dans les détails du manuel:
Address Flag_Bits Section Size NameLe code de programmation compilé va dans cette section dans la mémoire flash .
La section part de l'adresse ORIGIN du flash et _etext pointe vers la dernière adresse de la section.
08000000 l d . text 00000000 . text
08000 c00 g . text 00000000 _etextNous pouvons voir l'emplacement des fonctions, que nous utilisons dans notre fichier de chargeur de démarrage:
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 section réside dans la mémoire RAM et contient toutes les variables avec des valeurs définies. La plage d'adresses s'étend de _sdata à _edata .
20000000 g . data 00000000 _sdata
20000010 g . data 00000000 _edataVérifions quelles variables de main.c résident dans cette section:
20000000 l O . data 00000004 static_data_int
20000008 g O . data 00000008 data_double Les valeurs réelles doivent être remplies par un script de chargeur de démarrage en copiant les données à partir de la mémoire flash à l'adresse _sidata :
08000 c08 g * ABS * 00000000 _sidata Bloquer la section de données de RAM de symbole de démarrage pour toutes les variables sans valeur affectée. Le chargeur de démarrage s'occupe de définir les données de ce bloc sur 0 .
La plage de mémoire s'étend de _sbss à _ebss .
20000010 g . bss 00000000 _sbss
20000044 g . bss 00000000 _ebssToutes les variables de main.c sans valeur explicite résident dans cette section:
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_unionVérifions GDB :
(gdb) p bss_double
$1 = 0
(gdb) p & bss_double
$2 = (double * ) 0x20000030 < bss_double > Une fois que vous avez attribué une certaine valeur à la variable, son adresse ne change pas et elle reste en .bss :
(gdb) p bss_double
$3 = 86
(gdb) p & bss_double
$4 = (double * ) 0x20000030 < bss_double > Toute la mémoire de RAM au-dessus _end et jusqu'à ce que _estack soit dédié à un tas et à empiler la mémoire.
20000048 g . _user_heap_stack 00000000 _end
20020000 g . isr_vector 00000000 _estack L'adresse _estack est calculée comme ORIGIN(RAM) + LENGTH(RAM) . De sorte que pour 128 Ko RAM:
_estack = 0x20000000 + 128 * 1024 # dec
= 0x20000000 + 0x20000 # hex
= 0x20020000 La pile est une structure LIFO qui commence à _estack et se développe vers le bas. La taille minimale de la pile est définie dans le fichier de linker comme _Min_Stack_Size . La mémoire de pile est automatiquement libérée.
+ -- -- 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 Heat à son tour commence à partir de _end et se développe jusqu'à _estack - _Min_Stack_Size à la demande de malloc .
Nous avons une variable stack_int définie dans main.c. Vérifions l'adresse avec GDB après avoir été initialisé:
(gdb) p & stack_int
$1 = (unsigned short * ) 0x2001ffd6
(gdb) p $msp
$2 = (void * ) 0x2001ffd0 Ce qui correspond à nos attentes, lorsque la variable est supérieure au $msp .
Il convient de mentionner que l'implémentation standard de la bibliothèque C couramment utilisée dans les applications C intégrées est NewLib. Cette bibliothèque nécessite l'implémentation de certaines fonctions spécifiques au système. STM32cubemx génère le fichier syscalls.c avec les implémentations par défaut nécessaires.
C'est également le cas pour sbrk Call qui augmente l'espace de données du programme. malloc utilise cette fonction pour allouer plus de mémoire de tas. Vous pouvez trouver une implémentation générée par STM32cubemx dans sysmem.c. Il permet simplement au tas de grandir de _end jusqu'à _estack - _Min_Stack_Size .
Maintenant, lorsque nous comprenons la mémoire MCUS, connectons notre programme avec GDB , voici ce que nous considérons comme la première sortie:
...
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 a été identifié en quelque sorte comme un point de démarrage pour notre application.
Vous avez peut-être remarqué qu'il y a une instruction ENTRY dans le fichier de linker:
// linker file
ENTRY ( Reset_Handler ) En fait, il enregistre une référence à l'adresse de fonction Reset_Handler dans l'en-tête de fichier .elf :
arm-none-eabi-objdump -t -f stm32-boot-explained.elf | grep " start address "
start address 0x080006e9 En regardant notre table de symboles, Reset_Handler est présent dans la section Flash .text , comme toutes les autres fonctions:
080006e8 g F . text 00000064 Reset_HandlerLes adresses sont alignées sur la page, c'est pourquoi il y a une possibilité de décalage entre l'en - tête et l'adresse réelle.
Bien que ces informations soient principalement utilisées par le linker pour vérifier l'existence du symbole du point d'entrée dans le code, il n'a aucune signification pratique pour le MCU.
Selon la spécification STM32, le CPU récupère la valeur _estack haut de gamme à partir de l'adresse 0x00000000 , puis commence l'exécution du code à partir de la mémoire de démarrage à partir de 0x00000004 .
+ -- -- 0x001FFFFF -- -- +
| |
| Alias |
| |
+ -- -- 0x00000000 -- -- + C'est exactement là que la mémoire d'alias mentionnée ci-dessus est définie. Avec la configuration par défaut, lorsque la valeur PIN BOOT0 = 0 , il alimente le bloc de mémoire flash à partir de 0x8000000 .
D'autres options basées sur le BOOT0 et BOOT1 incluent la mémoire système avec un chargeur de démarrage embarqué ou une mémoire RAM. Le chargeur de démarrage intégré est programmé par ST pendant la production et hors de la portée de ce manuel.
Mais Reset_Handler a une adresse 0x08000524 qui n'est pas exactement le début de la mémoire flash, comment le MCU trouve-t-il alors la méthode bootstrap?
Voici où le tableau vectoriel entre en jeu.
08000000 g O . isr_vector 000001 c4 Vector_Table MCU traite le début de la mémoire comme une table vectorielle, qui contient des pointeurs vers diverses routines de service d'interruption et des fonctions de démarrage essentielles, y compris le Reset_Handler . Consultez la spécification pour voir la structure de la table exacte que MCU prévoit de charger à partir de 0x00000000 . La table réelle doit être remplie par le chargeur de démarrage.
| Adresse | Nom |
|---|---|
| 0x00000000 | Réservé |
| 0x00000004 | Réinitialiser le gestionnaire |
| 0x00000008 | Interruption non masquable |
| 0x00000012 | Défaut dur |
| ... | Autres interruptions |
Cette Reset_Handler est une fonction de chargeur de démarrage qui peut être utilisée pour de nombreuses applications, des tâches spécifiques à la sécurité à la mise à jour automatique du micrologiciel. Ici, nous explorerons l'implémentation de base par défaut pour comprendre son interaction avec la mémoire du MCU.
Par défaut, la méthode Reset_Handler est définie dans le fichier startup_stm32f436xx.s ASM fourni par STM23cubemx. L'implémentation réelle de bootloader.c dans ce projet est écrite en C pour plus de clarté.
Notez que les variables définies dans le script de linker sont accessibles dans le code C:
extern uint32_t _estack ;Afin qu'il soit facilement possible de reproduire la version ASM.
Le processus de chargement minimal pourrait alors être divisé en étapes suivantes:
SystemInit() ).data du flash à la RAM.bss__libc_init_array() )main() )Pour les étapes n ° 1 et n ° 4, STM32CUBEMX fournit des implémentations de fonction, vous pouvez vérifier les détails dans System_STM32F4XX.C.
Le projet a un ensemble minimal de fichiers requis pour démarrer le STM32. Vous voudrez peut-être l'essayer vous-même pour vérifier la sortie du bras-none-eai-objdump et parcourir GDB .
La chaîne d'outils ARM GNU est nécessaire pour construire le projet.
Il est recommandé d'installer un package d'outils de commande STM32CUBECLT tout-en-un avec ARM-None-EABI-GCC , STM32_PROGRAMMER_CLI et ST-LINK_GDBSERVER INTOLLES inclus.
Construisez le projet à l'aide de 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=1Notez que la dernière commande est l'étape de liaison. Si nous supprimons tous les autres drapeaux du compilateur, la commande pourrait ressembler à ce qui suit. C'est là que le linker est invité à utiliser notre script de linker.
arm-none-eabi-gcc
...
-T " %SCRIPT_DIR%/STM32F446RETx_FLASH.ld "
...
" %OBJ_DIR%/%OBJECT_NAME%.c.obj "
...
-o stm32-boot-explained.elfSTM32_PROGRAMMER_CLI est préconfiguré pour SWD Procotol, il suffit d'exécuter:
make flash Prenez une note sur la sortie du programmeur, qui utilise l'adresse 0x08000000 comme point de départ:
...
Memory Programming ...
Opening and parsing file: stm32-boot-explained.elf
File : stm32-boot-explained.elf
Size : 1,46 KB
Address : 0x08000000
...C'est en fait l'adresse de départ de la mémoire flash, que nous connaissons déjà.
Il y a une cible personnalisée préconfigurée pour exécuter 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