Cincel - (no del todo) un nuevo enfoque para el desarrollo de la lógica digital


Con el desarrollo de la microelectrónica, los diseños RTL se han vuelto cada vez más. La reutilización del código Verilog es una gran molestia, incluso con los chips Verilog Generate, Macros y System. Sin embargo, Chisel permite aplicar toda la potencia de la programación funcional y de objetos al desarrollo rtl, que es un paso muy esperado que puede llenar los pulmones de los desarrolladores de ASIC y FPGA con aire fresco.


Este artículo brindará una breve descripción de la funcionalidad principal y considerará algunos casos de uso de los usuarios, también hablaremos sobre las deficiencias de este lenguaje. En el futuro, si el tema es interesante, continuamos el artículo en tutoriales más detallados.


Requisitos del sistema


  • nivel base scala
  • Verilog y los principios básicos de la construcción de diseños digitales.
  • tenga a mano la documentación del cincel

Trataré de comprender los conceptos básicos del cincel utilizando ejemplos simples, pero si algo no está claro, puede echar un vistazo aquí .


En cuanto a scala, esta hoja de trucos puede ayudar para una inmersión rápida.


Hay uno similar para cincel .


El código completo del artículo (en forma de proyecto scala sbt) se puede encontrar aquí .


Contador simple


Como su nombre lo indica, el cincel 'Construyendo hardware en un lenguaje embebido scala' es un lenguaje de descripción de hardware construido sobre scala.


Brevemente sobre cómo funciona todo, entonces: se construye un gráfico de hardware a partir de la descripción rtl en chisel, que, a su vez, se convierte en una descripción intermedia en el primer idioma, y ​​después de eso, el intérprete de fondo incorporado se genera a partir de firrtl verilog.


Veamos dos implementaciones de un contador simple.


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 

cincel


 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 poco sobre cincel:


  • Module - contenedor para la descripción del módulo rtl
  • Bundle es una estructura de datos en cincel, utilizada principalmente para definir interfaces.
  • io : variable para determinar puertos
  • Bool - tipo de datos, señal simple de un solo bit
  • UInt(width: Width) : entero sin signo, el constructor acepta la profundidad de bits de la señal como entrada.
  • RegInit[T <: Data](init: T) es un constructor de registros; toma un valor de reinicio en la entrada y tiene el mismo tipo de datos.
  • <> - operador de conexión de señal universal
  • when(cond: => Bool) { /*...*/ } - el if analógico en verilog

Hablaremos sobre qué verilog genera un cincel un poco más tarde. Ahora solo compara estos dos diseños. Como puede ver, no se mencionan las señales clk y reset en el cincel. El hecho es que el cincel agrega estas señales al módulo por defecto. El valor de reinicio para el registro del counter se RegInit constructor RegInit registro con el reinicio RegInit . Cincel tiene soporte para módulos con muchas señales de reloj, pero un poco más tarde.


El contador es un poco más complicado.


Vamos más allá y complicamos un poco la tarea, por ejemplo: haremos un contador multicanal con un parámetro de entrada en forma de una secuencia de bits para cada canal.


Comencemos ahora con la versión cincel


 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 poco sobre scala:


  • width: Seq[Int] - parámetro de entrada para el constructor de la clase MultiChannelCounter , tiene el tipo Seq[Int] - una secuencia con elementos enteros.
  • Seq es uno de los tipos de colecciones en scala con una secuencia de elementos bien definida.
  • .map es una función familiar para colecciones para todos, capaz de convertir una colección en otra debido a la misma operación en cada elemento, en nuestro caso una secuencia de valores enteros se convierte en una secuencia de SimpleCounter con la profundidad de bits correspondiente.

