Chisel入门(三)------Chisel的基本语法2

概述

    继续介绍Chisel的基本语法

3 组件

3.1 Chisel中的组件是模块

    Chisel中的每个模块都拓展了class,并包含了接口的io字段。接口是由封装为IO()的Bundle所定义的。Bundle包含的字段表示模块的输入输出端口。方向有Input()和Output()字段指定。

    下边是一个计数器的示例设计,由加法器和寄存器所组成。

加法器:

class Adder extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W))
val b = Input(UInt(8.W))
val y = Output(UInt(8.W))
})
io.y := io.a + io.b
}

寄存器:

class Register extends Module {
val io = IO(new Bundle {
val d = Input(UInt(8.W))
val q = Output(UInt(8.W))
})
val reg = RegInit(0.U)
reg := io.d
io.q := reg
}

计数器:

class Count10 extends Module {
val io = IO(new Bundle {
val dout = Output(UInt(8.W))
})
val add = Module(new Adder())
val reg = Module(new Register())
// the register output
val count = reg.io.q
// connect the adder
add.io.a := 1.U
add.io.b := count
val result = add.io.y
// connect the Mux and the register input
val next = Mux(count === 9.U, 0.U, result)
reg.io.d := next
io.dout := count
}

这两个组件实例化的方法是使用new创建并包装到Module()中,并为它们分配名称add和reg。这个例子中将寄存器的输出命名为count。分别连接1.U和count到加法器组件的两个输入,将加法器组件的输出命名为result。多路选择器则根据当前计数器count的值来在0.U和result之间选择。命名选择器的输出为next并连接到寄存器组件的输入。最终,将计数器的值count连接到Count10组件的端口io.dout。

3.2 嵌套组件

    一个嵌套型数字电路:

 模块A和模块B的定义:

class CompA extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W))
val b = Input(UInt(8.W))
val x = Output(UInt(8.W))
val y = Output(UInt(8.W))
})
// function of A
}
class CompB extends Module {
val io = IO(new Bundle {
val in1 = Input(UInt(8.W))
val in2 = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
// function of B
}

模块C的定义:

class CompC extends Module {
val io = IO(new Bundle {
val inA = Input(UInt(8.W))
val inB = Input(UInt(8.W))
val inC = Input(UInt(8.W))
val outX = Output(UInt(8.W))
val outY = Output(UInt(8.W))
})
// create components A and B
val compA = Module(new CompA())
val compB = Module(new CompB())
// connect A
compA.io.a := io.inA
compA.io.b := io.inA
io.outX := compA.io.x
// connect B
compB.io.in1 := compA.io.y
compB.io.in2 := io.inC
io.outY := compB.io.out
}

模块D的定义:

class CompD extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
// function of D
}

顶层定义:

class TopLevel extends Module {
val io = IO(new Bundle {
val inA = Input(UInt(8.W))
val inB = Input(UInt(8.W))
val inC = Input(UInt(8.W))
val outM = Output(UInt(8.W))
val outN = Output(UInt(8.W))
})
// create C and D
val c = Module(new CompC())
val d = Module(new CompD())
// connect C
c.io.inA := io.inA
c.io.inB := io.inB
c.io.inC := io.inC
io.outM := c.io.outX
// connect D
d.io.in := c.io.outY
io.outN := d.io.out
}

3.3 一个算数逻辑单元

    计算电路(如微处理器)的核心部件之一是算数逻辑单元(arithmetic-logic unit),简称ALU:

 ALU有两个数据输入:a和b,一个函数输入fn和一个输出y。ALU计算a和b,并输出y作为结果。输入fn用于选择作用在a和b上的操作。操作通常是一些算数运算,如加、减、与或非等。ALU通常是一个没有任何撞见的组合逻辑电路,也还可能会对结果的属性(如零或正负)有额外的输出。

    下边代码是一个16位宽输入输出的ALU,支持加、减、或和与运算,由2bit的fn信号片选:

class Alu extends Module {
val io = IO(new Bundle {
val a = Input(UInt(16.W))
val b = Input(UInt(16.W))
val fn = Input(UInt(2.W))
val y = Output(UInt(16.W))
})
// some default value is needed
io.y := 0.U
// The ALU selection
switch(io.fn) {
is(0.U) { io.y := io.a + io.b }
is(1.U) { io.y := io.a - io.b }
is(2.U) { io.y := io.a | io.b }
is(3.U) { io.y := io.a & io.b }
}
}

这个例子中使用了一个新的Chisel结构:switch/is结构用来描述选择ALU输出的表,要使用这个函数需要导入:

import chisel3.util._

3.4 批量连接

