Portfolio Architektur Eingebetteter Systeme

Jonas Otto

Sommersemester 2020

Die aktuellste Version dieses Dokuments ist immer als HTML-Version unter ottojo.github.io/ArchitekturEingebetteterSysteme verfügbar.

Dieses Dokument ist während der Vorlesung “Architektur Eingebetteter Systeme” entstanden, meine Vorlesungsnotizen sind im gleichen Repository und als HTML auf ottojo.github.io/ArchitekturEingebetteterSysteme/vl.html verfügbar.

Den Quellcode für beide Dokumente (Markdown für den Text, Übungen mir VHDL) findet man auf github.com/ottojo/ArchitekturEingebetteterSysteme.

Einleitung und Überblick

Eingebettete Systeme repräsentieren einen Großteil aller Computersysteme die uns täglich umgeben. Bei der Entwicklung dieser gibt es spezielle Herausforderungen, die in anderen Disziplinen der Soft- und Hardwareentwicklung so nicht gelten. In der Vorlesung, und damit auch in diesem Portfolio, wird ein Überblick über die Herausforderungen, Möglichkeiten und Eigenschaften von eingebetteten Systemen gegeben.

Im ersten Teil dieses Portfolios werden einige Teile der Vorlesung in Form von Kurseinlagen hervorgehoben und mit zusätzlichen Quellen ergänzt: Zuerst wird auf die verschiedenen Methoden der Realisierung eingebetteter Rechnersysteme eingegangen, und wichtige Unterschiede werden dargelegt. Die zweite Kurseinlage betrachtet die Anbindung von Sensoren und Aktoren am Beispiel eines Flugzeuges aus der Vorlesung, was zu einem kurzen Ausblick auf ein Netzwerkprotokoll für Avioniksysteme führt. Darauf folgt ein Teil zu Analog-Digital Wandlern, mit Fokus auf dem Sigma-Delta Verfahren. Die letzten beiden Kurseinlagen befassen sich mit der Programmierung eingebetteter Systeme, speziell werden zwei konkrete Beispiele beleuchtet: Das I2C Subsystem von Linux und Schedulingkonzepte in FreeRTOS.

Mein Zusatzthema beinhaltet einen kurzen Überblick über alternative Hardware-Beschreibungssprachen, und gibt dann eine kurze Einführung, wie mittels nMigen Hardware in Python spezifiziert werden kann. Die Motivation hierzu war ein wachsendes Interesse an diesem und ähnlichen Tools in der Open-Source-FPGA Community und anfängliche Frustration mit VHDL während der ersten Übungsaufgaben.

In der Programmierung von eingebetteten Systemen habe ich bereits einige Vorkenntnisse mitgebracht, aber speziell in den Bereichen programmierbarer Logik und Hardwaredesign hat die Vorlesung mir einen guten ersten Einblick gegeben. Von der Übung habe ich mir einen Einstieg in die FPGA Programmierung erwartet, was sich dann auch erfüllt hat. Die Übungen sind im vierten Kapitel dokumentiert.

Abschließend werden in einem kurzem Fazit die Erkenntnisse der Vorlesung und Übung zusammengefasst.

Kurseinlagen

Kurseinlage zu Vorlesung 4: “Logic Fabrics: Technologie und Entwurf”

In dieser Vorlesung wurden einige Varianten der Realisierung eines eingebetteten Systems gezeigt:

Gerade beim IC Design existieren eine Vielzahl unterschiedlicher Abstraktionsebenen, von der physikalischen Beschreibung der Transistoren, über elektrische und logische Schaltbilder bis hin zu Verhaltensbeschreibungen. Je tiefer die Abstraktionsebene gewählt wird, desto höher die Flexibilität:

Full Custom

Bei einem sogenannten “Full Custom” Entwurf, also dem direkten Spezifizieren der Halbleiterschichten des ICs, kann zum Beispiel das Layout einzelner Schaltungselemente so angepasst werden, wie es für die spezielle Anwendung ideal ist.

Semi Custom

Der Semi Custom Entwurf unterscheidet sich vom Full Custom Entwurf darin, dass hier nicht der Physikalische Aufbau des Chips und der genaue Fertigungsprozess interessiert, sondern die resultierende Logikfunktion der Schaltung. Der Entwurf beschränkt sich hier also darauf, Logikgatter zusammenzufügen. Einzelne Funktionen wie z.B. die Addition, können bereits als fertige Komponenten in einer Bibliothek vorliegen, die vom Entwickler direkt verwendet werden können.

Standardzellenentwurf

Beim Standardzellenentwurf werden vorgefertigte Komponenten verwendet, welche beim Design zusammengefügt werden. Jede Komponente enthält bereits ein Layout, welches somit nicht selbst entworfen werden muss. Zusätzlich können Komponenten Verhaltensbeschreibungen zur Simulation enthalten.

Gate Arrays

Beim Gate Array Entwurf werden Gates verwendet, die bereits in einem Raster angeordnet sind, und dann verbunden werden. Die Anordnung der Gates ist also vorgegeben, die Verbindungen dazwischen aber nicht. Diese werden üblicherweise auch nicht manuell, sondern mittels eines Synthesetools generiert.

Ein Nachteil des Gate Arrays im Vergleich zum Full-Custom oder Standardzellenentwurf ist ein größerer Flächenbedarf des Chips.

Die Verdrahtung des Gate Arrays kann auch in Form einer Metallisierungsebene permanent auf ein sogenanntes Sea-of-Gates aufgebracht werden, was ein kompakteres Design ermöglicht.

Field Programmable Gate Arrays (FPGA)

Auf modernen FPGAs kann ein ganzes eingebettetes System inklusive mehrerer Prozessorkerne realisiert werden. FPGAs enthalten auch eine Anordnung von vordefinierten Modulen, welche aber programmierbar sind, zum Beispiel in Form einer lookup table und Flipflops.

Zur Verdeutlichung der Vorlesungsinhalte habe ich nach einem zusammenfassenden Text gesucht, der insbesondere die einzelnen Methoden vergleichend gegenüberstellt, und mehr Unterschiede als Kosten und Flexibilität aufzählt. Einen guten Überblick habe ich in einem Beitrag auf der Website “Numato Lab” [1] gefunden. Der Artikel wiederholt Anfangs kurz, was ein FPGA und was ein ASIC ist, und zählt dann relevante Unterschiede auf. Ein interessanter Unterschied ist, dass ASICs deutlich energieeffizienter arbeiten als FPGAs, und gleichzeitig bei gleichem Produktionsprozess viel höhere Frequenzen in digitalen Schaltkreisen ermöglichen. Außerdem wird noch hervorgehoben, dass in ASICs auch Analogkomponenten oder HF Schaltkreise realisiert werden können.

Der Artikel nennt als Gemeinsamkeit von FPGA und ASIC, dass beide mittels HDLs wir Verilog und VHDL entworfen werden. Das wirft die Frage auf, wie das beim ASIC Workflow funktioniert, ob hier Verilog mit Hilfe eigener Bausteine für Gates direkt zu einem Maskenlayout synthetisiert wird, was natürlich im Kontrast zur oben genannten Flexibilität, einzelne Gates genau anzupassen, steht. Eine weitere interessante Frage, der hier aber nicht weiter nachgegangen wird, ist auch, wie ein Analogdesign in solch einen Chip integriert wird, und ob hier auch Beschreibungssprachen wie für digitale Logik existieren.

Kurseinlage zu Vorlesung 7: “Sensoren und Aktoren II”

In diesem Kapitel wird als Beispiel für einen Sensor im eingebetteten System das Staurohr eines Flugzeugs betrachtet. Ziel des Sensors ist es, die Geschwindigkeit des Flugzeugs relativ zur umgebenden Luft zu bestimmen. Wenn mittels einer Sonde sowohl der Staudruck als auch der statische Druck außerhalb des Flugzeugs gemessen werden, kann aus der Differenz der beiden Werte die Geschwindigkeit ermittelt werden. Da nur die Differenz von Staudruck und statischem Druck interessiert, kann der Sensor in Form einer Membran zwischen zwei Volumina, in denen der jeweilige Druck herrscht, realisiert werden. Ein Dehnungsmessstreifen auf der Membran verändert seinen Widerstand je nach Wölbung der Membran. Die Messung des Widerstands geschieht mit einer Messbrücke.

