ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

ゲームボーイを作る(11) - ロード命令の実装

スポンサーリンク

ゲームボーイを作るその11。前回作ったテストをパスするため、ロード命令を実装していく。

ゲームボーイのCPUの命令

実装、、、、の前に、まずはゲームボーイのCPUの命令や動作について、ざっと確認しておく。

命令の一覧については、次のサイトがとても見やすくまとまっており、基本的にはこのページを見つつ実装を進めている。

Pan Docsの命令セットの区分けによると、大きくは次の8系統の命令が存在している。

  1. 8-bit Load instructions
  2. 16-bit Load instructions
  3. 8-bit Arithmetic / Logic instructions
  4. 16-bit Arithmetic/Logic instructions
  5. Rotate and Shift instructions
  6. Single-bit Operation instructions
  7. CPU Control instructions
  8. Jump instructions

命令自体は基本的に1byteで、メモリの読み書きが入る場合は命令1byteの後に、必要なサイクルだけメモリアクセスが継続される。 「基本的に」と書いたのは、stopprefixedという命令のみ、2byteで構成される場合があるからだ。

このあたりの命令に必要なサイクル数も、先ほどの命令一覧に記載されている。 ざっと眺めての感想なのだが、、、規則性があるんだかないんだか、、、というなんとも言えない感じのエンコード表だな、、と。 途中から空いてるところに突っ込んでいったようにも思える。 最初はあんまりキレイに書こうとすると、ドツボにハマる気がするので、ある程度そのまま実装していくことにしよう。

CPUの動作波形

CPUは命令によって、実行にかかるサイクルが異なるのは前述したとおりだが、その場合の命令フェッチのタイミングについても(一応)確認しておく必要がある。 そのあたりについては、次の資料に丁寧に解説が載っていた。

https://gekkio.fi/files/gb-docs/gbctr.pdf

下の図は上記資料の「CHAPTER1. CPU CORE TIMING」に掲載されていたものになる。波形からわかるように「各命令の最後の実行サイクルで次の命令をフェッチする」という事で大丈夫そうだ。(まあ、そうなるよね。)

f:id:diningyo-kpuku-jougeki:20210728230231j:plain
CPUの動作波形

波形からわかる通り、ゲームボーイには2つクロックが存在している。CPUが4MHzでメモリ(というか周辺デバイス)が1MHzになる。オリジナルに沿うように設計するなら、ちゃんとCPUも4MHzにするべきなのだが、今の所CPUも1MHzで動かす形にすることを考えている。(なんかダメっぽかったら調整ということで。。。さて、どうなるのだろう?)

とりあえずロードの実装

なんとなく、CPUの動作を把握したところで最初のテスト01_ld.sを通すべく、CPUの実装を進めていこう。

ゲームボーイのCPUのベースになった(であろう)Intelの8080のデータシートとかをみると、大体の次のような割り当てになっている。

bit7------0
1. OODDDSSS
2. OODDDOOO
3. OOOOOSSS
4. OORPOOOO

なお、各アルファベットは1bitに対応していて、それぞれの意味は次のようになっている。

O   : opcode
DDD : Destination register
SSS : Source register

DDD/SSSの3bitは次のような対応関係になる。

DDD or SSS register name
111 A
000 B
001 C
010 D
011 E
100 H
101 L

またRP(Register Pair)は次のようになる。

RP register name
00 B-C
01 D-E
10 H-L
11 SP

このあたりの情報をChiselで記述していく。後で変えるかもだが、この手の情報はobjectとして、まとめておいて参照するのが良さそう。今回01_ld.sをパスさせるために必要なのはレジスタ間のロードと、メモリから1byteを読んできて対象のレジスタに格納するロードの2種類。今回の実装ではこれらをLDRR/LDRNとした。

object Instructions {

  /*
   * Basiccaly, instruction formats are follwing cases:
   *
   * 1. OODDDSSS
   * 2. OODDDOOO
   * 3. OOOOOSSS
   * 4. OORPOOOO
   *
   * O   : opcode
   * DDD : Destination register
   * SSS : Source register
   *
   */
  def A = "b111".U
  def B = "b000".U
  def C = "b001".U
  def D = "b010".U
  def E = "b011".U
  def H = "b100".U
  def L = "b101".U

  def BC = "b00".U
  def DE = "b01".U
  def HL = "b10".U
  def SP = "b11".U

  def LDRR     = BitPat("b01??????") // DDD <- SSS
  def LDRN     = BitPat("b00???110") // DDD <- ($PC+1)
}

CPUのレジスタは、まとめて扱えると便利な気がするので、次のようにBundleを使って定義してある。

/**
  * Base class for CPU register
  */
trait BaseCpuReg extends Bundle {
  // NOTE: these methods must be implemented in derived class.
  def write(data: UInt)
  def read(): UInt
}

/**
  * General Purpose register
  *
  * @param bits : numbers of register bit width
  */
class GP(bits: Int) extends BaseCpuReg {
  val data = UInt(bits.W)
  def write(wr_val: UInt): Unit = data := wr_val
  def read(): UInt = data

  override def cloneType: this.type = new GP(bits).asInstanceOf[this.type]
}

class PC(bits: Int) extends GP(bits) {
  def inc: Unit = data := data + 1.U

