ゲームボーイを作るその11。前回作ったテストをパスするため、ロード命令を実装していく。
ゲームボーイのCPUの命令
実装、、、、の前に、まずはゲームボーイのCPUの命令や動作について、ざっと確認しておく。
命令の一覧については、次のサイトがとても見やすくまとまっており、基本的にはこのページを見つつ実装を進めている。
Pan Docsの命令セットの区分けによると、大きくは次の8系統の命令が存在している。
- 8-bit Load instructions
- 16-bit Load instructions
- 8-bit Arithmetic / Logic instructions
- 16-bit Arithmetic/Logic instructions
- Rotate and Shift instructions
- Single-bit Operation instructions
- CPU Control instructions
- Jump instructions
命令自体は基本的に1byteで、メモリの読み書きが入る場合は命令1byteの後に、必要なサイクルだけメモリアクセスが継続される。
「基本的に」と書いたのは、stop
とprefixed
という命令のみ、2byteで構成される場合があるからだ。
stop
: 0x10 0x00prefixed
: 0xCB 0xXX (2byte目の0xXXは別のエンコード表)
このあたりの命令に必要なサイクル数も、先ほどの命令一覧に記載されている。 ざっと眺めての感想なのだが、、、規則性があるんだかないんだか、、、というなんとも言えない感じのエンコード表だな、、と。 途中から空いてるところに突っ込んでいったようにも思える。 最初はあんまりキレイに書こうとすると、ドツボにハマる気がするので、ある程度そのまま実装していくことにしよう。
CPUの動作波形
CPUは命令によって、実行にかかるサイクルが異なるのは前述したとおりだが、その場合の命令フェッチのタイミングについても(一応)確認しておく必要がある。 そのあたりについては、次の資料に丁寧に解説が載っていた。
https://gekkio.fi/files/gb-docs/gbctr.pdf
下の図は上記資料の「CHAPTER1. CPU CORE TIMING」に掲載されていたものになる。波形からわかるように「各命令の最後の実行サイクルで次の命令をフェッチする」という事で大丈夫そうだ。(まあ、そうなるよね。)
波形からわかる通り、ゲームボーイには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) }
上記のInstructions
とCpuReg
、OP
/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
のフィールドすら参照していない状態だが、とりあえずこれでロードが何となく動く状態になった。
次は加算あたりかなぁ。