gb-emu: initial commit

Add a mostly non-functional Gameboy CPU and the skeleton
of a Gameboy assembler intended for unit tests.
This commit is contained in:
2016-12-18 23:43:41 -08:00
commit 6e2f4096a2
38 changed files with 4424 additions and 0 deletions

79
src/apps/gbasm-test.c Normal file
View File

@@ -0,0 +1,79 @@
#include "gbasm/assemble.h"
#include "gbasm/emitter.h"
#include "tests/gbasm/test.h"
#include "tests/gbasm/inc.h"
#include "tests/gbasm/fixed.h"
#include "common.h"
#include <string.h> /* memset */
#include <glib.h>
const static struct gbasm_tests *tests[] = {
&gbasm_fixed_tests,
&gbasm_inc_tests,
};
static int cmp_mem(const uint8_t *test_data, const uint8_t *expected_data, size_t len)
{
for (size_t i = 0; i < len; i++) {
if (test_data[i] != expected_data[i]) {
g_test_message("memory contents differed at index %zu, data=%x, expected=%x",
i, test_data[i], expected_data[i]);
return -1;
}
}
return 0;
}
static void run_test(const void *test_data)
{
static uint8_t program[4096];
const struct gbasm_test *test = test_data;
struct emitter emitter;
struct buffer_emitter buffer_emitter;
char *src;
int rc;
size_t len;
src = strdup(test->asm_source);
memset(program, 0, sizeof(program));
buffer_emitter_init(&emitter, &buffer_emitter, program, sizeof(program));
rc = gbasm_assemble(src, &emitter);
g_assert_cmpint(rc, ==, 0);
len = test->expected_output_len;
if (len != buffer_emitter.cursor) {
g_warning("output lengths were not equal expected %zu, actual %zu",
len, buffer_emitter.cursor);
g_test_fail();
len = MIN(len, buffer_emitter.cursor);
}
rc = cmp_mem(program, test->expected_output, len);
g_assert_cmpint(rc, ==, 0);
free(src);
}
int main(int argc, char **argv)
{
char test_name_buff[256];
g_test_init(&argc, &argv, NULL);
for (int i = 0; i < ARRAY_SIZE(tests); i++) {
for (int j = 0; j < tests[i]->num_tests; j++) {
snprintf(test_name_buff, sizeof(test_name_buff),
"/gbasm/%s/%s",
tests[i]->name,
tests[i]->tests[j]->name);
g_test_add_data_func(test_name_buff, tests[i]->tests[j], run_test);
}
}
return g_test_run();
}

87
src/apps/gbasm.c Normal file
View File

@@ -0,0 +1,87 @@
/* A simple, two-pass gameboy assembler */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "gbasm/emitter.h"
#include "gbasm/assemble.h"
#define GBASM_FILE_BUFFER_SIZE (1024 * 1024)
static char file_buffer[GBASM_FILE_BUFFER_SIZE];
static void usage()
{
printf("gbasm -i <file> -o <file>\n"
" -h Print this usage \n"
" -i <input file> The file to assemble\n"
" -o <output file> The file to write the assembled program to\n");
}
int main(int argc, char **argv)
{
struct emitter emitter;
struct fd_emitter fd_emitter;
char opt;
int in_fd = -1;
int out_fd = -1;
while ((opt = getopt(argc, argv, "i:o:h")) != -1) {
switch (opt) {
case 'o':
if (out_fd >= 0) {
close(out_fd);
}
out_fd = open(optarg, O_WRONLY | O_CREAT);
if (out_fd < 0) {
fprintf(stderr, "failed to open output file %s\n", optarg);
exit(1);
}
break;
case 'i':
if (in_fd >= 0) {
close(in_fd);
}
in_fd = open(optarg, O_RDONLY);
if (in_fd < 0) {
fprintf(stderr, "failed to open input file %s\n", optarg);
exit(1);
}
break;
case 'h':
usage();
exit(1);
case ':':
fprintf(stderr, "Option -%c requires argument\n", optopt);
usage();
exit(1);
case '?':
fprintf(stderr, "Unrecognized option -%c\n", optopt);
usage();
exit(1);
}
}
if (in_fd < 0) {
exit(1);
} else {
pread(in_fd, file_buffer, GBASM_FILE_BUFFER_SIZE - 1, 0);
file_buffer[GBASM_FILE_BUFFER_SIZE - 1] = '\0';
}
if (out_fd < 0) {
exit(1);
}
fd_emitter_init(&emitter, &fd_emitter, out_fd);
return gbasm_assemble(file_buffer, &emitter);
}

