FPGA開発日記

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

Diplomacyを使ってOCPバスを作成する(5. OCP SRAMのデバッグ)

Diplomacyを使ってOCPバスを作成している。OCP SRAMの動作を確認しているが、どうも上手く動いていないようだ。Diplomacyの構成を確認している。SRAMは1サイクル後にReadデータが出てくるので、それに合わせてパイプを調整した。

    val d_raw_data  = Wire(Vec(4, UInt(8.W)))
    dontTouch(d_raw_data)
    val w_mem_read = in.cmd.fire() && in.cmd.bits.mcmd === OCPMessages.Read
    d_raw_data := mem.read(in.cmd.bits.address, w_mem_read)

    val r_resp_valid = Reg(Bool())
    val r_resp_bits_tagId = Reg(UInt())

データは1サイクル後に出てくるのでそのまま接続するが、resp_validresp_tagIdがその分1サイクル遅延を挿入している。

    when (w_mem_read) {
      r_resp_valid := true.B
      r_resp_bits_tagId := in.cmd.bits.tagId
    } .otherwise {
      r_resp_valid := false.B
      r_resp_bits_tagId := 0.U
    }

    in.resp.valid := r_resp_valid
    in.resp.bits.data  := Cat(d_raw_data.reverse)
    in.resp.bits.tagId := r_resp_bits_tagId

あとは、PatternPusher側でReadリクエストを出しているときはflight信号をAssertして、その間は次のコマンドを出さないように調整した。

    when ((cmd.fire() && cmd.bits.mcmd === OCPMessages.Write) ||
      data.fire() ||
      resp.fire()
    ) {
      step := step + UInt(1)
    }

    when (cmd.fire() && cmd.bits.mcmd === OCPMessages.Read) {
      flight := true.B
    }

    when (resp.fire()) {
      flight := false.B
    }
...
    cmd.valid  := pvalid(step) && io.run && !flight && ready && !end
    cmd.bits   := pbits(step)

    data.valid := wvalid(step) && io.run && ready && !end
    data.bits  := wbits(step)

この状態でシミュレーションを行った。ちゃんと動作しているようだ。ここまでは良し。次に、機能の拡張や部品の追加を行っていく。

f:id:msyksphinz:20200201003827p:plain
OCP SRAMを接続した際のPatternPusherシミュレーション結果

OCP Pattern PusherのデバッグとOCP コマンド発行のためのクラスの確認

OCP Pattern Pusherのデバッグをしながら、Diplomacyの構築に必要なクラスの構成について見ていこう。

OCP Pattern PusherではDiplomacyのノードが定義され、出力ノードに対してコマンドが発行されている。

f:id:msyksphinz:20200130235943p:plain
TileLinkのDiplomacyノード構成

上記の図ではTileLinkの仕様で書いてあるが、実際にはOCP向けのEdgeで定義してある。

f:id:msyksphinz:20200131000008p:plain
OCPのDiplomacy構成図

ノードの定義は下記のようになっている。OCPClientNodeを定義している。OCPClientPortParametersOCPClientParmetersは以下のように定義されている。

  • chisel-hw/src/main/scala/ocp/PatternPusher.scala
  val node = OCPClientNode(Seq(OCPClientPortParameters(Seq(OCPClientParameters(name = name)))))
  • chisel-hw/src/main/scala/ocp/Nodes.scala
case class OCPClientNode (portParams: Seq[OCPClientPortParameters ])(implicit valName: ValName) extends SourceNode(OCPImp)(portParams) with OCPFormatNode
case class OCPManagerNode(portParams: Seq[OCPManagerPortParameters])(implicit valName: ValName) extends SinkNode  (OCPImp)(portParams) with OCPFormatNode
f:id:msyksphinz:20200131000039p:plain
OCP Diplomacyノードのパラメータ

ocp_outは各コマンドのチャネルが挿入されている。

  • Cmdチャネル
  • WriteDataチャネル
  • Respチャネル

の3つが定義されている。このチャネルに対してコマンドを発行するなどの制御を行う。