Ein anderer im Flugzeug benötigter Sensor ist der Drehratensensor am Fahrwerk. Die gemessene Drehrate kann zum Beispiel als Eingang für ein Antiblockiersystem verwendet werden. Zur Messung der Drehrate gibt es verschiedene Messmethoden. Die erste vorgestellte Methode ist ein optischer Sensor, der durchsichtige und undurchsichtige Bereiche einer sich drehenden Scheibe erkennt. Durch Zählen der Ausgangsfrequenz kann die Drehrate bestimmt werden. Eine andere Möglichkeit ist, Magnete so am sich drehenden Teil zu befestigen, dass diese sich an einem stationären Hallsensor vorbeibewegen, welcher das Magnetfeld misst. Wie beim optischen Sensor kann dann die Drehrate bestimmt werden. Ein Unterschied ist, dass beim (analogen) Hallsensor das sinusförmige Ausgangssignal zuerst in eine Impulsfolge umgewandelt werden muss, was mit einem Schmitt Trigger möglich ist.

Im Flugzeug-Beispiel ist neben der Sensorik auch die Aktorik vertreten. Die Klappen werden heutzutage elektrisch angesteuert, der Aktor kann direkt an der Klappe angebracht sein, und es sind nur elektronische Leitungen zum Steuercomputer notwendig. In der Vorlesung wird als Steuersignal PWM vorgestellt. Da PWM als nicht sehr resistent gegenüber Elektromagnetischer Interferenz bekannt ist, und natürlich keinerlei Fehlererkennung und -korrektur beinhaltet, wirkte es etwas verwunderlich, dass ein PWM Signal für eine solche sicherheitsrelevante Aufgabe über längere Strecken benutzt wird. Im nächsten Teil der Vorlesung wurden dann auch SPI, I2C und UART als digitale Protokolle zwischen Sensor-, Aktor-, und Prozessorkomponenten in einem System vorgestellt, auf LVDS wurde detaillierter eingegangen. Nach kurzer Recherche bin ich auf AFDX gestoßen, ein auf Ethernet aufbauendes Netzwerk, welches von Airbus speziell für Flugzeugsysteme entwickelt wurde.

Ein Überblick ist in [2] gegeben. AFDX wurde entwickelt, da bestehende Netzwerktechnologien im Flugzeugbereich nicht die ausreichenden Datenraten unterstützten. Das Paper erklärt zuerst, dass bei traditionellem Ethernet Übertragungen kollidieren können, was dazu führen kann dass manche Pakete nie übertragen werden, oder nur mit großer Verzögerung. Daher ist es notwendig, Full-Duplex Switched Ethernet zu verwenden, bei dem ein Link nur einen einzigen Host mit einem Switch verbindet. AFDX Ethernet Frames enthalten wie normale Ethernet Frames eine Checksumme, die Start- und Zieladressen sind allerdings genau spezifiziert und enthalten eine “Virtual Link ID”, welche zum Routen der Pakete verwendet wird. Ein AFDX Payload wird in einem UDP Paket verschickt, welches im UDP Header eine weitere Prüfsumme enthält. Nach dem Payload wird allerdings noch eine Sequenznummer angefügt, was bei UDP normalerweise nicht der Fall ist. In [2] wird auch ein 20 Byte IP Header, was auf eine Verwendung von IPv4 schließen lässt, obwohl das nicht explizit angegeben ist. Warum hier UDP Pakete mit Sequenznummer im Payload verwendet wird, und nicht TCP, wird nicht ganz klar. Das charakteristische Feature sind die oben erwähnten “Virtual Links”, die mit einer maximalen Bandbreite spezifiziert sind. Die Switches kennen die Spezifikation aller Virtual Links, und können eintreffende Pakete, die das Limit überschreiten verwerfen, um die Bandbreite für andere Links zu garantieren.

Kurseinlage zu Vorlesung 10: “Analog-Digital Umsetzer”

In der Zehnten Vorlesung wurden die Analog-Digital Umsetzer (ADC) vorgestellt, als essenzieller Baustein zwischen jeder Art von Sensor und einem digitalen System zur Verarbeitung, wie einem Mikroprozessor.

Es gibt viele verschiedene Arten, einen ADC zu realisieren, die alle unterschiedliche Vor- und Nachteile aufweisen und für verschiedene Anwendungsbereiche geeignet sind:

Zuerst wurden die Parallelverfahren vorgestellt, hier wird im einfachsten Fall mir genau so vielen Komparatoren wie Quantisierungsstufen das Signal abgeglichen, wobei die Komparatoren beispielsweise von einer Widerstandskette Referenzspannungen in gleichen Abständen erhalten. Diese Art von ADC hat den Vorteil, in einem einzelnen Taktzyklus das Signal zu digitalisieren, hat aber den größten Hardwareaufwand von allen Typen. Ähnliche Umsetzer, die aber mehrstufig aufgebaut sind, finden zum Beispiel in schnellen Oszilloskopen Anwendung.

Eine Variante, die mit weniger Hardwareaufwand auskommt, ist das Wägeverfahren. Hierbei wird nur ein einziger Komparator genutzt, die Vergleichsspannung muss allerdings von einem DAC (Digital-Analog Wandler) erzeugt werden. Durch bitweises annähern der Vergleichsspannung an die Eingangsspannung, kann so der Wert dieser Digitalisiert werden. Da in dieser Variante die Zeit der Messung direkt proportional ist zur Anzahl der zu messenden Bits, wird üblicherweise eine Sample-and-Hold Schaltung benötigt, die die Eingangsspannung während der Messung konstant hält.

Die Zählverfahren arbeiten nach dem Prinzip, die Analogspannung in eine Frequenz oder eine Abfolge von Impulsen umzuwandeln, welche dann mit einem digitalen Zähler gezählt werden können. Dafür muss die Spannung zuerst in eine Zeitdauer umgewandelt werden, in dieser zu zählende Pulse generiert werden. Typischerweise wird dies realisiert, indem die Eingangsspannung mit einem Sägezahnsignal verglichen wird, beziehungsweise eine solche Rampe im Dual-Slope Verfahren ein Rampenförmiges Signal abhängig von der Eingangsspannung innerhalb vorgegebener Zeit erzeugt wird.

Im Sigma-Delta Verfahren ist die Herangehensweise, das Signal mittels des Sigma-Delta Modulators in einen Bitstream umzuwandeln, in dem das Verhältnis von Einsen und Nullen proportional zur Eingangsspannung ist. Da mir dieses Prinzip in der Vorlesung nicht ganz klar wurde, habe ich zuerst einen Artikel von TI [3] gefunden, welcher das Verfahren der Sigma-Delta Modulation noch mal Schritt für Schritt auflistet. In letzten Teil wird auch kurz auf das Noise-Shaping eingegangen, und mit einem Diagramm der unterschiedlichen Rauschdichten verdeutlicht, warum Sigma-Delta ADCs höherer Ordnung nochmals deutlich geringeres Rauschen aufweisen. Die Notwendigkeit der anschließenden digitalen Filterung des Signals wird zwar angesprochen, für Details wird aber auf einen anderen Artikel verwiesen. Eine weitere Resource, die besonders für das Verständnis, wie der vom Modulator kommende Bitstream genau zu interpretieren ist, war das interaktive Tutorial von Analog Devices [4], welches das Eingeben von Eingangs- und Referenzspannung erlaubt und dann für jeden einzelnen Schritt die Spannungswerte an den relevanten Stellen darstellt.

Kurseinlage zu Vorlesung 11: “Programmierung eingebetteter Systeme”

Ab dieser Vorlesung wird sich nicht mehr mit der Hardware, sondern der Software und der Schnittstelle zwischen Hard- und Software befasst.

Eine populäre Methode zur Anbindung von Peripherie an einen Prozessorkern ist das zugänglich machen von Registern mittels Memory Mapped IO. Hier kann das Gerät in einer Weise vom Prozessor angesprochen werden, die sich nicht von “normalen” Speicherzugriffen unterscheidet. Dafür werden üblicherweise vom Treiber Datenstrukturen definiert, die das Interface zur Peripherie so abbildet, dass es für andere Programme benutzbar ist. Der Treiber bietet oft auch Funktionen zum interagieren mit dem Gerät an, welche die Abstraktionsebene anheben und z.B. verschiedene Varianten eines Gerätetyps ansprechen können oder Fehlerbehandlung beherrschen.

