Überprüfen der Speicheradressen auf einem Cortex-M0 / M3 / M4 / M7

Hallo Habr!

In Bezug auf die Lockerung des Regimes neulich, die Empörung in den Kommentaren eines benachbarten Beitrags, dass die Artikel über Mikrocontroller nur durch die LED blinken, sowie den vorzeitigen Tod meines Standardblogs, den ich zu faul bin, um ihn wiederherzustellen, werde ich hier nützliches Material über ein bedauerliches kleines Licht übertragen Ein Pressetrick bei der Arbeit mit Cortex-M-Kernen - Überprüfung der Gültigkeit zufälliger Adressen.


Eine der sehr nützlichen und gleichzeitig aus irgendeinem Grund vorgefertigten Funktionen, die auf den Cortex-M-Mikrocontrollern (alle) nirgendwo beschrieben sind, ist die Möglichkeit, die Richtigkeit der Adresse im Speicher zu überprüfen. Mit ihm können Sie die Größe von Flash, RAM und EEPROM bestimmen, das Vorhandensein bestimmter Peripheriegeräte und Register auf einem bestimmten Prozessor bestimmen, heruntergefallene Prozesse herunterfahren und gleichzeitig den Gesamtzustand des Betriebssystems beibehalten usw.

Wenn Sie im normalen Modus zu einer nicht vorhandenen Adresse in Cortex-M3 / M4 / M7 gelangen, wird eine BusFault-Ausnahme ausgelöst. Wenn kein Handler vorhanden ist, wird sie zu HardFault eskaliert. Auf Cortex-M0 gibt es keine „detaillierten“ Ausnahmen (MemFault, BusFault, UsageFault), und alle Fehler eskalieren sofort zu HardFault.

Im Allgemeinen können Sie HardFault nicht ignorieren. Dies kann beispielsweise auf einen Hardwarefehler zurückzuführen sein und das weitere Verhalten des Geräts wird unvorhersehbar. Dies kann und sollte aber im Einzelfall geschehen.

Cortex-M3 und Cortex-M4: der unerfüllte BusFault


Bei Cortex-M3 und höher ist die Überprüfung der Gültigkeit der Adresse recht einfach: Sie müssen alle Ausnahmen (außer natürlich nicht maskierbar) über das FAULTMASK-Register verbieten, die BusFault-Verarbeitung speziell deaktivieren und dann in die zu überprüfende Adresse stecken und prüfen, ob das BFARVALID-Flag im BFAR-Register vorhanden ist d.h. Busfehler-Adressregister. Wenn Sie es genommen haben, hatten Sie gerade einen BusFault, d. H. Die Adresse ist falsch.

Der Code sieht so aus, alle Definitionen und Funktionen stammen aus dem Standard-CMSIS (nicht vom Hersteller), daher sollte er auf jedem M3, M4 oder M7 funktionieren:

bool cpu_check_address(volatile const char *address) { /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */ static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; /* Clear BFARVALID flag by writing 1 to it */ SCB->CFSR |= BFARVALID_MASK; /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */ uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; /* probe address in question */ *address; /* Check BFARVALID flag */ if ((SCB->CFSR & BFARVALID_MASK) != 0) { /* Bus Fault occured reading the address */ is_valid = false; } /* Reenable BusFault by clearing BFHFNMIGN */ SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; } 

Cortex-M0 und Cortex-M0 +


Mit Cortex-M0 und Cortex-M0 + ist alles komplizierter, wie ich oben sagte, sie haben nicht BusFault und alle entsprechenden Register, und Ausnahmen werden sofort zu HardFault eskaliert. Daher gibt es nur einen Ausweg: Der HardFault-Handler kann verstehen, dass die Ausnahme absichtlich verursacht wurde, und zu der Funktion zurückkehren, die sie aufgerufen hat, und dort ein Flag übergeben, das angibt, dass HardFault vorhanden war.