  override def cloneType: this.type = new PC(bits).asInstanceOf[this.type]
}

class F extends Bundle with BaseCpuReg {
  val z = Bool()
  val n = Bool()
  val h = Bool()
  val c = Bool()

  def write(data: UInt): Unit = {
    z := data(7)
    n := data(6)
    h := data(5)
    c := data(4)
  }
  def read(): UInt = Cat(z, n, h, c, 0.U(4.W))

  override def cloneType: this.type = new F().asInstanceOf[this.type]
}

class CpuReg extends Bundle {

  import Instructions._

  val a = new GP(8)
  val f = new F
  val b = new GP(8)
  val c = new GP(8)
  val d = new GP(8)
  val e = new GP(8)
  val h = new GP(8)
  val l = new GP(8)
  val sp = new GP(16)
  val pc = new PC(16)
}

各命令をある程度の区分で分けたOPと、デコード後の情報をまとめたDecodedInstも定義した。OPは値を参照して、ロード/ALU系、ジャンプ系をざっくり分けて処理を分けて行くつもりで作っているのだが、実際にどうなるかは、もう少し作ってみてから改めて考えてみるつもりでいる。

object OP extends ChiselEnum {
  val LD = Value
  val PUSH, POP = Value
  val ADD, ADC, SUB, SUC, AND, XOR, OR, CP, INC, DEC, DAA, CPL = Value
  val PREFIXED = Value
  val RLCA, RLA, RRCA, RRA, RLC, RL, RRC, RR, SLA, SWAP, SRA, SRL = Value
  val BIT, SET, RES = Value
  val CCF, SCF, NOP, HALT, STOP, DI, EI = Value
  val JP, JR, CALL, RET, RETI, RST = Value
}

class DecodedInst extends Bundle {
  val cycle = UInt(3.W)
  val is_prefixed = Bool()
  val is_imm = Bool()
  val is_rp = Bool()
  val dst = UInt(3.W)
  val src = UInt(3.W)
}

上記のInstructionsCpuRegOP/DecodedInstを使って、CPUの実装は次のようになった。(超暫定&多分でき上がってから見直したら発狂物な気がしてる。)

class Cpu extends Module {
  val io = IO(new CpuIO)

  io := DontCare

  // declare registers
  val w_reg_init_val = WireInit(0.U.asTypeOf(new CpuReg))
  w_reg_init_val.pc.data := 0x100.U
  val r_regs = RegInit(w_reg_init_val)

  // increment PC.
  r_regs.pc.inc

  // decode
  import Instructions._

  val op_code = WireInit(io.mem.rddata)

  val dst_reg = op_code(5, 3)
  val src_reg = op_code(2, 0)
  val rp      = op_code(5, 4)

  def decode(op: EnumType, cycle: UInt, is_prefixed: Bool, is_imm: Bool, is_rp: Bool, dst: UInt, src: UInt) = {
    val d = Wire(new DecodedInst())
    d.cycle := cycle
    d.is_prefixed := is_prefixed
    d.is_imm := is_imm
    d.is_rp := is_rp
    d.dst := dst
    d.src := src

    d
  }

  // 命令のデコードテーブル
  val decode_table = Array(
    LDRN     -> List(decode(OP.LD, 2.U, false.B, true.B,  false.B, dst_reg, src_reg)),
    LDRR     -> List(decode(OP.LD, 1.U, false.B, false.B, false.B, dst_reg, src_reg)),
  )

  // ListLookupでデコードして、結果をDecodedInstに格納
  val ctrl = ListLookup(op_code, List(decode(OP.NOP, 1.U, false.B, false.B, false.B, dst_reg, src_reg)), decode_table)

  val valid = Wire(Bool())
  val mcyc_counter = RegInit(0.U(3.W))
  valid := (mcyc_counter =/= 0.U)
  val r_ctrl = Reg(new DecodedInst)

  op_code := Mux(valid, 0.U, io.mem.rddata)

  // 1cycle以外の命令はカウンターを動かす
  when (!valid && ctrl(0).cycle =/= 0.U) {
    mcyc_counter := ctrl(0).cycle - 1.U
    r_ctrl := ctrl(0)
  }.elsewhen (valid) {
    mcyc_counter := mcyc_counter - 1.U
  }

  val w_wrbk = Wire(UInt(16.W))

  // valid(名前がイマイチ)が立ってるときは、レジスタに保存したDecodedInstを参照
  w_wrbk := Mux(valid, Mux(r_ctrl.is_imm, io.mem.rddata, r_regs.read(r_ctrl.is_rp, r_ctrl.src)),
    r_regs.read(ctrl(0).is_rp, ctrl(0).src)
    )

  // 各命令の最後のサイクルでデータをレジスタにライト
  when ((!valid && (ctrl(0).cycle === 1.U))) {
    r_regs.write(ctrl(0).is_rp, ctrl(0).dst, w_wrbk)
  }.elsewhen((mcyc_counter === 1.U) && r_ctrl.is_imm) {
    r_regs.write(r_ctrl.is_rp, r_ctrl.dst, w_wrbk)
  }

  io.mem.addr := r_regs.pc.data
  io.mem.wen := false.B
}

現状ロード系しか実装していないので、OPのフィールドすら参照していない状態だが、とりあえずこれでロードが何となく動く状態になった。 次は加算あたりかなぁ。