f:id:msyksphinz:20200131000102p:plain
OCP Diplomacyのエッジチャネル構成

実際に制御を行っているのは以下の記述だ。

    val (plegal, pvalid, pbits) = pattern.map(_.bits  (edgeOut).getOrElse(edgeOut.NopCmd ())).unzip3
    val (wlegal, wvalid, wbits) = pattern.map(_.wrdata(edgeOut).getOrElse(edgeOut.NopData())).unzip3
    assert (end || plegal(step), s"Pattern pusher ${name} tried to push an illegal request")

    cmd.valid  := pvalid(step) && io.run && ready && !end
    cmd.bits   := pbits(step)

    data.valid := wvalid(step) && io.run && ready && !end
    data.bits  := wbits(step)

この読み方だが、pattern.bits(edgeOut)(mapを除く)の結果は(Bool, Bool, OCPBundleCmd)が返される。一方でpattern.wrdata(edgeOut)(mapを除く)の結果は(Bool, Bool, OCPBundleData)が返される。それぞれ定義は以下のようになっている。

trait Pattern {
  def address: BigInt
  def size: Int
  def bits(edge: OCPEdgeOut): Option[(Bool, Bool, OCPBundleCmd)]
  def wrdata(edge: OCPEdgeOut): Option[(Bool, Bool, OCPBundleData)]
  def dataIn: Option[BigInt] = None
  require ((address & ((BigInt(1) << size) - 1)) == 0)
}

bitswrdataについてはOption型で定義されているので、もしNoneであった場合はgetOrElseでデフォルト値を挿入することになっている。デフォルト値としてはedgeOut.NopCmdedgeOut.NopDataを使用している。

  // Nop
  def NopCmd(): (Bool, Bool, OCPBundleCmd) = {
    val cmd = Wire(new OCPBundleCmd(bundle))
    cmd.mcmd    := OCPMessages.NopCmd
    cmd.tagId   := 0.U
    cmd.address := 0.U
    // Illegal?, Valid?, Command Data
    (true.B, false.B, cmd)
  }
  def NopData(): (Bool, Bool, OCPBundleData) = {
    val data = Wire(new OCPBundleData(bundle))
    data.tagId := 0.U
    data.data  := 0.U
    // Illegal?, Valid?, Write Data
    (true.B, false.B, data)
  }

そしてコマンドとデータをそれぞれの条件で発行している。

    cmd.valid  := pvalid(step) && io.run && ready && !end
    cmd.bits   := pbits(step)

    data.valid := wvalid(step) && io.run && ready && !end
    data.bits  := wbits(step)

stepはパタンの進捗を示すインデックスなので、コマンドやデータの発行が完了するたびにステップを進めるような仕組みにしている。

    when (cmd.fire() || data.fire()) {
      step := step + UInt(1)
    }

これで一応書き込みパタンを送信できるようになった。次はメモリの受け取り側だ。

f:id:msyksphinz:20200131000232p:plain
OCP DiplomacyのRTLシミュレーション結果

RISC-V Vector命令をサポートした自作命令セットシミュレータの実装検討 (2. 全ベクトル命令のデコーダ生成)

ベクトル命令の全命令をリストアップしたので、次はいよいよベクトル命令のデコーダ生成に入る。デコーダの生成を人力で頑張る必要はなく、私のRISC-Vシミュレータはすべて自動的にデコーダを吐き出す仕組みが搭載されている。したがって、ひたすらベクトル命令のエンコーディングの定義を書いていけばよい。

