
С развитием микроэлектроники, rtl дизайны становились все больше и больше. Реюзабилити кода на verilog доставляет массу неудобств, даже с использованием generate, макросов и фишек system verilog. Chisel же, дает возможность применить всю мощь объектного и функционального программирования к разработке rtl, что является достаточно долгожданным шагом, который может наполнить свежим воздухом легкие разработчиков ASIC и FPGA.
В этой статье будет дан краткий обзор основного функционала и рассмотрены некоторые юзкейсы использования, также поговорим о недостатках этого языка. В дальнейшем, если тема будет интересна, продолжим статью в более детальных туториалах.
Системные требования
- scala базовый уровень
- verilog и основные принципы построения цифровых дизайнов.
- держать документацию chisel под рукой
Я постараюсь разобрать основы chisel на простых примерах, но если что-то будет непонятно, то можете подглядеть сюда.
Что касается scala для быстрого погружения может помочь этот чит-лист.
Подобный есть и для chisel.
Полный код статьи (в виде scala sbt проекта) вы сможете найти тут.
Простой счетчик
Как можно понять из названия 'Constructing Hardware In a scala Embedded Language' chisel — это язык описания аппаратуры надстроенный над scala.
Если коротко о том как все работает, то: из rtl описания на chisel строится hardware граф, который, в свою очередь, превращается в промежуточное описание на языке firrtl, а уже после встроенный бэкэнд интерпретатор генерит из firrtl verilog.
Посмотрим на две реализации простого счетчика.
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
chisel :
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 } }
Немного о chisel:
Module— контейнер для rtl описания модуляBundle— структура данных в chisel, в основном используется для определения интерфейсов.io— переменная для определения портовBool— тип данных, простой однобитовый сигналUInt(width: Width)— беззнаковое целое, конструктор принимает на вход разрядность сигнала.RegInit[T <: Data](init: T)— конструктор регистра, на вход принимает значение по сбросу и имеет такой же тип данных.<>— универсальный оператор соединения сигналовwhen(cond: => Bool) { /*...*/ }— аналогifв verilog
О том какой verilog генерирует chisel поговорим немного позже. Сейчас просто сравним эти два дизайна. Как можно заметить, в chisel отсутствует какое-либо упоминание сигналов clk и reset. Дело в том, что chisel по умолчанию добавляет эти сигналы к модулю. Значение по сбросу для регистра counter мы передаем в конструктор регистра со сбросом RegInit. Поддержка модулей с множеством тактовых сигналов в chisel есть, но о ней тоже немного позже.
Счетчик чуть посложнее
Пойдем дальше и немного усложним задачу, например — сделаем многоканальный счетчик с входным параметром в виде последовательности разрядностей для каждого из каналов.
Начнем теперь с версии на chisel
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) } }
Немного о scala:
width: Seq[Int]— входной параметр для конструктора классаMultiChannelCounter, имеет типSeq[Int]— последовательность с целочисленными элементами.Seq— один из типов коллекций в scala c четко определенной последовательностью элементов..map— для всех знакомая функция над коллекциями, способная преобразовать одну коллекцию в другую за счет одной и той же операции над каждым элементом, в нашем случае последовательность целых значений превращается в последовательностьSimpleCounter'ов с соответствующей разрядностью.
Немного о chisel:
Vec[T <: Data](gen: T, n: Int): Vec[T]— тип данных chisel, является аналогом массива.Module[T <: BaseModule](bc: => T): T— обязательный метод обертки для инстантируемых модулей.util.Cat[T <: Bits](r: Seq[T]): UInt— функция конкатенации, аналог{1'b1, 2'b01, 4'h0}в verilog
Обратим внимание на порты:
enable — развернулся уже в Vec[Bool]*, грубо говоря, в массив однобитных сигналов по одному для каждого канала, можно было сделать и UInt(width.length.W).
out — расширился до суммы ширин всех наших каналов.
Переменная counters является массивом наших счетчиков. Подключаем enable сигнал каждого счетчика к соответствующему входному порту, а все сигналы out объединяем в один с помощью встроенной util.Cat функции и пробрасываем на выход.
Отметим еще и функцию getOut(i: Int) — эта функция высчитывает и возвращает диапазон битов в сигнале out для i'ого канала. Будет очень полезна при дальнейшей работе с таким счетчиком. Реализовать нечто подобное в verilog не выйдет
*Vec не путать с Vector, первый это массив данных в chisel, второй же коллекция в scala.
Давайте теперь попробуем написать этот модуль на verilog, для удобства даже на systemVerilog.
Посидев подумав я пришел к такому варианту(скорее всего он не является единственно верным и самым оптимальным, но вы всегда можете предложить свою реализацию в комментариях).
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
Выглядит уже куда внушительнее. Но что если, мы пойдем дальше и прикрутим к этому популярный wishbone интерфейс с регистровым доступом.
Bundle интерфейсы
Wishbone — небольшая шина по типу AMBA APB, используется в основном для ip ядер с открытым исходным кодом.
Чуть подробнее на вики: https://ru.wikipedia.org/wiki/Wishbone
Т.к. chisel предоставляет нам контейнеры данных типа Bundle имеет смысл обернуть шину в такой контейнер, который в последствии можно будет использовать в любых проектах на chisel.
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] }
Немного о scala:
Option— опциональная обертка данных в scala который может быть либо элементом либоNone,Option[UInt]— это либоSome(UInt(/*...*/))либоNone, полезно при параметризации сигналов.
Вроде ничего необычного. Просто описание интерфейса со стороны мастера, за исключением нескольких сигналов и методов:
tag_master и tag_slave — опциональные сигналы общего назначения в протоколе wishbone, у нас они будут появляться если параметр gotTag, будет равен true.
wbTransaction, wbWrite, wbRead — функции для упрощения работы с шиной.
cloneType — обязательный метод клонирования типа для всех параметризированых [T <: Bundle] классов
Но нам нужен еще и slave интерфейс, посмотрим как можно его реализовать.
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] }
Метод Flipped, как можно было догадаться из названия переворачивает интерфейс, и теперь наш мастер интерфейс превратился в слейв, добавим такой же класс но для мастера.
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] }
Ну вот и все, интерфейс готов. Но прежде чем писать обработчик, посмотрим как можно будет пользоваться этими интерфейсами в случае если нам нужно сделать коммутатор или что-то с большим набором 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) // ... }
Это небольшая заготовка под коммутатор. Удобно объявить интерфейс типа Vec[wishboneSlave], а соединять интерфейсы можно тем же оператором <>. Достаточно полезные фишки chisel когда речь идет об управлении большим набором сигналов.
Универсальный контроллер шины
Как говорилось ранее про мощь функционального и объектного программирования, попробуем его применить. Дальше речь пойдет о реализации универсального контроллера шины wishbone в виде trait, это будет некий mixin для любого модуля с шиной wishboneSlave, для модуля лишь нужно определить карту памяти и замешать trait — контроллер к нему при генерации.
Реализация
Перейдем к реализации обработчика. Он будет простым и сразу отвечать на одиночные транзакции, в случае выпадения из пула адресов выдавать ноль.
Разеберем по частям:
на каждую транзакцию нужно отвечать acknowlege-ом
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
- На чтение отвечаем данными
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— встроенная кобинационная схема типаcaseв verilog*.
Как примерно выглядело бы в 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 }
*Вообще в данном случае это небольшой хак ради параметризируемости, в chisel есть стандартная конструкция которую лучше использовать если, пишите что-то более простое.
switch(x) { is(value1) { // ... } is(value2) { // ... } }
Ну и запись
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— обычный мультиплексор
Встраиваем нечто подобное к нашему мультиканальному счетчику, вешаем регистры на управление каналами и дело в шляпе. Но тут уже рукой подать до универсального контроллер шины WB которому мы будем передавать карту памяти такого вида:
val readMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 /*...*/ ) val writeMemMap = Map( ADDR_1 -> DATA_1, ADDR_2 -> DATA_2 /*...*/ )
Для такой задачи нам помогут trait — что-то вроде mixin-ов в Sala. Основной задачей будет привести readMemMap: [Int, Data] к виду Seq(условие -> данные), а еще было бы неплохо если бы можно было передавать внутри карты памяти базовый адрес и массив данных
val readMemMap = Map( ADDR_1_BASE -> DATA_SEQ, ADDR_2 -> DATA_2 /*...*/ )
Что будет раскрываться с в нечто подобное, где WB_DAT_WIDTH ширина данных в байтах
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 /*...*/ )
Для реализации этого, напишем функцию конвертор из Map[Int, Any] в Seq[(Bool, UInt)]. Придется задействовать scala pattern mathcing.
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
Окончательно наш трейт будет выглядеть так :
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)] = { /*...*/} }
Немного о scala :
io , readMemMap, writeMemMap— абстрактные поля нашегоtrait'a, которые должны быть определены в классе в который мы будем его замешивать.
Как им пользоваться
Чтобы замешать наш trait к модулю нужно соблюсти несколько условий:
ioдолжен наследоваться от классаwishboneSlave- нужно объявить две карты памяти
readMemMapи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 ) }
Создаем регистр softwareEnable он по 'и' складывается с входным сигналом hardwareEnable и заходит на enable counter[MultiChannelCounter].
Объявляем две карты памяти на чтение и на запись: readMemMap writeMemMap, подробнее о структуре можете посмотреть главу выше.
В карту памяти чтения передаем значение счетчика каждого канала*, softwareEnable и hardwareEnable. А на запись отдаем только softwareEnable регистр.
*width.indices.map(counter.io.getOut) — странная конструкция, разберем по частям.
width.indices— вернет массив с индексами элементов, т.е. еслиwidth.length == 4тоwidth.indices = {0, 1, 2, 3}{0, 1, 2, 3}.map(counter.io.getOut)— дает примерно следующее:
{ counter.io.getOut(0), counter.io.getOut(1), /*...*/ }
Теперь для любого модуля на chisel с мы можем объявлять карты памяти на чтение и запись и просто подключать наш универсальный контроллер шины wishbone при генерации, как-то так :
class wishbone_multicahnnel_counter extends WishboneMultiChannelCounter with wishboneSlaveDriver object countersDriver extends App { Driver.execute(Array("-td", "./src/generated"), () => new wishbone_multicahnnel_counter ) }
wishboneSlaveDriver — как раз и есть тот trait микс который мы описали под спойлером.
Конечно, этот вариант универсального контроллера далеко не окончательный, а скорей наоборот сырой. Его главная цель продемонстрировать один из возможных подходов к разработке rtl на chisel. Со всеми возможностями scala таких подходов может быть намного больше, так что у каждого разработчика свое поле для творчества. Правда вдохновляться особо пока неоткуда, кроме как :
- родная chisel библиотека utils, о которой немного дальше, там можно посмотреть на наследование модулей и интерфейсов
- https://github.com/freechipsproject/rocket-chip — risc-v ядро целиком реализованное на chisel, при условии что вы очень хорошо знаете scala, для новичков же без пол литра как говориться будете очень долго разбираться т.к. какой-либо официальной документации о внутренней структуре проекта нет.
MultiClockDomain
Что если мы захотим вручную управлять тактовыми сигналами и сигналами сброса в chisel. До недавнего времени сделать это было нельзя, но c одним из последних релизов появилась поддержка withClock {}, withReset {} и withClockAndReset {}. Посмотрим на примере :
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— регистр который будет тактироваться стандартным сигналомclockи сбрасываться стандартнымresetregClockB— этот же регистр тактируется, как вы догадались, сигналомio.clockB, но сброс будет использоваться стандартный.
Если же мы хотим убрать стандартные сигналы clock и reset полностью, то можно использовать пока экспериментальную фичу — RawModule(модуль без стандартных сигналов тактирования и сброса, всем придется управлять вручную). Пример :
class MultiClockModule extends RawModule { val io = IO(new Bundle { val clockA = Input(Clock()) val clockB = Input(Clock()) val resetA = Input(Bool()) val resetB = Input(Bool()) val in = Input(Bool()) val outA = Output(Bool()) val outB = Output(Bool()) }) val regClockA = withClockAndReset(io.clockA, io.resetA) { RegNext(io.in, false.B) } regClockA <> io.outA val regClockB = withClockAndReset (io.clockB, io.resetB) { RegNext(io.in, false.B) } regClockB <> io.outB }
Utils библиотека
На этом приятные бонусы chisel не заканчиваются. Его создатели потрудились и написали небольшую но весьма полезную библиотеку маленьких, интерфейсов, модулей, функций. Как ни странно на вики нет описания библиотеки, но можно посмотреть чит-листе ссылка на который в самом начале(там два последних раздела)
Интерфейсы:
DecoupledIO— обкновенный частоиспользуемый ready/valid интерфейс.
DecoupledIO(UInt(32.W))— будет содержать в себе сигналы:
val ready = Input(Bool())
val valid = Output(Bool())
val data = Output(UInt(32.W))ValidIO— тоже что иDecoupledIOтолько безready
Модули:
Queue— модуль синхронного FIFO весьма полезная вещь интерфейс выглядит как
val enq: DecoupledIO[T]— перевернутыйDecoupledIO
val deq: DecoupledIO[T]— обычныйDecoupledIO
val count: UInt— количество данных в очередиPipe— модуль задержки, вставляет n-ое количество регистровых срезовArbiter— арбитр наDecoupledIOинтерфейсах, имеет множество подвидов различающихся по виду арбитража
val in: Vec[DecoupledIO[T]]— массив входных интерфейсов
val out: DecoupledIO[T]
val chosen: UInt— показывает выбранный канал
На сколько можно понять из обсуждения на github — в глобальных планах есть существенное расширение этой библиотеки модули: типа асинхронного FIFO, LSFSR, делителей частоты, шаблонов PLL для FPGA; различные интерфейсы; контроллеры под них и многое другое.
Chisel io-teseters
Следует упомянут и возможность тестирования в chisel, на данный момент сложилось два способа тестирования это:
peekPokeTesters— чисто симулиционные тесты которые проверяют логику вашего дизайнаhardwareIOTeseters— это уже интересней, т.к. с помощью этого подхода вы получите cгенерированный teset bench с тестами которые вы написали на chisel, и при наличае verilator даже получите временную диаграмму.
Но пока, подход к тестированию окончательно не выработан, а обсуждение еще идет. В будущем вероятней всего появится универсальный инструмент, для тестирования и тесты тоже можно будет писать на chisel. Но пока можете посмотреть на то что уже есть и как этим пользоваться тут.
Недостатки chisel
Нельзя сказать что chisel — это универсальный инструмент, и что всем стоит на него переходить. Он как и, пожалуй, все проекты на стадии разработки имеет свои недостатки, о которых стоит упомянуть для полноты картины.
Первый и пожалуй самый важный недостаток — это отсутствие асинхронных сбросов. Достаточно весомый, но его можно решить несколькими путями, и один из них это скрипты поверх verilog, которые превращают синхронный reset в асинхронный. Это легко сделать, т.к. все конструкции в генерируемом verilog с always достаточно однобразны.
Второй недостаток заключается, по мнению многих в нечитаемости сгенерированого verilog и как следствие усложнение отладки. Но давайте взглянем на сгенерированый код из примера с простым счетчиком
`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
На первый взгляд сгенерированый verilog может оттолкнуть, даже в средних размеров дизайне, но давайте немного разберемся.
- RANDOMIZE дефайны — (могут пригодиться при тестировании средствами chisel-testers) — в целом бесполезны, но особо не мешают
- Как видим название нашик портов, и регистра сохранились
- _GEN_0 бесполезная для нас переменная, но необходимая firrtl интерпритатору для генерации verilog. На нее тоже не обращаем внимания.
- Остаются _T_7 и _T_8, вся комбинационная логика в сгенерированом verilog будет представлена пошагово в виде переменных _T.
Самое главное, что все необходимые для отладки порты, регистры, провода сохраняют свои названия из chisel. И если смотреть не только на verilog но и на chisel, то вскоре процесс отладки пойдет так-же легко, как и с чистым verilog.
Заключение
В современных реалиях разработка RTL будь то asic или fpga вне академической среды, давно ушла от использования только чистого рукописного verilog кода к тем или иных разновидностей скриптов генерации, будь то маленький скрипт на tcl или целая IDE c кучей возможностей.
Chisel же в свою очередь является логичным развитием языков для разработки и тестирования цифровой логики. Пусть на данном этапе он далек от совершенства, но уже способен предоставить возможности ради которых можно мириться с его недостатками. Важно что проект живой и развивается, и есть большая вероятность что в обозримом будущем таких недостатков станет ну очень мало а функционала ну очень много.
