In Fortsetzung des
ersten Artikels möchte ich als Beispiel ein Beispiel für die Arbeit mit FPGA (FPGA) in Python zeigen. Dieser Artikel behandelt den Testaspekt ausführlicher. Wenn das
MyHDL- Framework es Benutzern, die mit der bekannten Syntax und dem bekannten Ökosystem an Python arbeiten, ermöglicht, einen Blick in die Welt des FPGA zu werfen, verstehen erfahrene FPGA-Entwickler die Bedeutung der Verwendung von Python nicht. Die Hardwarebeschreibungsparadigmen für MyHDL und Verilog sind ähnlich, und die Auswahl einer bestimmten Sprache ist eine Frage der Gewohnheit und des Geschmacks. Verilog / VHDL steht für die Tatsache, dass Firmware seit langer Zeit in diesen Sprachen geschrieben wurde und tatsächlich Standard für die Beschreibung digitaler Geräte ist. Python kann als Anfänger auf diesem Gebiet beim Schreiben von Testumgebungen mithalten. Ein wesentlicher Teil der Zeit des FPGA-Entwicklers wird für das Testen seiner Designs aufgewendet. Als nächstes möchte ich anhand eines Beispiels demonstrieren, wie dies in Python mit MyHDL gemacht wird.
Angenommen, es gibt eine Aufgabe, ein Gerät zu beschreiben, das mit Speicher auf dem FPGA arbeitet. Der Einfachheit halber verwende ich den Speicher, der mit anderen Geräten kommuniziert, über eine parallele Schnittstelle (und nicht über eine serielle Schnittstelle, z. B. I2C). Solche Mikroschaltungen sind angesichts der Tatsache, dass viele Stifte erforderlich sind, um mit ihnen zu arbeiten, nicht immer praktisch, andererseits wird ein schnellerer und einfacherer Informationsaustausch bereitgestellt. Zum Beispiel inländische 1645RU1U und ihre Analoga.

