//****************************************** // 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