NVMe

Überblick NVMe

NVMe ist eine Schnittstelle um SSDs direkt über PCIe zu verbinden. Durch diese Schnittstelle können extrem hohe Datenübertragungsraten erreicht werden. Die SSD besitzt einen NVMe-Controller, der die Schnittstelle zwischen dem tatsächlichen Flash-Speicher der SSD und dem PCIe-Bus darstellt. Die Basis Konfiguration des NVMe-Controllers erfolgt über das Setzen von Konfigurationsregistern. Weitere Konfiguration ist über Admin-Befehle möglich. Datenübertragungen werden ebenfalls über das Absetzen von Befehlen eingeleitet. Dies sind jedoch nicht Admin-, sondern IO-Befehle.

Befehle werden abgesetzt, indem diese mit ihren Parametern in einer Queue abgelegt werden. Der NVMe-Controller schreibt das Resultat der Befehle in eine eigene Queue. Admin- und IO-Befehle verwenden getrennte Queues, wobei es möglich ist, dass es mehrere IO-Queues gibt. Alle Queues liegen in Speichern, die vom PCIe-Bus erreichbar sein müssen. Pro Queue muss immer die Start-Adresse und Länge im NVMe-Controller konfiguriert werden. Für die Admin Queue erfolgt dies über das Setzen von Registern bei der Initialisierung. IO-Queues werden über Admin-Befehle erstellt und konfiguriert.

Daten, die auf die SSD geschrieben werden, müssen in einem, über PCIe addressierbaren Speicher zur Verfügung gestellt werden. Der NVMe-Controller liest diesen aus und überträgt ihn auf den Flash-Speicher. Beim Lesen von Daten muss ebenfalls ein Speicher bereitgestellt werden, auf den die Daten geschrieben werden können.

Umsetzung

Genereller Ablauf

  • Recherche der NVMe Spezifikation
  • Aufsetzen des Beispielprojektes von FPGADrive
    • Übersetzen des Beispiel Vivado-Projektes (+ CI)
    • Erstellen eines Linux-Systems mit PetaLinux
  • Entwickeln einer minimalen Beispielanwendung unter Linux
  • Übertragen der Beispielanwendung in ein FPGA-Design

Implementierung in Linux

Vor der Implementierung in Hardware wurde eine Implementierung in Linux durchgeführt. Dieser Schritt hat den Vorteil, dass alle notwendigen Schritte schneller umgesetzt werden können, da die Zeit zum Kompilieren bei Änderungen deutlich geringer ist, als die benötigte Synthese-Zeit.

Zuerst wurde mit dem Werkzeug nvme-cli die Ausführung von NVMe-Befehlen getestet. Dadurch konnten erste manuelle Lese- und Schreib-Operationen durchgeführt werden. Der NVMe-Treiber wurde im nächsten Schritt um weitere Ausgaben, welche ausführliche Informationen über die ausgeführten NVMe-Befehle und deren Parameter lieferten.

Anschließend wurde mit der Implementierung der Beispielanwendung gestartet. Dieses konnte schrittweise implementiert werden, da der NVMe Treiber von Linux weiter genutzt werden konnte. Dadurch konnten Teilfunktionalitäten getestet werden, bevor die finale Anwendung fertiggestellt wurde.

Herausforderungen

  • Initiales Setup des Linux-Systems
  • Recherche der benötigten Konfigurationen und Befehle
  • PCIe-Zugriffe vom User-Space
  • Reservieren eines Speicherblocks mit sowohl physischer, als auch virtueller Adresse.

Implementierung in Hardware