[
    /* BEGIN OPIVV */

    {"name":"vadd.vv      v[11:7],v[24:20],v[19:15][,vm[25]]", "length":"32", "dlength":"128", "field": ["00000", "0X", "XXXXX", "XXXXX", "000", "XXXXX", "10101", "11"], "category":"VEC", "func_suffix":"", "impl":[""], "inst_ctrl":[""]},
    {"name":"vsub.vv      v[11:7],v[24:20],v[19:15][,vm[25]]", "length":"32", "dlength":"128", "field": ["00001", "0X", "XXXXX", "XXXXX", "000", "XXXXX", "10101", "11"], "category":"VEC", "func_suffix":"", "impl":[""], "inst_ctrl":[""]},
    {"name":"vrsub.vv     v[11:7],v[24:20],v[19:15][,vm[25]]", "length":"32", "dlength":"128", "field": ["00001", "1X", "XXXXX", "XXXXX", "000", "XXXXX", "10101", "11"], "category":"VEC", "func_suffix":"", "impl":[""], "inst_ctrl":[""]},
    {"name":"vminu.vv     v[11:7],v[24:20],v[19:15][,vm[25]]", "length":"32", "dlength":"128", "field": ["00010", "0X", "XXXXX", "XXXXX", "000", "XXXXX", "10101", "11"], "category":"VEC", "func_suffix":"", "impl":[""], "inst_ctrl":[""]},
    {"name":"vmin.vv      v[11:7],v[24:20],v[19:15][,vm[25]]", "length":"32", "dlength":"128", "field": ["00010", "1X", "XXXXX", "XXXXX", "000", "XXXXX", "10101", "11"], "category":"VEC", "func_suffix":"", "impl":[""], "inst_ctrl":[""]},
...

今回はまだすべての命令定義の項目を埋め切っていないので、すべて横一列に書くことで後で一気に修正が行いやすい状態を保っている。この形式で合計300を超えるRISC-Vの命令リストをすべて並びあげた。

私のシミュレータはこのデコードテーブルをすべて舐めたうえで、適切なデコードジャンプテーブルを生成していく。例えば、vadd.vv命令に到達するまでには以下のテーブルをジャンプしていく。

vadd.vv 命令
命令エンコーディング ["00000", "0X", "XXXXX", "XXXXX", "000", "XXXXX", "10101", "11"]
  • swimmer_riscv/src/inst_decoder_riscv.cpp
InstId_t RiscvDec::DecodeInst (InstWord_t inst) {
  InstWord_t field_LD = ExtractLDField (inst);
  switch (field_LD) {
    case 0x03 : 
    // Remaining Instruction is 455
...
    // vlb.v   v[11:7],(r[19:15])
    // vadd.vv      vr[11:7],vr[24:20],vr[19:15][,vm[25]]
    // vsub.vv      vr[11:7],vr[24:20],vr[19:15][,vm[25]]
...
      return DecodeInst_LD_11 (inst); break;
InstId_t RiscvDec::DecodeInst_LD_11 (InstWord_t inst) {
  InstWord_t field_OP = ExtractOPField (inst);
  switch (field_OP) {
    case 0x15 : 
    // Remaining Instruction is 290
    // vsetvl  r[11:7],r[19:15],r[24:20]
    // vadd.vv      vr[11:7],vr[24:20],vr[19:15][,vm[25]]
    // vsub.vv      vr[11:7],vr[24:20],vr[19:15][,vm[25]]
...
      return DecodeInst_LD_11_OP_10101 (inst); break;
InstId_t RiscvDec::DecodeInst_LD_11_OP_10101 (InstWord_t inst) {
  InstWord_t field_F3 = ExtractF3Field (inst);
  switch (field_F3) {
...
    case 0x00 : 
    // Remaining Instruction is 44
    // vadd.vv      vr[11:7],vr[24:20],vr[19:15][,vm[25]]
...
      return DecodeInst_LD_11_OP_10101_F3_000 (inst); break;
InstId_t RiscvDec::DecodeInst_LD_11_OP_10101_F3_000 (InstWord_t inst) {
  InstWord_t field_R3 = ExtractR3Field (inst);
  switch (field_R3) {
    case 0x00 : 
      return InstId_t::INST_ID_VADD_VV;

これが例えばさらに難しくてvsub.vvvrsub.vvを見分ける場合、

  InstWord_t field_R3 = ExtractR3Field (inst);
  switch (field_R3) {
    case 0x00 : 
      return InstId_t::INST_ID_VADD_VV;
    case 0x01 : 
    // Remaining Instruction is 2
    // vsub.vv      vr[11:7],vr[24:20],vr[19:15][,vm[25]]
    // vrsub.vv     vr[11:7],vr[24:20],vr[19:15][,vm[25]]
      return DecodeInst_LD_11_OP_10101_F3_000_R3_00001 (inst); break;

さらにDecodeInst_LD_11_OP_10101_F3_000_R3_00001()の中ですべての要素を洗い出して分類する。

InstId_t RiscvDec::DecodeInst_LD_11_OP_10101_F3_000_R3_00001 (InstWord_t inst) {
  InstWord_t field_F2 = ExtractF2Field (inst);
  switch (field_F2) {
    case 0x00 : 
    case 0x01 : 
      return InstId_t::INST_ID_VSUB_VV;
    case 0x02 : 
    case 0x03 : 
      return InstId_t::INST_ID_VRSUB_VV;
    default : return InstId_t::INST_ID_SENTINEL_MAX;
  }
  return InstId_t::INST_ID_SENTINEL_MAX;
}

あとはすべての命令向けの関数を定義すれば完了だ。これで命令のスケルトンがすべて完成した。

  • swimmer_riscv/src/inst_riscv_vector.cpp
#include <stdint.h>
#include <gmpxx.h>
...
void InstEnv::RISCV_INST_VADD_VV(InstWord_t inst_hex) {}
void InstEnv::RISCV_INST_VSUB_VV(InstWord_t inst_hex) {}
void InstEnv::RISCV_INST_VRSUB_VV(InstWord_t inst_hex) {}
...
void InstEnv::RISCV_INST_VFWNMACC_VF(InstWord_t inst_hex) {}
void InstEnv::RISCV_INST_VFWMSAC_VF(InstWord_t inst_hex) {}
void InstEnv::RISCV_INST_VFWNMSAC_VF(InstWord_t inst_hex) {}

RISC-Vのベクトル命令のエンコーディングをまとめる

RISC-Vのベクトル命令について勉強している。ベクトル拡張の仕様はv0.8をベースにしている。命令のエンコーディングについて調べているが、理解に時間がかかっているのでまとめてみる。

公式の仕様書には、以下のようなエンコーディングのリストが載っているが、何が何だか分からない。

f:id:msyksphinz:20200118123938p:plain
ベクトル命令のエンコーディングテーブル

まず、この表について理解する必要がある。この表はfunct3エンコーディングによって複数の命令の分類があることを示している。そしてそれぞれの命令分類(OPIVV, OPIVX, ... など)は別の表で命令エンコーディングの定義がなされている。

  • 整数ベクトル命令1
    • OPIVV
    • OPIVX
    • OPIVI
  • 整数ベクトル命令2
    • OPMVV
    • OPMVX
  • 浮動小数点ベクトル命令
    • OPFVV
    • OPFVF

それぞれの命令フォーマットは以下のように定義されている。

Formats for Vector Arithmetic Instructions under OP-V major opcode

31       26  25   24      20 19      15 14   12 11      7 6     0
  funct6   | vm  |   vs2    |    vs1   | 0 0 0 |    vd   |1010111| OP-V (OPIVV)
  funct6   | vm  |   vs2    |    vs1   | 0 0 1 |  vd/rd  |1010111| OP-V (OPFVV)
  funct6   | vm  |   vs2    |    vs1   | 0 1 0 |  vd/rd  |1010111| OP-V (OPMVV)
  funct6   | vm  |   vs2    |   simm5  | 0 1 1 |    vd   |1010111| OP-V (OPIVI)
  funct6   | vm  |   vs2    |    rs1   | 1 0 0 |    vd   |1010111| OP-V (OPIVX)
  funct6   | vm  |   vs2    |    rs1   | 1 0 1 |    vd   |1010111| OP-V (OPFVF)
  funct6   | vm  |   vs2    |    rs1   | 1 1 0 |  vd/rd  |1010111| OP-V (OPMVX)
     6        1        5          5        3        5        7

つまり、命令のエンコーディングとしては以下の手順を取ることになる。

  1. まずはopcode[6:0]でベクトル命令であることを認識する。
  2. funct3でベクトル命令を7種類に分類する。
  3. funct6で命令を分類する。

さらに、もう一つ大きな分類として25ビット目にvmというビットが定義されている。vmビットによってベクトルレジスタに対してマスクを適用するかを決定する。

さて、もう一度命令一覧表に戻ってみると3種類の分類(OPIVV, OPIVV, OPIVIOPMVV, OPMVXOPFVV, OPFVF)によって命令が定義されていることが分かる。

f:id:msyksphinz:20200118124017p:plain
ベクトル命令の分類と命令エンコーディング

たとえばvadd命令だとV, X, Iの場合はOPIVV, OPIVX, OPIVIの3種類にチェックが付いているので、3種類の命令フォーマットが定義可能だ。

# Integer adds.
vadd.vv vd, vs2, vs1, vm   # Vector-vector
vadd.vx vd, vs2, rs1, vm   # vector-scalar
vadd.vi vd, vs2, imm, vm   # vector-immediate

一方でvsub命令はVXつまりOPIVV, OPIVXのフォーマットが定義可能である。

# Integer subtract
vsub.vv vd, vs2, vs1, vm   # Vector-vector
vsub.vx vd, vs2, rs1, vm   # vector-scalar

さらに、vrsub命令はXIつまりOPIVX, OPIVIのフォーマットが定義可能である。

# Integer reverse subtract
vrsub.vx vd, vs2, rs1, vm   # vd[i] = rs1 - vs2[i]
vrsub.vi vd, vs2, imm, vm   # vd[i] = imm - vs2[i]

これらをベクトル命令の一覧表をまとめた。命令の分類とfunct[5:4]で表を作った。

f:id:msyksphinz:20200118124059p:plain
ベクトル命令の全体テーブル。

RISC-V Vector命令をサポートした自作命令セットシミュレータの実装検討

RISC-V Vector命令の仕様がかなり固まってきた。いつの間にかVer 0.8が公開され、命令のニーモニックも安定しつつある。

  • RISC-V "V" Vector Extension
    • Version 0.8-draft-20191117

https://riscv.github.io/documents/riscv-v-spec/riscv-v-spec.pdf

ついでに、自分で日本語訳したRISC-V Vector命令仕様書がまだVer.0.7のままなのでアップデートしなければ。

msyksphinz-self.github.io

RVV (RISC-V)の最も基本的なパラメータ

RISC-Vベクトル拡張の最も基本的なパラメータは以下の3つである。

  1. 1ベクトル要素の最大ビットサイズELEN。2の累乗の必要がある。
  2. ベクトルレジスタのビットサイズVLEN ≥ ELEN, 2の累乗の必要がある。
  3. ストリップビット距離SLEN。VLEN ≥ SLEN ≥ 32 である必要がある。それぞれは2の累乗の必要がある。

それぞれ、自作RISC-Vシミュレータでそれぞれ以下のように表現した。

class RiscvPeThread : public EnvBase
{
...
  /*
   * Vector Constant Parameters
   */
  const int32_t RVV_ELEN = 32;  // The maximum size of a single vector element in bits, ELEN, which must be a power of 2.
  const int32_t RVV_VLEN = 512; // The number of bits in a vector register, VLEN ≥ ELEN, which must be a power of 2.
  const int32_t RVV_SLEN = 32;  // The striping distance in bits, SLEN, which must be VLEN ≥ SLEN ≥ 32, and which must be a power of 2.
...
 public:
  int32_t get_ELEN() { return RVV_ELEN; }
  int32_t get_VLEN() { return RVV_VLEN; }
  int32_t get_SLEN() { return RVV_SLEN; }
...

RVVサポートに必要なレジスタ

RVVのサポートには以下のシステムレジスタが新たに追加される。

Address Privilege Name Description
0x008 URW vstart ベクトルスタート位置を示すレジスタ
0x009 URW vxsat 浮動小数点飽和フラグレジスタ
0x00A URW vxrm 浮動小数点丸めモードレジスタ
0xC20 URO vl ベクトル長レジスタ
0xC21 URO vtype ベクトルデータ型レジスタ

それぞれ自作RISC-Vシミュレータに以下のように実装した。これらのCSRはすべてRubyスクリプトで自動生成するように設定している。

$sysreg_table.push(Array[0x008,  'URW',     'vstart'       , Array[Array[xlen-1, 0, 'vstart'        , 'RW', 0]]])
$sysreg_table.push(Array[0x009,  'URW',     'vxsat'        , Array[Array[xlen-1, 0, 'vxsat'         , 'RW', 0]]])
$sysreg_table.push(Array[0x00A,  'URW',     'vxrm'         , Array[Array[xlen-1, 0, 'vxrm'          , 'RW', 0]]])
...
$sysreg_table.push(Array[0xC20, 'URO', 'vl',    Array[Array[xlen-1, 0, 'vl',    'RO', 0]]])
$sysreg_table.push(Array[0xC21, 'URO', 'vtype', Array[Array[xlen-1, xlen-1, 'vill', 'RO', 0],
                                                      Array[6, 5, 'vediv', 'RO', 0],
                                                      Array[4, 2, 'vsew',  'RO', 0],
                                                      Array[1, 0, 'vlmul', 'RO', 0]]])
$sysreg_table.push(Array[0xC22, 'URO', 'vlenb', Array[Array[xlen-1, 0, 'vlenb', 'RO', 0]]])

上記の記述で、例えばvtypeについては以下のような構造体が自動的に生成される。

  union {
    struct {
      uint64_t vlmul : 2;
      uint64_t vsew : 3;
      uint64_t vediv : 2;
      uint64_t dummy_0 : 56;
      uint64_t vill : 1;
    } bit_vtype;
    uint64_t vtype;
  } vtype;

これに対して、Read/Writeの実装を付け加えていく。

template <typename Xlen_t>
CsrAccResult CsrEnv::Read_VTYPE (Xlen_t *data, PrivMode mode)
{
  *data = vtype.vtype;
  return CsrAccResult::Normal;
}

template <typename Xlen_t>
CsrAccResult CsrEnv::Write_VTYPE (Xlen_t data, PrivMode mode)
{
  vtype.vtype = data;
  DWord_t vl = m_pe_thread->get_VLEN() * vtype.bit_vtype.vlmul / vtype.bit_vtype.vsew;
  Write_VL (vl, mode);

  return CsrAccResult::Normal;
}

vsetvli, vsetvl命令の初期実装

まずは、RVVの初期設定を行うための基本的な命令を作り込んで行こう。 それぞれ、vsetvli, vsetvlという命令が定義されている。

f:id:msyksphinz:20200110015855p:plain
vsetvli, vsetvl命令の定義。

それぞれ、JSONファイルを定義して命令のデコーダとテンプレートを作成する。

    //
    // RVV : Vector Extension
    //
    {
        "name":"vsetvli r[11:7],r[19:15],h[30:20]",
        "length":"32", "dlength":"32",
        "field":["0XXXX", "XX", "XXXXX", "XXXXX", "111", "XXXXX", "10101", "11"],
        "category":"VECTOR",
        "func_suffix":"",
        "impl":[""],
        "inst_ctrl":[]
    },
    {
        "name":"vsetvl  r[11:7],r[19:15],r[24:20]",
        "length":"32", "dlength":"32",
        "field":["10000", "00", "XXXXX", "XXXXX", "111", "XXXXX", "10101", "11"],
        "category":"VECTOR",
        "func_suffix":"",
        "impl":[""],
        "inst_ctrl":[]
    },

さらに、実際の実装を作り込んで行く。

void InstEnv::RISCV_INST_VSETVLI (InstWord_t inst_hex)
{
  if (!m_pe_thread->IsVECAvailable ()) {
    m_pe_thread->GenerateException (ExceptCode::Except_IllegalInst, 0);
    return;
  }

  RegAddr_t rs1_addr = ExtractR1Field (inst_hex);
  RegAddr_t rd_addr  = ExtractRDField (inst_hex);

  DWord_t rs1_val = m_pe_thread->ReadGReg<DWord_t> (rs1_addr);

  m_pe_thread->CSRWrite (static_cast<Addr_t>(SYSREG_ADDR_VTYPE), ExtractBitField (inst_hex, 26, 20));
}

とりあえず、これで基本的な命令の初期実装は完了だ。

Diplomacyを使ってOCPバスを作成する方法の検討(4. OCP Pusherの生成)

前回、せっかくOCP PusherとOCP SRAMを作って接続してみたのだがなぜかOCP SRAMが途中で消されてしまいきちんとシミュレーションすることができていなかった。

FIR上ではマスターもスレーブも生成されているようなので最適化で消されている。最適化で消されているということは何らかの不具合で接続すべきところが接続されていないということだ。

色々と調査をしていると、どうもPusher側の実装を間違えていたことに気が付いた。Pusher側のData Read(Response)コマンドを扱いを間違えてコマンドバスを通じてデータを発行するように実装していたようだ。Responseバスを使うように変更すると正しく生成されるようになってきた。よしよし。

trait Pattern {
  def address: BigInt
  def size: Int
  def bits(edge: OCPEdgeOut): (Bool, OCPBundleCmd)
  def write_data(edge: OCPEdgeOut): (Bool, OCPBundleData)
  def dataIn: Option[BigInt] = None
  require ((address & ((BigInt(1) << size) - 1)) == 0)
}

case class WritePattern(address: BigInt, size: Int, data: BigInt) extends Pattern
{
  require (0 <= data && data < (BigInt(1) << (8 << size)))
  def bits (edge: OCPEdgeOut) = edge.Write(0.U, address.U)
  def write_data (edge: OCPEdgeOut) = edge.WriteData(0.U, data.U)
}

case class ReadPattern(address: BigInt, size: Int) extends Pattern
{
  def bits(edge: OCPEdgeOut) = edge.Read(0.U, address.U)
  def write_data (edge: OCPEdgeOut) = edge.WriteData(0.U, 0.U)
}

case class ReadExpectPattern(address: BigInt, size: Int, data: BigInt) extends Pattern
{
  def bits(edge: OCPEdgeOut) = edge.Read(0.U, address.U)
  def write_data (edge: OCPEdgeOut) = edge.WriteData(0.U, 0.U)
  override def dataIn = Some(data)
}

コマンドチャネルとデータチャネルはMasterとして動作し、レスポンスチャネルはスレーブとして動作する。

    val (plegal, pbits) = pattern.map(_.bits(edgeOut)).unzip
    val (wlegal, wbits) = pattern.map(_.write_data(edgeOut)).unzip
    assert (end || plegal(step), s"Pattern pusher ${name} tried to push an illegal request")

    cmd.valid  := io.run && ready && !end
    cmd.bits   := pbits(step)

    data.valid := io.run && (cmd.bits.mcmd === OCPMessages.Write) && ready && !end
    data.bits  := wbits(step)
f:id:msyksphinz:20200122232209p:plain

昨日とグラフ的には全く一緒だな... 波形を確認してみる。

f:id:msyksphinz:20200122232238p:plain
OCP Diplomacyのシミュレーション結果。何かがおかしい。

何となく動いてそうな...って全然正しく動いていない気がするぞ。デバッグしていこう。

Diplomacyを使ってOCPバスを作成する方法の検討(3. OCP SRAMを作る)

Diplomacyを使ってOCPのバスを作成するプロジェクト。PatternPusherウィジェットの次はSRAMを作成する。SRAMを定義することでクライアントとマネージャの両方を定義することで、やっと回路を構築することができるようになる。OCPのSRAMを定義してみよう。

SRAMはスレーブノードなので、OCPManegerNodeを使用することになる。TileLinkのSRAMをまずはそのまま流用する。

  • chisel-hw/src/main/scala/ocp/SRAM.scala
class OCPRAM(
    address: AddressSet,
    parentLogicalTreeNode: Option[LogicalTreeNode] = None,
    cacheable: Boolean = true,
    executable: Boolean = true,
    atomics: Boolean = false,
    beatBytes: Int = 4,
    ecc: ECCParams = ECCParams(),
    val devName: Option[String] = None,
    val dtsCompat: Option[Seq[String]] = None
  )(implicit p: Parameters) extends DiplomaticSRAM(address, beatBytes, devName, dtsCompat)
{
  val node = OCPManagerNode(Seq(OCPManagerPortParameters(
    Seq(OCPManagerParameters(
      address            = List(address),
      resources          = device.reg("mem"),
      regionType         = if (cacheable) RegionType.UNCACHED else RegionType.IDEMPOTENT,
      executable         = executable,
      supportsGet        = TransferSizes(1, beatBytes),
      supportsPutPartial = TransferSizes(1, beatBytes),
      supportsPutFull    = TransferSizes(1, beatBytes),
      supportsArithmetic = if (atomics) TransferSizes(1, beatBytes) else TransferSizes.none,
      supportsLogical    = if (atomics) TransferSizes(1, beatBytes) else TransferSizes.none,
      fifoId             = Some(0))), // requests are handled in order
    beatBytes  = beatBytes,
    minLatency = 1))) // no bypass needed for this device

supportsGetとか全然使わないけど、とりあえずそのままTileLinkから持ってきて定義している。

中身の実装は以下のようになっている。ガバガバで全然OCPのプロトコルを守っていないが(それどころか連続でリクエストを持ってこられると普通にデータを失ってしまう)、まずはVerilogが生成できるところまで持って行きたい。

  lazy val module = new LazyModuleImp(this) {
    val (in, edge) = node.in(0)

    val sram = Reg(Vec(256, UInt(32.W)))
    val cmd_addr = Reg(UInt(8.W))

    when (in.cmd.valid && in.cmd.bits.mcmd === OCPMessages.Write) {
      cmd_addr := in.cmd.bits.address(7, 0)
    }

    when (in.cmd.valid && in.cmd.bits.mcmd === OCPMessages.Read) {
      in.resp.valid := true.B
      in.resp.bits.data := sram(in.cmd.bits.address)
    } .otherwise {
      in.resp.valid := false.B
    }

    when (in.data.valid) {
      sram(cmd_addr) := in.data.bits.data
    }

    in.cmd.ready := true.B
    in.data.ready := true.B
  }

スレーブノードを作成したので、次にマスターとスレーブを接続してOCPタイルを作ってみる。

  • chisel-hw/src/main/scala/ocp/OCPUnitTest.scala
class OCPUnitTest()(implicit p: Parameters) extends LazyModule {
  val pusher = LazyModule(new OCPPatternPusher("pat1", Seq(
    new WritePattern(0x100, 0x2, 0x012345678L),
    new WritePattern(0x500, 0x2, 0x0abcdef01L),
    new ReadExpectPattern(0x100, 0x2, 0x012345678L),
    new ReadExpectPattern(0x500, 0x2, 0x0abcdef01L)
  )))
  val ram = LazyModule(new OCPRAM(AddressSet(0x0, 0x3ff)))

  ram.node := pusher.node

  lazy val module = new LazyModuleImp(this) with UnitTestModule {
    pusher.module.io.run := true.B
    io.finished := pusher.module.io.done
  }
}

単純にOCPPatternPusherOCPSRAMを接続しただけである。GraphMLを生成してみよう。

f:id:msyksphinz:20200121003459p:plain
OCPマスターとスレーブを接続した図。

良さそうだ。しかし、Verilogを生成してみるとバスごと消されてしまっていた。。。これは要解析。

module OCPUnitTest(
  input   clock,
  input   reset,
  output  io_finished
);
  wire  pusher_clock; // @[OCPUnitTest.scala 18:26]
  wire  pusher_reset; // @[OCPUnitTest.scala 18:26]
  wire  pusher_io_done; // @[OCPUnitTest.scala 18:26]
  OCPPatternPusher pusher ( // @[OCPUnitTest.scala 18:26]
    .clock(pusher_clock),
    .reset(pusher_reset),
    .io_done(pusher_io_done)
  );
  assign io_finished = pusher_io_done; // @[OCPUnitTest.scala 30:17]
  assign pusher_clock = clock;
  assign pusher_reset = reset;