
Mit der Entwicklung der Mikroelektronik sind RTL-Designs immer mehr geworden. Die Wiederverwendbarkeit von Verilog-Code ist selbst bei Generierungs-, Makro- und System-Verilog-Chips sehr unpraktisch. Chisel ermöglicht es jedoch, die volle Leistungsfähigkeit der Objekt- und Funktionsprogrammierung auf die RTL-Entwicklung anzuwenden. Dies ist ein lang erwarteter Schritt, der die Lungen von ASIC- und FPGA-Entwicklern mit frischer Luft füllen kann.
Dieser Artikel gibt einen kurzen Überblick über die Hauptfunktionen und berücksichtigt einige Anwendungsfälle der Benutzer. Wir werden auch auf die Mängel dieser Sprache eingehen. Wenn das Thema in Zukunft interessant ist, setzen wir den Artikel in detaillierteren Tutorials fort.
Systemanforderungen
- Scala Basisniveau
- Verilog und die Grundprinzipien für die Erstellung digitaler Designs.
- Halten Sie die Meißeldokumentation bereit
Ich werde versuchen, die Grundlagen des Meißelns anhand einfacher Beispiele zu verstehen, aber wenn etwas nicht klar ist, können Sie hier einen Blick darauf werfen.
Was Scala betrifft, kann dieser Spickzettel für einen schnellen Tauchgang hilfreich sein.
Es gibt einen ähnlichen für Meißel .
Den vollständigen Artikelcode (in Form eines Scala-Sbt-Projekts) finden Sie hier .
Einfacher Zähler
Wie der Name schon sagt, ist der Meißel "Hardware in einer eingebetteten Scala erstellen" eine Hardwarebeschreibungssprache, die auf der Scala aufbaut.
Kurz gesagt, wie alles funktioniert: Aus der RTL-Beschreibung auf dem Meißel wird ein Hardware-Diagramm erstellt, das sich wiederum in eine Zwischenbeschreibung in der Firrtl-Sprache verwandelt. Anschließend wird der integrierte Backend-Interpreter aus dem Firrtl-Verilog generiert.
Schauen wir uns zwei Implementierungen eines einfachen Zählers an.
Verilog:
module SimpleCounter #( parameter WIDTH = 8 )( input clk, input reset, input wire enable, output wire [WIDTH-1:0] out ); reg [WIDTH-1:0] counter; assign out = counter; always @(posedge clk) if (reset) begin counter <= {(WIDTH){1'b0}}; end else if (enable) begin counter <= counter + 1; end endmodule
Meißel:
class SimpleCounter(width: Int = 32) extends Module { val io = IO(new Bundle { val enable = Input(Bool()) val out = Output(UInt(width.W)) }) val counter = RegInit(0.U(width.W)) io.out <> counter when(io.enable) { counter := counter + 1.U } }
Ein bisschen über Meißel:
Module
- Container für die Beschreibung des RTL-ModulsBundle
ist eine Datenstruktur in Meißel, die hauptsächlich zum Definieren von Schnittstellen verwendet wird.io
- Variable zur Bestimmung von PortsBool
- Datentyp, einfaches Einzelbit-SignalUInt(width: Width)
- Ganzzahl ohne Vorzeichen, der Konstruktor akzeptiert die Bittiefe des Signals als Eingabe.RegInit[T <: Data](init: T)
ist ein Registerkonstruktor, der einen Rücksetzwert als Eingabe verwendet und denselben Datentyp hat.<>
- universeller Signalverbindungsoperatorwhen(cond: => Bool) { /*...*/ }
- das if
Analogon in Verilog
Wir werden etwas später darüber sprechen, welcher Verilog Meißel erzeugt. Vergleichen Sie nun einfach diese beiden Designs. Wie Sie sehen können, werden clk
und reset
Signale im Meißel nicht erwähnt. Tatsache ist, dass Meißel diese Signale standardmäßig zum Modul hinzufügt. Der Rücksetzwert für das RegInit
mit dem RegInit
Reset RegInit
Registerkonstruktor RegInit
. Chisel unterstützt Module mit vielen Taktsignalen, aber etwas später.
Der Zähler ist etwas komplizierter
Gehen wir weiter und komplizieren die Aufgabe zum Beispiel ein wenig - wir erstellen einen Mehrkanalzähler mit einem Eingabeparameter in Form einer Folge von Bits für jeden Kanal.
Beginnen wir jetzt mit der Meißelversion
class MultiChannelCounter(width: Seq[Int] = Seq(32, 16, 8, 4)) extends Module { val io = IO(new Bundle { val enable = Input(Vec(width.length, Bool())) val out = Output(UInt(width.sum.W)) def getOut(i: Int): UInt = { val right = width.dropRight(width.length - i).sum this.out(right + width(i) - 1, right) } }) val counters: Seq[SimpleCounter] = width.map(x => Module(new SimpleCounter(x)) ) io.out <> util.Cat(counters.map(_.io.out)) width.indices.foreach { i => counters(i).io.enable <> io.enable(i) } }
Ein bisschen über Scala:
width: Seq[Int]
- Eingabeparameter für den Konstruktor der MultiChannelCounter
Klasse vom Typ Seq[Int]
- eine Sequenz mit ganzzahligen Elementen.Seq
ist eine der Arten von Sammlungen in Scala mit einer genau definierten Folge von Elementen..map
ist eine vertraute Funktion für Sammlungen für alle, die aufgrund derselben Operation für jedes Element eine Sammlung in eine andere konvertieren kann. In unserem Fall wird aus einer Folge ganzzahliger Werte eine Folge von SimpleCounter
mit der entsprechenden Bittiefe.
Ein bisschen über Meißel:
Vec[T <: Data](gen: T, n: Int): Vec[T]
Datentyp Vec[T <: Data](gen: T, n: Int): Vec[T]
- Meißel ist ein Analogon des Arrays.Module[T <: BaseModule](bc: => T): T
ist die erforderliche Wrapper-Methode für instanziierbare Module.util.Cat[T <: Bits](r: Seq[T]): UInt
- Verkettungsfunktion, analog {1'b1, 2'b01, 4'h0}
in Verilog
Achten Sie auf die Häfen:
enable
- bereits in Vec[Bool]
* implementiert, grob gesagt, in einem Array von Ein-Bit-Signalen, eines für jeden Kanal, war es möglich, UInt(width.length.W)
.
out
- erweitert auf die Summe der Breiten aller unserer Kanäle.
Die variablen counters
sind ein Array unserer Zähler. Wir verbinden das enable
jedes Zählers mit dem entsprechenden Eingangsport und kombinieren alle Ausgangssignale mithilfe der integrierten util.Cat
Funktion zu einem und leiten es an den Ausgang weiter.
Wir bemerken auch die Funktion getOut(i: Int)
- diese Funktion berechnet und gibt den getOut(i: Int)
im getOut(i: Int)
für den i
Kanal zurück. Es wird bei der weiteren Arbeit mit einem solchen Zähler sehr nützlich sein. Die Implementierung von so etwas in Verilog funktioniert nicht
* Vec
sollte nicht mit Vector
verwechselt werden, das erste ist ein Array von Daten in Meißel, das zweite ist eine Sammlung in Scala.
Lassen Sie uns nun versuchen, dieses Modul auf Verilog zu schreiben, um es sogar auf systemVerilog zu vereinfachen.
Nachdem ich nachgedacht hatte, kam ich zu dieser Option (höchstwahrscheinlich ist sie nicht die einzig wahre und optimalste, aber Sie können Ihre Implementierung immer in den Kommentaren vorschlagen).
Verilog module MultiChannelCounter #( parameter TOTAL = 4, parameter integer WIDTH_SEQ [TOTAL] = {32, 16, 8, 4} )(clk, reset, enable, out); localparam OUT_WIDTH = get_sum(TOTAL, WIDTH_SEQ); input clk; input reset; input wire [TOTAL - 1 : 0] enable; output wire [OUT_WIDTH - 1 :0] out; genvar j; generate for(j = 0; j < TOTAL; j = j + 1) begin : counter_generation localparam OUT_INDEX = get_sum(j, WIDTH_SEQ); SimpleCounter #( WIDTH_SEQ[j] ) SimpleCounter_unit ( .clk(clk), .reset(reset), .enable(enable[j]), .out(out[OUT_INDEX + WIDTH_SEQ[j] - 1: OUT_INDEX]) ); end endgenerate function automatic integer get_sum; input integer array_width; input integer array [TOTAL]; integer counter = 0; integer i; begin for(i = 0; i < array_width; i = i + 1) counter = counter + array[i]; get_sum = counter; end endfunction endmodule
Es sieht schon viel beeindruckender aus. Was aber, wenn wir weiter gehen und die beliebte Querlenkerschnittstelle mit Registerzugriff anschrauben.
Bündelschnittstellen
Wishbone ist ein kleiner Bus ähnlich wie AMBA APB, der hauptsächlich für Open Source IP-Kerne verwendet wird.
Weitere Details im Wiki: https://ru.wikipedia.org/wiki/Wishbone
Weil chisel stellt uns Bundle
Typ Bundle
. Es ist sinnvoll, den Bus in einen Container zu wickeln, der später in beliebigen Meißelprojekten verwendet werden kann.
class wishboneMasterSignals( addrWidth: Int = 32, dataWidth: Int = 32, gotTag: Boolean = false) extends Bundle { val adr = Output(UInt(addrWidth.W)) val dat_master = Output(UInt(dataWidth.W)) val dat_slave = Input(UInt(dataWidth.W)) val stb = Output(Bool()) val we = Output(Bool()) val cyc = Output(Bool()) val sel = Output(UInt((dataWidth / 8).W)) val ack_master = Output(Bool()) val ack_slave = Input(Bool()) val tag_master: Option[UInt] = if(gotTag) Some(Output(Bool())) else None val tag_slave: Option[UInt] = if(gotTag) Some(Input(Bool())) else None def wbTransaction: Bool = cyc && stb def wbWrite: Bool = wbTransaction && we def wbRead: Bool = wbTransaction && !we override def cloneType: wishboneMasterSignals.this.type = new wishboneMasterSignals(addrWidth, dataWidth, gotTag).asInstanceOf[this.type] }
Ein bisschen über Scala:
Option
- ein optionaler Daten-Wrapper in Scala, der entweder ein Element oder None
kann. Option[UInt]
ist entweder Some(UInt(/*...*/))
oder None
, was bei der Parametrisierung von Signalen hilfreich ist.
Es scheint nichts Ungewöhnliches. Nur eine Beschreibung der Schnittstelle durch den Assistenten, mit Ausnahme einiger Signale und Methoden:
tag_master
und tag_slave
sind optionale Allzweck-Signale im Wishbone-Protokoll. Wir werden sie sehen, wenn der gotTag
Parameter true
.
wbTransaction
, wbWrite
, wbRead
- Funktionen zur Vereinfachung der Arbeit mit dem Bus.
cloneType
- erforderliche cloneType
für alle parametrisierten [T <: Bundle]
-Klassen
Wir brauchen aber auch eine Slave-Schnittstelle. Mal sehen, wie sie implementiert werden kann.
class wishboneSlave( addrWidth: Int = 32, dataWidth: Int = 32, tagWidht: Int = 0) extends Bundle { val wb = Flipped(new wishboneMasterSignals(addrWidth , dataWidth, tagWidht)) override def cloneType: wishboneSlave.this.type = new wishboneSlave(addrWidth, dataWidth, tagWidht).asInstanceOf[this.type] }
Die Flipped
Methode dreht, wie Sie vielleicht anhand des Namens erraten haben, die Schnittstelle um, und jetzt, da unsere Assistentenschnittstelle zu einem Slave geworden ist, fügen wir dieselbe Klasse für den Assistenten hinzu.
class wishboneMaster( addrWidth: Int = 32, dataWidth: Int = 32, tagWidht: Int = 0) extends Bundle { val wb = new wishboneMasterSignals(addrWidth , dataWidth, tagWidht) override def cloneType: wishboneMaster.this.type = new wishboneMaster(addrWidth, dataWidth, tagWidht).asInstanceOf[this.type] }
Nun, das war's, die Schnittstelle ist fertig. Bevor wir jedoch einen Handler schreiben, wollen wir sehen, wie wir diese Schnittstellen verwenden können, falls wir einen Switch oder etwas mit einer großen Anzahl von Wishbone-Schnittstellen vornehmen müssen.
class WishboneCrossbarIo(n: Int, addrWidth: Int, dataWidth: Int) extends Bundle { val slaves = Vec(n, new wishboneSlave(addrWidth, dataWidth, 0)) val master = new wishboneMaster(addrWidth, dataWidth, 0) } class WBCrossBar extends Module { val io = IO(new WishboneCrossbarIo(1, 32, 32)) io.master <> io.slaves(0)
Dies ist ein kleines Leerzeichen für den Schalter. Es ist praktisch, eine Schnittstelle vom Typ Vec[wishboneSlave]
zu deklarieren, und Sie können die Schnittstellen mit demselben <>
Operator verbinden. Nützliche Meißelchips, wenn es darum geht, eine große Anzahl von Signalen zu verwalten.
Universelle Bussteuerung
Wie bereits erwähnt, werden wir versuchen, diese auf die Leistungsfähigkeit der Funktions- und Objektprogrammierung anzuwenden. Weiter werden wir über die Implementierung des universellen Wishbone-Bus-Controllers in Form eines trait
sprechen. Es wird eine Art Mixin für jedes Modul mit dem wishboneSlave
Bus sein. Für das Modul müssen Sie lediglich eine Speicherkarte definieren und den trait
Controller während der Generierung mit diesem mischen.
Implementierung
Für diejenigen, die immer noch begeistert sindFahren wir mit der Implementierung des Handlers fort. Es ist einfach und reagiert sofort auf einzelne Transaktionen. Wenn Sie aus dem Adresspool herausfallen, geben Sie Null zurück.
Lassen Sie uns in Teilen analysieren:
Jede Transaktion muss mit Bestätigung beantwortet werden
val io : wishboneSlave = val wb_ack = RegInit(false.B) when(io.wb.wbTransaction) { wb_ack := true.B }.otherwise { wb_ack := false.B } wb_ack <> io.wb.ack_slave
- Wir antworten auf das Lesen mit Daten
val wb_dat = RegInit(0.U(io.wb.dat_slave.getWidth.W))
MuxCase[T <: Data] (default: T, mapping: Seq[(Bool, T)]): T
ist das in Verilog * integrierte Koordinationsschema des Falltyps.
Wie würde es in Verilog aussehen:
always @(posedge clock) if(reset) wb_dat_o <= 0; else if(wb_read) case (wb_adr_i) `ADDR_1 : wb_dat_o <= data_1; `ADDR_2 : wb_dat_o <= data_2; `ADDR_3 : wb_dat_o <= data_3; default : wb_dat_o <= 0; endcase }
* Im Allgemeinen ist dies in diesem Fall ein kleiner Hack zur Parametrisierung. Meißel hat ein Standarddesign, das besser zu verwenden ist, wenn Sie etwas Einfacheres schreiben.
switch(x) { is(value1) {
Nun, die Aufzeichnung
when(io.wb.wbWrite) { data_4 := Mux(io.wb.addr === ADDR_4, io.wb.dat_master, data_4) }
Mux[T <: Data](cond: Bool, con: T, alt: T): T
- regulärer Multiplexer
Wir binden etwas Ähnliches wie unseren Mehrkanalzähler ein, hängen Register für die Kanalverwaltung und einen Hut auf. Aber hier liegt es in der Nähe des universellen WB-Bus-Controllers, auf den wir eine Speicherkarte dieser Art übertragen werden:
val readMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 ) val writeMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 )
Für eine solche Aufgabe hilft uns das trait
- so etwas wie Mixins in Sala. Die Hauptaufgabe besteht darin, readMemMap: [Int, Data]
wie Seq( -> )
aussehen zu lassen, und es wäre auch schön, wenn Sie die Basisadresse und das Datenarray innerhalb der Speicherkarte übertragen könnten
val readMemMap = Map( ADDR_1_BASE -> DATA_SEQ, ADDR_2 -> DATA_2 )
Was wird in etwas Ähnliches erweitert, wobei WB_DAT_WIDTH die Breite der Daten in Bytes ist
val readMemMap = Map( ADDR_1_BASE + 0 * (WB_DAT_WIDHT)-> DATA_SEQ_0, ADDR_1_BASE + 1 * (WB_DAT_WIDHT)-> DATA_SEQ_1, ADDR_1_BASE + 2 * (WB_DAT_WIDHT)-> DATA_SEQ_2, ADDR_1_BASE + 3 * (WB_DAT_WIDHT)-> DATA_SEQ_3 ADDR_2 -> DATA_2 )
Um dies zu implementieren, schreiben wir eine Konverterfunktion von Map[Int, Any]
nach Seq[(Bool, UInt)]
. Sie müssen Scala Pattern Mathcing verwenden.
def parseMemMap(memMap: Map[Int, Any]): Seq[(Bool, UInt)] = memMap.flatMap { case(addr, data) => data match { case a: UInt => Seq((io.wb.adr === addr.U) -> a) case a: Seq[UInt] => a.map(x => (io.wb.adr === (addr + io.wb.dat_slave.getWidth / 8).U) -> x) case _ => throw new Exception("WRONG MEM MAP!!!") } }.toSeq
Schließlich wird unser Merkmal so aussehen:
trait wishboneSlaveDriver { val io : wishboneSlave val readMemMap: Map[Int, Any] val writeMemMap: Map[Int, Any] val parsedReadMap: Seq[(Bool, UInt)] = parseMemMap(readMemMap) val parsedWriteMap: Seq[(Bool, UInt)] = parseMemMap(writeMemMap) val wb_ack = RegInit(false.B) val wb_dat = RegInit(0.U(io.wb.dat_slave.getWidth.W)) when(io.wb.wbTransaction) { wb_ack := true.B }.otherwise { wb_ack := false.B } when(io.wb.wbRead) { wb_dat := MuxCase(default = 0.U, parsedReadMap) } when(io.wb.wbWrite) { parsedWriteMap.foreach { case(addrMatched, data) => data := Mux(addrMatched, io.wb.dat_master, data) } } wb_dat <> io.wb.dat_slave wb_ack <> io.wb.ack_slave def parseMemMap(memMap: Map[Int, Any]): Seq[(Bool, UInt)] = { } }
Ein bisschen über Scala:
io , readMemMap, writeMemMap
sind die abstrakten Felder unseres trait
'a, die in der Klasse definiert werden müssen, in die wir es mischen werden.
Wie man es benutzt
Um unser trait
mit dem Modul zu mischen, müssen mehrere Bedingungen erfüllt sein:
io
sollte von der wishboneSlave
Klasse erben- müssen zwei Speicherkarten
readMemMap
und writeMemMap
class WishboneMultiChannelCounter extends Module { val BASE = 0x11A00000 val OUT = 0x00000100 val S_EN = 0x00000200 val H_EN = 0x00000300 val wbAddrWidth = 32 val wbDataWidth = 32 val wbTagWidth = 0 val width = Seq(32, 16, 8, 4) val io = IO(new wishboneSlave(wbAddrWidth, wbDataWidth, wbTagWidth) { val hardwareEnable: Vec[Bool] = Input(Vec(width.length, Bool())) }) val counter = Module(new MultiChannelCounter(width)) val softwareEnable = RegInit(0.U(width.length.W)) width.indices.foreach(i => counter.io.enable(i) := io.hardwareEnable(i) && softwareEnable(i)) val readMemMap = Map( BASE + OUT -> width.indices.map(counter.io.getOut), BASE + S_EN -> softwareEnable, BASE + H_EN -> io.hardwareEnable.asUInt ) val writeMemMap = Map( BASE + S_EN -> softwareEnable ) }
Wir erstellen das softwareEnable
Register, es wird durch das hardwareEnable
Eingangssignal zu 'und' hinzugefügt und aktiviert den counter[MultiChannelCounter]
.
Wir deklarieren zwei Speicherkarten zum Lesen und Schreiben: readMemMap
writeMemMap
. Weitere Informationen zur Struktur finden Sie im obigen Kapitel.
Auf der Lesespeicherkarte übertragen wir den Zählerwert jedes Kanals *, softwareEnable
und hardwareEnable
. Und für die Aufzeichnung geben wir nur das softwareEnable
Register.
* width.indices.map(counter.io.getOut)
- ein seltsames Design, das wir in Teilen analysieren werden.
width.indices
- gibt ein Array mit Elementindizes zurück, d. width.indices
wenn width.length == 4
dann width.indices = {0, 1, 2, 3}
{0, 1, 2, 3}.map(counter.io.getOut)
- gibt {0, 1, 2, 3}.map(counter.io.getOut)
:
{ counter.io.getOut(0), counter.io.getOut(1), /*...*/ }
Jetzt können wir für jedes Modul auf Meißel mit Speicherkarten zum Lesen und Schreiben deklarieren und beim Generieren einfach unseren universellen Querlenker-Bus-Controller anschließen.
class wishbone_multicahnnel_counter extends WishboneMultiChannelCounter with wishboneSlaveDriver object countersDriver extends App { Driver.execute(Array("-td", "./src/generated"), () => new wishbone_multicahnnel_counter ) }
wishboneSlaveDriver
- das ist genau der Trait-Mix, den wir unter dem Spoiler beschrieben haben.
Natürlich ist diese Version des Universal-Controllers alles andere als endgültig, sondern im Gegenteil eher grob. Ihr Hauptziel ist es, einen der möglichen Ansätze zur Entwicklung von RTL auf Meißel aufzuzeigen. Mit all den Möglichkeiten von Scala können solche Ansätze viel größer sein, sodass jeder Entwickler sein eigenes Feld für Kreativität hat. Richtig, besonders nirgendwo, wo man sich besonders inspirieren lässt, außer:
- In der nativen Meißel-Utils-Bibliothek, über die Sie etwas weiter unten die Vererbung von Modulen und Schnittstellen betrachten können
- https://github.com/freechipsproject/rocket-chip - risc-v Der gesamte Kernel ist auf Meißel implementiert, vorausgesetzt, Sie kennen Scala sehr gut. Für Anfänger ohne einen halben Liter, wie sie sagen, wird das Verständnis sehr lange dauern. Es gibt keine offizielle Dokumentation zur internen Struktur des Projekts.
MultiClockDomain
Was ist, wenn wir die Uhr manuell steuern und die Signale im Meißel zurücksetzen möchten? Bis vor kurzem war dies nicht möglich, aber mit einer der neuesten Versionen wurde die Unterstützung von withClock {}
, withReset {}
und withClockAndReset {}
. Schauen wir uns ein Beispiel an:
class DoubleClockModule extends Module { val io = IO(new Bundle { val clockB = Input(Clock()) val in = Input(Bool()) val out = Output(Bool()) val outB = Output(Bool()) }) val regClock = RegNext(io.in, false.B) regClock <> io.out val regClockB = withClock(io.clockB) { RegNext(io.in, false.B) } regClockB <> io.outB }
regClock
- Ein Register, das vom Standardtaktsignal clock
und vom Standardreset reset
regClockB
- dasselbe Register wird vom io.clockB
Signal getaktet, Sie haben es erraten, aber der Standard-Reset wird verwendet.
Wenn wir die Standarduhr entfernen und die Signale vollständig reset
möchten, können wir die experimentelle Funktion - RawModule
(Modul ohne Standarduhr- und Rücksetzsignale, jeder muss manuell gesteuert werden) verwenden. Ein Beispiel:
class MultiClockModule extends RawModule { val io = IO(new Bundle { val clockA = Input(Clock()) val clockB = Input(Clock()) val resetA = Input(Bool()) val resetB = Input(Bool()) val in = Input(Bool()) val outA = Output(Bool()) val outB = Output(Bool()) }) val regClockA = withClockAndReset(io.clockA, io.resetA) { RegNext(io.in, false.B) } regClockA <> io.outA val regClockB = withClockAndReset (io.clockB, io.resetB) { RegNext(io.in, false.B) } regClockB <> io.outB }
Utils Bibliothek
Die angenehmen Boni des Meißels enden hier nicht. Die Entwickler haben hart gearbeitet und eine kleine, aber sehr nützliche Bibliothek mit kleinen Schnittstellen, Modulen und Funktionen geschrieben. Seltsamerweise gibt es im Wiki keine Beschreibung der Bibliothek, aber Sie können den Spickzettel-Link sehen, auf den ganz am Anfang verwiesen wird (es gibt zwei letzte Abschnitte).
Schnittstellen:
DecoupledIO
ist die häufig verwendete Ready / Valid-Schnittstelle.
DecoupledIO(UInt(32.W))
- enthält Signale:
val ready = Input(Bool())
val valid = Output(Bool())
val data = Output(UInt(32.W))
ValidIO
- wie DecoupledIO
nur ohne ready
Module:
Queue
- Das synchrone FIFO-Modul ist eine sehr nützliche Sache. Die Schnittstelle sieht so aus
val enq: DecoupledIO[T]
- invertiertes DecoupledIO
val deq: DecoupledIO[T]
- reguläres DecoupledIO
val count: UInt
- Datenmenge in der WarteschlangePipe
, fügt die n-te Anzahl von Registerscheiben einArbiter
- Arbiter auf DecoupledIO
Schnittstellen, hat viele Unterarten, die sich in der Art der Arbitrierung unterscheiden
val in: Vec[DecoupledIO[T]]
- Array von Eingangsschnittstellen
val out: DecoupledIO[T]
val chosen: UInt
- Zeigt den ausgewählten Kanal an
Soweit Sie aus der Diskussion über Github verstehen können, haben die globalen Pläne eine bedeutende Erweiterung dieser Modulbibliothek: wie asynchrones FIFO, LSFSR, Frequenzteiler, PLL-Vorlagen für FPGA; verschiedene Schnittstellen; Controller für sie und vieles mehr.
Meißel-Io-Teseter
Die Möglichkeit, in Meißel zu testen, sollte erwähnt werden. Derzeit gibt es zwei Möglichkeiten, dies zu testen:
peekPokeTesters
- reine Simulationstests, die die Logik Ihres Designs testenhardwareIOTeseters
ist seitdem schon interessanter Mit diesem Ansatz erhalten Sie eine generierte Testbank mit Tests, die Sie auf Meißel geschrieben haben, und selbst wenn Sie einen Verilator haben, erhalten Sie sogar eine Zeitleiste.
Bisher wurde der Testansatz jedoch noch nicht abgeschlossen, und die Diskussion ist noch nicht abgeschlossen. In Zukunft wird höchstwahrscheinlich ein universelles Werkzeug erscheinen, zum Testen und Testen wird es auch möglich sein, auf Meißel zu schreiben. Aber jetzt können Sie sich ansehen, was bereits vorhanden ist und wie Sie es hier verwenden können .
Nachteile des Meißels
Dies bedeutet nicht, dass Meißel ein universelles Werkzeug ist und dass jeder darauf umsteigen sollte. Er hat, wie vielleicht alle Projekte in der Entwicklungsphase, seine Nachteile, die der Vollständigkeit halber erwähnenswert sind.
Der erste und vielleicht wichtigste Nachteil ist das Fehlen asynchroner Spülungen. Gewichtig genug, aber es kann auf verschiedene Arten gelöst werden. Eines davon sind Skripte über Verilog, die das synchrone Zurücksetzen in asynchrones umwandeln. Das ist einfach, weil Alle Konstruktionen im generierten Verilog mit sind always
ziemlich einheitlich.
Der zweite Nachteil ist nach Ansicht vieler die Unlesbarkeit des generierten Verilogs und infolgedessen die Komplikation des Debuggens. Aber schauen wir uns den generierten Code aus dem Beispiel mit einem einfachen Zähler an
Verilog generiert `ifdef RANDOMIZE_GARBAGE_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_INVALID_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_REG_INIT `define RANDOMIZE `endif `ifdef RANDOMIZE_MEM_INIT `define RANDOMIZE `endif module SimpleCounter( input clock, input reset, input io_enable, output [7:0] io_out ); reg [7:0] counter; reg [31:0] _RAND_0; wire [8:0] _T_7; wire [7:0] _T_8; wire [7:0] _GEN_0; assign _T_7 = counter + 8'h1; assign _T_8 = _T_7[7:0]; assign _GEN_0 = io_enable ? _T_8 : counter; assign io_out = counter; `ifdef RANDOMIZE integer initvar; initial begin `ifndef verilator #0.002 begin end `endif `ifdef RANDOMIZE_REG_INIT _RAND_0 = {1{$random}}; counter = _RAND_0[7:0]; `endif // RANDOMIZE_REG_INIT end `endif // RANDOMIZE always @(posedge clock) begin if (reset) begin counter <= 8'h0; end else begin if (io_enable) begin counter <= _T_8; end end end endmodule
Auf den ersten Blick kann sich der generierte Verilog auch in einem mittelgroßen Design wegschieben, aber werfen wir einen Blick darauf.
- RANDOMIZE-Definitionen - (kann beim Testen mit Meißeltestern nützlich sein) - sind im Allgemeinen nutzlos, stören jedoch nicht besonders
- Wie Sie den Namen unserer Häfen und das Register sehen können
- _GEN_0 ist eine nutzlose Variable für uns, aber notwendig, damit firrtl an den Interpreter Verilog generiert. Wir achten auch nicht darauf.
- Es verbleiben _T_7 und _T_8, die gesamte Kombinationslogik im generierten Verilog wird Schritt für Schritt in Form von Variablen _T dargestellt.
Am wichtigsten ist, dass alle zum Debuggen erforderlichen Ports, Register und Drähte ihre Namen vom Meißel fernhalten. Und wenn Sie sich nicht nur Verilog, sondern auch Meißel ansehen, wird der Debugging-Prozess bald so einfach wie mit reinem Verilog.
Fazit
In der modernen Realität hat sich die Entwicklung von RTL, ob asic oder fpga außerhalb des akademischen Umfelds, längst von der Verwendung von rein handgeschriebenem Verilog-Code zu der einen oder anderen Art von Generierungsskript entwickelt, sei es ein kleines tcl-Skript oder eine gesamte IDE mit einer Reihe von Funktionen.
Meißel wiederum ist die logische Entwicklung von Sprachen für die Entwicklung und Erprobung digitaler Logik. Nehmen wir an, dass er zu diesem Zeitpunkt alles andere als perfekt ist, aber bereits Möglichkeiten bietet, für die Sie seine Mängel in Kauf nehmen können. , .