cartreader/Cart_Reader/LYNX.ino
2024-09-15 16:17:01 -07:00

504 lines
12 KiB
C++

//******************************************
// ATARI LYNX MODULE
//******************************************
//
// For use with SNES-Lynx adapter
// +----+
// | 1 |- GND
// | 2 |- D3
// | 3 |- D2
// | 4 |- D4
// | 5 |- D1
// | 6 |- D5
// | 7 |- D0
// | 8 |- D6
// | 9 |- D7
// | 10 |- /OE
// | 11 |- A1
// | 12 |- A2
// | 13 |- A3
// | 14 |- A6
// | 15 |- A4
// | 16 |- A5
// | 17 |- A0
// | 18 |- A7
// | 19 |- A16
// | 20 |- A17
// | 21 |- A18
// | 22 |- A19
// | 23 |- A15
// | 24 |- A14
// | 25 |- A13
// | 26 |- A12
// | 27 |- /WE
// | 28 |- A8
// | 29 |- A9
// | 30 |- A10
// | 31 |- VCC
// | 32 |- AUDIN
// | 33 |- VCC
// | 34 |- SWVCC
// +----+
//
// Version 1.2
// By @partlyhuman
// This implementation would not be possible without the invaluable
// documentation on
// https://atarilynxvault.com/
// by Igor (@theatarigamer) of K-Retro Gaming / Atari Lynx Vault
// and the reference implementation of the Lynx Cart Programmer Pi-Hat
// https://bitbucket.org/atarilynx/lynx/src/master/
// by Karri Kaksonen (whitelynx.fi) and Igor as well as countless contributions
// by the Atari Lynx community
//
#ifdef ENABLE_LYNX
#pragma region DEFS
#define LYNX_HEADER_SIZE 64
#define LYNX_WE 8
#define LYNX_OE 9
#define LYNX_AUDIN_ON 0x80000
#define LYNX_BLOCKADDR 2048UL
#define LYNX_BLOCKCOUNT 256
// Includes \0
static const char LYNX[5] = "LYNX";
// Cart information
static bool lynxUseAudin;
static uint16_t lynxBlockSize;
#pragma region LOWLEVEL
void setup_LYNX() {
setVoltage(VOLTS_SET_5V);
// Address pins output
// A0-7, A8-A16 (A11 doesn't exist)
DDRF = 0xff;
DDRK = 0xff;
DDRL = 0xff;
// Data pins input
DDRC = 0x00;
// Control pins output
// CE is tied low, not accessible
pinMode(LYNX_WE, OUTPUT);
pinMode(LYNX_OE, OUTPUT);
digitalWrite(LYNX_WE, HIGH);
digitalWrite(LYNX_OE, HIGH);
strcpy(romName, LYNX);
mode = CORE_LYNX;
}
static void dataDir_LYNX(byte direction) {
DDRC = (direction == OUTPUT) ? 0xff : 0x00;
}
static void setAddr_LYNX(uint32_t addr) {
PORTF = addr & 0xff;
PORTK = (addr >> 8) & 0xff;
// AUDIN connected to L3
PORTL = ((addr >> 16) & 0b1111);
}
static uint8_t readByte_LYNX(uint32_t addr) {
digitalWrite(LYNX_OE, HIGH);
setAddr_LYNX(addr);
digitalWrite(LYNX_OE, LOW);
delayMicroseconds(20);
uint8_t data = PINC;
digitalWrite(LYNX_OE, HIGH);
return data;
}
static bool waitPressButton_LYNX(bool ret = false) {
print_STR(press_button_STR, true);
display_Update();
wait();
return ret;
}
#pragma region HIGHLEVEL
static void compareStride_LYNX(uint8_t b, int i, int stride) {
uint8_t other = readByte_LYNX(i + stride);
if (other == 0xff || other == 0x00) {
// If this is NOR flash, these in-between spaces should be formatted to all 1's
// in which case, we DON'T report this as an unmirrored area
// Additionally, we have encountered commercial carts where the maskrom is larger than it needs to be
// and in this case, the maskrom will be initialized to 0s, not 1s.
return;
}
if (b != other) {
// if these bytes differ, they're likely in the same block, which means the block size contains both addresses (next POT beyond the stride)
lynxBlockSize = max(lynxBlockSize, stride << 1);
}
}
static bool detectCart_LYNX() {
// Could omit logging to save a few bytes
display_Clear();
println_Msg(F("Identifying..."));
lynxUseAudin = false;
lynxBlockSize = 0;
// Somewhat arbitrary, however many bytes would be unlikely to be
// coincidentally mirrored
const size_t DETECT_BYTES = 128;
for (int i = 0; i < DETECT_BYTES && lynxBlockSize < LYNX_BLOCKADDR; i++) {
uint8_t b = readByte_LYNX(i);
// If any differences are detected when AUDIN=1, AUDIN is used to bankswitch
// meaning we also use the maximum block size
// (1024kb cart / 256 blocks = 4kb block bank switched between lower/upper 2kb blocks)
if (b != readByte_LYNX(i + LYNX_AUDIN_ON)) {
lynxUseAudin = true;
lynxBlockSize = 2048;
break;
}
// Identify mirroring of largest stride
// Valid cart sizes of 128kb, 256kb, 512kb / 256 blocks = block sizes of 512b, 1024b, 2048b
compareStride_LYNX(b, i, 256);
compareStride_LYNX(b, i, 512);
compareStride_LYNX(b, i, 1024);
}
if (lynxBlockSize == 0) {
print_STR(error_STR, false);
display_Update();
wait();
resetArduino();
}
print_Msg(F("AUDIN="));
print_Msg(lynxUseAudin);
print_Msg(F(" BLOCK="));
println_Msg(lynxBlockSize);
display_Update();
}
static void writeHeader_LYNX() {
char header[LYNX_HEADER_SIZE] = {};
// Magic number
strcpy(header, LYNX);
// Cart name (dummy)
strcpy(header + 10, LYNX);
// Manufacturer (dummy)
strcpy(header + 42, LYNX);
// Version
header[8] = 1;
// Bank 0 page size
header[4] = lynxBlockSize & 0xff;
// Bank 1 page size
header[5] = (lynxBlockSize >> 8) & 0xff;
// AUDIN used
header[59] = lynxUseAudin;
// TODO detect EEPROM?
// header[60] = lynxUseEeprom;
myFile.write(header, LYNX_HEADER_SIZE);
}
static void readROM_LYNX() {
dataDir_LYNX(INPUT);
// The upper part of the address is used as a block address
// There are always 256 blocks, but the size of the block can vary
// So outer loop always steps through block addresses
const uint32_t upto = LYNX_BLOCKCOUNT * LYNX_BLOCKADDR;
for (uint32_t blockAddr = 0; blockAddr < upto; blockAddr += LYNX_BLOCKADDR) {
draw_progressbar(blockAddr, upto);
blinkLED();
for (uint32_t i = 0; i < lynxBlockSize; i++) {
uint32_t addr = blockAddr + i;
if (lynxUseAudin && i >= lynxBlockSize / 2) {
addr += LYNX_AUDIN_ON - lynxBlockSize / 2;
}
uint8_t byte = readByte_LYNX(addr);
sdBuffer[i % 512] = byte;
if ((i + 1) % 512 == 0) {
myFile.write(sdBuffer, 512);
}
}
}
draw_progressbar(upto, upto);
}
#pragma region FLASH
#ifdef ENABLE_FLASH
static void writeByte_LYNX(uint32_t addr, uint8_t data) {
digitalWrite(LYNX_OE, HIGH);
digitalWrite(LYNX_WE, HIGH);
setAddr_LYNX(addr);
PORTC = data;
digitalWrite(LYNX_WE, LOW);
delayMicroseconds(20);
digitalWrite(LYNX_WE, HIGH);
}
// Implements data complement status checking
// We only look at D7, or the highest bit of expected
void waitForDataComplement_LYNX(uint8_t expected) {
dataDir_LYNX(INPUT);
uint8_t status;
do {
digitalWrite(LYNX_OE, LOW);
// one nop = 62.5ns
// tOE = 30-50ns depending on flash
NOP;
status = PINC;
digitalWrite(LYNX_OE, HIGH);
// test highest bit
} while ((status ^ expected) >> 7);
dataDir_LYNX(OUTPUT);
}
static bool readHeader_LYNX() {
uint32_t romSize = fileSize;
print_Msg(F("Checking ROM..."));
display_Update();
char header[LYNX_HEADER_SIZE];
myFile.read(header, LYNX_HEADER_SIZE);
// Check for header to start with LYNX, assume valid .LNX header
if (strncmp(header, LYNX, 4) == 0) {
// Pull values from header
lynxBlockSize = (header[5] << 8) | header[4];
lynxUseAudin = header[59];
romSize = fileSize - LYNX_HEADER_SIZE;
println_Msg(FS(FSTRING_EMPTY));
} else {
// Header not valid, assume unheadered, rewind so we don't skip valid data
println_Msg(F("[NO HEADER]"));
myFile.seek(0);
// Get block size from file size
lynxBlockSize = fileSize / LYNX_BLOCKCOUNT;
lynxUseAudin = lynxBlockSize >= 2048;
romSize = fileSize;
}
print_Msg(F("AUDIN="));
print_Msg(lynxUseAudin);
print_Msg(F(" BLOCK="));
println_Msg(lynxBlockSize);
print_Msg(FS(FSTRING_ROM_SIZE));
print_Msg(romSize / 1024);
println_Msg(F("KB"));
// Ensure valid block size, file size, and file fits in flash
uint32_t expectedSize = (uint32_t)LYNX_BLOCKCOUNT * lynxBlockSize;
if (lynxBlockSize % 256 != 0 || lynxBlockSize > LYNX_BLOCKADDR || lynxBlockSize <= 0) {
println_Msg(FS(FSTRING_EMPTY));
print_STR(error_STR, false);
println_Msg(F("Invalid block size"));
return waitPressButton_LYNX();
}
if (romSize != expectedSize) {
println_Msg(FS(FSTRING_EMPTY));
print_STR(error_STR, false);
println_Msg(F("Invalid file size"));
return waitPressButton_LYNX();
}
if (expectedSize > flashSize) {
println_Msg(FS(FSTRING_EMPTY));
print_STR(error_STR, false);
print_STR(file_too_big_STR, true);
return waitPressButton_LYNX();
}
println_Msg(FS(FSTRING_OK));
display_Update();
return true;
}
static bool detectFlash_LYNX() {
print_Msg(F("Detecting flash..."));
// SOFTWARE ID PROGRAM
dataDir_LYNX(OUTPUT);
writeByte_LYNX(0x5555, 0xAA);
writeByte_LYNX(0x2AAA, 0x55);
writeByte_LYNX(0x5555, 0x90);
dataDir_LYNX(INPUT);
// tIDA = 150ns
NOP;
NOP;
NOP;
// MFG,DEVICE
uint16_t deviceId = (readByte_LYNX(0x0) << 8) | readByte_LYNX(0x1);
// EXIT SOFTWARE ID PROGRAM
dataDir_LYNX(OUTPUT);
writeByte_LYNX(0x5555, 0xAA);
writeByte_LYNX(0x2AAA, 0x55);
writeByte_LYNX(0x5555, 0xF0);
flashSize = 0;
switch (deviceId) {
case 0xBFB5:
// SST39SF010 = 1Mbit
flashSize = 131072UL;
break;
case 0xBFB6:
// SST39SF020 = 2Mbit
flashSize = 262144UL;
break;
case 0xBFB7:
// SST39SF040 = 4Mbit
flashSize = 524288UL;
break;
// case 0xC2A4:
// // MX29F040 = 4Mbit
// flashSize = 524288UL;
// break;
// case 0xC2D5:
// // MX29F080 = 8Mbit
// flashSize = 1048576UL;
// break;
}
if (flashSize <= 0) {
println_Msg(FS(FSTRING_EMPTY));
print_STR(error_STR, false);
println_Msg(F("Not recognized"));
return waitPressButton_LYNX();
}
print_Msg(flashSize / 1024);
println_Msg(F("KB"));
display_Update();
return true;
}
static void eraseFlash_LYNX() {
print_Msg(F("Erasing..."));
display_Update();
// CHIP ERASE PROGRAM
dataDir_LYNX(OUTPUT);
writeByte_LYNX(0x5555, 0xAA);
writeByte_LYNX(0x2AAA, 0x55);
writeByte_LYNX(0x5555, 0x80);
writeByte_LYNX(0x5555, 0xAA);
writeByte_LYNX(0x2AAA, 0x55);
writeByte_LYNX(0x5555, 0x10);
waitForDataComplement_LYNX(0xFF);
println_Msg(FS(FSTRING_OK));
display_Update();
}
static void writeROM_LYNX() {
filePath[0] = '\0';
sd.chdir("/");
fileBrowser(FS(FSTRING_SELECT_FILE));
display_Clear();
// HACK: openFlashFile() checks fileSize against flashSize for you, but this disregards header size:
// if size(rom) == size(flash) but size(rom + header) > size(flash) we'd get a false negative
// Pretend we have infinite flash size, allow openFlashFile() to pass this test, then do our own test in readHeader_LYNX()
flashSize = ULONG_MAX;
if (!openFlashFile()) return;
if (!detectFlash_LYNX()) return;
if (!readHeader_LYNX()) return;
// Pause to read debug info
// wait();
// or alternately auto-advance
delay(2000);
display_Clear();
eraseFlash_LYNX();
print_STR(flashing_file_STR, true);
display_Update();
dataDir_LYNX(OUTPUT);
uint8_t block[lynxBlockSize];
const uint32_t upto = LYNX_BLOCKCOUNT * LYNX_BLOCKADDR;
for (uint32_t blockAddr = 0; blockAddr < upto; blockAddr += LYNX_BLOCKADDR) {
draw_progressbar(blockAddr, upto);
blinkLED();
myFile.read(block, lynxBlockSize);
for (uint32_t i = 0; i < lynxBlockSize; i++) {
uint32_t addr = blockAddr + i;
if (lynxUseAudin && i >= lynxBlockSize / 2) {
addr += LYNX_AUDIN_ON - lynxBlockSize / 2;
}
// BYTE PROGRAM
uint8_t b = block[i];
writeByte_LYNX(0x5555, 0xAA);
writeByte_LYNX(0x2AAA, 0x55);
writeByte_LYNX(0x5555, 0xA0);
writeByte_LYNX(addr, b);
waitForDataComplement_LYNX(b);
}
}
draw_progressbar(upto, upto);
myFile.close();
dataDir_LYNX(INPUT);
print_STR(done_STR, true);
waitPressButton_LYNX();
}
#endif
#pragma region MENU
static const char PROGMEM LYNX_MENU_FLASH[] = "Program Flashcart";
static const char* const menuOptionsLYNX[] PROGMEM = { FSTRING_READ_ROM, LYNX_MENU_FLASH, FSTRING_RESET };
void lynxMenu() {
size_t menuCount = sizeof(menuOptionsLYNX) / sizeof(menuOptionsLYNX[0]);
convertPgm(menuOptionsLYNX, menuCount);
uint8_t mainMenu = question_box(F("LYNX MENU"), menuOptions, menuCount, 0);
display_Clear();
display_Update();
switch (mainMenu) {
case 0:
sd.chdir("/");
createFolderAndOpenFile(LYNX, "ROM", romName, "lnx");
detectCart_LYNX();
writeHeader_LYNX();
readROM_LYNX();
myFile.close();
sd.chdir("/");
compareCRC("lynx.txt", 0, true, LYNX_HEADER_SIZE);
print_STR(done_STR, true);
waitPressButton_LYNX();
break;
#ifdef ENABLE_FLASH
case 1:
writeROM_LYNX();
break;
#endif
case 2:
resetArduino();
break;
default:
print_MissingModule();
break;
}
}
#endif