Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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} }

Slide 12

Slide 12 text

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 この部分の記述

Slide 13

Slide 13 text

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を拡張できる

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

テスト環境

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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系のテストのみ

Slide 18

Slide 18 text

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)

Slide 19

Slide 19 text

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) <- io.in zip m_decs) { m_dec.io.in <> in } // dec.io.out(M) <-> arb.io.in(N) for ((m_dec, i) <- m_decs.zipWithIndex; (m_arb, j) <- m_arbs.zipWithIndex) { m_dec.io.out(j) <> m_arb.io.in(i) } // io.out(N) <-> arb.io.out(N) for ((out , arb) <- io.out zip m_arbs) { out <> arb.io.out } }

Slide 20

Slide 20 text

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) } })

Slide 21

Slide 21 text

合成結果 ・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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

参考資料 ・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