吃透Chisel语言.16.Chisel模块详解(三)——Chisel的整体连接(Bulk Connection),以流水线处理器为例

Chisel模块详解(三)——Chisel的整体连接(Bulk Connection),以流水线处理器为例

一般来说,一个Chisel模块都会有很多端口,为了连接多端口的模块,要是一根线一根线地连接就会很麻烦。而Chisel中提供了整体连接(Bulk Connection)操作符<>,可以将bundle的部分给双向连接起来,这样我们在连接组件的时候就可以用一个操作符就行了,方便很多。这一篇文章我们就来讲讲这个<>操作符的用法。但是注意:

这个<>在Chisel3中已经不能这么用了!!!

这个<>在Chisel3中已经不能这么用了!!!

这个<>在Chisel3中已经不能这么用了!!!

但是这里还是要说一下的,再说说哪里发生了变化,下面开始。

Chisel2中的整体连接

<>操作符用于连接两个bundle,连接的时候通过子字段的命名来连接,如果没有相应的名字的话,那就不连接了。

举个例子,下面假设我们要写一个流水线处理器,指令抓取阶段Fetch的接口如下:

class Fetch extends Module {
    val io = IO(new Bundle {
        val instr = Output(UInt(32.W))
        val pc = Output(UInt(32.W))
    })
    // fetch阶段的实现省略
}

下一个阶段是解码阶段Decode

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(32.W))
        val regA = Output(UInt(5.W))
        val regB = Output(UInt(5.W))
    })
    // decode阶段的实现省略
}

最后是执行阶段Execute

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))
    })
    // execute阶段的实现省略
}

现在我们通过<>运算符将这三个阶段连接起来:

class Top extends Module {
    val io = IO(new Bundle {
        val result = Input((UInt(32.W)))
        val out = Output(Bool())
    })

    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
}

注意,最后的io <> execute.io将父模块的端口和子模块的端口连接起来,这也是可以的。

一切都按照预期来的话,应该可以顺利编译并输出Verilog,喜大普奔,但是终端输出了一段错误信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMTbAKKu-1657687380858)(16.Chisel模块详解(三)——Chisel的整体连接(Bulk Connection)/image-20220712141526240.png)]

意思是说,<>右边的模块有的接口,左边的模块也必须有?看来是这个操作符根据版本发生了变化,于是我去Chisel-book的Github仓库的issue里面找了一下,果真有人也遇到了这个问题:What do we do if we still want partial bulk connection · Issue #18 · schoeberl/chisel-book (github.com)

原来是Chisel3中<>已经不能这么用了!太坑爹了吧!

官网Chisel/FIRRTL: Chisel3 vs. Chisel2 (chisel-lang.org)中给出了Chisel3和Chisel2之间的区别,其中有这么一段话:

in Chisel2, bulk-connects <> with unconnected source components do not update connections from the unconnected components. ** In Chisel3, bulk-connects strictly adhere to last connection semantics and unconnected OUTPUTs will be connected to INPUTs resulting in the assignment of random values to those inputs.

翻译一下:

Chisel2中,有未连接源组件的整体连接<>不会更新未连接组件的连接;而在Chisel3中,整体连接严格遵循最后连接语义,未连接的输出会连接到输入上,从而会导致这些输入分配随机值。

读懂了吗?反正我是没读懂。还不如听听那位在issue下回复的哥们儿的话:

I’m sorry that I have not understanded how to use <> in Chisel3.In my opinion, you should avoid to use <> in Chisel3.

我表示同意,不用这个整体连接就完事了呗。

Chisel3中的整体连接

但是这样并不能解决问题,我们换个解决方案。

我们可以把每两个模块之间的所有接口都封装成一个Bundle,然后在定义模块IO接口的使用使用这些Bundle,比如:

// Fetch阶段和Decode阶段的连接的捆绑包
class FetchDecodeIO extends Bundle {
    val instr = Output(UInt(32.W))
    val pc = Output(UInt(32.W))
}

class Fetch extends Module {
    val io = IO(new Bundle {
        val fetchdecodeIO = new FetchDecodeIO()
    })
    // fetch阶段的实现省略
}

