#ifndef LINK_CARD_H #define LINK_CARD_H // -------------------------------------------------------------------------- // A library to receive DLCs from a second GBA using the e-Reader. // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: // LinkCard* linkCard = new LinkCard(); // - 2) Probe the connected device: // auto device = getConnectedDevice(); // - 3) Send the DLC loader program: // if (device == LinkCard::ConnectedDevice::E_READER_USA) // LinkCard::SendResult result = linkCard->sendLoader(loader, []() { // u16 keys = ~REG_KEYS & KEY_ANY; // return keys & KEY_START; // })); // - 4) Receive scanned cards: // if (device == LinkCard::ConnectedDevice::DLC_LOADER) { // u8 card[LINK_CARD_SIZE]; // bool received = linkCard->receiveCard(card, []() { // u16 keys = ~REG_KEYS & KEY_ANY; // return keys & KEY_START; // })); // // if (received) { // // use `card` as DLC // } // } // -------------------------------------------------------------------------- #ifndef LINK_DEVELOPMENT #pragma GCC system_header #endif #include "LinkRawCable.hpp" #include "LinkSPI.hpp" #include "_link_common.hpp" LINK_VERSION_TAG LINK_CARD_VERSION = "vLinkCard/v8.0.3"; #define LINK_CARD_SIZE 1998 #define LINK_CARD_USE_SHUTDOWN_PROTOCOL 0 // ^^^ just for testing with official loaders, `DLC Loader` doesn't use it! /** * @brief A library to receive DLCs from a second GBA using the e-Reader. */ class LinkCard { private: using u32 = Link::u32; using u16 = Link::u16; using u8 = Link::u8; using vu32 = Link::vu32; static constexpr int MIN_LOADER_SIZE = 0x34 + 4; static constexpr int MAX_LOADER_SIZE = 0xEFFF + 1; static constexpr int HANDSHAKE_SEND = 0xFEFE; static constexpr int DEVICE_E_READER_USA = 0xCCC0; static constexpr int DEVICE_E_READER_JAP = 0xCCD0; static constexpr int DEVICE_LOADER = 0xFBFB; static constexpr int TRANSFER_SUCCESS = 0x1; static constexpr int HANDSHAKE_RECV_1 = 0xFBFB; static constexpr int HANDSHAKE_RECV_2 = 0x5841; static constexpr int HANDSHAKE_RECV_3 = 0x4534; static constexpr int GAME_ANIMATING = 0xF3F3; static constexpr int GAME_REQUEST = 0xECEC; static constexpr int GAME_READY = 0xEFEF; static constexpr int GAME_RECEIVE_READY = 0xFEFE; static constexpr int GAME_RECEIVE_OK = 0xF5F5; static constexpr int GAME_SIO_END = 0xF1F1; static constexpr int EREADER_ANIMATING = 0xF2F2; static constexpr int EREADER_READY = 0xF1F1; static constexpr int EREADER_SEND_READY = 0xF9F9; static constexpr int EREADER_SEND_START = 0xFDFD; static constexpr int EREADER_SEND_END = 0xFCFC; static constexpr int EREADER_SIO_END = 0xF3F3; static constexpr int EREADER_CANCEL = 0xF7F7; static constexpr int CMD_LINKCARD_RESET = 0; static constexpr int MODE_SWITCH_WAIT = 228; static constexpr int DEACTIVATION_WAIT = 50; static constexpr int PRE_TRANSFER_WAIT = 4; public: enum class ConnectedDevice { E_READER_USA, E_READER_JAP, DLC_LOADER, WRONG_CONNECTION, UNKNOWN_DEVICE }; enum class SendResult { SUCCESS, UNALIGNED, INVALID_SIZE, CANCELED, WRONG_DEVICE, FAILURE_DURING_TRANSFER }; enum class ReceiveResult { SUCCESS, CANCELED, WRONG_DEVICE, BAD_CHECKSUM, UNEXPECTED_FAILURE, SCAN_ERROR }; /** * Returns the connected device. * If it returns `E_READER_USA` or `E_READER_JAP`, you should send the loader * with `sendLoader(...)`. * If it returns `DLC_LOADER`, you should receive scanned cards with * `receiveCard(...)`. * \warning Blocks the system until completion. */ ConnectedDevice getConnectedDevice() { return getConnectedDevice([]() { return false; }); } /** * Returns the connected device. * If it returns `E_READER_USA` or `E_READER_JAP`, you should send the loader * with `sendLoader(...)`. * If it returns `DLC_LOADER`, you should receive scanned cards with * `receiveCard(...)`. * @param cancel A function that will be continuously invoked. If it returns * `true`, the transfer will be aborted. * \warning Blocks the system until completion or cancellation. */ template ConnectedDevice getConnectedDevice(F cancel) { linkRawCable.activate(); auto guard = Link::ScopeGuard([&]() { disableMulti(); }); if (linkRawCable.transfer(CMD_LINKCARD_RESET, cancel).playerId != 0) return ConnectedDevice::WRONG_CONNECTION; u16 remoteValues[3]; for (u32 i = 0; i < 3; i++) { remoteValues[i] = transferMulti(CMD_LINKCARD_RESET, cancel); if (i > 0 && remoteValues[i] != remoteValues[i - 1]) return ConnectedDevice::UNKNOWN_DEVICE; } return remoteValues[0] == DEVICE_E_READER_USA ? ConnectedDevice::E_READER_USA : remoteValues[0] == DEVICE_E_READER_JAP ? ConnectedDevice::E_READER_JAP : remoteValues[0] == DEVICE_LOADER ? ConnectedDevice::DLC_LOADER : ConnectedDevice::UNKNOWN_DEVICE; } /** * Sends the loader card and returns a `SendResult`. * @param loader A pointer to a e-Reader program that sends * the scanned cards to the game. Must be 4-byte aligned. * @param loaderSize Size of the loader program in bytes. Must be a multiple * of 32. * @param cancel A function that will be continuously invoked. If it returns * `true`, the transfer will be aborted. * \warning Blocks the system until completion or cancellation. */ template SendResult sendLoader(const u8* loader, u32 loaderSize, F cancel) { if ((u32)loader % 4 != 0) return SendResult::UNALIGNED; if (loaderSize < MIN_LOADER_SIZE || loaderSize > MAX_LOADER_SIZE || (loaderSize % 0x20) != 0) return SendResult::INVALID_SIZE; auto device = getConnectedDevice(cancel); if (device != ConnectedDevice::E_READER_USA && device != ConnectedDevice::E_READER_JAP) return SendResult::WRONG_DEVICE; auto deviceId = device == ConnectedDevice::E_READER_USA ? DEVICE_E_READER_USA : DEVICE_E_READER_JAP; // handshake { linkRawCable.activate(); auto guard = Link::ScopeGuard([&]() { disableMulti(); }); Link::wait(MODE_SWITCH_WAIT); if (cancel()) return SendResult::CANCELED; while (true) { if (cancel()) return SendResult::CANCELED; transferMulti(HANDSHAKE_SEND, cancel); if (transferMulti(HANDSHAKE_SEND, cancel) != deviceId) continue; if (transferMulti(deviceId, cancel) != deviceId) continue; break; } } // main transfer { linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS); auto guard = Link::ScopeGuard([&]() { disableNormal(); }); Link::wait(MODE_SWITCH_WAIT); if (cancel()) return SendResult::CANCELED; transferNormal(loaderSize, cancel); u32 checksum = 0; u32* dataOut = (u32*)loader; for (u32 i = 0; i < loaderSize / 4; i++) { u32 data = dataOut[i]; checksum += data; transferNormal(data, cancel); } transferNormal(0, cancel); transferNormal(checksum, cancel); transferNormal(checksum, cancel); } // confirmation { linkRawCable.activate(); auto guard = Link::ScopeGuard([&]() { disableMulti(); }); Link::wait(MODE_SWITCH_WAIT); if (cancel()) return SendResult::CANCELED; if (transferMulti(deviceId, cancel) != deviceId || transferMulti(deviceId, cancel) != TRANSFER_SUCCESS) return SendResult::FAILURE_DURING_TRANSFER; } return SendResult::SUCCESS; } /** * Receives a 1998-byte `card` from the DLC Loader and returns a * `ReceiveResult`. * @param card A pointer to fill the card bytes. * @param cancel A function that will be continuously invoked. If it returns * `true`, the transfer will be aborted. * \warning Blocks the system until completion or cancellation. */ template ReceiveResult receiveCard(u8* card, F cancel) { LINK_READ_TAG(LINK_CARD_VERSION); auto device = getConnectedDevice(cancel); if (device != ConnectedDevice::DLC_LOADER) return ReceiveResult::WRONG_DEVICE; linkRawCable.activate(); auto guard = Link::ScopeGuard([&]() { disableMulti(); }); // handshake if (!transferMultiAndExpect(HANDSHAKE_RECV_1, HANDSHAKE_RECV_1, cancel)) return ReceiveResult::CANCELED; if (!transferMultiAndExpect(HANDSHAKE_RECV_2, HANDSHAKE_RECV_2, cancel)) return ReceiveResult::CANCELED; if (!transferMultiAndExpect(HANDSHAKE_RECV_3, HANDSHAKE_RECV_3, cancel)) return ReceiveResult::CANCELED; // card request if (!transferMultiAndExpect(GAME_REQUEST, HANDSHAKE_RECV_3, cancel)) return ReceiveResult::CANCELED; if (!transferMultiAndExpectOneOf(EREADER_ANIMATING, GAME_ANIMATING, EREADER_READY, cancel)) return ReceiveResult::CANCELED; if (transferMultiAndExpectOneOf(EREADER_ANIMATING, EREADER_ANIMATING, EREADER_READY, cancel) < 0) return ReceiveResult::CANCELED; // wait for card while (true) { int received = 0; if ((received = transferMultiAndExpectOneOf( GAME_READY, EREADER_READY, EREADER_SEND_READY, cancel)) == -1) return ReceiveResult::CANCELED; if (received == -2) return ReceiveResult::SCAN_ERROR; if (received == EREADER_SEND_READY) break; } // start signal if (!transferMultiAndExpect(GAME_RECEIVE_READY, EREADER_SEND_READY, cancel)) return ReceiveResult::CANCELED; if (!transferMultiAndExpect(GAME_RECEIVE_READY, EREADER_SEND_START, cancel)) return ReceiveResult::CANCELED; // main transfer u32 checksum = 0; for (u32 i = 0; i < LINK_CARD_SIZE; i += 2) { if (cancel()) return ReceiveResult::CANCELED; u16 block = transferMulti(GAME_RECEIVE_READY, cancel); card[i] = Link::lsB16(block); card[i + 1] = Link::msB16(block); checksum += block; } // checksum if (transferMulti(GAME_RECEIVE_READY, cancel) != Link::lsB32(checksum)) return ReceiveResult::BAD_CHECKSUM; if (transferMulti(GAME_RECEIVE_READY, cancel) != Link::msB32(checksum)) return ReceiveResult::BAD_CHECKSUM; // end if (transferMulti(GAME_RECEIVE_READY, cancel) != EREADER_SEND_END) return ReceiveResult::UNEXPECTED_FAILURE; // shutdown protocol (not needed, just for testing with official loaders) #if LINK_CARD_USE_SHUTDOWN_PROTOCOL == 1 if (!transferMultiAndExpect(GAME_RECEIVE_OK, EREADER_SIO_END, cancel)) return ReceiveResult::CANCELED; transferMulti(GAME_SIO_END, cancel); #endif return ReceiveResult::SUCCESS; } private: LinkRawCable linkRawCable; LinkSPI linkSPI; template int transferMultiAndExpectOneOf(u16 value, u16 expected1, u16 expected2, F cancel) { u16 received; do { received = transferMulti(value, cancel); if (cancel()) return -1; if (received == EREADER_CANCEL) return -2; } while (received != expected1 && received != expected2); return received; } template bool transferMultiAndExpect(u16 value, u16 expected, F cancel) { u16 received; do { received = transferMulti(value, cancel); if (cancel() || received == EREADER_CANCEL) return false; } while (received != expected); return true; } template u16 transferMulti(u16 value, F cancel) { Link::wait(PRE_TRANSFER_WAIT); return linkRawCable.transfer(value, cancel).data[1]; } template void transferNormal(u32 value, F cancel) { Link::wait(PRE_TRANSFER_WAIT); linkSPI.transfer(value, cancel); } void disableMulti() { Link::wait(DEACTIVATION_WAIT); linkRawCable.deactivate(); } void disableNormal() { Link::wait(DEACTIVATION_WAIT); linkSPI.deactivate(); } }; extern LinkCard* linkCard; #endif // LINK_CARD_H