Bonjour, Habr!
En ce qui concerne le
relâchement du régime l' autre jour, l'indignation dans les commentaires d'un article voisin que les articles sur les microcontrôleurs ne font que clignoter par la LED, et aussi la mort prématurée de mon blog standard, que je suis trop paresseux pour restaurer, je vais transférer ici des informations utiles sur une petite lumière regrettable une astuce pour travailler avec les cœurs Cortex-M - vérifier la validité des adresses aléatoires.
L'une des fonctionnalités très utiles et en même temps pour une raison quelconque prédéfinies qui ne sont décrites nulle part sur les microcontrôleurs Cortex-M (tous) est la possibilité de vérifier l'exactitude de l'adresse en mémoire. Avec lui, vous pouvez déterminer la taille du flash, de la RAM et de l'EEPROM, déterminer la présence sur un processeur particulier de périphériques et de registres spécifiques, combattre les processus tombés tout en maintenant la santé globale du système d'exploitation, etc.
En mode normal, lorsque vous arrivez à une adresse inexistante sur Cortex-M3 / M4 / M7, une exception BusFault est levée et, en l'absence de son gestionnaire, elle dégénère en HardFault. Sur Cortex-M0, il n'y a pas d'exceptions «détaillées» (MemFault, BusFault, UsageFault), et toute défaillance est immédiatement transmise à HardFault.
En général, vous ne pouvez pas ignorer HardFault - cela peut être le résultat d'une défaillance matérielle, par exemple, et le comportement ultérieur de l'appareil deviendra imprévisible. Mais dans le cas particulier, cela peut et doit être fait.
Cortex-M3 et Cortex-M4: le défaut de bus non rempli
Sur Cortex-M3 et supérieur, la vérification de la validité de l'adresse est assez simple: vous devez interdire toutes les exceptions (sauf, évidemment, non masquables) via le registre FAULTMASK, désactiver spécifiquement le traitement BusFault, puis fouiller dans l'adresse vérifiée et voir si le drapeau BFARVALID dans le registre BFAR , c.-à -d. registre d'adresse de défaut de bus. Si vous l'avez pris, vous venez d'avoir un BusFault, c'est-à -dire L'adresse est incorrecte.
Le code ressemble à ceci, toutes les définitions et fonctions proviennent du CMSIS standard (non-fournisseur), donc il devrait fonctionner sur n'importe quel M3, M4 ou M7:
bool cpu_check_address(volatile const char *address) { static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; SCB->CFSR |= BFARVALID_MASK; uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; *address; if ((SCB->CFSR & BFARVALID_MASK) != 0) { is_valid = false; } SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; }
Cortex-M0 et Cortex-M0 +
Avec Cortex-M0 et Cortex-M0 +, tout est plus compliqué, comme je l'ai dit ci-dessus, ils n'ont pas BusFault et tous les registres correspondants, et les exceptions sont immédiatement escaladées en HardFault. Par conséquent, il n'y a qu'une seule issue: permettre au gestionnaire HardFault de comprendre que l'exception a été délibérément provoquée et de revenir à la fonction qui l'a appelée, en y passant un indicateur indiquant que HardFault était là .
Cela se fait uniquement en assembleur. Dans l'exemple ci-dessous, le registre R5 est mis à 1 et deux "nombres magiques" sont écrits dans les registres R1 et R2. Si HardFault se produit après avoir essayé de charger la valeur à l'adresse en cours de vérification, il doit alors vérifier les valeurs de R1 et R2, et s'ils trouvent les nombres nécessaires, définissez R5 sur zéro. La valeur de R5 est transférée au code syshech via une variable spéciale qui est rigidement liée à ce registre, l'adresse à assembler dans l'assembleur est implicite, nous savons juste que dans arm-none-eabi le premier paramètre de la fonction est placé dans R0.
bool cpu_check_address(volatile const char *address) { (void)address; register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" "ldr r1, =0xDEADF00D \n" "ldr r2, =0xCAFEBABE \n" "ldrb r3, [r0] \n" ); return result; }
Le code du gestionnaire HardFault dans sa forme la plus simple ressemble Ă ceci:
__attribute__((naked)) void hard_fault_default(void) { __asm__ volatile ( "movs r0, #4 \n" "mov r2, lr \n" "tst r2, r0 \n" "bne use_psp \n" "mrs r0, msp \n" "b out \n" " use_psp: \n" "mrs r0, psp \n" " out: \n" "ldr r1, [r0, #0x04] \n" "ldr r2, =0xDEADF00D \n" "cmp r1, r2 \n" "bne regular_handler \n" "ldr r1, [r0, #0x08] \n" "ldr r2, =0xCAFEBABE \n" "cmp r1, r2 \n" "bne regular_handler \n" "ldr r1, [r0, #0x18] \n" "add r1, r1, #2 \n" "str r1, [r0, #0x18] \n" "ldr r5, =0 \n" "bx lr \n" " regular_handler: \n" )
Au moment de quitter le gestionnaire d'exceptions, Cortex lance les registres, qui sont garantis corrompus par le gestionnaire (R0-R3, R12, LR, PC ...), sur la pile. Le premier fragment - il est déjà dans la plupart des gestionnaires HardFault prêts à l'emploi, à l'exception de ceux écrits sous du métal nu pur - détermine quelle pile: lorsque vous travaillez dans le système d'exploitation, il peut s'agir de MSP ou de PSP, et ils ont des adresses différentes. Dans les projets bare metal, la pile MSP (Main Stack Pointer) est généralement installée a priori, sans vérification - car la PSP (Process Stack Pointer) ne peut pas être là en raison du manque de processus.
Après avoir déterminé la pile souhaitée et mis son adresse dans R0, nous lisons les valeurs R1 (offset 0x04) et R2 (offset 0x08), la comparons avec des mots magiques, si les deux correspondent - nous lisons la valeur PC (offset 0x18) de la pile, ajoutons 2 (2 octets - la taille de l'instruction sur Cortex-M *) et enregistrez-la sur la pile. Si cela n'est pas fait, lorsque nous reviendrons du gestionnaire, nous nous retrouverons sur la même instruction qui a réellement provoqué l'exception, et nous tournerons toujours en rond. L'annexe 2 nous amène à la prochaine instruction au moment du retour.
* Upd. Dans les commentaires, la question s'est posée sur la taille des instructions sur le Cortex-M, je ferai la bonne réponse ici: dans ce cas, le crash provoque l'instruction LDRB, qui est disponible dans l'architecture ARMv7-M en deux versions - 16 bits et 32 ​​bits. La deuxième option sera sélectionnée si au moins une des conditions est remplie:
- l'auteur a explicitement indiqué l'instruction LDRB.W au lieu de LDRB (nous ne le faisons pas)
- des registres supérieurs à R7 sont utilisés (pour nous - R0 et R3)
- Un décalage supérieur à 31 octets est spécifié (nous n'avons pas de décalage)
Dans tous les autres cas (c'est-à -dire lorsque les opérandes correspondent au format de la version 16 bits de l'instruction), l'assembleur doit sélectionner la version 16 bits.
Par conséquent, dans notre cas, il y aura toujours une instruction de 2 octets qui doit être dépassée, mais si vous modifiez fortement le code, des options sont possibles.Ensuite, écrivez 0 dans R5, qui sert d'indicateur pour entrer dans HardFault. Les registres après R3 ne sont pas enregistrés dans la pile avant les registres spéciaux, et lorsque vous quittez le gestionnaire, ils ne sont en aucun cas restaurés, c'est donc notre conscience de les gâter ou de ne pas les gâcher. Dans ce cas, nous changeons délibérément R5 de 1 à 0.
Le retour du gestionnaire d'interruption se fait exactement dans un sens. Lorsque vous entrez dans le gestionnaire, une valeur spéciale est écrite dans le registre LR appelée EXC_RETURN, qui est nécessaire pour écrire sur le PC pour quitter le gestionnaire - et pas seulement l'écrire, mais le faire avec une commande POP ou BX (c'est-à -dire, «mov pc, lr, par exemple, ne fonctionne pas , bien que la première fois il vous semble que cela fonctionne). BX LR ressemble à une tentative pour accéder à une adresse vide de sens (dans LR, il y aura quelque chose comme 0xFFFFFFF1 qui n'a rien à voir avec l'adresse réelle de la procédure à laquelle nous devons revenir), mais en réalité, le processeur voit cette valeur sur le PC (où elle ira automatiquement), il restaurera les registres de la pile et continuera d'exécuter notre procédure - avec la procédure suivante après avoir appelé HardFault car nous avons augmenté manuellement le PC de cette pile de 2.
Vous pouvez lire sur tous les décalages et commandes
clairement où , bien sûr.
Eh bien, ou si les nombres magiques ne sont pas visibles, tout ira à regular_handler, après quoi la procédure de traitement HardFault habituelle suit - en règle générale, c'est une fonction qui imprime les valeurs de registre sur la console, décide quoi faire ensuite avec le processeur, etc.
Détermination de la taille de la RAM
L'utilisation de tout cela est simple et directe. Nous voulons écrire un firmware qui fonctionne sur plusieurs microcontrôleurs avec différentes quantités de RAM, tout en utilisant à chaque fois de la RAM dans un programme complet?
Oui facile:
static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) { char *address = base; do { address += block; if (!cpu_check_address(address)) { break; } } while ((uint32_t)(address - base) < maxsize); return (uint32_t)(address - base); } uint32_t get_cpu_ram_size(void) { return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024); }
maxsize est nécessaire ici afin qu'il ne puisse pas y avoir d'espace entre la quantité maximale possible de RAM entre celui-ci et le prochain bloc d'adresses, sur lequel cpu_check_address se cassera. Dans cet exemple, c'est 80 Ko. Cela n'a également aucun sens de sonder toutes les adresses - il suffit de regarder la fiche technique pour voir quelle est l'étape minimale possible entre les deux modèles de contrôleur et la définir comme bloc.
Transition programmatique vers le chargeur de démarrage, situé au milieu de nulle part
Parfois, vous pouvez effectuer des astuces plus complexes - par exemple, imaginez que vous souhaitez passer par programme au chargeur de démarrage d'usine STM32 pour passer en mode de mise à jour du firmware via UART ou USB, sans prendre la peine d'écrire votre chargeur de démarrage.
Le chargeur de démarrage STM32 se trouve dans la zone appelée mémoire système, à laquelle vous devez vous rendre, mais il y a un problème - cette zone a des adresses différentes non seulement sur différentes séries de processeurs, mais sur différents modèles de la même série (une plaque épique peut être trouvée dans
AN2606 sur pages 22 à 26). Lorsque vous ajoutez la fonctionnalité correspondante à la plate-forme en général, et pas seulement à un produit spécifique, vous recherchez la polyvalence.
Dans les fichiers CMSIS, l'adresse de début de la mémoire système est également manquante. Il n'est pas possible de le déterminer par l'ID du chargeur de démarrage, car il s'agit d'un problème de poule et d'oeuf - l'ID du chargeur de démarrage se trouve dans le dernier octet de la mémoire système, ce qui nous ramène à la question de l'adresse.
Cependant, si nous regardons la carte mémoire STM32, nous verrons quelque chose comme ceci:

Dans ce cas, nous nous intéressons à l'environnement de la mémoire système - par exemple, en haut se trouve une zone une fois programmable (pas dans tous les STM32) et des octets d'options (dans tous). Cette structure est observée non seulement dans différents modèles, mais dans différentes lignes STM32, avec la seule différence dans la présence d'OTP et la présence d'un écart dans les adresses entre la mémoire système et les options.
Mais pour nous dans ce cas, la chose la plus importante est que l'adresse de début des octets d'options se trouve dans les en-têtes CMSIS standard - elle est appelée OB_BASE.
Plus simple encore. Nous écrivons la fonction pour rechercher la première adresse valide ou invalide vers le bas ou vers le haut à partir de l'adresse spécifiée:
char *cpu_find_next_valid_address(char *start, char *stop, bool valid) { char *address = start; while (true) { if (address == stop) { return NULL; } if (cpu_check_address(address) == valid) { return address; } if (stop > start) { address++; } else { address--; } }; return NULL; }
Et regardez vers le bas à partir des octets d'options, d'abord la fin de la mémoire système ou de l'OTP adjacent, puis le début de la mémoire système en deux passes:
char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; c = cpu_find_next_valid_address(a, b, true); c = cpu_find_next_valid_address(c, b, false) + 1;
Et sans trop de difficulté, nous organisons cela en une fonction qui trouve le début de la mémoire système et y saute, c'est-à -dire qu'il lance le chargeur de démarrage:
static void jump_to_bootloader(void) __attribute__ ((noreturn)); static void jump_to_bootloader(void) { char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; c = cpu_find_next_valid_address(a, b, true); c = cpu_find_next_valid_address(c, b, false) + 1; if (!c) { NVIC_SystemReset(); } uint32_t boot_addr = (uint32_t)c; uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr); uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4); void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr); __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); }
Cela dépend du modèle de processeur spécifique ... rien ne dépend. La logique ne fonctionnera pas sur les modèles qui ont un trou entre OTP et la mémoire système - mais je n'ai pas vérifié s'il y en a. Travaillera activement avec OTP - vérifier.
D'autres astuces s'appliquent uniquement à la procédure habituelle pour appeler le chargeur de démarrage à partir de votre code - n'oubliez pas de réinitialiser le pointeur de pile et d'appeler la procédure pour quitter le chargeur de démarrage avant d'initialiser les périphériques, les vitesses d'horloge, etc.: en raison de son minimalisme, le chargeur de démarrage peut obstruer initialisez la périphérie et attendez-vous à ce qu'elle soit dans l'état par défaut. Une bonne option pour appeler le chargeur de démarrage à partir d'un endroit arbitraire dans votre programme consiste à écrire dans le registre de sauvegarde RTC ou simplement à une adresse connue dans la mémoire d'un nombre magique, à redémarrer le programme et à vérifier lors des premières étapes de l'initialisation de ce numéro.
PS Étant donné que toutes les adresses de la carte mémoire du processeur sont alignées dans le pire des cas par 4, la procédure ci-dessus sera considérablement accélérée par l'idée de les parcourir par incréments de 4 octets au lieu d'un.
Avis important
NB: veuillez noter que sur un contrôleur spécifique, la validité d'une adresse spécifique n'indique pas nécessairement la présence réelle de fonctionnalités pouvant se trouver à cette adresse. Par exemple, l'adresse du registre contrôlant un bloc périphérique optionnel peut être valide, bien que le bloc lui-même soit absent dans ce modèle. Du côté du fabricant, les astuces les plus intéressantes sont possibles, généralement enracinées dans l'utilisation des mêmes cristaux pour différents modèles de processeurs. Cependant, dans la plupart des cas, ces procédures fonctionnent et s'avèrent très utiles.