Chisel - (pas tout à fait) une nouvelle approche du développement de la logique numérique


Avec le développement de la microélectronique, les conceptions RTL sont devenues de plus en plus nombreuses. La réutilisation du code Verilog est très gênante, même avec les puces Generate, Macros et System Verilog. Chisel, cependant, permet d'appliquer toute la puissance de la programmation objet et fonctionnelle au développement rtl, qui est une étape attendue depuis longtemps qui peut remplir les poumons des développeurs ASIC et FPGA d'air frais.


Cet article donnera un bref aperçu des fonctionnalités principales et considérera certains cas d'utilisation de l'utilisateur, nous parlerons également des lacunes de cette langue. À l'avenir, si le sujet est intéressant, nous continuons l'article dans des tutoriels plus détaillés.


Configuration requise


  • niveau de base scala
  • verilog et les principes de base de la conception numérique.
  • garder la documentation de burin à portée de main

Je vais essayer de comprendre les bases du burin à l'aide d'exemples simples, mais si quelque chose n'est pas clair, vous pouvez jeter un œil ici .


Quant à la scala, cette feuille de triche peut aider pour une plongée rapide.


Il en existe un similaire pour le burin .


Le code d'article complet (sous la forme d'un projet scala sbt) peut être trouvé ici .


Compteur simple


Comme son nom l'indique, le ciseau 'Constructing Hardware In a scala Embedded Language' est un langage de description du matériel construit sur scala.


Brièvement sur la façon dont tout fonctionne, alors: un graphique matériel est construit à partir de la description rtl sur ciseau, qui, à son tour, se transforme en une description intermédiaire dans le langage firrtl, puis l'interpréteur backend intégré est généré à partir de firrtl verilog.


Regardons deux implémentations d'un simple compteur.


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 

ciseau:


 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 } } 

Un peu de burin:


  • Module - conteneur pour la description du module rtl
  • Bundle est une structure de données en ciseau, principalement utilisée pour définir des interfaces.
  • io - variable pour déterminer les ports
  • Bool - type de données, signal simple sur un seul bit
  • UInt(width: Width) - entier non signé, le constructeur accepte la profondeur de bits du signal en entrée.
  • RegInit[T <: Data](init: T) est un constructeur de registre; il prend une valeur de réinitialisation à l'entrée et a le même type de données.
  • <> - opérateur de connexion de signal universel
  • when(cond: => Bool) { /*...*/ } - analogique if un Verilog

Nous parlerons de quel verilog génère un burin un peu plus tard. Comparez maintenant ces deux modèles. Comme vous pouvez le voir, il n'y a aucune mention des signaux clk et reset dans le burin. Le fait est que le burin ajoute ces signaux au module par défaut. La valeur de réinitialisation du registre de counter est RegInit constructeur de registre avec la réinitialisation RegInit . Chisel prend en charge les modules avec de nombreux signaux d'horloge, mais à ce sujet un peu plus tard.


Le compteur est un peu plus compliqué


Allons de l'avant et compliquons un peu la tâche, par exemple - nous allons créer un compteur multicanal avec un paramètre d'entrée sous la forme d'une séquence de bits pour chaque canal.


Commençons maintenant avec la version burin


 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) } } 

Un peu sur la scala:


  • width: Seq[Int] - paramètre d'entrée pour le constructeur de la classe MultiChannelCounter , a le type Seq[Int] - une séquence avec des éléments entiers.
  • Seq est l'un des types de collections dans scala avec une séquence d'éléments bien définie.
  • .map est une fonction familière pour les collections pour tout le monde, capable de convertir une collection en une autre en raison de la même opération sur chaque élément, dans notre cas, une séquence de valeurs entières se transforme en une séquence de SimpleCounter avec la profondeur de bits correspondante.

