cartreader/Cart_Reader/SMS.ino
Andy Miles 644e1dc944
SMS Updates
1. SMS cart size detection algorithm is updated based on bank count instead of multiples of detected cart size from the header.
2. SMS cart size detection is only invoked if the "TMR SEGA" header is parsed for non-JP carts.
3. Manual selection of ROM size is now present in the SMS/GG menus to allow for manual override as detection algorithm does not work correctly for Cloud Master and Penguin Land US SMS carts.
4. SG-1000 Operation menu now allows for reset instead of relying on a power cycle.
2024-07-29 13:18:26 -07:00

759 lines
21 KiB
C++

//***********************************************************
// SEGA MASTER SYSTEM / MARK III / GAME GEAR / SG-1000 MODULE
//***********************************************************
#ifdef ENABLE_SMS
//******************************************
// Menus
//******************************************
// Adapters menu
static const char SMSAdapterItem1[] PROGMEM = "SMS/MarkIII raphnet";
static const char SMSAdapterItem2[] PROGMEM = "SMS Retrode";
static const char SMSAdapterItem3[] PROGMEM = "SMS Retron3in1";
static const char SMSAdapterItem4[] PROGMEM = "GameGear Retrode";
static const char SMSAdapterItem5[] PROGMEM = "GameGear Retron3in1";
static const char SMSAdapterItem6[] PROGMEM = "SG-1000 raphnet";
static const char* const SMSAdapterMenu[] PROGMEM = { SMSAdapterItem1, SMSAdapterItem2, SMSAdapterItem3, SMSAdapterItem4, SMSAdapterItem5, SMSAdapterItem6 };
// Operations menu
static const char SMSOperationMenuItem4[] PROGMEM = "Set ROM Size";
static const char* const SMSOperationMenu[] PROGMEM = { FSTRING_READ_ROM, FSTRING_READ_SAVE, FSTRING_WRITE_SAVE, SMSOperationMenuItem4, FSTRING_RESET };
// SG Operations menu
static const char* const SGOperationsMenu[] PROGMEM = { FSTRING_READ_ROM, FSTRING_RESET };
// Rom sizes menu
static const char SMSRomSizeItem1[] PROGMEM = "8 KB";
static const char SMSRomSizeItem2[] PROGMEM = "16 KB";
static const char SMSRomSizeItem3[] PROGMEM = "24 KB";
static const char SMSRomSizeItem4[] PROGMEM = "32 KB";
static const char SMSRomSizeItem5[] PROGMEM = "40 KB"; //SG-1000 40k mapping not yet supported
static const char SMSRomSizeItem6[] PROGMEM = "48 KB"; //SG-1000 40k mapping not yet supported
static const char SMSRomSizeItem7[] PROGMEM = "64 KB";
static const char SMSRomSizeItem8[] PROGMEM = "128 KB";
static const char SMSRomSizeItem9[] PROGMEM = "256 KB";
static const char SMSRomSizeItem10[] PROGMEM = "512 KB";
static const char SMSRomSizeItem11[] PROGMEM = "1024 KB";
static const char* const SG1RomSizeMenu[] PROGMEM = { SMSRomSizeItem1, SMSRomSizeItem2, SMSRomSizeItem3, SMSRomSizeItem4 }; // Rom sizes for SG-1000
static const char* const SMSRomSizeMenu[] PROGMEM = { SMSRomSizeItem4, SMSRomSizeItem7, SMSRomSizeItem8, SMSRomSizeItem9, SMSRomSizeItem10, SMSRomSizeItem11 }; // Rom sizes for SMS and GG
// Init systems
static bool system_sms = false; // SMS or MarkIII
static bool system_gg = false; // GameGear
static bool system_sg1000 = false; // SG-1000
// Init adapters
static bool adapter_raphnet = false; // raphet adapater (SMS-to-MD or MIII-to-MD)
static bool adapter_retrode = false; // Retrode adapter (SMS-to-MD or GG-to-MD)
static bool adapter_retron = false; // Retron 3in1 adapter (SMS-to-MD or GG-to-MD)
// Manual ROM Size Selection Flag
bool manRomSizeSelected = false;
//*********************************************************
// Main menu with systems/adapters setups to choose from
//*********************************************************
void smsMenu() {
unsigned char SMSSetup;
convertPgm(SMSAdapterMenu, 6);
SMSSetup = question_box(F("Select your setup"), menuOptions, 6, 0);
switch (SMSSetup) {
case 0:
// SMS or MarkIII with raphnet adapter
system_sms = true;
adapter_raphnet = true;
break;
case 1:
// SMS with Retrode adapter
system_sms = true;
adapter_retrode = true;
break;
case 2:
// SMS with Retron 3in1 adapter
system_sms = true;
adapter_retron = true;
break;
case 3:
// GameGear with Retrode adapter
system_gg = true;
adapter_retrode = true;
break;
case 4:
// GameGear with Retron 3in1 adapter
system_gg = true;
adapter_retron = true;
break;
case 5:
// SG-1000 with raphnet adapter
system_sg1000 = true;
adapter_raphnet = true;
break;
}
for (;;) operationsMenu();
}
//****************************************************
// Create custom menu depending on selected setup
//****************************************************
void operationsMenu() {
unsigned char SMSOperation = '3';
if (system_sg1000) {
convertPgm(SGOperationsMenu, 2);
SMSOperation = question_box(FS(SMSAdapterItem6), menuOptions, 2, 0);
switch (SMSOperation) {
case 0: // Read ROM
mode = CORE_SMS;
setup_SMS();
readROM_SMS();
break;
default:
case 1:
// Reset
resetArduino();
break;
}
} else {
convertPgm(SMSOperationMenu, 5);
if (system_sms) {
if (adapter_raphnet) {
SMSOperation = question_box(FS(SMSAdapterItem1), menuOptions, 5, 0);
} else if (adapter_retrode) {
SMSOperation = question_box(FS(SMSAdapterItem2), menuOptions, 5, 0);
} else if (adapter_retron) {
SMSOperation = question_box(FS(SMSAdapterItem3), menuOptions, 5, 0);
}
} else if (system_gg) {
if (adapter_retrode) {
SMSOperation = question_box(FS(SMSAdapterItem4), menuOptions, 5, 0);
} else if (adapter_retron) {
SMSOperation = question_box(FS(SMSAdapterItem5), menuOptions, 5, 0);
}
}
switch (SMSOperation) {
case 0: // Read ROM
mode = CORE_SMS;
setup_SMS();
readROM_SMS();
break;
case 1: // Read SRAM
mode = CORE_SMS;
setup_SMS();
readSRAM_SMS();
break;
case 2: // Write SRAM
mode = CORE_SMS;
setup_SMS();
writeSRAM_SMS();
break;
case 3: // Select ROM Size
manual_selectRomSize();
break;
default:
case 4:
// Reset
resetArduino();
break;
}
}
display_Update();
wait();
}
//********************************
// Setup I/O
//********************************
void setup_SMS() {
// Request 5V
setVoltage(VOLTS_SET_5V);
// Set Address Pins to Output
//A0-A7
DDRF = 0xFF;
//A8-A14
DDRK = 0xFF;
//A15
DDRH |= (1 << 3);
//For Retrode adapter
if (adapter_retrode) {
PORTH &= ~((1 << 0) | (1 << 3) | (1 << 5));
PORTL &= ~(1 << 1);
DDRH &= ~((1 << 0) | (1 << 5));
DDRL &= ~((1 << 1));
// Set Control Pins to Output OE(PH6)
DDRH |= (1 << 6);
// WR(PL5) and RD(PL6)
DDRL |= (1 << 5) | (1 << 6);
// Setting OE(PH6) HIGH
PORTH |= (1 << 6);
// Setting WR(PL5) and RD(PL6) HIGH
PORTL |= (1 << 5) | (1 << 6);
}
// For Raphnet and Retron adapters
else {
// Set Control Pins to Output RST(PH0) WR(PH5) OE(PH6)
DDRH |= (1 << 0) | (1 << 5) | (1 << 6);
// CE(PL1)
DDRL |= (1 << 1);
// Setting RST(PH0) WR(PH5) OE(PH6) HIGH
PORTH |= (1 << 0) | (1 << 5) | (1 << 6);
// CE(PL1)
PORTL |= (1 << 1);
}
if (!system_sg1000) {
// SMS/GG ROM has 16KB banks which can be mapped to one of three slots via register writes
// Register Slot Address space
// $fffd 0 $0000-$3fff
// $fffe 1 $4000-$7fff
// $ffff 2 $8000-$bfff
// Disable sram
writeByte_SMS(0xFFFC, 0);
// Map first 3 banks so we can read-out the header info
writeByte_SMS(0xFFFD, 0);
writeByte_SMS(0xFFFE, 1);
writeByte_SMS(0xFFFF, 2);
}
delay(400);
// Read and print cart info only if ROM size not manually selected
if (manRomSizeSelected == false) {
getCartInfo_SMS();
}
}
//*****************************************
// Low level functions
//*****************************************
void writeByte_SMS(word myAddress, byte myData) {
if (adapter_retrode && system_gg) {
// Set Data Pins (D8-D15) to Output
DDRA = 0xFF;
} else {
// Set Data Pins (D0-D7) to Output
DDRC = 0xFF;
}
// Set address
PORTF = myAddress & 0xFF;
PORTK = (myAddress >> 8) & 0xFF;
if (!adapter_retrode) {
// A15/M0-7(PH3) and OE(PH6) are connected
PORTH = (PORTH & 0b11110111) | ((myAddress >> 12) & 0b00001000);
}
// Output data
if (adapter_retrode && system_gg) {
PORTA = myData;
} else {
PORTC = myData;
}
// Arduino running at 16Mhz -> one nop = 62.5ns
// Wait till output is stable
__asm__("nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t");
if (adapter_retrode) {
// Switch WR(PL5) and OE/CE(PH6) to LOW
PORTL &= ~(1 << 5);
PORTH &= ~(1 << 6);
} else {
// Switch CE(PL1) and WR(PH5) to LOW
PORTL &= ~(1 << 1);
PORTH &= ~(1 << 5);
}
// Leave WR low for at least 60ns
__asm__("nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t");
if (adapter_retrode) {
// Switch WR(PL5) and OE/CE(PH6) to HIGH
PORTH |= (1 << 6);
PORTL |= (1 << 5);
} else {
// Switch CE(PL1) and WR(PH5) to HIGH
PORTH |= (1 << 5);
PORTL |= (1 << 1);
}
// Leave WR high for at least 50ns
__asm__("nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t");
if (adapter_retrode && system_gg) {
// Set Data Pins (D8-D15) to Input
DDRA = 0x00;
} else {
// Set Data Pins (D0-D7) to Input
DDRC = 0x00;
}
}
byte readByte_SMS(word myAddress) {
if (adapter_retrode && system_gg) {
// Set Data Pins (D8-D15) to Input
DDRA = 0x00;
} else {
// Set Data Pins (D0-D7) to Input
DDRC = 0x00;
}
// Set Address
PORTF = myAddress & 0xFF;
PORTK = (myAddress >> 8) & 0xFF;
if (!adapter_retrode) {
// A15/M0-7(PH3) and OE(PH6) are connected
PORTH = (PORTH & 0b11110111) | ((myAddress >> 12) & 0b00001000);
}
__asm__("nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t");
if (adapter_retrode) {
// Switch RD(PL6) and OE(PH6) to LOW
PORTL &= ~(1 << 6);
PORTH &= ~(1 << 6);
} else {
// Switch CE(PL1) and OE(PH6) to LOW
PORTL &= ~(1 << 1);
PORTH &= ~(1 << 6);
}
__asm__("nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t");
// Read
byte tempByte = (adapter_retrode && system_gg) ? PINA : PINC;
if (adapter_retrode) {
// Switch RD(PL6) and OE(PH6) to HIGH
PORTH |= (1 << 6);
PORTL |= (1 << 6);
} else {
// Switch CE(PL1) and OE(PH6) to HIGH
PORTH |= (1 << 6);
PORTL |= (1 << 1);
}
__asm__("nop\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t");
return tempByte;
}
byte readNibble(byte data, byte number) {
return ((data >> (number * 4)) & 0xF);
}
//*****************************************
// Cartridges functions
//*****************************************
void getCartInfo_SMS() {
byte cartNib = readNibble(readByte_SMS(0x7FFF), 0);
// Get rom size from header
// Note: Common for this value to be smaller than the actual value.
// Normally used for BIOS checksum calculations on export hardware (non JP).
switch (cartNib) {
case 0xa:
cartSize = 8192; // 8 KiB
break;
case 0xb:
cartSize = 16384; // 16 KiB
break;
case 0xc:
cartSize = 32768; // 32 KiB
break;
case 0xd:
cartSize = 49152; // 48 KiB
break;
case 0xe:
cartSize = 65536; // 64 KiB
break;
case 0xf:
cartSize = 131072; // 128 KiB
break;
case 0x0:
cartSize = 262144; // 256 KiB
break;
case 0x1:
case 0x2:
cartSize = 524288; // 512 KiB
break;
case 0x3:
// 0x3 is (only?) used in The Pro Yakyuu '91 (Game Gear)
cartSize = 131072; // 128 KiB
break;
default:
cartSize = 49152; // 48 KiB
// LED Error
rgbLed(blue_color);
break;
}
// Get rom name (expecting "TMR SEGA" string)
for (byte i = 0; i < 8; i++) {
romName[i] = char(readByte_SMS(0x7FF0 + i));
}
romName[8] = '\0';
// Attempt to detect cart size by checking if "TMR SEGA" is mirrored
// Based on https://www.raphnet.net/electronique/sms_cartridge_programmer/index_en.php#6
// Note: Logic does not work on US CloudMaster (256K) and Penquin Land (128K) SMS carts.
// Both detect as 512 KB based on the logic below.
if (strcmp(romName, "TMR SEGA") == 0) {
byte bank = 1;
char romNameBuf[9];
while (bank < 64) { // Total number of possible banks: 1 MiB / 16 (KiB/Bank)
bank++; // Increment the bank
writeByte_SMS(0xfffe, bank); // Load bank into slot 1
for (byte i = 0; i < 8; i++) {
romNameBuf[i] = char(readByte_SMS(0x7FF0 + i));
}
romNameBuf[8] = '\0';
// Debug info:
// print_Msg(F("Bank: "));
// println_Msg(bank);
// print_Msg(F("Parsed ROM name: "));
// println_Msg(romNameBuf);
// println_Msg(FS(FSTRING_EMPTY));
if (strcmp(romNameBuf, romName) == 0) {
break;
}
}
if (bank > 2) { // 32 KiB is the smallest SMS/GG ROM size
cartSize = (bank - 1) * 16384UL;
}
// Debug info:
// print_Msg(F("Calculated ROM Size: "));
// println_Msg(cartSize);
// Reset Bank Slot 1
writeByte_SMS(0xFFFE, 1);
// Display header info
display_Clear();
if (system_sms) {
println_Msg(F("SMS Header:"));
} else {
println_Msg(F("GG Header:"));
}
} else { // romName != "TMR SEGA"
// Fix for "Fantasy Zone (J) (V1.0)" that has not the normal header, but "COPYRIGHT SEGAPRG. BY T.ASAI".
char headerFZ[29];
if (strcmp(romName, "G. BY T.A") != 0) {
for (byte i = 0; i < 28; i++) {
headerFZ[i] = char(readByte_SMS(0x7FE0 + i));
}
headerFZ[28] = '\0';
if (strcmp(headerFZ, "COPYRIGHT SEGAPRG. BY T.ASAI") == 0) {
strcpy(romName, "TMR SEGA");
cartSize = 131072; // 128 KiB
}
}
manual_selectRomSize();
// Display cart info
display_Clear();
println_Msg(F("SMS/GG header not found"));
}
println_Msg(FS(FSTRING_SPACE));
print_Msg(FS(FSTRING_NAME));
println_Msg(romName);
print_Msg(cartSize / 1024);
println_Msg(F("KB"));
println_Msg(FS(FSTRING_SPACE));
// Wait for user input
#if (defined(ENABLE_LCD) || defined(ENABLE_OLED))
// Prints string out of the common strings array either with or without newline
print_STR(press_button_STR, 1);
display_Update();
wait();
#endif
// Turn off LED
rgbLed(black_color);
}
void manual_selectRomSize() {
// Set rom size manually
unsigned char SMSRomSize;
if (system_sg1000) {
// Rom sizes for SG-1000
convertPgm(SG1RomSizeMenu, 4);
SMSRomSize = question_box(F("Select ROM size"), menuOptions, 4, 0);
switch (SMSRomSize) {
case 0:
cartSize = 8192; // 8 KiB
break;
case 1:
cartSize = 16384; // 16 KiB
break;
case 2:
cartSize = 24576; // 24 KiB
break;
case 3:
cartSize = 32768; // 32 KiB
break;
//case 4:
// cartSize = 40960; // 40KB
// break;
//case 5:
// cartSize = 49152; // 48KB
// break;
}
} else {
// Rom sizes for SMS and GG
convertPgm(SMSRomSizeMenu, 6);
SMSRomSize = question_box(F("Select ROM size"), menuOptions, 6, 0);
switch (SMSRomSize) {
case 0:
cartSize = 32768; // 32 KiB
break;
case 1:
cartSize = 65536; // 64 KiB
break;
case 2:
cartSize = 131072; // 128 KiB
break;
case 3:
cartSize = 262144; // 256 KiB
break;
case 4:
cartSize = 524288; // 512 KiB
break;
case 5:
cartSize = 1048576; // 1 MiB
break;
}
}
strcpy(romName, "UNKNOWN"); // Use default UNKNOWN rom name upon manual selection
manRomSizeSelected = true; // This ensures manually selected value is used upon read from the menu
// Display info
display_Clear();
print_Msg(F("Selected Size: "));
print_Msg(cartSize / 1024);
println_Msg(F("KB"));
print_STR(press_button_STR, 1);
}
//******************************************
// Read ROM and save it to the SD card
//******************************************
void readROM_SMS() {
// Get name, add extension depending on the system and convert to char array for sd lib
if (system_sms) {
createFolder("SMS", "ROM", romName, "sms");
} else if (system_gg) {
createFolder("GG", "ROM", romName, "gg");
} else {
createFolder("SG1000", "ROM", romName, "sg");
}
printAndIncrementFolder(true);
print_Msg(F("ROM Size: "));
print_Msg(cartSize / 1024);
println_Msg(F("KB"));
// Open file on sd card
if (!myFile.open(fileName, O_RDWR | O_CREAT)) {
print_FatalError(sd_error_STR);
}
// Set default bank size to 16 KiB
word bankSize = 16384;
// For carts not using mappers (SG1000 or SMS/GG 32 KiB)
if ((system_sg1000) || (cartSize == 32768)) {
bankSize = cartSize;
}
// Initialize progress bar
uint32_t processedProgressBar = 0;
uint32_t totalProgressBar = (uint32_t)(cartSize);
draw_progressbar(0, totalProgressBar);
for (byte currBank = 0x0; currBank < (cartSize / bankSize); currBank++) {
// Write current 16KB bank to slot 2 register 0xFFFF
if (!system_sg1000) {
writeByte_SMS(0xFFFF, currBank);
}
// Blink led
blinkLED();
// Read 16KB from slot 2 which starts at 0x8000
for (word currBuffer = 0; currBuffer < bankSize; currBuffer += 512) {
// Fill SD buffer
for (int currByte = 0; currByte < 512; currByte++) {
sdBuffer[currByte] = readByte_SMS(((system_sg1000) || (cartSize == 32768) ? 0 : 0x8000) + currBuffer + currByte);
}
// hexdump for debugging:
// if (currBank == 0 && currBuffer == 0) {
// for (word xi = 0; xi < 0x100; xi++) {
// if (xi%16==0) {
// print_Msg_PaddedHex16(xi);
// print_Msg(FS(FSTRING_SPACE));
// }
// print_Msg_PaddedHexByte(sdBuffer[xi]);
// if (xi>0&&((xi+1)%16)==0) {
// println_Msg(FS(FSTRING_EMPTY));
// } else {
// print_Msg(FS(FSTRING_SPACE));
// }
// }
// }
myFile.write(sdBuffer, 512);
}
// Update progress bar
processedProgressBar += bankSize;
draw_progressbar(processedProgressBar, totalProgressBar);
}
// Close file
myFile.close();
// Compare dump checksum with database values
if (system_sms) {
compareCRC("sms.txt", 0, 1, 0);
} else if (system_gg) {
compareCRC("gg.txt", 0, 1, 0);
} else {
compareCRC("sg1000.txt", 0, 1, 0);
}
#ifdef ENABLE_GLOBAL_LOG
save_log();
#endif
print_STR(press_button_STR, 1);
}
//******************************************
// Read SRAM and save to the SD card
///*****************************************
void readSRAM_SMS() {
// Get name, add extension and convert to char array for sd lib
const char* system;
if (system_gg) {
system = "GG";
} else {
system = "SMS";
}
createFolderAndOpenFile(system, "SAVE", romName, "sav");
// Write the whole 32 KiB
// When there is only 8 KiB of SRAM, the contents should be duplicated
word bankSize = 16384;
for (byte currBank = 0x0; currBank < 2; currBank++) {
writeByte_SMS(0xFFFC, 0x08 | (currBank << 2));
// Blink led
blinkLED();
// Read 16 KiB from slot 2 which starts at 0x8000
for (word currBuffer = 0; currBuffer < bankSize; currBuffer += 512) {
// Fill SD buffer
for (int currByte = 0; currByte < 512; currByte++) {
sdBuffer[currByte] = readByte_SMS(0x8000 + currBuffer + currByte);
}
myFile.write(sdBuffer, 512);
}
}
// Close file
myFile.close();
print_STR(press_button_STR, 1);
display_Update();
}
//**********************************************
// Read file from SD card and write it to SRAM
//**********************************************
void writeSRAM_SMS() {
if (false) {
print_Error(F("DISABLED"));
} else {
fileBrowser(FS(FSTRING_SELECT_FILE));
sd.chdir();
sprintf(filePath, "%s/%s", filePath, fileName);
display_Clear();
println_Msg(F("Restoring from "));
println_Msg(filePath);
display_Update();
if (myFile.open(filePath, O_READ)) {
// Get SRAM size from file, with a maximum of 32KB
uint32_t sramSize = myFile.fileSize();
if (sramSize > ((uint32_t)32 * (uint32_t)1024)) {
sramSize = (uint32_t)32 * (uint32_t)1024;
}
print_Msg(F("sramSize: "));
print_Msg(sramSize);
println_Msg(FS(FSTRING_EMPTY));
word bankSize = 16384;
for (word address = 0x0; address < sramSize; address += 512) {
byte currBank = address >= bankSize ? 1 : 0;
word page_address = address - (currBank * bankSize);
writeByte_SMS(0xFFFC, 0x08 | (currBank << 2));
// Blink led
blinkLED();
myFile.read(sdBuffer, 512);
for (int x = 0; x < 512; x++) {
writeByte_SMS(0x8000 + page_address + x, sdBuffer[x]);
}
}
// Close file
myFile.close();
print_STR(press_button_STR, 1);
display_Update();
} else {
print_FatalError(sd_error_STR);
}
}
sd.chdir(); // root
filePath[0] = '\0'; // Reset filePath
}
#endif
//******************************************
// End of File
//******************************************