Chisel模块详解(五)——Chisel中使用Verilog模块
上一篇文章讲述了用函数实现轻量级模块的方法,可以大幅度提升编码效率。Chisel中也提供了一些好用的函数,方便我们编写代码,也方便Chisel编译器优化生成的硬件电路。在Chisel中除了使用我们写的模块或函数硬件生成器,我们可能还需要使用现有的IP,而这些IP通常是用Verilog来写的,我们该如何使用这些IP呢?这一篇文章就来说说。
有时候我们需要在项目中使用一个模块,这个模块可能是个IP,或者是之前项目中比较成熟的模块,但通常都是Verilog描述的电路。又或者我们希望确保某个模块生成的Verilog代码有特定的结构,希望以此来让综合工具识别并映射到一个好用的原语。又或者有个硬件构造Chisel描述不了,只能用Verilog描述。再或者需要连接到Chisel中未定义的FPGA或者其他IP中。Chisel中就有BlackBox
和ExtModule
这两个类帮我们在Chisel中使用Verilog代码描述的模块。
这两个类的使用是类似的,都需要用Map[String, Param]
来参数化,用于转换为生成的Verilog代码中的模块参数。其中BlackBox
以独立地Verilog文件发射,而ExtModule
是类似占位符一样的存在,作为无源码的模块实例发射。这个特性使得ExtModule
有时候很有用,比如针对Xilinx或Intel设备中类似时钟或输入缓存这类原语的时候。
先看一个BlackBox
的例子,这里的BUFGCE
是FPGA中的一个原语,是带有时钟使能端的全局缓冲,有一个时钟输入I
,一个使能端CE
以及一个输出O
,注意BlockBox
类和Module
不同,不会隐式包含时钟和复位信号:
import chisel3._
import chisel3.util._
class BUFGCE extends BlackBox(Map("SIM_DEVICE" -> "7SERIES")) {
val io = IO(new Bundle {
val I = Input(Clock())
val CE = Input(Bool())
val O = Output(Clock())
})
}
class Top extends Module {
val io = IO(new Bundle {})
val bufgce = Module(new BUFGCE)
// 连接BUFGCE的时钟输入端口到顶层模块的时钟信号
bufgce.io.I := clock
}
生成的Verilog代码如下:
module Top(
input clock,
input reset
);
wire bufgce_I; // @[hello.scala 18:24]
wire bufgce_CE; // @[hello.scala 18:24]
wire bufgce_O; // @[hello.scala 18:24]
BUFGCE #(.SIM_DEVICE("7SERIES")) bufgce ( // @[hello.scala 18:24]
.I(bufgce_I),
.CE(bufgce_CE),
.O(bufgce_O)
);
assign bufgce_I = clock; // @[hello.scala 20:17]
assign bufgce_CE = 1'h0;
endmodule
可以看到顶层模块Top
里面包含了一个用参数实例化了的BUFGCE
,然后它的端口分别连接到了Top
中对应的线网上。
ExtModule
也是类似的,不过在Chisel 3.5中需要导入实验性包:
import chisel3._
import chisel3.util._
import chisel3.experimental.ExtModule
class alt_inbuf extends ExtModule(Map("io_standard" -> "1.0 V",
"location" -> "IOBANK_1",
"enable_bus_hold" -> "on",
"weak_pull_up_resistor" -> "off",
"termination" -> "parallel 50 ohms")) {
val io = IO(new Bundle {
val i = Input(Bool())
val o = Output(Bool())
})
}
class Top extends Module {
val io = IO(new Bundle {})
val inbuf = Module(new alt_inbuf)
}
输出的Verilog代码如下:
module Top(
input clock,
input reset
);
wire inbuf_io_i; // @[hello.scala 21:23]
wire inbuf_io_o; // @[hello.scala 21:23]
alt_inbuf
#(.termination("parallel 50 ohms"), .location("IOBANK_1"), .enable_bus_hold("on"), .io_standard("1.0 V"), .weak_pull_up_resistor("off"))
inbuf ( // @[hello.scala 21:23]
.io_i(inbuf_io_i),
.io_o(inbuf_io_o)
);
assign inbuf_io_i = 1'h0;
endmodule
生成了这样的代码之后,我们只需要给出对应的模块的Verilog实现就能够使用了。
我们有三种方法给出对应的Verilog实现,比如对于下面的加法器的IO端口:
class BlockBoxAdderIO extends Bundle {
val a = Input(UInt(32.W))
val b = Input(UInt(32.W))
val cin = Input(Bool())
val c = Output(UInt(32.W))
val cout = Output(Bool())
}
第一种方法是Chisel代码中内联Verilog代码:
class InlineBlackBoxAdder extends HasBlackBoxInline {
val io = IO(new BlockBoxAdderIO)
setInline("InlineBlackBoxAdder.v",
s"""
|module InlineBlackBoxAdder(a, b, cin, c, cout);
|input [31:0] a, b;
|input cin;
|output [31:0] c;
|output cout;
|wire [32:0] sum;
|
|assign sum = a + b + cin;
|assign c = sum[31:0];
|assign cout = sum[32];
|
|endmodule
""".stripMargin)
}
class Top extends Module {
val io = IO(new Bundle {})
val adder = Module(new InlineBlackBoxAdder)
}
Verilog代码在这种方式中,以字符串字面值的形式给出,开头有个s
或f
,然后三对双引号括起来的就是,用竖线|
可以放格式很好看的Verilog代码。此外,这种方法也可以参数化,因为Scala变量可以用$
或${}
插入到字符串里面。最后的stripMargin
方法会在发射代码的时候移除竖线和制表符。
还有两种方法是直接导入Verilog源文件,需要Verilog源代码放在一个单独的文件里面,可以这么写:
class ResourceBlackBoxAdder extends HasBlackBoxResource {
val io = IO(new BlackBoxAdderIO)
addResource("/ResourceBlackBoxAdder.v")
}
还可以这么写:
class PathBlackBoxAdder extends HasBlackBoxPath {
val io = IO(new BlackBoxAdderIO)
addResource("./src/main/resources/ResourceBlackBoxAdder.v")
}
其中HasBlackBoxPath
版本的写法要提供项目文件夹的相对路径,而HasBlackBoxResource
会默认在./src/main/resources
文件夹下搜索Verilog代码。
上面的例子输出的Verilog代码类似下面的代码:
module Top(
input clock,
input reset
);
wire [31:0] adder_a; // @[hello.scala 37:23]
wire [31:0] adder_b; // @[hello.scala 37:23]
wire adder_cin; // @[hello.scala 37:23]
wire [31:0] adder_c; // @[hello.scala 37:23]
wire adder_cout; // @[hello.scala 37:23]
InlineBlackBoxAdder adder ( // @[hello.scala 37:23]
.a(adder_a),
.b(adder_b),
.cin(adder_cin),
.c(adder_c),
.cout(adder_cout)
);
assign adder_a = 32'h0;
assign adder_b = 32'h0;
assign adder_cin = 1'h0;
endmodule
使用emitVerilog
发射Verilog代码时,还会生成一个InlineBlackBoxAdder.v
的实现:
module InlineBlackBoxAdder(a, b, cin, c, cout);
input [31:0] a, b;
input cin;
output [31:0] c;
output cout;
wire [32:0] sum;
assign sum = a + b + cin;
assign c = sum[31:0];
assign cout = sum[32];
endmodule
BlackBox
和其他模块的实例化是一样的,用个Module
封装就行了,即Module(new BlackBoxModule)
,前面的例子已经展示过。但是BlackBox
类是不能直接测试的,必须要封装到测试代码中的一个命名类或匿名类中,这两种方法都允许和BlackBox
有相同的IO端口:
class InlineAdder extends Module {
val io = IO(new BlackBoxAdderIO)
val adder = Module(new InlineBlackBoxAdder)
io <> adder.io
}
或者:
test(new Module {
val io = IO(new BlackBoxAdderIO)
val adder = Module(new InlineBlackBoxAdder)
io <> adder.io
})
注意,HasBlackBoxInline
、HasBlackBoxResource
、HasBlackBoxPath
这三个类都是从Chisel的BlackBox
类拓展出来的traits
(特质),也就是说class Example extends BlackBox with HasBlackBoxInline
和class Example extends HasBlackBoxInline
是等价的。
结语
这一篇文章解决了Chisel中使用Verilog代码的问题,在项目中需要的时候可以通过BlackBox
类型使用Verilog模块,将相应的模块与相应的接口连接起来就可以了。关于Chisel模块的内容到这里就结束了,下一大部分我们会一起学习Chisel中的组合电路的相关语法,敬请期待!