Compare commits

..
12 Commits
7 changed files with 429 additions and 50 deletions
+13
View File
@@ -44,6 +44,18 @@ We start counting from the right as bit *#0*!
d = data \\
n = constant
**** Flag conditions
Flag conditions, sometimes referred to as ~cc~, are the state the flags need to
be in to execute the opcode. There are four possible checks[[#references][(4)]]:
| Code | Meaning | State |
|------+---------------------+-------|
| C | Carry flag is set | 1 |
| NC | Carry flag is unset | 0 |
| Z | Zero flag is set | 1 |
| NZ | Zero flag is unset | 0 |
*** Variables
| Name | Meaning |
@@ -59,3 +71,4 @@ n = constant
1. https://gbdev.io/pandocs/Memory_Map.html
2. https://gbdev.io/pandocs/Specifications.html
3. [[https://robdor.com/2016/08/10/gameboy-emulator-half-carry-flag/]["A Visual Guide to the Gameboy's Half-Carry Flag"]]. robdor.com. Retrieved 24 Aug 2022.
4. [[http://www.devrs.com/gb/files/opcodes.html]["GameBoy Opcode Summary"]]. devrs.com. Retrieved 25 Aug 2022.
+341 -42
View File
@@ -67,41 +67,61 @@ void CPU::update()
print("running opcode: {:#x}\n", opcode);
switch (opcode) {
case 0x00: nop(); break;
case 0x01: ldi16(); break;
case 0x02: ldr8(); break;
case 0x04: inc(); break;
case 0x05: dec8(); break;
case 0x06: ldi8(); break;
case 0x07: ra(); break;
case 0x08: ldr16(); break;
case 0x09: addr16(); break;
case 0x0a: ldr8(); break;
case 0x0b: decr16(); break;
case 0x0b: dec16(); break;
case 0x0c: inc(); break;
case 0x0d: dec8(); break;
case 0x0e: ldi8(); break;
case 0x0f: ra(); break;
case 0x11: ldi16(); break;
case 0x12: ldr8(); break;
case 0x14: inc(); break;
case 0x15: dec8(); break;
case 0x16: ldi8(); break;
case 0x17: ra(); break;
case 0x18: jrs8(); break;
case 0x19: addr16(); break;
case 0x1a: ldr8(); break;
case 0x1b: decr16(); break;
case 0x1b: dec16(); break;
case 0x1c: inc(); break;
case 0x1d: dec8(); break;
case 0x1e: ldi8(); break;
case 0x20: jr8(); break;
case 0x1f: ra(); break;
case 0x20: jrs8(); break;
case 0x21: ldi16(); break;
case 0x22: ldr8(); break;
case 0x24: inc(); break;
case 0x25: dec8(); break;
case 0x26: ldi8(); break;
case 0x28: jrs8(); break;
case 0x29: addr16(); break;
case 0x2a: lda8(); break;
case 0x2b: decr16(); break;
case 0x2b: dec16(); break;
case 0x2c: inc(); break;
case 0x2d: dec8(); break;
case 0x2e: ldi8(); break;
case 0x2f: misc(); break;
case 0x30: jrs8(); break;
case 0x31: ldi16(); break;
case 0x32: ldr8(); break;
case 0x34: inc(); break;
case 0x35: dec8(); break;
case 0x36: ldi8(); break;
case 0x38: jrs8(); break;
case 0x39: addr16(); break;
case 0x3a: ldr8(); break;
case 0x3b: decr16(); break;
case 0x3b: dec16(); break;
case 0x3c: inc(); break;
case 0x3d: dec8(); break;
case 0x3e: ldi8(); break;
case 0x40: ldr8(); break;
case 0x41: ldr8(); break;
@@ -160,6 +180,14 @@ void CPU::update()
case 0x7d: ldr8(); break;
case 0x7e: ldr8(); break;
case 0x7f: ldr8(); break;
case 0xa0: and8(); break;
case 0xa1: and8(); break;
case 0xa2: and8(); break;
case 0xa3: and8(); break;
case 0xa4: and8(); break;
case 0xa5: and8(); break;
case 0xa6: and8(); break;
case 0xa7: and8(); break;
case 0xa8: xor8(); break;
case 0xaf: xor8(); break;
case 0xb8: cp(); break;
@@ -171,17 +199,31 @@ void CPU::update()
case 0xbe: cp(); break;
case 0xbf: cp(); break;
case 0xc3: jp16(); break;
case 0xc6: add(); break;
case 0xc4: call(); break;
case 0xc6: addi8(); break;
case 0xc7: rst(); break;
case 0xcc: call(); break;
case 0xcd: call(); break;
case 0xcf: rst(); break;
case 0xd4: call(); break;
case 0xd7: rst(); break;
case 0xdc: call(); break;
case 0xdf: rst(); break;
case 0xe0: ldffi8(); break;
case 0xe2: ldr8(); break;
case 0xe6: and8(); break;
case 0xe7: rst(); break;
case 0xe8: adds8(); break;
case 0xea: ldr8(); break;
case 0xef: rst(); break;
case 0xf0: ldffi8(); break;
case 0xf2: lda8(); break;
case 0xf7: rst(); break;
case 0xf8: ldr16(); break;
case 0xf9: ldr16(); break;
case 0xfa: lda8(); break;
case 0xfe: cp(); break;
case 0xff: rst(); break;
default:
print("opcode {:#x} not implemented\n", opcode);
@@ -193,7 +235,7 @@ void CPU::update()
// -------------------------------------
void CPU::add()
void CPU::addi8()
{
uint8_t opcode = pcRead();
uint8_t immediate = pcRead();
@@ -217,24 +259,87 @@ void CPU::add()
}
}
void CPU::dec8()
void CPU::and8()
{
auto bitwise_and = [this](uint32_t byte) {
// AND r8, flags: Z 0 1 0
m_wait_cycles += 4;
// Set flags
m_nf = 0;
m_hf = 1;
m_cf = 0;
// Bitwise AND between the value in r8 and A
m_a = m_a & byte;
// Zero flag
m_zf = m_a == 0;
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0x0d: { // DEC C, flags: Z 1 H -
case 0xa0: /* AND B */ bitwise_and(m_b); break;
case 0xa1: /* AND C */ bitwise_and(m_c); break;
case 0xa2: /* AND D */ bitwise_and(m_d); break;
case 0xa3: /* AND E */ bitwise_and(m_e); break;
case 0xa4: /* AND H */ bitwise_and(m_h); break;
case 0xa5: /* AND L */ bitwise_and(m_l); break;
case 0xa6: /* AND (HL) */ {
m_wait_cycles += 4; // + 4 = 8 total
// Bitwise AND between the byte pointed to by HL and A
bitwise_and(read(hl()));
break;
}
case 0xa7: /* AND A */ bitwise_and(m_a); break;
case 0xe6: /* AND i8 */ {
m_wait_cycles += 4; // + 4 = 8 total
// Bitwise AND between the value in i8 and A
bitwise_and(pcRead());
break;
}
default:
VERIFY_NOT_REACHED();
}
}
void CPU::dec8()
{
auto decrement = [this](uint32_t& reg) -> void {
// DEC r8, flags: Z 1 H -
m_wait_cycles += 4;
// Set flags
m_nf = 1;
m_hf = isCarry(m_c, -1, 0x10);
m_hf = isCarry(reg, -1, 0x10);
// C = C - 1
m_c = (m_c - 1) & 0xff;
// Decrement value in register r8 by 1
reg = (reg - 1) & 0xff;
// Zero flag
m_zf = m_c == 0;
m_zf = reg == 0;
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0x05: /* DEC B */ decrement(m_b); break;
case 0x0d: /* DEC C */ decrement(m_c); break;
case 0x15: /* DEC D */ decrement(m_d); break;
case 0x1d: /* DEC E */ decrement(m_e); break;
case 0x25: /* DEC H */ decrement(m_h); break;
case 0x2d: /* DEC L */ decrement(m_l); break;
case 0x35: /* DEC (HL) */ {
m_wait_cycles += 8; // + 4 = 12 total
// Decrement the byte pointed to by HL by 1
uint32_t data = read(hl());
decrement(data);
write(hl(), data);
break;
}
case 0x3d: /* DEC A */ decrement(m_a); break;
default:
VERIFY_NOT_REACHED();
}
@@ -302,7 +407,59 @@ void CPU::lda8()
}
}
void CPU::decr16()
void CPU::addr16()
{
auto add = [this](uint32_t reg) -> void {
// ADD HL,r16, flags: - 0 H C
m_wait_cycles += 8;
// Set flags
m_nf = 0;
m_hf = isCarry(hl(), reg, 0x1000);
m_cf = isCarry(hl(), reg, 0x10000);
// Add the value in r16 to HL
uint32_t data = (hl() + reg) & 0xffff;
m_l = data & 0xff;
m_h = data >> 8;
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0x09: /* ADD HL,BC */ add(bc()); break;
case 0x19: /* ADD HL,DE */ add(de()); break;
case 0x29: /* ADD HL,HL */ add(hl()); break;
case 0x39: /* ADD HL,SP */ add(sp()); break;
default:
VERIFY_NOT_REACHED();
}
}
void CPU::adds8()
{
uint8_t opcode = pcRead();
switch (opcode) {
case 0xe8: { // ADD SP,s8, flags: 0 0 H C
m_wait_cycles += 16;
uint32_t signed_data = (pcRead() ^ 0x80) - 0x80;
// Set flags
m_zf = 0;
m_nf = 0;
m_hf = isCarry(m_sp, signed_data, 0x10);
m_cf = isCarry(m_sp, signed_data, 0x100);
// Add the signed value s8 to SP
m_sp = m_sp + signed_data;
break;
}
default:
VERIFY_NOT_REACHED();
}
}
void CPU::dec16()
{
uint8_t opcode = pcRead();
switch (opcode) {
@@ -498,6 +655,93 @@ void CPU::ldr8()
m_wait_cycles += 4;
}
// Rotate accumulator
void CPU::ra()
{
// Make sure we only look at the bottom 8 bits
m_a = m_a & 0xff;
uint8_t opcode = pcRead();
switch (opcode) {
case 0x07: // RLCA
// Rotates A to the left with bit 7 being moved to bit 0 and also stored
// into the carry
// ┌──────────────┐
// │ ┌─────────┐ │
// C <─┴─│7 <── 0│<─┘
// └─────────┘
// A
// Copy bit 7 into carry flag
m_cf = (m_a & 0x80) == 0x80;
// Rotate register A left
m_a = (m_a << 1) | (m_a >> 7);
break;
case 0x0f: // RRCA
// Rotates A to the right with bit 0 being moved to bit 7 and also
// stored into the carry
// ┌──────────────┐
// │ ┌─────────┐ │
// └─>│7 ──> 0│─┴─> C
// └─────────┘
// A
// Copy bit 0 into carry flag
m_cf = (m_a & 0x1) == 0x1;
// Rotate register A right
m_a = (m_a << 7) | (m_a >> 1);
break;
case 0x17: { // RLA
// Rotates A to the left with the carry's value put into bit 0 and bit 7
// is put into the carry
// ┌────────────────────┐
// │ ┌─────────┐ │
// └─ C <──│7 <── 0│<─┘
// └─────────┘
// A
uint32_t old_carry = m_cf != 0;
m_cf = (m_a & 0x80) == 0x80; // Copy bit 7 into carry flag
// Rotate register A left through carry
m_a = (m_a << 1) | old_carry;
break;
}
case 0x1f: { // RRA
// Rotates A to the right with the carry's value put into bit 7 and bit 0
// is put into the carry
// ┌────────────────────┐
// │ ┌─────────┐ │
// └─>│7 ──> 0│──> C ─┘
// └─────────┘
// A
uint32_t old_carry = m_cf != 0;
m_cf = (m_a & 0x1) == 0x1; // Copy bit 0 into carry flag
// Rotate register A right through carry
m_a = (old_carry << 7) | (m_a >> 1);
break;
}
default:
VERIFY_NOT_REACHED();
}
// RLCA/RRCA/RLA/RRA, Flags: 0 0 0 C
m_wait_cycles += 4;
// Set flags
m_zf = 0;
m_nf = 0;
m_hf = 0;
}
void CPU::cp()
{
uint8_t opcode = pcRead();
@@ -562,21 +806,13 @@ void CPU::inc()
case 0x1c: /* INC E */ increment(m_e); break;
case 0x24: /* INC H */ increment(m_h); break;
case 0x2c: /* INC L */ increment(m_l); break;
case 0x34: { /* INC (HL) */
m_wait_cycles += 12;
uint32_t data = read(hl());
// Set flags
m_nf = 0;
m_hf = isCarry(data, 1, 0x10);
case 0x34: /* INC (HL) */ {
m_wait_cycles += 8; // + 4 = 12 total
// Increment the byte pointed to by HL by 1
data = (data + 1) & 0xff;
uint32_t data = read(hl());
increment(data);
write(hl(), data);
// Zero flag
m_zf = data == 0;
break;
}
case 0x3c: /* INC A */ increment(m_a); break;
@@ -671,13 +907,17 @@ void CPU::ldr16()
void CPU::call()
{
uint8_t opcode = pcRead();
switch (opcode) {
case 0xcd: { // CALL a16
m_wait_cycles += 24;
auto function_call = [this](bool should_call) -> void {
// CALL cc,i16
// Note: the operand is read even when the condition is false
uint32_t data = pcRead16();
if (!should_call) {
m_wait_cycles += 12;
}
m_wait_cycles += 24;
// Push address of next 2 bytes in memory onto stack
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, data >> 8);
@@ -686,8 +926,15 @@ void CPU::call()
// Jump to this address
m_pc = data;
break;
}
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0xc4: /* CALL NZ,i16 */ function_call(!m_nf); break;
case 0xcc: /* CALL Z,i16 */ function_call(m_nf); break;
case 0xcd: /* CALL i16 */ function_call(true); break;
case 0xd4: /* CALL NC,i16 */ function_call(!m_cf); break;
case 0xdc: /* CALL C,i16 */ function_call(m_cf); break;
default:
VERIFY_NOT_REACHED();
}
@@ -706,20 +953,62 @@ void CPU::jp16()
}
}
// Jump relative
void CPU::jr8()
void CPU::jrs8()
{
auto jump_relative = [this](bool should_jump) -> void {
// JR cc,s8
// Note: the operand is read even when the condition is false
uint32_t signed_data = (pcRead() ^ 0x80) - 0x80;
if (!should_jump) {
m_wait_cycles += 8;
return;
}
m_wait_cycles += 12;
// Relative jump by adding s8 to the address of the instruction following the JR
m_pc = m_pc + signed_data;
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0x20: { // JR NZ,s8
m_wait_cycles += 8;
case 0x18: /* JR s8 */ jump_relative(true); break;
case 0x20: /* JR NZ,s8 */ jump_relative(!m_zf); break;
case 0x28: /* JR Z,s8 */ jump_relative(m_zf); break;
case 0x30: /* JR NC,s8 */ jump_relative(!m_cf); break;
case 0x38: /* JR C,s8 */ jump_relative(m_cf); break;
default:
VERIFY_NOT_REACHED();
}
}
if (!m_zf) {
m_wait_cycles += 4;
// TODO
}
break;
}
void CPU::rst()
{
auto function_call = [this](uint32_t fixed_address) -> void {
// RST vec
m_wait_cycles += 16;
// Push present address onto stack
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, m_pc >> 8);
m_sp = (m_sp - 1) & 0xffff;
write(m_sp, m_pc & 0xff);
// Jump to this address
m_pc = fixed_address;
};
uint8_t opcode = pcRead();
switch (opcode) {
case 0xc7: /* RST 0x00 */ function_call(0x00); break;
case 0xcf: /* RST 0x08 */ function_call(0x08); break;
case 0xd7: /* RST 0x10 */ function_call(0x10); break;
case 0xdf: /* RST 0x18 */ function_call(0x18); break;
case 0xe7: /* RST 0x20 */ function_call(0x20); break;
case 0xef: /* RST 0x28 */ function_call(0x28); break;
case 0xf7: /* RST 0x30 */ function_call(0x30); break;
case 0xff: /* RST 0x38 */ function_call(0x38); break;
default:
VERIFY_NOT_REACHED();
}
@@ -743,6 +1032,16 @@ void CPU::misc()
}
}
void CPU::nop()
{
uint8_t opcode = pcRead();
switch (opcode) {
case 0x0: /* NOP */ m_wait_cycles += 4; break;
default:
VERIFY_NOT_REACHED();
}
}
// -----------------------------------------
uint32_t CPU::pcRead()
+10 -3
View File
@@ -28,12 +28,15 @@ public:
// Arithmetic and Logic Instructions
// 8-bit
void add();
void addi8();
void and8();
void dec8();
void xor8();
// 16-bit
void decr16();
void addr16();
void adds8();
void dec16();
// -------------------------------------
// Bit Operations Instructions
@@ -41,6 +44,8 @@ public:
// -------------------------------------
// Bit Shift Instructions
void ra();
// -------------------------------------
// Load Instructions
@@ -61,7 +66,8 @@ public:
void call();
void jp16();
void jr8();
void jrs8();
void rst();
// -------------------------------------
// Stack Operations Instructions
@@ -70,6 +76,7 @@ public:
// Miscellaneous Instructions
void misc();
void nop();
// -------------------------------------
+21 -1
View File
@@ -12,6 +12,8 @@
#include "cpu.h"
#include "emu.h"
#include "loader.h"
#include "ruc/format/log.h"
#include "ruc/format/print.h"
#include "ruc/meta/assert.h"
@@ -55,11 +57,19 @@ void Emu::addMemorySpace(std::string_view name, uint32_t start_address, uint32_t
m_memory_spaces.emplace(name, std::move(memory_space));
}
void Emu::removeMemorySpace(std::string_view name)
{
m_memory_spaces.erase(name);
}
void Emu::writeMemory(uint32_t address, uint32_t value)
{
for (auto& memory_space : m_memory_spaces) {
auto& memory = memory_space.second;
if (address >= memory.start_address && address <= memory.end_address) {
if (address == 0xff50) {
Loader::the().disableBootrom();
}
else if (address >= memory.start_address && address <= memory.end_address) {
// Note: ECHO RAM hack
if (address >= 0xc000 && address <= 0xddff) {
writeMemory(address + (0xe000 - 0xc000), value);
@@ -70,6 +80,8 @@ void Emu::writeMemory(uint32_t address, uint32_t value)
}
}
ruc::error("writing into address '{:#06x}' which is not in a memory space!", address);
VERIFY_NOT_REACHED();
}
@@ -82,6 +94,14 @@ uint32_t Emu::readMemory(uint32_t address) const
}
}
// When trying to access the cartridge header
if (address >= 0x100 && address <= 0x14f) {
ruc::error("no cartridge loaded!");
}
else {
ruc::error("reading from address '{:#06x}' which is not in a memory space!", address);
}
VERIFY_NOT_REACHED();
return 0;
}
+2
View File
@@ -36,6 +36,7 @@ public:
void addProcessingUnit(std::string_view name, ProcessingUnit* processing_unit);
void addMemorySpace(std::string_view name, uint32_t start_address, uint32_t end_address, uint32_t amount_of_banks = 1);
void removeMemorySpace(std::string_view name);
void writeMemory(uint32_t address, uint32_t value);
uint32_t readMemory(uint32_t address) const;
@@ -43,6 +44,7 @@ public:
// -------------------------------------
ProcessingUnit* processingUnit(std::string_view name) const { return m_processing_units.at(name); }
MemorySpace memorySpace(std::string_view name) { return m_memory_spaces[name]; }
private:
uint32_t m_frequency { 0 };
+39 -3
View File
@@ -5,6 +5,7 @@
*/
#include <cstddef> // size_t
#include <cstdint> // uint32_t
#include "cpu.h"
#include "emu.h"
@@ -22,6 +23,19 @@ void Loader::loadRom(std::string_view rom_path)
init();
}
void Loader::disableBootrom()
{
Emu::the().removeMemorySpace("BOOTROM1");
Emu::the().removeMemorySpace("CARTHEADER");
Emu::the().removeMemorySpace("BOOTROM2");
// Load cartridge bank 0
Emu::the().addMemorySpace("CARTROM1", 0x0000, 0x3fff); // 16KiB
for (size_t i = 0x0000; i <= 0x3fff; ++i) {
Emu::the().writeMemory(i, m_rom_data[i]);
}
}
// -----------------------------------------
void Loader::init()
@@ -41,8 +55,9 @@ void Loader::init()
Emu::the().addMemorySpace("BOOTROM1", 0x0000, 0x00ff); // 256B
loadCartridgeHeader();
Emu::the().addMemorySpace("BOOTROM2", 0x0200, 0x08ff); // 1792B
loadCartridgeBanks();
Emu::the().addMemorySpace("VRAM", 0x8000, 0x9fff, 2); // 8KiB * 2 banks
Emu::the().addMemorySpace("CARDRAM", 0xa000, 0xbfff, 1); // 8KiB * ? banks, if any
Emu::the().addMemorySpace("CARTRAM", 0xa000, 0xbfff, 1); // 8KiB * ? banks, if any
Emu::the().addMemorySpace("WRAM1", 0xc000, 0xcfff); // 4 KiB, Work RAM
Emu::the().addMemorySpace("WRAM2", 0xd000, 0xdfff, 7); // 4 KiB * 7 banks, Work RAM
Emu::the().addMemorySpace("ECHORAM", 0xe000, 0xfdff); // 7680B, Mirror of 0xc000~0xddff
@@ -84,9 +99,30 @@ void Loader::loadCartridgeHeader()
return;
}
Emu::the().addMemorySpace("CARTHEADER", 0x100, 0x14f);
Emu::the().addMemorySpace("CARTHEADER", 0x100, 0x14f); // 80B
for (size_t i = 0x100; i <= 0x14f; ++i) {
for (size_t i = 0x0100; i <= 0x014f; ++i) {
Emu::the().writeMemory(i, m_rom_data[i]);
}
}
void Loader::loadCartridgeBanks()
{
if (m_rom_data.empty()) {
return;
}
uint32_t rom_size = 32 * 1024 * (1 << m_rom_data[0x0148]);
uint32_t rom_banks = rom_size / (16 * 1024) - 1;
Emu::the().addMemorySpace("CARTROM2", 0x4000, 0x7fff, rom_banks); // 16KiB * banks
// Load cartridge bank 1~NN
auto rom_memory_spaces = Emu::the().memorySpace("CARTROM2");
for (size_t i = 0; i < rom_banks; ++i) {
for (size_t i = 0x4000; i <= 0x7fff; ++i) {
Emu::the().writeMemory(i, m_rom_data[i]);
}
rom_memory_spaces.active_bank += 1;
}
rom_memory_spaces.active_bank = 0;
}
+2
View File
@@ -16,6 +16,7 @@ public:
Loader(s) {}
void loadRom(std::string_view rom_path);
void disableBootrom();
void setBootromPath(std::string_view bootrom_path) { m_bootrom_path = bootrom_path; }
@@ -25,6 +26,7 @@ private:
void destroy();
void loadCartridgeHeader();
void loadCartridgeBanks();
std::string_view m_bootrom_path;
std::string m_rom_data;