Besonders für Sensoren, dessen Messungen mit wenig Verzögerung verarbeitet werden sollen, sind Interrupts ein wichtiges Konzept. Hier wird der Prozessor (durch den Interrupt Controller) in der Ausführung unterbrochen, und eine vorher definierte Interrupt Service Routine wird ausgeführt, welche dann z.B. die Verarbeitung des Sensorwerts anstoßen kann.

Ein verbreitetes Kommunikationsprotokoll zwischen Prozessoren und Ein-/Ausgabegeräten ist I2C. Hier sind mehrere Geräte an einem geteilten Bus verbunden, und ein oder mehrere dieser Geräte können als Initiator den Bus für die Kommunikation mit einem anderen, durch eine eindeutige Adresse spezifiziertem Gerät nutzen. Aus aktuellem Eigenbedarf und zum verdeutlichen der Prinzipien eines Gerätetreibers soll hier die Benutzung von I2C Geräten unter Linux betrachtet werden. Dies unterscheidet sich etwas von der Art, wie entsprechende Geräte z.B. auf Mikrocontrollern angesprochen werden, da bereits einige Schichten an Abstraktion vom Betriebssystem zur Verfügung gestellt werden.

In der aktuellen Kernel Dokumentation [5] im Kapitel “I2C and SMBus Subsystem” werden die Funktionen und Datenstrukturen definiert, mit denen Gerätetreiber im Linux Kernel das I2C Subsystem verwenden können. Ein spezifisches I2C Gerät wird beispielsweise durch den i2c_client struct repräsentiert:

struct i2c_client {
  unsigned short flags;
  unsigned short addr;
  char name[I2C_NAME_SIZE];
  struct i2c_adapter *adapter;
  struct device dev;
  int init_irq;
  int irq;
  struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE);
  i2c_slave_cb_t slave_cb;
#endif;
};

Die hier dokumentierte API richtet sich allerdings an Entwickler, welche einen Kernel-Treiber für ein Gerät schreiben. Es ist auch möglich, I2C Geräte vom userspace zu nutzen, ohne einen speziellen Treiber zu benötigen (Kapitel “Implementing I2C device drivers in userspace”): Hier wird jedem I2C Interface vom Kernel eine spezielle Datei /dev/i2c-0 (bzw -1 usw.) zugewiesen, auf die je nach System Konfiguration auch von nicht-superuser Benutzern zugegriffen werden kann. Nach dem öffnen der Datei kann mittels ioctl [6] die Target-Adresse gesetzt, und danach mit den üblichen write() und read() Systemaufrufen I2C-Transaktionen durchgeführt werden. Obwohl die Dokumentation ein Beispiel in C liefert, kann dies natürlich aus jeder Programmiersprache geschehen, die I/O und ioctl unter Linux unterstützt. Da ich gerne C++ nutze, habe ich diese Methoden in einer C++ Bibliothek zusammengefasst. Ein I2C Bus wird so mit

i2c::Bus bus{"/dev/i2c-0"};

initialisiert. Danach kann vom Gerät gelesen und zum Gerät geschrieben werden. Hier im Beispiel werden zuerst 3 Bytes geschrieben und dann 7 Bytes gelesen:

constexpr auto DEVICE_ADDRESS = 0x40;
using namespace std;
i2c::Bus bus{"/dev/i2c-0"};
bus.lock(DEVICE_ADDRESS).write<3>({byte{0xA}, byte{0xB}, byte{0xC}});
auto dataFromDevice = bus.lock(DEVICE_ADDRESS).read<7>();

Dabei wird der Bus vor jeder Transaktion gelockt und nach Abschluss automatisch wieder unlocked, was das Benutzen des Bus von mehreren Threads ermöglicht. Soll eine Transaktion mit mehreren Read- und Write Aufrufen am Stück, ohne eventuelles verwenden des Bus von anderen Threads, durchgeführt werden, wird der Bus nur ein mal für das gesamte Scope gelockt:

constexpr auto DEVICE_ADDRESS = 0x40;
using namespace std;
i2c::Bus bus{"/dev/i2c-0"};
{
    auto lockedBus = bus.lock(DEVICE_ADDRESS);
    lockedBus.write<3>({byte{0xA}, byte{0xB}, byte{0xC}});
    auto dataFromDevice = lockedBus.read<7>();
}

Für ein Spezielles Gerät kann nun eine Klasse definiert werden, welche eine Referenz auf den Bus enthält, und die Funktionen des Geräts bereitstellt. Im oben verlinkten Repository ist beispielsweise eine Klasse für den PCA9685 PWM Controller enthalten.

Im Linux Treiber sind einige Probleme ersichtlich geworden, die in der Vorlesung nicht direkt erwähnt wurden: Ein Gerätetreiber ist üblicherweise in ein komplexes System integriert. Es können mehrere Interfaces zu diesem Treiber existieren, hier zum Beispiel das /dev/- und das Kernel-Interface. Ein Treiber muss auch nicht immer den gesamten Stack zwischen Applikation und Hardware abdecken, in Linux gibt es ein I2C Subsystem, auf welches die diversen Gerätetreiber dann aufbauen, ohne das I2C Interface an sich selbst zu implementieren. Außerdem kommen in Mehrbenutzersystemen oder Multithreaded Applikationen die üblichen Probleme des gleichzeitigen Zugriffs hinzu, die im Treiber bedacht werden müssen.

Kurseinlage zu Vorlesung 13: “Echtzeitbetriebssysteme”

Zur Veranschaulichung der Konzepte von Scheduling und insbesondere Scheduling im Realtime Kontext, habe ich mir angeschaut, wie das bei einem RTOS gelöst ist, mit dem ich bereits im Microcontroller-Praktikum in Kontakt gekommen bin, FreeRTOS [7].

Das Scheduling wird in der Dokumentation im Kapitel RTOS Fundamentals erklärt. Zur Einführung wird wiederholt, dass hier auf Multitasking eingegangen wird, was nicht zwingend auch eine tatsächliche gleichzeitige Ausführung impliziert, wie es in einem System mit mehreren Prozessoren möglich wäre. FreeRTOS ist populär besonders auf Microcontroller-Systemen, wo meist nur ein einziger Prozessorkern zur Verfügung steht. Aus diesem Grund implementiert FreeRTOS einen Scheduler, der mehrere scheinbar gleichzeitig ausführende Tasks auf einem Kern ausführt.

Der FreeRTOS Kernel kann Prozesse anhalten, um einen anderen Task auszuführen, ein Prozess kann aber auch freiwillig die Kontrolle an das Betriebssystem abgeben, zum Beispiel wenn dieser Prozess einen delay braucht oder auf ein Event (z.B. ein Tastendruck) oder eine Resource (z.B. Serial-port) gewartet wird. Dies wird in der Dokumentation mit einem Ablaufdiagramm veranschaulicht, welches sowohl ein Unterbrechen eines Tasks aufgrund der aktuellen scheduling policy (fair), als auch ein Unterbrechen durch den Task selbst. Diese Scheduling Variante hat noch keine Prioritäten.

Den kurzen Einschub zu Context Switching, der in der Doku an dieser Stelle steht, überspringe ich hier.

Der nächste Abschnitt betrifft Realtime Applikationen. Das Beispiel, welches von den Entwicklern gegeben ist, besteht aus zwei Tasks: Der erste Task ist für user-input zuständig. Er wartet, bis ein Tastendruck erkannt wird, und gibt dann das Resultat auf einem Display aus. Die maximale Latenz zwischen Tastendruck und Feedback auf dem Bildschirm beträgt 100ms.

Der zweite Task implementiert einen Regler, der alle 2ms, mit einer Toleranz von 0.5ms, ausgeführt werden muss. Da die Deadline des zweiten Tasks früher als die des ersten ist, sollte diesem eine höhere Priorität zugewiesen werden. Außerdem könnte ein Überschreiten der Deadline beim zweiten Task schwerwiegendere Auswirkungen haben.

