Arduino gameboy cart reader software, using https://github.com/insidegadgets/GBCartRead
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
6.0 KiB
227 lines
6.0 KiB
4 years ago
|
#include "cart.h"
|
||
|
|
||
|
#include <stdint.h>
|
||
|
#include <avr/io.h>
|
||
|
#include <util/delay.h>
|
||
|
|
||
|
#include "utils.h"
|
||
|
#include "pins.h"
|
||
|
#include "spi.h"
|
||
|
#include "rom.h"
|
||
|
|
||
|
|
||
|
// Most of the pulses described here are arbitrarily chosen,
|
||
|
// to satisfy my idiotic UX requirements with the leds.
|
||
|
// Here's a pretty cool bus analysis:
|
||
|
// https://dhole.github.io/post/gameboy_cartridge_emu_1/
|
||
|
|
||
|
static void cart_address(const uint16_t address)
|
||
|
{
|
||
|
spi_transfer(address >> 8);
|
||
|
spi_transfer(address & 0xFF);
|
||
|
writepin(PIN_STORE, HIGH);
|
||
|
__asm__ volatile ("nop");
|
||
|
writepin(PIN_STORE, LOW);
|
||
|
}
|
||
|
|
||
|
uint8_t cart_read(const uint16_t address)
|
||
|
{
|
||
|
cart_address(address);
|
||
|
__asm__ volatile ("nop");
|
||
|
return PINS_DAT;
|
||
|
}
|
||
|
|
||
|
void cart_write(const uint16_t address, const uint8_t byte)
|
||
|
{
|
||
|
cart_address(address);
|
||
|
PORTB = (PORTB & ~PINS_DATB) | (byte >> 6);
|
||
|
PORTD = (PORTD & ~PINS_DATD) | (byte << 2);
|
||
|
writepin(PIN_WRITE, LOW);
|
||
|
__asm__ volatile ("nop");
|
||
|
writepin(PIN_WRITE, HIGH);
|
||
|
}
|
||
|
|
||
|
void cart_set_read(void)
|
||
|
{
|
||
|
// Set the data pins as pull-up inputs
|
||
|
DDRB &= ~PINS_DATB;
|
||
|
DDRD &= ~PINS_DATD;
|
||
|
PORTB |= PINS_DATB;
|
||
|
PORTD |= PINS_DATD;
|
||
|
|
||
|
writepin(PIN_READ, LOW);
|
||
|
writepin(PIN_WRITE, HIGH);
|
||
|
}
|
||
|
|
||
|
void cart_set_write(void)
|
||
|
{
|
||
|
// Set the data pins as low outputs
|
||
|
DDRB |= PINS_DATB;
|
||
|
DDRD |= PINS_DATD;
|
||
|
PORTB &= ~PINS_DATB;
|
||
|
PORTD &= ~PINS_DATD;
|
||
|
|
||
|
writepin(PIN_READ, HIGH);
|
||
|
writepin(PIN_WRITE, HIGH);
|
||
|
}
|
||
|
|
||
|
void cart_set_standby(void)
|
||
|
{
|
||
|
// Set the data pins as pull-up inputs
|
||
|
DDRB &= ~PINS_DATB;
|
||
|
DDRD &= ~PINS_DATD;
|
||
|
PORTB |= PINS_DATB;
|
||
|
PORTD |= PINS_DATD;
|
||
|
|
||
|
writepin(PIN_READ, HIGH);
|
||
|
writepin(PIN_WRITE, HIGH);
|
||
|
}
|
||
|
|
||
|
void cart_select_rom(void)
|
||
|
{
|
||
|
writepin(PIN_CS, HIGH);
|
||
|
}
|
||
|
|
||
|
void cart_select_ram(void)
|
||
|
{
|
||
|
writepin(PIN_CS, LOW);
|
||
|
}
|
||
|
|
||
|
void cart_init(void)
|
||
|
{
|
||
|
// Set the appropriate pins as output
|
||
|
DDRB |= pin(PIN_STORE);
|
||
|
DDRC |= pin(PIN_READ) | pin(PIN_WRITE) | pin(PIN_CS);
|
||
|
|
||
|
// Initialize the cart as standby
|
||
|
cart_set_standby();
|
||
|
cart_select_rom();
|
||
|
|
||
|
// Initialize the shift register
|
||
|
writepin(PIN_STORE, LOW);
|
||
|
spi_init(SPI_DIV8);
|
||
|
cart_address(0);
|
||
|
}
|
||
|
|
||
|
void cart_off(void)
|
||
|
{
|
||
|
cart_address(0);
|
||
|
spi_off();
|
||
|
PORTC &= ~pin(PIN_READ) & ~pin(PIN_CS) & ~pin(PIN_WRITE);
|
||
|
writepin(PIN_STORE, LOW);
|
||
|
}
|
||
|
|
||
|
int cart_boot(struct cart_info *info)
|
||
|
{
|
||
|
cart_set_read();
|
||
|
|
||
|
// "simulate" the GB power-up process, by reading the logo once, before comparing.
|
||
|
// This might be compatible with carts that hijack the logo on bootup.
|
||
|
// There is no point in simulating this more accurately (i.e. timing)
|
||
|
// if we don't even simulate the RD/WR pulses properly.
|
||
|
// http://gbdev.gg8.se/wiki/articles/Gameboy_Bootstrap_ROM
|
||
|
|
||
|
// Read the logo once (to display on the screen)
|
||
|
for (unsigned i = rom_offset(logo);
|
||
|
i < rom_offset_end(logo); i++) cart_read(i);
|
||
|
|
||
|
// Read the logo again (to verify it)
|
||
|
for (unsigned i = rom_offset(logo); i < rom_offset_end(logo); i++) {
|
||
|
if (rom_logo[i - rom_offset(logo)] != cart_read(i)) {
|
||
|
cart_set_standby();
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read and verify the checksum (and save some info from the header)
|
||
|
uint8_t checksum = rom_offset(header_checksum) - rom_offset(title);
|
||
|
for (unsigned i = rom_offset(title);
|
||
|
i < rom_offset_end(header_checksum); i++) {
|
||
|
uint8_t c = cart_read(i);
|
||
|
checksum += c;
|
||
|
|
||
|
if (i == rom_offset(type)) info->type = c;
|
||
|
if (i == rom_offset(rom_size)) info->rom_size = c;
|
||
|
if (i == rom_offset(ram_size)) info->ram_size = c;
|
||
|
}
|
||
|
|
||
|
cart_set_standby();
|
||
|
|
||
|
return checksum;
|
||
|
}
|
||
|
|
||
|
// https://ia801906.us.archive.org/19/items/GameBoyProgManVer1.1/GameBoyProgManVer1.1.pdf (page 215)
|
||
|
|
||
|
// NOTE: These functions only support MBC1, MBC2, MBC3 and MBC5.
|
||
|
// Thankfully, that covers most of the licensed carts.
|
||
|
|
||
|
void cart_rom_bank(__attribute__((unused)) struct cart_info *info, const unsigned bank)
|
||
|
{
|
||
|
// MBC1: 0x2000-0x3FFF (bit 0-4) 0x4000-0x5FFF (bit 5-6, if 0x6000-0x7FFF is set to 0)
|
||
|
// MBC2: 0x2100-0x21FF (bit 0-3)
|
||
|
// MBC3: 0x2000-0x3FFF (bit 0-6)
|
||
|
// MBC5: 0x2000-0x2FFF (bit 0-7) 0x3000-0x3FFF (bit 8)
|
||
|
cart_set_write();
|
||
|
|
||
|
if (info->type >= ROM_TYPE_MBC1 &&
|
||
|
info->type <= ROM_TYPE_MBC1_RAM_BATTERY) {
|
||
|
cart_write(0x2000, bank & 0x1F);
|
||
|
cart_write(0x6000, 0);
|
||
|
cart_write(0x4000, bank >> 5);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
cart_write(0x2100, bank);
|
||
|
if (info->type >= ROM_TYPE_MBC5 &&
|
||
|
info->type <= ROM_TYPE_MBC5_RUMBLE_RAM_BATTERY) {
|
||
|
cart_write(0x3000, bank >> 8);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void cart_ram_enable(__attribute__((unused)) struct cart_info *info)
|
||
|
{
|
||
|
// MBC1: 0x0000-0x1FFF = 0x0A
|
||
|
// MBC2: 0x0000-0x0FFF = 0x0A
|
||
|
// MBC3: 0x0000-0x1FFF = 0x0A
|
||
|
// MBC5: 0x0000-0x1FFF = 0x0A
|
||
|
cart_set_write();
|
||
|
cart_write(0x0000, 0x0A);
|
||
|
cart_select_ram();
|
||
|
}
|
||
|
|
||
|
void cart_ram_disable(__attribute__((unused)) struct cart_info *info)
|
||
|
{
|
||
|
// MBC1: 0x0000-0x1FFF = 0x00
|
||
|
// MBC2: 0x0000-0x0FFF = 0x00
|
||
|
// MBC3: 0x0000-0x1FFF = 0x00
|
||
|
// MBC5: 0x0000-0x1FFF = 0x00
|
||
|
cart_set_write();
|
||
|
cart_select_rom();
|
||
|
cart_write(0x0000, 0x00);
|
||
|
}
|
||
|
|
||
|
void cart_ram_bank(__attribute__((unused)) struct cart_info *info, const unsigned bank)
|
||
|
{
|
||
|
// MBC1: 0x4000-0x5FFF (bit 0-1, if 0x6000-0x7FFF is set to 1)
|
||
|
// MBC2: None, it has only one bank. (Writing to 0x4000-0x5FFF should have no effect)
|
||
|
// MBC3: 0x4000-0x5FFF (bit 0-1) NOTE: Writing values 0x08-0x0C opens up possible problems with the RTC!
|
||
|
// MBC5: 0x4000-0x5FFF (bit 0-3)
|
||
|
cart_set_write();
|
||
|
|
||
|
if (info->type >= ROM_TYPE_MBC1 &&
|
||
|
info->type <= ROM_TYPE_MBC1_RAM_BATTERY) {
|
||
|
cart_write(0x6000, 1);
|
||
|
}
|
||
|
|
||
|
// Since writing 0x08-0x0C here opens up the timer,
|
||
|
// it opens up the possibility for corruption.
|
||
|
// As such, it's a good idea to check for it...
|
||
|
if (info->type >= ROM_TYPE_MBC3_TIMER_BATTERY &&
|
||
|
info->type <= ROM_TYPE_MBC3_RAM_BATTERY) {
|
||
|
cart_write(0x4000, bank & 0x07);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
cart_write(0x4000, bank);
|
||
|
}
|