Close

ABI

A project log for Dodo: 6502 Game System

Handheld Game System featuring the 6502

peter-noyesPeter Noyes 04/03/2016 at 04:020 Comments

Dodo has now taken a big step forward and supports swappable game cartridges. To accomplish this a separation needed to be established between system calls and game logic. Up until now the game has been compiled and assembled monolithically into a binary that was run from the EEPROM. The goal was to get just the system calls on the EEPROM, and the game copied into memory from the SPI FRAM chip.

The best description for the set of system calls is an ABI (Application Binary Interface). It would be a stretch to call it an OS because it is not multi-tasking, there is no virtual memory and no file system. I guess it could also be considered a BIOS. Separating the ABI is tricky because the functions need to be in a consistent memory location so that they can be called by a the game which would now be separately compiled. I also want developers for Dodo to be able to make the system calls from C, so the functions need to remain compatible with the cc65 calling convention.

The 6502 looks to the last 6 bytes of memory for a special set of pointers for RESET, IRQ, and NMI. I decided to add one more custom vector of my own that points to what I am calling a trampoline function. I have created a header file which defines a series of function pointers with the correct prototype for each function, but they all point to the same memory location which is the trampoline. Each function has been assigned a unique index which is used to find the real pointer to the function in a jump table.

This is what some of the header file looks like now:

#ifndef _API_H
#define _API_H

#define byte unsigned char

#define SPI_WREN  0x06
#define SPI_WRDI  0x04
#define SPI_RDSR  0x05
#define SPI_WRSR  0x01
#define SPI_READ  0x03
#define SPI_WRIT  0x02
#define SPI_RDID  0x9F

#define DRAW_SPRITE(sprite, x, y, w, h, f) draw_sprite_proto(sprite, x, y, w, h, f, 0)
#define DISPLAY() display_proto(1)
#define CLEAR_SPRITE(x, y, w, h) clear_sprite_proto(x, y, w, h, 2)
#define SET_PIXEL(x, y, c) set_pixel_proto(x, y, c, 3)
#define DRAW_LINE(x0, y0, x1, y1, c) draw_line_proto(x0, y0, x1, y1, c, 4)
#define DELAY_MS(delay) delay_ms_proto(delay, 5)
#define LED_ON() led_on_proto(6)
#define LED_OFF() led_off_proto(7)
#define WAIT() wait_proto(8)
#define LOAD_MUSIC(music) load_music_proto(music, 9)
#define PLAY_EFFECT(effect) play_effect_proto(effect, 10)
#define SPI_ENABLE() spi_enable_proto(11)
#define SPI_DISABLE() spi_disable_proto(12)
#define SPI_WRITE(v) spi_write_proto(v, 13)

static void (*draw_sprite_proto)(byte*, byte, byte, byte, byte, byte, byte);
static void (*display_proto)(byte);
static void (*clear_sprite_proto)(byte, byte, byte, byte, byte);
static void (*set_pixel_proto)(byte, byte, byte, byte);
static void (*draw_line_proto)(byte, byte, byte, byte, byte, byte);
static void (*delay_ms_proto)(byte, byte);
static void (*led_on_proto)(byte);
static void (*led_off_proto)(byte);
static void (*wait_proto)(byte);
static void (*load_music_proto)(byte*, byte);
static void (*play_effect_proto)(byte*, byte);
static void (*spi_enable_proto)(byte);
static void (*spi_disable_proto)(byte);
static byte (*spi_write_proto)(byte, byte);


static unsigned char get_sp() {
	asm("lda #sp");
	return __A__;
}

void api_init() {
	byte* sp_ptr = (byte*)get_sp();
	__A__ = (byte)sp_ptr;
	asm("sta $0");

	draw_sprite_proto = (void (*)(byte*, byte, byte, byte, byte, byte, byte))(*(int*)0xFFF8);
	display_proto = (void (*)(byte))(*(int*)0xFFF8);
	clear_sprite_proto = (void (*)(byte, byte, byte, byte, byte))(*(int*)0xFFF8);
	set_pixel_proto = (void (*)(byte, byte, byte, byte))(*(int*)0xFFF8);
	draw_line_proto = (void (*)(byte, byte, byte, byte, byte, byte))(*(int*)0xFFF8);
	delay_ms_proto = (void (*)(byte, byte))(*(int*)0xFFF8);
	led_on_proto = (void (*)(byte))(*(int*)0xFFF8);
	led_off_proto = (void (*)(byte))(*(int*)0xFFF8);
	wait_proto = (void (*)(byte))(*(int*)0xFFF8);
	load_music_proto = (void (*)(byte*, byte))(*(int*)0xFFF8);
	play_effect_proto = (void (*)(byte*, byte))(*(int*)0xFFF8);
	spi_enable_proto = (void (*)(byte))(*(int*)0xFFF8);
	spi_disable_proto = (void (*)(byte))(*(int*)0xFFF8);
	spi_write_proto = (byte (*)(byte, byte))(*(int*)0xFFF8);
}
The index into the jump table is hidden as the last parameter for each function and is used by the trampoline to find the real function. The #define for each function is what hides the index.

What I have working now is that when Dodo boots, it waits for button input. If the user presses 'A', it will go ahead and copy the game from FRAM into a block in memory and execute it. If the user presses 'B', it will flash the FRAM chip from a computer connected via RS232. The game developer only needs to reference the api.h file, and have just a bit of bootstrap assembly code. The developer can write almost the entire game in C and it should still have decent performance because the ABI calls are all written in assembly and optimized.

Discussions