Rocket-chip-RoCC(2)

关于rocc的内容很多,我会分多章进行讲解。
初步规划以下面章节为划分。

1. 协议,自定义指令说明。
2. rocket-chip RoCC各模块的scala代码说明。
3. 软件代码编译实现。
4. rocket-chip RoCC各模块硬件实现和硬件仿真。

第二部分rocket-chip RoCC各模块的scala代码说明。

1) Rocket-chip中的定制CSR寄存器
利用grep指令搜索scala中关于0x7c0和0x7c1的内容,结果如下图。
在这里插入图片描述
顺着找下去可以找到RocketCore.scala模块。
在这里插入图片描述
利用bpmCSR(bpmCSRId=0x7c0)和chickenCSR(chickenCSRId=0x7c1)定义不同的方法。
在这里插入图片描述
getOrElse()主要就是防范措施,如果有值,那就可以得到这个值,如果没有就会得到一个默认值。

def getOrElse[B1 >: B](key: A, default: => B1): B1 = get(key) match {    
case Some(v) => v
    case None => default
}

从API中可以看出,传入的参数是(key,default)这种形式,返回值是:如果有key那就get(key),如果没有,就返回default。
在代码中的意思是如果chickenCSRId=0x7c1存在相应的bit,则取对应bit的值,如果不存在相应的bit,则取false.B/true.B值,固定相应def的值。
可以通过继续追查disableICachePrefetch等关键字,找到使用这些def的具体位置。
0x7c0和0x7c1的内相关容可以看前文。

2) Rocket-chip中定制指令的使用
我们利用grep和正则表达式可以轻易搜到定制指令相关的scala源文件,如下图所示。
在这里插入图片描述
可以看到,与rocket-chip定制指令相关的scala文件有sybsystem/Configs.scala和tile/LazyRoCC.scala。
在sybsystem/Configs.scala中对各定制指令模块进行例化,并指定各模块使用的定制指令编号,具体入下图。
在这里插入图片描述
再利用WithRoccExample作为关键字进行搜索后,可以在system/Configs.scala文件中找到WithRoccExample的调用,使用的方式为:new WithRoccExample ++。这样编译后,生成的rocket-chip RTL就会带有accumulator、translator、counter和blackbox这四个模块。

下面对tile/LazyRoCC.scala的部分代码进行简单说明。
先看一下rocc统一的模块端口,如下图。
在这里插入图片描述
我在第一章节已经说明了,custom0~3都是使用R类指令格式的,所以可以看到上图中RoCCInstruction就是按照R类指令格式来定义的,funct就是funct7,而xd、xs1和xs2就是funct3。在accumulator模块中xs1和xs2没有用到,xd是作为需要返回计算结果到rd寄存器的标志。也就是说,funct、rs2(32个通用寄存器的编号)、rs1(32个通用寄存器的编号)、xd、xs1、xs2、rd(32个通用寄存器的编号)、opcode都要以散开信号的形式进入到rocc模块中,这里的rocc模块没有特定指明是哪个,也就是全部都要遵守这个约定。
继续看RoCCCommand,首先将刚定义的RoCCInstruction拿出来,然后再加了rs1、rs2和status。这里的rs1和rs2和前面RoCCInstruction的不同,这里的rs1和rs2不是32个通用寄存器的编号,是该对应编号通用寄存器所存储的数值,具体就是0-31共32个通用寄存器,现在的编号2寄存器存的32bits数据为0x12345678,而编号7存的是0xFFFFAAAA,如果RoCCInstruction中的rs1为2,rs2为7,则RoCCCommand中的rs1为0x12345678,rs2为0xFFFFAAAA,它们都是xLen的宽度,我的总线宽度为32bits,所以它们的存储值也就是32bits。status就是CSR寄存器mstatus的值。到这里,RoCCCommand在RoCCInstruction的基础上增加了三个信号,这些在rocc模块中都要声明这些端口。
接下来是定义RoCCResponse,是rocc模块应答的信号,rd是通用寄存器的编号,共5bits,data是返回的计算结果,共xLen位。
在这里插入图片描述
然后就是RoCCCoreIO,将RoCCCommand以cmd为前缀,然后散开为各个信号,RoCCRespons则以resp为前缀,然后多声明mem、busy、initerrupt、exception这几个信号,很好理解,mem就是memory的总线接口,HellaCacheIO不是在tile/LazyRoCC.scala定义的,所以要自己去找,这里就不说了。busy定义为output,interrupt定义为output,exception定义为input,这三个信号后面可以直接用这样的名字来使用,没有那些前缀的存在。
最后就是RoCCIO,是在RoCCCoreIO的基础上增加了ptw、fpu_req和fpu_resp这三组信号,用组作为单位,是因为它们是捆绑的一组端口信号。ptw要看nPTWPorts这个参数的输入,如果nPTWPorts=0,那么久不存在ptw这组端口了,同理,如果没有usingFPU(这个参数在其他地方定义的),那么fpu_req、fpu_resp也会没有,因为我配置的rocket-chip是不存在浮点运算的,所以usingFPU=0,所以全部rocc模块都没有生成fpu_req和fpu_resp接口。
做一个小结,如果你配置的rocket-chip是使用了浮点运算,且你定制模块功能中需要使用到ptw的,那么你的rocc模块接口应该包含RoCCCommand散开、RoCCRespons散开、mem、busy、initerrupt、exception、ptw散开、fpu_req散开和fpu_resp散开这么多的信号。