Un poco sobre cincel:


  • Vec[T <: Data](gen: T, n: Int): Vec[T] - tipo de datos de cincel, es un análogo de la matriz.
  • Module[T <: BaseModule](bc: => T): T es el método de envoltura requerido para módulos instanciables.
  • util.Cat[T <: Bits](r: Seq[T]): UInt - función de concatenación, análogo {1'b1, 2'b01, 4'h0} en verilog

Presta atención a los puertos:
enable : desplegado ya en Vec[Bool] *, en términos generales, en una matriz de señales de un bit, una para cada canal, fue posible hacer UInt(width.length.W) .
out - expandido a la suma de los anchos de todos nuestros canales.


Los counters variables son una matriz de nuestros contadores. Conectamos la señal de enable de cada contador al puerto de entrada correspondiente, y combinamos todas out señales de out en una utilizando la función incorporada util.Cat y la util.Cat a la salida.


También observamos la función getOut(i: Int) : esta función calcula y devuelve el rango de bits en la señal de out para el i canal. Será muy útil en futuros trabajos con dicho contador. Implementar algo como esto en verilog no funcionará


* Vec no debe confundirse con Vector , el primero es una matriz de datos en cincel, el segundo es una colección en escala.


Ahora intentemos escribir este módulo en verilog, por conveniencia, incluso en systemVerilog.


Después de pensar, llegué a esta opción (lo más probable es que no sea la única verdadera y óptima, pero siempre puede sugerir su implementación en los comentarios).


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 

Ya se ve mucho más impresionante. Pero qué pasa si, vamos más allá y atornillamos la popular interfaz wishbone con acceso de registro.


Interfaces de paquete


Wishbone es un pequeño bus similar a AMBA APB, utilizado principalmente para núcleos de IP de código abierto.


Más detalles en la wiki: https://ru.wikipedia.org/wiki/Wishbone


Porque chisel nos proporciona contenedores de datos del tipo Bundle , tiene sentido envolver el bus en un contenedor que luego pueda usarse en cualquier proyecto de cincel.


 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 poco sobre scala:


  • Option : un contenedor de datos opcional en scala que puede ser un elemento o None , la Option[UInt] es Some(UInt(/*...*/)) o None , útil cuando se parametrizan señales.

No parece nada inusual. Solo una descripción de la interfaz realizada por el asistente, con la excepción de algunas señales y métodos:


tag_master y tag_slave son tag_slave opcionales de propósito general en el protocolo wishbone, las veremos si el parámetro gotTag es true .


wbTransaction , wbWrite , wbRead : funciones para simplificar el trabajo con el bus.


cloneType : método de clonación de tipo requerido para todas las clases parametrizadas [T <: Bundle]


Pero también necesitamos una interfaz esclava, veamos cómo se puede implementar.


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

El método Flipped , como se puede adivinar por el nombre, voltea la interfaz, y ahora nuestra interfaz de asistente se ha convertido en esclava, agregamos la misma clase para el asistente.


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

Bueno, eso es todo, la interfaz está lista. Pero antes de escribir un controlador, veamos cómo podemos usar estas interfaces en caso de que necesitemos hacer un cambio o algo con un gran conjunto de interfaces de espoleta.


 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 es un pequeño espacio en blanco para el interruptor. Es conveniente declarar una interfaz de tipo Vec[wishboneSlave] , y puede conectar las interfaces con el mismo operador <> . Útiles chips de cincel cuando se trata de gestionar un gran conjunto de señales.


Controlador de bus universal


Como se mencionó anteriormente sobre el poder de la programación funcional y de objetos, intentaremos aplicarlo. Además, hablaremos sobre la implementación del controlador universal de bus wishbone en forma de trait , será una especie de mezcla para cualquier módulo con el bus wishboneSlave , para el módulo solo necesita definir una tarjeta de memoria y mezclar el controlador de trait durante la generación.


Implementación


Para aquellos que todavía están entusiasmados.

Pasemos a la implementación del controlador. Será simple e inmediatamente responderá a transacciones individuales, en caso de caerse del grupo de direcciones, devuelva cero.


Analicemos en partes:


  • cada transacción debe ser respondida con acuse de recibo


     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 a la lectura con datos
     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 es el esquema de coordinación incorporado del tipo de case en verilog *.

¿Cómo se vería en 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 general, en este caso, este es un pequeño truco en aras de la parametrización, en Cincel hay un diseño estándar que es mejor usar si, escribe algo más simple.


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

Bueno, el 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 - multiplexor regular

Incorporamos algo similar a nuestro contador multicanal, cuelgamos registros para la gestión de canales y un sombrero. Pero aquí está al alcance del controlador universal de bus WB al que transferiremos una tarjeta de memoria de este 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 tarea, el trait nos ayudará, algo así como los mixins en Sala. La tarea principal será hacer que readMemMap: [Int, Data] vea como Seq( -> ) , y también sería bueno si pudiera transferir la dirección base y la matriz de datos dentro de la tarjeta de memoria


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

Lo que se ampliará con algo similar, donde WB_DAT_WIDTH es el ancho de los datos en 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 esto, escribimos una función de conversión de Map[Int, Any] a Seq[(Bool, UInt)] . Tienes que usar la matemática de patrón de escala.


  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, nuestro rasgo se verá así:


 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 poco sobre scala:


  • io , readMemMap, writeMemMap son los campos abstractos de nuestro trait 'a, que deben definirse en la clase en la que lo mezclaremos.

Como usarlo


Para mezclar nuestro trait con el módulo, se deben cumplir varias condiciones:


  • io debería heredar de la clase wishboneSlave
  • necesita declarar dos tarjetas de memoria readMemMap y 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 ) } 

