mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-03-21 17:44:21 -05:00
306 lines
7.9 KiB
C++
306 lines
7.9 KiB
C++
#ifndef LINK_CUBE_H
|
|
#define LINK_CUBE_H
|
|
|
|
// --------------------------------------------------------------------------
|
|
// A JOYBUS handler for the Link Port.
|
|
// --------------------------------------------------------------------------
|
|
// Usage:
|
|
// - 1) Include this header in your main.cpp file and add:
|
|
// LinkCube* linkCube = new LinkCube();
|
|
// - 2) Add the required interrupt service routines: (*)
|
|
// interrupt_init();
|
|
// interrupt_add(INTR_SERIAL, LINK_CUBE_ISR_SERIAL);
|
|
// - 3) Initialize the library with:
|
|
// linkCube->activate();
|
|
// - 4) Send 32-bit values:
|
|
// linkCube->send(0x12345678);
|
|
// // (now linkCube->pendingCount() will be 1 until the value is sent)
|
|
// - 5) Read 32-bit values:
|
|
// if (linkCube->canRead()) {
|
|
// u32 value = linkCube->read();
|
|
// // ...
|
|
// }
|
|
// --------------------------------------------------------------------------
|
|
// (*) 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)
|
|
// --------------------------------------------------------------------------
|
|
|
|
#ifndef LINK_DEVELOPMENT
|
|
#pragma GCC system_header
|
|
#endif
|
|
|
|
#include "_link_common.hpp"
|
|
|
|
#ifndef LINK_CUBE_QUEUE_SIZE
|
|
/**
|
|
* @brief Buffer size (how many incoming and outgoing values the queues can
|
|
* store at max). The default value is `10`, which seems fine for most games.
|
|
* \warning This affects how much memory is allocated. With the default value,
|
|
* it's around `120` bytes. There's a double-buffered pending queue (to avoid
|
|
* data races), and 1 outgoing queue.
|
|
* \warning You can approximate the usage with `LINK_CUBE_QUEUE_SIZE * 12`.
|
|
*/
|
|
#define LINK_CUBE_QUEUE_SIZE 10
|
|
#endif
|
|
|
|
LINK_VERSION_TAG LINK_CUBE_VERSION = "vLinkCube/v8.0.3";
|
|
|
|
/**
|
|
* @brief A JOYBUS handler for the Link Port.
|
|
*/
|
|
class LinkCube {
|
|
private:
|
|
using u32 = Link::u32;
|
|
using u16 = Link::u16;
|
|
using u8 = Link::u8;
|
|
using U32Queue = Link::Queue<u32, LINK_CUBE_QUEUE_SIZE>;
|
|
|
|
static constexpr int BIT_CMD_RESET = 0;
|
|
static constexpr int BIT_CMD_RECEIVE = 1;
|
|
static constexpr int BIT_CMD_SEND = 2;
|
|
static constexpr int BIT_IRQ = 6;
|
|
static constexpr int BIT_JOYBUS_HIGH = 14;
|
|
static constexpr int BIT_GENERAL_PURPOSE_LOW = 14;
|
|
static constexpr int BIT_GENERAL_PURPOSE_HIGH = 15;
|
|
|
|
public:
|
|
/**
|
|
* @brief Returns whether the library is active or not.
|
|
*/
|
|
[[nodiscard]] bool isActive() { return isEnabled; }
|
|
|
|
/**
|
|
* @brief Activates the library.
|
|
*/
|
|
void activate() {
|
|
LINK_READ_TAG(LINK_CUBE_VERSION);
|
|
static_assert(LINK_CUBE_QUEUE_SIZE >= 1);
|
|
|
|
LINK_BARRIER;
|
|
isEnabled = false;
|
|
LINK_BARRIER;
|
|
|
|
resetState();
|
|
stop();
|
|
|
|
LINK_BARRIER;
|
|
isEnabled = true;
|
|
LINK_BARRIER;
|
|
|
|
start();
|
|
}
|
|
|
|
/**
|
|
* @brief Deactivates the library.
|
|
*/
|
|
void deactivate() {
|
|
isEnabled = false;
|
|
resetState();
|
|
stop();
|
|
}
|
|
|
|
/**
|
|
* @brief Waits for data. Returns `true` on success, or `false` on
|
|
* JOYBUS reset.
|
|
*/
|
|
bool wait() {
|
|
return wait([]() { return false; });
|
|
}
|
|
|
|
/**
|
|
* @brief Waits for data. Returns `true` on success, or `false` on
|
|
* JOYBUS reset or cancellation.
|
|
* @param cancel A function that will be invoked after every SERIAL interrupt.
|
|
* If it returns `true`, the wait be aborted.
|
|
* \warning Blocks the system until the next SERIAL interrupt!
|
|
*/
|
|
template <typename F>
|
|
bool wait(F cancel) {
|
|
if (!isEnabled)
|
|
return false;
|
|
|
|
resetFlag = false;
|
|
|
|
while (!resetFlag && !canRead() && !cancel())
|
|
Link::_IntrWait(1, Link::_IRQ_SERIAL);
|
|
|
|
return canRead();
|
|
}
|
|
|
|
/**
|
|
* @brief Returns `true` if there are pending received values to read.
|
|
*/
|
|
[[nodiscard]] bool canRead() { return !incomingQueue.isEmpty(); }
|
|
|
|
/**
|
|
* @brief Dequeues and returns the next received value.
|
|
* \warning If there's no received data, a `0` will be returned.
|
|
*/
|
|
u32 read() { return incomingQueue.syncPop(); }
|
|
|
|
/**
|
|
* @brief Returns the next received value without dequeuing it.
|
|
* \warning If there's no received data, a `0` will be returned.
|
|
*/
|
|
[[nodiscard]] u32 peek() { return incomingQueue.peek(); }
|
|
|
|
/**
|
|
* @brief Sends 32-bit `data`.
|
|
* @param data The value to be sent.
|
|
* \warning If the other end asks for data at the same time you call this
|
|
* method, a `0x00000000` will be sent.
|
|
*/
|
|
void send(u32 data) {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
outgoingQueue.syncPush(data);
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the number of pending outgoing transfers.
|
|
*/
|
|
[[nodiscard]] u32 pendingCount() { return outgoingQueue.size(); }
|
|
|
|
/**
|
|
* @brief Returns whether the internal queue lost messages at some point due
|
|
* to being full. This can happen if your queue size is too low, if you
|
|
* receive too much data without calling `read(...)` enough times, or if
|
|
* excessive `read(...)` calls prevent the ISR from copying data. After this
|
|
* call, the overflow flag is cleared if `clear` is `true` (default behavior).
|
|
*/
|
|
bool didQueueOverflow(bool clear = true) {
|
|
bool overflow = newIncomingQueue.overflow;
|
|
if (clear)
|
|
newIncomingQueue.overflow = false;
|
|
return overflow;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns whether a JOYBUS reset was requested or not. After this
|
|
* call, the reset flag is cleared if `clear` is `true` (default behavior).
|
|
* @param clear Whether it should clear the reset flag or not.
|
|
*/
|
|
bool didReset(bool clear = true) {
|
|
bool reset = resetFlag;
|
|
if (clear)
|
|
resetFlag = false;
|
|
return reset;
|
|
}
|
|
|
|
/**
|
|
* @brief This method is called by the SERIAL interrupt handler.
|
|
* \warning This is internal API!
|
|
*/
|
|
void _onSerial() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
if (isBitHigh(BIT_CMD_RESET)) {
|
|
resetState();
|
|
resetFlag = true;
|
|
setBitHigh(BIT_CMD_RESET);
|
|
}
|
|
|
|
if (isBitHigh(BIT_CMD_RECEIVE)) {
|
|
newIncomingQueue.push(getData());
|
|
setBitHigh(BIT_CMD_RECEIVE);
|
|
}
|
|
|
|
if (isBitHigh(BIT_CMD_SEND)) {
|
|
setPendingData();
|
|
setBitHigh(BIT_CMD_SEND);
|
|
}
|
|
|
|
copyState();
|
|
}
|
|
|
|
private:
|
|
U32Queue newIncomingQueue;
|
|
U32Queue incomingQueue;
|
|
U32Queue outgoingQueue;
|
|
volatile bool resetFlag = false;
|
|
volatile bool needsClear = false;
|
|
volatile bool isEnabled = false;
|
|
|
|
void copyState() {
|
|
if (incomingQueue.isReading())
|
|
return;
|
|
|
|
if (needsClear) {
|
|
incomingQueue.clear();
|
|
needsClear = false;
|
|
}
|
|
|
|
while (!newIncomingQueue.isEmpty() && !incomingQueue.isFull())
|
|
incomingQueue.push(newIncomingQueue.pop());
|
|
}
|
|
|
|
void resetState() {
|
|
LINK_BARRIER;
|
|
needsClear = false;
|
|
newIncomingQueue.clear();
|
|
if (incomingQueue.isReading())
|
|
needsClear = true;
|
|
else
|
|
incomingQueue.clear();
|
|
outgoingQueue.syncClear();
|
|
resetFlag = false;
|
|
|
|
newIncomingQueue.overflow = false;
|
|
LINK_BARRIER;
|
|
}
|
|
|
|
void setPendingData() {
|
|
setData(outgoingQueue.isWriting() ? 0 : outgoingQueue.pop());
|
|
}
|
|
|
|
void setData(u32 data) {
|
|
Link::_REG_JOY_TRANS_H = Link::msB32(data);
|
|
Link::_REG_JOY_TRANS_L = Link::lsB32(data);
|
|
}
|
|
|
|
u32 getData() {
|
|
return Link::buildU32(Link::_REG_JOY_RECV_H, Link::_REG_JOY_RECV_L);
|
|
}
|
|
|
|
void stop() {
|
|
setInterruptsOff();
|
|
setGeneralPurposeMode();
|
|
}
|
|
|
|
void start() {
|
|
setJoybusMode();
|
|
setInterruptsOn();
|
|
}
|
|
|
|
void setJoybusMode() {
|
|
Link::_REG_RCNT = Link::_REG_RCNT | (1 << BIT_JOYBUS_HIGH) |
|
|
(1 << BIT_GENERAL_PURPOSE_HIGH);
|
|
}
|
|
|
|
void setGeneralPurposeMode() {
|
|
Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) |
|
|
(1 << BIT_GENERAL_PURPOSE_HIGH);
|
|
}
|
|
|
|
void setInterruptsOn() { setBitHigh(BIT_IRQ); }
|
|
void setInterruptsOff() { setBitLow(BIT_IRQ); }
|
|
|
|
bool isBitHigh(u8 bit) { return (Link::_REG_JOYCNT >> bit) & 1; }
|
|
void setBitHigh(u8 bit) { Link::_REG_JOYCNT |= 1 << bit; }
|
|
void setBitLow(u8 bit) { Link::_REG_JOYCNT &= ~(1 << bit); }
|
|
};
|
|
|
|
extern LinkCube* linkCube;
|
|
|
|
/**
|
|
* @brief SERIAL interrupt handler.
|
|
*/
|
|
inline void LINK_CUBE_ISR_SERIAL() {
|
|
linkCube->_onSerial();
|
|
}
|
|
|
|
#endif // LINK_CUBE_H
|