上一篇文章讲了Chisel中组合电路的基本写法,还介绍了Chisel中的条件语句when/elsewhen/otherwise
结构,需要注意的是Chisel中的条件语句块并非条件执行,而是对应着多路选择器。这一篇文章我们将会用Chisel实现编码器和解码器,同时引入Chisel中的switch
语句。
解码器是用于将一个n
位二进制数解码为m
位信号的电路,其中m
不大于 2 n 2^n 2n。我们这里要实现的是2-4的独热解码器,用于生成二位二进制数对应的独热(One-hot)编码,独热编码中有且仅有一位是1
,其余都是0
。
下图就是2-4独热解码器的示意图:
如果用真值表来描述这个解码器的功能的话,真值表如下:
a |
b |
---|---|
00 | 0001 |
01 | 0010 |
10 | 0100 |
11 | 1000 |
我们当然可以用上一篇文章中学到的when
语句来实现:
result := 0.U
when(sel === 0.U) {
result := 1.U
}.elsewhen (sel === 1.U) {
result := 2.U
}.elsewhen (sel === 2.U) {
result := 4.U
}.otherwise {
result := 8.U
}
虽然看起来还行,但是不太简洁。由于when
的判断条件只有一个信号,那我们是否可以像其他语言一样有个switch
或者case
语句呢?Chisel中就提供了switch
结构,要使用switch
语句的话首先需要包含chisel3.util
包:
import chisel3.util._
然后就可以用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}
}
这个语法也是很好懂的,根据sel
信号来赋不同的解码值给result
信号。需要注意的是,即使我们在switch
语句中枚举了所有的可能值,Chisel这种还是需要赋一个默认值,比如上面的代码中就赋了0.U
给result
。但这个赋值不会被激活,因此会在后端工具被优化掉。这么做是为了避免组合电路(Chisel中的Wire
)中不完全赋值的情况,没定义的情况会在其他硬件描述语言比如Verilog中生成不想要的锁存器(latch),所以Chisel中不允许不完全赋值的情况。
不过上面的例子看起来不是那么好懂,把十进制数值写成二进制会让这个解码器功能看起来更清晰。下面是用二进制描述的完整代码:
class Decoder_2_4 extends Module {
val io = IO(new Bundle {
val sel = Input(UInt(2.W))
val result = Output(UInt(4.W))
})
io.result := 0.U
switch(io.sel) {
is("b00".U) { io.result := "b0001".U}
is("b01".U) { io.result := "b0010".U}
is("b10".U) { io.result := "b0100".U}
is("b11".U) { io.result := "b1000".U}
}
}
这种表述看起来清晰多了,就跟真值表一样,输出如下:
module Decoder_2_4(
input clock,
input reset,
input [1:0] io_sel,
output [3:0] io_result
);
wire [3:0] _GEN_0 = 2'h3 == io_sel ? 4'h8 : 4'h0; // @[hello.scala 12:15 14:20 18:33]
wire [3:0] _GEN_1 = 2'h2 == io_sel ? 4'h4 : _GEN_0; // @[hello.scala 14:20 17:33]
wire [3:0] _GEN_2 = 2'h1 == io_sel ? 4'h2 : _GEN_1; // @[hello.scala 14:20 16:33]
assign io_result = 2'h0 == io_sel ? 4'h1 : _GEN_2; // @[hello.scala 14:20 15:33]
endmodule
关于编码器其实就是这样了,不过上面的代码还可以更简单一点。可以注意到,0/1/2/3
对应的输出分别是0001
左移0/1/2/3
位,因此我们可以直接用一个根据sel
信号的0001
的移位来实现,用Chisel中的移位操作符<<
就可以实现:
result := 1.U << sel
解码器通常用作多路复用器的构建块,将解码器的输出作为使能信号,经过一个与门作为另一个多路选择器的数据输入。不给过在Chisel中不需要手动构造一个多路选择器,因为Mux
在Chisel库中就是有的。解码器还能用于地址解码,然后将输出作为选择信号,比如用于连接到处理器的不同IO设备的选择。
这里的编码器是上一节的解码器翻转过来的样子,也就是从一个四位独热编码到二位二进制编码信号的编码电路。下图就展示了4-2独热编码器的示意图:
对应的真值表也和解码器是反过来的:
a |
b |
---|---|
0001 | 00 |
0010 | 01 |
0100 | 10 |
1000 | 11 |
??? | ?? |
不同的是最底下多了一行,因为独热编码器只定义了上面的四种情况,即输入必须为独热编码,对于其他的任何输入,输出都是未定义的。因为我们没办法描述一个有未定义输出的函数,我们必须使用默认赋值来捕获所有未定义的输入模式。
下面的Chisel代码赋了默认值00
,然后用switch
语句赋值为所有合法输入给输出赋值:
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}
}
这一篇文章以解码器和编码器为例实践了组合电路模块的构造,同时介绍了switch
语句的使用。我们自己也可以尝试编写更复杂的组合电路,比如四位二进制输入到七段数码管编码的编码/解码功能模块,进一步可用于16进制数字的显示。下一部分我们将进入时序电路的学习,从基本的寄存器开始讲述,再到计数器、计时器、移位寄存器,最后是内存。时序电路讲完,基础的内容就讲完了,后面会开始介绍更高阶的内容,敬请期待。