前言
通过之前的学习,读者应当已经掌握Chisel的基本数据类型和操作符的知识,并且了解如何构建小型电路。而在实际大型电路工程中,常用的硬件模块无需重复编写,Chisel提供了丰富的硬件原语,帮助工程师构建大型电路工程。
多路选择器
在Chisel中,有几种不同的多路选择器方式可以实现,包括Mux
、MuxCase
、MuxLookup
和Mux1H
。
-
Mux
:Mux
函数用于创建一个简单的二选一多路选择器。
val sel = Wire(Bool())
val in0 = Wire(UInt(4.W))
val in1 = Wire(UInt(4.W))
val out = Mux(sel, in0, in1)
-
MuxCase
:MuxCase
是一种通用的多路选择器,可以处理多个输入。它接受一个选择信号和一系列输入-输出对(键值对),根据选择信号的值选择相应的输入作为输出。如果选择信号的值没有匹配到任何键值对,则可以提供一个默认值作为最后的参数。
val sel = Wire(UInt(2.W))
val in0 = Wire(UInt(4.W))
val in1 = Wire(UInt(4.W))
val in2 = Wire(UInt(4.W))
val out = MuxCase(0.U, Array(
(sel === 0.U) -> in0,
(sel === 1.U) -> in1,
(sel === 2.U) -> in2
))
-
MuxLookup
:MuxLookup
多路选择器,它接受一个选择信号、一个默认值和一个选择表。选择表是一个由选择信号的值和对应的输入构成的键值对数组。根据选择信号的值,在选择表中查找匹配的键值对,并选择对应的输入作为输出。如果选择信号的值没有匹配到任何键值对,则选择默认值作为输出。
val sel = Wire(UInt(2.W))
val in0 = Wire(UInt(4.W))
val in1 = Wire(UInt(4.W))
val in2 = Wire(UInt(4.W))
val out = MuxLookup(sel, 0.U, Array(
(0.U) -> in0,
(1.U) -> in1,
(2.U) -> in2
))
-
Mux1H
:Mux1H
函数是一种高级的多路选择器,它接受一个选择信号和一系列输入。选择信号可以是一个位宽为N的编码信号,其中只有一个比特位为1,其他都为0。根据选择信号的编码,Mux1H
选择对应位置为1的输入作为输出。
val sel = Wire(UInt(3.W))
val inputs = VecInit(Seq(in0, in1, in2, in3))
val out = Mux1H(sel,inputs)
优先编码器
优先编码器(Priority Encoder)是一种数字电路,用于将多个输入信号中的最高优先级信号编码为对应的输出信号。
在Chisel中,PriorityEncoder
和PriorityEncoderOH
是两个预定义的模块,用于实现优先编码器和优先编码器OH。
PriorityEncoder
模块将多个输入信号中的最高优先级信号编码为对应的输出信号。输出信号是一个表示最高优先级输入信号的二进制编码。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(2.W))
})
val encoder = PriorityEncoder(io.in)
io.out := encoder
}
在MyModule
模块中使用PriorityEncoder
。输入信号是一个4位无符号整数(io.in),输出信号是一个2位无符号整数(io.out)。通过调用PriorityEncoder
函数并传递输入信号,可以获取优先编码器的输出。
PriorityEncoderOH
模块也类似地将多个输入信号中的最高优先级信号编码为对应的输出信号。不同之处在于,输出信号是一个优先编码器OH,只有一个位置为1,其他位置为0。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
val encoder = PriorityEncoderOH(io.in)
io.out := encoder
}
在MyModule
模块中使用PriorityEncoderOH
。输入信号是一个4位无符号整数(io.in),输出信号是一个4位无符号整数(io.out)。通过调用PriorityEncoderOH
函数并传递输入信号,可以获取优先编码器OH的输出。
仲裁器
在Chisel中,可以使用Chisel提供的库和构建模块的能力来实现仲裁器。
仲裁器用于在多个请求者之间进行冲突解决和资源分配。常见的仲裁器类型包括轮询仲裁器、优先级仲裁器和固定优先级仲裁器。
import chisel3._
import chisel3.util._
class PriorityArbiterIO[T <: Data](gen: T, n: Int) extends Bundle {
val in = Flipped(Vec(n, Decoupled(gen)))
val out = Decoupled(gen)
}
class PriorityArbiter[T <: Data](gen: T, n: Int) extends Module {
val io = IO(new PriorityArbiterIO(gen, n))
val arbiter = Module(new Arbiter(gen, n))
for (i <- 0 until n) {
arbiter.io.in(i) <> io.in(i)
}
io.out <> arbiter.io.out
}
在上述示例中,定义了一个通用的优先级仲裁器模块(PriorityArbiter),它接受一个泛型类型的数据(gen)和请求者的数量(n)作为参数。它的输入和输出接口使用了Chisel的Decoupled接口,以支持流水线通信。
在模块内部,使用了Chisel提供的Arbiter模块来实现仲裁逻辑。我们将多个输入端口(io.in)连接到Arbiter的输入(arbiter.io.in),将Arbiter的输出(arbiter.io.out)连接到输出端口(io.out)。
通过这种方式,可以构建一个优先级仲裁器模块,将多个请求者的输入连接到仲裁器,并从仲裁器的输出获得分配的资源。
队列
Queue
模块提供了灵活的配置选项,可以用于控制队列的行为和特性,如深度、流水线寄存器、读写操作的阻塞/非阻塞等。
import chisel3._
import chisel3.util._
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
val queue = Module(new Queue(UInt(8.W), 4, pipe=true, flow=false))
queue.io.enq.valid := true.B
queue.io.enq.bits := io.in
io.out := queue.io.deq.bits
queue.io.deq.ready := true.B
}
-
UInt(8.W)
: 指定队列中数据的类型为 8 位无符号整数。
-
4
: 指定队列的深度为 4,即队列可以存储 4 个元素。
-
pipe=true
: 启用流水线模式,表示在读取数据时,队列将使用流水线寄存器来提高吞吐量。
-
flow=false
: 禁用流控模式,表示队列在写入数据时不会等待读取端口就绪,可以连续写入数据。
通过这些配置选项,可以灵活地定制队列的行为。根据实际需求修改这些选项,以适应设计要求。
在示例中,使用 queue.io.enq.valid
和 queue.io.enq.bits
将输入数据放入队列中,然后使用 queue.io.deq.bits
将队列中的数据输出。通过设置 queue.io.deq.ready
为真,表明输出端口已准备好接收数据。
上述示例中的队列使用的是默认时钟域和复位,可以根据需要进行调整。
存储
ROM
ROM(Read-Only Memory)是一种只读存储器,用于存储固定的数据内容。在 Chisel 中,可以使用 Vec
和 SeqMem
等组件来实现 ROM。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val address = Input(UInt(4.W))
val data = Output(UInt(8.W))
})
val romData = VecInit(Seq(10.U, 20.U, 30.U, 40.U, 50.U, 60.U, 70.U, 80.U))
val rom = Module(new SeqMem(16, UInt(8.W), romData))
io.data := rom.read(io.address)
}
在上述示例中,定义了一个 MyModule
模块,其中包含一个 ROM。输入接口 io.address
是一个 4 位无符号整数,输出接口 io.data
是一个 8 位无符号整数。
使用 VecInit
将固定的数据序列传递给 romData
,这个序列即存储在 ROM 中的数据内容。这里简单地使用了一个 8 个元素的向量,每个元素为 8 位无符号整数。
然后,使用 SeqMem
模块实例化了一个 ROM。SeqMem
接受三个参数:depth
表示 ROM 的深度,dataType
表示数据类型,init
表示初始化数据。
将 romData
作为初始化数据传递给 SeqMem
,从而在 ROM 中存储了指定的数据内容。
最后,使用 rom.read
方法根据输入的地址从 ROM 中读取数据,并将结果赋值给输出接口 io.data
。
请注意,上述示例中的 ROM 是静态的,无法在运行时进行写入操作。
RAM
RAM(Random Access Memory)是一种随机存取存储器,用于存储和读取数据。在 Chisel 中,可以使用 Mem
组件来实现 RAM。
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val address = Input(UInt(4.W))
val dataIn = Input(UInt(8.W))
val dataOut = Output(UInt(8.W))
val writeEnable = Input(Bool())
})
val ram = Mem(16, UInt(8.W))
when(io.writeEnable) {
ram.write(io.address, io.dataIn)
}
io.dataOut := ram.read(io.address)
}
在上述示例中,定义了一个 MyModule
模块,其中包含一个 RAM。输入接口 io.address
是一个 4 位无符号整数,io.dataIn
是一个 8 位无符号整数,io.writeEnable
是一个写使能信号。输出接口 io.dataOut
是一个 8 位无符号整数。
使用 Mem
组件实例化了一个 RAM。Mem
接受两个参数:depth
表示 RAM 的深度,dataType
表示数据类型。
根据 io.writeEnable
的值来判断是否进行写操作。当 io.writeEnable
为真时,使用 ram.write
方法将 io.dataIn
写入到 RAM 中的指定地址 io.address
。
使用 ram.read
方法根据输入的地址从 RAM 中读取数据,并将结果赋值给输出接口 io.dataOut
。
通过以上设置,构建了一个 RAM,并实现了根据地址读取和写入数据的功能。
从文件向RAM写入
在 Chisel 中,可以使用 loadMemoryFromFile
函数来从文件中加载数据到内存(包括 RAM)。
loadMemoryFromFile
是 Chisel Test API 中的一个函数,用于在测试过程中将数据加载到内存中。
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester, loadMemoryFromFile}
class MyModule extends Module {
val io = IO(new Bundle {
// IO定义
})
val ramDepth = 16
val ramDataWidth = 8
val ram = Mem(ramDepth, UInt(ramDataWidth.W))
// 在测试中加载数据到 RAM
val testDataFile = "data.txt"
loadMemoryFromFile(ram, testDataFile)
// ...
}
在上述示例中,假设数据存储在名为 data.txt
的文本文件中,每行一个数据。使用 loadMemoryFromFile
函数将数据从文件加载到 RAM 中。
计数器
在 Chisel 中,可以使用 Counter
组件实现计数器功能。Counter
组件提供了一种简单且方便的方式来生成计数器信号。
import chisel3._
import chisel3.util._
class MyModule extends Module {
val io = IO(new Bundle {
val enable = Input(Bool())
val count = Output(UInt(8.W))
})
val maxCount = 255.U
val counter = Counter(io.enable, maxCount)
io.count := counter.value
}
在示例中定义了一个 MyModule
模块,其中包含一个计数器。输入接口 io.enable
是一个使能信号,输出接口 io.count
是一个 8 位无符号整数,用于输出计数器的当前值。
使用 Counter
组件实例化了一个计数器。Counter
接受两个参数:cond
表示计数器的使能条件,n
表示计数器的最大值。
示例中将 io.enable
作为计数器的使能条件,并将 255(即 2^8-1)作为计数器的最大值。计数器会在每个时钟周期中根据使能信号进行递增,并在达到最大值后重新归零。
计数器的初始值默认为零,但也可以通过 Counter
组件的第三个参数来指定初始值。
例如,Counter(io.enable, maxCount, startValue)
。
状态机
在 Chisel 中,可以使用有限状态机(Finite State Machine,FSM)来实现基于状态的控制逻辑。状态机是一种在不同状态之间转换的模型,每个状态对应着一组特定的行为或操作。
import chisel3._
import chisel3.util._
class MyModule extends Module {
val io = IO(new Bundle {
val input = Input(Bool())
val output = Output(Bool())
})
// 定义状态
val idle :: stateA :: stateB :: stateC :: Nil = Enum(4)
val currentState = RegInit(idle)
// 状态转换逻辑
when(currentState === idle) {
when(io.input) {
currentState := stateA
}
}.elsewhen(currentState === stateA) {
when(io.input) {
currentState := stateB
}.otherwise {
currentState := idle
}
}.elsewhen(currentState === stateB) {
when(io.input) {
currentState := stateC
}.otherwise {
currentState := idle
}
}.elsewhen(currentState === stateC) {
currentState := idle
}
// 状态动作逻辑
io.output := false.B
switch(currentState) {
is(stateA) {
io.output := true.B
}
is(stateB) {
io.output := io.input
}
// 其他状态的动作逻辑
}
}
示例中定义了一个 MyModule
模块,其中包含一个基于状态机的逻辑。输入接口 io.input
是一个布尔值,输出接口 io.output
也是一个布尔值。
使用 Enum
函数定义了四个状态:idle
、stateA
、stateB
和 stateC
。使用 RegInit
将 currentState
寄存器初始化为 idle
状态。
when
和 elsewhen
语句根据当前状态和输入信号 io.input
来定义状态之间的转换逻辑。例如,如果当前状态为 idle
,并且输入信号为高电平,则将当前状态转换为 stateA
。如果当前状态为 stateA
,并且输入信号为高电平,则将当前状态转换为 stateB
,否则返回 idle
状态,以此类推。
switch
语句根据当前状态来定义状态动作逻辑。在示例中,当状态为 stateA
时,将输出信号 io.output
设置为高电平;当状态为 stateB
时,将输出信号 io.output
设置为输入信号 io.input
的值。
在状态转换逻辑中需要使用时钟边沿敏感的条件(例如 risingEdge
、fallingEdge
或 io.input && io.inputChanged
等)来触发状态转换和动作。
总结
更多内容可以查看Chisel其他官方资料以扩展学习。