Wenn die Tasks wie beschrieben geordnet sind, ändert sich das Scheduling Verhalten: Wenn eine Taste gedrückt wird, während der Regler rechnet, wird dieses Event zurückgestellt, bis der Regler fertig ist. Wenn das Zeitfenster für den Regler erreicht wird, während aber gerade ein Tastendruck verarbeitet wird, wird diese Verarbeitung unterbrochen.

Ein weiteres Konzept, was hier hinzukommt, ist der sogenannte “Idle Task”, der vom RTOS selbst angelegt wurde, und ausgeführt wird solange der Regler nicht rechnet, und keine Taste gedrückt wird.

Interessant an dieser Erklärung ist, dass die Anforderungen des Reglers nur durch geschicktes Wählen der Prioritäten eingehalten werden. Wie das dann aber aussieht, wenn mehrere Zeitkritische Tasks laufen, die z.B. unterschiedliche Toleranzen hinsichtlich der Ausführungszeit haben, wird hier nicht erläutert. Der Scheduling Algorithmus wurde hier nicht über die expliziten Zeitschranken informiert, und könnte z.B. nicht eine Verarbeitung eines Tastendrucks unterbrechen, um dafür eine fristgerechte Verarbeitung einer währenddessen gedrückten Taste zu gewährleisten. Dennoch hilft dieses praktische Beispiel dem Verständnis und der Intuition bezüglich Scheduling und Echtzeitanforderungen.

Zusatzthema: nMigen

Motivation

Schon seit den 1980er bzw. 1990er Jahren existieren die uns bekannten Sprachen zur Hardwarebeschreibung VHDL [8] und Verilog [9]. Wir haben in VHDL die Möglichkeiten, einen Schaltkreis sowohl anhand dessen Struktur, also der direkten Verschaltung und Verknüpfung von Komponenten und Signalen, als auch über das gewünschte Verhalten des Schaltkreises zu beschreiben. Es ist möglich, Teile eines Gesamtsystems in Komponenten auszulagern, welche auch mehrfach wiederverwendet beziehungsweise instanziiert werden können. Möglichkeiten zur Metaprogrammierung sind vorhanden mit Konstrukten wie generic und generate (Übung).

Dennoch wäre es wünschenswert, Hardwarebeschreibung mittels bereits bekannter, etablierter Programmiersprachen zu realisieren, die auch für wiederkehrende Probleme wie Dependency-Management, das Wiederverwenden von Code (Libraries), etc. bereits eine Lösung bereithalten. Ein nicht von der Hand zu weisender Vorteil ist auch, dass viele Entwickler bereits Erfahrung in einer dieser Sprachen besitzen, was auch zur wachsenden Popularität dieser Lösungen beiträgt.

Mehrere Projekte, die Hardwarebeschreibung in Form von domain specific languages (DSL) innerhalb anderer Programmiersprachen realisieren, existieren. Einige Beispiele, mit definierenden Features und Highlights sind:

Dies sind nur einige Beispiele von vielen verschiedenen DSLs, hier nur zu erwähnen sind noch PyRTL [14], CLasH [15], SysPy [16] und SpinalHDL [17]. Die hier näher betrachtete Lösung ist nMigen [18].

Im Folgenden soll ein beispielorientierter Überblick über die wichtigsten Konzepte in nMigen gegeben werden, um einen Eindruck von der Bibliothek zu bekommen. Mein Ziel ist es auch zu testen, wie vergleichbar das Ganze mit VHDL ist, und ob die oben genannten Ziele erfüllt werden.

Hintergrund

nMigen ist die zweite Version der Migen Bibliothek für Hardware Design in Python. Die gesamte Software ist Open Source, gehostet auf GitHub. Auf GitHub werden 25 Autoren gelistet, die initiale Entwicklung stammt aber von der Firma M-Labs, welche auch initialer Autor des Vorgängers Migen ist. Zusätzlich an der Entwicklung beteiligt ist die Firma LambdaConcept, welche wie M-Labs Migen und nMigen in eigenen Produkten einsetzen.

Board Definition

In nMigen (genauer: im Modul nmigen-boards) sind viele FPGA-Boards von unterschiedlichen Herstellern, mit freien und proprietären Toolchains, bereits definiert. Das DE2-115 fehlt zwar, kann aber mit vergleichsweise wenig Aufwand hinzugefügt werden, da bereits mehrere DE0 und DE10 Boards existieren: Eine neue Klasse DE2115Platform wird erstellt, diese erbt von IntelPlatform, was bereits die meisten relevanten Einstellungen enthält. Hier ein Ausschnitt der entsprechenden Datei, hier werden die vorhandenen Clocks, LEDs, Buttons, Schalter und eine serielle Schnittstelle definiert:

class DE2115Platform(IntelPlatform):
    device = "EP4CE115"  # Cyclone IV
    package = "F29"
    speed = "C8"
    default_clk = "clk50"
    resources = [
        Resource("clk50", 0, Pins("Y2", dir="i"),
                 Clock(50e6), Attrs(io_standard="3.3-V LVTTL")),
        Resource("clk50", 1, Pins("AG14", dir="i"),
                 Clock(50e6), Attrs(io_standard="3.3-V LVTTL")),
        Resource("clk50", 2, Pins("AG15", dir="i"),
                 Clock(50e6), Attrs(io_standard="3.3-V LVTTL")),

        *LEDResources(pins="E21 E22 E25 E24 H21 G20 G22 G21",
                      attrs=Attrs(io_standard="2.5 V")),

        *ButtonResources(pins="M23 M21 N21 R24",
                         attrs=Attrs(io_standard="2.5 V")),

        *SwitchResources(pins="AB28 AC28 AC27 AD27 AB27 AC26 AD26 AB26 AC25 "
                              "AB25 AC24 AB24 AB23 AA24 AA23 AA22 Y24 Y23",
                         attrs=Attrs(io_standard="2.5 V")),

        UARTResource(0,
                     rx="G12", tx="G9", rts="J13", cts="G14",
                     attrs=Attrs(io_standard="3.3-V LVTTL")),
    ]

Um anderen Nutzern von nMigen das Verwenden des DE2-115 einfacher zu machen, wurde ein Pull-Request auf GitHub für die Boarddefinition erstellt.

Hello World

Das klassische “Hello World” der Hardware Welt ist sicher das “Blinky” Programm, das nichts weiter tut, als eine LED zu blinken. So auch im nMigen Tutorial [19], dem in den nachfolgenden Kapiteln gefolgt wird:

from nmigen import *
from nmigen.cli import main
 
 
class Blinky(Elaboratable):
    def __init__(self):
        self.led = Signal()
 
    def elaborate(self, platform):
        m = Module()
        counter = Signal(3)
        m.d.sync += counter.eq(counter + 1)
        m.d.comb += self.led.eq(counter[2])
        return m
 
 
if __name__ == "__main__":
    top = Blinky()
    main(top, ports=[top.led])

Klassen, die synthetisierbare Module erzeugen, erben immer von Elaboratable. Die Funktion elaborate() der Klasse erzeugt dann das entsprechende Modul. Signale werden im Konstruktor angelegt.

Hier kann natürlich auf sämtliche Python Features zurückgegriffen werden, mit Hilfe der math Bibliothek beispielsweise wird der Counter auf ca. 1Hz eingestellt, und die tatsächliche Frequenz wird ausgegeben:

counter_width = round(log2(platform.default_clk_frequency))
actual_freq = platform.default_clk_frequency / (2 ** counter_width)
print(f"creating {counter_width}-bit counter, "
      f"resulting frequency will be {actual_freq}Hz")

counter = Signal(counter_width)
m.d.sync += counter.eq(counter + 1)
m.d.comb += self.led.eq(counter[-1])

Das Blinky Modul ist noch nicht mit Pins auf dem Board verbunden, dafür wurde ein zweites Modul geschrieben, um Blinky nicht darauf zu beschränken, direkt Hardware Pins anzusteuern:

class Blinker(Elaboratable):
    def __init__(self):
        self.blinky = Blinky()

    def elaborate(self, platform):
        m = Module()
        m.submodules.blinky = self.blinky
        led_pin = platform.request("led", 0)
        m.d.comb += led_pin.o.eq(self.blinky.led)

        return m