Modulbeschreibung
Der Datensatz sieht folgendermaßen aus: FPGA gibt eine 16-Bit-Zellenadresse, 8-Bit-Daten, erzeugt ein Schreibsignal WE (Schreibfreigabe). Da OE (Ausgangsfreigabe) und CE (Chipfreigabe) immer aktiviert sind, erfolgt das Lesen, wenn die Zellenadresse geändert wird. Das Schreiben und Lesen kann sowohl nacheinander in mehreren Zellen hintereinander erfolgen, beginnend mit einer bestimmten adr_start-Adresse, die an der Vorderflanke des adr_write-Signals aufgezeichnet ist, als auch in einer Zelle an einer beliebigen Adresse (Direktzugriff).
In MyHDL sieht der Code folgendermaßen aus (Schreib- und Lesesignale werden in umgekehrter Logik ausgeführt):
from myhdl import * @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we):
Bei Konvertierung in Verilog mit der Funktion:
def convert(hdl): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(0)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] inst = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) inst.convert(hdl=hdl) convert(hdl='Verilog')
Dies führt zu
`timescale 1ns/10ps module ram_driver ( data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we ); input [7:0] data_in; output [7:0] data_out; reg [7:0] data_out; output [15:0] adr; reg [15:0] adr; input [15:0] adr_start; input adr_write; inout [7:0] data_memory; wire [7:0] data_memory; input read; input write; output we; reg we; reg [7:0] mem_z; assign data_memory = mem_z; always @(write) begin: RAM_DRIVER_WRITE_DATA if ((!write)) begin mem_z <= data_in; we <= 0; end else begin mem_z <= 'bz; data_out <= data_memory; we <= 1; end end always @(posedge adr_write, posedge write, negedge read) begin: RAM_DRIVER_WRITE_START_ADR if (adr_write) begin adr <= adr_start; end else begin adr <= (adr + 1); end end endmodule
Es ist nicht erforderlich, ein Projekt zur Simulation in Verilog zu konvertieren. Dieser Schritt ist zum Flashen des FPGA erforderlich.
Modellierung
Nach der Beschreibung der Logik sollte das Projekt überprüft werden. Sie können sich beispielsweise darauf beschränken, Eingabeeinflüsse zu simulieren und die Reaktion des Moduls im Zeitdiagramm zu sehen. Mit dieser Option ist es jedoch schwieriger, die Interaktion Ihres Moduls mit einem Speicherchip vorherzusagen. Um den Betrieb des erstellten Geräts vollständig zu überprüfen, müssen Sie daher ein Speichermodell erstellen und die Interaktion zwischen diesen beiden Geräten testen.
Da die Arbeit in Python stattfindet, bietet sich für das Speichermodell der vom Typ angegebene Wörterbuch (Dictionary) an. Die Daten, in denen {key: value} und in diesem Fall {address: data} gespeichert sind.
memory = { 0: 123, 1: 456, 2: 789 } memory[0] >> 123 memory[1] >> 456
Für den gleichen Zweck ist der Listendatentyp geeignet, bei dem jedes Element seine eigenen Koordinaten hat, die die Position des Elements in der Liste angeben:
memory = [123, 456, 789] memory[0] >> 123 memory[1] >> 456
Die Verwendung von Wörterbüchern zur Simulation des Gedächtnisses erscheint im Hinblick auf eine bessere Sichtbarkeit vorzuziehen.
Die Beschreibung der Test-Shell (in der Datei test_seq_access.py) beginnt mit der Ansage der Signale, der Initialisierung der Anfangszustände und deren Einwurf in die oben beschriebene Speichertreiberfunktion:
@block def testbench(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we)
Im Folgenden wird das Speichermodell beschrieben. Anfangszustände werden initialisiert, standardmäßig ist der Speicher mit Nullwerten gefüllt. Begrenzen Sie das Speichermodell auf 128 Zellen:
memory = {i: intbv(0) for i in range(128)}
und beschreiben Sie das Verhalten des Speichers: Wenn WE sich im niedrigen Zustand befindet, schreiben Sie den Wert in die Zeile in die entsprechende Speicheradresse, andernfalls gibt das Modell den Wert an der angegebenen Adresse an:
mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None
Anschließend können Sie in derselben Funktion das Verhalten der Eingangssignale beschreiben (für den Fall des sequentiellen Schreibens / Lesens): Die Startadresse wird aufgezeichnet → 8 Informationszellen werden aufgezeichnet → Die Startadresse wird aufgezeichnet → 8 aufgezeichnete Informationszellen werden gelesen.
@instance def stimul(): init_adr = random.randint(0, 50) # yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr # yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # 8 write.next = 0 data_in.next = random.randint(0, 100) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr # adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # read.next = 0 yield delay(100) read.next = 1 yield delay(100) raise StopSimulation return stimul, ram, access
Simulation ausführen:
tb = testbench() tb.config_sim(trace=True) tb.run_sim()
Nach dem Starten des Programms wird die Datei testbench_seq_access.vcd im Arbeitsordner generiert. Öffnen Sie sie in gtkwave:
gtkwave testbench_seq_access.vcd
Und wir sehen das Bild:

Die aufgezeichneten Informationen wurden erfolgreich gelesen.
Sie können den Inhalt des Speichers anzeigen, indem Sie der Testbench den folgenden Code hinzufügen:
for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value))
In der Konsole wird Folgendes angezeigt:

Testen
Danach können Sie mehrere automatisierte Tests mit einer erhöhten Anzahl von beschreibbaren / lesbaren Zellen durchführen. Zu diesem Zweck werden der Testbench mehrere Testzyklen und Dummy-Wörterbücher hinzugefügt, in denen die geschriebenen und lesbaren Informationen und das Assert-Konstrukt hinzugefügt werden. Dies führt zu einem Fehler, wenn zwei Wörterbücher nicht gleich sind:
@instance def stimul(): for time in range(100): temp_mem_write = {} temp_mem_read = {} init_adr = random.randint(0, 50) yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[i] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): read.next = 0 temp_mem_read[i] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, " " for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) raise StopSimulation return stimul, ram, access
Als Nächstes können Sie eine zweite Testbench erstellen, um den Betrieb im Direktzugriffsmodus zu testen: test_random_access.py.
Die Idee des zweiten Tests ist ähnlich: Wir schreiben zufällige Informationen an eine zufällige Adresse und fügen dem Wörterbuch temp_mem_write ein Paar {address: data} hinzu. Dann gehen wir die Adressen in diesem Wörterbuch um, lesen die Informationen aus dem Speicher und geben sie in das Wörterbuch temp_mem_read ein. Und am Ende mit dem Assert-Konstrukt überprüfen wir den Inhalt von zwei Wörterbüchern.
import random from myhdl import * from ram_driver import ram_driver @block def testbench_random_access(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) memory ={i:intbv(0) for i in range(128)} mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None @instance def stimul(): for time in range(10): temp_mem_write = {} temp_mem_read = {} yield delay(100) for i in range(64): write.next = 1 adr_write.next = 1 adr_start.next = random.randint(0, 126) yield delay(100) adr_write.next = 0 yield delay(100) write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[int(adr_start.val)] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) for key in temp_mem_write.keys(): adr_start.next = key adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) read.next = 0 temp_mem_read[key] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, ' random access' raise StopSimulation return stimul, ram, access tb = testbench_random_access() tb.config_sim(trace=True) tb.run_sim()
Python verfügt über mehrere Frameworks zur Automatisierung der Testausführung. Ich werde der Einfachheit halber pytest nehmen, es muss von pip installiert werden:
pip3 install pytest
Wenn der Befehl "pysest" über die Konsole gestartet wird, findet das Framework alle Dateien im Arbeitsordner und führt sie aus, deren Namen "test_ *" enthalten.

Tests erfolgreich abgeschlossen. Ich werde einen Fehler in der Beschreibung des Geräts machen
@block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): mem_z = data_memory.driver() @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: adr.next = adr_start else: adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 1 # , else: mem_z.next = None data_out.next = data_memory we.next = 1
Ich führe Tests durch:

Wie erwartet wurden in beiden Tests die Anfangsinformationen (Nullen) berücksichtigt, dh neue Informationen wurden nicht aufgezeichnet.
Fazit
Durch die Verwendung von Python zusammen mit myHDL können Sie das Testen der entwickelten Firmware für FPGAs automatisieren und nahezu jede Testumgebung mit den umfangreichen Funktionen der Programmiersprache Python erstellen.
Der Artikel berücksichtigt:
- Erstellen eines Moduls, das mit dem Speicher arbeitet;
- Erstellen eines Speichermodells;
- Testfallerstellung;
- Testautomatisierung mit dem Pytest-Framework.