gba-link-connection/lib/LinkSPI.hpp
2025-04-19 08:22:33 -03:00

371 lines
11 KiB
C++

#ifndef LINK_SPI_H
#define LINK_SPI_H
// --------------------------------------------------------------------------
// An SPI handler for the Link Port (Normal Mode, either 32 or 8 bits).
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
// LinkSPI* linkSPI = new LinkSPI();
// - 2) (Optional) Add the interrupt service routines: (*)
// interrupt_init();
// interrupt_add(INTR_SERIAL, LINK_SPI_ISR_SERIAL);
// // (this is only required for `transferAsync`)
// - 3) Initialize the library with:
// linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS);
// // (use LinkSPI::Mode::SLAVE on the other end)
// - 4) Exchange 32-bit data with the other end:
// u32 data = linkSPI->transfer(0x12345678);
// // (this blocks the console indefinitely)
// - 5) Exchange data with a cancellation callback:
// u32 data = linkSPI->transfer(0x12345678, []() {
// u16 keys = ~REG_KEYS & KEY_ANY;
// return keys & KEY_START;
// });
// - 6) Exchange data asynchronously:
// linkSPI->transferAsync(0x12345678);
// // ...
// if (linkSPI->getAsyncState() == LinkSPI::AsyncState::READY) {
// u32 data = linkSPI->getAsyncData();
// // ...
// }
// --------------------------------------------------------------------------
// (*) 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)
// --------------------------------------------------------------------------
// considerations:
// - when using Normal Mode between two GBAs, use a GBC Link Cable!
// - only use the 2Mbps mode with custom hardware (very short wires)!
// - returns 0xFFFFFFFF (or 0xFF) on misuse or cancelled transfers!
// --------------------------------------------------------------------------
#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif
#include "_link_common.hpp"
LINK_VERSION_TAG LINK_SPI_VERSION = "vLinkSPI/v8.0.3";
#define LINK_SPI_NO_DATA_32 0xFFFFFFFF
#define LINK_SPI_NO_DATA_8 0xFF
#define LINK_SPI_NO_DATA LINK_SPI_NO_DATA_32
/**
* @brief An SPI handler for the Link Port (Normal Mode, either 32 or 8 bits).
*/
class LinkSPI {
private:
using u32 = Link::u32;
using u16 = Link::u16;
using u8 = Link::u8;
using vu32 = Link::vu32;
static constexpr int BIT_CLOCK = 0;
static constexpr int BIT_CLOCK_SPEED = 1;
static constexpr int BIT_SI = 2;
static constexpr int BIT_SO = 3;
static constexpr int BIT_START = 7;
static constexpr int BIT_LENGTH = 12;
static constexpr int BIT_IRQ = 14;
static constexpr int BIT_GENERAL_PURPOSE_LOW = 14;
static constexpr int BIT_GENERAL_PURPOSE_HIGH = 15;
public:
enum class Mode { SLAVE, MASTER_256KBPS, MASTER_2MBPS };
enum class DataSize { SIZE_32BIT, SIZE_8BIT };
enum class AsyncState { IDLE, WAITING, READY };
/**
* @brief Returns whether the library is active or not.
*/
[[nodiscard]] bool isActive() { return isEnabled; }
/**
* @brief Activates the library in a specific `mode`.
* @param mode One of the enum values from `LinkSPI::Mode`.
* @param dataSize One of the enum values from `LinkSPI::DataSize`.
*/
void activate(Mode mode, DataSize dataSize = DataSize::SIZE_32BIT) {
LINK_READ_TAG(LINK_SPI_VERSION);
this->mode = mode;
this->dataSize = dataSize;
this->waitMode = false;
this->asyncState = AsyncState::IDLE;
this->asyncData = 0;
setNormalMode();
disableTransfer();
if (mode == Mode::SLAVE)
setSlaveMode();
else {
setMasterMode();
if (mode == Mode::MASTER_256KBPS)
set256KbpsSpeed();
else if (mode == Mode::MASTER_2MBPS)
set2MbpsSpeed();
}
isEnabled = true;
}
/**
* @brief Deactivates the library.
*/
void deactivate() {
isEnabled = false;
setGeneralPurposeMode();
mode = Mode::SLAVE;
waitMode = false;
asyncState = AsyncState::IDLE;
asyncData = 0;
}
/**
* @brief Exchanges `data` with the other end. Returns the received data.
* @param data The value to be sent.
* \warning Blocks the system until completion.
*/
u32 transfer(u32 data) {
return transfer(data, []() { return false; });
}
/**
* @brief Exchanges `data` with the other end. Returns the received data.
* @param data The value to be sent.
* @param cancel A function that will be continuously invoked. If it returns
* `true`, the transfer will be aborted and the response will be empty.
* \warning Blocks the system until completion or cancellation.
*/
template <typename F>
u32 transfer(u32 data,
F cancel,
bool _async = false,
bool _customAck = false) {
if ((!_customAck && !isEnabled) || asyncState != AsyncState::IDLE)
return noData();
setData(data);
if (_async) {
asyncState = AsyncState::WAITING;
setInterruptsOn();
} else {
setInterruptsOff();
}
while (isMaster() && waitMode && !isSlaveReady())
if (cancel()) {
disableTransfer();
setInterruptsOff();
asyncState = AsyncState::IDLE;
return noData();
}
enableTransfer();
startTransfer();
if (_async)
return noData();
while (!isReady())
if (cancel()) {
stopTransfer();
disableTransfer();
return noData();
}
if (!_customAck)
disableTransfer();
return getData();
}
/**
* @brief Schedules a `data` transfer and returns. After this, call
* `getAsyncState()` and `getAsyncData()`. Note that until you retrieve the
* async data, normal `transfer(...)`s won't do anything!
* @param data The value to be sent.
* \warning If `waitMode` (*) is active, blocks the system until completion.
* See `setWaitModeActive(...)`.
*/
void transferAsync(u32 data) {
transfer(data, []() { return false; }, true);
}
/**
* @brief Schedules a `data` transfer and returns. After this, call
* `getAsyncState()` and `getAsyncData()`. Note that until you retrieve the
* async data, normal `transfer(...)`s won't do anything!
* @param data The value to be sent.
* @param cancel A function that will be continuously invoked. If it returns
* `true`, the transfer will be aborted and the response will be empty.
* \warning If `waitMode` (*) is active, blocks the system until completion or
* cancellation. See `setWaitModeActive(...)`.
*/
template <typename F>
void transferAsync(u32 data, F cancel) {
transfer(data, cancel, true);
}
/**
* @brief Returns the state of the last async transfer.
* @return One of the enum values from `LinkSPI::AsyncState`.
*/
[[nodiscard]] AsyncState getAsyncState() { return asyncState; }
/**
* @brief If the async state is `READY`, returns the remote data and switches
* the state back to `IDLE`. If not, returns an empty response.
*/
[[nodiscard]] u32 getAsyncData() {
if (asyncState != AsyncState::READY)
return noData();
u32 data = asyncData;
asyncState = AsyncState::IDLE;
return data;
}
/**
* @brief Returns the current `mode`.
*/
[[nodiscard]] Mode getMode() { return mode; }
/**
* @brief Returns the current `dataSize`.
*/
[[nodiscard]] DataSize getDataSize() { return dataSize; }
/**
* @brief Enables or disables `waitMode`: The GBA adds an extra feature over
* SPI. When working as master, it can check whether the other terminal is
* ready to receive (ready: `MISO=LOW`), and wait if it's not (not ready:
* `MISO=HIGH`). That makes the connection more reliable, but it's not always
* supported on other hardware units (e.g. the Wireless Adapter), so it must
* be disabled in those cases.
* \warning `waitMode` is disabled by default.
* \warning `MISO` means `SO` on the slave side and `SI` on the master side.
*/
void setWaitModeActive(bool isActive) {
if (!isEnabled)
return;
waitMode = isActive;
}
/**
* @brief Returns whether `waitMode` (*) is active or not.
* \warning See `setWaitModeActive(...)`.
*/
[[nodiscard]] bool isWaitModeActive() { return waitMode; }
/**
* @brief This method is called by the SERIAL interrupt handler.
* \warning This is internal API!
*/
void _onSerial(bool _customAck = false) {
if (!isEnabled || asyncState != AsyncState::WAITING)
return;
if (!_customAck)
disableTransfer();
setInterruptsOff();
asyncState = AsyncState::READY;
asyncData = getData();
}
/**
* @brief Sets SO output to HIGH.
* \warning This is internal API!
*/
void _setSOHigh() { setBitHigh(BIT_SO); }
/**
* @brief Sets SO output to LOW.
* \warning This is internal API!
*/
void _setSOLow() { setBitLow(BIT_SO); }
/**
* @brief Returns whether SI is HIGH or LOW.
* \warning This is internal API!
*/
[[nodiscard]] bool _isSIHigh() { return isBitHigh(BIT_SI); }
private:
Mode mode = Mode::SLAVE;
DataSize dataSize = DataSize::SIZE_32BIT;
bool waitMode = false;
volatile AsyncState asyncState = AsyncState::IDLE;
vu32 asyncData = 0;
volatile bool isEnabled = false;
void setNormalMode() {
Link::_REG_RCNT = Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_HIGH);
if (dataSize == DataSize::SIZE_32BIT)
Link::_REG_SIOCNT = 1 << BIT_LENGTH;
else
Link::_REG_SIOCNT = 0;
}
void setGeneralPurposeMode() {
Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) |
(1 << BIT_GENERAL_PURPOSE_HIGH);
}
void setData(u32 data) {
if (dataSize == DataSize::SIZE_32BIT)
Link::_REG_SIODATA32 = data;
else
Link::_REG_SIODATA8 = data & 0xFF;
}
u32 getData() {
return dataSize == DataSize::SIZE_32BIT ? Link::_REG_SIODATA32
: Link::_REG_SIODATA8 & 0xFF;
}
u32 noData() {
return dataSize == DataSize::SIZE_32BIT ? LINK_SPI_NO_DATA_32
: LINK_SPI_NO_DATA_8;
}
void enableTransfer() { _setSOLow(); }
void disableTransfer() { _setSOHigh(); }
void startTransfer() { setBitHigh(BIT_START); }
void stopTransfer() { setBitLow(BIT_START); }
bool isReady() { return !isBitHigh(BIT_START); }
bool isSlaveReady() { return !_isSIHigh(); }
void setMasterMode() { setBitHigh(BIT_CLOCK); }
void setSlaveMode() { setBitLow(BIT_CLOCK); }
void set256KbpsSpeed() { setBitLow(BIT_CLOCK_SPEED); }
void set2MbpsSpeed() { setBitHigh(BIT_CLOCK_SPEED); }
void setInterruptsOn() { setBitHigh(BIT_IRQ); }
void setInterruptsOff() { setBitLow(BIT_IRQ); }
bool isMaster() { return mode != Mode::SLAVE; }
bool isBitHigh(u8 bit) { return (Link::_REG_SIOCNT >> bit) & 1; }
void setBitHigh(u8 bit) { Link::_REG_SIOCNT |= 1 << bit; }
void setBitLow(u8 bit) { Link::_REG_SIOCNT &= ~(1 << bit); }
};
extern LinkSPI* linkSPI;
/**
* @brief SERIAL interrupt handler.
*/
inline void LINK_SPI_ISR_SERIAL() {
linkSPI->_onSerial();
}
#endif // LINK_SPI_H