In diesem Modul wird ein Blinky instanziiert und als Submodul verwendet. Mittels platform.request wird der als "led0" definierte Pin des jeweiligen Boards verwendet.

Um das Programm auf das Board zu schreiben genügt:

top = Blinker()
DE2115Platform().build(top, do_program=True)

In diesem Fall ist der Output von nMigen Verilog Code, welcher dann mit der Quartus Toolchain von Intel/Altera synthetisiert und auf den FPGA geladen wird. Für FPGAs der iCE40 und ECP5 Serie von Lattice wird eine vollständig freie Open Source Toolchain (Yosys+nextpnr [20]) verwendet.

Kombinatorische und Synchrone Logik

Die eigentliche Spezifikation der Logik im Blink Beispiel passiert in der Funktion elaborate:

def elaborate(self, platform):
    m = Module()
    counter = Signal(3)
    m.d.sync += counter.eq(counter + 1)
    m.d.comb += self.led.eq(counter[2])
    return m

Wie auch VHDL kennt nMigen kombinatorische und synchrone Logik. Mit

m.d.sync += counter.eq(counter + 1)

wird der counter in jedem Taktzyklus inkrementiert, in der nächsten Zeile

m.d.comb += self.led.eq(counter[2])

wird die LED aber fest mit dem höchstwertigem Bit des Zählers verbunden, unabhängig von der Clock (Kombinatorische Logik).

Hierarchische Designs

Im vorherigen Kapitel wurde bereits ein hierarchisches Design verwendet, ohne genauer darauf einzugehen. Um dieses Prinzip genauer zu verstehen, soll eine ALU (Arithmetic Logic Unit) aufgebaut werden, die zwei Eingänge abhängig von einem Kontrollsignal addiert oder subtrahiert.

Im ersten schritt wird ein Addierer und ein Subtrahierer geschrieben:

class Adder(Elaboratable):
    def __init__(self, width):
        self.a = Signal(width)
        self.b = Signal(width)
        self.o = Signal(width)

    def elaborate(self, platform):
        m = Module()
        m.d.comb += self.o.eq(self.a + self.b)
        return m
class Subtractor(Elaboratable):
    def __init__(self, width):
        self.a = Signal(width)
        self.b = Signal(width)
        self.o = Signal(width)

    def elaborate(self, platform):
        m = Module()
        m.d.comb += self.o.eq(self.a - self.b)
        return m

Diese Module enthalten beide jeweils zwei Eingänge a und b und einen Ausgang o. Die Addition und Subtraktion wird in Form von kombinatorischer Logik angegeben. Anzumerken ist hier aber, dass dem Konstruktor ein weiteres Argument width hinzugefügt wurde. Dies ist die Breite der Ein- und Ausgangssignale, und kann frei gewählt werden. Die ALU wird auch ein entsprechendes Argument erhalten und dann den passenden Addierer/Subtrahierer instanziieren:

class ALU(Elaboratable):
    def __init__(self, width):
        self.op  = Signal()
        self.a   = Signal(width)
        self.b   = Signal(width)
        self.o   = Signal(width)
 
        self.add = Adder(width)
        self.sub = Subtractor(width)
 
    def elaborate(self, platform):
        m = Module()
        m.submodules.add = self.add
        m.submodules.sub = self.sub
        m.d.comb += [
            self.add.a.eq(self.a),
            self.sub.a.eq(self.a),
            self.add.b.eq(self.b),
            self.sub.b.eq(self.b),
        ]
        with m.If(self.op):
            m.d.comb += self.o.eq(self.sub.o)
        with m.Else():
            m.d.comb += self.o.eq(self.add.o)
        return m

Im Konstruktor wird der Addierer und Subtrahierer instanziiert und als Membervariable zugewiesen. In der elaborate Funktion werden diese dem Modul als Submodul hinzugefügt, und die Ein- und Ausgänge der Module werden mit den entsprechenden Signalen der ALU verbunden. Das zusätzliche Signal op wählt die Funktion der ALU aus. Da das resultierende Design ja beide Modi enthalten muss, kann hier kein python-if verwendet werden, sondern in einer etwas abweichenden Syntax:

with m.If(self.op):
    m.d.comb += self.o.eq(self.sub.o)
with m.Else():
    m.d.comb += self.o.eq(self.add.o)

Testing

nMigen unterstützt Integration in Pythons standard Unit-Testing tools, und integriert Möglichkeiten zur Simulation. Natürlich kann auch ohne Hilfe von Unit-Test Bibliotheken ein Test geschrieben werden:

width = 4
alu = ALU(width)
sim = Simulator(alu)
with sim.write_vcd("alu.vcd"):
    def process():
        for a, b in itertools.product(range(2 ** width), range(2 ** width)):
            yield alu.a.eq(a)
            yield alu.b.eq(b)
            yield alu.op.eq(0)
            yield Delay()
            print(f"{a}+{b}={(yield alu.o)}")
            assert (a + b) % (2 ** width) == (yield alu.o)
            yield alu.op.eq(1)
            yield Delay()
            print(f"{a}-{b}={(yield alu.o)}")
            assert (a - b) % (2 ** width) == (yield alu.o)

Für den Simulator wird die process Funktion definiert, die mittels yield die Simulierten Eingänge generiert, und dann zum verifizieren der Ergebnisse mit assert die Korrektheit prüft.

Evaluation

Nach einigem Ausprobieren und Testen bin ich der Ansicht, dass HDLs wie nMigen gut nutzbar und eine echte Alternative zu VHDL sind. Dass die Integration in Python nicht nur eine Fülle an Möglichkeiten zur Metaprogrammierung liefert, sondern auch die Wiederverwendung von Modulen vereinfacht, ist ein enormer Vorteil. Für den Vorgänger von nMigen, Migen, existiert das LiteX Projekt [21], eine Open-Source Bibliothek, die alle Bestandteile enthält um einen eigenen SoC (System On Chip) im Baukastensystem aufzubauen. Neben verschiedenen Softcores sind Schnittstellen wie Ethernet, PCIe, DRAM und mehr verfügbar, für Videoverarbeitung existieren im LiteVideo Projekt HDMI Ein- und Ausgang sowie Module zur Konvertierung zwischen Farbräumen.

Die verfügbare Dokumentation zu nMigen lässt momentan noch zu Wünschen übrig, das Manual zum Vorgänger Migen ist aber ausführlich. Auf GitHub sind außerdem einige Projekte und Beispiele zu finden, teils auch für spezifische Boards.

Abschließend lässt sich sagen, dass besonders mit Python Vorkenntnissen nMigen einen einfacheren Einstieg bietet als VHDL, die Hardware-Kompatibilität und Dokumentation für den Produktiven Einsatz aber besser werden muss.

Übung

In diesem Teil werden die Ergebnisse der Übungen dargestellt. Bei der Darstellung des VHDL Codes habe ich mich auf die interessanten Teile beschränkt, die volle Lösung mit allen notwendigen Dateien ist zu jeder Aufgabe im git Repository einzusehen.

Einfacher Multiplexer

Zur besseren Strukturierung habe ich mich dazu entschieden, den eigentlichen Multiplexer als eigene Entity zu beschreiben, die nicht die Top-Level entity ist (Danke Dominik für den Hinweis!). Das ist anders als in der vorherigen Übung, in der direkt die Bezeichnungen des Boards verwendet wurden. Das erlaubt mir, den Eingangs- und Ausgangssignalen sinnvolle Namen zu geben, die nicht direkt mit dem Board zusammenhängen (IN_1 statt SW(1) oder so). Außerdem ist es damit möglich, mehrere Instanzen der entity zu verwenden (was hier aber noch nicht gemacht wird).

Beim instanziieren des Multiplexers ist das Problem aufgetreten, dass multiplexer nicht gefunden wurde. Kurzes recherchieren hat ergeben dass work.multiplexer das Problem löst, wobei an Stelle von work normalerweise der Name einer Bibliothek steht, work ist hier speziell und bedeutet “aktuelle Bibliothek”.

Fancy Multiplexer

Hier war der Ansatz, so weit wie möglich den Ansatz aus Aufgabe 1 zu übernehmen. Im Wesentlichen war es nur nötig, den Datentyp der Eingänge zu STD_LOGIC_VECTOR zu machen. Das SELECT Statement konnte übernommen werden, wobei für den select Eingang jeweils der passende Binärvektor notiert wurde:

