Constrained Random Verification mit OSVVM meistern
Constrained Random Verification ist eine Testbench-Strategie, die auf der Generierung pseudo-zufälliger Transaktionen für das Device Under Test (DUT) basiert. Ziel ist es, funktionale Abdeckung (Functional Coverage) einer Reihe vordefinierter Events durch zufällige Interaktion mit dem DUT zu erreichen.
Open Source VHDL Verification Methodology (OSVVM) ist eine freie VHDL-Bibliothek, die eine Reihe praktischer Pakete für die Erstellung constrained-random Testbenches enthält. Wir interessieren uns besonders für RandomPkg und CoveragePck, die wir in diesem Artikel nutzen. Ich empfehle, die OSVVM GitHub-Seite zu besuchen, um mehr über die Features dieser Bibliothek zu erfahren.
Das Device Under Test
Ich steige direkt in ein Beispiel ein, um besser zu erklären, wie sich ein constrained-random Testbench vom klassischen Testbench mit directed Tests unterscheidet. Wir haben im vorherigen Blogartikel einen Ringpuffer-FIFO erstellt, aber keinen selbstprüfenden Testbench zur Verifikation der Korrektheit des Moduls gebaut.
Wir werden einen ordentlichen Testbench für den Ringpuffer-FIFO erstellen, der constrained random verification nutzt.
entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- Anzahl Elemente in der FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer;
Die Entity des Ringpuffer-Moduls ist im obigen Code gezeigt. Wir behandeln das DUT als Black Box, d. h., wir nehmen keine Kenntnis davon an, wie das DUT implementiert ist. Schließlich geht es in diesem Artikel um den Testbench, nicht um den Ringpuffer-FIFO.
Wir werden das DUT im Testbench per Entity-Instanziierung instanziieren. Die Instanziierung ist trivial, daher lasse ich den Code vorerst weg, aber er kann später im Artikel heruntergeladen werden.
Die DUT-Generics werden auf folgende Werte gemappt:
- RAM_WIDTH: 16
- RAM_DEPTH: 256
Testbench-Strategie
Lassen Sie uns die Teststrategie durchgehen, bevor wir mit der Implementierung beginnen. Das folgende Bild zeigt das Hauptkonzept des Testbenchs, den wir erstellen werden.
Wir führen zufällige Schreibtransaktionen an der Eingangsseite des DUT durch. Die Eingangsdaten werden jeden Taktzyklus auf einen zufälligen Wert gesetzt, und die Strobes am Write-Enable-Eingang haben zufällige Dauer.
Ähnlich führen wir zufällige Lesevorgänge durch. Wir assertieren das Read-Enable-Signal in Bursts, die für eine zufällige Anzahl Taktzyklen andauern.
Parallel zum DUT wird ein Verhaltensmodell (Behavioral Model) betrieben. Dies ist eine FIFO, die anders implementiert ist als der Ringpuffer im DUT, aber dasselbe Interface hat. Im Gegensatz zum DUT muss das Verhaltensmodell nicht synthetisierbar sein. Das gibt uns die Freiheit, fortgeschrittene VHDL-Programmierfeatures für seine Erstellung zu nutzen.
Wir vergleichen die Ausgabe des DUT mit der Ausgabe des Verhaltensmodells in einem separaten Prozess. Dieser Prozess ist ausschließlich dafür verantwortlich, diesen Vergleich jeden Taktzyklus mittels Assert-Statements durchzuführen. Wenn sich die beiden FIFO-Implementierungen zu irgendeinem Zeitpunkt unterschiedlich verhalten, verursacht ein Assertion-Failure die Beendigung der Simulation mit einem Fehler.
Schließlich sammeln wir funktionale Coverage-Daten, indem wir die Transaktionen beobachten, die zum und vom DUT gehen. Ein funktionaler Coverage-Punkt könnte bedeuten: simultanes Lesen und Schreiben, oder dass die FIFO mindestens einmal gefüllt wird. Wir überwachen diese Events in unserem Haupt-Testbench-Sequencer-Prozess. Die Simulation wird gestoppt, sobald alle funktionalen Coverage-Events, die wir überwachen, aufgetreten sind.
OSVVM-Bibliothek importieren
Die OSVVM-Bibliothek kann mit jedem Simulator genutzt werden, der VHDL-2008 unterstützt. Sie ist möglicherweise bereits in den Standardbibliotheken Ihres Simulators enthalten. Sie ist im ModelSim PE Student Edition enthalten, das kostenlos von Mentor Graphics heruntergeladen werden kann.
ModelSim wird mit einer älteren OSVVM-Version ausgeliefert, aber das ist OK – sie hat alles, was wir brauchen. Wir können einfach die Random- und Coverage-Pakete so importieren:
library osvvm; use osvvm.RandomPkg.all; use osvvm.CoveragePkg.all;
Die neueste OSVVM-Version kann immer von der GitHub-Seite heruntergeladen werden. Tun Sie dies, wenn Ihr Simulator sie nicht beinhaltet oder Sie die neuesten Features nutzen wollen.
OSVVM-Variablen deklarieren
Die OSVVM-Bibliothek enthält Pakete mit Protected Types. Variablen dieser Typen wären auf den Prozess beschränkt, in dem sie definiert wurden. Daher deklarieren wir sie als Shared Variables im deklarativen Bereich der Testbench-Architektur, wie im folgenden Code gezeigt.
-- OSVVM Variablen shared variable rv : RandomPType; shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType;
Die rv-Variable vom Typ RandomPType dient der Generierung zufälliger Werte. Wir brauchen nur eine davon, da wir dasselbe Objekt in jedem Prozess nutzen können, in dem wir Zufallswerte brauchen. Die letzte Codezeile deklariert sechs Variablen vom Typ CovPType.
Wir deklarierten sechs Coverage-Variablen, weil wir sechs Coverage-Ziele haben; wir bezeichnen diese Objekte als «Bins». Die Shared Variables müssen initialisiert werden, bevor sie für die Coverage-Datensammlung genutzt werden können. Das tun wir, indem wir die AddBins-Prozedur auf jedem der CovPType-Bins aufrufen.
-- Coverage Bins einrichten bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN);
Wir übergeben eine String-Beschreibung des Coverage-Bins als ersten Parameter an die AddBins-Prozedur. Dieser String taucht am Ende der Simulation wieder auf, wenn wir die Statistiken für jeden Coverage-Bin ausgeben. Wie aus den Textbeschreibungen ersichtlich, nutzen wir die Bins, um zu prüfen, ob sehr spezifische Corner Cases aufgetreten sind.
AddBins ist eine überladene Prozedur, die für die Erstellung mehrerer Scoreboards innerhalb der Bin-Variablen genutzt werden kann. Wir werden jedoch nur ein Scoreboard pro Bin haben. Daher übergeben wir die Convenience-Konstante ONE_BIN als Parameter an AddBins. Dies initialisiert die CovPType-Variablen mit je einem Bin. Die durch die Bins repräsentierten Scoreboards gelten als abgedeckt (covered), wenn die Events, die sie überwachen, mindestens einmal aufgetreten sind.
Zufällige Eingabe generieren
Beginnen wir mit dem Prozess, der Eingabedaten für das DUT generiert. Der Ringpuffer-FIFO ist so designed, dass er versuchte Überschreibungen und Überlesungen ignoriert. Daher können wir einfach zufällige Daten in Bursts zufälliger Dauer schreiben. Wir müssen nicht darüber nachdenken, ob das DUT tatsächlich bereit ist, die Daten aufzunehmen.
PROC_WRITE : process begin wr_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wr_data <= rv.RandSlv(RAM_WIDTH); wait until rising_edge(clk); end loop; end process;
Die einzige Betrachtung, die dieser Prozess anstellt, ist, dass das DUT nicht im Reset ist. Wir aktivieren oder deaktivieren das Write-Enable-Signal zufällig in der ersten Zeile dieses Prozesses, aber es wird nur aktiviert, wenn rst '0' ist.
Die anschließende For-Schleife schreibt für eine zufällige Anzahl Taktzyklen zufällige Daten ins DUT, selbst wenn das Enable-Signal nicht aktiv ist. Das können wir tun, weil das DUT den wr_data-Port ignorieren soll, solange wr_en nicht '1' ist. Nach der For-Schleife springt das Programm zum Prozessanfang zurück und startet eine weitere zufällige Schreibtransaktion.
Zufällige Lesevorgänge durchführen
Der Prozess, der Daten vom DUT liest, ist dem Write-Prozess ähnlich. Wir können das rd_en-Signal zufällig aktivieren, da das DUT Leseversuche bei Leerlauf ignorieren soll. Die Daten, die am rd_data-Port erscheinen, werden nicht tatsächlich geprüft. Dieser Prozess steuert nur das Read-Enable-Signal.
PROC_READ : process begin rd_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wait until rising_edge(clk); end loop; end process;
Verhaltensverifikation
Wir werden ein Verhaltensmodell des DUT innerhalb unseres Testbenchs konstruieren, um dessen Verhalten zu verifizieren. Dies ist eine bekannte Testbench-Strategie. Zuerst füttern wir das Verhaltensmodell gleichzeitig mit denselben Eingaben wie das DUT. Dann können wir die Ausgaben der beiden vergleichen, um zu prüfen, ob das DUT das korrekte Verhalten zeigt.
Das obige Bild zeigt das Grundlayout eines solchen Testbenchs. Das Verhaltensmodell arbeitet parallel zum DUT. Wir nutzen es als Blueprint, um die Ausgaben des DUT zu prüfen.
Der Testbench-FIFO
Wir werden eine verkettete Liste (Linked List) zur Implementierung des Verhaltensmodells nutzen. Verkettete Listen sind nicht synthetisierbar, aber perfekt für Testbenches. Sie erinnern sich vielleicht an den Artikel How to create a Linked List in VHDL, wenn Sie regelmäßiger Leser dieses Blogs sind. Wir werden den Code daraus für das Verhaltensmodell des Ringpuffer-FIFOs nutzen.
package DataStructures is type LinkedList is protected procedure Push(constant Data : in integer); impure function Pop return integer; impure function IsEmpty return boolean; end protected; end package DataStructures;
Die Paketdeklaration für den Linked List FIFO ist oben gezeigt. Es ist ein Protected Type mit drei Funktionen: Push, Pop und IsEmpty. Diese werden zum Hinzufügen und Entfernen von Elementen aus der FIFO sowie zum Prüfen genutzt, ob null Elemente übrig sind.
-- Testbench FIFO, der das DUT emuliert shared variable fifo : LinkedList;
Protected Types sind klassenähnliche Konstrukte in VHDL. Wir erstellen ein Objekt der verketteten Liste, indem wir eine Shared Variable im deklarativen Bereich des Testbenchs deklarieren, wie oben gezeigt.
Das Verhaltensmodell
Um das Verhalten des Ringpuffer-FIFOs vollständig zu emulieren, deklarieren wir zwei neue Signale, die die Ausgangssignale des DUT spiegeln. Das erste Signal enthält die Ausgabedaten des Verhaltensmodells, das zweite das zugehörige Valid-Signal.
-- Testbench FIFO Signale signal fifo_out : integer; signal fifo_out_valid : std_logic := '0';
Der obige Code zeigt die Deklaration der zwei Ausgangssignale des Verhaltensmodells. Wir brauchen keine dedizierten Eingangssignale für das Verhaltensmodell, da sie dieselben sind wie die mit dem DUT verbundenen. Wir nutzen Signale zur Emulation des DUT-Ausgangs, weil es uns ermöglicht, Coverage-Daten einfach zu sammeln, wie wir später sehen werden.
PROC_BEHAVIORAL_MODEL : process begin wait until rising_edge(clk) and rst = '0'; -- Schreiben emulieren if wr_en = '1' and full = '0' then fifo.Push(to_integer(unsigned(wr_data))); report "Push " & integer'image(to_integer(unsigned(wr_data))); end if; -- Lesen emulieren if rd_en = '1' and empty = '0' then fifo_out <= fifo.Pop; fifo_out_valid <= '1'; else fifo_out_valid <= '0'; end if; end process;
Der Prozess, der das Verhaltensmodell des Ringpuffer-FIFOs implementiert, ist oben gezeigt. Dieser Prozess wird an jeder steigenden Taktflanke getriggert, wenn das Reset-Signal nicht aktiv ist.
Das Verhaltensmodell pusht einen neuen Wert in den Testbench-FIFO, wann immer wr_en assertiert ist, während full '0' ist. Ähnlich funktioniert die Lese-Logik im Verhaltensmodell-Prozess, indem sie auf rd_en und empty lauscht. Letzteres wird vom DUT gesteuert, aber wir werden verifizieren, dass es funktioniert, im nächsten Prozess.
Ausgaben prüfen
Die gesamte Logik, die für das Prüfen der DUT-Ausgaben verantwortlich ist, ist in einem Prozess namens «PROC_VERIFY» gesammelt. Wir nutzen Assert-Statements, um zu prüfen, dass die DUT-Ausgaben denen des Verhaltensmodells entsprechen. Wir prüfen auch, dass DUT und Verhaltensmodell darin übereinstimmen, wann die FIFO leer ist.
Der Code für den Verifikationsprozess ist unten gezeigt.
PROC_VERIFY : process begin wait until rising_edge(clk) and rst = '0'; -- Prüfen, dass DUT und TB FIFO empty simultan melden assert (empty = '1' and fifo.IsEmpty) or (empty = '0' and not fifo.IsEmpty) report "empty=" & std_logic'image(empty) & " while fifo.IsEmpty=" & boolean'image(fifo.IsEmpty) severity failure; -- Prüfen, dass Valid-Signale matchen assert rd_valid = fifo_out_valid report "rd_valid=" & std_logic'image(rd_valid) & " while fifo_out_valid=" & std_logic'image(fifo_out_valid) severity failure; -- Prüfen, dass DUT-Ausgabe mit TB FIFO matcht if rd_valid then assert fifo_out = to_integer(unsigned(rd_data)) report "rd_data=" & integer'image(to_integer(unsigned(rd_data))) & " while fifo_out=" & integer'image(fifo_out) severity failure; report "Pop " & integer'image(fifo_out); end if; end process;
Der Prozess wird durch die steigende Taktflanke getriggert, wie an der ersten Codezeile ersichtlich. Das DUT ist ein getakteter Prozess, und die nachgelagerte Logik, die mit dem DUT verbunden ist, wird ebenfalls synchron zum Taktsignal erwartet. Daher ist es sinnvoll, die Ausgänge an der steigenden Taktflanke zu prüfen.
Der zweite Codeblock prüft, dass das empty-Signal vom DUT nur dann assertiert ist, wenn der Testbench-FIFO leer ist. DUT und Verhaltensmodell müssen beide übereinstimmen, dass die FIFO leer ist oder nicht, damit dieser Test besteht.
Es folgt ein Vergleich der Read-Data-Valid-Signale. DUT und Verhaltensmodell sollten gleichzeitig Daten ausgeben, sonst stimmt etwas nicht.
Schließlich prüfen wir, dass die Ausgabedaten des DUT dem nächsten Element entsprechen, das wir aus dem Testbench-FIFO poppen. Das passiert natürlich nur, wenn rd_valid assertiert ist, was bedeutet, dass rd_data gesampelt werden kann.
Coverage-Daten sammeln
Um den Hauptfluss des Testbenchs zu steuern, erstellen wir einen Sequencer-Prozess. Dieser initialisiert die Coverage-Bins, führt die Tests aus und stoppt den Testbench, wenn alle Coverage-Ziele erreicht sind. Der folgende Code zeigt den vollständigen «PROC_SEQUENCER»-Prozess.
PROC_SEQUENCER : process begin -- Coverage Bins einrichten bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN); wait until rising_edge(clk); wait until rising_edge(clk); rst <= '0'; wait until rising_edge(clk); loop wait until rising_edge(clk); -- Coverage-Daten sammeln bin1.ICover(to_integer(wr_en = '1' and empty = '1')); bin2.ICover(to_integer(rd_en = '1' and full = '1')); bin3.ICover(to_integer(rd_en = '1' and wr_en = '1' and empty = '0' and empty_next = '1')); bin4.ICover(to_integer(rd_en = '1' and wr_en = '1' and full = '0' and full_next = '1')); bin5.ICover(to_integer(rd_en = '1' and wr_en = '0' and empty = '0' and empty_next = '1')); bin6.ICover(to_integer(rd_en = '0' and wr_en = '1' and full = '0' and full_next = '1')); -- Test stoppen, wenn alle Coverage-Ziele erreicht sind exit when bin1.IsCovered and bin2.IsCovered and bin3.IsCovered and bin4.IsCovered and bin5.IsCovered and bin6.IsCovered; end loop; report("Coverage goals met"); -- Sicherstellen, dass DUT leer ist, bevor Test endet wr_en <= force '0'; rd_en <= force '1'; loop wait until rising_edge(clk); exit when empty = '1'; end loop; -- Coverage-Daten ausgeben bin1.WriteBin; bin2.WriteBin; bin3.WriteBin; bin4.WriteBin; bin5.WriteBin; bin6.WriteBin; finish; end process;
Zuerst initialisieren wir die Coverage-Bin-Objekte durch Aufruf der AddBins-Prozedur, wie bereits früher im Artikel besprochen. Dann, nachdem der Reset freigegeben wurde, sammeln wir Coverage-Daten. An jeder steigenden Taktflanke wird der Code innerhalb der Loop-Konstruktion ausgeführt.
Der erste Codeblock innerhalb der Loop sammelt Coverage-Daten. Wir können die ICover-Prozedur auf dem Bin aufrufen, um einen Treffer auf dem Coverage-Punkt zu registrieren, den es repräsentiert. Übergeben wir den Integer-Parameter 0, hat der Aufruf keine Wirkung. Nutzen wir 1, zählt es als Treffer.
In jedem Coverage-Bin-Objekt gibt es nur ein «Bin», da wir sie mit der ONE_BIN-Konstante initialisiert haben. Dieser einzelne Bin kann durch Aufruf von ICover(1) erreicht werden. Wir registrieren einen Treffer oder Fehlschlag auf dem Coverage-Punkt, indem wir unsere Booleschen Ausdrücke mittels to_integer-Funktion in die Integers 1 oder 0 konvertieren.
Nach der Coverage-Datenerfassung prüfen wir, ob alle Coverage-Ziele erreicht wurden, indem wir IsCovered auf allen Bins aufrufen. Dann verlassen wir die Schleife, wenn alle Coverage-Ziele erreicht sind.
Wir stellen sicher, dass das DUT leer ist, bevor wir den Test beenden. Dazu übernehmen wir Kontrolle vom Writer- und Reader-Prozess, indem wir wr_en auf '0' und rd_en auf '1' forcen.
Schließlich geben wir Statistiken aus, wie oft jedes Coverage-Ziel getroffen wurde, indem wir WriteBin auf jedem Coverage-Bin aufrufen. Das finish-Keyword am Prozessende veranlasst den Simulator, die Simulation zu stoppen.
Testbench ausführen
Sie können das gesamte ModelSim-Projekt mit allen VHDL-Dateien über das untenstehende Formular herunterladen.
Nach dem Laden des Projekts durch Ausführen der im ZIP enthaltenen Do-Datei können wir den Testbench einfach durch Eingabe von «runtb» in der ModelSim-Konsole starten. Die Laufzeit des Testbenchs ist zufällig, da die Coverage-Ziele zufällig getroffen werden. Die Testergebnisse sind jedoch reproduzierbar, da eine pseudo-zufällige Sequenz verwendet wird.
Wir haben in unserem Code keinen Seed initialisiert, weshalb der Default-Seed-Wert für den Pseudo-Zufallsgenerator genutzt wird. Es ist möglich, einen anderen Seed zu setzen, indem man die InitSeed-Prozedur auf dem RandomPType-Objekt aufruft; das erzeugt eine andere Zufallssequenz.
Konsolenausgabe
Die Ausgabe, die nach dem «runtb»-Befehl in der ModelSim-Konsole erscheint, ist unten gezeigt. Es wird viel zufälliges Pushen und Poppen vom Testbench-FIFO während der Simulation geben.
VSIM 2> runtb # ** Warning: Design size of 15929 statements or 2 leaf instances exceeds # ModelSim PE Student Edition recommended capacity. # Expect performance to be quite adversely affected. # ** Note: Push 34910 # Time: 790 ns Iteration: 0 Instance: /ring_buffer_tb ... # ** Note: Pop 37937 # Time: 83100 ns Iteration: 0 Instance: /ring_buffer_tb # ** Note: Pop 13898 # Time: 83110 ns Iteration: 0 Instance: /ring_buffer_tb # %% WriteBin: # %% Write while empty Bin:(1) Count = 2 AtLeast = 1 # # %% WriteBin: # %% Read while full Bin:(1) Count = 3 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost empty Bin:(1) Count = 106 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost full Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Read without write when almost empty Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Write without read when almost full Bin:(1) Count = 3 AtLeast = 1 # # Break in Process PROC_SEQUENCER at C:/crv/ring_buffer_tb.vhd line 127
Statistiken für alle Coverage-Bins werden ausgegeben, sobald alle Coverage-Ziele erreicht sind. Manche Bins wurden nur einmal getroffen, einer 106-mal. Aber am Ende wurde jeder Bin mindestens einmal getroffen. Damit wissen wir, dass alle Events, für die wir Coverage-Bins definiert haben, getestet und verifiziert wurden.
Das Wellenform
Betrachten wir das Wellenform, um eine Vorstellung davon zu bekommen, was der Testbench getan hat. Das folgende Bild zeigt das Wellenform mit dem fill_count-Signal als analoger Wert dargestellt. Die FIFO ist voll, wenn der Trace für dieses Signal oben ist, und leer, wenn er unten ist.