525
src/apps/gbdb.c Normal file
View File

@@ -0,0 +1,525 @@
/*
* A CLI-based tester and debugger for the Gameboy's LR35902 CPU
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <stdbool.h>
#include <signal.h>
#include "common.h"
#include "gb_disas.h"
#include "cpu.h"
#include "video.h"
#include "tri.h"
#define INPUT_MAX_LEN 512
#define MAX_BREAKPTS 256 /* Should be plenty for anyone */
#define MAX_RAM_LEN (1 << 20) /* Up to 8Mb Cartridge */
#define UNMAP_BOOTROM_ADDR 0xff50
typedef void (gbdb_cmd)(char *arg_string);
static bool cpu_at_breakpoint(void);
static uint8_t ram[MAX_RAM_LEN];
static const char prompt[] = "gbdb >";
static const char usage[] =
"Available commands:\n"
"load <file>: loads the given file as the gameboy cartridge\n"
"run: runs the CPU until a breakpoint or halt is hit\n"
"break <addr>: Adds a breakpoint for the given addess\n"
"step <cycles>: executes \"cycle\" instructions (default 1)\n"
"regs: dumps the state of the registers\n"
"peek: view the next instruction to run\n"
"exit: quit the program\n";
static const char *reg16_names[] = {
"AF",
"BC",
"DE",
"HL",
"SP",
"PC",
};
static const char *reg8_names[] = {
"B",
"C",
"D",
"E",
"H",
"L",
"N/A",
"A",
"F"
};
static struct lr35902_state cpu;
static struct gb_video video;
static unsigned char bootrom[0x100] = {0};
static int bootrom_mapped = 1;
static volatile sig_atomic_t paused = 0;
static void break_execution_handler(int signum)
{
paused = 1;
}
/*
* ram_{read,write} work easily because the cartridge is mapped to 0x0000
*
*/
static void strip_newline(char *string)
{
char *pos;
if (string == NULL)
return;
if ((pos=strchr(string, '\n')) != NULL) {
*pos = '\0';
}
}
static uint8_t mem_read(struct lr35902_state *cpu, uint16_t addr)
{
switch (addr) {
case 0 ... 0x100-1:
if (bootrom_mapped) {
return bootrom[addr];
} else {
return ram[addr];
}
case 0xFF40 ... 0xFF4B:
return gb_video_mem_read(&video, addr);
default:
return ram[addr];
}
}
static void mem_write(struct lr35902_state *cpu, uint16_t addr, uint8_t val)
{
switch (addr) {
case UNMAP_BOOTROM_ADDR:
if (val == 1) {
bootrom_mapped = 0;
printf("bootrom unmapped\n");
}
break;
case 0xFF40 ... 0xFF4B:
gb_video_mem_write(&video, addr, val);
break;
case 0 ... 0x100:
if (bootrom_mapped) {
break;
}
/* Intentional fall-through */
default:
ram[addr] = val;
}
}
static void mem_dump(char *filename)
{
//TODO: Check error codes like a real programmer :P
FILE* dump_file;
char *token = strtok(filename, " ");
if (token == NULL) {
printf("usage: load <file>\n");
return;
}
strip_newline(token);
dump_file = fopen(token, "w");
if(dump_file < 0) {
printf("Failed to open mem dump file: %d\n", errno);
return;
}
fwrite(ram, MAX_RAM_LEN, 1, dump_file);
fclose(dump_file);
}
static void init(void)
{
lr35902_init(&cpu, mem_read, mem_write);
gb_video_init(&video);
memset(&ram, 0, MAX_RAM_LEN);
bootrom_mapped = 1;
}
static void cycle()
{
lr35902_cycle(&cpu);
gb_video_cycle(&video);
}
static void show_prompt()
{
printf("%s ", prompt);
fflush(stdout);
}
static void step(char *arg_list)
{
uint64_t steps;
int i = 0;
char *token;
token = strtok(arg_list, " ");
if (token == NULL) {
steps = 1;
} else {
strip_newline(token);
steps = strtol(token, NULL, 0);
}
paused = 0;
signal(SIGINT, break_execution_handler);
do {
cycle();
i++;
} while (i < steps && !cpu_at_breakpoint() && !cpu.halted && !paused);
if (i == steps) {
printf("CPU stopped after %d cycles\n", i);
} else if (cpu_at_breakpoint()) {
printf("Breakpoint hit\n");
} else if (cpu.halted) {
printf("CPU halted\n");
} else {
printf("Interrupted after %d cycles\n", i);
}
}
static void regs(char *arg_list)
{
int i;
for(i = 0; i < NUM_LR35902_REGS_8; i++){
if (i != LR35902_REG_HL_DEREF) {
printf("%s: 0x%02x\n", reg8_names[i], lr35902_get_reg_8(&cpu, i));
};
}
for(i = 0; i < NUM_LR35902_REGS_16; i++){
printf("%s: 0x%04x\n", reg16_names[i], lr35902_get_reg_16(&cpu, i));
}
}
static void peek(char *arg_list)
{
uint16_t pc = lr35902_get_reg_16(&cpu, LR35902_REG_PC);
uint8_t byte = mem_read(&cpu, pc);
printf("0x%04x:%s\n", pc, gb_byte_to_opcode(byte));
}
static void stats(char *arg_list)
{
printf("Cycles: %lu\n", cpu.metrics.cycles);
printf("Retired Insructions %lu\n", cpu.metrics.retired_instrs);
printf("Memory Reads: %lu\n", cpu.metrics.mem_reads);
printf("Memory Writes: %lu\n", cpu.metrics.mem_writes);
}
static void help(char *arg_list)
{
printf(usage);
}
static void mem(char *arg_list)
{
uint16_t addr, bytes, i;
char *token = strtok(arg_list, " ");
if (token == NULL) {
printf("usage: mem <addr> (num bytes) (format)\n");
return;
}
addr = strtol(token, NULL, 0);
token = strtok(NULL, " ");
if (token == NULL) {
bytes = 1;
} else {
bytes = strtol(token, NULL, 0);
}
token = strtok(NULL, " ");
for (i = 0; i < bytes; i++) {
//TODO: Make sure this has no side effects
int val = mem_read(&cpu, addr + i);
if (token != NULL && token[0] == 'i') {
printf("0x%04x:%s\n", addr + i, gb_byte_to_opcode(val));
} else if (token != NULL && token[0] == 'b') {
printf("0x%04x:0x%02x %s\n", addr + i, val, gb_byte_to_opcode(val));
} else {
printf("0x%04x:0x%02x\n", addr + i, val);
}
}
}
static void load_file_to_buffer(char *filename, uint8_t *buffer, size_t size)
{
//TODO: Check error codes like a real programmer :P
FILE* prog_file;
long filesize;
prog_file = fopen(filename, "r");
if(prog_file < 0) {
printf("Failed to load game file: %d\n", errno);
return;
}
fseek(prog_file, 0, SEEK_END);
filesize = ftell(prog_file);
fseek(prog_file, 0, SEEK_SET);
fread(buffer, MIN(filesize, size), 1, prog_file);
fclose(prog_file);
}
static void load(char *arg_list)
{
char *token = strtok(arg_list, " ");
if (token == NULL) {
printf("usage: load <file>\n");
return;
}
strip_newline(token);
load_file_to_buffer(token, ram, sizeof(ram));
}
static void load_bootrom(char *arg_list)
{
char *token = strtok(arg_list, " ");
if (token == NULL) {
printf("usage: bootrom <file>\n");
return;
}
strip_newline(token);
load_file_to_buffer(token, bootrom, sizeof(bootrom));
}
static void quit(char *arg_list)
{
exit(0);
}
static void run(char *arg_list)
{
paused = 0;
signal(SIGINT, break_execution_handler);
while(!cpu.halted && !cpu_at_breakpoint() && !paused) {
cycle();
}
if (cpu.halted) {
printf("CPU halted after %ld cycles\n", cpu.metrics.cycles);
} else if (paused) {
printf("Interrupted.\n");
} else {
printf("Breakpoint hit\n");
}
}
static struct {
uint16_t addr;
bool active;
} breakpoints[MAX_BREAKPTS];
static void set_breakpoint(char *arg_string)
{
uint16_t addr, i;
char *token = strtok(arg_string, " ");
if (token == NULL) {
printf("usage: breakpoint add <addr>\n");
return;
}
addr = strtol(token, NULL, 0);
for (i = 0; i < ARRAY_SIZE(breakpoints); i++) {
if (breakpoints[i].active == false) {
breakpoints[i].addr = addr;
breakpoints[i].active = true;
return;
}
}
printf("maximum number of breakpoints reached\n");
}
static void delete_breakpoint(char *arg_string)
{
int bkpt;
char *token = strtok(arg_string, " ");
if (token == NULL) {
printf("usage: breakpoint rm <bkpt num>\n");
return;
}
bkpt = strtol(token, NULL, 0);
if (bkpt < 0 || bkpt >= ARRAY_SIZE(breakpoints) || !breakpoints[bkpt].active) {
printf("%d is not a valid breakpoint number\n", bkpt);
return;
}
breakpoints[bkpt].active = false;
}
static void display_breakpoints(char *arg_string)
{
bool found1 = false;
int i;
for (i = 0; i < ARRAY_SIZE(breakpoints); i++) {
if (breakpoints[i].active) {
printf("#%d: 0x%04x\n", i, breakpoints[i].addr);
found1 = true;
}
}
if(!found1) {
printf("No breakpoints set\n");
}
}
static void breakpoint(char *arg_list)
{
static bool init = false;
static struct tri commands;
if (!init) {
memset(breakpoints, 0, sizeof(breakpoints));
tri_init(&commands);
tri_add_string(&commands, "add", set_breakpoint);
tri_add_string(&commands, "set", set_breakpoint);
tri_add_string(&commands, "delete", delete_breakpoint);
tri_add_string(&commands, "remove", delete_breakpoint);
tri_add_string(&commands, "info", display_breakpoints);
tri_add_string(&commands, "display", display_breakpoints);
tri_add_string(&commands, "list", display_breakpoints);
init = true;
}
char * remainder;
gbdb_cmd *cmd;
bool ambiguous = false;
remainder = strchr(arg_list, ' ');
if (remainder != NULL) {
remainder[0] = '\0';
remainder++;
}
cmd = tri_get_string_autocomplete(&commands, arg_list, &ambiguous);
if(ambiguous) {
printf("ambiguous breakpoint command: '%s'\n", arg_list);
return;
}
if (cmd == NULL) {
printf("unrecognized breakpoint command: '%s'\n", arg_list);
return;
}
cmd(remainder);
}
static bool breakpoint_is_at_addr(uint16_t addr) {
int i;
for (i = 0; i < ARRAY_SIZE(breakpoints); i++) {
if (breakpoints[i].active && addr == breakpoints[i].addr) {
return true;
}
}
return false;
}
static bool cpu_at_breakpoint(void)
{
return breakpoint_is_at_addr(lr35902_get_reg_16(&cpu, LR35902_REG_PC));
}
int main(int argc, char **argv)
{
struct tri commands;
char line_buffer[INPUT_MAX_LEN];
char old_buffer[INPUT_MAX_LEN];
tri_init(&commands);
tri_add_string(&commands, "step", step);
tri_add_string(&commands, "run", run);
tri_add_string(&commands, "regs", regs);
tri_add_string(&commands, "stats", stats);
tri_add_string(&commands, "exit", quit);
tri_add_string(&commands, "quit", quit);
tri_add_string(&commands, "load", load);
tri_add_string(&commands, "bootrom", load_bootrom);
tri_add_string(&commands, "mem", mem);
tri_add_string(&commands, "dump", mem_dump);
tri_add_string(&commands, "peek", peek);
tri_add_string(&commands, "help", help);
tri_add_string(&commands, "breakpoint", breakpoint);
tri_add_string(&commands, "reset", init);
init();
while (1) {
bool ambiguous;
char *remainder;
gbdb_cmd* cmd;
char *cmd_string;
show_prompt();
fgets(line_buffer, INPUT_MAX_LEN, stdin);
if (line_buffer[0] != '\n') {
cmd_string = line_buffer;
} else {
cmd_string = old_buffer;
}
strip_newline(cmd_string);
remainder = strchr(cmd_string, ' ');
if (remainder != NULL) {
remainder[0] = '\0';
remainder++;
}
cmd = tri_get_string_autocomplete(&commands, cmd_string, &ambiguous);
if (ambiguous) {
printf("ambiguous command: '%s'\n", cmd_string);
} else if (cmd == NULL) {
printf("unrecognized command: '%s'\n", cmd_string);
} else {
cmd(remainder);
}
if (cmd_string == line_buffer) {
strncpy(old_buffer, line_buffer, INPUT_MAX_LEN);
}
}
return 0;
}