SEL : IN std_logic_vector(2 DOWNTO 0);
WITH SEL SELECT
    RESULT <= IN1 WHEN "000",
    IN2 WHEN "001",
    IN3 WHEN "010",
    IN4 WHEN "011",
    IN5 WHEN "100",
    IN1 WHEN OTHERS;

Für Verwirrung beim Testen hat gesorgt, dass die Vektor-literals hier in anderer Reihenfolge als erwartet sind. Kurzes Testen:

LEDG(3 TO 7) <= "11100";

Das Ergebnis ist, dass LEDs 3 bis 5 leuchten. Die Literals im Multiplexer werden also umgedreht. In Übung 2 ist das Problem wohl nicht aufgetreten, da das SEL Signal da mit DOWNTO statt TO deklariert war, was hier aber das Problem auch nicht gelöst hat…

Finaler VHDL Code

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;

ENTITY aufgabe2 IS
    PORT (
        SW : IN std_logic_vector(0 TO 17);
        LEDR : OUT std_logic_vector(0 TO 17);
        LEDG : OUT std_logic_vector(0 TO 7));
END aufgabe2;

ARCHITECTURE LogicFunction OF aufgabe2 IS
BEGIN
    LEDR(0 TO 17) <= SW(0 TO 17);
    my_mux : ENTITY work.multiplexer(LogicFunction)
        PORT MAP(
            IN1 => SW(0 TO 2),
            IN2 => SW(3 TO 5),
            IN3 => SW(6 TO 8),
            IN4 => SW(9 TO 11),
            IN5 => SW(12 TO 14),
            SEL => SW(15 TO 17),
            RESULT => LEDG(0 TO 2));
END LogicFunction;
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;

ENTITY multiplexer IS
    PORT (
        IN1 : IN std_logic_vector(0 TO 2);
        IN2 : IN std_logic_vector(0 TO 2);
        IN3 : IN std_logic_vector(0 TO 2);
        IN4 : IN std_logic_vector(0 TO 2);
        IN5 : IN std_logic_vector(0 TO 2);
        SEL : IN std_logic_vector(0 TO 2);
        RESULT : OUT std_logic_vector(0 TO 2)
    );
END multiplexer;

ARCHITECTURE LogicFunction OF multiplexer IS
BEGIN
    WITH SEL SELECT
        RESULT <= IN1 WHEN "000",
        IN2 WHEN "100",
        IN3 WHEN "010",
        IN4 WHEN "110",
        IN5 WHEN "001",
        IN1 WHEN OTHERS;
END LogicFunction;

7 Segment

Wieder mal eine geringfügige Erweiterung der vorherigen Aufgabe: Dieses mal mit literals als Ausgabe, die Pin-Belegung konnte im Manual gefunden werden. Außerdem sind hier erstmals hex-literals verwendet, wo vorher binary Zahlen waren. Das Problem mit der Reihenfolge wurde durch konsequentes Verwenden von DOWNTO bei Binärvektoren, die Zahlen darstellen sollen, umgangen.

Volladdierer

Der Volladdierer konnte ohne Probleme implementiert werden:

ENTITY fulladder IS
    PORT (
        a : IN std_logic;
        b : IN std_logic;
        ci : IN std_logic;
        s : OUT std_logic;
        co : OUT std_logic
    );
END fulladder;

ARCHITECTURE LogicFunction OF fulladder IS
    SIGNAL ab : std_logic;
BEGIN
    ab <= a XOR b;
    s <= ab XOR ci;
    co <= (ab AND ci) OR (a AND b);
END LogicFunction;

Und dann zum Testen mit LEDs verbunden:

adder : work.fulladder
PORT MAP(
    a => SW(0),
    b => SW(1),
    ci => SW(2),
    s => LEDG(0),
    co => LEDG(1));

Carry-Ripple-Addierer

Hier war ein 3-bit Carry-Ripple-Addierer gefragt, da ich aber in der Einführung schon mal Generics verwendet habe, habe ich mich dazu entschieden den Addierer hinsichtlich der Wortbreite zu parametrisieren. Das Instantiieren der einzelnen Addierer hat mit einem FOR ... GENERATE statement funktioniert, auf der Suche nach Dokumentation bin ich dann auf einen Artikel auf allaboutcircuits.com gestoßen, der auch genau dieses Beispiel zur Veranschaulichung nutzt. Neben dem FOR ... GENERATE wurde noch ein Vektor c_inputs für die carry Signale zwischen den Addierern eingefügt.

gen : FOR i IN 0 TO BIT_WIDTH - 1 GENERATE
        adder : work.fulladder PORT MAP(a => a(i), b => b(i), ci => c_inputs(i), s => s(i), co => c_inputs(i + 1));
    END GENERATE;
    c_inputs(0) <= ci;
    co <= c_inputs(BIT_WIDTH);

D-Latch

In diesem Teil der Übung soll ein gated D-Latch implementiert werden. Hier wird vor dem Testen auf dem Board wieder auf die Simulation zurückgegriffen, die hier zwar keine Bugs im code findet, aber das VHDL Attribut KEEP einführt. Es scheint etwas undurchsichtig an welchen Stellen die proprietäre Intel Software im Synthetisierungsprozess optimiert, es wirkt aber so, als ob interne Signale nicht bestehen bleiben müssen, was an den Kompilierprozess von z.B C++ Code erinnert, wo zum Debuggen oftmals die entsprechenden Optimierungen deaktiviert werden (gcc -Og).

Master-slave D flip-flop

In diesem Teil konnte auf das bereits im vorherigen Teil konstruierte D-Latch zurückgegriffen werden. Um Redundanz zu vermeiden habe ich das File ../part2/d_latch.vhd zum neuen Projekt hinzugefügt, was auch funktioniert hat.

Latches + flip-flop: Beschreibung als Process

In diesem Teil wird wieder das D-Latch verwendet. Hier ist allerdings eine Implementierung angegeben, also wird nicht auf die vorherigen Aufgaben zurückgegriffen. Der Flipflop soll im gleichen Stil implementiert werden. Neu ist hier die Funktion rising_edge ( signal s : std_ulogic ) return boolean, die an der Stelle verwendet wird, wo beim Latch Clk = '1' geprüft wird:

ENTITY flipflop IS
    PORT (
        D, Clk : IN STD_LOGIC;
        Q : OUT STD_LOGIC);
END flipflop;
ARCHITECTURE Behavior OF flipflop IS
BEGIN
    PROCESS (D, Clk)
    BEGIN
        IF RISING_EDGE(Clk) THEN
            Q <= D;
        END IF;
    END PROCESS;
END Behavior;

Der Technology Map Viewer zeigt für das Latch einen Block LOGIC_CELL_COMB und für die Flipflops tatsächlich nur einen Block der dem Schaltbild eines Flipflops entspricht. Dies deutet darauf hin, dass tatsächlich die im FPGA enthaltenen Flipflops verwendet wurden.

Counter

Die erste Herausforderung in dieser Aufgabe war es, die 50MHz Clock zu einer 1Hz Clock umzuwandeln, mit der dann gezählt wird. Dafür wurde in jedem 50MHz-cycle eine Zählvariable erhöht, und wenn der Wert 25000000 erreicht, also alle 0.5s, der Clock-output invertiert:

-- Make a 1 Hz, 50% duty cycle clock
PROCESS (CLOCK_50, clk_1)
    VARIABLE count : INTEGER RANGE 0 TO 25000000;
BEGIN
    IF RISING_EDGE(CLOCK_50) THEN
        IF count = 25000000 THEN
            clk_1 <= NOT clk_1;
            count := 0;
        ELSE
            count := count + 1;
        END IF;
    END IF;
END PROCESS;

Mit dieser 1Hz Clock konnte dann das eigentlich Zählen ganz änhlich implementiert werden:

-- Increment the number each clock cycle
PROCESS (clk_1)
    VARIABLE number : INTEGER RANGE 0 TO 9;
BEGIN
    IF RISING_EDGE(clk_1) THEN
        IF number = 9 THEN
            number := 0;
        ELSE
            number := number + 1;
        END IF;
    END IF;
    number_signal <= conv_std_logic_vector(number, 4);
END PROCESS;