Wie aus dem Wellenform ersichtlich, wird der Ringpuffer zufällig gefüllt und geleert. Doch wir haben diese Anstiege und Abfälle im Füllstand nicht explizit programmiert. Dennoch sehen wir ein organisch wirkendes Interaktionsmuster mit dem DUT.
Mehr über Constrained Random Verification
Constrained Random Verification ist eine gute Testbench-Strategie, wenn der Testvektor zu viele Permutationen für einen exhaustiven Test hat. Die zufälligen Interaktionen zeigen ein natürlicheres Verhalten als ein gezielter Corner-Case-Test, ohne an Genauigkeit einzubüßen.
Wir können sicher sein, dass alle Corner Cases abgedeckt sind, solange wir die Coverage-Datensammlung korrekt aufgesetzt haben. Der zusätzliche Nutzen: Randomisiertes Testen deckt eher Schwächen im DUT auf, die Sie nicht explizit testen. Solange Sie alle Corner Cases kennen, können Sie directed Tests dafür erstellen. Aber Corner Cases werden leicht übersehen – und genau dann profitieren Sie von der Constrained Random Verification Methodik.
Dieser Artikel hat nur an der Oberfläche gekratzt, was mit Constrained Random Verification möglich ist. Ich empfehle, die Docs auf der OSVVM GitHub-Seite zu lesen, um tiefer in das Thema einzusteigen.
Ich empfehle auch SynthWorks' Advanced VHDL Testbenches and Verification Kurs, mit dem ich nicht affiliiert bin. Ich habe jedoch die 5-Tage-Version dieses physischen Kurses besucht. Der Kurs wird von Jim Lewis geleitet, dem Vorsitzenden der VHDL Analysis and Standardization Group (VASG). Insgesamt eine großartige Investition für jede Firma, die ihre VHDL-Testbenches auf das nächste Level heben will.
VHDL
- Cadence stellt Rapid-Performance Emulation & Prototyping für Billion-Gate SoC Verification vor
- Siemens erweitert Veloce System um vier neue Produkte für nahtlose Hardware-Assisted Verification
- Synopsys präsentiert vollständiges HBM3 IP-Paket für Multi-Die-Systeme mit 921 GB/s Bandbreite
- Integriertes QR-, RFID- und Temperatur-Verifikations-Zugangskontrollsystem
- Die unberechenbaren Herausforderungen der Instandhaltung: Einblicke von Experte Jason Afara
- Zufallszahlen in Java generieren: Praktischer Leitfaden mit Random und Math.random
- Java 8 Streams: Deklarative Datenverarbeitung – filter, map, reduce, collect & parallel
- Hurco TM8 Schrägbettdrehmaschine – Kompaktes Design, Präzisionssteuerung & Vielseitiges Werkzeugsystem
- High-Speed PCB Design: Differenzielle Signalverarbeitung & Simulationsverifikation
- CAGI Rotary Compressor Performance Verification Program: Zuverlässiger, effizienter Betrieb sichern