PICKPLACE Hub

Debugging von HardFault mittels Handler in ARM Controllern

Geschrieben von Hendrik Schnack | Sep 30, 2023 6:13:00 PM

Das Debugging von HardFaults kann mitunter schwierig zu sein. Um im Falle sporadischer Fälle gewappnet zu sein, empfiehlt es sich durch geschickte Handler vorzubeugen. So können dann durch gezieltes Auslesen kritischer Stellen Schlüsse zur Fehlerherkunft und -ursache getroffen werden.

ARM-basierte Controller sind ein zentraler Bestandteil vieler eingebetteter Systeme und bieten beeindruckende Rechenleistung bei geringem Energieverbrauch. Dennoch kann die Komplexität eingebetteter Anwendungen zu unvorhergesehenen Fehlern führen. Die Fähigkeit, diese effektiv zu debuggen, ist daher von unschätzbarem Wert für Entwickler. Dieser Artikel konzentriert sich darauf, wie Sie HardFaults auf ARM Controllern mit spezifischen Registerinformationen identifizieren und debuggen können.

CMSIS-Faults

Der ARM Cortex-M-Kern implementiert eine Reihe von Fehlerausnahmen. Jede Ausnahme bezieht sich auf eine Fehlerbedingung. Tritt der Fehler auf, hält der ARM Cortex-M-Core die Ausführung des aktuellen Befehls an und verzweigt zur Handler-Funktion der Ausnahme. Dieser Mechanismus entspricht dem für Interrupts verwendeten, bei dem der ARM Cortex-M-Core zu einem Interrupt-Handler verzweigt, wenn er einen Interrupt akzeptiert.
Die CMSIS-Namen für die Fault-Handler lauten wie folgt:

  • UsageFault_Handler()
  • BusFault_Handler()
  • MemMang_Handler()
  • HardFault_Handler()

 

Unterschiede zwischen diesen werden zunächst wie folgt aufgezeigt:

 

UsageFault_Handler(): Usage Faults werden von einer Anwendung verursacht, die fälschlicherweise den Cortex M4-Prozessor verwendet und versucht, einen undefinierten Befehl auszuführen oder einen Befehl auszuführen, der das Execution Program Status Register (EPSR) unzulässig verwendet.

▪ Der Aufruf eines Befehls, der nicht Teil des ARM-Befehlssatzes ist (UNDEFINSTR)
▪ Das Springen zu einer gültigen Speicheradresse, ohne dass das Thumb-Bit gesetzt ist (INVSTATE)
▪ Das Dividieren einer beliebigen Zahl durch Null führt zum Division-by-Zero UsageFault (DIVBYZERO)

Die Erkennung des Divisionsfehlers durch Null ist standardmäßig deaktiviert, was bedeutet, dass eine solche Operation Null ergibt und der Fehler nicht erkannt wird. In ähnlicher Weise unterstützt der Cortex-M4-Prozessor bei bestimmten Befehlen den nicht ausgerichteten Zugriff. Die Erkennung von Fehlern sowohl bei der Division durch Null als auch beim nicht ausgerichteten Zugriff (für jeden Befehl) kann im Konfigurations- und Kontrollregister (CCR) aktiviert werden.

 

BusFault_Handler(): Bus-Fehler treten auf, wenn ein Bus-Slave eine Fehlerantwort zurückgibt, während einer der folgenden Operationen

  • Stacking für einen Ausnahmeeintrag
  • Entstapeln für eine Ausnahme-Rückgabe
  • beim Prefetching einer Anweisung
  • während der Floating-point lazy state preservation

Neben den oben aufgeführten Fehlern gibt es auch Busfehler, die als Precise und Imprecise bezeichnet werden. Ein ungenauer Busfehler tritt auf, wenn eine Anwendung in einen gepufferten Speicherbereich schreibt und mit der Ausführung nachfolgender Anweisungen fortfährt, bevor der eigentliche Busfehler erkannt wird. Daher zeigt der Programmzähler zu dem Zeitpunkt, zu dem die Ausnahme auftritt, nicht auf die Anweisung, die den Busfehler verursacht hat. Für Debugging-Zwecke ist ein "präziser" Programmzählerwert erforderlich, um zu wissen, welcher Befehl die Fehlerausnahme verursacht hat. Unpräzise Busfehler können durch Deaktivieren des Schreibpuffers in (ACTLR_DISDEFWBUF = 1) erzwungen werden, um präzise zu sein. Dies kann jedoch die Leistung beeinträchtigen.


MemManage_Handler(): Typischerweise treten diese Ausnahmen auf, wenn versucht wird, auf Bereiche zuzugreifen, die durch die ARM Cortex M4 Memory Protection Unit geschützt sind.

  • Versuch des Ladens oder Speicherns an einer geschützten Stelle
  • Befehlsabruf von einem geschützten Speicherplatz
  • Stacking/Upstacking-Fehler aufgrund einer Verletzung des Speicherschutzes
  • Verletzung des Schutzes während der Floating-Point Lazy State Preservation

 

HardFault_Handler(): Der HardFault ist diffiziler und beschreibt vor allem Probleme mit der Runtime. Dies können ungültige Pointer-Dereferences sein, Callbacks auf nicht gültige Speicherbereiche oder gescheiterte Zugriffe auf Register (zum Beispiel im Zuge fehlerhafter AMBA-Zugriffe). HardFaults sind daher grundsätzlich Programm-Ablauffehler und liegen in der Regel an falscher Programmierung.

 

Stack Trace Methode

Wenn ein Fehler ausgelöst wird, kann der Prozessor angehalten und der Prozessorstatus sowie die Speicherinhalte untersucht werden. Der Einsatz der Stack Trace Methode erfordert das Einsetzen von Breakpoints, sei es durch Hardware-Unterstützung oder manuelle Einbindung von Breakpoint-Anweisungen. 