Dies erfolgt ausschließlich im Assembler. Im folgenden Beispiel wird das Register R5 auf 1 gesetzt und zwei "magische Zahlen" werden in die Register R1 und R2 geschrieben. Wenn HardFault auftritt, nachdem versucht wurde, den Wert in die zu überprüfende Adresse zu laden, müssen die Werte von R1 und R2 überprüft werden. Wenn sie die erforderlichen Zahlen finden, setzen Sie R5 auf Null. Der Wert von R5 wird über eine spezielle Variable, die fest mit diesem Register verbunden ist, in den Syshech-Code übertragen. Die in Assembler zusammenzusetzende Adresse ist implizit. Wir wissen nur, dass in arm-none-eabi der erste Parameter der Funktion in R0 platziert wird.

 bool cpu_check_address(volatile const char *address) { /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */ (void)address; /* R5 will be set to 0 by HardFault handler */ /* to indicate HardFault has occured */ register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" /* set default R5 value */ "ldr r1, =0xDEADF00D \n" /* set magic number */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic to be sure */ "ldrb r3, [r0] \n" /* probe address */ ); return result; } 

Der Code des HardFault-Handlers in seiner einfachsten Form sieht folgendermaßen aus:

 __attribute__((naked)) void hard_fault_default(void) { /* Get stack pointer where exception stack frame lies */ __asm__ volatile ( /* decide if we need MSP or PSP stack */ "movs r0, #4 \n" /* r0 = 0x4 */ "mov r2, lr \n" /* r2 = lr */ "tst r2, r0 \n" /* if(lr & 0x4) */ "bne use_psp \n" /* { */ "mrs r0, msp \n" /* r0 = msp */ "b out \n" /* } */ " use_psp: \n" /* else { */ "mrs r0, psp \n" /* r0 = psp */ " out: \n" /* } */ /* catch intended HardFaults on Cortex-M0 to probe memory addresses */ "ldr r1, [r0, #0x04] \n" /* read R1 from the stack */ "ldr r2, =0xDEADF00D \n" /* magic number to be found */ "cmp r1, r2 \n" /* compare with the magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x08] \n" /* read R2 from the stack */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic number to be found */ "cmp r1, r2 \n" /* compare with 2nd magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x18] \n" /* read PC from the stack */ "add r1, r1, #2 \n" /* move to the next instruction */ "str r1, [r0, #0x18] \n" /* modify PC in the stack */ "ldr r5, =0 \n" /* set R5 to indicate HardFault */ "bx lr \n" /* exit the exception handler */ " regular_handler: \n" /* here comes the rest of the fucking owl */ ) 

Beim Verlassen des Ausnahmehandlers wirft Cortex die Register, die garantiert vom Handler (R0-R3, R12, LR, PC ...) beschädigt werden, auf den Stapel. Das erste Fragment - es ist bereits in den meisten vorgefertigten HardFault-Handlern enthalten, mit Ausnahme derjenigen, die unter reinem Bare-Metal geschrieben sind - bestimmt, welcher Stack: Bei der Arbeit im Betriebssystem kann es sich entweder um MSP oder PSP handeln, und sie haben unterschiedliche Adressen. In Bare-Metal-Projekten wird der MSP-Stack (Main Stack Pointer) normalerweise a priori ohne Überprüfung installiert, da der PSP (Process Stack Pointer) aufgrund fehlender Prozesse nicht vorhanden sein kann.

Nachdem wir den gewünschten Stapel bestimmt und seine Adresse in R0 eingegeben haben, lesen wir die Werte R1 (Offset 0x04) und R2 (Offset 0x08) daraus und vergleichen ihn mit magischen Wörtern. Wenn beide übereinstimmen, lesen wir den PC-Wert (Offset 0x18) aus dem Stapel und addieren 2 (2 Bytes - die Größe der Anweisung in Cortex-M *) und speichern Sie sie zurück auf dem Stapel. Wenn dies nicht getan wird, befinden wir uns bei der Rückkehr vom Handler in derselben Anweisung, die die Ausnahme tatsächlich verursacht hat, und laufen immer im Kreis. Anhang 2 bringt uns zum Zeitpunkt der Rückgabe zur nächsten Anweisung.

* Upd. In den Kommentaren stellte sich die Frage nach der Größe der Anweisungen auf dem Cortex-M. Ich werde hier die richtige Antwort geben: In diesem Fall verursacht der Absturz den LDRB-Befehl, der in der ARMv7-M-Architektur in zwei Versionen verfügbar ist - 16-Bit und 32-Bit. Die zweite Option wird ausgewählt, wenn mindestens eine der Bedingungen erfüllt ist:

  • Der Autor hat ausdrücklich die Anweisung LDRB.W anstelle von LDRB angegeben (wir nicht).
  • Register über R7 werden verwendet (für uns - R0 und R3)
  • Es wird ein Offset von mehr als 31 Byte angegeben (wir haben keinen Offset).


In allen anderen Fällen (d. H. Wenn die Operanden zum Format der 16-Bit-Version des Befehls passen) muss der Assembler die 16-Bit-Version auswählen.

Daher gibt es in unserem Fall immer eine 2-Byte-Anweisung, die übersprungen werden muss. Wenn Sie den Code jedoch stark bearbeiten, sind Optionen möglich.

Als nächstes schreiben Sie 0 in R5, was als Indikator für den Einstieg in HardFault dient. Register nach R3 werden nicht vor speziellen Registern im Stapel gespeichert, und beim Beenden des Handlers werden sie in keiner Weise wiederhergestellt. Daher ist es unser Gewissen, sie zu verderben oder nicht zu verderben. In diesem Fall ändern wir R5 gezielt von 1 auf 0.

Die Rückgabe vom Interrupt-Handler erfolgt auf genau eine Weise. Bei der Eingabe des Handlers wird ein spezieller Wert in das LR-Register EXC_RETURN geschrieben, der zum Beenden des Handlers auf den PC geschrieben werden muss - und zwar nicht nur, sondern mit einem POP- oder BX-Befehl (dh „mov pc, lr funktioniert beispielsweise nicht“) , obwohl es Ihnen beim ersten Mal so scheint, als ob es funktioniert). BX LR sieht aus wie ein Versuch, zu einer bedeutungslosen Adresse zu wechseln (in LR gibt es so etwas wie 0xFFFFFFF1, das nichts mit der tatsächlichen Adresse der Prozedur zu tun hat, zu der wir zurückkehren müssen), aber in Wirklichkeit sieht der Prozessor diesen Wert auf dem PC (wohin er gehen wird) automatisch) werden die Register aus dem Stapel wiederhergestellt und unsere Prozedur fortgesetzt - mit der nächsten Prozedur nach dem Aufruf von HardFault, da wir den PC in diesem Stapel manuell um 2 erhöht haben.