    为了连接具有多个IO端口的组件,Chisel提供了批量连接操作符<>。这个操作符在两个方向上连接bundles部分。Chisel使用叶字段进行连接。如果缺少名称,则不连接。

一个流水线处理器取指阶段的接口:

class Fetch extends Module {
val io = IO(new Bundle {
val instr = Output(UInt(32.W))
val pc = Output(UInt(32.W))
})
// ... Implementation of fetch
}

译码阶段的接口:

class Decode extends Module {
val io = IO(new Bundle {
val instr = Input(UInt(32.W))
val pc = Input(UInt(32.W))
val aluOp = Output(UInt(5.W))
val regA = Output(UInt(32.W))
val regB = Output(UInt(32.W))
})
// ... Implementation of decode
}

执行阶段的接口:

class Execute extends Module {
val io = IO(new Bundle {
val aluOp = Input(UInt(5.W))
val regA = Input(UInt(32.W))
val regB = Input(UInt(32.W))
val result = Output(UInt(32.W))
})
// ... Implementation of execute
}

要连接上面的三个模块,只需要两个<>。另外也还可以将子模块的端口与父模块进行连接:

val fetch = Module(new Fetch())
val decode = Module(new Decode())
val execute = Module(new Execute)
fetch.io <> decode.io
decode.io <> execute.io
io <> execute.io

3.5 外部模块

    有时,可能会希望包含一个用Verilog编写的组件,或者可能希望生成的Verilog组件具有非常特定的结构,以便于综合工具可以识别并映射到可用的原语。Chisel通过BlackBox和ExtModule类提供了这方面的支持,允许你是用Verilog源定义组件。两者都是用Map[String, Param]进行参数化,这将被转换为生成的Verilog中的模块参数。BlackBoxes作为单独的Verilog文件,而ExtModules充当占位符,并作为无源模块的例化。这个特性使得ExtModules在以下方面很有用:比如使用Xinlinx或Intel的时钟或输入缓冲这类的原语器件时。

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 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())
})
}

另一方面,黑河可以代表任何组件。它们可以有三种不同的方式声明,可以源代码互嵌,也可以在独立的文件中。例如,考虑使用以下IO的32位加法器:

class BlackBoxAdderIO 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())
}

内嵌的版本如下:

class InlineBlackBoxAdder extends HasBlackBoxInline {
val io = IO(new BlackBoxAdderIO)
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)
}

在字符串文本中提供源代码(用双引号前的s或f表示)并用管道允许包含格式化良好的Verilog代码。此外,还支持参数化,因为Scala的变量可以使用$或${}转义字符插入。stripMargin方法在发出代码的时候删除管道和制表符。

    内嵌黑盒有两种替代方案,都要求Verilog源代码放在单独的文件中:

class ResourceBlackBoxAdder extends HasBlackBoxResource {
val io = IO(new BlackBoxAdderIO)
addResource("/ResourceBlackBoxAdder.v")
}
class PathBlackBoxAdder extends HasBlackBoxPath {
val io = IO(new BlackBoxAdderIO)
addPath("./src/main/resources/PathBlackBoxAdder.v")
}

HasBlackBoxResource版本期望Verilog源代码在./src/main/resource文件夹中。HasBlackBoxPath版本可以指定任意的相对路径。

    黑盒的例化方式则和其他的模块一样。但它们不能直接测试,必须以命名类或匿名类的方式包装为测试器才行。两者都允许具有和黑盒相同的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
})

4 组合逻辑构建块

4.1 组合逻辑电路

    建立一个最简单的布尔表达式并命名:

val e = (a & b) | c

布尔表达式通过赋值给Scala值来命名(e),表达式还可以在其他表达式中使用:

val f = ˜e

这样的表达式被认为是固定的,不能使用=给e重赋值,这回导致编译错误。尝试使用Chisel操作符:

e := c & b

运行会导致异常:cannot reassign to read-only。

    下边代码声明了一个UInt类型的Wire w,并分配了默认值0。when块接收Chisel的Bool类型,当cond为true时重赋值为3:

val w = Wire(UInt())
w := 0.U
when (cond) {
w := 3.U
}

Chisel中的When条件结构还有一种形式的else,被称之为.otherwise。通过在else条件下的赋值可以省略默认值的赋值:

val w = Wire(UInt())
when (cond) {
w := 1.U
} .otherwise {
w := 2.U
}

Chisel还支持带有.elsewhen的条件语句链(像if/elseif/else):