Hier war noch eine Konvertierung von integer zu std_logic_vector nötig, da das Modul für die 7-Segment Codierung ein Signal dieses Datentyps erwartet, und eine integer Variable für den Zähler gewählt wurde.

Counter bis 999

Der Counter an sich ist gleich wie im vorherigen Programm, für die höhere Frequenz wurde einfach das maximum des Counters entsprechend angepasst, sodass der Counter jetzt eine Clock mit 10Hz statt 1Hz erzeugt. Für die Anzeige als Dezimalzahl ist eine Umwandlung in BCD hilfreich, hier wurde der “Double dabble” Algorithus verwendet. Viele Implementierungen existieren, hier wurde die auf Wikipedia gegebene verwendet, die zwar nicht optimal aber gut verständlich und kommentiert ist. Die Verwendung von BCD erlaubt es dann, für die einzelnen Zehnerstellen den 7-Segment Encoder der vorherigen Aufgaben vier mal zu verwenden:

Digit0 : ENTITY work.htb(LogicFunction) PORT MAP (ones, HEX0);
Digit1 : ENTITY work.htb(LogicFunction) PORT MAP (tens, HEX1);
Digit2 : ENTITY work.htb(LogicFunction) PORT MAP (hundreds, HEX2);
Digit3 : ENTITY work.htb(LogicFunction) PORT MAP (thousands, HEX3);

bcd : ENTITY work.bin2bcd_12bit(Behavioral)
    PORT MAP(number_signal, ones, tens, hundreds, thousands);

Hardware CRC

In dieser Aufgabe wurde die CRC in VHDL implementiert. Die größte Herausforderung hier war das Verstehen der CRC Berechnung, nicht die anschließende Umsetzung. Zum Testen wurde der Eingang auf die 7-Segment Anzeigen und Schalter gelegt, der Ausgang auf die linken beiden 7-Segment Anzeigen.

CRC für Anbindung an NIOS

Hier wurde die CRC Logik aus der letzten Aufgabe in ein Interface gebracht, welches später an den Prozessor angeschlossen werden kann. Die Eingangs-Signale der vorherigen Aufgabe wurden in Variablen umgewandelt, welche abhängig vom Adress-Eingang gesetzt werden. Der Ausgang ist auch direkt vom Adress-Eingang abhängig. Zum Testen wurden die Schalter für die oberen und unteren Bits des Datenbus verwendet, um sowohl das Polynom als auch das enable-bit setzten zu können. Für die Ausgabe der Prüfsumme wurden wiederum zwei 7-Segment Displays eingesetzt.

NIOS Lights + SDRAM

Für die einführenden Aufgaben zu NIOS wurde die Anleitung von Altera abgearbeitet, Schwierigkeiten bereitete hier nur die Installation der entsprechenden Software um das C-Programm auf den Prozessor zu laden. Der Teil mit Assembler-Programmierung wurde übersprungen, da sehr gute C-Compiler existieren, und jegliche weitere Benutzung der Intel-Software Kopfschmerzen verursacht.

Software CRC

Das Implementieren der CRC in C lief ähnlich wie die Implementierung in VHDL, mit dem Unterschied dass sich die C-Implementierung deutlich einfacher lokal testen lies. Nachdem der Abgleich mit diversen Online-CRC-Tools widersprüchliche Ergebnisse lieferte, wurde die Programmausgabe Schritt für Schritt mit dem manuell gerechneten Beispiel auf Wikipedia verglichen, und mittels CRC-Berechnung, Prüfung, und Prüfung nach Flippen eines Bits das erforderliche Vertrauen in die Korrektheit der Implementierung gewonnen.

Hardware CRC Integration

Da das Integrieren der Hardware CRC nicht auf Anhieb wie in der Anleitung funktioniert hat (Resultierende Checksumme war immer 0x00), wurde zum Debuggen eine VHDL Komponente mit ähnlicher Schnittstelle konstruiert:

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.numeric_std.ALL;

ENTITY ding IS
    PORT (
        DataBusIn : IN STD_LOGIC_VECTOR(31 DOWNTO 0);
        DataBusOut : OUT STD_LOGIC_VECTOR(31 DOWNTO 0);
        Clock : IN STD_LOGIC;
        Reset : IN STD_LOGIC;
        Address : IN STD_LOGIC;
        Write : IN STD_LOGIC
    );
END ding;

ARCHITECTURE Behavior OF ding IS
BEGIN
    PROCESS (Clock, Reset)
        VARIABLE data : STD_LOGIC_VECTOR(31 DOWNTO 0);
        VARIABLE increment : STD_LOGIC_VECTOR(7 DOWNTO 0);
        VARIABLE enable : STD_LOGIC;
    BEGIN
        IF (Reset = '1') THEN
            -- Reset
            data := (OTHERS => '0');
            DataBusOut <= data;
            increment := (OTHERS => '0');
            enable := '0';
        ELSIF RISING_EDGE(Clock) THEN
            IF Write = '1' THEN
                -- Input
                IF Address = '1' THEN
                    -- control
                    increment := DataBusIn(31 DOWNTO 24);
                    enable := DataBusIn(0);
                ELSE
                    -- data
                    data := DataBusIn;
                END IF;
            ELSIF enable = '1' THEN
                -- Calculation
                data := std_logic_vector(unsigned(data) + unsigned(increment));
                enable := '0';
            END IF;

            -- Set correct output depending on address
            IF Address = '0' THEN
                DataBusOut <= data;
            ELSE
                DataBusOut <= (OTHERS => '0');
                DataBusOut(31 DOWNTO 24) <= increment;
                DataBusOut(0) <= enable;
            END IF;
        END IF;
    END PROCESS;
END Behavior;

Die Komponente hat wie die CRC zwei Register, wobei im ersten die Daten, auf denen gearbeitet wird stehen, und im zweiten eine Konfiguration und ein enable Signal. Statt der CRC-Berechnung wird hier eine einfache Addition durchgeführt, wobei der eine Summand die Daten und der andere Summand die 8-Bit Konfiguration (Polynom beim CRC) ist.

Die Komponente wurde dann in ein QSys Modul integriert, und zur Konfiguration hinzugefügt. Für das Lesen und Schreiben der Register werden die Makros IOWR_32DIRECT und IORD_32DIRECT aus dem io.h Header verwendet:

#include <stdio.h>
#include <io.h>
#include <stdint.h>

#define DING_BASE 0x10004000
int main()
{
    // Write initial value to data register
    volatile uint32_t initialValue = 0xFF0;
    IOWR_32DIRECT(DING_BASE, 0, initialValue);
    // Read it back
    printf("Written: 0x%x\n", IORD_32DIRECT(DING_BASE, 0));

    uint8_t increment = 3;
    // Write increment to config register
    IOWR_32DIRECT(DING_BASE, 4, increment << 24);
    // Read it back
    printf("Written to config: 0x%x\n", IORD_32DIRECT(DING_BASE, 4));

    for (int i = 0; i < 10; i++)
    {
        // Command is increment and enable bit
        volatile uint32_t command = (increment << 24) | 1;
        IOWR_32DIRECT(DING_BASE, 4, command);
        printf("Written to config: 0x%x\n", command);

        // Wait for computation to complete (enable bit reset)
        volatile uint32_t res = command;
        while (res & 1)
        {
            res = IORD_32DIRECT(DING_BASE, 4);
        }

        // Read result
        res = IORD_32DIRECT(DING_BASE, 0);
        printf("Result: 0x%x\n", res);
    }
}

Wie zu erwarten, inkrementiert die Hardware-Komponente den internen Wert bei jedem Aktivieren des enable bits um den eingestellten Wert.

In diese Komponente wurde dann erneut die CRC Berechnung eingefügt, was diesmal dann auch funktioniert hat, und gleiche Resultate wie die Software Implementierung lieferte.

Entwurfsraum

Im ersten Schritt soll die Laufzeit der Software- und Hardware CRC bestimmt werden. Aus Frustration mit Intel-Tools wird dies nicht mir sicher irgendwo vorhandenen Profiling Tools gemacht, sondern mittels an- und ausschalten einer LED und einer Stoppuhr.