Un peu de burin:


  • Vec[T <: Data](gen: T, n: Int): Vec[T] - type de données burin, est un analogue du tableau.
  • Module[T <: BaseModule](bc: => T): T est la méthode d'encapsulation requise pour les modules instanciables.
  • util.Cat[T <: Bits](r: Seq[T]): UInt - fonction de concaténation, analogique {1'b1, 2'b01, 4'h0} dans verilog

Faites attention aux ports:
enable - déjà déployé dans Vec[Bool] *, en gros, dans un tableau de signaux à un bit, un pour chaque canal, il était possible de faire UInt(width.length.W) .
out - il est étendu à une largeur totale de tous nos canaux.


Les counters variables sont un tableau de nos compteurs. Nous connectons le signal d' enable de chaque compteur au port d'entrée correspondant, et combinons tous out signaux de out en un à l'aide de la fonction util.Cat intégrée et le transmettons à la sortie.


Nous notons également la fonction getOut(i: Int) - cette fonction calcule et retourne la plage de bits dans le signal de out pour le i canal. Il sera très utile pour poursuivre les travaux avec un tel compteur. L'implémentation de quelque chose comme ça dans verilog ne fonctionnera pas


* Vec ne doit pas être confondu avec Vector , le premier est un tableau de données en ciseau, le second est une collection en scala.


Essayons maintenant d'écrire ce module sur verilog, pour plus de commodité, même sur systemVerilog.


Après avoir réfléchi, je suis arrivé à cette option (très probablement ce n'est pas la seule vraie et la plus optimale, mais vous pouvez toujours suggérer votre mise en œuvre dans les commentaires).


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 

Cela semble déjà beaucoup plus impressionnant. Mais que se passe-t-il si, nous allons plus loin et vis sur l'interface populaire de wishbone avec accès au registre.


Interfaces groupées


Wishbone est un petit bus similaire à AMBA APB, principalement utilisé pour les cœurs IP open source.


Plus de détails sur le wiki: https://ru.wikipedia.org/wiki/Wishbone


Parce que ciseau nous donne un type de données de conteneur Bundle est logique pour envelopper le bus dans un tel récipient, qui peut ensuite être utilisé dans des projets sur le ciseau.


 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] } 

Un peu sur la scala:


  • Option - un wrapper de données facultatif dans scala qui peut être un élément ou None , Option[UInt] est Some(UInt(/*...*/)) ou None , utile lors du paramétrage des signaux.

Cela ne semble rien d'inhabituel. Juste une description de l'interface par l'assistant, à l'exception de quelques signaux et méthodes:


tag_master et tag_slave sont des signaux universels facultatifs dans le protocole wishbone, nous les verrons si le paramètre gotTag est true .


wbTransaction , wbWrite , wbRead - fonctions pour simplifier le travail avec le bus.


cloneType - méthode de clonage de type requise pour toutes les classes [T <: Bundle] paramétrées


Mais nous avons également besoin d'une interface esclave, voyons comment elle peut être implémentée.


 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] } 

La méthode Flipped , comme vous pouvez le deviner d'après le nom, retourne l'interface, et maintenant notre interface d'assistant est devenue un esclave, nous ajoutons la même classe pour l'assistant.


 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] } 

Eh bien, c'est tout, l'interface est prête. Mais avant d'écrire un gestionnaire, voyons comment nous pouvons utiliser ces interfaces au cas où nous aurions besoin de faire un changement ou quelque chose avec un grand ensemble d'interfaces de triangulation.


 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) // ... } 

Ceci est un petit blanc pour le commutateur. Il est pratique de déclarer une interface de type Vec[wishboneSlave] , et vous pouvez connecter les interfaces avec le même opérateur <> . Puces de burin utiles pour gérer un large ensemble de signaux.


Contrôleur de bus universel


Comme mentionné précédemment sur la puissance de la programmation fonctionnelle et objet, nous allons essayer de l'appliquer. Plus loin, nous parlerons de la mise en œuvre du contrôleur de bus universel à triangulation sous forme de trait , il s'agira d'une sorte de mixin pour tout module avec le bus wishboneSlave , pour le module dont vous avez juste besoin de définir une carte mémoire et de lui mélanger le contrôleur de trait pendant la génération.


Implémentation


Pour ceux qui sont toujours enthousiastes

Passons à l'implémentation du gestionnaire. Il sera simple et répondra immédiatement aux transactions uniques, en cas de chute hors du pool d'adresses, retournez zéro.


Analysons en plusieurs parties:


  • chaque transaction doit recevoir une réponse avec accusé de réception


     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 

  • Nous répondons à la lecture avec des données
     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 est le schéma de coordination intégré du type de case dans verilog *.

À quoi cela ressemblerait-il dans verilog:


  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 } 

* En général, dans ce cas, il s'agit d'un petit hack pour le paramétrage, en ciseau il y a une conception standard qui est préférable d'utiliser si, écrivez quelque chose de plus simple.


 switch(x) { is(value1) { // ... } is(value2) { // ... } } 

Eh bien, le record


  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 - multiplexeur régulier

Nous intégrons quelque chose de similaire à notre compteur multicanal, raccrochons des registres pour la gestion des canaux et un chapeau. Mais ici, c'est à portée de main du contrôleur de bus universel WB vers lequel nous allons transférer une carte mémoire de ce type:


  val readMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 /*...*/ ) val writeMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 /*...*/ ) 

