Diese Seite ist die dritte in einer Reihe von fünf Seiten darüber, wie du ein professioneller FPGA-Designer wirst. Auf dieser Seite versuche ich, die wichtigsten praktischen Fähigkeiten zu umreißen, die man als FPGA-Designer braucht. Ich versuche außerdem zu erklären, warum jede Fähigkeit wichtig ist. Überspringe nicht den Verilog-Teil, auch wenn er offensichtlich erscheint – ich habe dazu vielleicht ein paar nicht ganz so offensichtliche Dinge zu sagen.
Und bevor ich anfange, erkläre ich, warum ich nur Verilog erwähne und nicht VHDL: Einfach, weil Verilog empfohlen wird, da es so aussieht, als würde es die dominierende HDL-Sprache der Zukunft werden. Das liegt unter anderem daran, dass man von VHDL in China kaum hört. Aber alles, was unten gesagt wird, gilt genauso für VHDL.
Die zwei Seiten von Verilog
Warum man Verilog kennen muss, braucht kaum eine Erklärung. Tatsächlich denken viele da draußen, dass Verilog zu kennen gleichbedeutend damit ist, ein FPGA-Designer zu sein. Der eine oder andere Projektmanager hat seine Nachwuchsprogrammierer zu einem Verilog-Kurs geschickt und war enttäuscht, als sie nicht als FPGA-Champions zurückkamen.
Zunächst einmal ist es also ein weiter Weg von der Kenntnis der Verilog-Syntax bis zu dem Wissen, wie man diese Sprache einsetzt. Das gilt für jede Sprache, aber bei Verilog ist die Lücke deutlich größer. Zweitens ist es wichtig zu verstehen, dass Verilog-Code kein Computerprogramm ist. Er beschreibt Hardware. Deshalb geschieht im Prinzip alles parallel. Das ist der mentale Umschwung, mit dem viele Softwareentwickler Schwierigkeiten haben, insbesondere diejenigen, die nicht an Multithreading-Programmierung gewöhnt sind.
Die Ja-oder-Nein-Frage „Programmiersprache?“ ist tatsächlich etwas komplizierter, denn Verilog wird für zwei verschiedene Zwecke verwendet: die Simulation von Hardware und die Erzeugung von Logik für ein FPGA oder ein ASIC (Synthese).
Der Zweck einer Simulation ist oft, den Verilog-Code zu prüfen, bevor er in Logik im FPGA synthetisiert wird. Dazu schreiben wir eine Testbench, die im Wesentlichen ein Computerprogramm ist, das künstlich Stimulussignale erzeugt und mit den Ausgangssignalen des Logikteils, den wir testen, etwas anstellt: üblicherweise die Werte der Signale in eine Datei schreiben oder vielleicht prüfen, dass sie den Erwartungen entsprechen.
Wenn Verilog für die Simulation verwendet wird, ist es tatsächlich eine Programmiersprache, aber nichts wie C, Python oder JavaScript. Trotzdem macht der Computer genau das, was wir verlangt haben: Wenn die Simulation läuft, verhalten sich sowohl die Testbench als auch der Code, den wir synthetisieren wollen, genau so, wie es durch die Syntax definiert ist.
Und hier kommt der große Haken: Wenn derselbe Verilog-Code, den wir getestet haben, zur Synthese verwendet wird, ist Verilog keine Programmiersprache mehr. Er beschreibt Logik, genauso wie HTML beschreibt, was auf einer Webseite angezeigt werden soll. Schlimmer noch, und anders als bei HTML, kann die Interpretation des Verilog-Codes ziemlich schwammig sein.
Es ist wichtig zu verstehen, dass das Synthetisieren von Verilog-Code eine Art Magie ist. Wir Menschen beschreiben das gewünschte Verhalten, und der Synthesizer (synthesizer) setzt es irgendwie mit Hilfe von Logikressourcen um. Deshalb müssen wir dieses Verhalten mit bestimmten Codemustern (coding patterns) definieren, die der Synthesizer erkennt und für die er die von uns beabsichtigte Logik erzeugt. Anders als bei der normalen Programmierung können wir nicht einfach schreiben, was wir wollen. Wir müssen an die Logikelemente denken, die der Synthesizer als Ergebnis unserer Anweisungen erzeugen wird. Der Verilog-Code ist nur ein Hinweis an den Synthesizer, welche Logik er erzeugen soll.
Im KI-Zeitalter muss ich außerdem hinzufügen: Verilog ist kein Vibe Coding. Es ist eine formale Sprache, und der Synthesizer ist kein LLM, dem wir schmeicheln können, damit er tut, was wir wollen. Synthesizer gab es lange, bevor LLMs ein Thema waren, und sie interpretieren unseren Code nach strengen Regeln, und nicht mit einem trainierten neuronalen Netz.
Und am wichtigsten: Wenn wir schlechten Verilog-Code schreiben, verhält sich die Logik nicht wie die Simulation. Der Simulator macht genau das, was wir wollen, weil der Simulator das tut, was der Code sagt. Der Synthesizer hingegen kann still und leise Logik erzeugen, die etwas anderes tut, wenn wir nicht sorgfältig darauf achten, den Verilog-Code richtig zu schreiben.
Dieser Punkt ist entscheidend und es lohnt sich, ihn zu wiederholen: Wenn Verilog für die Simulation verwendet wird, ist es eine Programmiersprache, auch wenn eine ziemlich lahmende. Du musst nur die Syntax richtig hinbekommen, und der Simulator macht genau, was du willst. Der Synthesizer hingegen kann Logik erzeugen, die völlig anders ist als die Simulation, selbst wenn die Verilog-Syntax vollkommen korrekt ist. Wenn du Glück hast, gibt der Synthesizer eine Warnung aus, dass du möglicherweise nicht das bekommst, was du erwartet hast. Hoffentlich siehst du diese Warnung, obwohl sie unter Hunderten anderen begraben ist. Wenn du noch mehr Glück hast, bricht der Synthesizer mit einem Fehler ab. Aber allzu oft bekommst du einen stillen Bug. Um das zu vermeiden, musst du wissen, welche Codemuster für die Synthese in Ordnung sind. Und nichts anderes ausprobieren.
Und nun kommt die Frage: Wie lernt man also richtiges Verilog?
Grundlegende Verilog-Kenntnisse
Der erste Schritt ist natürlich das Erlernen der Syntax. Es gibt viele Verilog-Bücher und Tutorials da draußen. Leider gehen sehr viele davon durch alles und jedes, was nicht nur unnötig ist, sondern dich dazu verleiten kann, Codemuster zu verwenden, die der Synthesizer vermasselt.
Ich schlage das folgende minimale Themenset vor. Lerne sie aus irgendeiner Quelle, die du findest, und du bist, was die Syntax angeht, gut aufgestellt. Wenn du in Verilog-Code, auf den du stößt, auf etwas triffst, das du nicht kennst, frage deinen Lieblings-KI-Prompt, was es bedeutet. Schwerer ist es nicht. Hier ist also meine Einkaufsliste:
- Module und Instanzen, Ports (input, output, output reg, inout).
- Wires und Register. Register, die Flip-Flops erzeugen, und solche, die es nicht tun (mit always @(*) und Ähnlichem).
- Zuweisungen mit „assign“ gegenüber mit always @(posedge clk) gegenüber always @(*). „=“ gegenüber „<=“ innerhalb von always-Blöcken.
- if-Anweisungen
- case-Anweisungen und ihre Verwendung für Zustandsautomaten (state machines), Multiplexer und ROMs.
- Konstanten in Verilog, z. B. 16'h1234 und 4'b1001. Verstehe außerdem x und z als Werte.
- Arithmetische und logische Operationen in Verilog. Verstehe zum Beispiel den Unterschied zwischen & und &&. Unäre Reduktionsoperatoren, z. B. &myreg. Wahr/falsch als logischer Wert, z. B. myreg <= (this == 1).
- Bit-Manipulationen: Auswahl von Bits (z. B. myreg[3:2]) sowie Verkettung (z. B. { myreg1, myreg2 }) und Vervielfachung (z. B. { 8{myreg} }).
- Modulparameter (parameter und localparam).
- IP nutzen und sie in deinem Verilog-Modul instanziieren (instantiation). Das ist keine rein Verilog-bezogene Fähigkeit, da es die Nutzung des Entwicklungswerkzeugs zum Erstellen und Konfigurieren eines IP-Blocks (IP core) erfordert (eines FIFO oder einer PLL zum Beispiel). Und trotzdem besteht der größte Teil der Arbeit darin, die Ports in deinem Design zu instanziieren und zu verbinden, und das ist eine Verilog-Fähigkeit.
- Der „initial“-Block zum Initialisieren von Registern beim Einschalten sowie zum Setzen von Variablen in der Simulation.
- Befehle, die nur für die Simulation verwendet werden: $stop, $finish, $display, $readmemh und der ausgiebige Gebrauch von „initial“.
- Zeitverzögerungen mit „#“ und ihre Verwendung in Simulationen.
- Die `timescale-Direktive (und ihre oft fehlende Bedeutung).
- generate-Anweisungen – weniger wichtig für den Anfang, aber weit verbreitet.
- Synthese-Attribute. Das sind direkte Anweisungen an den Synthesizer, und ihre Syntax unterscheidet sich von einem Hersteller zum anderen. Zum Beispiel wird DONT_TOUCH = "TRUE" oft verwendet, um dem Synthesizer zu sagen, dass er ein Register nicht wegoptimieren soll, selbst wenn das Logik sparen würde. Lies dir also unbedingt die Liste solcher Attribute durch, die dein Synthesizer unterstützt, denn sie können sehr nützlich sein.
So weit der einfache Teil. Das eigentliche Problem ist, den Synthesizer dazu zu bringen, die Logik zu erzeugen, die wir tatsächlich wollen, indem wir Verilog-Code richtig schreiben. Genauer gesagt: jeden Synthesizer der Welt dazu zu bringen, genau dieselbe Logik zu erzeugen.
Ich halte mich an ein einfaches Prinzip, das Regel Nr. 4 in meiner eigenen Liste der Goldenen Regeln ist: Verwende unbedingt Codemuster, die sehr verbreitet sind. Die Idee dahinter ist, dass, wenn der Synthesizer meinen Code falsch interpretiert, er denselben Fehler auch mit dem Code vieler anderer Leute macht. Und das wird nicht passieren, weil all diese Leute bald einen anderen Synthesizer wählen würden. Also schaue ich mir den Code an, den ich geschrieben habe, und frage: „Wie viele andere Leute wären betroffen, wenn der Synthesizer das vermasselt?“ Wenn die Antwort „viele“ lautet, bin ich wahrscheinlich auf der sicheren Seite. Betonung auf „wahrscheinlich“.
Aber wie schreiben all diese anderen Leute dann Verilog? Das ist tatsächlich eine harte Nuss, denn die große Mehrheit des Verilog-Codes wird in Unternehmen geschrieben und nie veröffentlicht. Und verschiedene Leute schreiben in verschiedenen Codestilen. Um es noch schlimmer zu machen, werden Beispiele im Internet oft von unerfahrenen Leuten geschrieben. Selbst wenn ihr Code funktioniert, ist er nicht unbedingt etwas zum Nachahmen: Ein anderer Synthesizer könnte denselben Code vermasseln, möglicherweise weil Synthese-Tricks bis an ihre Grenzen getrieben werden. Verilog-Code von OpenCores schwankt in der Qualität von ausgezeichnet bis zu Code, der nur in der Simulation funktioniert.
Wie kann man es also wissen? Der beste Vorschlag, der mir einfällt, ist, den Verilog-Code zu lesen, den die IP-Werkzeuge von AMD erzeugen. Einige dieser IPs, zum Beispiel die DDR-Speichercontroller, erzeugen synthetisierbaren Verilog-Code, und er ist von Leuten geschrieben, die wissen, was sie tun. Beachte jedoch, dass automatisch generierter Verilog-Code oft viel sinnlosen und ungenutzten Code enthält. Das ist typisch für Code, der von Skripten (scripts) erzeugt wird. Es gibt oft Module, die nur andere Module instanziieren, die wiederum andere Module instanziieren, und so weiter. Module auf diese Weise endlos zu verschachteln, ist nichts zum Nachahmen; es ist nur eine Möglichkeit, die Hierarchie so strukturiert zu halten, wie es nötig ist, um verschiedene Konfigurationen desselben Codes zu ermöglichen. Automatisch generierter Verilog-Code neigt außerdem dazu, viele Parameter zu haben, was man ebenfalls nicht unbedingt nachahmen sollte. Denke daran, dass dein Ziel ist, die Art und Weise nachzuahmen, wie Funktionalität ausgedrückt wird, nicht die unordentliche Hierarchie.
Ein Wort zu SystemVerilog: Es ist ziemlich beliebt, aber ich habe persönlich noch nie Code in diesem Sprachdialekt geschrieben. Das liegt hauptsächlich daran, dass ich meinen Verilog so einfach wie möglich halten will. Je weniger ich dem Synthesizer vertrauen muss, desto besser. Wenn ich komplizierte Strukturen brauche, schreibe ich ein Skript in Perl, das einfachen Verilog erzeugt. Füttere den Synthesizer mit einem Teelöffel, sonst übergibt er sich über dir.
Werkzeuge für die Implementierung
Diese Fähigkeit zu beherrschen bedeutet, effizient mit den Werkzeugen zu arbeiten, sie gut unter Kontrolle zu haben, ihre Warnmeldungen zu verstehen, eine reproduzierbare Bitstrom-Erzeugung (bitstream) zu ermöglichen und das Projekt überschaubar zu halten, während es wächst. Kurz gesagt: deine Arbeit am Projekt im Griff zu haben.
Vivado ist das bevorzugte Werkzeug, mit dem man arbeitet. Nicht nur dominiert AMD den Markt, auch andere Hersteller, insbesondere die neuen chinesischen FPGA-Hersteller, neigen dazu, ihre Werkzeuge kompatibel zu gestalten. Abgesehen davon, dass man weiß, wie man diese Werkzeuge als IDE benutzt, sollte man auch den Prozess verstehen, in dem der Verilog-Code und die IPs in einen Bitstrom umgewandelt werden. Er beginnt mit der Synthese, gefolgt von mehreren anderen Schritten, die herstellerabhängig sind, aber im Prinzip alle dasselbe tun: die Ausgabe des Synthesizers schrittweise in einen Bitstrom umzuwandeln, den du in das FPGA laden kannst. Diese Abfolge von Ausführungsschritten ist keine Blackbox, und sie sollte auch nicht als eine behandelt werden.
Der Hauptgrund dafür, zu verstehen, wie die Werkzeuge funktionieren, ist, richtig auf Fehler und Warnungen zu reagieren. Während der Implementierung eines Projekts entstehen viele Warnungen, und es ist wichtig, zu unterscheiden, welche wichtig sind und welche ignoriert werden können. Und wenn ein Fehler auftritt, schlägt der Prozess fehl, und ein Problem muss gelöst werden. Die Fähigkeit, solche Probleme zu lösen, kommt aus dem Verständnis der Theorie hinter dem Handeln der Werkzeuge sowie aus gesammelter Erfahrung.
Zu versuchen, die KI zu fragen, wie man ein Problem löst, funktioniert manchmal, aber oft nimmt die KI dich mit auf eine lange Reise des vergeblichen Debuggens. Und wenn du auf die KI hörst, ohne ein eigenes Urteil zu haben, könnte es passieren, dass du etwas tust, das das Problem scheinbar löst, während du tatsächlich nur eine Fehlermeldung losgeworden bist und dafür ein echtes Problem geschaffen hast. Kurz gesagt: Es gibt keinen Ersatz für dein eigenes Gehirn, und es wird auch nie einen geben.
Ein weiterer wichtiger Punkt ist, dass jedes Werkzeug seine Eigenheiten hat, zum Beispiel irreführende Fehlermeldungen. Oder noch schlimmer: Die Werkzeuge ignorieren still und leise Code, Einstellungen oder Vorgaben (constraints). Diese Dinge zu lernen ist nur eine Frage der Erfahrung, und ein Teil dieser Erfahrung besteht darin, die Berichte tatsächlich zu lesen und herauszufinden, was diese Meldungen bedeuten, auch wenn sie keinen konkreten Bezug haben.
Hier ist eine Liste von Konzepten und Routinen, die ich als Checkliste vorschlage. Sie betreffen nur die Synthese und das Erhalten eines Bitstroms (bitstream). Simulation, Verifikation und Debugging hebe ich mir für später auf.
- Synthese und Erzeugung von Netzlisten (netlists), insbesondere edif
- Technologie-Mapping (technology mapping) (auch wenn das in Vivado der Synthesizer erledigt).
- Einbindung von IPs in das Design.
- Platzierung und Verdrahtung (place and route)
- Timing-Optimierungen nach der Platzierung und Verdrahtung (post-route timing optimizations)
- Bitstrom-Erzeugung (bitstream generation)
- JTAG verwenden, um den Bitstrom zu laden oder den Flash-Speicher zu programmieren
- Das synthetisierte und/oder platzierte und verdrahtete Design mit Vivados Werkzeugen ansehen und analysieren.
Wenn du ein Beispielprojekt ausprobierst, wirst du mit großer Wahrscheinlichkeit alles durchlaufen, was in dieser Liste genannt ist, mit Ausnahme des letzten Punktes. Die Werkzeuge zu benutzen, wenn jemand bereits ein Beispielprojekt für dich vorbereitet hat, ist einfach. Im echten Leben werden Projekte selten so ordentlich organisiert, und du wirst dafür verantwortlich sein, die Werkzeuge richtig zum Laufen zu bringen und die bestmöglichen Ergebnisse zu erzielen. Wenn du nicht verstehst, wie die Maschinerie funktioniert, wirst du es schwer haben, sie zu reparieren, wenn sie hängt oder nicht tut, was du willst. Denke daran: Seltsame Dinge passieren ständig, selbst wenn du alles richtig machst.
Ein Ozean von Dateien
Die Entwicklungswerkzeuge legen Dateien an, und davon jede Menge. Jeder Schritt, den die Werkzeuge ausführen, von der Synthese bis zum fertigen Bitstrom, ist wie ein Computerprogramm, das einige Dateien einliest und seine Ergebnisse als andere Dateien ausgibt.
Um die Sache noch schwieriger zu machen, legen die Werkzeuge oft Kopien von Quelldateien an und verlassen sich auf diese Kopien statt auf die Originale. Die Werkzeuge erzeugen außerdem Zwischendateien und verlassen sich auf diese statt auf irgendetwas, das wie eine Quelldatei aussieht. Behalte das im Hinterkopf, insbesondere wenn du Änderungen in den Quellen machst und sich an den Ergebnissen nichts ändert.
Ich würde nicht priorisieren, als ersten Lernschritt zu verstehen, was jede einzelne Datei tut und wofür sie gut ist. Es ist sinnlos, zu versuchen, sie alle zu beherrschen, aber es ist wichtig zu wissen, welche Dateien als „Quelle“ (source) gelten sollten und welche „generiert“ (generated) sind. Je besser du in diesem Ozean von Dateien schwimmst, desto besser sind deine Chancen, den Kopf über Wasser zu halten. Du wirst verstehen, was ich meine, wenn du das erste Mal versuchst, eine unabhängige Kopie eines ganzen Projekts zu erstellen, um es in eine separate Richtung weiterzuentwickeln. Oder es auf einen anderen Computer zu verschieben.
Die beste Methode ist, einen minimalen Satz von Dateien, die das FPGA-Projekt definieren, in einem Git-Repository zu pflegen. Lösche ab und zu alle anderen Dateien und baue das Projekt aus diesem minimalen Satz neu auf. Für ein Beispiel, wie ein Projekt aus einem minimalen Satz von Dateien aufgebaut werden kann, lade eines von Xillybus' Demo-Bundles (demo bundles) herunter (verfügbar für Vivado und Quartus) und versuche, es zu implementieren. Beachte, dass du die Quellen zum Starten nicht auf normalem Weg in Vivado importierst, sondern ein Tcl-Skript (script) ausführst. Das mag nach einer beängstigenden Lösung klingen, aber dieses Skript lässt sich leicht anpassen, auch wenn du kein Tcl kennst.
Eine wissenswerte Sache über Vivado ist, dass das DCP eine komprimierte Datei ist, die Netzlisten (netlists) in edif, Vorgaben (constraints) und andere Informationen enthält. Wenn das DCP zum Beispiel das Ergebnis von Platzierung und Verdrahtung oder späterer Phasen ist, enthält es die genauen Platzierungen der Logikelemente. Es ist ein Schnappschuss des Designs nach Abschluss einer bestimmten Phase im Prozess. Ich schlage vor, ein DCP zu entpacken und einfach aus Neugier hineinzuschauen.
Verifikation und Simulation
In einer perfekten Welt funktioniert das Logikdesign beim ersten Versuch, und es gibt keine Bugs zu beheben. Die Realität sieht natürlich fast immer anders aus.
In jedem Anfängerkurs über Verilog wird dir das gesagt: Schreibe zuerst den Verilog-Code, den du als Logik im FPGA haben willst. Das nennen wir „Code für die Synthese“ oder „synthetisierbaren Code“. Schreibe dann eine Testbench in Verilog und verwende sie in einer Simulation, damit du prüfen kannst, dass der Code für die Synthese korrekt funktioniert. Oder besser gesagt: sieh, wie er nicht korrekt funktioniert, und behebe die Bugs. Die Testbench ist Verilog-Code, der nur zum Zweck der Simulation geschrieben wird und nie in die Nähe eines Synthesizers kommt.
Tatsächlich ist die übliche Arbeitsweise, ein Verilog-Modul oder ein paar Verilog-Module zu schreiben und dann eine Testbench zu schreiben, um zu verifizieren, dass sie korrekt funktionieren. Das wiederholt sich, während das Projekt größer wird, und am Ende wird möglicherweise das ganze Projekt simuliert. Für eine so große Simulation gibt es oft mehrere verschiedene Testbenches, von denen jede dazu gedacht ist, andere Funktionalitäten zu prüfen.
Aber nicht alle Simulationen werden auf dieselbe Weise durchgeführt. Es gibt drei Hauptansätze für die Simulation.
Der erste Ansatz ist die Simulation um der Wellenformen willen. Bei diesem Ansatz erzeugt die Testbench nur Signale, die die Eingänge des Moduls speisen, das wir simulieren wollen. Dazu gehören die Takte und Resets sowie andere Signale, die das Verhalten echter physischer Signale oder von Signalen nachahmen, die von anderen Modulen erzeugt werden. Nach Abschluss der Simulation wird eine GUI-Oberfläche verwendet, um die Wellenformen anzusehen, die von der getesteten Logik erzeugt wurden, um zu sehen, ob sie korrekt funktioniert oder warum nicht. Diese Methode eignet sich für einfachere Logik. Zum Beispiel kann der Zustandsautomat (state machine), der ein Videoausgabemuster erzeugt, auf diese Weise simuliert werden, da das korrekte wiederholte Ausgabemuster leicht allein durch Betrachten der Wellenformen überprüft werden kann.
Der zweite Ansatz ist, Eingaben aus Dateien zu lesen und Ausgaben in Dateien zu schreiben. Hier erzeugt die Testbench ein paar einfache Signale, aber die wichtigen Signale werden stattdessen aus einer Datei gelesen. Die Werte an den Ausgängen oder an einigen ausgewählten Ausgängen des getesteten Moduls werden in eine andere Datei geschrieben. Es ist ziemlich üblich, ein Computerprogramm oder Skript (script) zu schreiben, das die Datei erzeugt, die die Testbench liest, sowie die Datei mit der erwarteten Ausgabe der Testbench. Nach dem Ausführen der Simulation kann ein einfacher Textvergleich zwischen der erwarteten Ausgabe und dem, was die Testbench tatsächlich geschrieben hat, durchgeführt werden. Natürlich gibt es dazu endlose Varianten: Die Testbench kann den Vergleich mit den erwarteten Ausgaben selbst durchführen, oder auch umgekehrt: Eine Computersoftware liest die Ausgabe der Testbench und analysiert sie.
Diese Simulationsmethode eignet sich besonders für das, was ich „Verarbeitungslogik“ genannt habe, also Logik, die irgendeine Art von Datenverarbeitung umsetzt. Sie ist auch für Regressionstests nützlich, also Simulationen, die prüfen, dass sich von einer Version des Codes zur nächsten funktional nichts geändert hat.
Der dritte Ansatz ist, die Testbench die Korrektheit selbst prüfen und bei einem Problem mit einem Fehler abbrechen zu lassen. Diese Methode erfordert das Schreiben aussagekräftiger Testmuster in Verilog, was oft schwieriger ist, als es in einer Skriptsprache (scripting language) zu erledigen. Andererseits ist das FPGA-Projekt leichter zu pflegen, wenn es eine in sich geschlossene Testbench gibt, die eine Bestanden- oder Fehlgeschlagen-Anzeige liefert. Wer die Regressionssuite laufen lässt, wird mit dieser Arbeitsweise glücklich sein.
Das sind die drei Hauptansätze. Ich habe es so klingen lassen, als würde nur das Verilog simuliert, das wir Menschen schreiben, aber das ist nicht wahr:
Post-Synthese-Simulationen
Wie ich bereits erwähnt habe, kann der Synthesizer Logik erzeugen, die sich anders verhält als das durch den Verilog-Code beschriebene Verhalten. Eine Möglichkeit, dieses Problem anzugehen, ist, die Ausgabe (also die Netzliste, netlist) zu simulieren, die der Synthesizer erzeugt. Das nennt man Post-Synthese-Simulation.
Um eine Post-Synthese-Simulation auszuführen, bitten wir die Entwicklungswerkzeuge, ein Verilog-Modell des synthetisierten Codes zu erstellen. Das ist ein riesiges Verilog-Modul, das dieselben Ports hat wie das, das wir für die Synthese geschrieben haben. Im Inneren besteht es jedoch aus kleinen Modulen (genannt Simulations-Primitive, simulation primitives), die die tatsächlichen Logikelemente des anvisierten FPGA darstellen. Es ist also eine wirklich unordentliche Verilog-Datei, aber wir können in der Testbench darauf verweisen statt auf das ursprüngliche Verilog-Modul, das wir geschrieben haben.
Die klassische Methode ist, eine Simulation auf dem von Menschen geschriebenen Verilog-Code und dann auf dem Post-Synthese-Modell laufen zu lassen und die Ausgaben zu vergleichen. Die zweite oben genannte Methode (mit Dateien als Ein- und Ausgabe) ist am besten, weil wir erwarten, dass sich das Logikdesign nach der Synthese genau gleich verhält. Wenn nicht, betrachte das als einen schweren Schlag gegen deinen Verilog-Codestil. Wenn du Verilog richtig schreibst, solltest du eigentlich keine Post-Synthese-Simulation brauchen. Diese Art von Simulation ist in der ASIC-Branche verbreiteter, wo man paranoid davor ist, am Ende einen Bug im endgültigen Chip zu haben. Ebenfalls erwähnenswert: Selbst wenn deine Post-Synthese-Simulation genau dieselbe Ausgabe liefert wie das Original, garantiert das immer noch nicht, dass der Synthesizer das getan hat, was du erwartet hast.
Es gibt auch die Simulation nach der Platzierung und Verdrahtung (post-place-and-route simulation). Wie der Name schon sagt, simuliert diese die Logikelemente so, wie sie im FPGA platziert und verbunden sind. Sie berücksichtigt die Ausbreitungsverzögerungen (propagation delays) innerhalb des FPGA, aber auf sehr eingeschränkte Weise. Es gibt immer noch einen riesigen Unterschied zwischen der Simulation und dem, was im echten Leben passiert. Das liegt daran, dass die tatsächlichen Verzögerungen im FPGA von vielen physischen Faktoren abhängen, zum Beispiel Temperatur und Versorgungsspannungen. Diese Verzögerungen sind außerdem etwas zufällig, wegen Verunreinigungen im Silizium des Chips, die überall verstreut sind. Diese Verunreinigungen verändern die physikalischen Eigenschaften, sodass die elektrischen Verzögerungen leicht danebenliegen. Jedes physische FPGA hat daher andere Verzögerungen, aber innerhalb der Spezifikation. Wenn die Simulation läuft, wird auf jeden Pfad nur eine bestimmte Verzögerung angewendet, üblicherweise die größte erlaubte Verzögerung. Wenn dein Design in einer Post-Place-and-Route-Simulation perfekt funktioniert, heißt das trotzdem nicht, dass es auf einem echten physischen FPGA funktionieren wird.
Alles in allem haben Simulationen also ziemlich ernste Grenzen. Zum einen gehorcht die Simulation deinen Wünschen, wo der Synthesizer es nicht tut. Und selbst Post-Synthese-Simulationen können viele Effekte des echten Lebens im FPGA nicht abdecken: Glitches, Toleranzen der physischen Logikelemente aufgrund von Temperatur, Versorgungsspannung oder Fertigungstoleranzen. Die Simulation kann auch das Verhalten der Logik infolge von Timing-Verletzungen nicht reproduzieren, zum Beispiel beim unsicheren Überqueren von Taktbereichen (clock domain crossings).
Wenn das Design korrekt geschrieben und mit Vorgaben versehen (constrained) ist, verhalten sich Simulation und Realität gleich. Andernfalls ist die Simulation wertlos. Das ist wichtig im Hinterkopf zu behalten.
Eine weitere Einschränkung ist, dass die Simulation nur einen sehr kurzen Zeitabschnitt abdecken kann. Wenn die Simulation eine Million Taktzyklen läuft, was lange dauern kann, deckt das bei einem Takt von 100 MHz nur 10 ms Echtzeit ab. Seltene Probleme und Randfälle werden leicht übersehen, einfach weil sie zufällig nicht in das simulierte Fenster gefallen sind.
Meine Empfehlung zu Simulationen
Was empfehle ich einem Neuling? Zu wissen, wie man simuliert, ist ein Muss, und es ist etwas, das man zusammen mit den anderen Verilog-bezogenen Fähigkeiten lernt. Insbesondere solltest du dich mit der zweiten Simulationsmethode, die Dateien für Ein- und Ausgabe verwendet, wohlfühlen. Das ist die Methode, die als seriös gilt, unter anderem weil sie in Regressionstests üblich ist. Probiere sie auch mit Post-Synthese- und Post-Place-and-Route-Simulationen aus, zumindest um eines Bewerbungsgesprächs willen.
Aber: Werde nicht simulationssüchtig. Schlag dir selbst auf die Finger, jedes Mal, wenn du mit einer Simulation einen Bug findest. Wenn du einen Bug mit der ersten Methode findest (Wellenformen aus einer Simulation betrachten), schlag dir noch härter auf die Finger. Das oberste Ziel ist, Code zu schreiben, der beim ersten Versuch funktioniert. Simulationen fangen die einfachen Bugs, nicht die, die bei jeder Milliarde Taktzyklen etwas Seltsames verursachen. Das sind die, denen man nicht ewig hinterherjagen will.
Zum Schluss noch ein kleines Geheimnis über mich: Ich führe selten eine Simulation aus. Ich achte darauf, Verilog-Code sorgfältig und durchdacht zu schreiben, damit er sofort korrekt funktioniert oder zumindest mit wenigen genug Bugs, um sie direkt auf dem FPGA zu beheben. Aber ich kenne niemanden sonst, der so arbeitet, und ich bin nicht sicher, ob ich das als allgemeinen Ansatz empfehlen würde.
Eigentlich möchte ich noch einen fortgeschrittenen Bonus-Tipp hinzufügen, den man sich später merken sollte: Wenn Leute Simulationen laufen lassen, fügen sie ihrer Logik oft Resets hinzu, weil in einer Simulation alle Register mit dem unbekannten Zustand beginnen, der mit „X“ gekennzeichnet ist. Dadurch kommt nichts Brauchbares heraus, denn das gesamte System verharrt im „X“-Zustand. Der übliche Fehler ist, schnell überall asynchrone Resets (asynchronous resets) einzubauen, um diese X's loszuwerden. Und dann zu vergessen, dass das eine sehr schlechte Idee sein kann, wie auf einer separaten Seite erklärt wird. Also ja, löse das Problem mit den X's auf jeden Fall, aber mach es mit Bedacht: Vielleicht mit einem synchronen Reset (synchronous reset) oder vielleicht mit einem „initial“-Block im synthetisierbaren Code? Die meisten Synthesizer implementieren diesen Block, obwohl er ursprünglich nur für die Simulation gedacht ist. Lass nicht zu, dass die Simulation diktiert, wie du Code schreibst.
Testen und Debuggen
Nach allem, was gesagt und getan ist, wirst du dein Design testen und die Gründe finden, warum es nicht wie erwartet funktioniert. Es gibt ein Sprichwort, dass Debuggen wie ein Krimi ist, in dem dieselbe Person das Opfer, der Detektiv und der Täter ist.
Und die Wahrheit ist, dass es in diesem Krimi keine einzige beste Methode gibt, das Rätsel zu lösen. Es geht darum, jedes Mal kreativ den richtigen Ansatz, die richtigen Werkzeuge und Methoden zu finden, um sich der Quelle des Bugs zu nähern. Manche Leute halten sich jedes Mal an dieselben Debugging-Methoden. Manchmal finden sie das Problem ziemlich schnell, und manchmal dauert es bei ihnen ewig.
Es ist ziemlich üblich, für ein bestimmtes Projekt eine Reihe spezialisierter Debugging-Werkzeuge und -Methoden zu entwickeln. Das passiert auch bei großen Softwareprojekten, aber beim FPGA-Design ist es oft unvermeidlich. Es ist wie das Entwickeln einer Testbench zum Testen, nur in Hardware.
Aber obwohl es keine einzige optimale Methode zum Finden eines Bugs gibt, gibt es trotzdem bestimmte Werkzeuge, die häufig verwendet werden. Ich erwähne ein paar davon.
Das Werkzeug, mit dem die meisten am liebsten anfangen, ist ein On-Chip-Logikanalysator (on-chip logic analyzer). Je nachdem, welche Entwicklungssuite du hast, heißt dieses Werkzeug ILA, ChipScope, SignalTap oder ähnlich, und sie alle tun dasselbe: Sie verwandeln deinen Computer in einen Logikanalysator. Mit diesem Werkzeug kannst du Wellenformen betrachten, die im FPGA aufgezeichnet wurden, und zwar genauso, wie du Wellenformen aus Simulationen betrachtest. Du wählst aus, welche Signale du beobachten willst und unter welcher Bedingung eine Aufzeichnung dieser Signale ausgelöst wird.
Die Kommunikation zwischen Computer und FPGA läuft über die JTAG-Schnittstelle, dieselbe, die auch verwendet wird, um die Bitstromdatei zum FPGA zu senden. Es ist also keine zusätzliche Hardware erforderlich, und viele FPGA-Designer, die Elektronik nicht mögen, schätzen diese Tatsache. Das FPGA und seine ohnehin vorhandene JTAG-Verbindung sind ebenfalls das Debugging-Werkzeug.
Diese Methode ist so bequem, dass viele FPGA-Designer süchtig danach werden und andere Methoden vergessen, die in manchen Situationen besser geeignet sein mögen.
Der Hauptnachteil dieser Methode ist, dass JTAG eine relativ geringe Datenbandbreite hat, weshalb ILA und ähnliche Werkzeuge nicht geeignet sind, wenn große Datenmengen für die Untersuchung benötigt werden. Es dauert außerdem ein wenig, bis die Daten über die JTAG-Verbindung ankommen. Das kann problematisch sein, wenn wir eine sofortige Reaktion auf Ereignisse wollen, um sie mit anderen gleichzeitig stattfindenden Dingen in Zusammenhang bringen zu können.
Wenn die Grenzen der JTAG-Verbindung zum Problem werden, sind schnellere Schnittstellen erforderlich. Wenn die FPGA-Platine eine PCIe-Schnittstelle hat, kann diese verwendet werden, um Daten mit hoher Datenrate zu und von einem Computer zu transportieren. Eine PCIe-Schnittstelle und den Treiber für den Computer zu implementieren, kann für sich genommen ein eigenes Projekt sein, aber mit Xillybus ist es schnell und einfach, eine solche Verbindung zum Laufen zu bringen. Eine ähnliche Lösung mit Xillybus ist auch mit Zynq-7000-Platinen möglich, die mit Xillinux arbeiten.
Eine Verbindung mit hoher Bandbreite ist oft erforderlich, wenn ein punktuelles Problem in einer großen Datenmenge versteckt ist. Zum Beispiel in der Bildverarbeitung, in diesem Fall wird das Bild über Xillybus zum Computer übertragen und mit Hilfe eines speziellen Computerprogramms auf dem Monitor des Computers betrachtet.
Ob du dich für einen On-Chip-Logikanalysator oder für Xillybus entscheidest, das sind sterile Debugging-Werkzeuge. Es wird keine Hardware angefasst. Aber manchmal brauchen wir genau diese einfache und unmittelbare Reaktion der Hardware, um herauszufinden, was los ist.
Deshalb möchte ich zuerst das einfachste Debugging-Werkzeug von allen vorschlagen: die LED. Überraschend oft ist die schnellste Methode, einen Bug zu finden, verschiedene Logikbedingungen zu definieren und sicherzustellen, dass eine LED blinkt, wenn sie wahr sind. Insbesondere wenn etwas, das niemals passieren sollte, tatsächlich passiert, sollen LEDs blinken. Je mehr LEDs, desto besser. Das ist ein bisschen wie das Einfügen eines „printf“ in C-Code zum Debuggen. Ebenso kannst du ein wenig Verilog-Code zum Debuggen hinzufügen. Lade den Bitstrom in das FPGA, probiere ein paar zufällige, alberne Dinge aus, und vielleicht verraten dir diese LEDs, womit der Bug zusammenhängt.
Eine wichtige Sache ist bei dieser Methode zu beachten: Wenn die Bedingung erfüllt ist, stelle sicher, dass die LED als Reaktion darauf mindestens 20 ms leuchtet. Sonst sieht das menschliche Auge es nicht.
Eine ausgefeiltere Version der LED-Methode ist die Verwendung eines Oszilloskops. Deshalb habe ich auf der ersten Seite dieser Reihe vorgeschlagen, ein Oszilloskop zu kaufen und sich damit vertraut zu machen. Die Idee ist, mehrere Signale aus dem FPGA mit seinen Ausgangspins zu verbinden, die mit einer Oszilloskop-Tastspitze erreichbar sind. In echten Projekten ist es nicht ungewöhnlich, dass eine Platine einen eigenen Steckverbinder hat, bei dem der Platinenentwickler viele ungenutzte Pins des FPGA zusammengefasst hat, damit sie zum Debuggen verwendet werden können.
Im Vergleich zum On-Chip-Logikanalysator mag das Oszilloskop lahm erscheinen: Es kann zwei Signale gleichzeitig überwachen, bei teureren Oszilloskopen vielleicht ein paar mehr. Seine Bandbreite ist begrenzt, sodass wirklich schnelle Signale übersehen werden können. Und trotzdem ist es manchmal das stärkste Werkzeug, das man zur Hand hat. Insbesondere wenn es wiederkehrende Signalmuster gibt, kann es helfen, sie mit einem Oszilloskop zu beobachten, um zu erkennen, was schiefgeht.
Und manchmal ist das Oszilloskop erforderlich, um zu verstehen, was auf der reinen Hardwareebene passiert. Sind die Spannungen korrekt? Sehen die digitalen Signale so aus, wie sie sollten? Gibt es irgendein verrücktes Rauschen auf dem Spannungssignal?
Und damit komme ich zur bodenständigsten Art des Debuggens. Häufiger, als wir glauben wollen, ist das Problem ein wirklich albernes Hardwareproblem, insbesondere bei Platinen, die für ein bestimmtes Projekt entwickelt wurden. Eine der Versorgungsspannungen kann instabil sein, vielleicht mit gelegentlichen kurzen Spitzen oder Einbrüchen. Der Taktoszillator (clock oscillator) verhält sich möglicherweise nicht wie erwartet und erzeugt ein unzureichendes Taktsignal. Es ist immer eine gute Idee, sich diese Dinge anzusehen, bevor man ausgefeilte Theorien entwickelt.
Wenn du also gut im FPGA-Design werden willst, brauchst du ein bisschen von beidem: sterile Methoden, um Daten aus dem FPGA zu holen, sowie die Bereitschaft, sich mit der Hardware die Hände schmutzig zu machen.
Und vergiss nie: Es gibt nur ein einziges Debugging-Werkzeug, das wirklich effizient ist, wenn es richtig eingesetzt wird: dein eigenes Gehirn.
Programmiersprachen
Auch wenn Programmieren nicht direkt mit FPGA-Design zu tun hat, ist ein minimaler Satz an Programmierkenntnissen nötig, um die Arbeit zu erledigen. Zum Beispiel habe ich oben erwähnt, dass Testdaten für Simulationen oft mit einem extra geschriebenen Programm oder Skript (script) erzeugt werden.
Ich erwähne ein paar Programmiersprachen, deren Beherrschung sich lohnen könnte. Von allen Dingen, die ich zum Lernen vorschlage, ist das tatsächlich der leichtere Teil, und man muss definitiv nicht alles davon vom ersten Tag an wissen. Oder überhaupt jemals.
- Tcl: Diese Sprache wird üblicherweise verwendet, um Skripte (scripts) zu schreiben, die von Vivado und anderen FPGA-Entwicklungswerkzeugen ausgeführt werden. Einzeiler sind oft ebenfalls nützlich. Die vollständige Syntax dieser Sprache und alle ihre Features zu lernen, ist jedoch sinnlos. Du wirst in dieser Sprache wahrscheinlich nie richtig programmieren, aber die Grundlagen ihrer Syntax zu beherrschen, ist empfehlenswert. Lerne insbesondere die verschiedenen Arten von Anführungszeichen und Klammern und was sie bedeuten.
- C: Diese Sprache ist so weit verbreitet, dass ich es als ein wenig handicapiert betrachten würde, sie nicht zu kennen. Für die FPGA-Entwicklung ist sie oft ein guter Kandidat, um große Mengen von Testdaten zu erzeugen.
- Python: Das ist eine beliebte Skriptsprache (scripting language), die nützlich sein kann, um Testdaten zu erzeugen, aber noch wichtiger: um Verilog zu erzeugen, das repetitiven Code enthält, der sich mit Verilogs eigenen syntaktischen Möglichkeiten nicht sauber ausdrücken lässt. Es ist oft einfacher, ein Skript zu schreiben, das Verilog-Code auf Basis von Code-Vorlagen erzeugt, als es direkt mit Verilog zu machen.
- Perl: Diese Sprache kann für dieselben Zwecke verwendet werden, die ich oben für Python erwähnt habe. Meiner Meinung nach ist Perl definitiv besser darin, praktisch alles zu erledigen, aber Python ist beliebter, weil es von Universitäten übernommen wurde: Pythons Syntax ist das, was Akademiker der Programmiersprachen mögen, und Perl hat eine sehr saubere und pragmatische Syntax, aber sie bricht die akademischen Regeln darüber, wie eine Programmiersprache aussehen sollte. Perl ist also großartig, um die Arbeit zu erledigen, aber es gilt als veraltete Sprache, weil die meisten Leute Python als die primäre Skriptsprache betrachten. Wenn du nicht ohnehin schon in Python zu Hause bist, ziehe stattdessen Perl in Betracht. Du wirst es nicht bereuen.
- Makefile und Bash-Shell-Skripting: Wenn du damit schon vertraut bist, behalte sie im Hinterkopf, um manche Projekte zu bauen. Ansonsten würde ich sie nicht hoch priorisieren. Es ist sehr schwierig, ein richtiges Makefile für ein FPGA-Projekt zu schreiben, weil die Abhängigkeiten nicht immer so geradlinig sind wie bei einer reinen Softwarekompilierung. Und trotzdem verwende ich Makefiles, wenn der Build-Prozess komplex ist und ich nur die Teile bauen will, die neu kompiliert werden müssen.
Es gibt mehrere andere Sprachen, zum Beispiel MATLAB, die ebenfalls nützlich sein können, insbesondere zum Erzeugen von Testdaten für Simulationen.
Ein weiterer Aspekt des Kennens von Programmiersprachen ist, dass sie oft eine gemeinsame Sprache mit dem Softwareteam schaffen. Das ist relevant für FPGA-Projekte, die irgendeinen eingebetteten Prozessor oder sogar einen Computer umfassen. Wenn du selbst auf irgendeinem Niveau ein Programmierer bist, hilft dir das, mit den Software-Leuten zu kommunizieren. Außerdem ist es oft einfacher, das Low-Level-Programm, das mit deinem FPGA-Design kommuniziert, selbst zu schreiben, als jemand anderen dazu zu bringen, es anhand einer Spezifikation zu tun.
Und als allgemeine Empfehlung: Lerne, wie man Git benutzt, und benutze es, selbst wenn du allein an einem Projekt arbeitest. Versionskontrolle dient nicht nur dazu, Manager zufriedenzustellen, sondern ist ein wertvolles Werkzeug, mit dem du etwas ausprobieren und dann zurückgehen und dann bereuen kannst, dass du zurückgegangen bist. Und nachdem du mit dem Herumspielen fertig bist, ist von diesem ganzen Durcheinander nichts zu sehen.
Ich benutze gitk für eine grafische Darstellung des Versionsbaums.
Damit schließt die dritte Seite in dieser Reihe ab. Die nächste Seite behandelt ebenfalls praktische Fähigkeiten, aber solche, die für den Anfang weniger wichtig sind. Der Hauptpunkt ist zu erklären, wie und wann sie wichtig sein können.