class Decode extends Module {
    val io = IO(new Bundle {
        val fetchdecodeIO = Flipped(new FetchDecodeIO())
    })
    // decode阶段的实现省略
}

注意,由于定义Bundle的时候都是Output,而Decode阶段是作为输入的,所以需要Flipped()一下,把输入输出翻转一下。那么对于这个三级流水线,我们可以这么写:

import chisel3._
import chisel3.util._

class FetchDecodeIO extends Bundle {
    val instr = Output(UInt(32.W))
    val pc = Output(UInt(32.W))
}

class Fetch extends Module {
    val io = IO(new Bundle {
        val fetchdecodeIO = new FetchDecodeIO()
        val a = Input(UInt(32.W))
        val b = Input(UInt(32.W))
    })
    // fetch阶段的实现省略
    io.fetchdecodeIO.instr := io.a
    io.fetchdecodeIO.pc := io.b
}

class DecodeExecuteIO extends Bundle {
    val aluOp = Output(UInt(5.W))
    val regA = Output(UInt(32.W))
    val regB = Output(UInt(32.W))
}

class Decode extends Module {
    val io = IO(new Bundle {
        val fetchdecodeIO = Flipped(new FetchDecodeIO())
        val decodeexecuteIO = new DecodeExecuteIO()
    })
    // decode阶段的实现省略
    io.decodeexecuteIO.aluOp := io.fetchdecodeIO.instr(5, 0)
    io.decodeexecuteIO.regA := io.fetchdecodeIO.instr
    io.decodeexecuteIO.regB := io.fetchdecodeIO.pc
}

class ExecuteTopIO extends Bundle {
    val result1 = Output(UInt(32.W))
    val result2 = Output(UInt(32.W))
}

class Execute extends Module {
    val io = IO(new Bundle {
        val decodeexecuteIO = Flipped(new DecodeExecuteIO())
        val executetopIO = new ExecuteTopIO()
    })
    // execute阶段的实现省略
    io.executetopIO.result1 := io.decodeexecuteIO.regA
    io.executetopIO.result2 := io.decodeexecuteIO.regB
}

class Top extends Module {
    val io = IO(new Bundle {
        val a = Input(UInt(32.W))
        val b = Input(UInt(32.W))
        val executetopIO = new ExecuteTopIO()
    })

    val fetch = Module(new Fetch())
    val decode = Module(new Decode())
    val execute = Module(new Execute())

    fetch.io.a := io.a
    fetch.io.b := io.b
    fetch.io.fetchdecodeIO <> decode.io.fetchdecodeIO
    decode.io.decodeexecuteIO <> execute.io.decodeexecuteIO
    io.executetopIO <> execute.io.executetopIO
}

object MyModule extends App {
    // emitVerilog(new Count10(), Array("--target-dir", "generated"))
    println(getVerilogString(new Top()))
}

注意,我们在Top模块中使用executetopIO的时候并没有进行翻转,这是因为Execute模块是在Top内部的,应该理解为Execute模块的输出作为Top模块的输出就对了。还有其中的io.aio.b是为了生成Verilog代码而加入的随便写的信号,没有实际意义。最终生成的Verilog代码如下:

module Fetch(
  output [31:0] io_fetchdecodeIO_instr,
  output [31:0] io_fetchdecodeIO_pc,
  input  [31:0] io_a,
  input  [31:0] io_b
);
  assign io_fetchdecodeIO_instr = io_a; // @[hello.scala 18:28]
  assign io_fetchdecodeIO_pc = io_b; // @[hello.scala 19:25]
endmodule
module Decode(
  input  [31:0] io_fetchdecodeIO_instr,
  input  [31:0] io_fetchdecodeIO_pc,
  output [31:0] io_decodeexecuteIO_regA,
  output [31:0] io_decodeexecuteIO_regB
);
  assign io_decodeexecuteIO_regA = io_fetchdecodeIO_instr; // @[hello.scala 35:29]
  assign io_decodeexecuteIO_regB = io_fetchdecodeIO_pc; // @[hello.scala 36:29]