下面看一下accumulator和translator模块RTL的端口。
Accumulator:
在这里插入图片描述在这里插入图片描述
Translator:
在这里插入图片描述
看图就行了,我就不说明了。
需要注意的是,在scala代码中如果RoCCIO中某些信号你不需要用到,那只需要将这部分信号强接false,那么就会在生成RTL的时候对这些信号进行优化去掉。也就是说RoCCIO是一个总的类,里面就申明了那么多的信号,用不用看你自己的实际情况。

由于scala代码我也在学习中,很多也不是太懂,如果有错误的地方,麻烦各位指出。
此外下面的代码说明我加入了很多我自己的个人理解,而且对于scala、java和chisel我也没有系统学习过,所以不排除存在说明错误的情况,大家也不要扣scala的问题来问我,我也是不太懂,我是通过RTL和scala代码进行比对,然后一步步理解它的配置是起什么作用的。

继续看下面的代码。

/** Base classes for Diplomatic TL2 RoCC units **/
abstract class LazyRoCC(
      val opcodes: OpcodeSet,
      val nPTWPorts: Int = 0,
      val usesFPU: Boolean = false
    )(implicit p: Parameters) extends LazyModule {
  val module: LazyRoCCModuleImp
  val atlNode: TLNode = TLIdentityNode()
  val tlNode: TLNode = TLIdentityNode()
}

class LazyRoCCModuleImp(outer: LazyRoCC) extends LazyModuleImp(outer) {
  val io = IO(new RoCCIO(outer.nPTWPorts))
}

LazyRoCCModuleImp继承LazyModuleImp的类,并将RoCCIO作为IO的类型,命名为io,可以看到上面的图中,accumulator和translator模块RTL的端口信号都带有了io_*的标识。
LazyRoCC是一个抽象类,继承LazyModule的类。后面具体的rocc模块实现中,会调用这个LazyRoCC,并对module、atlNode和tlNode进行重写。
这里的LazyRoCCModuleImp我理解更像是模块接口,而LazyRoCC更像是模块的内部逻辑。

HasLazyRoCC和HasLazyRoCCModule这两个特质的用途,我的理解是这样的,HasLazyRoCC是用于rocc和tilelink、rocc和PTWPorts、rocc和DCachePorts进行互连的,即rocc和外部模块的一些信号连接关系。而HasLazyRoCCModule用于处理rocc模块和respArb、cmdRouter、FPU这些模块的互连关系,其中因为会存在多个rocc定制模块,所以它们的应答是需要仲裁才能返回到core的,而custom0~3定制指令的内容也会由core先到cmdRouter模块进行命令解析,然后再分配到各rocc模块中。

Scala特质的定义和用法可以查看下面链接,具体的含义其实我自己也说不明白,我也是scala的新手:https://docs.scala-lang.org/zh-cn/tour/traits.html

接下来看OpcodeSet,class OpcodeSet定义了添加opcode的方法和匹配的方法,序列按顺序添加,每次新添加都加在原序列的最后。匹配提示,当oc的值和opcodes集合中各映射的值相同时会置1,然后利用oc和集合中全部opcodes映射的值进行比对,只要有一个为1,那么matches的值就为1,reduce(_ || _)是将各比较结果或在一起的操作。

class OpcodeSet(val opcodes: Seq[UInt]) {
  def |(set: OpcodeSet) =
    new OpcodeSet(this.opcodes ++ set.opcodes)

  def matches(oc: UInt) = opcodes.map(_ === oc).reduce(_ || _)
}

伴生对象OpcodeSet,定义custom0的opcode为b0001011,并加到opcodes集合中,以此类推。

