Debugging Zufällige Interrupts in Multicore-Umgebungen

Von Aaron Bauch *

Mit der steigenden Zahl von komplexen Anwendungen nimmt auch der Einsatz von Multicore-Prozessoren zu. Auch hier ist das Debugging ein unabdingbarer Bestandteil der Entwicklung. Aber: Wie fängt der Entwickler in sehr komplexen Anwendungen zufällige Interrupts ab oder behält die Kontrolle über die Interrupt-Verarbeitung? Es gibt Mittel und Wege, um mit diesen zufälligen Interrupts umzugehen – und einige nützliche Tools für die Entwicklung von Arm-basierten Anwendungen.

Anbieter zum Thema

Viele Entwickler deaktivieren gerne sämtliche Interrupts, um gleichzeitige oder zufällige Interrupts zu vermeiden. Das neigt allerdings gerade bei asynchronen Multicore-Systemen dazu, ihre Effizienz zu schmälern. Die richtigen Tools können dabei helfen, gerichtete wie auch zufällige Interrupts besser zu beherrschen.
Viele Entwickler deaktivieren gerne sämtliche Interrupts, um gleichzeitige oder zufällige Interrupts zu vermeiden. Das neigt allerdings gerade bei asynchronen Multicore-Systemen dazu, ihre Effizienz zu schmälern. Die richtigen Tools können dabei helfen, gerichtete wie auch zufällige Interrupts besser zu beherrschen.
(Bild: Clipdealer)

In Embedded-Systemen ist die Verwendung von Interrupts eine Methode zur Verarbeitung externer Ereignisse, die naturgemäß nicht mit der auf dem System laufenden Software synchronisiert sind. Wenn ein Interrupt-Ereignis eintritt, zum Beispiel wenn unvermittelt eine Taste gedrückt wurde, stoppt der Kern im Allgemeinen sofort die Ausführung des laufenden Codes und beginnt stattdessen mit der Ausführung einer Interrupt Service Routine (ISR).

Wenn der Interrupt Service Code die Reaktion auf das externe Ereignis abgeschlossen hat, sollte der Prozessor mit der Anweisung fortfahren, die auf die Anweisung folgt, die vor der ISR ausgeführt wurde. Läuft alles korrekt, sollte der Hauptanwendungscode nicht einmal „merken“, dass ein Interrupt stattgefunden hat. Er wurde einfach für die Zeit angehalten, während die Interrupt Service Routine lief. Wichtig ist in jedem Fall, dass der Zustand der Anwendung nach dem Interrupt wiederhergestellt wird, einschließlich der Werte von Prozessorregistern und des Prozessorstatusregisters. Dadurch ist es möglich, die Ausführung des ursprünglichen Codes fortzusetzen, nachdem der Code, der den Interrupt behandelt hat, ausgeführt wurde.

Bildergalerie
Bildergalerie mit 5 Bildern

Ein professioneller Compiler unterstützt die Syntax zum Schreiben von Interrupts, Software-Interrupts und schnellen Interrupts in C/C++. Für jeden Interrupt-Typ kann eine Interrupt-Routine geschrieben werden, aber die Syntax und die Handhabung hängen von der MCU-Implementierung ab. Als zum Beispiel Arm die Varianten Cortex A, R und M definierte, wurden auch die Hardware und die Mechanismen für die Interrupt-Verarbeitung für jede dieser Familien festgelegt. Bei früheren Chips der Varianten Arm 7, Arm 9 usw. variierten das Design und der Betrieb des Interrupt-Controllers je nach Chiphersteller, was die Portabilität erschwerte.

Eine moderne kommerzielle Toolchain berücksichtigt diese Unterschiede und macht die Implementierung von Interrupt-Unterstützung im Code so transparent wie möglich – unabhängig von der zugrunde liegenden Interrupt-Hardware und den Mechanismen für das Interrupt-Management.