Sie können natürlich klar über alle Offsets und Befehle lesen, wo .

Nun, oder wenn magische Zahlen nicht sichtbar sind, wird alles an den regulären_Handler gesendet. Danach folgt das übliche HardFault-Verarbeitungsverfahren. In der Regel ist dies eine Funktion, die Registerwerte an die Konsole druckt, entscheidet, was als nächstes mit dem Prozessor zu tun ist usw.

RAM-Größenbestimmung


All dies zu verwenden ist einfach und unkompliziert. Wir möchten eine Firmware schreiben, die auf mehreren Mikrocontrollern mit unterschiedlichem RAM funktioniert, während jedes Mal RAM in einem vollständigen Programm verwendet wird.

Ja einfach:

 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); } 

Hier ist dann die maximale Größe erforderlich, damit bei der maximal möglichen RAM-Größe zwischen ihm und dem nächsten Adressblock keine Lücke entsteht, in der cpu_check_address unterbrochen wird. In diesem Beispiel sind es 80 KB. Es macht auch keinen Sinn, alle Adressen zu prüfen. Sehen Sie sich einfach das Datenblatt an, um zu sehen, was der minimal mögliche Schritt zwischen den beiden Controller-Modellen ist, und legen Sie ihn als Block fest.

Programmatischer Übergang zum Bootloader mitten im Nirgendwo


Manchmal können Sie kompliziertere Tricks ausführen. Stellen Sie sich beispielsweise vor, Sie möchten programmgesteuert zum werkseitigen Bootloader STM32 springen, um über UART oder USB in den Firmware-Aktualisierungsmodus zu wechseln, ohne den Bootloader schreiben zu müssen.

Der STM32-Bootloader befindet sich im Bereich Systemspeicher, zu dem Sie gehen müssen, aber es gibt ein Problem: Dieser Bereich hat unterschiedliche Adressen nicht nur auf verschiedenen Prozessorserien, sondern auch auf verschiedenen Modellen derselben Serie (eine epische Platte finden Sie in AN2606 on Seiten 22 bis 26). Wenn Sie der Plattform im Allgemeinen und nicht nur einem bestimmten Produkt die entsprechende Funktionalität hinzufügen, möchten Sie Vielseitigkeit.

