
Com o desenvolvimento da microeletrônica, os projetos de RTL tornaram-se cada vez mais. A reutilização do código verilog é um grande inconveniente, mesmo com gerar, macros e chips de sistema verilog. O Chisel, no entanto, permite aplicar toda a potência da programação de objetos e funcional ao desenvolvimento de rtl, que é uma etapa muito esperada que pode encher os pulmões dos desenvolvedores de ASIC e FPGA com ar fresco.
Este artigo fornecerá uma breve visão geral das principais funcionalidades e considerará alguns casos de uso do usuário. Também falaremos sobre as deficiências desse idioma. No futuro, se o tópico for interessante, continuaremos o artigo em tutoriais mais detalhados.
Requisitos de sistema
- nível de base scala
- verilog e os princípios básicos da construção de projetos digitais.
- mantenha a documentação do cinzel à mão
Vou tentar entender o básico do cinzel usando exemplos simples, mas se algo não estiver claro, você pode dar uma olhada aqui .
Quanto à scala, esta folha de dicas pode ajudar a dar um mergulho rápido.
Existe um semelhante para o cinzel .
O código completo do artigo (na forma de um projeto scala sbt) pode ser encontrado aqui .
Contador simples
Como o nome indica, o cinzel 'Construindo hardware em uma linguagem incorporada da scala' é uma linguagem de descrição de hardware criada sobre a scala.
Resumidamente, sobre como tudo funciona: um gráfico de hardware é construído a partir da descrição rtl no cinzel, que, por sua vez, se transforma em uma descrição intermediária na linguagem firrtl e, depois disso, o interpretador de back-end incorporado é gerado a partir do firrtl verilog.
Vejamos duas implementações de um contador simples.
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
cinzel:
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 } }
Um pouco sobre o cinzel:
Module
- container para descrição do módulo rtlBundle
é uma estrutura de dados no cinzel, usada principalmente para definir interfaces.io
- variável para determinar portasBool
- tipo de dados, sinal simples de bit únicoUInt(width: Width)
- número inteiro sem sinal, o construtor aceita a profundidade do bit do sinal como entrada.RegInit[T <: Data](init: T)
é um construtor de registradores, que recebe um valor de redefinição na entrada e tem o mesmo tipo de dados.<>
- operador de conexão de sinal universalwhen(cond: => Bool) { /*...*/ }
- o if
analógico no verilog
Falaremos sobre qual verilog gera formão um pouco mais tarde. Agora basta comparar esses dois modelos. Como você pode ver, não há menção de sinais clk
e reset
no cinzel. O fato é que o cinzel adiciona esses sinais ao módulo por padrão. O valor de redefinição para o registrador do counter
é RegInit
construtor RegInit
registrador com a redefinição do RegInit
. O cinzel tem suporte para módulos com muitos sinais de clock, mas um pouco mais tarde.
O balcão é um pouco mais complicado
Vamos prosseguir e complicar um pouco a tarefa, por exemplo - criaremos um contador multicanal com um parâmetro de entrada na forma de uma sequência de bits para cada canal.
Vamos começar agora com a versão do cinzel
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) } }
Um pouco sobre scala:
width: Seq[Int]
- parâmetro de entrada para o construtor da classe MultiChannelCounter
, possui o tipo Seq[Int]
- uma sequência com elementos inteiros.Seq
é um dos tipos de coleções em scala com uma sequência bem definida de elementos..map
é uma função familiar para coleções para todos, capaz de converter uma coleção em outra devido à mesma operação em cada elemento. No nosso caso, uma sequência de valores inteiros se transforma em uma sequência de SimpleCounter
com a profundidade de bits correspondente.
Um pouco sobre o cinzel:
Vec[T <: Data](gen: T, n: Int): Vec[T]
- tipo de dados cinzel é um análogo da matriz.Module[T <: BaseModule](bc: => T): T
é o método de invólucro necessário para módulos instanciados.util.Cat[T <: Bits](r: Seq[T]): UInt
- função de concatenação, analógico {1'b1, 2'b01, 4'h0}
no verilog
Preste atenção às portas:
enable
- implantado já no Vec[Bool]
*, grosso modo, em uma matriz de sinais de um bit, um para cada canal, foi possível criar UInt(width.length.W)
.
out
- expandido para a soma das larguras de todos os nossos canais.
Os counters
variáveis são uma matriz dos nossos contadores. Conectamos o sinal de enable
de cada contador à porta de entrada correspondente e combinamos todos out
sinais de out
em um usando a função util.Cat
incorporada e a encaminhamos para a saída.
Também observamos a função getOut(i: Int)
- essa função calcula e retorna o intervalo de bits no sinal de out
para o i
- i
canal. Será muito útil em trabalhos futuros com esse contador. Implementar algo assim no verilog não funcionará
* Vec
não deve ser confundido com Vector
, o primeiro é uma matriz de dados no cinzel, o segundo é uma coleção no scala.
Vamos agora tentar escrever este módulo no verilog, por conveniência, mesmo no systemVerilog.
Depois de pensar, cheguei a essa opção (provavelmente não é a única verdadeira e a mais ideal, mas você sempre pode sugerir sua implementação nos comentários).
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
Já parece muito mais impressionante. Mas e se formos mais longe e mexer na popular interface wishbone com acesso a registros.
Interfaces de pacotes
O Wishbone é um pequeno barramento semelhante ao AMBA APB, usado principalmente para núcleos IP de código aberto.
Mais detalhes no wiki: https://ru.wikipedia.org/wiki/Wishbone
Porque O cinzel nos fornece contêineres de dados do tipo Bundle
; faz sentido agrupar o barramento em um contêiner que possa ser usado posteriormente em qualquer projeto de cinzel.
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] }
Um pouco sobre scala:
Option
- um invólucro de dados opcional no scala que pode ser um elemento ou None
, Option[UInt]
é Some(UInt(/*...*/))
ou None
, útil ao parametrizar sinais.
Parece nada de anormal. Apenas uma descrição da interface pelo assistente, com exceção de alguns sinais e métodos:
tag_master
e tag_slave
são sinais opcionais de uso geral no protocolo wishbone, vamos vê-los se o parâmetro gotTag
for true
.
wbTransaction
, wbWrite
, wbRead
- funciona para simplificar o trabalho com o barramento.
cloneType
- método de clonagem de tipo necessário para todas as classes [T <: Bundle]
parametrizadas
Mas também precisamos de uma interface escrava, vamos ver como ela pode ser implementada.
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] }
O método Flipped
, como você pode imaginar pelo nome, inverte a interface e agora nossa interface do assistente se transformou em escrava, adicionamos a mesma classe ao assistente.
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] }
Bem, é isso, a interface está pronta. Mas antes de escrever um manipulador, vamos ver como podemos usar essas interfaces, caso precisemos fazer uma troca ou algo com um grande conjunto de interfaces de wishbone.
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)
Este é um pequeno espaço em branco para o comutador. É conveniente declarar uma interface do tipo Vec[wishboneSlave]
e você pode conectar as interfaces com o mesmo operador <>
. Chisel chips úteis quando se trata de gerenciar um grande conjunto de sinais.
Controlador de barramento universal
Como mencionado anteriormente sobre o poder da programação funcional e de objetos, tentaremos aplicá-lo. Além disso, falaremos sobre a implementação do controlador de barramento universal wishbone na forma de trait
, será algum tipo de combinação para qualquer módulo com o barramento wishboneSlave
, para o módulo, você só precisa definir um cartão de memória e misturar o controlador de trait
durante a geração.
Implementação
Para quem ainda está entusiasmadoVamos seguir para a implementação do manipulador. Será simples e responderá imediatamente a transações únicas; em caso de queda do pool de endereços, retorne zero.
Vamos analisar em partes:
cada transação precisa ser respondida com reconhecimento
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
- Respondemos à leitura com dados
val wb_dat = RegInit(0.U(io.wb.dat_slave.getWidth.W))
MuxCase[T <: Data] (default: T, mapping: Seq[(Bool, T)]): T
é o esquema de coordenação interno do tipo de case
no verilog *.
Como seria no 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 }
* Em geral, neste caso, este é um pequeno truque para parametrizar, no cinzel, existe um design padrão que é melhor usar se, escreva algo mais simples.
switch(x) { is(value1) {
Bem, o registro
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
- multiplexador regular
Incorporamos algo semelhante ao nosso contador multicanal, desligamos os registros para gerenciamento de canais e um chapéu. Mas aqui está próximo do controlador de barramento WB universal para o qual transferiremos um cartão de memória desse tipo:
val readMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 ) val writeMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 )
Para tal tarefa, a trait
nos ajudará - algo como mixins em Sala. A principal tarefa será fazer com que o readMemMap: [Int, Data]
pareça com Seq( -> )
, e também seria bom se você pudesse transferir o endereço base e a matriz de dados para dentro do cartão de memória
val readMemMap = Map( ADDR_1_BASE -> DATA_SEQ, ADDR_2 -> DATA_2 )
O que será expandido para algo semelhante, onde WB_DAT_WIDTH é a largura dos dados em bytes
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 )
Para implementar isso, escrevemos uma função de conversão de Map[Int, Any]
para Seq[(Bool, UInt)]
. Você precisa usar o padrão matemático de 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
Finalmente, nossa característica ficará assim:
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)] = { } }
Um pouco sobre scala:
io , readMemMap, writeMemMap
são os campos abstratos de nossa trait
'a, que devem ser definidos na classe na qual a misturaremos.
Como usá-lo
Para misturar nossa trait
com o módulo, várias condições devem ser atendidas:
io
deve herdar da classe wishboneSlave
- precisa declarar dois cartões de memória como
readMemMap
e 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 ) }
Nós criamos o registro softwareEnable
, ele é adicionado a 'e' pelo sinal de entrada hardwareEnable
e vai ativar o counter[MultiChannelCounter]
.
Declaramos dois cartões de memória para leitura e gravação: readMemMap
writeMemMap
, para obter mais detalhes sobre a estrutura, consulte o capítulo acima.
No cartão de memória de leitura, transferimos o valor do contador de cada canal *, softwareEnable
e hardwareEnable
. E para o registro, damos apenas o registro softwareEnable
.
* width.indices.map(counter.io.getOut)
- um design estranho, vamos analisá-lo em partes.
width.indices
- retornará uma matriz com índices de elementos, ou seja, se width.length == 4
, width.indices = {0, 1, 2, 3}
{0, 1, 2, 3}.map(counter.io.getOut)
- fornece algo parecido com isto:
{ counter.io.getOut(0), counter.io.getOut(1), /*...*/ }
Agora, para qualquer módulo no cinzel, podemos declarar cartões de memória para leitura e gravação e simplesmente conectar nosso controlador de barramento universal wishbone ao gerar, algo como isto:
class wishbone_multicahnnel_counter extends WishboneMultiChannelCounter with wishboneSlaveDriver object countersDriver extends App { Driver.execute(Array("-td", "./src/generated"), () => new wishbone_multicahnnel_counter ) }
wishboneSlaveDriver
- esse é exatamente o mix de características que descrevemos no spoiler.
Obviamente, esta versão do controlador universal está longe de ser final, mas é grosseira pelo contrário. Seu principal objetivo é demonstrar uma das abordagens possíveis para o desenvolvimento de rtl no cinzel. Com todos os recursos do scala, essas abordagens podem ser muito maiores, para que cada desenvolvedor tenha seu próprio campo de criatividade. Verdade, especialmente em nenhum lugar para ser especialmente inspirado, exceto:
- a biblioteca de utilitários de cinzel nativa, sobre a qual um pouco mais adiante, você pode ver a herança de módulos e interfaces
- https://github.com/freechipsproject/rocket-chip - o kernel do risc-v é totalmente implementado no cinzel, desde que você conheça muito bem o scala, para iniciantes sem meio litro, como eles dizem, levará muito tempo para entender. não há documentação oficial sobre a estrutura interna do projeto.
MultiClockDomain
E se quisermos controlar manualmente o relógio e redefinir os sinais no cinzel. Até recentemente, isso não podia ser feito, mas com uma das versões mais recentes, o suporte withClock {}
, withReset {}
e withClockAndReset {}
. Vejamos um exemplo:
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
- um registro que será cronometrado pelo sinal de clock
padrão e redefinido pelo reset
padrãoregClockB
- o mesmo registro é cronometrado, você adivinhou, pelo sinal io.clockB
, mas a redefinição padrão será usada.
Se queremos remover completamente o clock
padrão e reset
sinais, podemos usar o recurso experimental - RawModule
(módulo sem sinais padrão de relógio e redefinição, todos terão que ser controlados manualmente). Um exemplo:
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 }
Biblioteca Utils
Os agradáveis bônus do cinzel não param por aí. Seus criadores trabalharam duro e escreveram uma biblioteca pequena, mas muito útil, de pequenas interfaces, módulos, funções. Curiosamente, não há descrição da biblioteca no wiki, mas você pode ver o link da folha de dicas para o qual, no início (há duas últimas seções)
Interfaces:
DecoupledIO
é a interface pronta / válida comumente usada.
DecoupledIO(UInt(32.W))
- conterá sinais:
val ready = Input(Bool())
val valid = Output(Bool())
val data = Output(UInt(32.W))
ValidIO
- o mesmo que o DecoupledIO
apenas sem ready
Módulos:
Queue
- o módulo FIFO síncrono é uma coisa muito útil.A interface parece
val enq: DecoupledIO[T]
- DecoupledIO
invertido
val deq: DecoupledIO[T]
- DecoupledIO
regular
val count: UInt
- quantidade de dados na fila- Módulo
Pipe
- delay, insere o enésimo número de fatias de registro Arbiter
- árbitro nas interfaces DecoupledIO
, tem muitas subespécies diferentes no tipo de arbitragem
val in: Vec[DecoupledIO[T]]
- conjunto de interfaces de entrada
val out: DecoupledIO[T]
val chosen: UInt
- mostra o canal selecionado
Até onde você pode entender da discussão sobre o github - nos planos globais, há uma extensão significativa dessa biblioteca de módulos: como FIFO assíncrono, LSFSR, divisores de freqüência, modelos de PLL para FPGA; várias interfaces; controladores para eles e muito mais.
Cinzel io-teseters
A possibilidade de testar no cinzel deve ser mencionada, no momento existem duas maneiras de testar isso:
peekPokeTesters
- testes puramente de simulação que testam a lógica do seu designhardwareIOTeseters
já é mais interessante, pois com essa abordagem, você obterá um banco de tesets gerado com testes que você escreveu no cinzel e, mesmo se você tiver verilator, receberá uma linha do tempo.
Mas até agora, a abordagem para o teste não foi finalizada e a discussão ainda está em andamento. No futuro, provavelmente uma ferramenta universal aparecerá, para testes e testes também será possível escrever no cinzel. Mas, por enquanto, você pode ver o que já está lá e como usá-lo aqui .
Desvantagens do cinzel
Isso não quer dizer que o cinzel é uma ferramenta universal e que todos devem mudar para ele. Ele, como talvez todos os projetos na fase de desenvolvimento, tem suas desvantagens, que vale a pena mencionar para completar o quadro.
A primeira e talvez mais importante desvantagem é a falta de descargas assíncronas. Pesado o suficiente, mas pode ser resolvido de várias maneiras, e um deles é o script no topo do verilog, que transforma a redefinição síncrona em assíncrona. Isso é fácil de fazer porque todas as construções no verilog gerado com always
bastante uniformes.
A segunda desvantagem, segundo muitos, é a ilegibilidade do verilog gerado e, como conseqüência, a complicação da depuração. Mas vamos dar uma olhada no código gerado a partir do exemplo com um contador simples
verilog gerado `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
À primeira vista, o verilog gerado pode se afastar, mesmo em um design de tamanho médio, mas vamos dar uma olhada.
- RANDOMIZE define - (pode ser útil ao testar com cinzéis) - geralmente são inúteis, mas não interferem particularmente
- Como vemos o nome de nossas portas, e o registro é preservado
- _GEN_0 é uma variável inútil para nós, mas necessária para que o firrtl ao intérprete gere o verilog. Também não prestamos atenção a isso.
- Restam _T_7 e _T_8, toda a lógica combinatória no verilog gerado será apresentada passo a passo na forma das variáveis _T.
Mais importante, todas as portas, registradores e fios necessários para depuração mantêm seus nomes do cinzel. E se você olhar não apenas para o verilog, mas também para o cinzel, em breve o processo de depuração será tão fácil quanto o verilog puro.
Conclusão
Nas realidades modernas, o desenvolvimento de RTL, seja asic ou fpga fora do ambiente acadêmico, deixou de usar apenas código verilog manuscrito puro para um ou outro tipo de script de geração, seja um pequeno script tcl ou um IDE inteiro com vários recursos.
Cinzelar, por sua vez, é o desenvolvimento lógico de linguagens para o desenvolvimento e teste da lógica digital. Suponha que, nesta fase, ele esteja longe de ser perfeito, mas já seja capaz de oferecer oportunidades para as quais você pode aturar suas deficiências. , .