Reverse Engineering: Was ein FPGA-Baustein steuert
Es ist mir in den vorherigen drei Teilen der Artikelserie bisher gelungen, Linux auf einem Oszilloskop zu installieren und eine ganze Reihe von Pins des Mikrocontrollers und ihre Funktion herauszufinden. Doch auf der Hauptplatine des Oszilloskops befindet sich noch ein FPGA, der wahrscheinlich genauso wichtig ist. Deshalb wil ich im vierten Teil herausfinden, welche Funktionen dessen Pins haben. Im Gegensatz zum Mikrocontroller kann ich auf dem FPGA aber nicht einfach ein paar einfache Programme laufen lassen.
Ich habe die GPIO-Pins des SoC analysiert, indem ich sie als Eingang konfiguriert und dann an den verschiedensten Stellen auf der Platine ein Signal angelegt habe. Ein selbstgeschriebener Linux-Treiber hat mir dann den zugehörigen Pin angezeigt. Mit der gleichen Methode will ich jetzt auch die Pins des FPGA analysieren. Der FPGA im Oszilloskop ist ein Xilinx Spartan 6 XC6SLX9.
Er hat einen integrierten Speichercontroller in der Pin-Bank 3, die sich im obigen Bild am oberen Rand des FPGA befindet. Der Speichercontroller ist hoffentlich mit dem Hynix-DDR2-Speicher direkt oberhalb des FPGA verbunden. Die Pin-Bank 2 ist auf der linken Seite des FPGA und wahrscheinlich über die Widerstände mit dem SoC verbunden. Die Pin-Bank 1 befindet sich unten, viele Leiterbahnen verlaufen von dort zu den Analog-Digital-Konvertern (ADC). Die Pin-Reihe 0 liegt rechts und deren Verwendung ist nicht direkt ersichtlich. Aber das herauszufinden, ist schließlich die Idee hinter dem Reverse Engineering.
Wie die FPGA-Programmierung funktioniert
Ein FPGA ist ein programmierbarer Logikbaustein. Abhängig vom Blickwinkel hat ein FPGA entweder überhaupt nichts mit der Programmierung zu tun und ist einfach ein Stück rekonfigurierbare Hardware oder es ist ein programmierbares Ding, das mit einer sehr ungewöhnlichen Programmiersprache funktioniert.
Die Logikgatter des FPGA können direkt konfiguriert und Logikschaltungen mit AND-, OR- XOR-Gatter, Invertern, Latches und Flip-Flops aufgebaut werden. Üblicherweise wird aber eine Hardware-Beschreibungssprache (HDL) verwendet, wie zum Beispiel Verilog(öffnet im neuen Fenster) oder VHDL(öffnet im neuen Fenster). Diese Sprachen werden auch als Registertransferebene(öffnet im neuen Fenster) (RTL) bezeichnet, sie abstrahieren die Abläufe in Form von Signalübertragungen zwischen Registern und logischen Operationen mit den Signalen dazwischen.
Ich habe schon früher mit Verilog herumgespielt, allerdings bin ich damit nie richtig warm geworden. Für einen Softie wie mich, also jemanden, der aus der Software-Ecke kommt, hat sich Verilog immer sehr primitiv angefühlt. Die Syntax ist ein wenig von C inspiriert, aber es ist noch systemnäher als C. Zum Beispiel hat Verilog kein Sprachenkonstrukt wie Struct. Schon einmal probiert, in C zu programmieren, ohne zusammengehörige Variablen in einem Struct zusammenfassen zu können und stattdessen jede Variable einzeln einer Funktion als Argument zu übergeben? Es gibt Verilog-Module, die Hunderte von Signalen als Argumente erwarten!
Es ist allerdings möglich, mehrere Signale in einen einzelnen großen Bitvektor zu packen. Aber das entspräche dem Verpacken eines Struct in einem Integer-Array von Hand, um es nach der Übergabe an die C-Funktion wieder von Hand zu entpacken. Das ist eine eher schmerzhafte Angelegenheit.
Systemverilog, eine Erweiterung von Verilog, soll solche Probleme lösen. Aber viele Herstellerwerkzeuge wie das kostenlose Xilinx SE von Spartan unterstützen es nicht. VHDL ist umfangreicher, es hat mit dem Entity-Konstrukt eine Entsprechung zum Struct-Konzept von C. Allerdings bin ich mit VHDL nie zurechtgekommen. Die Syntax erinnert mich an ADA und ich mag es einfach nicht.
Mit einer Hochsprache Hardware beschreiben
Da Verilog eher primitiv aufgebaut ist, haben viele bereits Werkzeuge entwickelt, um Verilog aus anderen Sprachen zu erzeugen. Vor einigen Jahren bin ich über MyHDL(öffnet im neuen Fenster) gestolpert, das Verilog oder VHDL aus Python-Code erzeugt. Es sah sehr interessant aus und ich wollte es schon immer einmal ausprobieren.
Ich stieß dabei auch auf Rhea(öffnet im neuen Fenster). Dabei handelt es sich um eine Handvoll nützlicher Bibliotheken für MyHDL, es enthält auch eine Reihe von Build-Skripten für viele verschiedene FPGAs.
Jede Menge neue Spielzeuge, um herumzuspielen und zu lernen.
Hello World
Der FPGA wird vom SoC mit fünf GPIO-Pins gesteuert. Drei der Pins dienen als allgemeine I/O-Leitungen nach der Konfiguration des FPGA. Als Erstes schreibe ich eine Hello-World-Variante für einen FPGA. Es ist ein kleines Programm in MyHDL, das einen der I/O-Pins regelmäßig an- und ausschaltet. Ich benutze dafür die FPGA-Primitive STARTUP_SPARTAN6, die einen 50-Mhz-Taktgenerator aktiviert. Mit diesem Takt wird ein Zähler hochgezählt, bei einem Zähler-Überlauf wird das Signal am I/O-Pin geändert. Ich erzeuge das FPGA-Image mit dem Rhea-Build-System und benutze dann meinen FPGA-Linux-Treiber, um das Image auf dem FPGA einzuspielen.
Nachdem das Image geladen ist, verfolge ich mit meinem Linux-Pin-Treiber die Signaländerungen am I/O-Pin. Und der Pin ändert regelmäßig seinen Zustand, der FPGA funktioniert also.
FPGA-I/O-Pins einlesen
Der nächste Schritt besteht darin, alle I/O-Pins des FPGA als Eingänge mit Pull-up-Widerständen zu definieren und dann etwas MyHDL-Code zu schreiben, um die drei freien I/O-Pins der Konfigurationsschnittstelle für aussagekräftigere Meldungen zu verwenden.
Ein I/O-Pin dient jetzt als LOAD-Signal: Ändert der Pin am FPGA seinen Zustand von Low auf High, wird der Zustand der zirka 180 FPGA-Eingangspins in ein Register geladen. Der zweite I/O-Pin wird als Taktsignal (CLOCK) benutzt und der dritte als Datenüberträger vom FPGA (DATA OUTPUT). So kann ich die Daten aus dem FPGA-Register auf den SoC schieben, ein Bit pro Takt. Ich simuliere den Ablauf mit MyHDL und verfolge die Signalausgabe mit GtkWave(öffnet im neuen Fenster) und der Code macht, was er sollte.
In der Theorie sollte ich damit die gleichen Tests wie beim SoC durchführen und den Zustand eines jeden Pins erkennen können, der als Eingabe-Pin auf dem FPGA konfiguriert wurde. Leider funktioniert es nicht so gut wie beim SoC. Viele Pins ändern ständig ihren Zustand, ohne dass ich eine Ursache erkennen kann oder weiß, wo ich mit der Suche anfangen soll. Außerdem führt die Aktivierung der Pull-up-Widerstände an den FPGA-Pins zu einem instabilen System. Linux stürzt häufig ab, nachdem ich den FPGA mit meinem Image bespielt habe. Lasse ich die Pull-up-Widerstände weg, läuft das System stabil. Sehr merkwürdig, darüber werde ich noch nachdenken müssen.
Aufsteigende Flanken zählen
Mein nächster Versuch ist es, einen Zähler für jeden Pin hinzuzufügen, der die Anzahl der Zustandsänderungen des Pins von Low auf High mitzählt. Mit einem 32-Bit-Register, das einmal pro Sekunde ausgelesen wird, sollte ich bis zu vier Milliarden Änderungen pro Sekunde erfassen können. Mehr als genug, da der Spartan FPGA kaum schneller als 400 MHz takten kann. Da mein Zähler zur Signalprüfung mit einem 50 Mhz-Takt betrieben wird, könnte ich sowieso nicht schneller zählen. Ich kann nicht genug Zählregister für alle 180 Pins im FPGA anlegen, so begrenze ich die Erfassung jeweils auf eine Pin-Bank pro Testdurchlauf. Das funktioniert zum Teil. Einige Pins ändern sich überhaupt nicht, ein Pin ändert seinen Zustand zirka 50 Mal pro Sekunde, aber viele Pins ändern ihre Werte scheinbar willkürlich mit jedem Zähler-Durchlauf. Ich weiß nicht, ob das an meinem HDL-Code liegt oder die Ergebnisse einfach eine Vielzahl von Glitches sind. Die Signale sehen in einer Simulation korrekt aus, aber ich weiß nicht, ob die Testfälle tatsächlich alles abdecken, was in Wahrheit passiert.
Ich benutze einen Trick, um herauszufinden, ob die Zähler zumindest bei geringen Geschwindigkeiten funktionieren. Ich zähle die Anzahl der Impulse auf der Taktleitung, mit der ich die Registerdaten zum SoC übertrage. Gibt es 55 Zählerwerte zu übertragen und ist jeder Wert 32 Bit groß, so sollte ich, jedesmal wenn Daten übertragen werden, 55 * 32 = 1760 Flanken auf der Taktleitung sehen können. Doch ich zähle immer einige Flanken mehr, zum Beispiel 1765. Irgendetwas stimmt nicht.
Digitale Signale sind nicht binär
Programmierer betrachten digitale Signale meist als binär, ein Signal ist entweder High oder Low. Allerdings stimmt das nicht. In Wahrheit sind alle Signale analoge Signale. Ändert sich der Signalwert von 0 Volt auf 3,3 Volt, dauert das eine gewisse Zeitspanne. Wird das Signal während einer Änderung geprüft, kann es eine beliebige Zwischenspannung aufweisen. Dieses bekannte Problem wird als Metastabilität(öffnet im neuen Fenster) bezeichnet.
Die Korrektur ist recht einfach. Ich füge einen weiteren Flip-Flop hinzu, der die Ausgabe des ersten Flip-Flops prüft, dadurch wird das Risiko reduziert. Es eliminiert das Problem nicht komplett, aber in der Praxis sollte es nicht mehr auftreten. Jetzt zähle ich genau 1760 Flanken auf der Taktleitung, aber die anderen Zähler laufen weiterhin chaotische Resultate.
Asynchroner Zähler-Durchlauf
Was wäre, wenn die Eingaben an den Pins sich schneller änderten als mit der 50 MHz Frequenz des Taktgebers? Das könnte die merkwürdigen Resultate erklären. Ich überarbeite die Zähler erneut. Statt die Signalpegel an den Pins regelmäßig auszulesen, sollen die Signale selbst die Zählung takten. Die Xilinx-Werkzeuge kreiden mir das als Fehler an. Es ist eine schlechte Angewohnheit, Signale an Pins als Taktgeber zu nutzen, die nicht explizit als Taktpins definiert wurden. Ich muss erst einmal nachlesen, wie ich den Werkzeugen sagen kann: "Schnauze, ich weiß, was ich tue!" Ich weiß es zwar nicht wirklich, aber wenigstens funktioniert jetzt die Synthese, die Umwandlung des Codes in ein Image für den FPGA.
Dass die Zähler nun asynchron zum internen Takt im FPGA laufen, bringt aber auch neue Probleme mit sich. Lese ich den binären Zähler mit einem anderen Takt als dem Zählertakt aus, habe ich wieder das Problem der Metastabilität, oder womöglich sind noch nicht alle Bits im Zähler bereits auf dem neuen Stand. Ein binärer Zähler, der von 01 (Dezimal: 1) auf 10 (Dezimal: 2) inkrementiert wird, hat vielleicht gerade das niedrigere Bit von 1 auf 0 geändert, während das höherwertige Bit gerade noch von 1 auf 0 wechselt. Beim Lesen würde der Zähler dann einen Wert von 00 (Dezimal: 0) aufweisen, hätte also rückwärts gezählt. Das passiert mir natürlich. Auch das ist ein bekanntes Thema, der allgemeine Begriff dafür lautet Clock Domain Crossing(öffnet im neuen Fenster). Er beschreibt allgemein die Probleme, die auftreten, wenn mit Signalen von zwei verschiedenen Taktquellen (Domains) gearbeitet wird.
Es gibt eine Reihe von bekannten Techniken, um Signale zwischen verschiedenen Domains auszutauschen. Eine davon wird als Gray-Code bezeichnet. Eine Änderung um +1 oder -1 führt nur zu einer einzelnen Bitänderung und vermeidet das Problem mit binären Zählern, bei dem sich mehrere Bits auf einmal ändern können. Ich füge einen Gray-Encoder zu meinen Zählern hinzu, der den Wert in Gray-Code umwandelt. Dieser Code wird dann in das Register zum Senden des Wertes eingefügt und zum SoC gesendet, wo der Gray-Code wieder dekodiert wird. Jetzt läuft kein Zähler mehr rückwärts.
Die Auswertung wird beschleunigt
Ich bringe die Abende einer Woche damit zu, mehr über MyHDL zu lernen und meine Zähler zu verbessern. Ich will die Flanken an allen Pins gleichzeitig zählen können, aber dafür muss ich die Menge an verwendeten Logikbausteinen zum Zählen reduzieren. Ich finde heraus, dass 180 Counter in den FPGA passen würden, wenn ich die Extraregister für die Gray-Encodierung weglassen und die Zählergröße auf 10 Bits reduzieren würde. Aber mit 10 Bits kann ich nur bis 1023 zählen, betrug die Frequenz an einem Pin 1 MHz, würde der Zähler innerhalb einer Millisekunde überlaufen. Das ist zu schnell, um sie über den SoC auszulesen.
So entscheide ich mich letztlich für ein hybrides Zählerdesign, bei dem 180 Zähler mit je 10 Bit Kapazität zur Anwendung kommen. Jeder Zähler wird vom Signal seines Pins getaktet und zählt direkt in Gray-Code. Ein Zähler, der direkt in Gray-Code zählt, ist nicht so schnell zu takten wie ein binärer Zähler, aber er erfordert weniger FPGA-Resourcen – wobei ich mir dabei nicht ganz sicher bin.
Da die 10-Bit-Zähler schnell überlaufen, füge ich eine periodisch aufgerufene Schaltung im FPGA ein, der jeden Zähler regelmäßig ausliest und bei einem Überlauf einen zugehörigen 32-Bit-Zähler im RAM entsprechend erhöht. Wenn die 180 Zähler jeweils bis 1023 zählen und dabei regelmäßig ausgelesen werden, wobei das Auslesen vom 50-Mhz-Takt vorgegeben wird, sollte ich jedes Signal mit einer maximalen Frequenz von 1024 * 50 /180 = 284 MHz pro Pin erfassen können. Vorausgesetzt, die Gray-Zähler packen das.
Tausende von Flanken werden gezählt
Das Ergebnis dieses Aufwandes ist in dieser Übersicht zu sehen, dabei wird die Anzahl der aufsteigenden Flanken an jedem Pin und dessen aktueller Zustand angezeigt.
pre> 0 C4 0 ^ A4 0 ^ B5 0 ^ A5 0 ^ D5 0 ^ 5 C5 0 ^ B6 0 ^ A6 0 ^ F7 0 ^ E6 17.3k ^ 10 C7 0 ^ A7 0 ^ D6 0 ^ C6 0 ^ B8 0 ^ 15 A8 0 ^ C9 0 ^ A9 0 ^ B10 0 v A10 0 ^ 20 E7 126.7M v E8 126.7M v E10 0 ^ C10 10.0M v D8 0 v 25 C8 0 ^ C11 0 ^ A11 0 v F9 0 v D9 0 v 30 B12 0 v A12 0 ^ C13 0 v A13 0 ^ F10 0 v 35 E11 0 ^ B14 0 ^ A14 0 v D11 0 v D12 0 ^ 40 E13 0 v E12 0 ^ B15 0 v B16 0 ^ F12 0 ^ 45 G11 0 v D14 0 ^ D16 0 v F13 0 ^ F14 0 v 50 C15 0 v C16 0 ^ E15 0 v E16 0 ^ F15 0 v 55 F16 0 ^ G14 0 v G16 0 ^ H15 0 ^ H16 0 v 60 G12 0 ^ H11 0 v H13 0 v H14 0 ^ J11 0 v 65 J12 0 ^ J13 0 v K14 0 ^ K12 0 v K11 0 ^ 70 J14 0 v J16 0 ^ K15 0 v K16 0 ^ N14 0 ^ 75 N16 0 v M15 0 v M16 0 ^ L14 0 ^ L16 0 v 80 P15 0 v P16 0 ^ R15 0 v R16 0 ^ R14 0 v 85 T15 0 ^ T14 0 ^ T13 0 v R12 17.3k ^ T12 0 ^ 90 L12 0 v L13 0 ^ M13 0 v M14 0 ^ T11 0 ^ 95 M12 1.8M v M11 722.9k v T10 210.2k ^ N12 104.0k v P12 977.8k ^ 100 N11 0 ^ P11 210.2k ^ N9 3.4M ^ P9 719.6k v L10 976.7k ^ 105 M10 0 ^ R9 13.8M ^ T9 27.3M v M9 545.1k v N8 694.1k v 110 P8 807.9k ^ T8 494.4k ^ P7 133.3M ^ M7 133.3M ^ R7 180.8k ^ 115 T7 713.9k v P6 458.5k ^ T6 828.5k v R5 2.0M v T5 0 ^ 120 N5 1.1M ^ P5 1.9M v L8 959.5k ^ L7 1.1M v P4 647.2k v 125 T4 1.2M v M6 27.3M v N6 1.6M v T3 1.1M v M4 996.0k v 130 M3 0 ^ M5 2.0M v N4 1.8M v R2 0 v R1 0 v 135 P2 0 v P1 0 v N3 0 v N1 0 v M2 0 v 140 M1 0 v L3 0 v L1 0 v K2 0 v K1 0 v 145 J3 0 v J1 0 v H2 0 v H1 0 v G3 0 v 150 G1 0 v F2 0 v F1 0 v K3 0 v J4 0 v 155 J6 0 v H5 0 v H4 0 v H3 0 v L4 0 v 160 L5 0 v E2 0 v E1 0 v K5 0 v K6 0 v 165 C3 0 v C2 0 v D3 0 v D1 0 v C1 0 v 170 B1 128.4k ^ G6 0 v G5 0 v B2 0 v A2 0 v 175 F4 6.8M ^ F3 0 v E4 971.0k ^ E3 0 v F6 115.4k v 180 F5 1.0M v B3 972.3k ^ A3 0 ^Wow! Da passiert jede Menge, obwohl der FPGA eigentlich nichts tut. Es gibt einiges Interessante zu entdecken.
Pin C10m in der Zeile mit der 20 am Anfang wird 20 Millionen Mal pro Sekunde geschaltet. Mit ein wenig Glück ist das der 10-MHz-Referenztakt des Kristallquarzes (U98). Pins E7 und E8 schalten mit einem Takt von 126 MHz. Laut dem FPGA-Datenblatt können diese Pins zur symmetrischen Digitalübertragung(öffnet im neuen Fenster) benutzt werden und dienen auch als globaler Taktgeber für den FPGA. Wahrscheinlich handelt es sich um ein weiteres Taktsignal von einem anderen Bauteil des Oszilloskops.
Die Pins M7 und R7 scheinen ebenfalls ein symmetrisches Taktsignal für den FPGA zu sein und mit 133 MHz zu takten. Diese Pins in Pin-Bank 2 scheinen auch häufig geschaltet zu werden.
Unangenehme Überraschung
Wo habe ich die 133 MHz schonmal gesehen? In der Ausgabe des Linux-Kernels, wenn er die Taktgeschwindigkeiten ausgibt:
Was? Wollt ihr mich verar***en? Was haben die Ingenieure geraucht, als sie sich das ausgedacht haben?
Sie benutzen das Bussystem für den DDR-Speicher als Highspeed-Verbindung zwischen den SoC und den FPGA. Anscheinend haben sie den FPGA so programmiert, dass er als DDR2-Speicher fungiert. Ernsthaft: Es gibt am SoC ein Bussystem, das mit dem Flash-Speicher verbunden ist und als Erweiterungsbus konzipiert wurde. Dieser Erweiterungsbus unterstützt sogar DMA (Direct Memory Access). Damit kann ein Gerät auf dem Bus Daten direkt in den Speicher des SoC kopieren, ohne die CPU zu nutzen. Warum sollte jemand so etwas Närrisches tun und den DDR-Speicherbus als Erweiterungsbus nutzen? Aber offensichtlich haben sie es getan, und die ganzen Signalwechsel in den Pin-Bänken 2 und 3 sind die Pins des Speicherbusses. Das erklärt auch, warum die Aktivierung der Pull-up-Widerstände an den FPGA-Pins des Linux zum Absturz brachte. Die Widerstände nahmen Einfluss auf den Speicherbus und verfälschten die Daten im Speicher.
Das würde alles noch einmal komplizierter machen.
Auch wenn das erst einmal ein Showstopper ist, setze ich meine Analysen fort. Es sollte trotzdem möglich sein, die anderen Komponenten zum Laufen zu bekommen und ein elektrisches Signal über das Analog-Frontend einzulesen und über die Konfigurationsleitung zum SoC zu übertragen. Ein ordentliches Oszilloskop unter Linux lässt sich so allerdings nicht ohne Weiteres realisieren.
Im letzten Teil der Artikelserie wird es Christer Weinigel schließlich gelingen, die vom Oszilloskop gemessenen Signalwerte auszuwerten und darzustellen.
Christer Weinigel(öffnet im neuen Fenster) ist freiberuflicher Ingenieur in seinem eigenen Unternehmen (Weinigel Ingenjörsbyrå AB). Er entwickelt hardwarenahe Software, arbeitet mit Embedded Betriebssystemen und spielt deshalb auch öfters direkt mit Hardware.
Diese Artikelserie erschien zuerst in seinem Blog. Mit seiner Erlaubnis hat Golem.de seine Artikel ins Deutsche übersetzt und dabei einige Kürzungen und Ergänzungen vorgenommen, damit der Inhalt auch Einsteigern verständlich ist.
- Anzeige Hier geht es zu Linux: Das umfassende Handbuch bei Amazon Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.