Pour une telle tâche, le trait nous aidera - quelque chose comme les mixins à Sala. La tâche principale sera de faire en sorte que readMemMap: [Int, Data] ressemble à Seq( -> ) , et ce serait bien aussi si vous pouviez transférer l'adresse de base et le tableau de données à l'intérieur de la carte mémoire


  val readMemMap = Map( ADDR_1_BASE -> DATA_SEQ, ADDR_2 -> DATA_2 /*...*/ ) 

Ce qui sera développé avec quelque chose de similaire, où WB_DAT_WIDTH est la largeur des données en octets


  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 /*...*/ ) 

Pour implémenter cela, nous écrivons une fonction de conversion de Map[Int, Any] à Seq[(Bool, UInt)] . Vous devez utiliser le calcul du modèle scala.


  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 

Enfin, notre trait ressemblera à ceci:


 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)] = { /*...*/} } 

Un peu sur la scala:


  • io , readMemMap, writeMemMap sont les champs abstraits de notre trait 'a, qui doivent être définis dans la classe dans laquelle nous le mélangerons.

Comment l'utiliser


Afin de mélanger notre trait avec le module, plusieurs conditions doivent être remplies:


  • io devrait hériter de la classe wishboneSlave
  • besoin de déclarer deux cartes mémoire readMemMap et 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 ) } 

Nous créons le registre softwareEnable , il est ajouté à 'et' par le signal d'entrée hardwareEnable et va activer le counter[MultiChannelCounter] .


Nous déclarons deux cartes mémoire pour la lecture et l'écriture: readMemMap writeMemMap , pour plus de détails sur la structure, voir le chapitre ci-dessus.
Dans la carte mémoire de lecture, nous transférons la valeur du compteur de chaque canal *, softwareEnable et hardwareEnable . Et pour mémoire, nous ne donnons que le registre softwareEnable .


* width.indices.map(counter.io.getOut) - une conception étrange, analyse fragmentaire.


  • width.indices - retournera un tableau avec des indices d'éléments, c'est-à-dire si width.length == 4 alors width.indices = {0, 1, 2, 3}
  • {0, 1, 2, 3}.map(counter.io.getOut) - donne quelque chose comme ceci:
    { counter.io.getOut(0), counter.io.getOut(1), /*...*/ }

Maintenant, pour tout module sur burin avec, nous pouvons déclarer des cartes mémoire pour la lecture et l'écriture et connecter simplement notre contrôleur de bus universel lors de la génération, quelque chose comme ceci:


 class wishbone_multicahnnel_counter extends WishboneMultiChannelCounter with wishboneSlaveDriver object countersDriver extends App { Driver.execute(Array("-td", "./src/generated"), () => new wishbone_multicahnnel_counter ) } 

wishboneSlaveDriver - c'est exactement le mélange de traits que nous avons décrit sous le spoiler.


Bien sûr, cette version du contrôleur universel est loin d'être définitive, mais plutôt grossière au contraire. Son objectif principal est de démontrer l'une des approches possibles pour développer rtl sur ciseau. Avec toutes les capacités de scala, de telles approches peuvent être beaucoup plus vastes, donc chaque développeur a son propre champ de créativité. Certes, il n'y a nulle part où s'inspirer particulièrement, sauf:


  • la bibliothèque native des burins, dont un peu plus loin, vous pouvez voir l'héritage des modules et des interfaces
  • https://github.com/freechipsproject/rocket-chip - risc-v tout le noyau est implémenté sur ciseau, à condition que vous connaissiez très bien scala, pour les débutants sans un demi-litre, comme on dit, il vous faudra beaucoup de temps pour comprendre. il n'y a pas de documentation officielle sur la structure interne du projet.

MultiClockDomain


Et si nous voulons contrôler manuellement l'horloge et réinitialiser les signaux au ciseau. Jusqu'à récemment, cela ne pouvait pas être fait, mais avec l'une des dernières versions, le support withClock {} , withReset {} et withClockAndReset {} . Regardons un exemple:


 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 - un registre qui sera cadencé par le signal d' clock standard et réinitialisé par la reset standard
  • regClockB - ce même registre est cadencé comme vous l' aurez deviné le signal io.clockB , mais remise à zéro est utilisé une norme.

Si nous voulons supprimer complètement l' clock standard et reset signaux, nous pouvons utiliser la fonctionnalité expérimentale - RawModule (module sans horloge standard et signaux de réinitialisation, tout le monde devra être contrôlé manuellement). Un exemple:


 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 } 

Bibliothèque d'utils


Les bonus agréables du ciseau ne s'arrêtent pas là. Ses créateurs ont travaillé dur et ont écrit une petite mais très utile bibliothèque de petites interfaces, modules, fonctions. Curieusement, il n'y a pas de description de la bibliothèque sur le wiki, mais vous pouvez voir le lien de la feuille de triche vers lequel au tout début (il y a deux dernières sections)