object OpcodeSet {
  def custom0 = new OpcodeSet(Seq(Bits("b0001011")))
  def custom1 = new OpcodeSet(Seq(Bits("b0101011")))
  def custom2 = new OpcodeSet(Seq(Bits("b1011011")))
  def custom3 = new OpcodeSet(Seq(Bits("b1111011")))
  def all = custom0 | custom1 | custom2 | custom3
}

利用new OpcodeSet形式实例化的是class OpcodeSet,没有带new的是用伴生对象生成新对象。类和伴生对象的说明:
https://blog.csdn.net/u011437229/article/details/53389706

接下来看RoccCommandRouter的代码。

class RoccCommandRouter(opcodes: Seq[OpcodeSet])(implicit p: Parameters)
    extends CoreModule()(p) {
  val io = new Bundle {
    val in = Decoupled(new RoCCCommand).flip
    val out = Vec(opcodes.size, Decoupled(new RoCCCommand))
    val busy = Bool(OUTPUT)
  }

  val cmd = Queue(io.in)
  val cmdReadys = io.out.zip(opcodes).map { case (out, opcode) =>
    val me = opcode.matches(cmd.bits.inst.opcode)
    out.valid := cmd.valid && me
    out.bits := cmd.bits
    out.ready && me
  }
  cmd.ready := cmdReadys.reduce(_ || _)
  io.busy := cmd.valid

  assert(PopCount(cmdReadys) <= UInt(1),
    "Custom opcode matched for more than one accelerator")
}

声明一个in值,它是RoCCCommand组合信号的散开,同时输入输出方向取反(flip),也就是原来是input的信号,到了in后作为output,原来是output的现在变为input。声明多个out值,个数由opcodes的数量来确定,有上面可知,有4个opcode,所以生成的out值应该有4个,分别为out0,out1,out2和out3,它们分别都是RoCCCommand组合信号的散开。在声明一个busy值,然后将in,out0,out1,out2,out3和busy捆绑为io值,所以生成的代码前缀应该是io_in_xxx,io_out0_xxx……
接着声明cmd值,它是Queue输出的值,而Queue输入的值io.in,刚刚io端口in的值,这部分值是由core那边传过来的,然后输入Queue,Queue类似于FIFO的作用,先入先出,将io.in的信号打入后,另一边作为cmd.io信号打出。

先说明一下zip的用法:https://www.iteblog.com/archives/1225.html
将io.out的各outX值和opcodes的值组合在一起,例如out0的值是对应custom0的,out1的值是对应custom1的,以此类推,然后再用它们的组合值去做一些处理。处理的动作首先匹配opcode的值,先判断cmd.bits.inst.opcode(core传过来的值)的值是否和这个组合的opcode值相等,如果相等则置me为1,即判断core传过来的opcode是不是custom指令的opcode,是哪个custom的opcode。然后将cmd的valid和me做与操作,输入到out.valid中,也就是只有opcode对得上,me为1的情况,core传过来的valid信号才会传给对应rocc模块的valid。out.bits这是很多信号的捆绑值,各个outX.bits都会经过这个操作连接到队列解出来的cmd.bits上,也就是cmd.bits是多驱动,分别驱动out0.bits,out1.bits,out2.bits和out3.bits,但具体用到那一组需要看valid信号的输入,valid信号确定使用哪个rocc,而valid的值又由opcode来决定。最后将outX.ready和me做与操作,并输出到cmdReadys中,记作cmdReadysX,例如现在的组合是(out0 b0001011),如果现在cmd.bits.inst.opcode的值也是b0001011,那么me就会为1,同时现在out0(custom0)对应的rocc模块模块是空闲的,那么该模块的ready信号肯定为1,所以这时候cmdReadys0就会为1。
cmd.ready := cmdReadys.reduce(_ || _)是将cmdReadys里面的cmdReadys0,cmdReadys1,cmdReadys2和cmdReadys3进行或操作,然后得到最终的一个ready信号,并接到cmd.ready上。
io.busy := cmd.valid是将cmd.valid信号接到io.busy信号上,也就是有valid信号来的时候告诉core现在的RoccCommandRouter繁忙,变相告诉core现在的rocc模块在处理中,busy状态。
assert是断言,debug用的,这个断言用于报出多个opcode match的情况,这种情况实际上是不应该存在的,因为一个opcode对应一个定制指令,同时对应一个rocc的模块,不应该有一对多的情况存在,当遇到这种情况时,会触发断言,然后打印信息“Custom opcode matched for more than one accelerator”。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值