С развитием микроэлектроники, 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
и сбрасываться стандартным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 и как следствие усложнение отладки. Но давайте взглянем на сгенерированый код из примера с простым счетчиком
`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)
xFFFF
06.08.2018 21:46Статья интересная, но существенных преимуществ не вижу. Может в будущем добавят что-то стоящее.
maslyaev
06.08.2018 23:04Не знаю кому как, но мне пример на Verilog кажется и проще, и нагляднее. Кроме того, автоматическое своевольное добавление chisel-ом логики тактовой частоты и ресета мне кажется странной практикой. В аппаратных делах всё же лучше, когда всё чётко делается своими руками. Чтобы не приходилось играть со средой разработки в игру «кто кого умнее».
Вообще, идея притянуть ООП в проектирование аппаратуры мне не очень нравится. Хотя вопрос, конечно, спорный.atrosinenko
07.08.2018 13:33Я нуб и извините, если скажу чушь, но всё же...
Чтобы не приходилось играть со средой разработки в игру «кто кого умнее».
Когда я читал книгу Harris&Harris, у меня сложилось впечатление, что на Verilog тоже нужно писать не просто по стандарту, а по идиомам, иначе компилятор не поймёт. В этом смысле язык, в котором бы идиомы явно записывались в коде, был бы, наверное, шагом вперёд (не утверждаю, что это Chisel). Ну и здесь это поведение, во всяком случае, детерминированное, а не какие-то эвристики.
автоматическое своевольное добавление chisel-ом логики тактовой частоты и ресета мне кажется странной практикой.
Со своей, программистской, колокольни: ассемблер нагляднее C с точки зрения того, что в итоге получится, и можно, например, в каждую функцию предавать параметры, как в данном случае удобно. Можно договориться о calling convention и аккуратно везде её соблюдать. А можно писать на C, и там нужно исхитриться, чтобы не соблюсти calling convention. Последнее мне кажется более безопасным, хотя и может помешать, например, написанию низкоуровневого кода ядра ОС — ну так на то и ассемблерные вставки и отдельный ассемблерные файлы.
maslyaev
07.08.2018 16:44Аппаратные языки — это в наших прекрасных инфотехнологиях почти самый нижний уровень. Ниже ассемблера. Ниже этих языков — только паяльник. Если средство разработки и там будет додумывать за разработчика и всовывать под шумок невесть что, то это чревато как минимум неудобствами, а как максимум невозможностью решить задачу (выкрутиться через спрыгивание на уровень вниз уже не получится, потому что там, внизу, только паяльник).
Переусложнение и интеллектуализация технологий разработки софта (плюс, конечно же, идеологическая победа ООП) уже привели к тому, что те программы, для которых хватало мегабайта, теперь еле помещаются в сто. Такого же эффекта в аппаратуре как-то совсем не хочется.atrosinenko
07.08.2018 17:15Под связкой C-ассемблер я имел в виду просто два языка программирования разного уровня. То есть не "напишем всё на Чизеле, а потом вручную поправим ниже уровнем". Я имел в виду "что удобно, быстро напишем на Чизеле, что неудобно — допишем на Verilog/VHDL" — я же не предлагаю выкинуть классические HDL-языки и заменить их на Chisel. А вот с количеством ресурсов — это да, остаётся надеяться разве, что компилятор выкинет всё лишнее (как компилятор C вычисляет константные выражения и т.д., образовавшиеся после раскрытия макросов). Ну и на то, что взамен это хотя бы даст то железо (или FPGA-дизайны), которое раньше было сильно сложно сделать (ну или простое, но полезное и много видов) за счёт упрощения процесса разработки.
maslyaev
07.08.2018 18:28Компилятор С++, конечно, выкидывает всякую мелочёвку типа константных выражений, но при этом раскопипащивает темплейты так, что простенькая хренька, коей красная цена 10 кил после компиляции внезапно даёт мегабайт бинарника.
Упрощение процесса разработки — да, чрезвычайно ценно. Рабочее время на вес золота, а туда-сюда гигабайт — копейки. Но поговаривают, что нужно потихоньку отвыкать закладываться на развитие элементной базы.
mcu_by
спасибо за статью)