In CMSIS-Dateien fehlt auch die Startadresse des Systemspeichers. Es ist nicht möglich, es anhand der Bootloader-ID zu ermitteln, da Dies ist ein Henne-Ei-Problem. Die Bootloader-ID befindet sich im letzten Byte des Systemspeichers, wodurch wir zur Frage der Adresse zurückkehren.

Wenn wir uns jedoch die STM32-Speicherkarte ansehen, sehen wir ungefähr Folgendes:


In diesem Fall interessieren wir uns für die Systemspeicherumgebung - oben befinden sich beispielsweise ein einmal programmierbarer Bereich (nicht in allen STM32) und Optionsbytes (in allen). Diese Struktur wird nicht nur in verschiedenen Modellen, sondern auch in verschiedenen STM32-Leitungen beobachtet, wobei der einzige Unterschied im Vorhandensein von OTP und im Vorhandensein einer Lücke in den Adressen zwischen dem Systemspeicher und den Optionen besteht.

In diesem Fall ist es für uns jedoch am wichtigsten, dass sich die Startadresse von Option Bytes in den regulären CMSIS-Headern befindet - dort heißt sie OB_BASE.

Weiter einfach. Wir schreiben die Funktion zum Suchen nach der ersten gültigen oder ungültigen Adresse nach unten oder oben von der angegebenen:

 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; } 

Schauen Sie von den Optionsbytes nach unten, zuerst zum Ende des Systemspeichers oder des angrenzenden OTP und dann zum Beginn des Systemspeichers in zwei Durchgängen:

 /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; 

Und ohne große Schwierigkeiten ordnen wir dies in eine Funktion ein, die den Anfang des Systemspeichers findet und darauf springt, dh den Bootloader startet:

 static void jump_to_bootloader(void) __attribute__ ((noreturn)); /* Sets up and jumps to the bootloader */ static void jump_to_bootloader(void) { /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ 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); /* Reset the stack pointer */ __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); } 

Es hängt vom spezifischen Prozessormodell ab ... nichts hängt davon ab. Die Logik funktioniert nicht bei Modellen, die eine Lücke zwischen OTP und Systemspeicher aufweisen - ich habe jedoch nicht überprüft, ob überhaupt welche vorhanden sind. Arbeitet aktiv mit OTP - check.

Andere Tricks gelten nur für das übliche Verfahren zum Aufrufen des Bootloaders aus Ihrem Code. Vergessen Sie nicht, den Stapelzeiger zurückzusetzen und das Verfahren zum Verlassen des Bootloaders aufzurufen, bevor Sie Peripheriegeräte, Taktraten usw. initialisieren. Aufgrund seines Minimalismus kann der Bootloader verstopfen Initialisieren Sie die Peripherie und erwarten Sie, dass sie sich im Standardzustand befindet. Eine gute Option, um den Bootloader von einer beliebigen Stelle in Ihrem Programm aus aufzurufen, besteht darin, in das RTC-Sicherungsregister oder einfach an eine bekannte Adresse im Speicher einer magischen Nummer zu schreiben, das Programm neu zu starten und in den ersten Phasen der Initialisierung dieser Nummer zu überprüfen.

PS Da alle Adressen auf der Prozessorspeicherkarte im schlimmsten Fall um 4 ausgerichtet sind, wird das obige Verfahren durch die Idee, sie in Schritten von 4 Bytes anstelle von 1 zu durchlaufen, erheblich beschleunigt.

Wichtiger Hinweis


NB: Bitte beachten Sie, dass auf einem bestimmten Controller die Gültigkeit einer bestimmten Adresse nicht unbedingt das tatsächliche Vorhandensein von Funktionen anzeigt, die sich möglicherweise an dieser Adresse befinden. Beispielsweise kann die Adresse des Registers, das einen optionalen Peripherieblock steuert, gültig sein, obwohl der Block selbst in diesem Modell fehlt. Aus Herstellerseite sind die interessantesten Tricks möglich, die normalerweise auf der Verwendung derselben Kristalle für verschiedene Prozessormodelle beruhen. In den meisten Fällen funktionieren diese Verfahren jedoch und erweisen sich als sehr nützlich.

Source: https://habr.com/ru/post/de437256/


All Articles