endmodule
module Execute(
  input  [31:0] io_decodeexecuteIO_regA,
  input  [31:0] io_decodeexecuteIO_regB,
  output [31:0] io_executetopIO_result1,
  output [31:0] io_executetopIO_result2
);
  assign io_executetopIO_result1 = io_decodeexecuteIO_regA; // @[hello.scala 50:29]
  assign io_executetopIO_result2 = io_decodeexecuteIO_regB; // @[hello.scala 51:29]
endmodule
module Top(
  input         clock,
  input         reset,
  input  [31:0] io_a,
  input  [31:0] io_b,
  output [31:0] io_executetopIO_result1,
  output [31:0] io_executetopIO_result2
);
  wire [31:0] fetch_io_fetchdecodeIO_instr; // @[hello.scala 61:23]
  wire [31:0] fetch_io_fetchdecodeIO_pc; // @[hello.scala 61:23]
  wire [31:0] fetch_io_a; // @[hello.scala 61:23]
  wire [31:0] fetch_io_b; // @[hello.scala 61:23]
  wire [31:0] decode_io_fetchdecodeIO_instr; // @[hello.scala 62:24]
  wire [31:0] decode_io_fetchdecodeIO_pc; // @[hello.scala 62:24]
  wire [31:0] decode_io_decodeexecuteIO_regA; // @[hello.scala 62:24]
  wire [31:0] decode_io_decodeexecuteIO_regB; // @[hello.scala 62:24]
  wire [31:0] execute_io_decodeexecuteIO_regA; // @[hello.scala 63:25]
  wire [31:0] execute_io_decodeexecuteIO_regB; // @[hello.scala 63:25]
  wire [31:0] execute_io_executetopIO_result1; // @[hello.scala 63:25]
  wire [31:0] execute_io_executetopIO_result2; // @[hello.scala 63:25]
  Fetch fetch ( // @[hello.scala 61:23]
    .io_fetchdecodeIO_instr(fetch_io_fetchdecodeIO_instr),
    .io_fetchdecodeIO_pc(fetch_io_fetchdecodeIO_pc),
    .io_a(fetch_io_a),
    .io_b(fetch_io_b)
  );
  Decode decode ( // @[hello.scala 62:24]
    .io_fetchdecodeIO_instr(decode_io_fetchdecodeIO_instr),
    .io_fetchdecodeIO_pc(decode_io_fetchdecodeIO_pc),
    .io_decodeexecuteIO_regA(decode_io_decodeexecuteIO_regA),
    .io_decodeexecuteIO_regB(decode_io_decodeexecuteIO_regB)
  );
  Execute execute ( // @[hello.scala 63:25]
    .io_decodeexecuteIO_regA(execute_io_decodeexecuteIO_regA),
    .io_decodeexecuteIO_regB(execute_io_decodeexecuteIO_regB),
    .io_executetopIO_result1(execute_io_executetopIO_result1),
    .io_executetopIO_result2(execute_io_executetopIO_result2)
  );
  assign io_executetopIO_result1 = execute_io_executetopIO_result1; // @[hello.scala 69:21]
  assign io_executetopIO_result2 = execute_io_executetopIO_result2; // @[hello.scala 69:21]
  assign fetch_io_a = io_a; // @[hello.scala 65:16]
  assign fetch_io_b = io_b; // @[hello.scala 66:16]
  assign decode_io_fetchdecodeIO_instr = fetch_io_fetchdecodeIO_instr; // @[hello.scala 67:28]
  assign decode_io_fetchdecodeIO_pc = fetch_io_fetchdecodeIO_pc; // @[hello.scala 67:28]
  assign execute_io_decodeexecuteIO_regA = decode_io_decodeexecuteIO_regA; // @[hello.scala 68:31]
  assign execute_io_decodeexecuteIO_regB = decode_io_decodeexecuteIO_regB; // @[hello.scala 68:31]
endmodule

可以看到,Verilog代码行数和Chisel代码行数基本相当,但排除掉括号、空行这些,Chisel的代码量少了很多,而且可读性也比Verilog代码好很多,最重要的时候写的时候清晰多了!

结语

有了整体连接可以让代码编写效率更好,可读性也更好,但是要注意到Chisel2到Chisel3的变化,正确在Chisel3中使用整体连接。

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值