CPU Functions
Now that we have our memory and cpu structure setup, it's was time to create functions to initialize the cpu and start processing our ROM!
// Creates a new cpu
int init_cpu(struct cpustate* cpu);
// Tick once in the CPU
int process_cpu(struct cpustate* cpu, uint8_t* memory, uint16_t memory_size);
// Our debug function
int dump_registers(struct cpustate* cpu);
I'm using init_cpu to set the initial state of the 8080 cpu. This is as simple as zeroing out all the registers and setting our program and stack pointers to predefined constants (See my earlier blog post if you are curious where these constants come from).
int init_cpu(struct cpustate* cpu) {
// Set the PC register to PROGRAM_START
// Set SP to start at 0xF000
cpu->PC = PROGRAM_START;
cpu->SP = STACK_START;
// Zero out the registers
cpu->A = 0;
cpu->BC = 0;
cpu->DE = 0;
cpu->HL = 0;
cpu->PSW = 0;
return 0;
}
I figured I should probably create a function to dump all the register values at runtime. The next piece of code I wrote did just that and would prove immensely useful in debugging.
int dump_registers(struct cpustate* cpu)
{
printf("\nPC: 0x%04X | SP: 0x%04X\n", cpu->PC, cpu->SP);
printf("A: 0x%02X | BC: 0x%04X | DE: 0x%04X | HL: 0x%04X\n",
cpu->A, cpu->BC, cpu->DE, cpu->HL);
printf("F-C: %u | F-P: %u | F-AC: %u | F-Z: %u | F-S: %u\n",
cpu->FLAGS.C, cpu->FLAGS.P, cpu->FLAGS.AC, cpu->FLAGS.Z, cpu->FLAGS.S);
}
Last is the process_cpu function which does exactly what it is labeled. Every call of this function should cycle the CPU.
// Helper macro for panic
#define PANIC(...) { printf("\npanic: "); printf(__VA_ARGS__); dump_registers(cpu); return -1; }
// Process a CPU instruction
int process_cpu(struct cpustate* cpu, uint8_t* memory, uint16_t memory_size)
{
// Sanity check
if(cpu->PC >= memory_size) {
PANIC("pc counter overflowed");
}
switch(memory[cpu->PC]) {
// 0x00 = NOP
case 0x00:
cpu->PC += 1;
break;
// Panic if we don't know the instruction
default:
printf("Cannot process opcode %02X\n", memory[cpu->PC]);
PANIC("opcode not implemented");
}
return 0;
}
The code is pretty straightforward. I first fetch the instruction from the memory where the program counter is currently set. I implemented the easiest opcode first, 0x00, which is simply a NOP or no-operation opcode. This just causes the program counter to advance by one. If our CPU encounters an unknown opcode, it will panic and use our dump_registers command from earlier. Panic returns -1 and in the main loop, this will halt the CPU.
Giving our code a test run
The last thing I need to do now is update main.c!
int main()
{
// Set up CPU
struct cpustate cpu;
init_cpu(&cpu);
printf("Starting..\n");
// Main program loop
do {
// I'll put something here shortly I promise!
} while(!process_cpu(&cpu, prom, MEMORY_SIZE));
return 0;
}
Awesome! Now to see what happens when I build and run the program...
It looks like processed over the first few NOP instructions and then panicked immediately when the saw our first non-NOP instruction (0xC3). Our registers are all zero'd out, our program counter advanced to 3, and our stack pointer is at the value it should be. Though this is great manual validation that our code works, we really should have written something that will validate this sort of stuff automatically...
Writing our unit tests
I picked munit for my testing framework for this project primarily because of how light-weight it is. It may lack the features of a more robust test suite such as google test, but it gets the job done.
I wrote a test_main.c file with the first of our tests.
#include "test/munit.h"
#include "test/disasm_test.h"
#include "test/cpu_test.h"
#include "test/cpu_process_test.h"
// List of tests
MunitTest tests[] = {
/*
* CPU Logic
*/
{"/cpu/cpuinit", test_initcpu,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{"/cpu/cpuinit_reset", test_initcpu_reset,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{"/cpu/cpuinit_registers", test_initcpu_registers,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{"/cpu/dump_registers", test_dump_registers,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
// Required
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }
};
// Test Suite
static const MunitSuite suite = {
"/unit-tests", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE
};
// Program entry point
int main (int argc, const char* argv[]) {
return munit_suite_main(&suite, NULL, argc, (char* const*)argv);
}
And our initial tests for init_cpu and dump_registers look like this.
void assert_state_zero(struct cpustate* state) {
munit_assert_int(state->A, ==, 0);
munit_assert_int(state->BC, ==, 0);
munit_assert_int(state->DE, ==, 0);
munit_assert_int(state->HL, ==, 0);
munit_assert_int(state->SP, ==, STACK_START);
munit_assert_int(state->PC, ==, PROGRAM_START);
munit_assert_int(state->PSW, ==, 0);
}
// Ensures the cpu registers start at the proper values
MunitResult
test_initcpu(const MunitParameter params[], void* fixture) {
struct cpustate cpu;
init_cpu(&cpu);
assert_state_zero(&cpu);
return MUNIT_OK;
}
// Ensures calling init_cpu again resets it
MunitResult
test_initcpu_reset(const MunitParameter params[], void* fixture) {
struct cpustate cpu;
init_cpu(&cpu);
assert_state_zero(&cpu);
cpu.A = 0xFF;
cpu.BC = 0xFFFF;
cpu.DE = 0xDEAD;
cpu.HL = 0xDEAD;
cpu.SP = 0xFEFE;
cpu.PC = 0x1010;
cpu.PSW = 0xDE;
init_cpu(&cpu);
assert_state_zero(&cpu);
return MUNIT_OK;
}
// Make sure the unions work as intended
MunitResult
test_initcpu_registers(const MunitParameter params[], void* fixture) {
struct cpustate cpu;
init_cpu(&cpu);
assert_state_zero(&cpu);
cpu.A = 0xFF;
munit_assert_int(cpu.A, ==, 0xFF);
cpu.B = 0xFF;
munit_assert_int(cpu.BC, ==, 0xFF00);
cpu.B = 0xAA;
cpu.C = 0xFF;
munit_assert_int(cpu.BC, ==, 0xAAFF);
cpu.D = 0xAA;
munit_assert_int(cpu.DE, ==, 0xAA00);
cpu.L = 0xBB;
munit_assert_int(cpu.HL, ==, 0x00BB);
cpu.SP = 0xAAAA;
munit_assert_int(cpu.SP, ==, 0xAAAA);
cpu.PC = 0x1234;
munit_assert_int(cpu.PC, ==, 0x1234);
cpu.FLAGS.S = 1;
cpu.FLAGS.Z = 1;
cpu.FLAGS.AC = 1;
cpu.FLAGS.C = 1;
cpu.FLAGS.P = 1;
munit_assert_int(cpu.PSW, ==, 0b11010101);
return MUNIT_OK;
}
// Ensure this function works
MunitResult
test_dump_registers(const MunitParameter params[], void* fixture) {
struct cpustate cpu;
init_cpu(&cpu);
dump_registers(&cpu);
return MUNIT_OK;
}
I am most concerned that my unions are working correctly and that I ordered the bits/registers correctly. For example, if I update register B, the register pair BC should reflect the proper value. In my initial post, I already set Travis CI and Appveyor to automatically run this test program after each build. A failed test will fail the build automatically.
Implementing the first opcode
Earlier, my cpu panicked on instruction C3. Referring our handy dandy 8080 programming bible, we can see that C3 is an opcode referring to an unconditional jump.
A much quicker way, I found out after, is using Control-F on this 8080 opcodes page. This opcode is actually pretty straightforward. We're simply going to load the program counter register will the word following the instruction. While writing my unit test, there are a few important things I need to test for:
- We don't try to read the address out of bounds of memory
- We don't jump to an area outside of memory
- The program counter should update to the address in memory
- The low part of the address is read first and then the high part.
MunitResult
test_cpuprocess_C3(const MunitParameter params[], void* fixture)
{
// Setup CPU
struct cpustate cpu;
// Ensure there is no overflow
{
init_cpu(cpu);
uint8_t program[2] = { x, 0x00 };
int res = process_cpu(cpu, program, 2);
munit_assert_int(res, ==, -1);
}
// JMP instruction should set the PC to 0x0002
{
init_cpu(cpu);
uint8_t program[MEMORY_SIZE] = {0xC3, 0x02, 0x00};
int res = process_cpu(cpu, program, MEMORY_SIZE);
munit_assert_int(res, ==, 0);
munit_assert_int(cpu->PC, ==, 0x0002);
}
// JMP instruction should set the PC
{
init_cpu(cpu);
uint8_t program[MEMORY_SIZE] = {0xC3, 0x30, 0x10};
int res = process_cpu(cpu, program, MEMORY_SIZE);
munit_assert_int(res, ==, 0);
munit_assert_int(cpu->PC, ==, 0x1030);
}
// JMP instruction should fail, jumping to out of bounds
{
init_cpu(cpu);
uint8_t program[MEMORY_SIZE] = {0xC3, 0xFF, 0xFF};
int res = process_cpu(cpu, program, MEMORY_SIZE);
munit_assert_int(res, ==, -1);
}
return MUNIT_OK;
}
With my C3 opcode test function in place, I just need to add another line in test_main.c to actually run the test.
#include "test/munit.h"
#include "test/disasm_test.h"
#include "test/cpu_test.h"
#include "test/cpu_process_test.h"
// List of tests
MunitTest tests[] = {
/*
* CPU Logic
*/
{"/cpu/cpuinit", test_initcpu,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{"/cpu/cpuinit_reset", test_initcpu_reset,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{"/cpu/cpuinit_registers", test_initcpu_registers,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{"/cpu/dump_registers", test_dump_registers,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
/*
* JMP instruction
*/
{"/cpu_process/C3",test_cpuprocess_C3,
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
// Required
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }
};
// Test Suite
static const MunitSuite suite = {
"/unit-tests", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE
};
// Program entry point
int main (int argc, const char* argv[]) {
return munit_suite_main(&suite, NULL, argc, (char* const*)argv);
}
Now onto the actual implementation.
// Helper macro for panic
#define PANIC(...) { printf("\npanic: "); printf(__VA_ARGS__); dump_registers(cpu); return -1; }
// Process a CPU instruction
int process_cpu(struct cpustate* cpu, uint8_t* memory, uint16_t memory_size)
{
// Sanity check
if(cpu->PC >= memory_size) {
PANIC("pc counter overflowed");
}
// Temp 16-bit register
uint16_t tmp;
tmp = 0;
switch(memory[cpu->PC]) {
// 0x00 = NOP
case 0x00:
cpu->PC += 1;
break;
// 0xC3 = JMP 0x0000
case 0xC3:
if(cpu->PC+2 >= memory_size)
PANIC("%02X instruction overflows buffer", cpu->PC);
tmp = memory[cpu->PC+1] + (memory[cpu->PC+2] << 8);
if(tmp >= memory_size)
PANIC("C3 instruction jumped outside memory bounds");
cpu->PC = tmp;
break;
// Panic if we don't know the instruction
default:
printf("Cannot process opcode %02X\n", memory[cpu->PC]);
PANIC("opcode not implemented");
}
return 0;
}
I added some sanity checks to pass the first of our tests, and then set the program counter to our new address. To better see how I'm processing instructions, I also implemented a function called opcode_to_text which will print out the opcode as the cpu processes them. This file looks like this:
#include <stdio.h>
#include "disasm.h"
#define CHECK_BUFFER(x) { if((*counter)+x >= buffer_size) { printf("\nbuffer overflow\n"); return -2; }};
#define PRINT_WORD(x) { CHECK_BUFFER(2); printf("%s 0x%02X%02X", x, buffer[(*counter)+1], buffer[(*counter)+2]); *counter += 3; };
#define PRINT_BYTE(x) { CHECK_BUFFER(1); printf("%s 0x%02X", x, buffer[(*counter)+1]); *counter += 2; };
#define PRINT_OP(x) { printf("%s", x); *counter += 1; }
int op_to_text(unsigned char* buffer, int buffer_size, int* counter)
{
int res = 0;
CHECK_BUFFER(0);
printf("%04X: %02X ", *counter, buffer[*counter]);
switch(buffer[*counter]) {
case 0x00: PRINT_OP("NOP"); break;
case 0xC3: PRINT_WORD("JMP"); break;
default:
PRINT_OP("???");
res = -1;
}
printf("\n");
return res;
}
Adding this to my main function now makes main.c look like this:
int main()
{
// Set up CPU
struct cpustate cpu;
init_cpu(&cpu);
printf("Starting..\n");
// Main program loop
do {
// Need to copy the PC since op_to_name advances it.
int tPC = cpu.PC;
op_to_text(prom, MEMORY_SIZE, &tPC);
} while(!process_cpu(&cpu, prom, MEMORY_SIZE));
return 0;
}
Compiling and Testing
With my new opcode in place, let's compile and give my test program a quick run!
All the tests are passing! Alright, now time to run the emulator itself..
With my new opcode_to_name function, we can see the CPU process over the first few NOP opcodes before processing our C3 opcode. It panics at the very next opcode (0x31) and then dumps it's registers. Our program counter is now set to 0x18D4 where according to the instructions is exactly where it should be. Looks like everything is working!
Next Steps
The next steps are to go through each opcode and implement them. The strategy I've observed is to implement as you go since some opcodes aren't going to be used in space invaders and implementing those would be a waste of effort.