Creamos el registro softwareEnable , se agrega a 'y' mediante la señal de entrada hardwareEnable y se activa el counter[MultiChannelCounter] .


Declaramos dos tarjetas de memoria para leer y escribir: readMemMap writeMemMap , para obtener más detalles sobre la estructura, consulte el capítulo anterior.
En la tarjeta de memoria de lectura, transferimos el valor del contador de cada canal *, softwareEnable y hardwareEnable . Y para el registro solo damos el registro softwareEnable .


* width.indices.map(counter.io.getOut) - un diseño extraño, lo analizaremos en partes.


  • width.indices : devolverá una matriz con índices de elementos, es decir if width.length == 4 then width.indices = {0, 1, 2, 3}
  • {0, 1, 2, 3}.map(counter.io.getOut) : proporciona algo como esto:
    { counter.io.getOut(0), counter.io.getOut(1), /*...*/ }

Ahora, para cualquier módulo con cincel podemos declarar tarjetas de memoria para leer y escribir y simplemente conectar nuestro controlador universal de bus de espoleta al generar, algo como esto:


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

wishboneSlaveDriver : esta es exactamente la combinación de rasgos que describimos bajo el spoiler.


Por supuesto, esta versión del controlador universal está lejos de ser final, pero por el contrario es tosca. Su objetivo principal es demostrar uno de los posibles enfoques para desarrollar rtl en cincel. Con todas las capacidades de scala, tales enfoques pueden ser mucho más grandes, por lo que cada desarrollador tiene su propio campo de creatividad. Es cierto, no hay ningún lugar para inspirarse especialmente, excepto:


  • la biblioteca nativa de chisel utils, sobre la cual un poco más allá, puede ver la herencia de módulos e interfaces
  • https://github.com/freechipsproject/rocket-chip - risc-v todo el kernel está implementado en cincel, siempre que conozca muy bien scala, para principiantes sin medio litro, como dicen, tomará mucho tiempo entenderlo. No existe documentación oficial sobre la estructura interna del proyecto.

MultiClockDomain


¿Qué pasa si queremos controlar manualmente el reloj y restablecer las señales en el cincel? Hasta hace poco, esto no se podía hacer, pero con una de las últimas versiones, withClock {} soporte con withClock {} , withReset {} y withClockAndReset {} . Veamos un ejemplo:


 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 registro que se sincronizará mediante la señal de clock estándar y se restablecerá mediante el reset estándar
  • regClockB : el mismo registro se sincroniza, lo adivinó, mediante la señal io.clockB , pero se utilizará el restablecimiento estándar.

Si queremos eliminar el clock estándar y reset señales por completo, entonces podemos usar la función experimental: RawModule (módulo sin reloj estándar y señales de restablecimiento, todos tendrán que ser controlados manualmente). Un ejemplo:


 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 de utilidades


Las bonificaciones agradables del cincel no terminan ahí. Sus creadores trabajaron duro y escribieron una pequeña pero muy útil biblioteca de pequeñas interfaces, módulos y funciones. Por extraño que parezca, no hay una descripción de la biblioteca en la wiki, pero puede ver el enlace de la hoja de trucos al que al principio (hay dos últimas secciones)


