Meißel - (nicht ganz) ein neuer Ansatz zur Entwicklung der digitalen Logik


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-Moduls
  • Bundle ist eine Datenstruktur in Meißel, die hauptsächlich zum Definieren von Schnittstellen verwendet wird.
  • io - Variable zur Bestimmung von Ports
  • Bool - Datentyp, einfaches Einzelbit-Signal
  • UInt(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 Signalverbindungsoperator
  • when(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 sind

Fahren 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)) // getWidth   when(io.wb.wbRead) { wb_dat := MuxCase(default = 0.U, Seq( (io.wb.addr === ADDR_1) -> data_1, (io.wb.addr === ADDR_3) -> data_2, (io.wb.addr === ADDR_3) -> data_2 )) } wb_dat <> io.wb.dat_slave 

    • 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) { // ... } is(value2) { // ... } } 

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 Warteschlange
  • Pipe , fügt die n-te Anzahl von Registerscheiben ein
  • Arbiter - 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 testen
  • hardwareIOTeseters 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. , .

Source: https://habr.com/ru/post/de419413/


All Articles