FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Diplomacyを使ってOCPバスを作成する(7. Bundleの改善)

これまでDiplomacyを使ってOCPバスを作ってきたが、まだ改善したいところがある。まずは

  • OCPバスなのにコマンドチャネルにValid信号がある。
  • 各種コマンドをサポートしていきたい。

特にValid信号は大きな問題だ。OCPはCommand ⇔ Ready信号のハンドシェークなので、Valid信号は不要だ。これを何とかして削除したい。

DiplomacyのBundleがValid/Readyを持っている理由

ではそもそもなぜDiplomacyのバスはValid/Readyを使うようになっているのかというと、Bundleの基本がDecoupledIOになっているからだ。DecoupledIOはValid信号、Ready信号、データという構成になっており、これがTileLink / AMBAのベースになっている。

  • chisel-hw/rocketchip/src/main/scala/tilelink/Bundles.scala
class TLBundle(val params: TLBundleParameters) extends Record
{
...
  private val optA = Some                (Decoupled(new TLBundleA(params)))
  private val optB = params.hasBCE.option(Decoupled(new TLBundleB(params)).flip)
  private val optC = params.hasBCE.option(Decoupled(new TLBundleC(params)))
  private val optD = Some                (Decoupled(new TLBundleD(params)).flip)
  private val optE = params.hasBCE.option(Decoupled(new TLBundleE(params)))

  def a: DecoupledIO[TLBundleA] = optA.getOrElse(Wire(Decoupled(new TLBundleA(params))))
  def b: DecoupledIO[TLBundleB] = optB.getOrElse(Wire(Decoupled(new TLBundleB(params))))
  def c: DecoupledIO[TLBundleC] = optC.getOrElse(Wire(Decoupled(new TLBundleC(params))))
  def d: DecoupledIO[TLBundleD] = optD.getOrElse(Wire(Decoupled(new TLBundleD(params))))
  def e: DecoupledIO[TLBundleE] = optE.getOrElse(Wire(Decoupled(new TLBundleE(params))))

上記の通り、TileLinkのバスはすべてDecoupledIOがベースとなっている。従って、通信はすべてValid / Readyのハンド―シェークとなる。

  • rocketchip/src/main/scala/amba/axi4/Bundles.scala
class AXI4Bundle(params: AXI4BundleParameters) extends AXI4BundleBase(params)
{
  val aw = Irrevocable(new AXI4BundleAW(params))
  val w  = Irrevocable(new AXI4BundleW (params))
  val b  = Irrevocable(new AXI4BundleB (params)).flip
  val ar = Irrevocable(new AXI4BundleAR(params))
  val r  = Irrevocable(new AXI4BundleR (params)).flip
...

IrrevocalbleはReadyValidIOをベースとしているので、これも結局はReady / Validのハンドシェークだ。

一方で、APBはベースとなるBundleを使わずに、それぞれのビットを分解して定義していた。

  • rocketchip/src/main/scala/amba/apb/Bundles.scala
// Signal directions are from the master's point-of-view
class APBBundle(params: APBBundleParameters) extends APBBundleBase(params)
{
  // Flow control signals from the master
  val psel      = Bool(OUTPUT)
  val penable   = Bool(OUTPUT)

  // Payload signals
  val pwrite    = Bool(OUTPUT)
  val paddr     = UInt(OUTPUT, width = params.addrBits)
  val pprot     = UInt(OUTPUT, width = params.protBits)
  val pwdata    = UInt(OUTPUT, width = params.dataBits)
  val pstrb     = UInt(OUTPUT, width = params.dataBits/8)
  val pauser    = if (params.userBits > 0) Some(UInt(OUTPUT, width = params.userBits)) else None

  val pready    = Bool(INPUT)
  val pslverr   = Bool(INPUT)
  val prdata    = UInt(INPUT, width = params.dataBits)
...

そこで、OCPのBundleも新しく自分でベースのBundleを定義することにした。

  • chisel-hw/src/main/scala/ocp/Bundles.scala
class OCPBundle(val params: OCPBundleParameters) extends Record
{
  // Emulate a Bundle with elements abcde or ad depending on params.hasBCE

  private val optCmd  = Some (CmdReady(new OCPBundleCmd (params)))
  private val optData = Some (Decoupled(new OCPBundleData(params)))
  private val optResp = Some (Flipped(Decoupled(new OCPBundleResp(params))))

  def cmd : CmdReadyIO [OCPBundleCmd ] = optCmd. getOrElse(Wire(CmdReady (new OCPBundleCmd(params))))
  def data: DecoupledIO[OCPBundleData] = optData.getOrElse(Wire(Decoupled(new OCPBundleData(params))))
  def resp: DecoupledIO[OCPBundleResp] = optResp.getOrElse(Wire(Decoupled(new OCPBundleResp(params))))
...

DataチャネルとRespチャネルはそのままDecoupledIOを使用しているが、CommandチャネルはCmdReadyIOという新しいIOバンドルを作成した。

f:id:msyksphinz:20200208133913p:plain:w600
  • chisel-hw/src/main/scala/ocp/CmdReadyIO.scala
class CmdReadyIO[+T <: Data](gen: T) extends Bundle
{
  ...
  val ready = Input(Bool())
  val mcmd  = Output(UInt(3.W))
  val bits  = Output(genType)
}
object CmdReadyIO {
  implicit class AddMethodsToReadyValid[T<:Data](target: CmdReadyIO[T]) {
    /** Indicates if IO is both ready and valid
     */
    def fire(): Bool = target.ready && (target.mcmd =/= 0.U(3.W))
  }
}

上記のように、コマンドを示すOuput(UInt(3.W))と、Ready信号、そしてデータを示すbitsを定義した。

さて、これだけでは終わらない。次にOCPバッファの改造をおこなう。OCPバッファにはBufferParamsというパラメータが使われているが、実はこのパラメータの内部にDecoupledIOを使用するQueueの実装が隠れている。

  • rocketchip/src/main/scala/diplomacy/Parameters.scala
case class BufferParams(depth: Int, flow: Boolean, pipe: Boolean)
{
  require (depth >= 0, "Buffer depth must be >= 0")
  def isDefined = depth > 0
  def latency = if (isDefined && !flow) 1 else 0

  def apply[T <: Data](x: DecoupledIO[T]) =
    if (isDefined) Queue(x, depth, flow=flow, pipe=pipe)
    else x
  ...

上記のQueueの実装はDecoupledIOで実装されている。これではちょっと厳しいので、ついでにQueueも改造してCmdQueueを定義してしまおう。

  • chisel-hw/src/main/scala/ocp/CmdReadyIO.scala
case class BufferCmdParams(depth: Int, flow: Boolean, pipe: Boolean)
{
  require (depth >= 0, "Buffer depth must be >= 0")
  def isDefined = depth > 0
  def latency = if (isDefined && !flow) 1 else 0

  def apply[T <: Data](x: CmdReadyIO[T]) =
    if (isDefined) CmdQueue(x, depth, flow=flow, pipe=pipe)
    else x

  override def toString() = "BufferCmdParams:%d%s%s".format(depth, if (flow) "F" else "", if (pipe) "P" else "")
}


object BufferCmdParams
{
  implicit def apply(depth: Int): BufferCmdParams = BufferCmdParams(depth, false, false)

  val default = BufferCmdParams(2)
  val none    = BufferCmdParams(0)
  val flow    = BufferCmdParams(1, true, false)
  val pipe    = BufferCmdParams(1, false, true)
}

CmdQueueは通常のキューを、インタフェースをCmdReadyIOに変えただけである。

ここまでで、OCPの新しいBundleを使う準備が整った。