Eine allgemein bekannte und häufig diskutierte Praxis ist, den Interrupt-Code so kurz wie möglich zu machen. Dadurch wird sichergestellt, dass die CPU schnell zur Hauptaufgabe zurückkehren kann. Die Interrupt Service Routine sollte nur den notwenigen Code ausführen, und der Rest der Aufgabe kann durch Setzen einer Flag-Variablen an den Hauptprozess übergeben werden. Ruft man eine normale Funktion von einer Interrupt-Service-Routine aus auf, kann es zu einem unerwarteten Aufblähen des Codes kommen, da ungenutzte Register erhalten bleiben.

Eine nützliche Technik zum Managen dieses Prozesses ist die zeitversetzte Verarbeitung mit Hilfe von Software-Interrupts. Hier löst der Hardware-Interrupt, z.B. der oben erwähnte Tastendruck, eine Interrupt Service Routine mit hoher Priorität aus. Diese Routine kümmert sich um die Dinge, die sofort erledigt werden müssen, z.B. das Zurücksetzen der Taste, damit sie wieder bedient werden kann. Die ISR kann jedoch einen Software-Interrupt mit niedrigerer Priorität auslösen, der dann ausgeführt wird, wenn er aktuell das Ereignis mit der höchsten Priorität im System ist. Ist seine Priorität niedriger als einige wichtige, zeitkritische Anwendungsfunktionen, wird der Interrupt zurückgestellt, und die Ausführung dieser Funktionen findet zuerst statt. Ist seine Ausführungspriorität jedoch höher, wird er ausgeführt und die weniger zeitkritischen Funktionen, die für die Verwaltung des Interrupt-Ereignisses erforderlich sind, werden pausiert.

Gibt es gute Gründe für Funktionsaufrufe innerhalb einer ISR, ist das Wichtigste, dass der Compiler Informationen erhält, damit er den Code so gut wie möglich optimieren kann. Im Allgemeinen muss geprüft werden, was in der speziellen Situation passiert. Aber es kann auch versucht werden, die aufgerufene Funktion a) statisch und b) in derselben Datei (Kompiliereinheit) wie die ISR zu definieren. Dadurch weiß der Compiler genau, welche Register von der aufgerufenen Funktion beim Kompilieren der ISR verwendet werden, und dem Compiler wird vorgegeben, dass diese Funktion nur innerhalb derselben Quelldatei (Kompiliereinheit) aufgerufen wird. Wenn es ein Pragma oder eine Option gibt, die den Compiler dazu bringt, Inline-Code zu erzeugen, ist das auch einen Versuch wert. Mit dieser Information kann der Compiler die aufgerufene Funktion inline schreiben, was weitere Optimierungen ermöglicht, ähnlich wie wenn die aufgerufene Funktion tatsächlich inline geschrieben worden wäre (z.B. das Entfernen nicht benötigter Push/Pop-Paare). Damit der Code wunschgemäß und so effizient wie möglich ausgeführt wird, sollte die entsprechende Optimierung eingeschalten und der generierte Code geprüft werden.

Interrupts in Multicore-Systemen

Durch den Einsatz von Multicore-Prozessoren ist es möglich, die Belastung auszugleichen und die unterschiedlichen Stärken der verschiedenen Kerne zu nutzen. Unter Umständen lassen sich auch die Produktkosten senken, wenn ein Multicore-Prozessor anstelle mehrerer CPUs verwendet wird. Verschiedene Multicore-Systeme können Interrupts unterschiedlich handhaben. In komplexeren SMP (Symmetrische Multiprozessor)-Systemen, bei denen der Code willkürlich für die Ausführung auf einem von mehreren identischen Kernen geplant wird, kann die Interrupt-Behandlung auch auf der Grundlage eines Algorithmus zufällig den verfügbaren Kernen zugewiesen werden.

Jetzt Newsletter abonnieren

Verpassen Sie nicht unsere besten Inhalte

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung

Das interessantere Modell für Embedded-Systeme ist jedoch ein asymmetrischer Multi-Processing-Ansatz. Hier führt jeder Kern sein eigenes Programm aus und kommuniziert und synchronisiert sich von Zeit zu Zeit mit einem oder mehreren der anderen Kerne im System.

Im Allgemeinen ist jeder Interrupt bei dieser Art von System an eine einzige CPU gerichtet. Der programmierbare Interrupt-Controller (PIC) des Chips steuert, wie dies geschieht. Werden die PICs beim Starten des Systems initialisiert, kann man sie so programmieren, dass sie Interrupts für jede beliebige CPU bereitstellen.

