Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ChiselでシンプルなRISC-Vコアを作った話

diningyo
February 24, 2020

 ChiselでシンプルなRISC-Vコアを作った話

第1回 自作CPUもくもく会で発表で使う資料

diningyo

February 24, 2020
Tweet

Other Decks in Technology

Transcript

  1. ChiselでシンプルなRISC-Vコアを作った話
    2020/02/24 第一回自作CPUもくもく会
    @diningyo

    View Slide

  2. 登壇者について
    なまえ:だいにんぎょー
    ついったー:@diningyo (適当に作ったGmailのアドレス由来)
    しごと:デジタル回路の論理設計@某半導体設計会社
    せんもん:今の所ネットワーク関連のアクセラレータ(TCP/IPとかIPsecとか)
    ハマっているもの:Chisel(マイクラではなくてプログラミング言語のほう)
    → ブログ(*1)
    とか同人誌(*2)
    作って日々布教中。
    → 技術書典8で二冊目出すつもりだった(あとでBOOTHで販売予定)
    *1)ハードウェアの気になるあれこれ: https://www.tech-diningyo.info/
    *2)Chisel始めたい人に読んで欲しい本: https://diningyo.booth.pm/items/1718207

    View Slide

  3. モチベーション
    ・Chiselが少し分かったので、何か達成感があるものが作りたい!
    ・そこそこに簡単で、かつ動いた!!ってなるものは何かないかしら??
    ・githubにはriscv-sodorがある!!
    → 簡単なRISC-VのCPUを作ってみよう!!
    ・とりあえず、以下の目標を達成するために開発を開始
    → riscv-testsのrv32iで必要なテストを全部通す

    View Slide

  4. Chisel
    ・UCBerkeleyが作ったハードウェア記述言語
    ・Scalaの内部DSLとして実装されている
    ・ChiselのモジュールはFIRRTLという中間表現を経てVerilogのRTLに変換される
    class MyModule extends Module {
    val io = IO(new Bundle {
    val in = Input(Bool())
    val out = Output(Bool())})
    // wire宣言
    val w_in = Wire(Bool())
    // reg宣言
    val r_out = RegInit(false.B)
    // 接続は”:=”
    w_in := io.in
    r_out := w_in
    io.out := r_out
    }
    module MyModule
    (
    input clk
    ,input reset
    ,input in
    ,output reg out
    );
    wire w_in;
    assign w_in = in;
    always @(posedge clk) begin
    if (reset) out <= 1'b0;
    else out <= w_in;
    end
    endmodule : MyModule

    View Slide

  5. 作ったRISC-Vコア(dirv)
    ・RV32Iのみ
    ・2-stageパイプライン(F-DEMW)
    ・User-Level ISA version 2.2
    ・Privileged ISA version 1.10
    ・割り込みは未サポート
    ・上記だけだと面白くないので、追加で以下の要素も
     ・NIC
     ・UART
    ・タイトルからも分かる通りChiselで作成

    View Slide

  6. 開発環境
    ・Ubuntu 16.04 LTS
    ・IntelliJ IDEA :Scala/Chiselの開発環境
    ・シミュレータ:Verilator 4.0.12(Chiselのテスト環境→VerilatorのSim)
    ・波形ビューア:GtkWave
    ・論理合成:Vivado 2018.3

    View Slide

  7. 全体のブロック図
    ・dirvからはInstruction/Dataのバスを独立に2port持つ形
    ・メモリもデュアルポートのRAMにしてInstruction/Dataで別々にアクセス
    ・MbusICでアクセスの制御を行うのはデータのみ

    View Slide

  8. Dirvのブロック図
    ・IFU/IDU/EXU/LDUとシンプルな構成
    ・ISAを確認しつつ、分からないところは既存の実装を確認して実装を進める

    View Slide

  9. バスの設計
    ・シンプルにするためにready-validのハンドシェイクを使ったプロトコル
    ・cmdは共用で、read/writeは独立
    ・最初なのでバースト転送とかは考えずにシングルのリード/ライトのみ
    ・size = byte/half/word
    ・addr/sizeを出すので、strbは別に無くてもよかった・・・
    リードの波形 ライトの波形

    View Slide

  10. 各ブロックのChiselコードを少しだけ

    View Slide

  11. IFU
    ・ChiselのIFUモジュール宣言部分
    ・Moduleクラスを継承して任意のモジュールを実装していく
    class Ifu(implicit cfg: Config) extends Module {
    val io = IO(new IfuIO())
    val sIdle :: sFetch :: Nil = Enum(2)
    // 以下はリセット解除後の初期命令フェッチのトリガー
    val initPcSeq = Seq.fill(3)(RegInit(false.B))
    val initPc = !initPcSeq(2) && initPcSeq(1)
    // Seqの要素を1つずつずらしていく
    when (!initPcSeq.reduce(_ && _)) {
    (Seq(true.B) ++ initPcSeq).zip(initPcSeq).foreach{case (c, n) => n := c}
    }

    View Slide

  12. IDU
    ・RISC-Vの命令デコード記述の抜粋
     → 実際には140行くらいのコード
    ・論理を実体化する部分
    class Idu(implicit cfg: Config) extends Module with InstInfoRV32 {
    val io = IO(new IduIO())
    val inst = Wire(new InstRV32())
    // InsrRV32のクラスで定義したメソッドを呼んでデコード論理を実体化する。
    inst.decode(io.ifu2idu.valid && io.ifu2idu.ready, io.ifu2idu.inst)
    def decode(dataValid: Bool, data: UInt): Unit = {
    funct7 := data(funct7Msb, funct7Lsb)
    rs2 := data(rs2Msb, rs2Lsb)
    rs1 := data(rs1Msb, rs1Lsb)
    funct3 := data(funct3Msb, funct3Lsb)
    rd := data(rdMsb, rdLsb)
    opcode := data(opCodeMsb, opCodeLsb)
    jalr := opcode === "b1100111".U
    この部分の記述

    View Slide

  13. EXUの中のALU
    ・ALUの中のrv32iの命令の処理部分
     → rv32iAluの命令のみを固めてScalaのSeqに実装。
     →M-extensionとかをサポートする際にはval aluにM用のSeqを足せばOK
    val rv32iAlu = scala.collection.mutable.Seq(
    (add || aluThrough) -> (rs1 + rs2),
    (inst.slti || inst.slt) -> (rs1.asSInt() < rs2.asSInt()).asUInt(),
    (inst.sltiu || inst.sltu) -> (rs1 < rs2),
    inst.sub -> (rs1 - rs2),
    (inst.andi || inst.and) -> (rs1 & rs2),
    (inst.ori || inst.or) -> (rs1 | rs2),
    (inst.xori || inst.xor) -> (rs1 ^ rs2),
    (inst.slli || inst.sll) -> (rs1 << shamt)(cfg.arch.xlen - 1, 0),
    (inst.srli || inst.srl) -> (rs1 >> shamt),
    (inst.srai || inst.sra) -> (rs1.asSInt() >> shamt).asUInt(),
    inst.lui -> inst.immU)
    val alu = rv32iAlu // ++ Seq(他のextension)でALUを拡張できる

    View Slide

  14. LSU
    ・LSUのミスアラインチェック用のメソッド
     → Verilogのfunction文と同様のイメージ
    def setMaAddr(dataReq: Bool, addr: UInt, size: UInt): ExcMa = {
    val uaMsb = log2Ceil(cfg.arch.xlen / 8)
    val uaAddr = addr(uaMsb - 1, 0)
    val excReq = MuxCase(false.B, Seq(
    (size === MbusSize.half.U) -> uaAddr(0).toBool(),
    (size === MbusSize.word.U) -> (uaAddr =/= "b00".U)
    ))
    val ret = Wire(new ExcMa())
    ret.excReq := excReq && dataReq
    ret.excAddr := addr
    ret
    }
    io.lsu2exu.excRdMa := setMaAddr(inst.loadValid, io.exu2lsu.memAddr, size)
    io.lsu2exu.excWrMa := setMaAddr(inst.storeValid, io.exu2lsu.memAddr, size)

    View Slide

  15. テスト環境

    View Slide

  16. テスト環境構築
    ・DirvTesterはChsielのテストクラスでriscv-testsのHexデータの供給を行う
    → 処理はただ単にfin信号をチェックして終了するだけ
    ・SimDtmは検証用のトップモジュール
    テスト環境 DirvUnitTesterの実装
    b.breakable {
    reset()
    step(1)
    for (_ if (peek(c.io.fin) == 0x1) {
    println("c.io.fin becomes high." +
    "**TEST SUCCEEDED**")
    b.break
    }
    step(1)
    }
    }
    expect(c.io.fin, true)

    View Slide

  17. RISC-Vのビルド環境の構築
    ・テスト用のデータ作成が目的
    ・まずはRISC-Vのビルド環境とriscv-testsのビルドを実行
    ・ビルド環境はcrosstool-NGを使って構築(去年の4月〜)
    → http://crosstool-ng.github.io/docs/
    ・riscv-toolsリポジトリを使うと、SW系のエコシステムをまとめてビルド
    → https://github.com/riscv/riscv-tools
      → isa-sim/opcodes/openocd/pk/testsがビルド
      → 今回使ったのはriscv-pkとriscv-tests
      → riscv-testsをriscv-pkで動かして作ったログがデバッグに有効
    ・手順はどちらもリンク先に丁寧に書いてあるので割愛
    ・rv32miの構成だと必要になるテストはrv32ui/rv32mi系のテストのみ

    View Slide

  18. NIC
    ・ChiselのArbiterとDecoderを利用して作成
    ・渡したアドレスマップに従ってデコードが行われ、要求がアービターに出力
    // Tmp.
    // 0x0000 - 0x7fff : Memory
    // 0x8000 - 0x8100 : Uart
    val base_p = MbusICParams(
    MbusRW,
    // マスタの設定、タプルにしたけど現状意味なし。。。
    Seq((0x0, 0x1000),
    (0x0, 0x1000)),
    // ここがメモリマップの設定
    // Seqのサイズに応じた個数のアービターが作られる
    Seq((0x0, 32 * 1024),// MemTop (32KBytes)
    (32 * 1024, 0x100) // Uart
    ), 32)

    View Slide

  19. NIC
    ・DecoderとArbiterのインスタンスと接続処理は以下の感じ
     → パラメータで色々渡せるのは便利
    class MbusIC(p: MbusICParams) extends Module {
    val io = IO(new MbusICIO(p))
    // パラメータに従ってデコーダとアービターを必要数インスタンスする
    val m_decs = p.decParams.map( dp => Module(new MbusDecoder(dp)))
    val m_arbs = p.arbParams.map( ap => Module(new MbusArbiter(ap)))
    // io.in(N) dec(N).io.in
    for ((in , m_dec) in }
    // dec.io.out(M) arb.io.in(N)
    for ((m_dec, i) m_dec.io.out(j) <> m_arb.io.in(i)
    }
    // io.out(N) arb.io.out(N)
    for ((out , arb) arb.io.out }
    }

    View Slide

  20. Uart
    ・XilinxのUartLiteマクロの仕様書をベースに実装
     →コードは制御部のポートのパラメタライズ部分
    https://japan.xilinx.com/products/intellectual-property/axi_uartlite.html
    // directionでTX/RXを切り替え
    class Ctrl(direction: UartDirection,
    durationCount: Int) extends Module {
    val io = IO(new Bundle {
    val uart = direction match {
    case UartTx => Output(UInt(1.W))
    case UartRx => Input(UInt(1.W))
    }
    val reg = direction match {
    case UartTx => Flipped(new FifoRdIO)
    case UartRx => Flipped(new FifoWrIO)
    }
    })

    View Slide

  21. 合成結果
    ・FPGAはArty-35Tを使用
    ・周波数は50MHz
    ・SysUartブロックでの合成結果は下記のとおり
     →参考にしたSCR1はrv32imcでデバッグモジュール付きで4500LUTくらいだった
     →サイズ的にはこんなもんかなぁ、、という感じ
    Name LUTs BlockRAM F7 Mux F8 Mux Slice
    dirv 1904 0 256 14 714
    mbus_ic 236 0 0 0 139
    mem 79 16 0 0 56
    uart 189 0 28 12 117

    View Slide

  22. UARTの動作結果
    ・リセットボタン押すとHello, World!を出すだけのプログラムを実行
    ・適当に作った割にはきちんと動いてくれました

    View Slide

  23. Chiselと生成したRTL
    ・Chiselで書いたコード行数は3067行に対して生成されたRTLは7928行
     →レジスタがあると初期値をランダムにするために`ifdefブロックが入る(50行くらい)
     →実際にはパラメタライズでもう少し効率はいいかな?
    ・やっぱり読みづらい
     →ある程度の緩和は可能
    ・ChiselのMemはリセットが入らない
     →ASIC作るときは困るところあるんじゃなかろうか
     →代替案はVecになるけど、たまに凄いRTL生成する
     
     
    initial begin
    `ifdef RANDOMIZE
    `ifdef INIT_RANDOM
    `INIT_RANDOM
    `endif
    `ifndef VERILATOR
    `ifdef RANDOMIZE_DELAY
    #`RANDOMIZE_DELAY begin end
    `else
    #0.002 begin end
    `endif
    `endif
    _RAND_0 = {1{`RANDOM}};
    `ifdef RANDOMIZE_MEM_INIT
    always @(posedge clock) begin
    if(ram_addr__T_5_en & ram_addr__T_5_mask) begin
    ram_addr[ram_addr__T_5_addr] <= ram_addr__T_5_data;
    end

    View Slide

  24. Chiselでの実装の感想
    ・実装時は手探りでやってたので、謎にハマることも多々。。
     →ScalaもChiselも一緒に学んだことが一因
     →Verillatorとfirrtl/treadleで動きが違う、、とか 
    ・ChiselのハードウェアとScalaの変数の境目が分かる楽になる
     →パラメタライズとかがうまく行くと気持ちいい!
     →System Verilogのgenerateやfunctionを使いこなす感覚
    ・テスト環境が作りやすいので、テストを書くことへの敷居が下がった!
     →簡単なテストはChiselのテストクラス、それ以外はテストベンチ作って使うといい感じ
     →普通のRTL実装のテストフレームワークとして使うのも良いかも
    ・個人的には書いていて楽しい!!(慣れはいるけど)
     
     

    View Slide

  25. まとめ
    ・Chiselの習熟も兼ねてRISC-VのISAで一番シンプルなrv32iのコアを作ってみた
     → コア単体を作るのに1週間位
     → そのあとNIC+Uartでまた1週間位
     → Chiselの記述を色々試しながら作ったので、少し時間がかかった。
    ・Chiselで書くのも楽しいので、興味があれば是非!!
    ・以下の3つが実装する上でものすごくありがたかった。。。
     → コンパイラやエミュレータ等のSWが充実していること
     → 基本的な動作を書いたテストが存在していること(riscv-tests)
     → 参考になる実装がgithub等で色々公開されていること
    ・何もわからん!!から手探りで始めてもなんとか作れる!!
     → HW/SWの両面の知識が得られるのでお得感が高い!

    View Slide

  26. 今後の展開
    ・以下を進めていきたい
     1.割り込みのサポート
     2.ベンチマーク試験の実施
     3.riscv-complianceの実施
     4.C-extensionのサポート
     5.M-extensionのサポート
     6.User-modeのサポート
     

    View Slide

  27. 参考資料
    ・riscv-tools : https://github.com/riscv/riscv-tools
    ・crosstool-NG:https://crosstool-ng.github.io/
    ・Chisel:https://github.com/freechipsproject/chisel3/wiki
    ・riscv-sodor:https://github.com/ucb-bar/riscv-sodor
    ・scr1:https://github.com/syntacore/scr1
    ・作ったCPU(dirv):https://github.com/diningyo/dirv

    View Slide