Die Berechnung von \(10^6\) CRCs mittels Hardware-CRC dauert ca. 9.8s, was bei einer Taktfrequenz von 50MHz ca. 490 Taktzyklen pro CRC entspricht. Bei der reinen Software Implementierung dauert eine CRC Berechnung ganze 7750 Taktzyklen.

Das Hinzufügen des CRC Moduls in QSys führt zu einem Anstieg der genutzten Logikelemente von 2171 auf 2410, statt 1328 Registern werden 1436 gebraucht.

In diesem konkreten Fall ist die Implementierung der CRC in Hardware sicherlich sinnvoll, da die Anzahl der benötigten Logikelemente besonders im Vergleich zu den bereits benötigten Elementen für den Prozessor gering ist. Die Laufzeit der Hardware CRC ist bedeutend kleiner, was allerdings auch der Tatsache geschuldet ist, dass die Software CRC keine optimale Implementierung darstellt. Vor einer Entscheidung müsste hier noch eine optimierte Softwareimplementierung getestet werden. Die Software-Variante hat darüber hinaus den enormen Vorteil, dass sie jederzeit ausgetauscht werden kann, auch wenn die Konfiguration des Chips nicht mehr geändert werden kann. Gibt es an die CRC Berechnung allerdings harte Echtzeitanforderungen, muss dies zusätzlich sichergestellt werden, die VHDL Implementierung läuft unabhängig von anderen Tasks auf dem Prozessor immer in gleicher Zeit.

Fazit

Die Vorlesung hat einen umfassenden Überblick über die Thematik “Eingebettete Systeme” gegeben. In einigen Bereichen, etwa den Analog-Digital Wandlern, wurden auch die speziellen Problemstellungen und mehrere Lösungen präsentiert. In der Übung wurde die Hardwarebeschreibungssprache VHDL praxisnah angewendet. Vor dieser Vorlesung dachte ich bei Eingebetteten Systemen nur an Microcontroller, maximal noch an Echtzeitbetriebssysteme. Die Möglichkeiten der engen Integration von Hard- und Software, die Vorteile, spezielle Funktionen in Hardware zu Implementieren, und die Knackpunkte bei Systemen mit Echtzeitanforderungen waren mir neu.

Die Übung mit VHDL war eine aufschlussreiche Erfahrung, aber besonders nach Bearbeitung meines Zusatzthemas und der letzten Übungsaufgaben sehe ich mich in näherer Zukunft keinen VHDL Code selbst schreiben. Das Zusatzthema hat mir in dieser Hinsicht die Gelegenheit gegeben, herauszufinden, dass die Konzepte der Hardwarebeschreibung nicht zwingend an das Lernen einer neuen, bisher unbekannten Beschreibungssprache gebunden sind. Die Übung hat am Ende etwas mehr Zeit als erwartet in Anspruch genommen, was aber sicher großteils auf die mangelnde direkte Kollaboration im Labor zurückzuführen ist.

Auf die Frage, “welche weiteren Ideen mir für die eventuelle Fortführung meiner Forschungen im Bereich der Rechnerarchitektur eingefallen sind”1, kann ich keine konkrete Antwort geben. Was ich mir aber durchaus gewünscht habe, besonders während der Übung, ist dass der Prozess der Hardwareentwicklung von der modernen Softwareentwicklung lernt. Dazu gehören nicht nur von Anfang bis Ende durchschaubare Open Source Toolchains, sondern auch einfach wiederverwendbare Blöcke, oder “Bibliotheken”, die auch öffentlich geteilt werden. Starke Veränderung in diese Richtung ist momentan im Gange, siehe die oben erwähnte Yosys Toolchain für Lattice FPGAs, und andere im Zusatzthema angesprochene Projekte.

References

[1] R. Singh, „FPGA Vs ASIC: Differences Between Them And Which One To Use“. 2018, Zugegriffen: Sep. 27, 2020. [Online]. Verfügbar unter: https://numato.com/blog/differences-between-fpga-and-asics/.

[2] M. Yanik, „Avionics full duplex switched ethernet (AFDX) data bus“. Okt. 2007.

[3] B. Baker, „How delta-sigma ADCs work, Part 1“, Analog Applications Journal, Issue Q3 2011, 2011.

[4] Analog Devices, Inc., „Sigma-Delta ADC Tutorial“. Zugegriffen: Sep. 29, 2020. [Online]. Verfügbar unter: https://www.analog.com/en/design-center/interactive-design-tools/sigma-delta-adc-tutorial.html.

[5] The Linux Kernel contributors, „The Linux Kernel documentation“. Zugegriffen: Sep. 29, 2020. [Online]. Verfügbar unter: https://www.kernel.org/doc/html/latest/.

[6] ioctl(2) Linux Programmer’s Manual, 5.08 Aufl. 2020.

[7] „FreeRTOS™ Real-time operating system for microcontrollers“. Zugegriffen: Sep. 27, 2020. [Online]. Verfügbar unter: https://www.freertos.org/index.html.

[8] „IEEE Standard VHDL Language Reference Manual“, IEEE Std 1076-1987, S. 1–218, 1988, doi: 10.1109/IEEESTD.1988.122645.

[9] „IEEE Standard Hardware Description Language Based on the Verilog(R) Hardware Description Language“, IEEE Std 1364-1995, S. 1–688, Okt. 1996, doi: 10.1109/IEEESTD.1996.81542.

[10] J.-C. L. Lann, „RubyRTL : Ruby-on-gates !“ 2020, Zugegriffen: Sep. 25, 2020. [Online]. Verfügbar unter: https://github.com/JC-LL/ruby_rtl.

[11] J.-C. L. Lann, H. Badier, und F. Kermarrec, „Towards a Hardware DSL Ecosystem : RubyRTL and Friends“. 2020, [Online]. Verfügbar unter: http://arxiv.org/abs/2004.09858.

[12] K. Jaic und M. C. Smith, „Enhancing Hardware Design Flows with MyHDL“, in Proceedings of the 2015 ACM/SIGDA International Symposium on Field-Programmable Gate Arrays, 2015, S. 28–31, doi: 10.1145/2684746.2689092.

[13] J. Bachrach u. a., „Chisel: Constructing hardware in a Scala embedded language“, in DAC Design Automation Conference 2012, 2012, S. 1212–1221, doi: 10.1145/2228360.2228584.

[14] J. Clow, G. Tzimpragos, D. Dangwal, S. Guo, J. McMahan, und T. Sherwood, „A pythonic approach for rapid hardware prototyping and instrumentation“, in 2017 27th International Conference on Field Programmable Logic and Applications (FPL), 2017, S. 1–7, doi: 10.23919/FPL.2017.8056860.

[15] C. Baaij, „CLasH: From Haskell To Hardware“, Magisterarbeit, University of Twente, 2009.

[16] E. Logaras und E. S. Manolakos, „SysPy: using Python for processor-centric SoC design“, in 2010 17th IEEE International Conference on Electronics, Circuits and Systems, Dez. 2010, S. 762–765, doi: 10.1109/ICECS.2010.5724624.

[17] „SpinalHDL, A high level hardware description language“. Zugegriffen: Sep. 25, 2020. [Online]. Verfügbar unter: https://github.com/SpinalHDL.

[18] „nMigen: A refreshed Python toolbox for building complex digital hardware“. 2018, Zugegriffen: Sep. 25, 2020. [Online]. Verfügbar unter: https://nmigen.org.

[19] LambdaConcept S.A.S., „nMigen Step by Step Tutorial“. 2018, Zugegriffen: Sep. 25, 2020. [Online]. Verfügbar unter: http://blog.lambdaconcept.com/doku.php?id=nmigen:tutorial.

[20] D. Shah, E. Hung, C. Wolf, S. Bazanski, D. Gisselquist, und M. Milanovic, „Yosys+nextpnr: an Open Source Framework from Verilog to Bitstream for Commercial FPGAs“, CoRR, Bd. abs/1903.10407, 2019, [Online]. Verfügbar unter: http://arxiv.org/abs/1903.10407.

[21] F. Kermarrec, S. Bourdeauducq, J.-C. L. Lann, und H. Badier, „LiteX: an open-source SoC builder and library based on Migen Python DSL“. 2020, [Online]. Verfügbar unter: http://arxiv.org/abs/2005.02506.


  1. Anleitung zum Kursportfolio↩︎