val w = Wire(UInt())
when (cond) {
w := 1.U
} .elsewhen (cond2) {
w := 2.U
} .otherwise {
w := 3.U
}

这个由when、.elsewhen和.otherwise组成的链构成了一个多路复用器链,具有优先级:

 默认赋值可以通过WireDefault函数和wire的类型声明来一并完成:

val w = WireDefault(0.U)
when (cond) {
w := 3.U
}
// ... and some more complex conditional assignments

可能会有疑惑,为什么Scala中已经有if else语句了,Chisel中还要添加when .elsewhen语句。这是因为Scala语句只用于条件执行,而不生成Chisel的硬件。这些Scala的条件语句在Chisel中还有特别的用处,其可以用来设置参数,有条件的生成不同的硬件实例。

4.2 译码器

    译码器可以将n位的二进制数据编程一个m比特的信号,其中m<2^n且输出为独热编码:

    Chisel中的switch语句可以将该逻辑描述为真值表,要使用switch语句需要引入chiesl.util包:

import chisel3.util._

 以下代码使用Chisel中的switch语句来描述一个译码器:

result := 0.U
switch(sel) {
is (0.U) { result := 1.U}
is (1.U) { result := 2.U}
is (2.U) { result := 4.U}
is (3.U) { result := 8.U}
}

上述switch语句列出了sel信号所有可能的值,并将解码后的值赋给result信号。注意:即使列举了所有可能的输入值,Chisel仍然需要分配一个默认值。这个赋值永远不会被处罚,所以会被综合工具优化掉。这是为了避免组合逻辑分配不完整的情况,这将导致以外的综合出锁存器,像VHDL和Verilog一样,所以Chisel不允许不完整的赋值语句。

上边的电路也可以使用二进制编码:

switch (sel) {
is ("b00".U) { result := "b0001".U}
is ("b01".U) { result := "b0010".U}
is ("b10".U) { result := "b0100".U}
is ("b11".U) { result := "b1000".U}
}

上边的代码仍然有点冗长,同时可以注意到,输出信号其实是被sel信号所左移的,所以可以使用Chisel的左移符号来表示一个译码器:

result := 1.U << sel

4.3 编码器

    编码器将独热编码变为二进制编码:

     当输入不是独热码格式时,输出是未定义的,所以需要给定一个默认值:

b := "b00".U
switch (a) {
is ("b0001".U) { b := "b00".U}
is ("b0010".U) { b := "b01".U}
is ("b0100".U) { b := "b10".U}
is ("b1000".U) { b := "b11".U}
}

Scala的循环结构:

// Loops i from 0 to 9
for (i <- 0 until 10) {
// use i to index into a Wire or Vec
}

使用生成器来实现一个16位的编码器,输出为4bit:

val v = Wire(Vec(16, UInt(4.W)))
v(0) := 0.U
for (i <- 1 until 16) {
v(i) := Mux(hotIn(i), i.U, 0.U) | v(i - 1)
}
val encOut = v(15)

其中,编码器的输入是hotIn(i),输出是encOut。Vec中的0元素表示case(0),也表示最小有效位有效时的输出。

4.4 仲裁器

    使用仲裁器来仲裁多个从客户端到单个共享资源的请求:

 4bit仲裁器的电路结构:

 下面代码是一个3个从机的仲裁程序:

val grant = VecInit(false.B, false.B, false.B)
val notGranted = VecInit(false.B, false.B)
grant(0) := request(0)
notGranted(0) := !grant(0)
grant(1) := request(1) && notGranted(0)
notGranted(1) := !grant(1) && notGranted(0)
grant(2) := request(2) && notGranted(1)

小型仲裁器还可以直接使用真值表来描述:

val grant = WireDefault("b0000".U(3.W))
switch (request) {
is ("b000".U) { grant := "b000".U}
is ("b001".U) { grant := "b001".U}
is ("b010".U) { grant := "b010".U}
is ("b011".U) { grant := "b001".U}
is ("b100".U) { grant := "b100".U}
is ("b101".U) { grant := "b001".U}
is ("b110".U) { grant := "b010".U}
is ("b111".U) { grant := "b001".U}
}

对于更大的优先级仲裁器,可以使用上边学习到的for循环生成器策略来实现:

val grant = VecInit.fill(n)(false.B)
val notGranted = VecInit.fill(n)(false.B)
grant(0) := request(0)
notGranted(0) := !grant(0)
for (i <- 1 until n) {
grant(i) := request(i) && notGranted(i-1)
notGranted(i) := !grant(i) && notGranted(i-1)
}

