FPGA開発日記

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

オリジナルLLVM Backendを追加しよう (20. Conditional Moveの実装)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

第8章の後半は、Conditional Moveの実装を行っていく。 Cpu0のConditional Moveはどのように指定実装しているのかと思ったら、シレっとCpu0にmovzとmovnという新規命令が追加されていた。 そういうことじゃないんだよ...

ここのオリジナル実装を付け加えるために、非常に苦労してしまった。

// Instantiation of instructions.
def MOVZ_I_I : CondMovIntInt<CPURegs, CPURegs, 0x0a, "movz">;
def MOVN_I_I : CondMovIntInt<CPURegs, CPURegs, 0x0b, "movn">;

このようなConditional Moveの命令はRISC-Vには存在しないので、どのように進めていくのかが問題になる。 オリジナルのRISC-Vの実装を参考にすると、SELECT_CCというOpcodeを使用して実装する。

def SDT_MYRISCVXSelectCC   : SDTypeProfile<1, 5, [SDTCisSameAs<1, 2>,
                                               SDTCisSameAs<0, 4>,
                                               SDTCisSameAs<4, 5>]>;
def SelectCC : SDNode<"MYRISCVXISD::SELECT_CC", SDT_MYRISCVXSelectCC,
                      [SDNPInGlue]>;

これはオリジナルの実装のパクリなのだが、SDT_MYRISCVXSelectCCは新しくConditional Move用のノードを作成する。 SDTypeProfileの定義はあまり情報がないのだが、最初の引数(=1)がOutput Nodeの数、2番目の引数(=5)が入力ノードの数、そして最後に入力ノードの条件を書くらしい。 SDT_MYRISCVXSelectCCを定義すると、SelectCCというノードを定義する。これにより、MYRISCVXオリジナルの命令ノードであるMYRISCVXISD::SELECT_CCというノードが作られる。

このMYRISCVXISD::SELECT_CCに推論される条件を書き下していくわけだ。select IRと条件文を使用して、以下のように定義する。

let usesCustomInserter = 1 in
class SelectCC_rrirr<RegisterClass RC, RegisterClass cmpty>
    : MYRISCVXPseudo<(outs RC:$dst),
             (ins cmpty:$lhs, cmpty:$rhs, simm12:$imm,
              RC:$truev, RC:$falsev),
              "",
             [(set RC:$dst,
               (SelectCC cmpty:$lhs,
                         cmpty:$rhs,
                         (i32 imm:$imm),
                         RC:$truev,
                         RC:$falsev))]>;

余談だがこのusesCustomInserterがとても大切になる。これは、この変換後に作られたSelectCCのIRを、手動で変換する処理が入る、という印になる(という理解であっているかな?)。逆に言うと、このusesCustomInserterを置いておかないと、後続の命令変換の最中にSELECT_CCを変換することができないとしてエラーが出力されてしまう。

これでIRの定義ができたので、次にC++の実装に移る。通常のIRであるSELECTは、直接RISC-Vの命令に変換できないので、上記で定義したSELECT_CCに置き換える作業が必要となる。これが以下のSwitch文で定義される。

  • lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
    case ISD::GlobalAddress: return lowerGlobalAddress(Op, DAG);
    case ISD::SELECT:        return lowerSELECT(Op, DAG);
  }
  return SDValue();
}

lowerSELECTは以下のように定義した。これも要するに、SELECTの構文をSELECT_CCに置き換えている作業となっている。

SDValue MYRISCVXTargetLowering::
lowerSELECT(SDValue Op, SelectionDAG &DAG) const
{
  SDValue CondV = Op.getOperand(0);
  SDValue TrueV = Op.getOperand(1);
  SDValue FalseV = Op.getOperand(2);
  SDLoc DL(Op);

  // (select condv, truev, falsev)
  // -> (myriscvxisd::select_cc condv, zero, setne, truev, falsev)
  SDValue Zero = DAG.getConstant(0, DL, MVT::i32);
  SDValue SetNE = DAG.getConstant(ISD::SETNE, DL, MVT::i32);

  SDVTList VTs = DAG.getVTList(Op.getValueType(), MVT::Glue);
  SDValue Ops[] = {CondV, Zero, SetNE, TrueV, FalseV};

  return DAG.getNode(MYRISCVXISD::SELECT_CC, DL, VTs, Ops);
}

こうしてSELECT_CCの構文は、最後にRISC-Vの比較命令と分岐命令に置き換えていく。これがその実装だ。 (これもオリジナルのRISC-Vのものを丸パクリした)

  • lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