Interfaces:


  • DecoupledIO es la interfaz lista / válida de uso común.
    DecoupledIO(UInt(32.W)) - contendrá señales:
    val ready = Input(Bool())
    val valid = Output(Bool())
    val data = Output(UInt(32.W))
  • ValidIO : igual que DecoupledIO solo sin ready

Módulos:


  • Queue : el módulo FIFO síncrono es algo muy útil.
    val enq: DecoupledIO[T] - DecoupledIO invertido
    val deq: DecoupledIO[T] - DecoupledIO regular
    val count: UInt - cantidad de datos en la cola
  • Pipe : módulo de retardo, inserta el enésimo número de segmentos de registro
  • Arbiter : árbitro en interfaces DecoupledIO , tiene muchas subespecies que difieren en el tipo de arbitraje
    val in: Vec[DecoupledIO[T]] - conjunto de interfaces de entrada
    val out: DecoupledIO[T]
    val chosen: UInt - muestra el canal seleccionado

Por lo que puede entender de la discusión sobre github, en los planes globales hay una extensión significativa de esta biblioteca de módulos: como FIFO asíncrono, LSFSR, divisores de frecuencia, plantillas PLL para FPGA; varias interfaces; controladores para ellos y mucho más.


Cincel io-teseters


Debe mencionarse la posibilidad de realizar pruebas en cincel, en este momento hay dos formas de probar esto:


  • peekPokeTesters : pruebas de simulación pura que prueban la lógica de su diseño
  • hardwareIOTeseters ya es más interesante desde con este enfoque, obtendrá un banco de pruebas generado con pruebas que escribió en el cincel, e incluso si tiene un verificador, incluso obtendrá una línea de tiempo.


    Pero hasta ahora, el enfoque de las pruebas no se ha finalizado, y la discusión aún está en curso. En el futuro, lo más probable es que aparezca una herramienta universal, para pruebas y pruebas también será posible escribir en cincel. Pero por ahora, puede ver lo que ya está allí y cómo usarlo aquí .



Desventajas de cincel


Esto no quiere decir que el cincel sea una herramienta universal, y que todos deberían cambiar a él. Él, como, tal vez, todos los proyectos en la etapa de desarrollo, tiene sus inconvenientes, que vale la pena mencionar en aras de la integridad.


El primer inconveniente, y quizás el más importante, es la falta de descargas asincrónicas. Suficientemente pesado, pero se puede resolver de varias maneras, y una de ellas es scripts en la parte superior de verilog, que convierten el restablecimiento síncrono en asíncrono. Esto es fácil de hacer porque Todas las construcciones en el verilog generado con always bastante uniformes.


El segundo inconveniente, según muchos, es la imposibilidad de leer el verilog generado y, como consecuencia, la complicación de la depuración. Pero echemos un vistazo al código generado del ejemplo con un contador simple


Verilog generado
 `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 

A primera vista, el verilog generado puede alejarse, incluso en un diseño de tamaño mediano, pero echemos un vistazo.


  • ALEATORIZAR define (puede ser útil cuando se prueba con cinceles), generalmente son inútiles, pero no interfieren particularmente
  • Como vemos el nombre de nuestros puertos, y el registro se conserva
  • _GEN_0 es una variable inútil para nosotros, pero necesaria para que el intérprete genere verilog. Tampoco le prestamos atención.
  • Quedan _T_7 y _T_8, toda la lógica combinatoria en el verilog generado se presentará paso a paso en forma de variables _T.

Lo que es más importante, todos los puertos, registros, cables necesarios para la depuración evitan que se cincelen sus nombres. Y si nos fijamos no solo en verilog sino también en cincel, pronto el proceso de depuración será tan fácil como con verilog puro.


Conclusión


En las realidades modernas, el desarrollo de RTL, ya sea asic o fpga fuera del entorno académico, ha pasado de usar solo código verilog escrito a mano puro a uno u otro tipo de script de generación, ya sea un pequeño script tcl o un IDE completo con un montón de características.


Cincel, a su vez, es el desarrollo lógico de lenguajes para el desarrollo y prueba de la lógica digital. Suponga que en esta etapa está lejos de ser perfecto, pero ya puede proporcionar oportunidades para que pueda tolerar sus defectos. , .

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


All Articles