4.5 优先级编码器

    上边的电路中,编码器的输入只能是独热码,如果是其他形式的数据将会导致以外的输入。这里可以通过优先仲裁器+编码器的方式解决这个问题。即将优先仲裁器的输出连接到编码器上:

 4.6 比较器

 比较器有两个多bit的输入,其作用时比较这两个值,其有两个输出:1)表示两个相等;2)表示a大于b。然而,只需要这两个数据便可以知道a和b之间的所有关系,比如大于等于、小于等于之类的。下边是一个比较器的代码片段:

val equ = a === b
val gt = a > b

5 时序构建块

5.1 寄存器

     Chisel中定义一个寄存器:

val q = RegNext(d)

注意,这里不需要将时钟信号连接到寄存器,Chisel隐式的完成这一步。寄存器的输入可以是任意复杂的类型,可以是向量和束的任意组合。一个寄存器也可以定义成如下格式:

val delayReg = Reg(UInt(4.W))
delayReg := delayIn

首先,定义一个寄存器并命名。然后将信号delayIn连接到寄存器的输入端。为了更好的区分组合逻辑电路和时序逻辑电路,建议将reg作为时序名称的一部分。Scala和Chisel中的变量名多为驼峰体:变量名以小写字母开头,类名以大写字母开头。

    寄存器可以进行初始化,由reset信号赋值,默认同步复位:

val valReg = RegInit(0.U(4.W))
valReg := inVal

对于同步复位来说,D触发器本身不需要改变,只需要在输入添加一个多路选择器,在初始值和数据值之间选择即可:

 另一个常用的设计是带有使能信号的寄存器:

val enableReg = Reg(UInt(4.W))
when (enable) {
enableReg := inVal
}

此外Chisel中还专门定义了RegEnable来实现该寄存器:

val enableReg2 = RegEnable(inVal , enable)

再加上复位值:

val resetEnableReg = RegInit(0.U(4.W))
when (enable) {
resetEnableReg := inVal
}

当使用3个参数版本的RegEnable时可以实现使能和复位的组合:

val resetEnableReg2 = RegEnable(inVal , 0.U(4.W), enable)

其中第一个参数是输入信号,第二个参数是初始值,第三个参数是使能信号。

    寄存器也可以是表达式的一部分,且不需要给它命名,下边是一个检查信号上升沿的描述:

val risingEdge = din & !RegNext(din)

5.2 计数器

    计数器是最基本的时序逻辑电路:

val cntReg = RegInit(0.U(4.W))
cntReg := cntReg + 1.U

当需要对一个事件计数时,需要设置一个触发条件:

val cntEventsReg = RegInit(0.U(4.W))
when(event) {
cntEventsReg := cntEventsReg + 1.U
}

 5.2.1 上下计数

向上计数:

val cntReg = RegInit(0.U(8.W))
cntReg := cntReg + 1.U
when(cntReg === N) {
cntReg := 0.U
}

也可以使用选择器实现:

val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === N, 0.U, cntReg + 1.U)

向下计数:

val cntReg = RegInit(N)
cntReg := cntReg - 1.U
when(cntReg === 0.U) {
cntReg := N
}

当需要编写和使用更多的计数器时,可以定义一个带有参数的函数来生成计数器:

// This function returns a counter
def genCounter(n: Int) = {
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
cntReg
}
// now we can easily create many counters
val count10 = genCounter(10)
val count99 = genCounter(99)

函数的最后一句是函数的返回值,本例中是计数寄存器cntReg的输出。

5.2.2 使用计数器来计时

    每个几个时钟周期生成一个有效信号,并以此为条件将计数器置零:

val tickCounterReg = RegInit(0.U(32.W))
val tick = tickCounterReg === (N-1).U
tickCounterReg := tickCounterReg + 1.U
when (tick) {
tickCounterReg := 0.U
}

5.2.3 约减计数器

    上边的一个计数器需要寄存器、加法器和比较器,前两个是没办法省略的,但是最后的比较器可以通过对单个bit的与非门来实现,在ASIC中其资源会比比较器要少。

    进一步优化,原来的设计时从N-1到0,这里将从N-2到-1,跨度是一样的,但使用补码的方式计算的话,-1只有最高位为1,这个时候判断的话只需要判断最高位即可,可以节省较多的硬件资源:

val MAX = (N - 2).S(8.W)
val cntReg = RegInit(MAX)
io.tick := false.B
cntReg := cntReg - 1.S
when(cntReg(7)) {
cntReg := MAX
io.tick := true.B
}

 5.2.4 定时器

    另一种可以使用的定时器是一次性定时器,就像一个厨房计时器,设置一个时间数,然后开始倒计时。

