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:
525
src/apps/gbdb.c
Normal file
525
src/apps/gbdb.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user