lynxeyedの電音鍵盤

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

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

汎用レジスタ(x0-x31)の実装

RISC-V(RV32I)の汎用レジスタは32bit長で32個あります。


f:id:Lynx-EyED:20200423201049j:plain
下記Manualのpp.10(Fig2.1)参照
https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf

まずRISC-Vの命令デコードには必須なのでこちらを用意します。
Chiselだとレジスタ宣言、ゼロ初期化まで1行でかけます。x0 = zero registerで固定ですが、めんどくさいのでx1-x31とまとめて確保しました。

val rv32i_reg   = RegInit(VecInit(Seq.fill(32)(0.U(32.W)))) // x0 - x31:All zero initialized

デバッグトレース

今のところ、デバッグトレースで必要なものは何かを考え、テストオブジェクトに入出力できるようにしました。

  • 汎用レジスタ (x0-x31)
    • x0は要らないと思ったのですが、論理回路の設計ミスでx0にゼロ以外が書き込めちゃったりしたのでしばらくの間監視できるようにします。アセンブラによるエラーを期待してもいいのですが、そうすると悪意あるコードからHWを守れないので。

  • PC
    • Program counterの読み書きができるようにしました。halt信号(後述)をtrueにしてから書き換え推奨。
  • halt信号
    • 前回の記事で既に実装済み。命令メモリへの書き込みや汎用レジスタの読み書き、PC書き換え、その他デバッグの時にこの信号をtrueにしてCPUを停止させます。
  • 命令メモリ

(ID)命令デコードの実装

力弱く、addiだけ実装しました。これを実装すると、上記で用意したx0-x31までのテストができるからです。

以下のようなアセンブラを記述します。

addi x1, x0, 0; // x1 = x0 + 0 (=0) 
addi x2, x0, 1; // x2 = x0 + 1 (=1) 
addi x3, x0, 2; // x3 = x0 + 2 (=2) 
addi x4, x0, 3; // x4 = x0 + 3 (=3) 
….
addi x31, x0, 1E; x31 = x0 + 0x1E (=30) 

ここまで説明しておいてアレですけどアセンブラ流し込むスクリプトを記述してなかったので(おい)、、機械語で書きましょう。
AddiはRISC-Vの中でI-Typeの命令語で12bitの即値(imm)、1つの代入元(ソース)レジスタ(rs1)、1つの代入先(デスティネーション)レジスタ(rd)から構成されます。rs1はx0~x31、rdはx1~x31上にあるいずれかのレジスタ*1となります。


f:id:Lynx-EyED:20200423203344j:plain
ADDIのopcodeはb0010011、funct3はb000です。

f:id:Lynx-EyED:20200423203521j:plain
よって、addiの機械語b????_????_????_????_?000_????_?001_0011であれば良いことがわかります。16進数で表記すると、

  0x00000093, // addi x1,x0,0 (x1 = x0 + 0 = 0)
  0x00100113, // addi x2,x0,1 (x2 = x0 + 1 = 1)
  0x00200193, // addi x3,x0,2 (x3 = x0 + 2 = 2)
  0x00300213, // addi x4,x0,3 (x4 = x0 + 3 = 3)
  ....
  0x01E00F93  // addi x31,x0,30

となります。うっひょ、マシン語とか超楽しいマジ無理。早くアセンブラからバイナリ流し込む機能実装しないと。
Chiselで機械語の判定はBitPatを使うと簡単にできます。

when (inst_code === BitPat("b????_????_????_????_?000_????_?001_0011")){ 
        // ADDI = imm12=[31:20], src=[19:15], funct3=[14:12], rd=[11:7], opcode=[6:0]
        val dest = inst_code(11,7) // rd-pointer
        val imm  = inst_code(31,20) // imm
        val src  = inst_code(19,15)  // src-pointer
        val dsrc = rv32i_reg(src)  //rs
        when (0.U < dest && dest < 32.U){ //アドレスは1~31か?
            rv32i_reg(dest) :=  dsrc + imm
        }
}

と言った感じです。
テストコードを書き直します。1部抜粋です。

poke(c.io.sw.halt, true.B) // CPUを停止
step(1)
for (addr <- 0 to (memarray.length * 4 - 1) by 4){    
  poke(c.io.sw.wAddr, addr)
  poke(c.io.sw.wData, memarray(addr/4)) // 指定アドレスに機械語をロード
  println(f"write: addr = 0x${addr}%08X, data = 0x${memarray(addr/4)}%08X")
  step(1)
}
poke(c.io.sw.w_pc, 0)   // プログラムカウンタをゼロに戻す=reset
step(1)                 // fetch pc  

poke(c.io.sw.halt, false.B) // CPUを再開させる
 step(1)

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"read : addr = 0x$a%08X, data = 0x$d%08X") // メモリ上の機械語のダンプ
  step(1)
}

step(1)
poke(c.io.sw.halt, true.B)
step(1)
for (lp <- 0 to 31 by 1){

  poke(c.io.sw.gAddr, lp)
  step(1)
  val d = peek(c.io.sw.gData)

  println(f"read : x$lp%2d = 0x$d%08X") //x0 ~ x31レジスタの内容をダンプ
  step(1)
}

動作確認


f:id:Lynx-EyED:20200423205112p:plain
うん、うごいた。

コード全文

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

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

きょうはここまで。

*1:x0はゼロレジスタなので代入先(デスティネーション)レジスタにはなれない