val cntReg = RegInit(0.U(8.W))
val done = cntReg === 0.U
val next = WireDefault(0.U)
when (load) {
next := din
} .elsewhen (!done) {
next := cntReg - 1.U
}
cntReg := next

4.2.5 脉冲宽度调制

    PWM是一种具有恒定周期的信号,在该周期内对信号为高的时间进行调制:

 下面的代码将生成每10个时钟周期3个高周期的波形:

def pwm(nrCycles: Int, din: UInt) = {
val cntReg = RegInit(0.U(unsignedBitLength(nrCycles -1).W))
cntReg := Mux(cntReg === (nrCycles -1).U, 0.U, cntReg +1.U)
din > cntReg
}
val din = 3.U
val dout = pwm(10, din)

其中,使用了unsignedBitLength(n)来指定计数器cntReg的位数,用于表示不超过n的无符号数。同样也有signedBitLength来指定有符号数。

PWM的其中一个应用就是LED,这种情况下眼睛相当于一个低通滤波器,将上述实例拓展为通过三角函数驱动的PWM生成,结果就是LED具有连续变化的能力:

val FREQ = 100000000 // a 100 MHz clock input
val MAX = FREQ/1000 // 1 kHz
val modulationReg = RegInit(0.U(32.W))
val upReg = RegInit(true.B)
when (modulationReg < FREQ.U && upReg) {
modulationReg := modulationReg + 1.U
} .elsewhen (modulationReg === FREQ.U && upReg) {
upReg := false.B
} .elsewhen (modulationReg > 0.U && !upReg) {
modulationReg := modulationReg - 1.U
} .otherwise { // 0
upReg := true.B
// divide modReg by 1024 (about the 1 kHz)
val sig = pwm(MAX, modulationReg >> 10)

4.3 移位寄存器

 下边是一个简单的Chisel写的移位寄存器:1)创建一个4bit的寄存器shiftReg;2)将移位寄存器的低3位于输入din连接;3)使用移位寄存器的最高位作为输出:

val shiftReg = Reg(UInt(4.W))
shiftReg := shiftReg(2, 0) ## din
val dout = shiftReg(3)

5.3.1 并行输出的移位寄存器

    移位寄存器可以用来串行转并行:

val outReg = RegInit(0.U(4.W))
outReg := serIn ## outReg(3, 1)
val q = outReg

5.3.2 并行加载的移位寄存器

    并行输入串行输出的移位寄存器可以将并行的输入数据转换为串行输出。可能应用于UART这类的串行数据端口中。一个具有并行加载功能的4bit移位寄存器:

val loadReg = RegInit(0.U(4.W))
when (load) {
loadReg := d
} otherwise {
loadReg := 0.U ## loadReg(3, 1)
}
val serOut = loadReg(0)

 5.4 存储器

    为了支持片上存储,Chisel提供了内存构造函数SyncReadMem,下边是一个Memory组件,实现了1KB的内存,并包含1bye的输入和输入位宽以及写使能:

class Memory() extends Module {
val io = IO(new Bundle {
val rdAddr = Input(UInt(10.W))
val rdData = Output(UInt(8.W))
val wrAddr = Input(UInt(10.W))
val wrData = Input(UInt(8.W))
val wrEna = Input(Bool())
})
val mem = SyncReadMem(1024, UInt(8.W))
io.rdData := mem.read(io.rdAddr)
when(io.wrEna) {
mem.write(io.wrAddr , io.wrData)
}
}

还有一个问题是,同时读写一个地址时,读出来的数据会是什么样的,新写入的值?旧值?或者是不确定的值?FPGA中不同型号的情况不一样,Chisel中读出的值回事不确定的,即一部分新值,一部分旧值。

    如果想要确保读到新值,可以在上边的电路中增加转发机制:

class ForwardingMemory() extends Module {
val io = IO(new Bundle {
val rdAddr = Input(UInt(10.W))
val rdData = Output(UInt(8.W))
val wrAddr = Input(UInt(10.W))
val wrData = Input(UInt(8.W))
val wrEna = Input(Bool())
})
val mem = SyncReadMem(1024, UInt(8.W))
val wrDataReg = RegNext(io.wrData)
val doForwardReg = RegNext(io.wrAddr === io.rdAddr &&
io.wrEna)
val memData = mem.read(io.rdAddr)
when(io.wrEna) {
mem.write(io.wrAddr , io.wrData)
}
io.rdData := Mux(doForwardReg , wrDataReg , memData)
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值