lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

Chiselを使ったRISC-Vの勉強(1)

前回紹介したようにプロトタイプ用のFPGAができて, その後USB Type-C化などもいたしました。
これで電力問題が解決したため、FPGA基板からRaspiに給電かつ給電時の衝突防止までできるようFETを追加する改造もしました。


f:id:Lynx-EyED:20200420161916j:plain

さて、とても使いやすいRISCVのSCR1を紹介する予定でしたが、こちらはまた後での紹介にしようかなと思います。
なおgithubにSCR1のCyclone10LP向けの修正は置いてありますので参照ください。
GitHub - panda5mt/fpga-sdk-prj: FPGA-based SDK projects for SCRx cores

Chiselやるか

あまり気乗りはしてなかったのですが、昨今のコロナの影響で、思うように部品調達ができず仕事が従来の半分くらいの稼働率なのでChiselを勉強しています。動機としては

  • UC BerkeleyでもRISC-Vの実装に使われている
  • HDLのようにとりあえずシミュレーションのために合成するという手間が省けそう
  • 高位合成言語ではない(抽象度は同じ)
  • 自前のRISC-Vが必要になってきた

と言った感じでしょうか。
合成時間中にいろいろ閃いたことを忘れたり、ロジックが複雑になると合成時間だけでかなりもったいない。
あと、自分の開発の仕方に問題があるとは思うのですが、複雑なロジックの場合、単純なロジックから組み立ててシミュレーションするのですが、それでも結構作り込んでからシミュレーションしてしまう(FPGA実機でうごいてるし、わざわざめんどくさいとか。そんなの)。複雑化していくと指数関数的にシミュレーションも遅くなっていきます。
あと、この期に及んでフルスクラッチする理由としては入手できるRISC-V実装がバスがオリジナル(おそらく学習用)、またはAXIベース(その方が汎用性は高い)であり、SCR1でもAvalon-MMへはバスブリッジを利用していてQsysで細かい設定をしたい自分としては不便ということがあります。
またSCR1はパイプラインが最大4段となっていて、CPUストール頻度が5段に比べてかなり高い。

などなどがあります。intel PSG FPGAに実装する前提で、はじめからCPUバスを意識して実装してフルスクラッチで実装していきたい。

参考となったブログ、サイト

  • Chisel handbook

https://github.com/schoeberl/chisel-book/wiki/chisel-book.pdf

  • Test(PeekPokeTester)の書き方

Chiselで作ったモジュールをPeekPokeTesterでテストするまでの流れのまとめ - ハードウェアの気になるあれこれ

  • 初めはここを見ながら書きました。とても丁寧なまとめ

ChiselのBundleの使い方をまとめてみる - ハードウェアの気になるあれこれ

  • IFステージの実装はここを参考に

Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (1. デザインの作成) - FPGA開発日記

方針

基本的にCPUは以下のステージ構成を取るはずです。命令が複数存在しているCPUであることが前提ですが。。

(IF)命令フェッチ
(ID)命令デコード
(EX) 演算
(MA)メモリアクセス
(WB)レジスタ書き込み

  • それぞれのステージをChiselで記述、テスト
  • 上記ステージを接続
  • シングルパイプラインCPUとしてテスト
  • 5ステージパイプライン化
  • 64bit(RV64I)化

とりあえず、命令をフェッチする部分を記述します。
今の状態ですとはっきり言ってVerilog/VHDLで記述した方が10倍速で組めそうですが、我慢して組んでいきます。上記参考ブログでも書かれていますが、本当にBundleは慣れると武器になるはず。
まず、CPUのPCとかメモリの前にインターフェースを記述しておきます。後でAvalon-MMに移行しやすいように。
一部抜粋になります。

// address channel bundle
class AddressChannel extends Bundle {
    val req     = Output(Bool())        // request signal
    val addr    = Output(UInt(32.W))   // address (32bit)
}

// data channel bundle
class DataChannel extends Bundle {
    val ack = Output(Bool())        // data is available ack
    val data = Output(UInt(32.W))   // data (32bit)
}


