~williamvds/microlator

1e2939a83697de12097cd38038ba1d0d71b980c6 — williamvds 3 years ago 7721587
Add cycle counting

The CPU may spend an extra cycle fixing the upper address when adding an
offset, depending on the type of instruction (as indicated by
InstructionType) and whether a page boundary was crossed. Read
instructions tend to only require that extra cycle if a page was
crossed, while all other instructions will always use that extra cycle.
This page cross detection and cycle increments are now performed by
relativeAddress().
As address modes are processed in getTarget(), this function is now
provided with the type of instruction it is processing, so cycle counts
can be calculated correctly.

Popping from the stack requires spending one cycle beforehand, to
pre-increment the stack pointer. Subsequent pops within an instruction
only require a single cycle, as the stack pointer can be incremented in
the same cycle as popping from the stack.
The preIncrement argument to pop() and popFlags() must be set to true
for the first pop operation of the instruction to include this initial
cycle.

read() is no longer [[nodiscard]] as some instructions will implicitly read
and discard the result. Such reads have been annotated with a comment.

Some instructions seem to always use an extra cycle during execution, so
in those cases the cycle is manually incremented within the instruction.
4 files changed, 171 insertions(+), 43 deletions(-)

M readme.md
M src/cpu.cpp
M src/cpu.hpp
M test/testCPU.cpp
M readme.md => readme.md +1 -0
@@ 17,6 17,7 @@ Emulator for the 6502 microprocessor
[6502 Instruction Set (masswerk.at)](https://www.masswerk.at/6502/6502_instruction_set.html)  
[Visual 6502](http://visual6502.org/wiki)  
[6502 Instruction Reference (obelisk.me.uk)](http://www.obelisk.me.uk/6502/reference.html)  
[64doc](http://www.atarihq.com/danb/files/64doc.txt): Precise cycle counting details

### Test suites


M src/cpu.cpp => src/cpu.cpp +149 -36
@@ 84,6 84,7 @@ constexpr void CPU::reset() {
	pc = initialProgramCounter;
	stack = initialStackPointer;
	flags.reset();
	cycle = 0;
}

void CPU::loadProgram(const std::span<const uint8_t> program, uint16_t offset) {


@@ 106,15 107,18 @@ auto CPU::step() noexcept -> bool {
	if (!instruction.function)
		return false;

	const auto target = getTarget(instruction.addressMode);
	const auto type = getInstructionType(instruction.function);
	const auto target = getTarget(instruction.addressMode, type);
	std::invoke(instruction.function, this, target);

	return true;
}

// Get the target address depending on the addressing mode
constexpr auto CPU::getTarget(AddressMode mode) noexcept -> ValueStore {
constexpr auto CPU::getTarget(AddressMode mode, InstructionType type) noexcept
    -> ValueStore {
	using Mode = AddressMode;
	using Type = InstructionType;

	assert(mode >= Mode::Implicit && mode <= Mode::ZeropageY);



@@ 144,12 148,18 @@ constexpr auto CPU::getTarget(AddressMode mode) noexcept -> ValueStore {

	// Like Absolute, but add value of register X, e.g. JMP $1234,X
	case Mode::AbsoluteX: {
		return {self, toU16(getTarget(Mode::Absolute).get() + indexX)};
		const uint16_t address =
		    relativeAddress(getTarget(Mode::Absolute).get(), indexX,
				    type != Type::Read);
		return {self, address};
	}

	// Like Absolute, but add value of register Y, e.g. JMP $1234,Y
	case Mode::AbsoluteY: {
		return {self, toU16(getTarget(Mode::Absolute).get() + indexY)};
		const uint16_t address =
		    relativeAddress(getTarget(Mode::Absolute).get(), indexY,
				    type != Type::Read);
		return {self, address};
	}

	// Use the value at the address embedded in the instruction


@@ 170,16 180,20 @@ constexpr auto CPU::getTarget(AddressMode mode) noexcept -> ValueStore {
	// Like Zeropage, but the X index to the indirect address
	// e.g. LDA ($12,X)
	case Mode::IndirectX: {
		const auto indirectAddr =
		    getTarget(Mode::Zeropage).get() + indexX;
		const auto indirectAddr = relativeAddress(
		    getTarget(Mode::Zeropage).get(), indexX, true);
		return {self, read2(indirectAddr, true)};
	}

	// Like Indirect, but the Y index to the final address
	// e.g. LDA ($12),Y
	case Mode::IndirectY: {
		const auto indirectAddr = getTarget(Mode::Zeropage).get();
		return {self, toU16(read2(indirectAddr, true) + indexY)};
		const uint16_t indirectAddr = getTarget(Mode::Zeropage).get(),
			       address =
				   relativeAddress(read2(indirectAddr, true),
						   indexY, type != Type::Read);

		return {self, address};
	}

	// Use the value embedded in the instruction as a signed offset


@@ 205,22 219,30 @@ constexpr auto CPU::getTarget(AddressMode mode) noexcept -> ValueStore {
	// Like Zeropage, but add value of register X and wrap within the page
	case Mode::ZeropageX: {
		return {self,
			wrapToByte(getTarget(Mode::Immediate).get() + indexX)};
			wrapToByte(relativeAddress(
			    getTarget(Mode::Immediate).get(), indexX, true))};
	}

	// Like Zeropage, but add value of register Y and wrap within the page
	case Mode::ZeropageY: {
		return {self,
			wrapToByte(getTarget(Mode::Immediate).get() + indexY)};
			wrapToByte(relativeAddress(
			    getTarget(Mode::Immediate).get(), indexY, true))};
	}
	}

	return ValueStore(self);
}

constexpr void CPU::branch(uint16_t address) noexcept { pc = address; }
constexpr void CPU::branch(uint16_t address, bool useCycle) noexcept {
	pc = address;

	if (useCycle)
		cycle++;
}

constexpr auto CPU::read(uint16_t address) const noexcept -> uint8_t {
	cycle++;
	return memory[address];
}



@@ 235,12 257,21 @@ constexpr auto CPU::read2(uint16_t address, bool wrapToPage) const noexcept
		<< 8U);
}

constexpr auto CPU::relativeAddress(uint16_t address, uint8_t offset,
				    bool fixCycle) -> uint16_t {
	if (fixCycle || toU8(address) + offset > u8Max)
		cycle++;

	return address + offset;
}

constexpr void CPU::write(uint16_t address, uint8_t value) noexcept {
	cycle++;
	memory[address] = value;
}

constexpr void CPU::push(uint8_t value) noexcept {
	memory[stackTop + stack--] = value;
	write(stackTop + stack--, value);
}

constexpr void CPU::push2(uint16_t value) noexcept {


@@ 248,16 279,19 @@ constexpr void CPU::push2(uint16_t value) noexcept {
	push(toU8(value & u8Max));
}

constexpr auto CPU::pop() noexcept -> uint8_t {
	return memory[stackTop + ++stack];
constexpr auto CPU::pop(bool preIncrement) noexcept -> uint8_t {
	if (preIncrement)
		cycle++;

	return read(stackTop + ++stack);
}

constexpr auto CPU::pop2() noexcept -> uint16_t {
	return pop() + (pop() << 8U);
constexpr auto CPU::pop2(bool preIncrement) noexcept -> uint16_t {
	return pop(preIncrement) + (pop() << 8U);
}

constexpr void CPU::popFlags() noexcept {
	auto value = pop();
constexpr void CPU::popFlags(bool preIncrement) noexcept {
	auto value = pop(preIncrement);
	value |= Flags::bitmask(F::Unused);
	value &= toU8(~Flags::bitmask(F::Break));
	flags = value;


@@ 323,8 357,8 @@ constexpr void CPU::oAND(ValueStore address) noexcept {
constexpr void CPU::oASL(ValueStore address) noexcept {
	const auto input = address.read();
	flags.set(F::Carry, getBit(7, input));

	const auto result = input << 1U;
	cycle++;
	calculateFlag(result, F::Zero, F::Negative);
	address.write(result);
}


@@ 384,15 418,24 @@ constexpr void CPU::oBVS(ValueStore target) noexcept {
		branch(target.get());
}

constexpr void CPU::oCLC(ValueStore) noexcept { flags.set(F::Carry, false); }
constexpr void CPU::oCLC(ValueStore) noexcept {
	cycle++;
	flags.set(F::Carry, false);
}

constexpr void CPU::oCLD(ValueStore) noexcept { flags.set(F::Decimal, false); }
constexpr void CPU::oCLD(ValueStore) noexcept {
	cycle++;
	flags.set(F::Decimal, false);
}

constexpr void CPU::oCLI(ValueStore) noexcept {
	flags.set(F::InterruptOff, false);
}

constexpr void CPU::oCLV(ValueStore) noexcept { flags.set(F::Overflow, false); }
constexpr void CPU::oCLV(ValueStore) noexcept {
	cycle++;
	flags.set(F::Overflow, false);
}

constexpr void CPU::oCMP(ValueStore address) noexcept {
	const auto input = address.read();


@@ 412,17 455,20 @@ constexpr void CPU::oCPY(ValueStore address) noexcept {
constexpr void CPU::oDEC(ValueStore address) noexcept {
	const auto input = address.read();
	const auto result = input - 1;
	cycle++;
	calculateFlag(result, F::Zero, F::Negative);
	address.write(result);
}

constexpr void CPU::oDEX(ValueStore) noexcept {
	cycle++;
	const auto result = indexX - 1;
	calculateFlag(result, F::Zero, F::Negative);
	indexX = result;
}

constexpr void CPU::oDEY(ValueStore) noexcept {
	cycle++;
	const auto result = indexY - 1;
	calculateFlag(result, F::Zero, F::Negative);
	indexY = result;


@@ 437,27 483,33 @@ constexpr void CPU::oEOR(ValueStore address) noexcept {
constexpr void CPU::oINC(ValueStore address) noexcept {
	const auto input = address.read();
	const auto result = input + 1;
	cycle++;
	calculateFlag(result, F::Zero, F::Negative);
	address.write(result);
}

constexpr void CPU::oINX(ValueStore) noexcept {
	cycle++;
	const auto result = indexX + 1;
	calculateFlag(result, F::Zero, F::Negative);
	indexX = result;
}

constexpr void CPU::oINY(ValueStore) noexcept {
	cycle++;
	const auto result = indexY + 1;
	calculateFlag(result, F::Zero, F::Negative);
	indexY = result;
}

constexpr void CPU::oJMP(ValueStore target) noexcept { pc = target.get(); }
constexpr void CPU::oJMP(ValueStore target) noexcept {
	branch(target.get(), false);
}

constexpr void CPU::oJSR(ValueStore target) noexcept {
	cycle++; // Internal operation
	push2(toU16(pc - 1));
	pc = target.get();
	branch(target.get(), false);
}

constexpr void CPU::oLDA(ValueStore address) noexcept {


@@ 481,12 533,13 @@ constexpr void CPU::oLDY(ValueStore address) noexcept {
constexpr void CPU::oLSR(ValueStore address) noexcept {
	const auto input = address.read();
	const auto result = input >> 1U;
	cycle++;
	calculateFlag(result, F::Zero, F::Negative);
	flags.set(F::Carry, getBit(0, input));
	address.write(result);
}

constexpr void CPU::oNOP(ValueStore) noexcept {}
constexpr void CPU::oNOP(ValueStore) noexcept { cycle++; }

constexpr void CPU::oORA(ValueStore address) noexcept {
	const auto input = address.read();


@@ 495,23 548,31 @@ constexpr void CPU::oORA(ValueStore address) noexcept {
	accumulator = result;
}

constexpr void CPU::oPHA(ValueStore) noexcept { push(accumulator); }
constexpr void CPU::oPHA(ValueStore) noexcept {
	read(pc); // Read and discard
	push(accumulator);
}

constexpr void CPU::oPHP(ValueStore) noexcept {
	read(pc); // Read and discard
	push(toU8(flags.get() | Flags::bitmask(F::Break)));
}

constexpr void CPU::oPLA(ValueStore) noexcept {
	accumulator = pop();
	read(pc); // Read and discard
	accumulator = pop(true);
	calculateFlag(accumulator, F::Zero, F::Negative);
}

constexpr void CPU::oPLP(ValueStore) noexcept { popFlags(); }
constexpr void CPU::oPLP(ValueStore) noexcept {
	read(pc); // Read and discard
	popFlags(true);
}

constexpr void CPU::oROL(ValueStore address) noexcept {
	const auto input = address.read();
	const auto result = setBit(0, input << 1U, flags.test(F::Carry));

	cycle++;
	flags.set(F::Carry, getBit(7, input));
	calculateFlag(result, F::Zero, F::Negative);
	address.write(result);


@@ 520,28 581,38 @@ constexpr void CPU::oROL(ValueStore address) noexcept {
constexpr void CPU::oROR(ValueStore address) noexcept {
	const auto input = address.read();
	const auto result = setBit(7, input >> 1U, flags.test(F::Carry));

	cycle++;
	flags.set(F::Carry, getBit(0, input));
	calculateFlag(result, F::Zero, F::Negative);
	address.write(result);
}

constexpr void CPU::oRTI(ValueStore) noexcept {
	popFlags();
	pc = pop2();
	popFlags(true);
	branch(pop2());
}

constexpr void CPU::oRTS(ValueStore) noexcept { pc = pop2() + 1; }
constexpr void CPU::oRTS(ValueStore) noexcept {
	read(pc); // Read and discard
	branch(pop2(true) + 1);
}

constexpr void CPU::oSBC(ValueStore address) noexcept {
	addWithCarry(~address.read());
}

constexpr void CPU::oSEC(ValueStore) noexcept { flags.set(F::Carry, true); }
constexpr void CPU::oSEC(ValueStore) noexcept {
	cycle++;
	flags.set(F::Carry, true);
}

constexpr void CPU::oSED(ValueStore) noexcept { flags.set(F::Decimal, true); }
constexpr void CPU::oSED(ValueStore) noexcept {
	cycle++;
	flags.set(F::Decimal, true);
}

constexpr void CPU::oSEI(ValueStore) noexcept {
	cycle++;
	flags.set(F::InterruptOff, true);
}



@@ 554,32 625,74 @@ constexpr void CPU::oSTX(ValueStore address) noexcept { address.write(indexX); }
constexpr void CPU::oSTY(ValueStore address) noexcept { address.write(indexY); }

constexpr void CPU::oTAX(ValueStore) noexcept {
	cycle++;
	indexX = accumulator;
	calculateFlag(indexX, F::Zero, F::Negative);
}

constexpr void CPU::oTAY(ValueStore) noexcept {
	cycle++;
	indexY = accumulator;
	calculateFlag(indexY, F::Zero, F::Negative);
}

constexpr void CPU::oTSX(ValueStore) noexcept {
	cycle++;
	indexX = stack;
	calculateFlag(indexX, F::Zero, F::Negative);
}

constexpr void CPU::oTXA(ValueStore) noexcept {
	cycle++;
	accumulator = indexX;
	calculateFlag(accumulator, F::Zero, F::Negative);
}

constexpr void CPU::oTXS(ValueStore) noexcept { stack = indexX; }
constexpr void CPU::oTXS(ValueStore) noexcept {
	cycle++;
	stack = indexX;
}

constexpr void CPU::oTYA(ValueStore) noexcept {
	cycle++;
	accumulator = indexY;
	calculateFlag(accumulator, F::Zero, F::Negative);
}

constexpr auto CPU::getInstructionType(Instruction::Function f)
    -> InstructionType {
	using C = CPU;
	using T = InstructionType;
	const auto map = std::to_array<std::pair<Instruction::Function, T>>({
	    {&C::oLDA, T::Read},
	    {&C::oLDX, T::Read},
	    {&C::oLDY, T::Read},
	    {&C::oEOR, T::Read},
	    {&C::oAND, T::Read},
	    {&C::oORA, T::Read},
	    {&C::oADC, T::Read},
	    {&C::oSBC, T::Read},
	    {&C::oCMP, T::Read},
	    {&C::oBIT, T::Read},
	    {&C::oNOP, T::Read},

	    {&C::oASL, T::ReadModifyWrite},
	    {&C::oLSR, T::ReadModifyWrite},
	    {&C::oROL, T::ReadModifyWrite},
	    {&C::oROR, T::ReadModifyWrite},
	    {&C::oINC, T::ReadModifyWrite},
	    {&C::oDEC, T::ReadModifyWrite},

	    {&C::oSTA, T::Write},
	    {&C::oSTX, T::Write},
	    {&C::oSTY, T::Write},
	});

	const auto *res = std::find_if(map.begin(), map.end(),
				       [&f](auto p) { return p.first == f; });
	return (res != map.end()) ? res->second : InstructionType::Other;
}

constexpr auto CPU::getInstructions() -> Instructions {
	using C = CPU;
	using M = AddressMode;

M src/cpu.hpp => src/cpu.hpp +19 -7
@@ 81,6 81,8 @@ private:
	CPU &cpu;
};

enum class InstructionType : uint8_t { Read, ReadModifyWrite, Write, Other };

struct Instruction {
	using Function = void (CPU::*)(ValueStore);
	Function function = nullptr;


@@ 109,6 111,8 @@ public:
	using Memory = std::array<uint8_t, memorySize>;
	Memory memory{};

	mutable uint64_t cycle{0};

	constexpr void push(uint16_t) = delete;
	constexpr void push2(uint8_t) = delete;



@@ 123,20 127,28 @@ private:
	using Instructions = std::array<Instruction, 256>;
	constexpr static auto getInstructions() -> Instructions;

	constexpr static auto getInstructionType(Instruction::Function f)
	    -> InstructionType;

	// Instruction helpers
	constexpr auto getTarget(AddressMode mode) noexcept -> ValueStore;
	[[nodiscard]] constexpr auto read(uint16_t address) const noexcept
	    -> uint8_t;
	constexpr auto
	getTarget(AddressMode mode,
		  InstructionType type = InstructionType::Other) noexcept
	    -> ValueStore;
	constexpr auto read(uint16_t address) const noexcept -> uint8_t;
	[[nodiscard]] constexpr auto
	read2(uint16_t address, bool wrapToPage = false) const noexcept
	    -> uint16_t;
	[[nodiscard]] constexpr auto
	relativeAddress(uint16_t address, uint8_t offset, bool fixCycle = false)
	    -> uint16_t;
	constexpr void write(uint16_t address, uint8_t value) noexcept;
	constexpr void push(uint8_t) noexcept;
	constexpr void push2(uint16_t) noexcept;
	constexpr auto pop() noexcept -> uint8_t;
	constexpr auto pop2() noexcept -> uint16_t;
	constexpr void popFlags() noexcept;
	constexpr void branch(uint16_t) noexcept;
	constexpr auto pop(bool preIncrement = false) noexcept -> uint8_t;
	constexpr auto pop2(bool preIncrement = false) noexcept -> uint16_t;
	constexpr void popFlags(bool preIncrement = false) noexcept;
	constexpr void branch(uint16_t, bool useCycle = true) noexcept;

	template <class T, class... Args>
	constexpr void calculateFlag(uint8_t value, T flag, Args... flags);

M test/testCPU.cpp => test/testCPU.cpp +2 -0
@@ 40,6 40,7 @@ TEST_CASE("CPU passes nestest", "[cpu]") {
	auto cpu = emu::CPU();
	cpu.loadProgram(nestestProgram, 0x8000);
	cpu.loadProgram(nestestProgram, 0xC000);
	cpu.cycle = nestestStates[0].cycle;

	const auto *prev = nestestStates.begin();
	for (const auto *it = nestestStates.begin(); it != nestestStates.end();


@@ 60,6 61,7 @@ TEST_CASE("CPU passes nestest", "[cpu]") {
		REQUIRE(cpu.flags == state.p);
		REQUIRE(static_cast<unsigned>(cpu.stack) ==
			static_cast<unsigned>(state.sp));
		REQUIRE(cpu.cycle == state.cycle);

		if (!cpu.step())
			break;