MachineBasicBlock *
MYRISCVXTargetLowering::EmitInstrWithCustomInserter(MachineInstr &MI,
                                                 MachineBasicBlock *BB) const {
  dbgs() << "MYRISCVXTargetLowering::EmitInstrWithCustomInserter\n";

  switch (MI.getOpcode()) {
    default:
      llvm_unreachable("Unexpected instr type to insert");
    case MYRISCVX::Select_GPR_Using_CC_GPR:
      break;
  }
...
  // Insert appropriate branch.
  unsigned LHS = MI.getOperand(1).getReg();
  unsigned RHS = MI.getOperand(2).getReg();
  auto CC = static_cast<ISD::CondCode>(MI.getOperand(3).getImm());
  unsigned Opcode = getBranchOpcodeForIntCondCode(CC);

  BuildMI(HeadMBB, DL, TII.get(Opcode))
    .addReg(LHS)
    .addReg(RHS)
    .addMBB(TailMBB);

  // IfFalseMBB just falls through to TailMBB.
  IfFalseMBB->addSuccessor(TailMBB);

  // %Result = phi [ %TrueValue, HeadMBB ], [ %FalseValue, IfFalseMBB ]
  BuildMI(*TailMBB, TailMBB->begin(), DL, TII.get(MYRISCVX::PHI),
          MI.getOperand(0).getReg())
      .addReg(MI.getOperand(4).getReg())
      .addMBB(HeadMBB)
      .addReg(MI.getOperand(5).getReg())
      .addMBB(IfFalseMBB);

  MI.eraseFromParent(); // The pseudo instruction is gone now.
  return TailMBB;

これで命令を生成してみる。テストにはch8_2_select.cppを使用している。

./bin/clang -c -target mips ../lbdex/input/ch8_2_select.cpp -emit-llvm
./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch8_2_select.bc -o -
# %bb.0:                                # %entry
        addi    x2, x2, -8
        addi    x10, x0, 1
        sw      x10, 4(x2)
        sw      x0, 0(x2)
        lw      x11, 4(x2)
        addi    x12, x0, 0
        xor     x11, x11, x12
        sltu    x13, x0, x11
        addi    x11, x0, 3
        bne     x13, x12, $BB1_2
# %bb.1:                                # %entry
        addi    x10, x11, 0
$BB1_2:                                 # %entry

どうにかうまく生成できたようだ。

f:id:msyksphinz:20190313021000p:plain

オリジナルLLVM Backendを追加しよう (20. 制御構文の実装と最適化Passの追加)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

8.2 章 Long Branchのサポート

第8章の前半はLongBranchのサポート。これは読んでみたがあまりよく分からない。 BAL命令(Branch and Link命令?)向けのサポートだが、RISC-Vには該当する命令が存在しないのでとりあえず追加だけしておいてテストはしない。

8.3章 不要なJump命令のサポート

8.3章は、不要なJump命令の削除するPassを追加する。 これにより、Passを追加することで以下のように生成されていたコードが、

        lw      x10, 12(x2)
        lw      x11, 8(x2)
        add     x10, x10, x11
        sw      x10, 12(x2)
        lw      x10, 8(x2)
        addi    x10, x10, -1
        sw      x10, 8(x2)
        jal     $BB0_9
$BB0_9:                                 # %if.end7
        jal     $BB0_10
$BB0_10:                                # %if.end8
        lw      x10, 4(x2)
...
        lw      x10, 12(x2)
        addi    x2, x2, 16
        ret

以下のように不要なJumpが削除される。

        lw      x10, 12(x2)
        lw      x11, 8(x2)
        add     x10, x10, x11
        sw      x10, 12(x2)
        lw      x10, 8(x2)
        addi    x10, x10, -1
        sw      x10, 8(x2)
$BB0_9:                                 # %if.end7
$BB0_10:                                # %if.end8
        lw      x10, 4(x2)
...

8.4 Delayed SlotのFill

RISC-VにはDelayed Slotが存在しないので不要。

小学校のプログラミング教育に関する学習指導要領を読んでプログラミング授業を想像体験してみた

どうやら2020年度から小学校にプログラミング教育が必修となるらしく、どこもかしこもプログラミング教育でアツい。

小学生向けのプログラミング教室も盛況らしく、英語に加えて、新しい教育の投資先としても注目されているようだ。 しかし、小学生に対してプログラミングってどのように教えればよいのかあまりイメージがつかない。 私自身は教職ではないのであまり関係ないのだが、学生の頃出張授業でMindStormを小学3年生に教えに行ったことがある。 小学生のモチベーションをキープさせるのは大変だった。

  • 小学生「これでちゃんとロボット動きますかー!?」
  • 私「(うーんこれだと動かないと思うけど)やってみようか。ロボットにダウンロードしてみて。」
  • 小学生「動かないー!!つまんない!!」
  • 私「(悲しい...)じゃあどこが間違ってるのかブロックをもう一度見てみようか」

みたいな流れの繰り返しである。うまいこと子供たちを誘導して、ヒントを出しながらプログラムを動かすところまで進めるのは本当に手間がかかる。 先生はこれをクラスの全員に対して実施しなければならないのだから、相当な苦労だと思う。

ここで、プログラミングの多少の経験がある大人のエンジニアが文部科学省をプログラミングに関する学習指導要領を眺めてみると、どんな面白いことが書いてあるのかと思い読んでみた。

資料

プログラミングに関しては、このあたりの資料が読みやすい。

あらかじめ断っておきますが、私は教育のプロフェッショナルではないし、教鞭を取る者でもありません。 ただ単純に情報技術者として、子供たちがどのようなプログラミングの教育を受けるのか、調査してみたかっただけなので、間違って解釈してしまう部分などがある場合は指摘していただければ幸いです。


実施項目と実施時期

まず、見て驚いたのは、別に小学校に限らず中学校、高校でも情報処理に関する科目の内容が変わるところだ。

  • 小学校
    • 必修化。プログラミングを行う実技が導入される。
  • 中学校
    • 倍増。技術家庭科で計測などのプログラミングの項目が増える。
  • 高校
    • すべての高校生に対して、共通必履修科目「情報Ⅰ」が必修となる(!!)。さらに発展したプログラミング科目「情報Ⅱ」も新設される。

これ、どちらかというと小学生よりも高校生の方が影響力大きいんじゃ。。。?

小さく書いてあるが、上記の新学習指導要領は小学校から導入らしい。

  • 小学校 平成32年度から全面実施
  • 中学校 平成33年度から全面実施
  • 高等学校 平成34年度から学年進行で実施
f:id:msyksphinz:20190315230603p:plain
「小学校プログラミング教育に関する概要資料」 p.3より抜粋

ここでは、小学生のプログラミング教育に着目して読み進めていく。

ねらい

プログラミングに取り組む狙いとは。

小学校段階において学習活動としてプログラミングに取り組むねらいは、プログラミング言語を覚えたり、プログラミングの技能を習得したりといったことではなく、論理的思考力を育むとともに、プログラムの働きやよさ、情報社会がコンピュータをはじめとする情報技術によって支えられていることなどに気付き、 プログラミング的思考を育成

長いなあ。しかし大事なことがここに集約されている気がする。 要するにプログラミング言語を覚えることが目的ではない、ということが重要なのだと思う。

  • 「プログラミング的思考」

ここだよ!このプログラミング的思考って何?

自分が意図する一連の活動を実現するために、どのような動きの組合せが必要であり、一つ一つの動きに対応した記号を、どのように組み合わせたらいいのか、記号の組合せをどのように改善していけば、より意図した活動に近づくのか、といったことを論理的に考えていく力

意味は分かる。でもこれってもともと算数とか理系の授業の目的じゃないの? 算数だって、数字を扱う教育ではあるものの、本来は論理的に物事を考えていくことが目的の授業じゃないの? でないと、いくら使わないとはいえ中学校で2次方程式の解の公式とか、sinとかcosとか教えるはずがないと思うんだけど。

※ 余談だが私は算数とか数学は、公式を教える科目ではなく、論理的思考を育むための科目だと思っているので「サインコサインとか将来使わない」という議論は最初から的外れだと思っている。

しかし、プログラミング的思考と論理的思考は少しは異なるところがあるのかな。 プログラミングを行うときは、問題の解答を導き出すためのフローを考える、というアルゴリズムを考えることと、それを「プログラミング言語」という規定のルールにどのように落とし込むか、という技術が必要とされているような気がする。つまり、論理的な考え方を、どのように表現するか、というところまでがプログラミング的思考、ということなのか(間違っているかもしれないが)。

とりあえず、プログラミング教育の目的は3つあるらしい。

  • 【知識・技能】身近な生活でコンピュータが活用されていることや、問題の解決には必要な手順があることに気付くこと。
  • 【思考力・判断力・表現力等】発達の段階に即して、「プログラミング的思考」を育成すること。
  • 【学びに向かう力・人間性等】発達の段階に即して、コンピュータの働きを、よりよい人生や社会づくりに生かそうとする態度を涵養すること。

たしかに、デジタルネイティブの時代に、このような教育は必須だと思う。 これまでは紙でやっていたことを、このようなプログラミング教育によって、「これコンピュータにやらせれば一瞬で終わるじゃん」みたいな発想になってくれるのが目的だと思う。 単純にプログラムが書けることになるのが目的じゃない。 たとえプログラミング自体ができない子供でも、大人になって「そういえばこういうの小学校のプログラミングの授業であった気がする」みたいな体験があれば、業務の効率化につながるのではなかろうか(と、老人の私が勝手に期待)。

具体的な科目と実施内容

とりあえず先に進もう。具体的にどのような授業が行われるのだろうか?

f:id:msyksphinz:20190315232552p:plain
「小学校プログラミング教育の手引(第二版)」p.9より抜粋。

なるほど。図の見方がさっぱりわからん。横軸は科目だけども、縦軸がこんなに長いのはなんでだ? そして、「プログラミング」という科目が増えるわけではないのね。 既存の授業に、プログラミングの内容が取り込まれるという形か。 今は「総合的な学習」という科目がある。ここに、情報技術に関する学習の時間が組み込まれる。

例えば、各科目でどんなプログラミングの授業が増えるのだろう?

  • A-① プログラミングを通して、正多角形の意味を基に正多角形をかく場面(算数 第5学年)
  • A-② 身の回りには電気の性質や働きを利用した道具があること等をプログラミングを通して学習する場面(理科 第6学年)
  • A-③ 「情報化の進展と生活や社会の変化」を探究課題として学習する場面(総合的な学習の時間)
  • A-④ 「まちの魅力と情報技術」を探究課題として学習する場面(総合的な学習の時間)
  • A-⑤ 「情報技術を生かした生産や人の手によるものづくり」を探究課題として学習する場面(総合的な学習の時間)

待って算数難しくない?

と思ったら普通に同じ長さの3本の線を180度/3回しながら線を書くってことか。もっと論理学的な方法かと思った。 (ここで大人は人工知能を使って正三角形を「学習させる」とかやって欲しい)。

それ以外の、総合的な学習の時間のプログラミングでは、「自動販売機」(ステートマシンか!?)とか、「自動運転」などのキーワードも散見される。 非常に面白そうな科目だが、教える側の先生もどうやって進めればよいのか試行錯誤しそうな内容だ。

それ以外に、「学習指導要領には例示されていないが、学習指導要領に示される各教科等の内容を指導する中で実施」という項目がある。ナニコレ。面倒くさそうだなあ。

  • B-② 都道府県の特徴を組み合わせて47都道府県を見付けるプログラムの活用を通して、その名称と位置を学習する場面(社会 第4学年)

ナニコレ?

都道府県の地理的環境や自然条件、面積、人口や特産物などの特色を組み合わせて47都道府県を見付けるプログラムの活用を通して、47都道府県の名称と位置を確かめ、その確実な習得を図ります。 この学習では、47都道府県の特徴が記されたブロックを組み合わせることにより、組み合わせたブロック(特徴)に合致した都道府県の名称と位置を示すプログラムを使用します。

なるほど。プログラミングというか、アプリを使用して47都道府県を覚える、って感じかな?

  • B-③ 自動炊飯器に組み込まれているプログラムを考える活動を通して、炊飯について学習する場面(家庭 第6学年)

組み込みプログラミングだ!!!!家庭科でこんなことするの?

さらに面倒くさそうなのがC分類である。これは、総合的な学習以外で実施しなければならない、ということ?

C 教育課程内で各教科等とは別に実施するもの プログラミングの体験は、各教科等の内容を指導する中で実施するほか、各教科等とは別に(何らかの教科等に位置付けることなく)、かつ教育課程内で、実施することも考えられます。この場合は、児童の負担過重にならないことを前提として、各学校の裁量で行うこととなります。

ええー、例としてはビジュアルプログラミング言語を体験させる、だって?? この項目は、学校と先生によって大きく内容が左右しそうな内容だ。先生方もこれは調整するのが大変そうだ。

こうしたプログラミングの体験は、各学校の創意工夫による取組であるため、どのような力を育みたいのかを明らかにした上で授業内容を検討し、よりよいものにしていくことが望まれます。

厳しいなあ。。。これ、先生方もどのようにして授業を組み立てるつもりなんだろうか?各学校のいろんな取り組みを知りたいところだ。

色々調べると、文部科学省の運営するプログラミング教育のポータルサイトがあった。

miraino-manabi.jp

この中に、C分類の実施例がある。見てみよう。

miraino-manabi.jp

なるほどなあ。これは、いろんな企業などと共同で、まとめた時間を取ってプログラミングを体験する、という項目なのか。社会科見学や社会科実習みたいなものかな? こういうところで、工業学校とか、高等専門学校などが活躍できる場所がありそうだ。

プログラミング以外で教えてほしいところ

個人的には、プログラミング教育だけでなく、情報リテラシーなどの教育なども強化してほしいところだ。

例えば、フェイクニュースを判別する力を養ったり、膨大な情報の中から自分の必要な情報を見つけだす力や、SNSなどのインターネット上のツールを正しく活用する技術は、デジタルネイティブの世代だからこそ、小学生から養ってもよさそうな気がする。

感想

今までもやもやとした印象だったプログラミング教育だが、学習指導要領には、より具体的に、例を挙げながら説明されており何となくイメージがついた。

しかしここで明らかになったのは、予想以上に学校や先生にゆだねられている裁量が大きいというところ。おそらく最初は先生方も試行錯誤なんじゃないかと思う。 こういうのは数年いろいろトライアル&エラーをして、やっと形が作られものだと思う。 1~2年で成果が出ないからって諦めないで、長い目で見ていきたいものである。

RustでRISC-V命令セットシミュレータを作ろう (4. MMUとスーパバイザモードの実装検討)

f:id:msyksphinz:20190224185310p:plain

Rustで作るRISC-Vシミュレータ。基本的な形が出来上がったので、今度はOSを動かすためのMMUとスーパバイザモードの実装に移っていく。 基本的なところは、C++で作った実装をコピーしていき、Rustに移植するだけでよいと思っている。

メモリアクセスを実行する場合には、一度アドレス変換を行う。この関数もC++で実装したものを移植する。

    fn read_memory_word(&mut self, addr: AddrType) -> XlenType {
        let (_result, phy_addr) = self.convert_virtual_address(addr, MemAccType::Fetch);

システムレジスタへのアクセスが増えてくるので気を付けて実装しなければならないのだが、システムレジスタの実装とビット切り出しのための定数の定義が面倒くさい。 C++ではそのままdefineで定義していたのだが、Rustでよい方法はないだろうか? enumで実装すると、複数の要素が同じ値を取ることが許されないルールなのか、コンパイルエラーとなってしまった。

enum {
  SYSREG_SSTATUS_SD_MSB: u8 = 63;
  SYSREG_SSTATUS_SD_LSB: u8 = 63;   // これはエラーとなる。
...
}

仕方がないので、constですべて定義した。

pub const SYSREG_FCSR_FRM_MSB: u8 = 7;
pub const SYSREG_FCSR_FRM_LSB: u8 = 5;
pub const SYSREG_FCSR_FFLAGS_MSB: u8 = 4;
pub const SYSREG_FCSR_FFLAGS_LSB: u8 = 0;
pub const SYSREG_SSTATUS_SD_MSB: u8 = 63;
pub const SYSREG_SSTATUS_SD_LSB: u8 = 63;
...

次に、各種様々なモードや、例外コードなどを使用しているが、これをenumで定義している。 これを、システムレジスタの値から生成するとき、u8などの値をenumに変換する必要がある。これはどうするのかわからなかったのだが、とりあえずfrom_u8を定義して回避している。

pub enum PrivMode {
    User = 0,
    Supervisor = 1,
    Hypervisor = 2,
    Machine = 3,
}

impl PrivMode {
    pub fn from_u8(n: u8) -> PrivMode {
        match n {
            0 => PrivMode::User,
            1 => PrivMode::Supervisor,
            2 => PrivMode::Hypervisor,
            3 => PrivMode::Machine,
            _ => PrivMode::Machine,
        }
    }
}

あとは、enumの比較は、通常はif文で行うことはできずmatchを使うのが通常儀礼なのだが、これは面倒くさすぎるので、PartialEq, Eqを使って回避している。

#[derive(PartialEq, Eq, Copy, Clone)]
pub enum PrivMode {
    User = 0,
    Supervisor = 1,
...
        if self.get_vm_mode() == VMMode::VmSv39
            && (priv_mode == PrivMode::Supervisor || priv_mode == PrivMode::User)
        {
            let ppn_idx: Vec<u8> = vec![12, 21, 30];
            let pte_len: Vec<u8> = vec![9, 9, 26];

メモリアクセスの例外発生などは、正常なアクセスができたかどうかをTupleで返すことで判定している。これももう少しきれいな実装ができないかなあ。

    fn read_memory_word(&mut self, addr: AddrType) -> XlenType {
        // let result: MemResult;
        // let phy_addr: AddrType;
        let (_result, phy_addr) = self.convert_virtual_address(addr, MemAccType::Fetch);
        assert!(phy_addr >= DRAM_BASE);

MMUのテストパタンは、まだ通らない。もう少しページテーブルウォークの実装を確認する必要がある。

Vivado-HLSを使って高位合成でCPUを作ってみる(9. HLSでAXIのバイトイネーブルはどう制御する?)

Vivado-HLSを使って簡単なRISC-V CPUを作ってみている。

現在の悩みどころはメモリアクセスの制御だ。CPUはワードアクセス以外に、ハーフワード、バイトアクセスなども発生する。 Vivado-HLSのモジュールのインタフェースはAXIだけれども、C言語でこれをどのように記述すればよいのか、悩みどころだ。

たとえば、最小単位がバイトアクセスだからと言って、

int8_t mem[MEM_SIZE];

mem[addr << 2 + 0] = (word >> 0)& 0xff;
mem[addr << 2 + 1] = (word >> 8) & 0xff;
mem[addr << 2 + 2] = (word >> 16) & 0xff;
mem[addr << 2 + 3] = (word >> 24) & 0xff;

などと書いていては、非常に効率が悪いしバイトアクセスが4回発生して性能に影響する。 どうにかして、Vivado-HLSの生成するHDLでAXIのバイトアクセスを制御したい。

とりあえず、現状ではメモリアクセスモジュールのAXIインタフェースで、WSTRBが固定値になっている。ここを変えたいんだー。

...
    .I_WVALID(mem_WVALID),
    .I_WREADY(mem_WREADY),
    .I_WDATA(grp_read_reg_fu_948_ap_return),
    .I_WID(1'd0),
    .I_WUSER(1'd0),
    .I_WLAST(1'b0),
    .I_WSTRB(4'd15),
...

いろいろ調査したのだが、まだ満足した結論には至っていない。 とりあえず、Vivado-HLSのネイティブっぽそうな信号単位であるap_int<>ap_uint<>を導入してデザインを書き直した。

-#include <stdint.h>
+#include <ap_int.h>

-typedef int32_t  XLEN_t   ;
-typedef uint32_t UXLEN_t  ;
-typedef uint8_t  RegAddr_t;
-typedef uint32_t Inst_t   ;
-typedef uint16_t Addr_t   ;
+typedef ap_int<32>  XLEN_t   ;
+typedef ap_uint<32> UXLEN_t  ;
+typedef ap_uint<8>  RegAddr_t;
+typedef ap_uint<32> Inst_t   ;
+typedef ap_uint<16> Addr_t   ;
...
     case SRA : {
-      XLEN_t reg_data = read_reg(m_rs1) >> (uint32_t)read_reg(m_rs2);
+      uint8_t shamt = read_reg(m_rs2) & 0x01f;
+      XLEN_t reg_data = read_reg(m_rs1) >> shamt;
       write_reg(m_rd, reg_data);
       break;
     }

あとは、ap_int<>, ap_uint<>を使ったデザインはfprintfで出力するときも明示的なキャストが必要らしい。

 #ifndef __SYNTHESIS__
-      fprintf(m_cpu_log, "x%02d <= %08x\n", addr, data);
+      fprintf(m_cpu_log, "x%02d <= %08x\n", static_cast<uint32_t>(addr), static_cast<uint32_t>(data));
 #endif // _SYNTHESIS

これで32ビットのAXIバスをCPUに接続した。さてバイトアクセスをどうするか。

ロード命令は32bitロードして残りは捨てればよい。ストア命令は、部分更新となるため、どうしても一度元となるデータを取り込む必要がある。

苦肉の策で、とりあえず現時点では32bitよりも小さいストアの時は、ストア先の32bitワードを一度ロードすることにした。

@@ -465,19 +462,21 @@ XLEN_t rv32_cpu::mem_access (memtype_t op, uint32_t data, uint32_t addr, AccSize
 #endif // __SYNTHESIS__
         switch(size) {
           case SIZE_BYTE  : {
-            m_data_mem[addr] = data;
+            XLEN_t tmp_word = m_data_mem[addr >> 2];
+            tmp_word = (tmp_word & ~(0xff << ((addr & 0x03) * 8))) |
+                       ((data & 0xff) << ((addr & 0x03) * 8));
+            m_data_mem[addr >> 2] = data;
             break;
           }

いやあ、これはダメな気がするな... もう少し賢い実装方法がないのか、調査中...

f:id:msyksphinz:20190226233836p:plain

オリジナルLLVM Backendを追加しよう (19. 分岐命令の実装)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

第8章は制御構文の追加。手始めにまずは分岐命令を実装していく。 分岐命令を生成させるためには、分岐命令向けのクラステンプレートを作って命令を当てはめていく。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// Branch insntructions with 2 register operands.
class CBranch12<bits<7> opcode, bits<3> funct3,
                string instr_asm,
                PatFrag cond_op, RegisterClass RC> :
  FB<opcode, funct3, (outs), (ins RC:$ra, RC:$rb, brtarget12:$imm12),
  !strconcat(instr_asm, "\t$ra, $rb, $imm12"),
  [], IIAlu> {
    let isBranch = 1;
    let isTerminator = 1;
}

ここで、(ins RC:$ra, RC:$rb, brtarget12:$imm12)に当てはめて命令が生成されるのだが、brtarget12というクラスを定義する必要がある。 これは以下のように定義される。OtherVTというは何だ?というかこのあたりの日本語の情報がなくて非常に苦労した。。。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// BEQ, BNE
def brtarget12    : Operand<OtherVT> {
  let EncoderMethod = "getBranch12TargetOpValue";
  let OperandType   = "OPERAND_PCREL";
  let DecoderMethod = "DecodeBranch12Target";
}

getBranch12TargetOpValueはとりあえず関数だけ定義して中身は省略する。

  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCCodeEmitter.cpp
unsigned MYRISCVXMCCodeEmitter::
getBranch12TargetOpValue(const MCInst &MI, unsigned OpNo,
                         SmallVectorImpl<MCFixup> &Fixups,
                         const MCSubtargetInfo &STI) const {
  return 0;
}

次に、分岐命令をすべて定義していく。RISC-Vには、以下の命令が定義されている。CondOpをそれぞれ使用して、Predicateできるような形を作っていく。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
def BEQ     : CBranch12<0b1100011, 0b000, "beq" , seteq,  GPR>;
def BNE     : CBranch12<0b1100011, 0b001, "bne" , setne,  GPR>;
def BLT     : CBranch12<0b1100011, 0b100, "blt" , setlt,  GPR>;
def BGE     : CBranch12<0b1100011, 0b101, "bge" , setge,  GPR>;
def BLTU    : CBranch12<0b1100011, 0b110, "bltu", setult, GPR>;
def BGEU    : CBranch12<0b1100011, 0b111, "bgeu", setuge, GPR>;

次に、LLVM IRから命令を生成させるためのパターンを定義していく。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// brcond for slt instruction
multiclass BrcondPatsSlt<RegisterClass RC, Instruction BEQOp, Instruction BNEOp,
                      Instruction SLTOp, Instruction SLTuOp, Instruction SLTiOp,
                      Instruction SLTiuOp, Register ZEROReg> {
def : Pat<(brcond (i32 (setne RC:$lhs, 0)), bb:$dst),
              (BNEOp RC:$lhs, ZEROReg, bb:$dst)>;
def : Pat<(brcond (i32 (seteq RC:$lhs, 0)), bb:$dst),
              (BEQOp RC:$lhs, ZEROReg, bb:$dst)>;

def : Pat<(brcond (i32 (seteq GPR:$lhs, GPR:$rhs)), bb:$dst),
              (BEQOp RC:$lhs, RC:$rhs, bb:$dst)>;
def : Pat<(brcond (i32 (setueq RC:$lhs, RC:$rhs)), bb:$dst),
              (BEQOp RC:$lhs, RC:$rhs, bb:$dst)>;
...
}

defm : BrcondPatsSlt<GPR, BEQ, BNE, SLT, SLTU, SLTI, SLTIU, ZERO>;

次に、条件なし分岐命令だ。これが無いと分岐命令の条件不成立側が処理できない。 実際にはこのパターンは不十分で、JAL命令は戻り先アドレスを格納できるのだが、それを定義していない(outsが空)。 そこを実装し始めると面倒なので、とりあえず条件不成立の時にジャンプできる最低限のパタンだけ追加する。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
class UncondBranch<bits<7> opcode, string instr_asm>:
  FJ<opcode, (outs), (ins brtarget20:$addr),
  !strconcat(instr_asm, "\t$addr"), [(br bb:$addr)], IIAlu> {
    let isBranch = 1;
    let isTerminator = 1;
    let isBarrier = 1;
    let hasDelaySlot = 0;
}

let isCall = 1 in
def JAL : UncondBranch<0b1101111, "jal">;

さらに、br_ccのパタンも追加しないとうまくコンパイルできないらしい。

  • lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
  // Branch Instructions
  setOperationAction(ISD::BR_CC, MVT::i32, Expand);
  setOperationAction(ISD::BR_CC, MVT::f32, Expand);
  setOperationAction(ISD::BR_CC, MVT::f64, Expand);

これで以下のプログラムをコンパイルしてアセンブリを出力した。

int test_ifctrl()
{
  unsigned int a = 0;

  if (a == 0) {
    a++; // a = 1
  }

  return a;
}
./bin/clang -c -target mips ../lbdex/input/ch8_1_1.cpp -emit-llvm
./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch8_1_1.bc -o -

正しく命令が生成できたようだ。

# %bb.0:                                # %entry
        addi    x2, x2, -8
        sw      x0, 4(x2)
        lw      x10, 4(x2)
        bne     x10, x0, $BB0_2
        jal     $BB0_1
$BB0_1:                                 # %if.then
        lw      x10, 4(x2)
        addi    x10, x10, 1
        sw      x10, 4(x2)
        jal     $BB0_2
$BB0_2:                                 # %if.end
        lw      x10, 4(x2)
        addi    x2, x2, 8
        ret

オリジナルLLVM Backendを追加しよう (18. 様々なデータタイプの実装)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

第7章の残りに取り組む。 浮動小数点の実装はとりあえず省略。RISC-Vではclzもclo命令も存在しないのでどのようにして実現すればよいのかは良く分からない。

次に配列と構造体のサポート。アドレスが微妙にずれるのを防ぐためのサポートとなる。これも元から実装していたのだが、アドレスの変換時の計算を少し追加する必要があるようだ。

  • lib/Target/MYRISCVX/MYRISCVXISelDAGToDAG.cpp
bool MYRISCVXDAGToDAGISel::
SelectAddr(SDNode *Parent, SDValue Addr, SDValue &Base, SDValue &Offset) {
  //@SelectAddr }
...
  // Addresses of the form FI+const or FI|const
  if (CurDAG->isBaseWithConstantOffset(Addr)) {
    ConstantSDNode *CN = dyn_cast<ConstantSDNode>(Addr.getOperand(1));
    if (isInt<12>(CN->getSExtValue())) {

      // If the first operand is a FI, get the TargetFI Node
      if (FrameIndexSDNode *FIN = dyn_cast<FrameIndexSDNode>
                                  (Addr.getOperand(0)))
        Base = CurDAG->getTargetFrameIndex(FIN->getIndex(), ValTy);
      else
        Base = Addr.getOperand(0);

      Offset = CurDAG->getTargetConstant(CN->getZExtValue(), DL, ValTy);
      return true;
    }
  }

最後に、ベクトル型をサポートする。テストパタンを通すために、getSetCCResultTypeを追加する。

%1 = load volatile <4 x i32>, <4 x i32>* %a0, align 16
%2 = load volatile <4 x i32>, <4 x i32>* %b0, align 16
%3 = icmp slt <4 x i32> %1, %2
%4 = sext <4 x i1> %3 to <4 x i32>
@@ -337,3 +344,10 @@ MYRISCVXTargetLowering::isOffsetFoldingLegal(const GlobalAddressSDNode *GA) cons
   // The MYRISCVX target isn't yet aware of offsets.
   return false;
 }
+
+EVT MYRISCVXTargetLowering::getSetCCResultType(const DataLayout &, LLVMContext &,
+                                               EVT VT) const {
+  if (!VT.isVector())
+    return MVT::i32;
+  return VT.changeVectorElementTypeToInteger();
+}
diff --git a/lib/Target/MYRISCVX/MYRISCVXISelLowering.h b/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
index 2418473550c..d8b80c7b2c9 100644
--- a/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
+++ b/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
@@ -240,6 +240,11 @@ class MYRISCVXTargetLowering : public TargetLowering {
                       const SmallVectorImpl<ISD::OutputArg> &Outs,
                       const SmallVectorImpl<SDValue> &OutVals,
                       const SDLoc &dl, SelectionDAG &DAG) const override;
+
+  /// getSetCCResultType - get the ISD::SETCC result ValueType
+  EVT getSetCCResultType(const DataLayout &DL, LLVMContext &Context,
+                         EVT VT) const override;
+
 };

これで、以下のコードをコンパイルできるようになる。

  • ch7_1_vector.cpp
int test_cmplt_long() {
  volatile vector8long a0 = {2, 2, 2, 2, 1, 1, 1, 1};
  volatile vector8long b0 = {1, 1, 1, 1, 2, 2, 2, 2};
  volatile vector8long c0;
  c0 = a0 < b0; // c0[0..3] = {0, 0, ...}, c0[4..7] = {-2147483647=0x80000001, ...}
  
  return (c0[0]+c0[1]+c0[2]+c0[3]+c0[4]+c0[5]+c0[6]+c0[7]); //4
}

こんなコードが生成された。とりあえず、ベクトル型がRISC-Vの命令になって出力されるようだ。

...
        lw      x5, 60(x2)
        lw      x6, 56(x2)
        lw      x7, 52(x2)
        lw      x28, 48(x2)
        lw      x29, 44(x2)
        lw      x30, 40(x2)
        lw      x31, 36(x2)
        lw      x4, 32(x2)
        slt     x17, x17, x4
        addi    x4, x0, 0
        sub     x17, x4, x17
        slt     x16, x16, x31
        sub     x16, x4, x16
        slt     x15, x15, x30
        sub     x15, x4, x15
        slt     x14, x14, x29
        sub     x14, x4, x14
        slt     x13, x13, x28
        sub     x13, x4, x13
        slt     x12, x12, x7
        sub     x12, x4, x12
        slt     x11, x11, x6
        sub     x11, x4, x11
        slt     x10, x10, x5
        sub     x10, x4, x10
        sw      x10, 28(x2)
        sw      x11, 24(x2)
        sw      x12, 20(x2)
...