// HOST :read only(IMem)
// HOST :read/Write(Dmem)
class HostIf extends Bundle {
    // IO definition
    val r_ach = new AddressChannel
    val r_dch = Flipped(new DataChannel)    // flipped I/O
    // write operation
    val w_ach = new AddressChannel   
    val w_dch = new wDataChannel	
//(中略)
}

// Memory-Mapped Slave IF
// Slave :read/Write(IMem)
// Slave :read/Write(Dmem)
class SlaveIf extends Bundle {
    // IO definition
    // read operation
    val r_ach = Flipped(new AddressChannel) // flipped I/O
    val r_dch = new DataChannel	
    // write operation
    val w_ach = Flipped(new AddressChannel) // flipped I/O
    val w_dch = Flipped(new wDataChannel)	

}

基本的インターフェース(read/write)を記述し、ホスト側スレーブ側に割り当てます。
Flippedを使うとIn/Out logicが反転するので記述が楽になります。

CPU側。PCをカウントアップし、アドレスをインターフェースに与えるだけ。
こちらも一部抜粋

class Cpu extends Module {
    val io = IO(new HostIf)
    
    // initialization    
    val r_addr  = RegInit(0.U(32.W))
    val r_data  = RegInit(0.U(32.W))    
    val r_req   = RegInit(true.B)       // fetch signal
    val r_rw    = RegInit(false.B)
    val r_ack   = RegInit(false.B)

    val w_req   = RegInit(true.B)
    val w_ack   = RegInit(false.B)
    val w_addr  = RegInit(0.U(32.W))
    val w_data  = RegInit(0.U(32.W)) 
    
    when (io.sw.halt === false.B){     
        when(r_ack === true.B){
            r_addr := r_addr + 4.U(32.W)    // increase program counter
        }
    }

// (中略)
    
    io.r_ach.addr   := r_addr
    io.r_ach.req    := r_req
    
    // write process
    io.w_ach.addr   := w_addr
    io.w_dch.data   := w_data
    io.w_ach.req    := w_req
    //io.w_dch.ack := w_ack
    
    // read process
    r_ack  := io.r_dch.ack
    r_data := io.r_dch.data 
    

}

メモリ側は割愛。必要であればこの記事最後にあるGithubからCloneしてください。

トップモジュールでCPUとmemoryをコネクトします。

class CpuBus extends Module {

    val cpu     = Module(new Cpu)
    val memory  = Module(new IMem)
    
  //(中略)

    // Read memory
    memory.io.r_ach.req     <> cpu.io.r_ach.req
    memory.io.r_ach.addr    <> cpu.io.r_ach.addr
    cpu.io.r_dch.data       <> memory.io.r_dch.data 
    cpu.io.r_dch.ack        <> memory.io.r_dch.ack

    // write memory
    memory.io.w_ach.req     <> cpu.io.w_ach.req
    memory.io.w_ach.addr    <> cpu.io.w_ach.addr
    memory.io.w_dch.data    <> cpu.io.w_dch.data
    cpu.io.w_dch.ack        <> memory.io.w_dch.ack

}

この後、テストオブジェクトからメモリへダミーデータを与え、CPUがフェッチできているか確認します。

// CPUを停止させ、メモリに書き込む
 poke(c.io.sw.halt, true.B)
// メモリへ書き込み            
for (addr <- 0 to (memarray.length * 4 - 1) by 4){    
	poke(c.io.sw.wAddr, addr)
	poke(c.io.sw.wData, memarray(addr/4))                
	step(1)     
}
step(1)
poke(c.io.sw.halt, false.B) // CPUを再開させる

for (lp <- 0 to (memarray.length - 1) by 1){
	val a = peek(c.io.sw.addr)
	val d = peek(c.io.sw.data)	
	println(f"addr =  0x$a%08X, data = 0x$d%08X") // メモリアドレス、データを16進表記
	step(1)
}

動作確認。make testで動作するようMakefileに記述してあります。


f:id:Lynx-EyED:20200420161633p:plain
一応動いてそう。。


ここまでのコード全文は以下で。コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.3 --depth 1 

sbtやjava導入後、上記Githubからcloneして,make testすると動作します。
きょうはここまで。