Debugging: Multicore ist nicht gleich Multicore
Für MCU-Multicore-Architekturen in „Deeply-Embedded“-Anwendungen gelten andere Spielregeln als beim symmetrischen Multiprocessing auf identischen Cores mit Support eines kapselnden Betriebssystems. Eine Erkenntnis, die Chip-Anbieter, Tool-Hersteller und Anwender mit völlig neuen Herausforderungen konfrontiert.
Anbieter zum Thema

Im Bereich der klassischen Mikroprozessoren sind Multicore-Bausteine schon seit Jahren gang und gäbe. So stellte ARM mit ihrem MPCore bereits 2003 ein Multicore-Konzept vor, bei dem es allerdings zunächst nur darum ging, einen ARM-Core, einen digitalen Signalprozessor (DSP) und ggf. weitere Funktionseinheiten zu integrieren. Genutzt wurde das Konzept u.a. in den ab Ende 2008 verfügbaren Snapdragon-Prozessoren von Qualcomm. Intel präsentierte 2006 mit dem Intel Core 2 die erste Multicore-Architektur für PC-Anwendungen.
2008 folgte dann von ARM die erste Cortex-MPCore-Architektur. Zum Einsatz kam sie 2011 erstmals im mit zwei Cortex-A9-Cores und weiteren sechs Spezialprozessoren für Video, Audio usw. ausgestatteten Tegra 2-SoC, Herzstück des Smartphones P990 Optimus Speed von LG.
Bei herkömmlichen Mikroprozessoren haben wir es fast ausschließlich mit einem symmetrischen Multiprocessing (SMP) auf identischen Cores zu tun. In diesem Fall kapselt für den Applikationsentwickler in der Regel ein Betriebssystem die Aufteilung der Programmabarbeitung auf die einzelnen Cores.
Bei Multicore-Mikrocontrollern für Deeply-Embedded-Applikationen im Automotive- oder Industrie-Bereich hingegen sehen sich Halbleiter-Hersteller, Tool-Anbieter und die Anwender mit einer völlig neuen Ausgangssituation konfrontiert.
Unterschiedliche Cores miteinander kombinieren
Multi-Core-Bausteine aus der Qorivva-Familie von Freescale, die Aurix-Controller von Infineon, Teile der Vybrid-Familie von Freescale oder die LPC4300-Bausteine von NXP haben neben großen Unterschieden in der Leistungsfähigkeit und den Zielapplikationen allesamt eine große Gemeinsamkeit: dass für ein System-on-Chip unterschiedliche Cores miteinander kombiniert werden.
Bei den für Automobilelektronik-Applikationen mit hohem Leistungsbedarf prädestinierten Aurix- und Qorivva-Bausteinen sind dies unterschiedliche TriCore- bzw. e200-Cores. Die Multicore-Bausteine der Vybrid-Famile kombinieren wiederum einen Cortex-A5- und einen Cortex-M4-Prozessorkern von ARM, bei der LPC4300-Familie wurde einem Cortex-M4- noch ein Cortex-M0-Core zur Seite gestellt.
Die beiden letztgenannten MCU-Serien kommen vor allem in der Industrieautomatisierung oder in weniger leistungshungrigen Automotive-Anwendungen zum Einsatz.
Die Motivation, überhaupt Multicore-Architekturen einzusetzen, ist in den meisten Fällen der Wunsch nach höherer Performance und/oder Verringerung der elektrischen Leistungsaufnahme. Der vermeintlich naheliegende Weg, bei einer Single-Core-Architektur die Performance durch Erhöhung der Taktfrequenz beliebig zu steigern, stößt nämlich inzwischen zunehmend an physikalische Grenzen.
Leistungsaufnahme und Taktfrequenzen beherrschen
Nicht nur die bei höheren Taktfrequenzen zwangsläufige steigende Leistungsaufnahme und die damit verbundene Wärmeentwicklung auf dem Chip bereiten den Chip-Designern Kopfzerbrechen, auch die Beherrschbarkeit der hohen Taktfrequenzen an sich. Die Verwendung mehrerer Cores ist diesbezüglich eine verlockende Alternative, weil sich hierdurch ein Leistungsschub ohne Erhöhung des Systemtaktes erreichen lässt, und die Zunahme der elektrischen Leistungsaufnahme deutlich geringer als bei Erhöhung der Taktfrequenz ausfällt.
Die Vorteile von mehreren Kernen kommen zudem nur bei einer Änderung der auf den Systemen laufenden Software zum Tragen. Aufgrund des asymmetrischen Aufbaus der Hardware und der aus den Anwendungen resultierenden strengen Anforderungen an deterministisches Zeitverhalten kommt typischerweise in diesen Bereichen kein Betriebssystem zum Einsatz, das eine Aufteilung von Aufgaben der Applikation auf verschiedene Cores dynamisch zur Laufzeit vornimmt.
Die Festlegung, welcher Anwendungsteil auf welchem Core läuft, wird vielmehr schon zum Zeitpunkt des Bildens der Applikation vorgenommen. Die Ideallösung wäre, eine Single-Core-Software für mehrere Kerne neu zu übersetzen. Davon sind wir zum gegenwärtigen Zeitpunkt allerdings weit entfernt.
Herausforderungen neben-läufiger Programmausführung
Nebenläufigkeit, also das wirklich gleichzeitige Ausführen von mehreren unterschiedlichen Tasks, ist wohl die größte Herausforderung bei der Entwicklung von Multicore-Applikationen. Weil man ohne spezielle Vorkehrungen nicht mehr sagen kann, welches Ereignis in welcher Reihenfolge auftritt, ist das Beobachten von Fehlern ungleich schwieriger geworden.
Bei nebenläufiger Ausführung kommt es immer wieder zu Fehlern, weil Ereignisse in der falschen Reihenfolge ablaufen oder Synchronisationsmechanismen fehlerhaft verwendet werden. Unglücklicherweise sind solche Fehler aber weder gut beobachtbar noch sicher reproduzierbar, weil sich durch den Einfluss des Debuggers auf die Laufzeit Ereignisabfolgen gänzlich ändern können und Fehler dadurch mitunter nicht mehr auftreten.
Cache-Architektur und Ausführungsgeschwindigkeit
Für den Informationsaustausch zwischen den einzelnen Tasks oder die Synchronisation werden gerne von allen Cores sichtbare globale Speicherbereiche benutzt. Problematisch wird es allerdings, wenn Lese- und vor allem Schreibzugriffe auf diese Speicherbereiche durch die lokalen Caches der einzelnen Cores geleitet werden. Dann hängt es nämlich von der Cache-Architektur ab, ob der aktuell geschriebene Wert auch tatsächlich für alle anderen Cores sichtbar ist, oder ob noch immer der alte Wert gelesen wird. Um die Inkonsistenz der Speicherstellen erkennen zu können, ist für den Entwickler ein konsistenter Snapshot des Gesamtsystems unabdingbar.
Es ist übrigens gar nicht so selten, das beim Wechsel zu Multi-Core-Lösungen die erreichte Ausführungsgeschwindigkeit ganz und gar nicht den Erwartungen entspricht. Ein sehr wichtiger Aspekt bei der Verteilung der Aufgaben auf die einzelnen Cores ist nämlich deren Anteil an der Gesamtrechenlast und der Kommunikationsbedarf. Eine ungünstige Partitionierung kann im Extremfall dazu führen, dass ein Core zu großen Teilen ausgelastet und andere die meiste Zeit mit Warten beschäftigt sind.
Ein weiteres Problem: Die meist zum Einsatz kommende Programmiersprache C bietet keine Mittel zur Aufteilung von Code und/oder Daten auf mehrere Cores. Gegenwärtig gibt es dazu von den Compiler-Herstellern unterschiedliche Lösungsansätze. Zum einen wird der C-Sprachumfang proprietär z.B. mit Attribut-Schlüsselwörtern oder 'pragma'-Direktiven erweitert. Beim anderen Ansatz erfolgt die Partitionierung der Software auf Linker-Ebene.
Multicore-Loader für alle Fälle einsetzbar
Damit lassen sich Code und Daten flexibel verschieben und der Quelltext bleibt zum C-Sprachstandard konform. Das als Quasi-Standard etablierte ELF/DWARF-Format für die Ausgabedateien, die sowohl die binären Muster für den On-Chip-Flash als auch symbolische Informationen für den Debugger enthalten, kennt auch keine Multicore-Aspekte. Auch die Frage, ob eine ELF-Datei für das gesamte Multicore-System erzeugt wird oder je eine pro Core, ist noch nicht entschieden. Möglicherweise setzen sich in der Praxis auch beide Ansätze durch.
Um für jede Situation gewappnet zu sein, enthält die UDE von PLS einen Multicore-Loader, der für alle genannten Fälle einsetzbar ist (Bild 1). Er erlaubt das getrennte Laden von Speicher-Images und symbolischen Informationen aus binären Dateien vom Typ Hex- und SRecord sowie ELF/DWARF-Dateien.
Auch die Steuerung und Kontrolle einer Multicore-Applikation durch den Debugger erfordert vom Entwickler ein gewisses Umdenken. Beim Single-Core-Debugging ist eindeutig geregelt, wie der Debugger mit einem Breakpoint umzugehen hat. Jeder weiß, was das Resultat eines Single-Maschinenbefehl-Steps ist. Bei mehreren parallel laufenden Cores ist allerdings nicht mehr sofort klar, wie sich ein Breakpoint oder ein Single-Step auf das Gesamtsystem auswirken soll.
Synchronisation in die Debug-Probe verlagern
Der Anwender muss sich entscheiden, ob ein Breakpoint einen, mehrere oder gar alle Cores anhalten soll und ob während des Steppens durch die Software eines Cores alle anderen Cores unabhängig davon weiterlaufen sollen oder nicht. Von entscheidender Bedeutung sind in diesem Zusammenhang auch die für die Synchronisation verwendbaren Mechanismen.
Erfolgt dieser Abgleich erst in der Debugger-Software auf dem PC, gibt es eine für die meisten Anwendungsfälle völlig inakzeptable Abweichung im Millisekundenbereich. Eine Verschiebung in den Mikrosekundenbereich kann durch die Verlagerung der Synchronisation in die verwendete Debug-Probe erzielt werden. Das ist insbesondere dann hilfreich bzw. ausreichend, wenn auf dem Chip keine entsprechende Unterstützung zur Verfügung steht und die Taktfrequenz nicht allzu hoch ist.
Nexus-Erweiterungen und Trigger-Switch bei hohem Takt
Für sehr hoch getaktete Systeme reicht diese mit vergleichsweise wenig Aufwand verbundene Maßnahme in den meisten Fällen jedoch nicht aus. Dann wird eine zusätzlich auf dem Chip integrierte Logik benötigt, die ein nahezu taktgenaues Anhalten und Starten der einzelnen Cores mit einem Versatz im Nanosekundenbereich ermöglicht.
Hier gibt es verschiedene Lösungsansätze, aber keine wirklich etablierten Standards. Freescale und STMicroelectronics verwenden in ihren MPC57xx- (Qorivva) bzw. SPC57-Familien Nexus-Erweiterungen. Bei den Aurix-Bausteinen von Infineon hingegen kommt ein Trigger-Switch zum Einsatz. Ungeachtet dieser unterschiedlichen zugrundeliegenden Mechanismen wird im Debugger eine abstrahierte Darstellung für den Anwender benötigt.
In der UDE von PLS wird dies durch Run-Control-Gruppen realisiert (Bild 2). Dabei lassen sich Debugger mehrerer Cores zu Gruppen zusammenfassen. Eine Gruppe kann eine Kombination folgender Eigenschaften aufweisen: Gemeinsamer Start der Cores, gemeinsamer Stop der Cores durch den Nutzer, gemeinsamer Stop der Cores bei Erreichen eines Breakpoints oder Debug-Events durch mindestens einen Core sowie gemeinsames Single-Step. Der Debugger nutzt die auf dem jeweiligen SoC vorhandenen Einheiten zur Umsetzung der Funktionalität. Der Nutzer gewinnt mit diesem verallgemeinerten Konzept eine hohe Flexibilität für die Steuerung seines Multicore-Targets ohne die zugrundeliegende Logik im Detail kennen zu müssen.
Für einen Debugger erfolgt die Verbindung zu den einzelnen Cores typischerweise über eine gemeinsame Schnittstelle an der MCU. Mit Unterstützung einer On-Chip-Debug-Logik können zwar die Kerne einzeln gesteuert und auch auf core-lokale Speicher zugegriffen werden. Viel spannender aber ist die Frage, wie der Entwickler ein Multicore-System in der Bedienoberfläche des Debuggers präsentiert bekommt. Theoretisch kann natürlich für jeden Core eine eigene Debugger-Instanz gestartet werden. Doch auch wenn heute für viele Entwicklerarbeitsplätze zwei oder mehr Monitore Standard sind, ist diese Möglichkeit als praktischen Gründen wohl nur als Notlösung zu betrachten.
Neuer Debug-Ansatz für Multicore-Systeme
Um ein effizientes Testen und Debuggen von Multicore-Systemen zu ermöglichen, bedarf es künftig völlig neuer Ansätze, die die Kontrolle von Multicore-Systemen in einer Bedienoberfläche ermöglichen. Bei der Universal Debug Engine (UDE) von PLS können über einen Target-Manager gezielt die Cores und Funktionseinheiten bestimmt werden, die durch den Debugger kontrolliert werden sollen.
Das Konzept wird dabei ergänzt durch die Definition von Sichtbarkeitsgruppen für die Fenster einzelner Cores oder völlig nutzerspezifisch über die core-spezifische Einfärbung von Elementen der Bedienoberfläche (Bild 3). Falls die Beobachtung des zeitlichen Verhaltens mehrerer Cores zueinander von Bedeutung ist, wird heutzutage in der Regel auf On-Chip-Trace zurückgegriffen.
Nicht nur, dass sich mit Hilfe von On-Chip-Trace einfach detaillierte Performance-messungen für Buszugriffe, Cacheverhalten etc. durchführen lassen. Auch die Softwareverifikation profitiert vom On-Chip-Trace. So können damit Coverage-Analysen zur Ermittlung der Testmustergüte ganz ohne Code-Instrumentierung erfolgen.
Nicht zuletzt lässt sich mit Hilfe von On-Chip-Trace auch der Einfluss des Debuggers auf das Laufzeitverhalten des Embedded-Systems eliminieren, da die Debug-Informationen dem Trace und nicht durch konkurrierende Buszugriffe über die Debug-Schnittstelle vom Target selbst entnommen werden.
Schnittstelle zur Trace-Datenübertragung als Engpass
So weit so gut, wäre da nicht die Schnittstelle, über die die Trace-Daten vom Chip zum Debugger übertragen werden müssen. Mit dem verstärkten Einsatz von Multicore-Systemen und den immer höheren Taktraten erweist sie sich häufiger als ziemlich enger Flaschenhals. Zur Lösung des Dilemmas wird zum einen versucht:
- die anfallende Datenmenge weiter zu verringern,
- zum anderen kommen neuartige Schnittstellen mit drastisch höherer Bandbreite zum Einsatz.
Bei der ersten Variante werden die anfallenden Trace-Daten bereits auf dem Chip vorverarbeitet und gefiltert. Einige Chiphersteller haben dafür ihre Trace-Einheiten um eine spezielle Trigger- und Filterlogik erweitert. Einmal durch den Debugger konfiguriert, erlauben diese erweiterten Trace-Einheiten eine genaue Auswahl dessen, was aufgezeichnet werden soll und was nicht.
Eine typische Anwendung ist die Beobachtung und Performance-Messung von Bus-transfers zwischen mehreren Cores. Die Trigger und Filter lassen sich über die entsprechende Datenadresse so einzustellen, dass nur Informationen aufgezeichnet werden, die ursächlich mit diesem Transfer zusammenhängen. Dadurch reduziert sich die erforderliche Bandbreite des Trace-Ports natürlich erheblich. Die umfangreichsten Filter und Trigger-Möglichkeiten bietet die Multi-Core-Debug-Solution (MCDS) von Infineon.
Emulation Devices in spezieller Debug-Hardware
Ein ähnliches Konzept, jedoch mit etwas anderem Funktionsumfang, ist auch für die neue MPC57xx-Familie von Freescale bzw. die SPC57x-Bausteine von STMicroelectronics verfügbar. Die für das Filtern und Triggern notwendige On-chip-Logik sowie der On-chip-Tracespeicher sind nur in sogenannten Emulation Devices enthalten, die dann in spezieller Debug-Hardware verbaut werden. Das Ganze ist weitaus komplexer als die oben angesprochene Run-Control-Logik und umfasst viele hundert Register.
Um einem Entwickler die intuitive Programmierung der zusätzlichen Triggerlogik in seiner Begriffswelt zu ermöglichen, ist auch hier ein Werkzeug mit überdurchschnittlichen Abstraktionsmöglichkeiten von Nöten. Ein bereits in der Praxis bewährtes Konzept ist die schematische Konfiguration der Triggerlogik, basierend auf einem State-Machine-Modell, wie sie der Universal Emulation Configurator (UEC) von PLS verwendet (Bild 4).
Schnelle serielle Schnittstellen als Alternative
Die andere Lösung ist der Einsatz von seriellen Hochgeschwindigkeitsschnittstellen, um die physikalischen Grenzen eines parallelen Trace-Busses zu überwinden. Hier kommt häufig das Aurora-Protokoll von Xilinx zum Einsatz, mit welchem gegenwärtig Datenraten von mehreren GBit/s möglich sind. Auf der Werkzeugseite ist hier neben der entsprechenden Hardware, die in der Lage ist, solche Daten aufzunehmen und zu speichern, vor allem ein entsprechendes Trace-Framework erforderlich.
Dieses muss nicht nur in der Lage sein, große Datenmengen von verschiedenen Cores und Quellen zu verarbeiten, sondern auch entsprechende grafische Darstellungen. Generell sind zwei unterschiedliche Nutzungsarten großer Trace-Datenmengen erkennbar. Zum einen die statistische Auswertung für Code-Coverage und Profiling-Funktionen, zum anderen die konventionelle Verwendung zum Aufspüren von fehlerhaften Programmabläufen. Letzteres verlangt vor allem ausgefeilte Suchalgorithmen in den großen Datenmengen.
:quality(80)/images.vogel.de/vogelonline/bdb/1628900/1628983/original.jpg)
Software für Multicore-Systeme entwickeln
:quality(80)/images.vogel.de/vogelonline/bdb/1092500/1092518/original.jpg)
Multicore
Effiziente Embedded-Multicore-Programmierung
* * Heiko Riessland leitet das Produktmarketing bei PLS Programmierbare Logik & Systeme, Lauta.
(ID:35885740)