Multicore-Arm-Chips verwenden häufig den Advanced Programmable Interrupt Controller (APIC) und einen integrierten Interrupt-Controller, der den Generic Interrupt Controller (GIC) implementiert. Dieser kann so konfiguriert werden, dass er E/A-Interrupts an bestimmte Kerne oder Gruppen von Kernen liefert. Darüber hinaus bietet er Interprozessor-Interrupts (IPI), die vom Betriebssystem zur Koordinierung der Aktivitäten mehrerer Kerne verwendet werden.

Vorteile von Multicore-Debugging

Interrupts sind in Single-Core-Systemen generell schwierig zu debuggen, da das Anhalten zur Überprüfung der Ausführung eines Interrupt-Handlers im Debugger nicht gut mit dem typischen zeitkritischen Aspekt der Interrupt-Behandlung zusammenpasst. Noch komplexer wird es in Multicore-Szenarien mit der Möglichkeit, Interrupts auszugleichen und einen anderen Kern zu setzen oder umzuleiten, falls ein Kern vollständig durch andere Interrupts belegt ist. Da Interrupts oft durch Peripheriegeräte oder externe Ereignisse ausgelöst werden, können bestimmte Fehler nur selten und scheinbar zufällig ausgelöst werden oder dadurch, dass die Interrupts mit einem falschen Kern oder einem belegten Kern verbunden sind.

Ein Multicore-Debugger kann im Gegensatz zu anderen Debuggern die Aufgabe erleichtern, indem er die auf mehreren Kernen laufenden Programme anzeigt. Er hat insbesondere die Fähigkeit, Kerne selektiv anzuhalten und zu starten, basierend auf Haltepunkten innerhalb einer Interrupt Service Routine (ISR) auf einem anderen Kern.

Das Multicore-Debugging kann auf zwei Arten durchgeführt werden: Durch symmetrisches Multicore-Debugging (SMP), also dem Debuggen von zwei oder mehr identischen Kernen. Oder durch asymmetrisches Multicore-Debugging (AMP), das heißt dem Debuggen von zwei oder mehr Kernen, die auf unterschiedlichen Architekturen basieren. Dabei kann es sich um zwei verschiedene Arm-Kerne handeln, zum Beispiel einen Cortex-A9 und einen Cortex-M0. Für ein optimales Ergebnis ist die volle Kontrolle über alle Kerne unerlässlich.

Die Mikrocontroller der Arm Cortex-Familie bieten mehrere erweiterte Debugging-Funktionen, die in früheren Arm-Mikrocontrollern nicht verfügbar waren. Das Serial-Wire-Debugging (SWD) mit der Serial-Wire-Output (SWO)-Schnittstelle ermöglicht den Zugriff auf Funktionen der CoreSight-Debugging-Infrastruktur, wie z. B. Zeitstempel und Verfolgung von Interrupt-Ereignissen.

Es gilt zu beachten, dass die meisten Software- und Hardware-Debugger in Multicore-Szenarien den Serial Wire Output (SWO) jeweils nur für einen der Kerne unterstützen. Dies kann am besten kombiniert werden, um einen der Kerne zu überwachen und Breakpoints und die Cross Trigger Interface (CTI)-Schnittstelle zu nutzen, um benachbarte Kerne selektiv zu stoppen und zu starten.

Das Interrupt-Protokoll liefert umfassende Informationen über die Interrupt-Ereignisse. Dies kann z. B. nützlich sein, um herauszufinden, welche Interrupts optimiert werden können, um die Geschwindigkeit zu erhöhen. Es lässt sich auch das Eintreten bzw. Verlassen des Interrupts protokollieren. Das Interrupt-Protokoll (Bild 2) gibt Aufschluss über die Ausführung von Interrupt-Dienstroutinen und die Dauer der Ausführung der einzelnen Routinen. Die Zeitstempelinformation kann entweder als Echtzeitwert oder in CPU-Taktzyklen vorliegen (siehe auch Bild 3).

