С развитием микроэлектроники, 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.


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


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

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


    Но пока, подход к тестированию окончательно не выработан, а обсуждение еще идет. В будущем вероятней всего появится универсальный инструмент, для тестирования и тесты тоже можно будет писать на 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.

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


Заключение


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


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

Комментарии (7)


  1. mcu_by
    06.08.2018 19:15

    спасибо за статью)


  1. xFFFF
    06.08.2018 21:46

    Статья интересная, но существенных преимуществ не вижу. Может в будущем добавят что-то стоящее.


  1. maslyaev
    06.08.2018 23:04

    Не знаю кому как, но мне пример на Verilog кажется и проще, и нагляднее. Кроме того, автоматическое своевольное добавление chisel-ом логики тактовой частоты и ресета мне кажется странной практикой. В аппаратных делах всё же лучше, когда всё чётко делается своими руками. Чтобы не приходилось играть со средой разработки в игру «кто кого умнее».

    Вообще, идея притянуть ООП в проектирование аппаратуры мне не очень нравится. Хотя вопрос, конечно, спорный.


    1. atrosinenko
      07.08.2018 13:33

      Я нуб и извините, если скажу чушь, но всё же...


      Чтобы не приходилось играть со средой разработки в игру «кто кого умнее».

      Когда я читал книгу Harris&Harris, у меня сложилось впечатление, что на Verilog тоже нужно писать не просто по стандарту, а по идиомам, иначе компилятор не поймёт. В этом смысле язык, в котором бы идиомы явно записывались в коде, был бы, наверное, шагом вперёд (не утверждаю, что это Chisel). Ну и здесь это поведение, во всяком случае, детерминированное, а не какие-то эвристики.


      автоматическое своевольное добавление chisel-ом логики тактовой частоты и ресета мне кажется странной практикой.

      Со своей, программистской, колокольни: ассемблер нагляднее C с точки зрения того, что в итоге получится, и можно, например, в каждую функцию предавать параметры, как в данном случае удобно. Можно договориться о calling convention и аккуратно везде её соблюдать. А можно писать на C, и там нужно исхитриться, чтобы не соблюсти calling convention. Последнее мне кажется более безопасным, хотя и может помешать, например, написанию низкоуровневого кода ядра ОС — ну так на то и ассемблерные вставки и отдельный ассемблерные файлы.


      1. maslyaev
        07.08.2018 16:44

        Аппаратные языки — это в наших прекрасных инфотехнологиях почти самый нижний уровень. Ниже ассемблера. Ниже этих языков — только паяльник. Если средство разработки и там будет додумывать за разработчика и всовывать под шумок невесть что, то это чревато как минимум неудобствами, а как максимум невозможностью решить задачу (выкрутиться через спрыгивание на уровень вниз уже не получится, потому что там, внизу, только паяльник).
        Переусложнение и интеллектуализация технологий разработки софта (плюс, конечно же, идеологическая победа ООП) уже привели к тому, что те программы, для которых хватало мегабайта, теперь еле помещаются в сто. Такого же эффекта в аппаратуре как-то совсем не хочется.


        1. atrosinenko
          07.08.2018 17:15

          Под связкой C-ассемблер я имел в виду просто два языка программирования разного уровня. То есть не "напишем всё на Чизеле, а потом вручную поправим ниже уровнем". Я имел в виду "что удобно, быстро напишем на Чизеле, что неудобно — допишем на Verilog/VHDL" — я же не предлагаю выкинуть классические HDL-языки и заменить их на Chisel. А вот с количеством ресурсов — это да, остаётся надеяться разве, что компилятор выкинет всё лишнее (как компилятор C вычисляет константные выражения и т.д., образовавшиеся после раскрытия макросов). Ну и на то, что взамен это хотя бы даст то железо (или FPGA-дизайны), которое раньше было сильно сложно сделать (ну или простое, но полезное и много видов) за счёт упрощения процесса разработки.


          1. maslyaev
            07.08.2018 18:28

            Компилятор С++, конечно, выкидывает всякую мелочёвку типа константных выражений, но при этом раскопипащивает темплейты так, что простенькая хренька, коей красная цена 10 кил после компиляции внезапно даёт мегабайт бинарника.

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