
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 rtlBundle
est une structure de données en ciseau, principalement utilisée pour définir des interfaces.io
- variable pour déterminer les portsBool
- type de données, signal simple sur un seul bitUInt(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 universelwhen(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 enthousiastesPassons à 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))
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) {
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
standardregClockB
- 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'attentePipe
- module de retard, insère le nième nombre de tranches de registreArbiter
- 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 conceptionhardwareIOTeseters
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. , .