Ein effizient implementierter HardFault Handler sollte daher:

  • melden, dass ein HardFault aufgetreten ist.
  • die Werte der Fault Status Register und Fault Address Register berichten.
  • zusätzliche Informationen aus dem Stack-Frame bereitstellen.

Es ist durchaus nicht unüblich alle vorher genannten Faults zu einem Handler zusammenzufassen. Dann muss im Handler zum besseren Verständnis und zur effektiveren Fehlerbehebung eine Auswertung bestimmter Prozessorregister erfolgen. Ohnehin müssen die BusFault, UsageFault und MemManage  speziell aktiviert werden und werden automatisch zu einem Hard Fault geroutet, wenn sie nicht aktiviert sind.

Daher müssen in einem HardFault Hanler auch die folgenden Register ausgewertet werden:

  • CFSR: Enthält Informationen darüber, welche Art von Ausnahme zuletzt ausgelöst wurde. Der CFSR zeigt die Ursache eines MemManage-, BusFault- oder UsageFault-Fehlers an.
  • HFSR: Bietet spezielle Flags für HardFaults, hat in der Regel aber wenig relevante Informationen.
  • MMFAR und BFAR: Zeigen Adressen an, die den letzten Memory-Management-Fehler bzw. Bus-Fehler verursacht haben.

 

Wichtiger aber allgemein ist, dass die Hardware automatisch mehrere CPU-Register auf den Stack schiebt, bevor sie den Hard Fault Handler aufruft. Diese können zur weiteren Fehlersuche nach der Ursache des Hard Fault eingesehen werden. Exakt dieses Feature macht sich ein HardFaultHandler zu Nutzen. Die folgende Abbildung zeigt den Aufbau solch eines StackTraces:

 

Automatischer Stacktrace im FaultHandler

 
HardFault Handler Implementierung

Zunächst braucht es einen kurzen Teil Assembly um den weak-Define des HardFault-Handlers zu überschreiben:

__attribute__((naked)) void HardFault_Handler(void) {
  __asm volatile(
      "tst lr, #4                                    \n"
      "ite eq                                        \n"
      "mrseq r0, msp                                 \n"
      "mrsne r0, psp                                 \n"
      "ldr r1, debugHardfault_address                \n"
      "bx r1                                         \n"
      "debugHardfault_address: .word debugHardfault  \n");
}

Mit mrseq r0, msp wird der Wert des Main Stack Pointers in Register R0 bewegt, aber nur wenn das Ergebnis der TST Instruktion gleich 0 war. Alternativ wird über mrsne r0, psp der Wert des Process Stack Pointers (PSP) in Register R0 bewegt. 

Über ldr r1, debugHardfault_address wird der Wert, der an der Adresse debugHardfault_address gespeichert ist, in das Register r1 geschrieben. Die statische Variable debugHardfault_address puffert den Pointer, den es in der Debugger-freundlichen C-Version auszulesen gilt:

 


void debugHardfault(uint32_t *sp) {
    
  uint32_t cfsr = SCB->CFSR;
  uint32_t hfsr = SCB->HFSR;
  uint32_t mmfar = SCB->MMFAR;
  uint32_t bfar = SCB->BFAR;
  
  uint32_t r0 = sp[0];
  uint32_t r1 = sp[1];
  uint32_t r2 = sp[2];
  uint32_t r3 = sp[3];
  uint32_t r12 = sp[4];
  uint32_t lr = sp[5];
  uint32_t pc = sp[6];
  uint32_t psr = sp[7];
  
  printf("HardFault:\n");
  printf("SCB->CFSR   0x%08lx\n", cfsr);
  printf("SCB->HFSR   0x%08lx\n", hfsr);
  printf("SCB->MMFAR  0x%08lx\n", mmfar);
  printf("SCB->BFAR   0x%08lx\n", bfar);
  printf("\n");
  printf("SP          0x%08lx\n", (uint32_t)sp);
  printf("R0          0x%08lx\n", r0);
  printf("R1          0x%08lx\n", r1);
  printf("R2          0x%08lx\n", r2);
  printf("R3          0x%08lx\n", r3);
  printf("R12         0x%08lx\n", r12);
  printf("LR          0x%08lx\n", lr);
  printf("PC          0x%08lx\n", pc);
  printf("PSR         0x%08lx\n", psr);
  
  while (1)
    ;
}

 

Das Programm verweilt in der while(1)-Schleife. Dies stellt sicher, dass der Stacktrace im Speicher bleibt. Ggf. können Debugger-spezifische BKPT-Instruktionen aufgerufen werden, um den Call aus dem HardFault-Handler direkt an den SWD zu melden.

 

Fazit

 

Zusammenfassend lässt sich sagen, dass durch die Verwendung der Informationen aus den Fehlerstatusregistern und dem entsprechenden Stack der Debugger die benötigten Daten liefert, um zu erkennen, welche Ausnahme aufgetreten ist und an welcher Stelle. Um dieses spezifische Problem weiter zu debuggen, muss das System mit einem Watchpoint neu gestartet werden, der auf den Funktionszeiger gesetzt wird, der korrumpiert wird. Dies wird die Wurzel des Problems offenlegen und ermöglicht den Entwicklern, eine zielgerichtete Lösung zu implementieren. Der HardFault Handler ist somit ein entscheidendes Werkzeug für die Diagnose und Behebung von Schwierigkeiten in eingebetteten ARM-Systemen, und durch Automatisierung dieses Prozesses kann die Effizienz des Debuggings deutlich gesteigert werden.