Bildergalerie
Bildergalerie mit 5 Bildern

Aus der Zusammenfassung des Interrupt-Protokolls (Bild 3) wird ersichtlich, wie oft jeder Interrupt ausgelöst wurde und wie lange die Ausführung des ISR gedauert hat. Die Interrupt-Protokollinformationen können auch grafisch dargestellt werden (Bild 4).

Die grafische Darstellung zeigt, wann die einzelnen ISRs auch im Vergleich mit anderen ISRs aktiv waren und wie sie mit anderen zeitlich festgelegten Aktivitäten wie Datenprotokoll-Haltepunkten korrelieren.

Eine weitere nützliche und leistungsstarke Funktion bei der Arbeit und Fehlersuche mit Interrupts ist die Verwendung von Trace. Echtzeit-Trace ist eine kontinuierlich gesammelte Abfolge aller ausgeführten Anweisungen für einen ausgewählten Teil der Codeausführung. Mit Hilfe von Trace kann der Programmablauf bis zu einem bestimmten Zustand, z. B. einem Anwendungsabsturz, untersucht und der Ursprung des Problems lokalisiert werden. Trace-Daten können auch dabei helfen, Programmierfehler aufzuspüren, die untypische Auswirkungen haben und nur sporadisch auftreten, wie z. B. zufällige Interrupts.

In Multicore-Szenarien können Trace-Daten meist nur für einen Kern auf einmal angezeigt werden. Es gibt Prozessoren mit Trace-Funnels, die die Trace-Daten von jeder Quelle zu einem einzigen Datenfluss zusammenfassen und von einem komplexeren Chip gesteuert werden. Mit den Trace-Informationen eines Kerns und mit Hilfe der Cross Trigger Interface (CTI)-Schnittstelle lassen sich alle anderen Kerne stoppen und starten, so dass der Entwickler ein genaueres Bild vom Verhalten der Anwendung erhält.

Ein professioneller Trace-Debugger kann Informationen zu verschiedenen Aspekten einer Anwendung liefern, die während der Ausführung der Anwendung gesammelt werden. Diese Daten können in einer Zeitleiste dargestellt werden, die die Abfolge der vom Trace-System erfassten Funktionsaufrufe und -rückgaben zeigt. Gleichzeitig werden auch Informationen zum Timing zwischen den Funktionsaufrufen ermittelt. Dies hilft bei der Analyse des Verhaltens der Anwendung und bei der schnellen Navigation zum nächsten oder vorherigen Interrupt--Eintrag, wenn klar ist, welche Abläufe unterbrochen wurden. Der grafische Call-Stack kann die Abfolge der Funktionsaufrufe und -rückgaben anzeigen, die vom Trace-System erfasst werden. Er enthält auch die Interrupts und ISRs, die ausgelöst und bearbeitet wurden, inklusive der genauen Zeit und Zykluszählung.

Diese Funktion liefert umfassende Informationen über Ausnahmen und Interrupts im System. Sie ist z. B. nützlich, um festzustellen, welcher Interrupt für eine schnellere Ausführung optimiert werden kann oder um Probleme mit verschachtelten Interrupts zu analysieren.

Fazit: Interrupts nicht immer gleich deaktivieren

Viele Entwickler deaktivieren gerne sämtliche Interrupts, um gleichzeitige oder zufällige Interrupts zu vermeiden. Dies kann jedoch dazu führen, dass die Systeme langsamer reagieren und einige kritische Probleme in sich verstecken, die in der Zukunft ausgelöst werden und das Ganze verschlimmern könnten.

Die volle Kontrolle zu haben und zu verstehen, wann und warum Interrupts ausgelöst werden, ist der wichtigste Schritt, um eine gute Performance zu gewährleisten und dafür zu sorgen, dass sich die Anwendung genau wie erwartet verhält. Ein Debugger, der in der Lage ist, die gesamte Interrupt-Verarbeitung zu protokollieren, ist entscheidend, um zufällige Interrupts abzufangen und die Interrupt-Verarbeitung unter Kontrolle zu halten. //SG

* Aaron Bauch ist Senior Field Applications Engineer bei IAR Systems.

(ID:48068853)