Posts Issued on November 13, 2026

posted by sakurai on November 13, 2026 #1149

命令メモリと逆アセンブル(Step 2)

Step 2では、Step 1で仮に置いていた命令メモリを、BRAMで作った本物の命令メモリに置き換えました。あわせて逆アセンブル関数を作り、<ID>で命令の中身が見えるようにしました。

命令メモリ

命令メモリ(IMem)は、BRAMCoreの同期BRAMで作りました。容量は64KBです。同期BRAMなので、アドレスを渡した次のサイクルに命令が出てきます。これを<PC>と<IF>の2段に割り振りました。

  • <PC>:次のPCを決めたら、同じサイクルにそのPCでIMemにreqを出します。
  • <IF>:次のサイクルに、IMemの出力から命令を受け取ります。
// <IF>  前のサイクルに<PC>がreqした命令を受け取る
rule if_stage (!ifKill && !stallW.if_);
   let x = pc2if.first; pc2if.deq;
   let inst = imem.data;
   if2id.enq(IF2ID { pc: x.pc, epoch: x.epoch, sn: x.sn, inst: inst });
endrule

// <PC>  PCの箱(last) → +4 → Mux(redirectが優先) → PCの箱、同じPCでreq
rule pc_stage (!stallW.pc);
   let cur = pc2if.last;
   Addr pc    = cur.pc + 4;
   Bool epoch = cur.epoch;
   if (redirect[1] matches tagged Valid .target) begin
      pc    = target;
      epoch = !cur.epoch;
      redirect[1] <= tagged Invalid;
   end
   pc2if.enq(PC2IF { pc: pc, epoch: epoch, sn: cur.sn + 1 });
   imem.req(pc);
endrule

<IF>が止まったときに、命令が消えないかが気になるところです。BRAMの出力は、次にreqが来るまで同じ値を出し続けます。<IF>が止まっている間はPCの箱が満杯なので、<PC>はenqできず、reqも出しません。そのため、<IF>が再び動いたときにも、BRAMには自分の命令が残っています。この性質のおかげで、出力を受けるレジスタやFIFOを足さずに済みました。Step 1のランダムに段を止めるテストを、そのまま全部通ることも確かめました。

逆アセンブル

逆アセンブル関数は、pcと命令コードを受け取って表示用の文字列(Fmt)を返す、デバッグ専用の関数です。回路には入りません。書き方は、objdumpの-M no-aliases,numericに合わせました。mvやretのような別名を使わず、レジスタはx0〜x31で書きます。

命令コード 逆アセンブル
7f9010ef jal x1,80002000
0047a703 lw x14,4(x15)
fec796e3 bne x15,x12,8000201c
00008067 jalr x0,0(x1)

RV32IとZicsr、fence.i、ecall/ebreak/mret/wfiに対応しています。

Konataの左の欄に出す

<ID>は逆アセンブルの結果を、Kanataログの左の欄に書き足します。Step 1では左の欄はPCだけでしたが、Step 2からは「PC ニーモニック」になり、Konataでどの命令が流れているかが一目で分かります。<ID>まで行かずに捨てられた命令は、PCだけが出ます。

命令 0 1 2 3 4 5 6 7 8
80000000 auipc x2,0x10 PC IF ID EX MA WB
80000004 addi x2,x2,0 PC IF ID EX MA WB
80000008 jal x1,80002000 PC IF ID EX MA WB
8000000c PC IF ID
80000010 PC IF
80002000 lui x15,0x80002 PC IF ID EX
80002004 addi x14,x15,104 PC IF ID

どう確かめたか

期待値はobjdumpです。riscv-tests(rv32ui-p)40本とfiboに出てくる全命令に、CSR命令や即値の端の値などを集めた命令列を加えて、合計9431命令をobjdumpで逆アセンブルしました。同じ命令を逆アセンブル関数に通し、1行ずつ比べて、bsimとVerilatorの両方で全部一致しました。

さらに、fiboをパイプラインに流しました。<ID>がKanataの左の欄に書いた29命令が、objdumpの結果と一致しました。

パイプラインビューア
図1149.1 パイプラインビューア

できたこと

命令は本物のメモリから読まれるようになり、Konataで「どの命令が、どの段に、いつ居るか」が読めるようになりました。次のStep 3では、デコーダ、レジスタファイル、ALUを入れ、命令が実際に計算をするようにします。


左矢前のブログ 次のブログ右矢