#ifndef LINK_WIRELESS_H #define LINK_WIRELESS_H // -------------------------------------------------------------------------- // A high level driver for the GBA Wireless Adapter. // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: // LinkWireless* linkWireless = new LinkWireless(); // - 2) Add the required interrupt service routines: (*) // irq_init(NULL); // irq_add(II_VBLANK, LINK_WIRELESS_ISR_VBLANK); // irq_add(II_SERIAL, LINK_WIRELESS_ISR_SERIAL); // irq_add(II_TIMER3, LINK_WIRELESS_ISR_TIMER); // - 3) Initialize the library with: // linkWireless->activate(); // - 4) Start a server: // linkWireless->serve(); // // // `getState()` should return SERVING now... // // `currentPlayerId()` should return 0 // // `playerCount()` should return the number of active consoles // - 5) Connect to a server: // LinkWireless::Server servers[LINK_WIRELESS_MAX_SERVERS]; // linkWireless->getServers(servers); // if (servers[0].id == LINK_WIRELESS_END) return; // // linkWireless->connect(servers[0].id); // while (linkWireless->getState() == LinkWireless::State::CONNECTING) // linkWireless->keepConnecting(); // // // `getState()` should return CONNECTED now... // // `currentPlayerId()` should return 1, 2, 3, or 4 (the host is 0) // // `playerCount()` should return the number of active consoles // - 6) Send data: // linkWireless->send(0x1234); // - 7) Receive data: // LinkWireless::Message messages[LINK_WIRELESS_QUEUE_SIZE]; // linkWireless->receive(messages); // if (messages[0].packetId != LINK_WIRELESS_END) { // // ... // } // - 8) Disconnect: // linkWireless->activate(); // // (resets the adapter) // -------------------------------------------------------------------------- // (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug. // That causes packet loss. You REALLY want to use libugba's instead. // (see examples) // -------------------------------------------------------------------------- // `send(...)` restrictions: // - 0xFFFF is a reserved value, so don't use it! // -------------------------------------------------------------------------- #ifndef LINK_DEVELOPMENT #pragma GCC system_header #endif #include "_link_common.hpp" #include #include "LinkGPIO.hpp" #include "LinkSPI.hpp" // #include // #include #ifndef LINK_WIRELESS_QUEUE_SIZE /** * @brief Buffer size (how many incoming and outgoing messages the queues can * store at max). The default value is `30`, which seems fine for most games. * \warning This affects how much memory is allocated. With the default value, * it's around `960` bytes. There's a double-buffered incoming queue and a * double-buffered outgoing queue (to avoid data races). * \warning You can approximate the usage with `LINK_WIRELESS_QUEUE_SIZE * 32`. */ #define LINK_WIRELESS_QUEUE_SIZE 30 #endif #ifndef LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH /** * @brief Max server transfer length per timer tick. Must be in the range * `[6;20]`. The default value is `20`, but you might want to set it a bit lower * to reduce CPU usage. */ #define LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH 20 #endif #ifndef LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH /** * @brief Max client transfer length per timer tick. Must be in the range * `[2;4]`. The default value is `4`. Changing this is not recommended, it's * already too low. */ #define LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4 #endif #ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM /** * @brief Put Interrupt Service Routines (ISR) in IWRAM (uncomment to enable). * This can significantly improve performance due to its faster access, but it's * disabled by default to conserve IWRAM space, which is limited. * \warning If you enable this, make sure that `LinkWireless.cpp` gets compiled! * For example, in a Makefile-based project, verify that the file is in your * `SRCDIRS` list. */ // #define LINK_WIRELESS_PUT_ISR_IN_IWRAM #endif #ifndef LINK_WIRELESS_ENABLE_NESTED_IRQ /** * @brief Allow LINK_WIRELESS_ISR_* functions to be interrupted (uncomment to * enable). * This can be useful, for example, if your audio engine requires calling a * VBlank handler with precise timing. * \warning This won't produce any effect if `LINK_WIRELESS_PUT_ISR_IN_IWRAM` is * disabled. */ // #define LINK_WIRELESS_ENABLE_NESTED_IRQ #endif #ifndef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH /** * @brief Use send/receive latch (uncomment to enable). * This makes it alternate between sends and receives on each timer tick * (instead of doing both things). Enabling it will introduce some latency but * also reduce overall CPU usage. */ // #define LINK_WIRELESS_USE_SEND_RECEIVE_LATCH #endif #ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY /** * @brief Optimize the library for two players (uncomment to enable). * This will make the code smaller and use less CPU. It will also let you * "misuse" 5 bits from the packet header to send small packets really fast * (e.g. pressed keys) without confirmation, using the `QUICK_SEND` and * `QUICK_RECEIVE` properties. */ // #define LINK_WIRELESS_TWO_PLAYERS_ONLY #endif static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v7.1.0"; #define LINK_WIRELESS_MAX_PLAYERS 5 #define LINK_WIRELESS_MIN_PLAYERS 2 #define LINK_WIRELESS_END 0 #define LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 22 #define LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30 #define LINK_WIRELESS_BROADCAST_LENGTH 6 #define LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH \ (1 + LINK_WIRELESS_BROADCAST_LENGTH) #define LINK_WIRELESS_MAX_SERVERS \ (LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / \ LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH) #define LINK_WIRELESS_MAX_GAME_ID 0x7fff #define LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14 #define LINK_WIRELESS_MAX_USER_NAME_LENGTH 8 #define LINK_WIRELESS_DEFAULT_TIMEOUT 10 #define LINK_WIRELESS_DEFAULT_INTERVAL 50 #define LINK_WIRELESS_DEFAULT_SEND_TIMER_ID 3 #define LINK_WIRELESS_BARRIER asm volatile("" ::: "memory") #define LINK_WIRELESS_CODE_IWRAM \ __attribute__((section(".iwram"), target("arm"), noinline)) #define LINK_WIRELESS_ALWAYS_INLINE inline __attribute__((always_inline)) #define LINK_WIRELESS_RESET_IF_NEEDED \ if (!isEnabled) \ return false; \ if (state == NEEDS_RESET) \ if (!reset()) \ return false; /** * @brief A high level driver for the GBA Wireless Adapter. */ class LinkWireless { private: using u32 = unsigned int; using u16 = unsigned short; using u8 = unsigned char; using vu32 = volatile unsigned int; using vs32 = volatile signed int; using s8 = signed char; static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024; #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY static constexpr int PACKET_ID_BITS = 5; #else static constexpr int PACKET_ID_BITS = 6; #endif static constexpr int MAX_PACKET_IDS = (1 << PACKET_ID_BITS); static constexpr int PACKET_ID_MASK = (MAX_PACKET_IDS - 1); static constexpr int MSG_PING = 0xffff; static constexpr int PING_WAIT = 50; static constexpr int TRANSFER_WAIT = 15; static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 60; static constexpr int CMD_TIMEOUT = 10; static constexpr int LOGIN_STEPS = 9; static constexpr int COMMAND_HEADER_VALUE = 0x9966; static constexpr int RESPONSE_ACK_VALUE = 0x80; static constexpr u32 DATA_REQUEST_VALUE = 0x80000000; static constexpr int SETUP_MAGIC = 0x003c0420; static constexpr int SETUP_MAX_PLAYERS_BIT = 16; static constexpr int WAIT_STILL_CONNECTING = 0x01000000; static constexpr int COMMAND_HELLO = 0x10; static constexpr int COMMAND_SETUP = 0x17; static constexpr int COMMAND_BROADCAST = 0x16; static constexpr int COMMAND_START_HOST = 0x19; static constexpr int COMMAND_ACCEPT_CONNECTIONS = 0x1a; static constexpr int COMMAND_END_HOST = 0x1b; static constexpr int COMMAND_BROADCAST_READ_START = 0x1c; static constexpr int COMMAND_BROADCAST_READ_POLL = 0x1d; static constexpr int COMMAND_BROADCAST_READ_END = 0x1e; static constexpr int COMMAND_CONNECT = 0x1f; static constexpr int COMMAND_IS_FINISHED_CONNECT = 0x20; static constexpr int COMMAND_FINISH_CONNECTION = 0x21; static constexpr int COMMAND_SEND_DATA = 0x24; static constexpr int COMMAND_RECEIVE_DATA = 0x26; static constexpr int COMMAND_BYE = 0x3d; static constexpr u16 LOGIN_PARTS[] = {0x494e, 0x494e, 0x544e, 0x544e, 0x4e45, 0x4e45, 0x4f44, 0x4f44, 0x8001}; public: #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY u32 QUICK_SEND = 0; u32 QUICK_RECEIVE = 0; #endif // std::function debug; // #define PROFILING_ENABLED #ifdef PROFILING_ENABLED u32 lastVBlankTime = 0; u32 lastSerialTime = 0; u32 lastTimerTime = 0; u32 lastFrameSerialIRQs = 0; u32 lastFrameTimerIRQs = 0; u32 serialIRQCount = 0; u32 timerIRQCount = 0; #endif enum State { NEEDS_RESET, AUTHENTICATED, SEARCHING, SERVING, CONNECTING, CONNECTED }; enum Error { // User errors NONE = 0, WRONG_STATE = 1, GAME_NAME_TOO_LONG = 2, USER_NAME_TOO_LONG = 3, BUFFER_IS_FULL = 4, // Communication errors COMMAND_FAILED = 5, CONNECTION_FAILED = 6, SEND_DATA_FAILED = 7, RECEIVE_DATA_FAILED = 8, ACKNOWLEDGE_FAILED = 9, TIMEOUT = 10, REMOTE_TIMEOUT = 11, BUSY_TRY_AGAIN = 12, }; struct Message { u32 packetId = 0; u16 data; u8 playerId = 0; }; struct Server { u16 id = 0; u16 gameId; char gameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1]; char userName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1]; u8 currentPlayerCount; bool isFull() { return currentPlayerCount == 0; } }; /** * @brief Constructs a new LinkWireless object. * @param forwarding If `true`, the server forwards all messages to the * clients. Otherwise, clients only see messages sent from the server * (ignoring other peers). * @param retransmission If `true`, the library handles retransmission for * you, so there should be no packet loss. * @param maxPlayers Maximum number of allowed players. If your game only * supports -for example- two players, set this to `2` as it will make * transfers faster. * @param timeout Number of *frames* without receiving *any* data to reset the * connection. * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers * *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values * will transfer faster but also consume more CPU. * @param sendTimerId GBA Timer to use for sending. * \warning You can use `Link::perFrame(...)` to convert from *packets per * frame* to *interval values*. */ explicit LinkWireless(bool forwarding = true, bool retransmission = true, u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS, u32 timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, u16 interval = LINK_WIRELESS_DEFAULT_INTERVAL, u8 sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID) { #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY maxPlayers = 2; #endif this->config.forwarding = forwarding; this->config.retransmission = retransmission; this->config.maxPlayers = maxPlayers; this->config.timeout = timeout; this->config.interval = interval; this->config.sendTimerId = sendTimerId; } /** * @brief Returns whether the library is active or not. */ [[nodiscard]] bool isActive() { return isEnabled; } /** * @brief Activates the library. When an adapter is connected, it changes the * state to `AUTHENTICATED`. It can also be used to disconnect or reset the * adapter. */ bool activate() { lastError = NONE; isEnabled = false; LINK_WIRELESS_BARRIER; bool success = reset(); LINK_WIRELESS_BARRIER; isEnabled = true; return success; } /** * @brief Puts the adapter into a low consumption mode and then deactivates * the library. It returns a boolean indicating whether the transition to low * consumption mode was successful. * @param turnOff Whether the library should put the adapter in the low * consumption mode or not before deactivation. Defaults to `true`. */ bool deactivate(bool turnOff = true) { bool success = true; if (turnOff) { activate(); success = sendCommand(COMMAND_BYE).success; } lastError = NONE; isEnabled = false; resetState(); stop(); return success; } /** * @brief Starts broadcasting a server and changes the state to `SERVING`. You * can optionally provide data that games will be able to read. If the adapter * is already serving, this method only updates the broadcast data. * @param gameName Game name. Maximum `14` characters + null terminator. * @param userName User name. Maximum `8` characters + null terminator. * @param gameId `(0 ~ 0x7FFF)` Game ID. * \warning Updating broadcast data while serving can fail if the adapter is * busy. In that case, this will return `false` and `getLastError()` will be * `BUSY_TRY_AGAIN`. */ bool serve(const char* gameName = "", const char* userName = "", u16 gameId = LINK_WIRELESS_MAX_GAME_ID) { LINK_WIRELESS_RESET_IF_NEEDED if (state != AUTHENTICATED && state != SERVING) { lastError = WRONG_STATE; return false; } if (std::strlen(gameName) > LINK_WIRELESS_MAX_GAME_NAME_LENGTH) { lastError = GAME_NAME_TOO_LONG; return false; } if (std::strlen(userName) > LINK_WIRELESS_MAX_USER_NAME_LENGTH) { lastError = USER_NAME_TOO_LONG; return false; } isSendingSyncCommand = true; if (asyncCommand.isActive) { lastError = BUSY_TRY_AGAIN; isSendingSyncCommand = false; return false; } char finalGameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1]; char finalUserName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1]; copyName(finalGameName, gameName, LINK_WIRELESS_MAX_GAME_NAME_LENGTH); copyName(finalUserName, userName, LINK_WIRELESS_MAX_USER_NAME_LENGTH); if (state != SERVING) setup(config.maxPlayers); addData(buildU32(buildU16(finalGameName[1], finalGameName[0]), gameId & LINK_WIRELESS_MAX_GAME_ID), true); addData(buildU32(buildU16(finalGameName[5], finalGameName[4]), buildU16(finalGameName[3], finalGameName[2]))); addData(buildU32(buildU16(finalGameName[9], finalGameName[8]), buildU16(finalGameName[7], finalGameName[6]))); addData(buildU32(buildU16(finalGameName[13], finalGameName[12]), buildU16(finalGameName[11], finalGameName[10]))); addData(buildU32(buildU16(finalUserName[3], finalUserName[2]), buildU16(finalUserName[1], finalUserName[0]))); addData(buildU32(buildU16(finalUserName[7], finalUserName[6]), buildU16(finalUserName[5], finalUserName[4]))); bool success = sendCommand(COMMAND_BROADCAST, true).success; if (state != SERVING) success = success && sendCommand(COMMAND_START_HOST).success; if (!success) { reset(); lastError = COMMAND_FAILED; return false; } wait(TRANSFER_WAIT); state = SERVING; return true; } /** * @brief Closes the server while keeping the session active, to prevent new * users from joining the room. * \warning Closing the server can fail if the adapter is busy. In that case, * this will return `false` and `getLastError()` will be `BUSY_TRY_AGAIN`. */ bool closeServer() { LINK_WIRELESS_RESET_IF_NEEDED if (state != SERVING || sessionState.serverClosed) { lastError = WRONG_STATE; return false; } isSendingSyncCommand = true; if (asyncCommand.isActive) { lastError = BUSY_TRY_AGAIN; isSendingSyncCommand = false; return false; } sessionState.serverClosed = true; sessionState.acceptCalled = true; bool success = sendCommand(COMMAND_END_HOST, false).success; if (!success) { reset(); lastError = COMMAND_FAILED; return false; } return true; } /** * @brief Fills the `servers` array with all the currently broadcasting * servers. * @param servers The array to be filled with data. * \warning This action takes 1 second to complete. * \warning For an async version, see `getServersAsyncStart()`. */ bool getServers(Server servers[]) { return getServers(servers, []() {}); } /** * @brief Fills the `servers` array with all the currently broadcasting * servers. * @param servers The array to be filled with data. * @param onWait A function which will be invoked each time VBlank starts. * \warning This action takes 1 second to complete. * \warning For an async version, see `getServersAsyncStart()`. */ template bool getServers(Server servers[], F onWait) { if (!getServersAsyncStart()) return false; waitVBlanks(BROADCAST_SEARCH_WAIT_FRAMES, onWait); if (!getServersAsyncEnd(servers)) return false; return true; } /** * @brief Starts looking for broadcasting servers and changes the state to * `SEARCHING`. After this, call `getServersAsyncEnd(...)` 1 second later. */ bool getServersAsyncStart() { LINK_WIRELESS_RESET_IF_NEEDED if (state != AUTHENTICATED) { lastError = WRONG_STATE; return false; } bool success = sendCommand(COMMAND_BROADCAST_READ_START).success; if (!success) { reset(); lastError = COMMAND_FAILED; return false; } state = SEARCHING; return true; } /** * @brief Fills the `servers` array with all the currently broadcasting * servers. Changes the state to `AUTHENTICATED` again. * @param servers The array to be filled with data. */ bool getServersAsyncEnd(Server servers[]) { LINK_WIRELESS_RESET_IF_NEEDED if (state != SEARCHING) { lastError = WRONG_STATE; return false; } auto result = sendCommand(COMMAND_BROADCAST_READ_POLL); bool success1 = result.success && result.responsesSize % LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH == 0; if (!success1) { reset(); lastError = COMMAND_FAILED; return false; } bool success2 = sendCommand(COMMAND_BROADCAST_READ_END).success; if (!success2) { reset(); lastError = COMMAND_FAILED; return false; } u32 totalBroadcasts = result.responsesSize / LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH; for (u32 i = 0; i < totalBroadcasts; i++) { u32 start = LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH * i; Server server; server.id = (u16)result.responses[start]; server.gameId = result.responses[start + 1] & LINK_WIRELESS_MAX_GAME_ID; u32 gameI = 0, userI = 0; recoverName(server.gameName, gameI, result.responses[start + 1], false); recoverName(server.gameName, gameI, result.responses[start + 2]); recoverName(server.gameName, gameI, result.responses[start + 3]); recoverName(server.gameName, gameI, result.responses[start + 4]); recoverName(server.userName, userI, result.responses[start + 5]); recoverName(server.userName, userI, result.responses[start + 6]); server.gameName[gameI] = '\0'; server.userName[userI] = '\0'; u8 connectedClients = (result.responses[start] >> 16) & 0xff; server.currentPlayerCount = connectedClients == 0xff ? 0 : (1 + connectedClients); servers[i] = server; } state = AUTHENTICATED; return true; } /** * @brief Starts a connection with `serverId` and changes the state to * `CONNECTING`. * @param serverId Device ID of the server. */ bool connect(u16 serverId) { LINK_WIRELESS_RESET_IF_NEEDED if (state != AUTHENTICATED) { lastError = WRONG_STATE; return false; } addData(serverId, true); bool success = sendCommand(COMMAND_CONNECT, true).success; if (!success) { reset(); lastError = COMMAND_FAILED; return false; } state = CONNECTING; return true; } /** * @brief When connecting, this needs to be called until the state is * `CONNECTED`. It assigns a player ID. Keep in mind that `isConnected()` and * `playerCount()` won't be updated until the first message from the server * arrives. */ bool keepConnecting() { LINK_WIRELESS_RESET_IF_NEEDED if (state != CONNECTING) { lastError = WRONG_STATE; return false; } auto result1 = sendCommand(COMMAND_IS_FINISHED_CONNECT); if (!result1.success || result1.responsesSize == 0) { reset(); lastError = COMMAND_FAILED; return false; } if (result1.responses[0] == WAIT_STILL_CONNECTING) return true; u8 assignedPlayerId = 1 + (u8)msB32(result1.responses[0]); if (assignedPlayerId >= LINK_WIRELESS_MAX_PLAYERS) { reset(); lastError = CONNECTION_FAILED; return false; } auto result2 = sendCommand(COMMAND_FINISH_CONNECTION); if (!result2.success) { reset(); lastError = COMMAND_FAILED; return false; } sessionState.currentPlayerId = assignedPlayerId; state = CONNECTED; return true; } /** * @brief Enqueues `data` to be sent to other nodes. * @param data The value to be sent. */ bool send(u16 data, int _author = -1) { LINK_WIRELESS_RESET_IF_NEEDED if (!isSessionActive()) { lastError = WRONG_STATE; return false; } if (!_canAddNewMessage()) { if (_author < 0) lastError = BUFFER_IS_FULL; return false; } Message message; message.playerId = _author >= 0 ? _author : sessionState.currentPlayerId; message.data = data; sessionState.newOutgoingMessages.syncPush(message); return true; } /** * @brief Fills the `messages` array with incoming messages, forwarding if * needed. * @param messages The array to be filled with data. */ bool receive(Message messages[]) { if (!isSessionActive()) return false; LINK_WIRELESS_BARRIER; sessionState.incomingMessages.startReading(); LINK_WIRELESS_BARRIER; u32 i = 0; while (!sessionState.incomingMessages.isEmpty()) { auto message = sessionState.incomingMessages.pop(); messages[i] = message; #ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY forwardMessageIfNeeded(message); #endif i++; } LINK_WIRELESS_BARRIER; sessionState.incomingMessages.stopReading(); LINK_WIRELESS_BARRIER; return true; } /** * @brief Returns the current state. * @return One of the enum values from `LinkWireless::State`. */ [[nodiscard]] State getState() { return state; } /** * @brief Returns `true` if the player count is higher than `1`. */ [[nodiscard]] bool isConnected() { return sessionState.playerCount > 1; } /** * @brief Returns `true` if the state is `SERVING` or `CONNECTED`. */ [[nodiscard]] bool isSessionActive() { return state == SERVING || state == CONNECTED; } /** * @brief Returns `true` if the server was closed with `closeServer()`. */ [[nodiscard]] bool isServerClosed() { return sessionState.serverClosed; } /** * @brief Returns the number of connected players. */ [[nodiscard]] u8 playerCount() { return sessionState.playerCount; } /** * @brief Returns the current player ID. */ [[nodiscard]] u8 currentPlayerId() { return sessionState.currentPlayerId; } /** * @brief If one of the other methods returns `false`, you can inspect this to * know the cause. After this call, the last error is cleared if `clear` is * `true` (default behavior). * @param clear Whether it should clear the error or not. */ Error getLastError(bool clear = true) { Error error = lastError; if (clear) lastError = NONE; return error; } /** * @brief Restarts the send timer without disconnecting. */ void resetTimer() { if (!isEnabled) return; stopTimer(); startTimer(); } ~LinkWireless() { delete linkSPI; delete linkGPIO; } /** * @brief Returns whether it's running an async command or not. * \warning This is internal API! */ [[nodiscard]] bool _hasActiveAsyncCommand() { return asyncCommand.isActive; } /** * @brief Returns whether there's room for sending messages or not. * \warning This is internal API! */ [[nodiscard]] bool _canSend() { return !sessionState.outgoingMessages.isFull(); } /** * @brief Returns whether there's room for scheduling new messages or not. * \warning This is internal API! */ [[nodiscard]] bool _canAddNewMessage() { return !sessionState.newOutgoingMessages.isFull(); } /** * @brief Returns the number of pending outgoing messages. * \warning This is internal API! */ [[nodiscard]] u32 _getPendingCount() { return sessionState.outgoingMessages.size(); } /** * @brief Returns the last packet ID. * \warning This is internal API! */ [[nodiscard]] u32 _lastPacketId() { return sessionState.lastPacketId; } /** * @brief Returns the last confirmation received from player ID 1. * \warning This is internal API! */ [[nodiscard]] u32 _lastConfirmationFromClient1() { return sessionState.lastConfirmationFromClients[1]; } /** * @brief Returns the last packet ID received from player ID 1. * \warning This is internal API! */ [[nodiscard]] u32 _lastPacketIdFromClient1() { return sessionState.lastPacketIdFromClients[1]; } /** * @brief Returns the last confirmation received from the server. * \warning This is internal API! */ [[nodiscard]] u32 _lastConfirmationFromServer() { return sessionState.lastConfirmationFromServer; } /** * @brief Returns the last packet ID received from the server. * \warning This is internal API! */ [[nodiscard]] u32 _lastPacketIdFromServer() { return sessionState.lastPacketIdFromServer; } /** * @brief Returns the next pending packet ID. * \warning This is internal API! */ [[nodiscard]] u32 _nextPendingPacketId() { return sessionState.outgoingMessages.isEmpty() ? 0 : sessionState.outgoingMessages.peek().packetId; } /** * @brief This method is called by the VBLANK interrupt handler. * \warning This is internal API! */ #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ __attribute__((noinline)) void _onVBlank() { #else void _onVBlank() { #endif if (!isEnabled) return; #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ if (interrupt) { pendingVBlank = true; return; } #endif #endif #ifdef PROFILING_ENABLED profileStart(); #endif if (!isSessionActive()) return; if (isConnected() && !sessionState.recvFlag) sessionState.recvTimeout++; if (sessionState.recvTimeout >= config.timeout) { reset(); lastError = TIMEOUT; return; } #ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY trackRemoteTimeouts(); if (!checkRemoteTimeouts()) { reset(); lastError = REMOTE_TIMEOUT; return; } #endif sessionState.recvFlag = false; if (!sessionState.serverClosed) sessionState.acceptCalled = false; sessionState.pingSent = false; #ifdef PROFILING_ENABLED lastVBlankTime = profileStop(); lastFrameSerialIRQs = serialIRQCount; lastFrameTimerIRQs = timerIRQCount; serialIRQCount = 0; timerIRQCount = 0; #endif } #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM void _onSerial(); void _onTimer(); #else void _onSerial() { __onSerial(); } void _onTimer() { __onTimer(); } #endif /** * @brief This method is called by the SERIAL interrupt handler. * \warning This is internal API! */ LINK_WIRELESS_ALWAYS_INLINE void __onSerial() { if (!isEnabled) return; #ifdef PROFILING_ENABLED profileStart(); #endif linkSPI->_onSerial(true); bool hasNewData = linkSPI->getAsyncState() == LinkSPI::AsyncState::READY; if (hasNewData) { if (!acknowledge()) { reset(); lastError = ACKNOWLEDGE_FAILED; return; } } else return; u32 newData = linkSPI->getAsyncData(); if (!isSessionActive()) return; if (asyncCommand.isActive) { if (asyncCommand.state == AsyncCommand::State::PENDING) { updateAsyncCommand(newData); if (asyncCommand.state == AsyncCommand::State::COMPLETED) processAsyncCommand(); } } #ifdef PROFILING_ENABLED lastSerialTime = profileStop(); serialIRQCount++; #endif } /** * @brief This method is called by the TIMER interrupt handler. * \warning This is internal API! */ LINK_WIRELESS_ALWAYS_INLINE void __onTimer() { if (!isEnabled) return; #ifdef PROFILING_ENABLED profileStart(); #endif if (!isSessionActive()) return; if (!asyncCommand.isActive) acceptConnectionsOrTransferData(); #ifdef PROFILING_ENABLED lastTimerTime = profileStop(); timerIRQCount++; #endif } struct Config { bool forwarding; bool retransmission; u8 maxPlayers; u32 timeout; // can be changed in realtime u16 interval; // can be changed in realtime, but call `resetTimer()` u8 sendTimerId; // can be changed in realtime, but call `resetTimer()` }; /** * @brief LinkWireless configuration. * \warning `deactivate()` first, change the config, and `activate()` again! */ Config config; private: using MessageQueue = Link::Queue; struct SessionState { MessageQueue incomingMessages; // read by user, write by irq&user MessageQueue outgoingMessages; // read and write by irq MessageQueue newIncomingMessages; // read and write by irq MessageQueue newOutgoingMessages; // read by irq, write by user&irq u32 recvTimeout = 0; // (~= LinkCable::IRQTimeout) u32 msgTimeouts[LINK_WIRELESS_MAX_PLAYERS]; // (~= LinkCable::msgTimeouts) bool recvFlag = false; // (~= LinkCable::IRQFlag) bool msgFlags[LINK_WIRELESS_MAX_PLAYERS]; // (~= LinkCable::msgFlags) bool acceptCalled = false; bool serverClosed = false; bool pingSent = false; #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH bool sendReceiveLatch = false; // true = send ; false = receive bool shouldWaitForServer = false; #endif u8 playerCount = 1; u8 currentPlayerId = 0; bool didReceiveLastPacketIdFromServer = false; u32 lastPacketId = 0; u32 lastPacketIdFromServer = 0; u32 lastConfirmationFromServer = 0; u32 lastPacketIdFromClients[LINK_WIRELESS_MAX_PLAYERS]; u32 lastConfirmationFromClients[LINK_WIRELESS_MAX_PLAYERS]; }; struct MessageHeader { unsigned int partialPacketId : PACKET_ID_BITS; unsigned int isConfirmation : 1; #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY unsigned int playerId : 1; unsigned int quickData : 5; #else unsigned int playerId : 3; unsigned int clientCount : 2; #endif unsigned int dataChecksum : 4; }; union MessageHeaderSerializer { MessageHeader asStruct; u16 asInt; }; struct LoginMemory { u16 previousGBAData = 0xffff; u16 previousAdapterData = 0xffff; }; struct CommandResult { bool success = false; u32 responses[LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH]; u32 responsesSize = 0; }; struct AsyncCommand { enum State { PENDING, COMPLETED }; enum Step { COMMAND_HEADER, COMMAND_PARAMETERS, RESPONSE_REQUEST, DATA_REQUEST }; u8 type; u32 parameters[LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; u32 responses[LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH]; CommandResult result; State state; Step step; u32 sentParameters, totalParameters; u32 receivedResponses, totalResponses; bool isActive; }; SessionState sessionState; AsyncCommand asyncCommand; LinkSPI* linkSPI = new LinkSPI(); LinkGPIO* linkGPIO = new LinkGPIO(); State state = NEEDS_RESET; u32 nextCommandData[LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; u32 nextCommandDataSize = 0; u32 nextAsyncCommandData[LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; u32 nextAsyncCommandDataSize = 0; volatile bool isSendingSyncCommand = false; Error lastError = NONE; volatile bool isEnabled = false; #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ volatile bool interrupt = false, pendingVBlank = false; #endif #endif #ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY void forwardMessageIfNeeded(Message& message) { if (state == SERVING && config.forwarding && sessionState.playerCount > 2) send(message.data, message.playerId); } #endif #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ void irqEnd() { Link::_REG_IME = 0; interrupt = false; LINK_WIRELESS_BARRIER; if (pendingVBlank) { _onVBlank(); pendingVBlank = false; } } #endif #endif void processAsyncCommand() { // (irq only) if (!asyncCommand.result.success) { if (asyncCommand.type == COMMAND_SEND_DATA) lastError = SEND_DATA_FAILED; else if (asyncCommand.type == COMMAND_RECEIVE_DATA) lastError = RECEIVE_DATA_FAILED; else lastError = COMMAND_FAILED; reset(); return; } asyncCommand.isActive = false; switch (asyncCommand.type) { case COMMAND_ACCEPT_CONNECTIONS: { // AcceptConnections (end) sessionState.playerCount = Link::_min( 1 + asyncCommand.result.responsesSize, config.maxPlayers); break; } case COMMAND_SEND_DATA: { // SendData (end) #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH if (state == CONNECTED) sessionState.shouldWaitForServer = true; sessionState.sendReceiveLatch = !sessionState.sendReceiveLatch; #else if (state == SERVING) { // ReceiveData (start) sendCommandAsync(COMMAND_RECEIVE_DATA); } #endif break; } case COMMAND_RECEIVE_DATA: { // ReceiveData (end) #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH sessionState.sendReceiveLatch = sessionState.shouldWaitForServer || !sessionState.sendReceiveLatch; #endif if (asyncCommand.result.responsesSize == 0) break; sessionState.recvFlag = true; sessionState.recvTimeout = 0; #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH sessionState.shouldWaitForServer = false; #endif addIncomingMessagesFromData(asyncCommand.result); #ifndef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH if (state == CONNECTED) { // SendData (start) sendPendingData(); } #endif break; } default: { } } } void acceptConnectionsOrTransferData() { // (irq only) if (state == SERVING && !sessionState.acceptCalled && sessionState.playerCount < config.maxPlayers) { // AcceptConnections (start) if (sendCommandAsync(COMMAND_ACCEPT_CONNECTIONS)) sessionState.acceptCalled = true; } else if (state == CONNECTED || isConnected()) { #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH bool shouldReceive = !sessionState.sendReceiveLatch || sessionState.shouldWaitForServer; #else bool shouldReceive = state == CONNECTED; #endif if (shouldReceive) { // ReceiveData (start) sendCommandAsync(COMMAND_RECEIVE_DATA); } else { // SendData (start) sendPendingData(); } } } void sendPendingData() { // (irq only) copyOutgoingState(); int lastPacketId = setDataFromOutgoingMessages(); if (sendCommandAsync(COMMAND_SEND_DATA, true)) clearOutgoingMessagesIfNeeded(lastPacketId); } int setDataFromOutgoingMessages() { // (irq only) u32 maxTransferLength = getDeviceTransferLength(); addAsyncData(0, true); if (config.retransmission) addConfirmations(); else addPingMessageIfNeeded(); int lastPacketId = -1; sessionState.outgoingMessages.forEach([this, maxTransferLength, &lastPacketId](Message message) { u16 header = buildMessageHeader(message.playerId, message.packetId, buildChecksum(message.data)); u32 rawMessage = buildU32(header, message.data); if (nextAsyncCommandDataSize /* -1 (wireless header) + 1 (rawMessage) */ > maxTransferLength) return false; addAsyncData(rawMessage); lastPacketId = message.packetId; return true; }); // (add wireless header) u32 bytes = (nextAsyncCommandDataSize - 1) * 4; nextAsyncCommandData[0] = sessionState.currentPlayerId == 0 ? bytes : bytes << (3 + sessionState.currentPlayerId * 5); return lastPacketId; } void addIncomingMessagesFromData(CommandResult& result) { // (irq only) for (u32 i = 1; i < result.responsesSize; i++) { u32 rawMessage = result.responses[i]; u16 headerInt = msB32(rawMessage); u16 data = lsB32(rawMessage); MessageHeaderSerializer serializer; serializer.asInt = headerInt; MessageHeader header = serializer.asStruct; u32 partialPacketId = header.partialPacketId; bool isConfirmation = header.isConfirmation; u8 remotePlayerId = Link::_min(header.playerId, config.maxPlayers - 1); #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY QUICK_RECEIVE = header.quickData; u8 remotePlayerCount = 2; #else u8 remotePlayerCount = LINK_WIRELESS_MIN_PLAYERS + header.clientCount; #endif u32 checksum = header.dataChecksum; bool isPing = data == MSG_PING; sessionState.msgTimeouts[0] = 0; sessionState.msgTimeouts[remotePlayerId] = 0; sessionState.msgFlags[0] = true; sessionState.msgFlags[remotePlayerId] = true; if (checksum != buildChecksum(data)) continue; Message message; message.packetId = partialPacketId; message.data = data; message.playerId = remotePlayerId; if (!acceptMessage(message, isConfirmation, remotePlayerCount) || isPing) continue; if (config.retransmission && isConfirmation) { if (!handleConfirmation(message)) continue; } else { sessionState.newIncomingMessages.push(message); } } copyIncomingState(); } bool acceptMessage(Message& message, bool isConfirmation, u32 remotePlayerCount) { // (irq only) if (state == SERVING) { u32 expectedPacketId = (sessionState.lastPacketIdFromClients[message.playerId] + 1) % MAX_PACKET_IDS; if (config.retransmission && !isConfirmation && message.packetId != expectedPacketId) return false; if (!isConfirmation) message.packetId = ++sessionState.lastPacketIdFromClients[message.playerId]; } else { u32 expectedPacketId = (sessionState.lastPacketIdFromServer + 1) % MAX_PACKET_IDS; if (config.retransmission && !isConfirmation && message.packetId != expectedPacketId) return false; sessionState.playerCount = remotePlayerCount; if (!isConfirmation) message.packetId = ++sessionState.lastPacketIdFromServer; } bool isMessageFromCurrentPlayer = !isConfirmation && message.playerId == sessionState.currentPlayerId; return !isMessageFromCurrentPlayer; } void clearOutgoingMessagesIfNeeded(int lastPacketId) { // (irq only) if (!config.retransmission && lastPacketId > -1) removeConfirmedMessages(lastPacketId); } void addPingMessageIfNeeded() { // (irq only) if (sessionState.outgoingMessages.isEmpty() && !sessionState.pingSent) { Message pingMessage; pingMessage.packetId = newPacketId(); pingMessage.playerId = sessionState.currentPlayerId; pingMessage.data = MSG_PING; sessionState.outgoingMessages.push(pingMessage); sessionState.pingSent = true; } } void addConfirmations() { // (irq only) if (state == SERVING) { #ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY if (config.maxPlayers > 2 && (sessionState.lastPacketIdFromClients[1] == 0 || sessionState.lastPacketIdFromClients[2] == 0 || sessionState.lastPacketIdFromClients[3] == 0 || sessionState.lastPacketIdFromClients[4] == 0)) { u32 lastPacketId = sessionState.lastPacketId; u16 header = buildConfirmationHeader(0, lastPacketId); u32 rawMessage = buildU32(header, lastPacketId & 0xffff); addAsyncData(rawMessage); } #endif for (int i = 0; i < config.maxPlayers - 1; i++) { u32 confirmationData = sessionState.lastPacketIdFromClients[1 + i]; u16 header = buildConfirmationHeader(1 + i, confirmationData); u32 rawMessage = buildU32(header, confirmationData & 0xffff); addAsyncData(rawMessage); } } else { u32 confirmationData = sessionState.lastPacketIdFromServer; u16 header = buildConfirmationHeader(sessionState.currentPlayerId, confirmationData); u32 rawMessage = buildU32(header, confirmationData & 0xffff); addAsyncData(rawMessage); } } bool handleConfirmation(Message confirmation) { // (irq only) u32 confirmationData = (confirmation.packetId << 16) | confirmation.data; if (state == CONNECTED) { if (confirmation.playerId == 0 && !sessionState.didReceiveLastPacketIdFromServer) { sessionState.lastPacketIdFromServer = confirmationData; sessionState.didReceiveLastPacketIdFromServer = true; } else if (confirmation.playerId == sessionState.currentPlayerId) { handleServerConfirmation(confirmationData); } else { return false; } } else { handleClientConfirmation(confirmationData, confirmation.playerId); } return true; } void handleServerConfirmation(u32 confirmationData) { // (irq only) sessionState.lastConfirmationFromServer = confirmationData; removeConfirmedMessages(confirmationData); } void handleClientConfirmation(u32 confirmationData, u8 playerId) { // (irq only) sessionState.lastConfirmationFromClients[playerId] = confirmationData; u32 min = 0xffffffff; for (int i = 0; i < config.maxPlayers - 1; i++) { u32 _confirmationData = sessionState.lastConfirmationFromClients[1 + i]; if (_confirmationData > 0 && _confirmationData < min) min = _confirmationData; } if (min < 0xffffffff) removeConfirmedMessages(min); } void removeConfirmedMessages(u32 confirmationData) { // (irq only) while (!sessionState.outgoingMessages.isEmpty() && sessionState.outgoingMessages.peek().packetId <= confirmationData) sessionState.outgoingMessages.pop(); } u16 buildConfirmationHeader(u8 playerId, u32 confirmationData) { // (irq only) // confirmation messages "repurpose" some message header fields: // packetId => high 6 bits of confirmation // data => low 16 bits of confirmation u8 highPart = (confirmationData >> 16) & PACKET_ID_MASK; u16 lowPart = confirmationData & 0xffff; return buildMessageHeader(playerId, highPart, buildChecksum(lowPart), true); } u16 buildMessageHeader(u8 playerId, u32 packetId, u8 dataChecksum, bool isConfirmation = false) { // (irq only) MessageHeader header; header.partialPacketId = packetId % MAX_PACKET_IDS; header.isConfirmation = isConfirmation; header.playerId = playerId; #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY header.quickData = QUICK_SEND; #else header.clientCount = sessionState.playerCount - LINK_WIRELESS_MIN_PLAYERS; #endif header.dataChecksum = dataChecksum; MessageHeaderSerializer serializer; serializer.asStruct = header; return serializer.asInt; } u32 buildChecksum(u16 data) { // (irq only) // (hamming weight) return __builtin_popcount(data) % 16; } #ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY void trackRemoteTimeouts() { // (irq only) for (u32 i = 0; i < sessionState.playerCount; i++) { if (i != sessionState.currentPlayerId && !sessionState.msgFlags[i]) sessionState.msgTimeouts[i]++; sessionState.msgFlags[i] = false; } } bool checkRemoteTimeouts() { // (irq only) for (u32 i = 0; i < sessionState.playerCount; i++) { if ((i == 0 || state == SERVING) && sessionState.msgTimeouts[i] > config.timeout) return false; } return true; } #endif u32 getDeviceTransferLength() { // (irq only) return state == SERVING ? LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH : LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH; } void copyOutgoingState() { // (irq only) if (sessionState.newOutgoingMessages.isWriting()) return; while (!sessionState.newOutgoingMessages.isEmpty()) { if (!_canSend()) break; auto message = sessionState.newOutgoingMessages.pop(); message.packetId = newPacketId(); sessionState.outgoingMessages.push(message); } } void copyIncomingState() { // (irq only) if (sessionState.incomingMessages.isReading()) return; while (!sessionState.newIncomingMessages.isEmpty()) { auto message = sessionState.newIncomingMessages.pop(); sessionState.incomingMessages.push(message); } } u32 newPacketId() { // (irq only) return ++sessionState.lastPacketId; } void addData(u32 value, bool start = false) { if (start) nextCommandDataSize = 0; nextCommandData[nextCommandDataSize] = value; nextCommandDataSize++; } void addAsyncData(u32 value, bool start = false) { if (start) nextAsyncCommandDataSize = 0; nextAsyncCommandData[nextAsyncCommandDataSize] = value; nextAsyncCommandDataSize++; } void copyName(char* target, const char* source, u32 length) { u32 len = std::strlen(source); for (u32 i = 0; i < length + 1; i++) if (i < len) target[i] = source[i]; else target[i] = '\0'; } void recoverName(char* name, u32& nameCursor, u32 word, bool includeFirstTwoBytes = true) { u32 character = 0; if (includeFirstTwoBytes) { character = lsB16(lsB32(word)); if (character > 0) name[nameCursor++] = character; character = msB16(lsB32(word)); if (character > 0) name[nameCursor++] = character; } character = lsB16(msB32(word)); if (character > 0) name[nameCursor++] = character; character = msB16(msB32(word)); if (character > 0) name[nameCursor++] = character; } bool reset() { bool wasEnabled = isEnabled; LINK_WIRELESS_BARRIER; isEnabled = false; LINK_WIRELESS_BARRIER; resetState(); stop(); bool success = start(); if (!success) stop(); LINK_WIRELESS_BARRIER; isEnabled = wasEnabled; LINK_WIRELESS_BARRIER; return success; } void resetState() { this->state = NEEDS_RESET; this->asyncCommand.isActive = false; #ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY QUICK_SEND = 0; QUICK_RECEIVE = 0; #endif this->sessionState.playerCount = 1; this->sessionState.currentPlayerId = 0; this->sessionState.recvFlag = false; this->sessionState.recvTimeout = 0; this->sessionState.acceptCalled = false; this->sessionState.serverClosed = false; this->sessionState.pingSent = false; #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH this->sessionState.sendReceiveLatch = false; this->sessionState.shouldWaitForServer = false; #endif this->sessionState.didReceiveLastPacketIdFromServer = false; this->sessionState.lastPacketId = 0; this->sessionState.lastPacketIdFromServer = 0; this->sessionState.lastConfirmationFromServer = 0; for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++) { this->sessionState.msgTimeouts[i] = 0; this->sessionState.msgFlags[i] = 0; this->sessionState.lastPacketIdFromClients[i] = 0; this->sessionState.lastConfirmationFromClients[i] = 0; } this->nextCommandDataSize = 0; this->nextAsyncCommandDataSize = 0; this->sessionState.incomingMessages.syncClear(); this->sessionState.outgoingMessages.clear(); this->sessionState.newIncomingMessages.clear(); this->sessionState.newOutgoingMessages.syncClear(); isSendingSyncCommand = false; } void stop() { stopTimer(); linkSPI->deactivate(); } bool start() { startTimer(); pingAdapter(); linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS); if (!login()) return false; wait(TRANSFER_WAIT); if (!sendCommand(COMMAND_HELLO).success) return false; if (!setup()) return false; linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); state = AUTHENTICATED; return true; } void stopTimer() { Link::_REG_TM[config.sendTimerId].cnt = Link::_REG_TM[config.sendTimerId].cnt & (~Link::_TM_ENABLE); } void startTimer() { Link::_REG_TM[config.sendTimerId].start = -config.interval; Link::_REG_TM[config.sendTimerId].cnt = Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY; } void pingAdapter() { linkGPIO->reset(); linkGPIO->setMode(LinkGPIO::Pin::SO, LinkGPIO::Direction::OUTPUT); linkGPIO->setMode(LinkGPIO::Pin::SD, LinkGPIO::Direction::OUTPUT); linkGPIO->writePin(LinkGPIO::Pin::SD, true); wait(PING_WAIT); linkGPIO->writePin(LinkGPIO::Pin::SD, false); } bool login() { LoginMemory memory; if (!exchangeLoginPacket(LOGIN_PARTS[0], 0, memory)) return false; for (u32 i = 0; i < LOGIN_STEPS; i++) { if (!exchangeLoginPacket(LOGIN_PARTS[i], LOGIN_PARTS[i], memory)) return false; } return true; } bool exchangeLoginPacket(u16 data, u16 expectedResponse, LoginMemory& memory) { u32 packet = buildU32(~memory.previousAdapterData, data); u32 response = transfer(packet, false); if (msB32(response) != expectedResponse || lsB32(response) != (u16)~memory.previousGBAData) return false; memory.previousGBAData = data; memory.previousAdapterData = expectedResponse; return true; } bool setup(u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS) { addData(SETUP_MAGIC | (((LINK_WIRELESS_MAX_PLAYERS - maxPlayers) & 0b11) << SETUP_MAX_PLAYERS_BIT), true); return sendCommand(COMMAND_SETUP, true).success; } CommandResult sendCommand(u8 type, bool withData = false) { CommandResult result; u32 command = buildCommand(type, withData ? (u16)nextCommandDataSize : 0); if (transfer(command) != DATA_REQUEST_VALUE) { isSendingSyncCommand = false; return result; } if (withData) { for (u32 i = 0; i < nextCommandDataSize; i++) { if (transfer(nextCommandData[i]) != DATA_REQUEST_VALUE) { isSendingSyncCommand = false; return result; } } } u32 response = transfer(DATA_REQUEST_VALUE); u16 header = msB32(response); u16 data = lsB32(response); u8 responses = msB16(data); u8 ack = lsB16(data); if (header != COMMAND_HEADER_VALUE || ack != type + RESPONSE_ACK_VALUE || responses > LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH) { isSendingSyncCommand = false; return result; } for (u32 i = 0; i < responses; i++) result.responses[i] = transfer(DATA_REQUEST_VALUE); result.responsesSize = responses; result.success = true; LINK_WIRELESS_BARRIER; isSendingSyncCommand = false; LINK_WIRELESS_BARRIER; return result; } bool sendCommandAsync(u8 type, bool withData = false) { // (irq only) if (asyncCommand.isActive || isSendingSyncCommand) return false; asyncCommand.type = type; if (withData) { for (u32 i = 0; i < nextAsyncCommandDataSize; i++) asyncCommand.parameters[i] = nextAsyncCommandData[i]; } asyncCommand.result.success = false; asyncCommand.state = AsyncCommand::State::PENDING; asyncCommand.step = AsyncCommand::Step::COMMAND_HEADER; asyncCommand.sentParameters = 0; asyncCommand.totalParameters = withData ? nextAsyncCommandDataSize : 0; asyncCommand.receivedResponses = 0; asyncCommand.totalResponses = 0; asyncCommand.isActive = true; u32 command = buildCommand(type, asyncCommand.totalParameters); transferAsync(command); return true; } void updateAsyncCommand(u32 newData) { // (irq only) switch (asyncCommand.step) { case AsyncCommand::Step::COMMAND_HEADER: { if (newData != DATA_REQUEST_VALUE) { asyncCommand.state = AsyncCommand::State::COMPLETED; return; } sendAsyncCommandParameterOrRequestResponse(); break; } case AsyncCommand::Step::COMMAND_PARAMETERS: { if (newData != DATA_REQUEST_VALUE) { asyncCommand.state = AsyncCommand::State::COMPLETED; return; } sendAsyncCommandParameterOrRequestResponse(); break; } case AsyncCommand::Step::RESPONSE_REQUEST: { u16 header = msB32(newData); u16 data = lsB32(newData); u8 responses = msB16(data); u8 ack = lsB16(data); if (header != COMMAND_HEADER_VALUE || ack != asyncCommand.type + RESPONSE_ACK_VALUE || responses > LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH) { asyncCommand.state = AsyncCommand::State::COMPLETED; return; } asyncCommand.totalResponses = responses; asyncCommand.result.responsesSize = responses; receiveAsyncCommandResponseOrFinish(); break; } case AsyncCommand::Step::DATA_REQUEST: { asyncCommand.result.responses[asyncCommand.receivedResponses] = newData; asyncCommand.receivedResponses++; receiveAsyncCommandResponseOrFinish(); break; } default: { } } } void sendAsyncCommandParameterOrRequestResponse() { // (irq only) if (asyncCommand.sentParameters < asyncCommand.totalParameters) { asyncCommand.step = AsyncCommand::Step::COMMAND_PARAMETERS; transferAsync(asyncCommand.parameters[asyncCommand.sentParameters]); asyncCommand.sentParameters++; } else { asyncCommand.step = AsyncCommand::Step::RESPONSE_REQUEST; transferAsync(DATA_REQUEST_VALUE); } } void receiveAsyncCommandResponseOrFinish() { // (irq only) if (asyncCommand.receivedResponses < asyncCommand.totalResponses) { asyncCommand.step = AsyncCommand::Step::DATA_REQUEST; transferAsync(DATA_REQUEST_VALUE); } else { asyncCommand.result.success = true; asyncCommand.state = AsyncCommand::State::COMPLETED; } } u32 buildCommand(u8 type, u8 length = 0) { return buildU32(COMMAND_HEADER_VALUE, buildU16(length, type)); } void transferAsync(u32 data) { // (irq only) #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ Link::_REG_IME = 0; #endif #endif linkSPI->transfer(data, []() { return false; }, true, true); } u32 transfer(u32 data, bool customAck = true) { if (!customAck) wait(TRANSFER_WAIT); u32 lines = 0; u32 vCount = Link::_REG_VCOUNT; u32 receivedData = linkSPI->transfer( data, [this, &lines, &vCount]() { return cmdTimeout(lines, vCount); }, false, customAck); if (customAck && !acknowledge()) return LINK_SPI_NO_DATA_32; return receivedData; } bool acknowledge() { u32 lines = 0; u32 vCount = Link::_REG_VCOUNT; linkSPI->_setSOLow(); while (!linkSPI->_isSIHigh()) if (cmdTimeout(lines, vCount)) return false; linkSPI->_setSOHigh(); while (linkSPI->_isSIHigh()) if (cmdTimeout(lines, vCount)) return false; linkSPI->_setSOLow(); return true; } bool cmdTimeout(u32& lines, u32& vCount) { return timeout(CMD_TIMEOUT, lines, vCount); } bool timeout(u32 limit, u32& lines, u32& vCount) { if (Link::_REG_VCOUNT != vCount) { lines += Link::_max((int)Link::_REG_VCOUNT - (int)vCount, 0); vCount = Link::_REG_VCOUNT; } return lines > limit; } void wait(u32 verticalLines) { u32 count = 0; u32 vCount = Link::_REG_VCOUNT; while (count < verticalLines) { if (Link::_REG_VCOUNT != vCount) { count++; vCount = Link::_REG_VCOUNT; } }; } template void waitVBlanks(u32 vBlanks, F onVBlank) { u32 count = 0; u32 vCount = Link::_REG_VCOUNT; while (count < vBlanks) { if (Link::_REG_VCOUNT != vCount) { vCount = Link::_REG_VCOUNT; if (vCount == 160) { onVBlank(); count++; } } }; } u32 buildU32(u16 msB, u16 lsB) { return (msB << 16) | lsB; } u16 buildU16(u8 msB, u8 lsB) { return (msB << 8) | lsB; } u16 msB32(u32 value) { return value >> 16; } u16 lsB32(u32 value) { return value & 0xffff; } u8 msB16(u16 value) { return value >> 8; } u8 lsB16(u16 value) { return value & 0xff; } #ifdef PROFILING_ENABLED void profileStart() { Link::_REG_TM1CNT_L = 0; Link::_REG_TM2CNT_L = 0; Link::_REG_TM1CNT_H = 0; Link::_REG_TM2CNT_H = 0; Link::_REG_TM2CNT_H = Link::_TM_ENABLE | Link::_TM_CASCADE; Link::_REG_TM1CNT_H = Link::_TM_ENABLE | Link::_TM_FREQ_1; } u32 profileStop() { Link::_REG_TM1CNT_H = 0; Link::_REG_TM2CNT_H = 0; return (Link::_REG_TM1CNT_L | (Link::_REG_TM2CNT_L << 16)); } public: u32 toMs(u32 cycles) { // CPU Frequency * time per frame = cycles per frame // 16780000 * (1/60) ~= 279666 return (cycles * 1000) / (279666 * 60); } #endif }; extern LinkWireless* linkWireless; /** * @brief VBLANK interrupt handler. */ inline void LINK_WIRELESS_ISR_VBLANK() { linkWireless->_onVBlank(); } /** * @brief SERIAL interrupt handler. */ inline void LINK_WIRELESS_ISR_SERIAL() { linkWireless->_onSerial(); } /** * @brief TIMER interrupt handler. */ inline void LINK_WIRELESS_ISR_TIMER() { linkWireless->_onTimer(); } #endif // LINK_WIRELESS_H