Главная » Хабрахабр » [Из песочницы] Chisel — (не совсем) новый подход к разработке цифровой логики

[Из песочницы] Chisel — (не совсем) новый подход к разработке цифровой логики

Реюзабилити кода на verilog доставляет массу неудобств, даже с использованием generate, макросов и фишек system verilog. С развитием микроэлектроники, rtl дизайны становились все больше и больше. 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 <= }; 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 по умолчанию добавляет эти сигналы к модулю. Как можно заметить, в chisel отсутствует какое-либо упоминание сигналов clk и reset. Поддержка модулей с множеством тактовых сигналов в chisel есть, но о ней тоже немного позже. Значение по сбросу для регистра counter мы передаем в конструктор регистра со сбросом RegInit.

Счетчик чуть посложнее

Пойдем дальше и немного усложним задачу, например — сделаем многоканальный счетчик с входным параметром в виде последовательности разрядностей для каждого из каналов.

Начнем теперь с версии на 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

W).
out — расширился до суммы ширин всех наших каналов. Обратим внимание на порты:
enable — развернулся уже в Vec[Bool]*, грубо говоря, в массив однобитных сигналов по одному для каждого канала, можно было сделать и UInt(width.length.

Подключаем enable сигнал каждого счетчика к соответствующему входному порту, а все сигналы out объединяем в один с помощью встроенной util. Переменная counters является массивом наших счетчиков. Cat функции и пробрасываем на выход.

Будет очень полезна при дальнейшей работе с таким счетчиком. Отметим еще и функцию getOut(i: Int) — эта функция высчитывает и возвращает диапазон битов в сигнале out для i'ого канала. Реализовать нечто подобное в verilog не выйдет

*Vec не путать с Vector, первый это массив данных в chisel, второй же коллекция в scala.

Давайте теперь попробуем написать этот модуль на verilog, для удобства даже на systemVerilog.

Посидев подумав я пришел к такому варианту(скорее всего он не является единственно верным и самым оптимальным, но вы всегда можете предложить свою реализацию в комментариях).

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

Но что если, мы пойдем дальше и прикрутим к этому популярный 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 /*...*/ )

Основной задачей будет привести readMemMap: [Int, Data] к виду Seq(условие -> данные), а еще было бы неплохо если бы можно было передавать внутри карты памяти базовый адрес и массив данных Для такой задачи нам помогут trait — что-то вроде mixin-ов в Sala.

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

Придется задействовать scala pattern mathcing. Для реализации этого, напишем функцию конвертор из Map[Int, Any] в Seq[(Bool, UInt)].

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

А на запись отдаем только softwareEnable регистр. Объявляем две карты памяти на чтение и на запись: readMemMap writeMemMap, подробнее о структуре можете посмотреть главу выше.
В карту памяти чтения передаем значение счетчика каждого канала*, softwareEnable и hardwareEnable.

*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

До недавнего времени сделать это было нельзя, но c одним из последних релизов появилась поддержка withClock {}, withReset {} и withClockAndReset {}. Что если мы захотим вручную управлять тактовыми сигналами и сигналами сброса в chisel. Посмотрим на примере :

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 и сбрасываться стандартным reset
  • regClockB — этот же регистр тактируется, как вы догадались, сигналом 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 — чисто симулиционные тесты которые проверяют логику вашего дизайна
  • с помощью этого подхода вы получите cгенерированный teset bench с тестами которые вы написали на chisel, и при наличае verilator даже получите временную диаграмму. hardwareIOTeseters — это уже интересней, т.к.

    В будущем вероятней всего появится универсальный инструмент, для тестирования и тесты тоже можно будет писать на chisel. Но пока, подход к тестированию окончательно не выработан, а обсуждение еще идет. Но пока можете посмотреть на то что уже есть и как этим пользоваться тут.

Недостатки chisel

Он как и, пожалуй, все проекты на стадии разработки имеет свои недостатки, о которых стоит упомянуть для полноты картины. Нельзя сказать что chisel — это универсальный инструмент, и что всем стоит на него переходить.

Достаточно весомый, но его можно решить несколькими путями, и один из них это скрипты поверх verilog, которые превращают синхронный reset в асинхронный. Первый и пожалуй самый важный недостаток — это отсутствие асинхронных сбросов. все конструкции в генерируемом verilog с always достаточно однобразны. Это легко сделать, т.к.

Но давайте взглянем на сгенерированый код из примера с простым счетчиком Второй недостаток заключается, по мнению многих в нечитаемости сгенерированого verilog и как следствие усложнение отладки.

generated 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.

И если смотреть не только на verilog но и на chisel, то вскоре процесс отладки пойдет так-же легко, как и с чистым verilog. Самое главное, что все необходимые для отладки порты, регистры, провода сохраняют свои названия из chisel.

Заключение

В современных реалиях разработка RTL будь то asic или fpga вне академической среды, давно ушла от использования только чистого рукописного verilog кода к тем или иных разновидностей скриптов генерации, будь то маленький скрипт на tcl или целая IDE c кучей возможностей.

Пусть на данном этапе он далек от совершенства, но уже способен предоставить возможности ради которых можно мириться с его недостатками. Chisel же в свою очередь является логичным развитием языков для разработки и тестирования цифровой логики. Важно что проект живой и развивается, и есть большая вероятность что в обозримом будущем таких недостатков станет ну очень мало а функционала ну очень много.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Особенности работы в интернациональной команде. Япония

Причины переезда всегда разные, но чаще всего люди просто хотят перемен. Поработать в другой стране это, как говорят сейчас, challenge (вызов) для многих российских специалистов: сможешь ли «найти» себя в новой стране, в новой роли. И хорошо, если они к ...

[Перевод] Как работают браузеры — введение в безопасность веб-приложений

Давайте начнем серию статей по безопасности веб-приложений с объяснением того, что делают браузеры и как именно они это делают. Поскольку большинство ваших клиентов будут взаимодействовать с вашим веб-приложением через браузеры, необходимо понимать основы функционирования этих замечательных программ. Chrome и lynx ...