Interfaces:


  • DecoupledIO est l'interface prête / valide couramment utilisée.
    DecoupledIO(UInt(32.W)) - contiendra des signaux:
    val ready = Input(Bool())
    val valid = Output(Bool())
    val data = Output(UInt(32.W))
  • ValidIO - identique à DecoupledIO uniquement sans ready

Modules:


  • Queue - le module FIFO synchrone est une chose très utile. L'interface ressemble à
    val enq: DecoupledIO[T] - DecoupledIO inversé
    val deq: DecoupledIO[T] - DecoupledIO régulier
    val count: UInt - quantité de données dans la file d'attente
  • Pipe - module de retard, insère le nième nombre de tranches de registre
  • Arbiter - arbitre sur les interfaces DecoupledIO , a de nombreuses sous-espèces différentes dans le type d'arbitrage
    val in: Vec[DecoupledIO[T]] - tableau d'interfaces d'entrée
    val out: DecoupledIO[T]
    val chosen: UInt - affiche le canal sélectionné

Autant que vous puissiez comprendre de la discussion sur github - dans les plans globaux, il y a une extension significative de cette bibliothèque de modules: tels que FIFO asynchrone, LSFSR, diviseurs de fréquence, modèles PLL pour FPGA; diverses interfaces; contrôleurs pour eux et bien plus encore.


Io-testeurs de burin


La possibilité de tester au burin doit être mentionnée, pour le moment il y a deux façons de tester ceci:


  • peekPokeTesters - des tests de simulation purement qui testent la logique de votre conception
  • hardwareIOTeseters est déjà plus intéressant car avec cette approche, vous obtiendrez un banc de test généré avec des tests que vous avez écrits sur un burin, et même si vous avez un vérificateur, vous obtiendrez même une chronologie.


    Mais jusqu'à présent, l'approche du test n'a pas été finalisée et la discussion est toujours en cours. À l'avenir, un outil universel apparaîtra probablement, pour les tests et les tests, il sera également possible d'écrire sur un burin. Mais pour l'instant, vous pouvez regarder ce qui existe déjà et comment l' utiliser ici .



Inconvénients du ciseau


Cela ne veut pas dire que le burin est un outil universel et que tout le monde devrait y passer. Lui, comme peut-être tous les projets en phase de développement, a ses inconvénients, qui méritent d'être mentionnés par souci d'exhaustivité.


Le premier inconvénient, et peut-être le plus important, est le manque de vidages asynchrones. Assez lourd, mais il peut être résolu de plusieurs façons, et l'un d'eux est des scripts au-dessus de verilog, qui transforment la réinitialisation synchrone en asynchrone. C'est facile à faire car toutes les constructions dans le verilog généré avec always assez uniformes.


Le deuxième inconvénient, selon beaucoup, est l'illisibilité du verilog généré et, par conséquent, la complication du débogage. Mais regardons le code généré à partir de l'exemple avec un simple compteur


Verilog généré
 `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 

À première vue, le verilog généré peut repousser, même dans une conception de taille moyenne, mais jetons un coup d'œil.


  • RANDOMIZE définit - (peut être utile lors de tests avec des burineurs) - sont généralement inutiles, mais ils n'interfèrent pas particulièrement
  • Comme nous voyons le nom de nos ports, et le registre est conservé
  • _GEN_0 est une variable inutile pour nous, mais nécessaire pour que firrtl à l'interpréteur génère du verilog. Nous n'y prêtons pas non plus attention.
  • Il reste _T_7 et _T_8, toute la logique combinatoire du verilog généré sera présentée pas à pas sous forme de variables _T.

Plus important encore, tous les ports, registres et câbles nécessaires au débogage gardent leurs noms à l'écart. Et si vous regardez non seulement le verilog mais aussi le ciseau, le processus de débogage sera bientôt aussi simple qu'avec le verilog pur.


Conclusion


Dans les réalités modernes, le développement de RTL, qu'il s'agisse d'asic ou de fpga en dehors de l'environnement académique, est depuis longtemps passé de l'utilisation de code verilog pur à la main à l'un ou l'autre type de script de génération, qu'il s'agisse d'un petit script tcl ou d'un IDE entier avec un tas de fonctionnalités.


Chisel, à son tour, est le développement logique des langages pour le développement et le test de la logique numérique. Supposons qu'à ce stade, il soit loin d'être parfait, mais déjà en mesure de fournir des opportunités pour lesquelles vous pouvez accepter ses lacunes. , .

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


All Articles