Чик уже дал хороший ответ, но я хочу привести еще один пример, чтобы проиллюстрировать и объяснить некоторые действительно мощные функции Chisel и Scala для проектирования аппаратного обеспечения. Я знаю, что вы (Гай), вероятно, знаете большую часть этого, но я хотел бы предоставить подробный ответ всем, кто сталкивался с этим вопросом.
Я начну с полного примера и затем выделю некоторые используемые функции ,
class MyModule[T <: Data](n: Int, gen: T)(op: (T, T) => T) extends Module {
require(n > 0, "reduce only works on non-empty Vecs")
val io = IO(new Bundle {
val in = Input(Vec(n, gen))
val out = Output(gen)
})
io.out := io.in.reduce(op)
}
[T <: Data]
Это называется Тип параметра (T
) с Верхний предел типа (<: Data
). Это позволяет нам сделать Module
generi c для типа оборудования, с которым мы его параметризовали. Мы даем T
верхнюю границу Data
(это тип от Chisel ), чтобы сообщить Scala, что это тип оборудования, который мы можем использовать для генерации оборудования с помощью Chisel. Верхняя граница означает, что это должен быть подтип данных, который включает в себя все типы аппаратных долот (например, UInt
, SInt
, Vec
, Bundle
и пользовательские классы, которые расширяются Bundle
). Это точно так же, как конструкторы долота, такие как Reg(...)
, параметризованы .
Вы заметите, что существует несколько списков параметров , (n: Int, gen: T)
и (op: (T, T) => T)
. Первый аргумент n: Int
- это простой целочисленный параметр. Второй аргумент, gen: T
, является нашим обобщенным c типом T
и, таким образом, подтипом Data
, который служит шаблоном для оборудования, которое мы будем генерировать внутри модуля.
Второй список параметров (op: (T, T) => T)
является функцией . В качестве функционального языка программирования функции являются значениями в Scala и, таким образом, могут использоваться в качестве аргументов так же, как наш Int
аргумент. (T, T) => T
читает как функцию двух аргументов, оба типа T
, которые возвращают T
. Помните, что T
- это наш тип оборудования, который является подклассом Data
. Поскольку op
находится во втором списке параметров, это говорит Scala, что он должен вывести T
из gen
, а затем использовать тот же T
для op
. Например, если gen
равно UInt(8.W)
, Scala выводит T
как UInt
. Это тогда заставляет op
быть функцией типа (UInt, UInt) => UInt
. Побитовое И является такой функцией, поэтому мы можем передать анонимную функцию в И два UInt
s: (_ & _)
.
Теперь, когда у нас есть наш абстрактный тип с параметризацией MyModule
класс, как мы на самом деле используем это? Выше я привел фрагменты того, как использовать его с UInt
с, но давайте посмотрим, как получить некоторый фактический Verilog:
object MyMain extends App {
println(chisel3.Driver.emitVerilog(new MyModule(4, UInt(8.W))(_ & _)))
}
В качестве альтернативы, мы можем параметризовать MyModule
с более сложным типом:
class MyBundle extends Bundle {
val bar = Bool()
val baz = Bool()
}
object MyMain extends App {
def combineMyBundle(a: MyBundle, b: MyBundle): MyBundle = {
val w = Wire(new MyBundle)
w.bar := a.bar && b.bar
w.baz := a.baz && b.baz
w
}
println(chisel3.Driver.emitVerilog(new MyModule(4, new MyBundle)(combineMyBundle)))
}
Нам также пришлось определить функцию типа (MyBundle, MyBundle) => MyBundle
, которую мы сделали с combineMyBundle
.
Вы можете увидеть полную, работоспособную версию кода, который я представил выше на Scast ie.
Надеюсь, кто-то найдет этот пример полезным!