Bachelorarbeit · 2017 · Hochschule Esslingen
Zwei Prozessorkerne, ein geteilter Speicher, und ein Betriebssystem, das davon noch nichts wusste. Diese Arbeit bringt eCos auf einem ARM Cortex-A9 Dual-Core dazu, beide Kerne wirklich gleichzeitig zu nutzen — vom Booten des zweiten Kerns bis zu Spinlocks auf Register-Ebene.
eCos ist ein schlankes Echtzeitbetriebssystem für eingebettete Systeme. Es unterstützt SMP (Symmetric Multiprocessing) grundsätzlich auf Kernel-Ebene — aber ob das auf einer konkreten Hardware tatsächlich funktioniert, hängt von der Hardware Abstraction Layer (HAL) ab. Für den Cyclone V SoC existierte diese SMP-Anbindung schlicht noch nicht.
Diese Arbeit schließt genau diese Lücke: fünf ineinandergreifende Bausteine, jeder einzeln getestet, bevor der nächste darauf aufbaut.
CPU1 steckt nach dem Reset fest. Ein minimales Trampolin an einer eigens gemappten Adresse holt ihn heraus und schickt ihn an den echten Einstiegspunkt.
Der GIC muss für jeden Kern einzeln aktiviert werden, bevor Software Generated Interrupts (SGIs) überhaupt zwischen den Kernen laufen können.
Reschedule- und TimeSlice-Signale werden als SGIs verschickt — die beiden Kerne können sich jetzt gegenseitig den Scheduler anstoßen.
Zwei getrennte L1-Caches dürfen nicht unterschiedliche Wahrheiten über denselben Speicher haben. Die Snoop Control Unit übernimmt das — wenn MMU und Speicher-Attribute stimmen.
Echte Hardware-Lock-Primitiven über ldrex/strex — die letzte Schicht, auf der sich kritische Bereiche zwischen beiden Kernen sicher schützen lassen.
Von allem, was in dieser Arbeit schiefgehen konnte — das hier war das Schlimmste. Solange die Snoop Control Unit noch nicht korrekt konfiguriert war, hatten CPU0 und CPU1 für dieselbe Speicheradresse buchstäblich unterschiedliche Wahrheiten: jeder Kern sah den Wert aus seinemeigenen L1-Cache, nicht den tatsächlich aktuellen.
Das Tückische: es war nicht "immer falsch", dann wäre es trivial gewesen. Es warmanchmal falsch — abhängig davon, wann genau welche Cache-Line evicted wurde. Ein Absturz trat auf, verschwand beim nächsten Versuch wieder, tauchte drei Durchläufe später an einer völlig anderen Stelle erneut auf. Im Debugger nachzusteppen half nicht wirklich — das Stepping selbst verändert das Timing und damit genau die Bedingung, die den Fehler überhaupt erst auslöst. Kein Stack-Trace, kein fester Reproduktionspfad, einfach zwei Kerne, die sich nicht einig waren, was im Speicher steht.
Aufgelöst erst durch das genaue Verständnis aus der SCU-Architektur: die Snoop Control Unit korrekt aktivieren, und den betroffenen Speicher explizit alsshareable markieren — fehlt eines von beiden, bleibt die Hardware-Kohärenz wirkungslos, egal wie richtig der Rest der Implementierung ist.
CPU1 verlässt den Reset-Zustand und muss zur echten Startadresse springen — eigentlich ein paar Zeilen Assembler. In der Praxis hieß das: drei Tage lang mit ARM DS-5 Instruktion für Instruktion durch den Boot-Code steppen, bis klar war, wo genau es schiefging.
Der Absturz selbst war ein Data-Abort bei Adresse 0x0 — und das war kein Zufall, sondern Absicht, nur an der falschen Stelle: die Boot-Region ist in der MMU bewusst als Fault gemappt, damit Zugriffe auf Nullpointer abgefangen werden. Genau dieses Mapping stand dem eigenen Trampolin-Code im Weg, der zu diesem Zeitpunkt noch versuchte, direkt bei 0x0 zu starten.
Die Lösung war am Ende das CPU1-Trampolin: ein winziges Code-Fragment, das stattdessen über das zweite, nicht gecachte Mapping der SDRAM eingespielt wird, die echte Einstiegsadresse aus dem cpu1startaddr-Register lädt und erst von dort aus zum eigentlichen Code springt. Zwei ldr-Befehle — aber ohne die drei Tage im Debugger wäre nie klar geworden, warum CPU1 überhaupt nie ankam.
Jeder Interrupt im GIC durchläuft eine feste Zustandsmaschine: Inactive → Pending → Active → Inactive. Für normale Peripherie-Interrupts reicht es, beim Bestätigen nur die Interrupt-ID zurückzuschreiben — das CPU-ID-Feld ist für sie ohnehin immer null.
Für SGIs ist das CPU-ID-Feld aber nie null — und eCos' bestehender Interrupt-Code schrieb beim Bestätigen trotzdem nur die unteren 10 Bits zurück. Ergebnis: ein SGI ließ sich genau einmal auslösen. Jeder weitere Versuch lief ins Leere, weil der GIC den Interrupt noch als "Active" führte.
Die Lösung war am Ende minimal-invasiv: das komplette Register puffern statt nur die Interrupt-ID, und genau dieser volle Wert geht zurück ins EOI-Register. Zwei zusätzliche Assembler-Zeilen — aber ohne die präzise Diagnose der Zustandsmaschine wäre der eigentliche Fehler nie sichtbar geworden.
Ein Software Generated Interrupt entsteht einzig dadurch, dass ein Kern ein bestimmtes Register beschreibt —GICD_SGIR. Bit 17 geschrieben heißt "CPU0 an CPU1", Bit 16 heißt "CPU1 an CPU0".
SGIINTID wählt den Nachrichtentyp — 0 für Reschedule, 1 für TimeSlice — statt beide Signale durch geteilten Speicher zu unterscheiden, was eine funktionierende Cache-Kohärenz vorausgesetzt hätte, noch bevor diese überhaupt eingerichtet war.