20
src/apps/sample-test.c Normal file
View File

@@ -0,0 +1,20 @@
#include <glib.h>
void test_fail(const void *data)
{
g_test_fail();
}
void test_pass(const void *data)
{
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_data_func("/pass", NULL, test_pass);
g_test_add_data_func("/fail", NULL, test_fail);
return g_test_run();
}

65
src/apps/tri-test.c Normal file
View File

@@ -0,0 +1,65 @@
#include "tri.h"
#include "common.h"
#include <glib.h>
#include <stdint.h>
#define TO_PTR(x) ((void *) (uintptr_t) (x))
#define TO_INT(x) ((int) (uintptr_t) (x))
static void test_tri_get_string(void)
{
struct tri tri;
tri_init(&tri);
g_assert_cmpint(tri_add_string(&tri, "a", TO_PTR(1)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "ab", TO_PTR(2)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "abc", TO_PTR(3)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "af", TO_PTR(4)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "abd", TO_PTR(5)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "$a!0123~&.`", TO_PTR(6)), ==, 0);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "a")), ==, 1);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "ab")), ==, 2);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "abc")), ==, 3);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "af")), ==, 4);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "abd")), ==, 5);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "abz")), ==, 0);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "ag")), ==, 0);
g_assert_cmpint(TO_INT(tri_get_string(&tri, "$a!0123~&.`")), ==, 6);
tri_free(&tri);
}
static void test_tri_prefix_match(void)
{
struct tri tri;
tri_init(&tri);
g_assert_cmpint(tri_add_string(&tri, "pr", TO_PTR(1)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "pref", TO_PTR(2)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "Z", TO_PTR(3)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "prefix", TO_PTR(4)), ==, 0);
g_assert_cmpint(tri_add_string(&tri, "blah", TO_PTR(5)), ==, 0);
g_assert_cmpint(TO_INT(tri_prefix_match(&tri, "p")), ==, 0);
g_assert_cmpint(TO_INT(tri_prefix_match(&tri, "pr")), ==, 1);
g_assert_cmpint(TO_INT(tri_prefix_match(&tri, "pre")), ==, 1);
g_assert_cmpint(TO_INT(tri_prefix_match(&tri, "pref")), ==, 2);
g_assert_cmpint(TO_INT(tri_prefix_match(&tri, "prefix10")), ==, 4);
g_assert_cmpint(TO_INT(tri_prefix_match(&tri, "asdfasdf")), ==, 0);
tri_free(&tri);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/tri/get_string", test_tri_get_string);
g_test_add_func("/tri/prefix_match", test_tri_prefix_match);
return g_test_run();
}