From cf7543e537b2758190a5de407f9ed9356cf5d0f1 Mon Sep 17 00:00:00 2001 From: haomin1996 Date: Sun, 8 Jun 2025 12:46:18 -0700 Subject: [PATCH 1/2] Fix CPU utilities and add basic tests --- src/gbaEmu/CPU.java | 18 ++--- src/gbaEmu/Memory.java | 30 ++++---- src/gbaEmu/Opcodes.java | 36 ++++----- src/gbaEmu/Util.java | 11 ++- src/gbaEmu/Video.java | 160 ++++++++++++++++++++++------------------ test/MemoryTest.java | 20 +++++ test/UtilTest.java | 16 ++++ 7 files changed, 172 insertions(+), 119 deletions(-) create mode 100644 test/MemoryTest.java create mode 100644 test/UtilTest.java diff --git a/src/gbaEmu/CPU.java b/src/gbaEmu/CPU.java index 97bcc96..57f9bec 100644 --- a/src/gbaEmu/CPU.java +++ b/src/gbaEmu/CPU.java @@ -101,9 +101,9 @@ private void doInterrupt(int id) { case 3: register.pc = 0x58; break; - case 4: - register.pc = 60; - break; + case 4: + register.pc = 0x60; + break; default: Log.fatalf("unknown interrupt: " + id); } @@ -1017,12 +1017,12 @@ public byte getValue8() { register.pc++; return ans; } - public short getValue16() { - int value1 = memory.readMemory(register.pc); - int value2 = memory.readMemory(register.pc + 1); - register.pc += 2; - return (short) (value1 << 8 + value2); - } + public short getValue16() { + int value1 = memory.readMemory(register.pc) & 0xFF; + int value2 = memory.readMemory(register.pc + 1) & 0xFF; + register.pc += 2; + return (short) ((value1 << 8) | value2); + } public int decrementHL() { int hl = register.hl(); hl--; diff --git a/src/gbaEmu/Memory.java b/src/gbaEmu/Memory.java index 23b9989..a3b6c5a 100644 --- a/src/gbaEmu/Memory.java +++ b/src/gbaEmu/Memory.java @@ -17,19 +17,19 @@ public void memoryIncrement(int address) { mainMemory[address]++; } // 16 bit stack push - public int stackPush(short value) { - cpu.register.sp--; - writeMemory(cpu.register.sp, (byte) (value >> 8)); - cpu.register.sp--; - writeMemory(cpu.register.sp, (byte) (value & 0xFF)); - return 0; - } - //16 bit stack pop - public short stackPop() { - cpu.register.sp++; - int low = readMemory(cpu.register.sp); - cpu.register.sp++; - int high = readMemory(cpu.register.sp); - return (short) (high << 8 | low); - } + public int stackPush(short value) { + cpu.register.sp--; + writeMemory(cpu.register.sp & 0xFFFF, (byte) (value >> 8)); + cpu.register.sp--; + writeMemory(cpu.register.sp & 0xFFFF, (byte) (value & 0xFF)); + return 0; + } + //16 bit stack pop + public short stackPop() { + int low = readMemory(cpu.register.sp & 0xFFFF); + cpu.register.sp++; + int high = readMemory(cpu.register.sp & 0xFFFF); + cpu.register.sp++; + return (short) ((high << 8) | (low & 0xFF)); + } } diff --git a/src/gbaEmu/Opcodes.java b/src/gbaEmu/Opcodes.java index ca1883a..4466018 100644 --- a/src/gbaEmu/Opcodes.java +++ b/src/gbaEmu/Opcodes.java @@ -1117,24 +1117,24 @@ public int OP3B() { cpu.putSP(cpu.register.sp - 1); return 0; } - public int OPCB() { - byte n = cpu.getValue8(); - if (cpu.opMap.containsKey(0xCB << 2 + n)) { - Runnable op = cpu.opMap.get(0xCB << 2 + n); - op.run(); - } else { - return -1; - } - return cpu.opCycles.get(0xCB << 2 + n); - } - private byte swapByteAndSetFlags(byte a) { - byte res = (byte) (((a & 0xF) << 4) | ((a & 0xF0) >> 4)); - cpu.register.nf = false; - cpu.register.hf = false; - cpu.register.hf = false; - cpu.register.zf = res == 0; - return res; - } + public int OPCB() { + byte n = cpu.getValue8(); + int op = (0xCB << 8) | (n & 0xFF); + if (cpu.opMap.containsKey(op)) { + Runnable run = cpu.opMap.get(op); + run.run(); + return cpu.opCycles.get(op); + } + return -1; + } + private byte swapByteAndSetFlags(byte a) { + byte res = (byte) (((a & 0xF) << 4) | ((a & 0xF0) >> 4)); + cpu.register.nf = false; + cpu.register.hf = false; + cpu.register.cf = false; + cpu.register.zf = res == 0; + return res; + } public int OPCB37() { cpu.register.a = swapByteAndSetFlags(cpu.register.a); return 0; diff --git a/src/gbaEmu/Util.java b/src/gbaEmu/Util.java index 1ae5bf1..1936422 100644 --- a/src/gbaEmu/Util.java +++ b/src/gbaEmu/Util.java @@ -9,12 +9,11 @@ public static boolean testBit(byte n, int pos) { assert pos >= 0: "testbit parameter must be positive"; return (n & (1 << pos)) > 0; } - public static byte clearBit(byte n, int pos) { - assert pos >= 0: "clearbit parameter must be positive"; - byte allSetTemplate = (byte) 0xFFFF; - byte clearTemplate = (byte) (((byte) (1 << pos)) ^ allSetTemplate); - return (byte) (pos & clearTemplate); - } + public static byte clearBit(byte n, int pos) { + assert pos >= 0: "clearbit parameter must be positive"; + byte clearTemplate = (byte) ~(1 << pos); + return (byte) (n & clearTemplate); + } public static byte getVal(byte val, int pos) { return (byte) ((val >> pos) & 1); } diff --git a/src/gbaEmu/Video.java b/src/gbaEmu/Video.java index 3f267e0..4393792 100644 --- a/src/gbaEmu/Video.java +++ b/src/gbaEmu/Video.java @@ -65,24 +65,28 @@ public void renderSprites() { int red = 0; int green = 0; int blue = 0; - switch (color) { - case 0: - red = 255; - green = 255; - blue = 255; - case 1: - red = 0xCC; - green = 0xCC; - blue = 0xCC; - case 2: - red = 0x77; - green = 0x77; - blue = 0x77; - default: - red = 0; - green = 0; - blue = 0; - } + switch (color) { + case 0: + red = 255; + green = 255; + blue = 255; + break; + case 1: + red = 0xCC; + green = 0xCC; + blue = 0xCC; + break; + case 2: + red = 0x77; + green = 0x77; + blue = 0x77; + break; + default: + red = 0; + green = 0; + blue = 0; + break; + } int xPix = 0 - tilePixel; xPix *= -1; int pixel = xPos + xPix; @@ -158,12 +162,12 @@ public void renderTiles() { } else { tileNum = (int)(byte)memory.readMemory(tileAddress); } - int tileLocation = tileData; - if (unSigned) { - tileLocation += tileData * 16; - } else { - tileLocation = tileLocation + (tileNum + 128) * 16; - } + int tileLocation; + if (unSigned) { + tileLocation = tileData + tileNum * 16; + } else { + tileLocation = tileData + (tileNum + 128) * 16; + } int line = yPos % 8; line *= 2; byte data1 = memory.readMemory(tileLocation + line); @@ -179,24 +183,28 @@ public void renderTiles() { int red = 0; int green = 0; int blue = 0; - switch (color) { - case 0: - red = 255; - green = 255; - blue = 255; - case 1: - red = 0xCC; - green = 0xCC; - blue = 0xCC; - case 2: - red = 0x77; - green = 0x77; - blue = 0x77; - default: - red = 0; - green = 0; - blue = 0; - } + switch (color) { + case 0: + red = 255; + green = 255; + blue = 255; + break; + case 1: + red = 0xCC; + green = 0xCC; + blue = 0xCC; + break; + case 2: + red = 0x77; + green = 0x77; + blue = 0x77; + break; + default: + red = 0; + green = 0; + blue = 0; + break; + } int yIndex = memory.readMemory(0xFF44); if (yIndex < 0 || yIndex > 143 || pixel < 0 || pixel > 159) { continue; @@ -216,39 +224,49 @@ public int getColor(byte colorNum, int address) { byte palette = memory.readMemory(address); int high; int low; - switch (colorNum) { - case 0: - high = 1; - low = 0; - case 1: - high = 3; - low = 2; - case 2: - high = 5; - low = 4; - case 3: - high = 7; - low = 6; - default: - high = 1; - low = 0; - } + switch (colorNum) { + case 0: + high = 1; + low = 0; + break; + case 1: + high = 3; + low = 2; + break; + case 2: + high = 5; + low = 4; + break; + case 3: + high = 7; + low = 6; + break; + default: + high = 1; + low = 0; + break; + } byte color = 0; color = (byte) (Util.getVal(palette, high) << 1); color |= Util.getVal(palette, low); - switch (color) { - case 0: - res = 0; - case 1: - res = 1; - case 2: - res = 2; - case 3: - res = 3; - default: - res = 0; - } + switch (color) { + case 0: + res = 0; + break; + case 1: + res = 1; + break; + case 2: + res = 2; + break; + case 3: + res = 3; + break; + default: + res = 0; + break; + } return res; } } diff --git a/test/MemoryTest.java b/test/MemoryTest.java new file mode 100644 index 0000000..f7ae46e --- /dev/null +++ b/test/MemoryTest.java @@ -0,0 +1,20 @@ +package gbaEmu; + +import org.junit.Test; +import static org.junit.Assert.*; +import gbaEmu.Memory; +import gbaEmu.CPU; + +public class MemoryTest { + @Test + public void testStackPushPop() { + Memory mem = new Memory(); + mem.cpu = new CPU(mem); // CPU constructor requires Memory + mem.cpu.register.sp = (short)0xFFFE; + mem.stackPush((short)0xABCD); + assertEquals((short)0xFFFC, mem.cpu.register.sp); + short val = mem.stackPop(); + assertEquals((short)0xABCD, val); + assertEquals((short)0xFFFE, mem.cpu.register.sp); + } +} diff --git a/test/UtilTest.java b/test/UtilTest.java new file mode 100644 index 0000000..4472109 --- /dev/null +++ b/test/UtilTest.java @@ -0,0 +1,16 @@ +package gbaEmu; + +import org.junit.Test; +import static org.junit.Assert.*; +import gbaEmu.Util; + +public class UtilTest { + @Test + public void testSetClearBit() { + byte b = 0; + b = Util.setBit(b, 3); + assertTrue(Util.testBit(b, 3)); + b = Util.clearBit(b, 3); + assertFalse(Util.testBit(b, 3)); + } +} From 5fd6bd28739cc68754a09e43b9a7868e47778275 Mon Sep 17 00:00:00 2001 From: haomin1996 Date: Sun, 8 Jun 2025 12:58:26 -0700 Subject: [PATCH 2/2] Implement basic HALT/STOP and ROM loading --- src/gbaEmu/CPU.java | 49 ++++++++++++++++++++++------------ src/gbaEmu/Memory.java | 20 ++++++++++---- src/gbaEmu/Opcodes.java | 22 ++++++++++----- src/gbaEmu/Rom.java | 22 +++++++-------- src/gbaEmu/cartridge/MBC1.java | 19 ++++++++++--- test/CPUTest.java | 27 +++++++++++++++++++ 6 files changed, 114 insertions(+), 45 deletions(-) create mode 100644 test/CPUTest.java diff --git a/src/gbaEmu/CPU.java b/src/gbaEmu/CPU.java index 57f9bec..ff75857 100644 --- a/src/gbaEmu/CPU.java +++ b/src/gbaEmu/CPU.java @@ -24,12 +24,21 @@ public CPU(Memory memory) { this.register = new Register(); this.timer = new Timer(); } - public int executeNextOp() { - short nextOp = this.register.pc; - this.register.pc++; - opMap.get(nextOp).run(); - return opCycles.get(nextOp); - } + public int executeNextOp() { + int opcode = memory.readMemory(register.pc & 0xFFFF) & 0xFF; + register.pc++; + if (opcode == 0xCB) { + int cb = memory.readMemory(register.pc & 0xFFFF) & 0xFF; + register.pc++; + opcode = (0xCB << 8) | cb; + } + Runnable op = opMap.get(opcode); + if (op == null) { + Log.fatalf(String.format("Unknown opcode: 0x%X", opcode)); + } + op.run(); + return opCycles.getOrDefault(opcode, 4); + } public void run() throws InterruptedException { while (true) { update(); @@ -108,10 +117,12 @@ private void doInterrupt(int id) { Log.fatalf("unknown interrupt: " + id); } } - public int initOpcodes() { - opcodes = new Opcodes(); - opMap = new HashMap<>(); - opCycles = new HashMap<>(); + public int initOpcodes() { + opcodes = new Opcodes(); + opcodes.cpu = this; + opcodes.memory = this.memory; + opMap = new HashMap<>(); + opCycles = new HashMap<>(); opMap.put(0x7F, () -> opcodes.OP7F()); opCycles.put(0x7F, 4); for (int i = 0x78; i <= 0x7D; i++) { @@ -479,10 +490,14 @@ public int initOpcodes() { opCycles.put(0x2F, 4); opMap.put(0x3F, () -> opcodes.OP3F()); opCycles.put(0x3F, 4); - opMap.put(0x37, () -> opcodes.OP37()); - opCycles.put(0x37, 4); - opMap.put(0x00, () -> opcodes.OP00()); - opCycles.put(0x00, 4); + opMap.put(0x37, () -> opcodes.OP37()); + opCycles.put(0x37, 4); + opMap.put(0x76, () -> opcodes.OP76()); + opCycles.put(0x76, 4); + opMap.put(0x10, () -> opcodes.OP10()); + opCycles.put(0x10, 4); + opMap.put(0x00, () -> opcodes.OP00()); + opCycles.put(0x00, 4); opMap.put(0x07, () -> opcodes.OP07()); opCycles.put(0x07, 4); opMap.put(0x17, () -> opcodes.OP17()); @@ -1018,10 +1033,10 @@ public byte getValue8() { return ans; } public short getValue16() { - int value1 = memory.readMemory(register.pc) & 0xFF; - int value2 = memory.readMemory(register.pc + 1) & 0xFF; + int low = memory.readMemory(register.pc & 0xFFFF) & 0xFF; + int high = memory.readMemory((register.pc + 1) & 0xFFFF) & 0xFF; register.pc += 2; - return (short) ((value1 << 8) | value2); + return (short) ((high << 8) | low); } public int decrementHL() { int hl = register.hl(); diff --git a/src/gbaEmu/Memory.java b/src/gbaEmu/Memory.java index a3b6c5a..c75cade 100644 --- a/src/gbaEmu/Memory.java +++ b/src/gbaEmu/Memory.java @@ -1,11 +1,21 @@ package gbaEmu; public class Memory { - byte[] mainMemory; - CPU cpu; - public Memory() { - mainMemory = new byte[0x10000]; - } + byte[] mainMemory; + CPU cpu; + public Memory() { + mainMemory = new byte[0x10000]; + } + + /** + * Copy the supplied ROM bytes into the memory area starting at 0x0000. + * Only the first 32KB of the ROM are mapped as this emulator does not + * implement any banking logic. + */ + public void loadRom(byte[] romData) { + int len = Math.min(romData.length, 0x8000); + System.arraycopy(romData, 0, mainMemory, 0, len); + } public byte readMemory(int address) { return mainMemory[address]; } diff --git a/src/gbaEmu/Opcodes.java b/src/gbaEmu/Opcodes.java index 4466018..b2533af 100644 --- a/src/gbaEmu/Opcodes.java +++ b/src/gbaEmu/Opcodes.java @@ -1183,12 +1183,22 @@ public int OP3F() { cpu.register.cf = !cpu.register.cf; return 0; } - public int OP37() { - cpu.register.nf = false; - cpu.register.hf = false; - cpu.register.cf = true; - return 0; - } + public int OP37() { + cpu.register.nf = false; + cpu.register.hf = false; + cpu.register.cf = true; + return 0; + } + // HALT - put CPU into low power mode until an interrupt occurs + public int OP76() { + cpu.halt = true; + return 0; + } + // STOP instruction is treated the same as HALT in this simplified emulator + public int OP10() { + cpu.halt = true; + return 0; + } public int OP00() { return 0; } diff --git a/src/gbaEmu/Rom.java b/src/gbaEmu/Rom.java index f7d5ce3..a946bc9 100644 --- a/src/gbaEmu/Rom.java +++ b/src/gbaEmu/Rom.java @@ -8,19 +8,15 @@ import java.util.Arrays; public class Rom { - byte[] content; - public byte[] readRom(String filename) { - try (InputStream inputStream = new FileInputStream(filename);) { - content = inputStream.readAllBytes(); - } catch (FileNotFoundException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return content; - } + byte[] content = new byte[0]; + public byte[] readRom(String filename) { + try (InputStream inputStream = new FileInputStream(filename)) { + content = inputStream.readAllBytes(); + } catch (IOException e) { + throw new RuntimeException("Failed to read ROM file: " + filename, e); + } + return content; + } public String getTitle() { String titleString; if (content.length == 0 ) { diff --git a/src/gbaEmu/cartridge/MBC1.java b/src/gbaEmu/cartridge/MBC1.java index c3a209a..e371b11 100644 --- a/src/gbaEmu/cartridge/MBC1.java +++ b/src/gbaEmu/cartridge/MBC1.java @@ -1,5 +1,9 @@ package gbaEmu.cartridge; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + public class MBC1 implements MBC { private byte[] rom; public byte currentRomBank; @@ -57,10 +61,17 @@ private void doChangeRomRamMode(byte data) { } } @Override - public void saveRam(String fileName) { - // TODO Auto-generated method stub - - } + public void saveRam(String fileName) { + if (ramBank == null) { + return; + } + try { + Files.write(Paths.get(fileName), ramBank); + } catch (IOException e) { + e.printStackTrace(); + } + + } private void doRamEnable(int address, byte value) { byte testData = (byte) (value & 0xF); diff --git a/test/CPUTest.java b/test/CPUTest.java new file mode 100644 index 0000000..5878618 --- /dev/null +++ b/test/CPUTest.java @@ -0,0 +1,27 @@ +package gbaEmu; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class CPUTest { + @Test + public void testHaltInstruction() { + Memory mem = new Memory(); + CPU cpu = new CPU(mem); + mem.cpu = cpu; + cpu.initOpcodes(); + mem.writeMemory(cpu.register.pc & 0xFFFF, (byte)0x76); + cpu.executeNextOp(); + assertTrue(cpu.halt); + } + + @Test + public void testLoadRom() { + Memory mem = new Memory(); + byte[] rom = new byte[] {1,2,3}; + mem.loadRom(rom); + assertEquals(1, mem.readMemory(0)); + assertEquals(2, mem.readMemory(1)); + assertEquals(3, mem.readMemory(2)); + } +}