Nach der Fertigstellung der Implementierung für Linux wurde diese auf VHDL übertragen. Der Steuerungspfad besteht aus 3 Kernmodulen:

  • WriteBuffer: Dieser liest Daten von einem Datenstrom und schreibt diese Daten in einen Speicher. Sobald ein Block im Speicher voll ist, wird der CoreController darüber informiert. Der CoreController gibt nicht mehr benötigt Blöcke wieder zum Schreiben frei. Ist der nächste Block belegt, werden keine neuen Daten mehr angefordert, bis der Block wieder verfügbar sein.
  • ReadBuffer: Dieser ist das Gegenstück zum WriteBuffer. Der CoreController informiert den ReadBuffer über beschriebene Blöcke und der ReadBuffer gibt diese als Datenstrom aus. Sobald ein Block ausgegeben wurde, wird der CoreController darüber informiert, dass der Block wieder frei ist.
  • CoreController: Der CoreController beinhaltet den endlichen Automaten zur Initialisierung der SSD und zur Steuerung der Datenflüsse. Zusätzlich führt der CoreController die Initialisierung der SSD durch und verwaltet die Queues für NVMe Befehle.

Die von Read- und Writebuffer sowie für die NVMe Queues verwendeten Speicher sind am PCIe-Bus verfügbar und können daher auch von der SSD angesprochen werden.

Kontroll- und Statusschnittstelle

Damit Informationen dieser Komponenten zur Laufzeit ausgelesen bzw. verändert werden können, wurde eine Register-Schnittstelle angelegt, die über eine AXI-Schnittstellen angesprochen werden kann.

In der aktuellen Version beinhalten diese Register vor allem folgende Informationen:

  • Ein globales Reset-Bit, das alle Register auf deren Initialzustände zurücksetzt und alle Komponenten entsprechend im Reset hält.
  • Ein Status-Bit, um ungültige Register-Zugriffe zu signalisieren.
  • Ein Reset-Bit für jede implementierte FSM, das verwendet werden kann, um gezielt FSMs zu deaktivieren, was z.B. beim Debugging nützlich sein kann.
  • Eine 4-Bit Repräsentation der Zustände jeder FSM (in welchem Zustand befindet sich jede FSM?).
  • FSMs die auf einen AXI-Bus zugreifen, speichern die letzte fehlerhafte AXI-Antwort.
  • Interne Informationen über die Zustände von IO-/Admin-Queues.
  • Basisadressen externer Komponenten.

Das Auslesen aller Register wurde über eine Reihe selbstgeschriebener Python-Skripts verwirklicht, die den devmem-Befehl für die tatsächlichen Speicherzugriffe verwenden. Die Ausgabe eines solchen Skripts könnte z.B. wie folgt aussehen:

Simulation mittels VUnit

Damit Fehler im entwickelten Design frühzeitig erkannt werden können, wurden für die einzelnen Komponenten Simulationen angelegt. Das Open-Source Framework VUnit wurde dabei verwendet, um die AXI-Zugriffe über bereits getestete Verifikations-Komponenten absetzen zu können. Zusätzlich ist es mit VUnit möglich die Simulationen Simulator-unabhängig skripten zu können und die Ergebnisse ansprechend aufzubereiten.

Resultat

Entstanden ist ein Proof-of-Concept, das zeigt, wie die Kommunikation mit einem NVMe-Controller abgehandelt werden kann, bestehend aus:

  • Einem konfigurierbaren und reproduzierbaren Linux-System (mittels PetaLinux).
  • Einem voll-geskripteten Workflow (Projekt anlegen, Synthese, generieren des Linux-Systems).
  • Konfiguration des NVMe-Controllers per FPGA
    • Schreiben von NVMe-Registern über PCIe
    • Anlegen von IO-Queues über Admin-Kommandos
  • Kommunikation mit dem Linux-System über Kontroll- und Statusregister (siehe Kontroll- und Statusschnittstelle).

Lessons Learned

  • Ein geskripteter Workflow braucht Zeit, hilft dann aber immens.
  • FPGA-Entwicklung benötigt/profitiert von starken Rechnern.
  • Ausgiebige Simulationen sind aufwendig, helfen aber Fehler früh zu finden (und benötigen keine Hardware).
  • Das Übertragen von C++-Anwendungen nach VHDL ist nicht trivial.
  • Einmal eingerichtet sind die Debug-Möglichkeiten von Vivado (ILA, System-ILA, etc.) enorm hilfreich, um Fehler in einem Design zu finden.
    • Zusammen mit einer ausgiebigen Register-Schnittstelle kann man diese ggf. genau ausmachen.