mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-05-03 12:05:20 -05:00
948 lines
30 KiB
C++
948 lines
30 KiB
C++
#ifndef LINK_MOBILE_H
|
|
#define LINK_MOBILE_H
|
|
|
|
// --------------------------------------------------------------------------
|
|
// A high level driver for the Mobile Adapter GB.
|
|
// --------------------------------------------------------------------------
|
|
// Usage:
|
|
// - 1) Include this header in your main.cpp file and add:
|
|
// LinkMobile* linkMobile = new LinkMobile();
|
|
// - 2) Add the required interrupt service routines: (*)
|
|
// irq_init(NULL);
|
|
// irq_add(II_VBLANK, LINK_MOBILE_ISR_VBLANK);
|
|
// irq_add(II_SERIAL, LINK_MOBILE_ISR_SERIAL);
|
|
// irq_add(II_TIMER3, LINK_MOBILE_ISR_TIMER);
|
|
// - 3) Initialize the library with:
|
|
// linkMobile->activate();
|
|
// --------------------------------------------------------------------------
|
|
// (*) 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)
|
|
// --------------------------------------------------------------------------
|
|
|
|
#include "_link_common.hpp"
|
|
|
|
#include "LinkGPIO.hpp"
|
|
#include "LinkSPI.hpp"
|
|
|
|
static volatile char LINK_MOBILE_VERSION[] = "LinkMobile/v7.0.0";
|
|
|
|
#define LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH 255
|
|
#define LINK_MOBILE_COMMAND_TRANSFER_BUFFER \
|
|
(LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH + 4)
|
|
#define LINK_MOBILE_DEFAULT_TIMER_ID 3
|
|
#define LINK_MOBILE_BARRIER asm volatile("" ::: "memory")
|
|
|
|
#if LINK_ENABLE_DEBUG_LOGS != 0
|
|
#define _LMLOG_(...) Link::log(__VA_ARGS__)
|
|
#else
|
|
#define _LMLOG_(...)
|
|
#endif
|
|
|
|
class LinkMobile {
|
|
private:
|
|
using u32 = unsigned int;
|
|
using u16 = unsigned short;
|
|
using u8 = unsigned char;
|
|
|
|
static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024;
|
|
static constexpr int PING_WAIT_FRAMES = 7;
|
|
static constexpr int TIMEOUT_INIT_FRAMES = 30;
|
|
static constexpr int ADAPTER_WAITING = 0xD2;
|
|
static constexpr u32 ADAPTER_WAITING_32BIT = 0xD2D2D2D2;
|
|
static constexpr int GBA_WAITING = 0x4B;
|
|
static constexpr u32 GBA_WAITING_32BIT = 0x4B4B4B4B;
|
|
static constexpr int OR_VALUE = 0x80;
|
|
static constexpr int COMMAND_MAGIC_VALUE1 = 0x99;
|
|
static constexpr int COMMAND_MAGIC_VALUE2 = 0x66;
|
|
static constexpr int DEVICE_GBA = 0x1;
|
|
static constexpr int DEVICE_ADAPTER_BLUE = 0x8;
|
|
static constexpr int DEVICE_ADAPTER_YELLOW = 0x9;
|
|
static constexpr int DEVICE_ADAPTER_GREEN = 0xA;
|
|
static constexpr int DEVICE_ADAPTER_RED = 0xB;
|
|
static constexpr int ACK_SENDER = 0;
|
|
static constexpr int CONFIGURATION_DATA_SIZE = 192;
|
|
static constexpr int CONFIGURATION_DATA_CHUNK = CONFIGURATION_DATA_SIZE / 2;
|
|
static constexpr int COMMAND_BEGIN_SESSION = 0x10;
|
|
static constexpr int COMMAND_END_SESSION = 0x11;
|
|
static constexpr int COMMAND_DIAL_TELEPHONE = 0x12;
|
|
static constexpr int COMMAND_HANG_UP_TELEPHONE = 0x13;
|
|
static constexpr int COMMAND_WAIT_FOR_TELEPHONE_CALL = 0x14;
|
|
static constexpr int COMMAND_TRANSFER_DATA = 0x15;
|
|
static constexpr int COMMAND_RESET = 0x16;
|
|
static constexpr int COMMAND_SIO32 = 0x18;
|
|
static constexpr int COMMAND_READ_CONFIGURATION_DATA = 0x19;
|
|
static constexpr int COMMAND_ISP_LOGIN = 0x21;
|
|
static constexpr int COMMAND_ISP_LOGOUT = 0x22;
|
|
static constexpr int COMMAND_OPEN_TCP_CONNECTION = 0x23;
|
|
static constexpr int COMMAND_CLOSE_TCP_CONNECTION = 0x24;
|
|
static constexpr int COMMAND_OPEN_UDP_CONNECTION = 0x25;
|
|
static constexpr int COMMAND_CLOSE_UDP_CONNECTION = 0x26;
|
|
static constexpr int COMMAND_DNS_QUERY = 0x28;
|
|
static constexpr int COMMAND_ERROR_STATUS = 0x6E | OR_VALUE;
|
|
|
|
static constexpr u8 WAIT_TICKS[] = {4, 8};
|
|
static constexpr int LOGIN_PARTS_SIZE = 8;
|
|
static constexpr u8 LOGIN_PARTS[] = {0x4e, 0x49, 0x4e, 0x54,
|
|
0x45, 0x4e, 0x44, 0x4f};
|
|
static constexpr int SUPPORTED_DEVICES_SIZE = 4;
|
|
static constexpr u8 SUPPORTED_DEVICES[] = {
|
|
DEVICE_ADAPTER_BLUE, DEVICE_ADAPTER_YELLOW, DEVICE_ADAPTER_GREEN,
|
|
DEVICE_ADAPTER_RED};
|
|
|
|
public:
|
|
enum State {
|
|
NEEDS_RESET,
|
|
PINGING,
|
|
WAITING_TO_START,
|
|
ENDING_SESSION,
|
|
STARTING_SESSION,
|
|
ACTIVATING_SIO32,
|
|
WAITING_TO_SWITCH,
|
|
READING_CONFIGURATION,
|
|
SESSION_ACTIVE,
|
|
SESSION_ACTIVE_CALL,
|
|
SESSION_ACTIVE_ISP
|
|
};
|
|
|
|
enum CommandResult {
|
|
PENDING,
|
|
SUCCESS,
|
|
NOT_WAITING,
|
|
INVALID_DEVICE_ID,
|
|
INVALID_COMMAND_ACK,
|
|
INVALID_MAGIC_BYTES,
|
|
WEIRD_DATA_SIZE,
|
|
WRONG_CHECKSUM,
|
|
ERROR,
|
|
WEIRD_ERROR,
|
|
TIMEOUT
|
|
};
|
|
|
|
struct Error {
|
|
enum Type {
|
|
NONE,
|
|
ADAPTER_NOT_CONNECTED,
|
|
UNEXPECTED_FAILURE,
|
|
WEIRD_RESPONSE,
|
|
BAD_CONFIGURATION_CHECKSUM
|
|
};
|
|
|
|
Error::Type type = Error::Type::NONE;
|
|
State state = State::NEEDS_RESET;
|
|
u8 cmdId = 0;
|
|
CommandResult cmdResult = CommandResult::PENDING;
|
|
u8 cmdErrorCode = 0;
|
|
bool cmdIsSending = false;
|
|
};
|
|
|
|
struct ConfigurationData {
|
|
char magic[2];
|
|
bool isRegistering;
|
|
u8 _unused1_;
|
|
u8 primaryDNS[4];
|
|
u8 secondaryDNS[4];
|
|
char loginID[10];
|
|
u8 _unused2_[22];
|
|
char email[24];
|
|
u8 _unused3_[6];
|
|
char smtpServer[20];
|
|
char popServer[19];
|
|
u8 _unused4_[5];
|
|
u8 configurationSlot1[24];
|
|
u8 configurationSlot2[24];
|
|
u8 configurationSlot3[24];
|
|
u8 checksumHigh;
|
|
u8 checksumLow;
|
|
} __attribute__((packed));
|
|
|
|
explicit LinkMobile(u8 timerId = LINK_MOBILE_DEFAULT_TIMER_ID) {
|
|
this->config.timerId = timerId;
|
|
}
|
|
|
|
[[nodiscard]] bool isActive() { return isEnabled; }
|
|
|
|
/**
|
|
* @brief ...
|
|
*/
|
|
void activate() {
|
|
error = {};
|
|
|
|
LINK_MOBILE_BARRIER;
|
|
isEnabled = false;
|
|
LINK_MOBILE_BARRIER;
|
|
|
|
resetState();
|
|
stop();
|
|
|
|
LINK_MOBILE_BARRIER;
|
|
isEnabled = true;
|
|
LINK_MOBILE_BARRIER;
|
|
|
|
start();
|
|
}
|
|
|
|
void deactivate() {
|
|
error = {};
|
|
isEnabled = false;
|
|
resetState();
|
|
stop();
|
|
}
|
|
|
|
bool readConfiguration(ConfigurationData& configurationData) {
|
|
if (state != SESSION_ACTIVE)
|
|
return false;
|
|
|
|
configurationData = adapterConfiguration.fields;
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] State getState() { return state; }
|
|
|
|
[[nodiscard]] LinkSPI::DataSize getDataSize() {
|
|
return linkSPI->getDataSize();
|
|
}
|
|
|
|
Error getError(bool clear = true) {
|
|
auto err = error;
|
|
if (clear)
|
|
error = {};
|
|
return err;
|
|
}
|
|
|
|
~LinkMobile() { delete linkSPI; }
|
|
|
|
void _onVBlank() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
if (state > NEEDS_RESET && state < SESSION_ACTIVE) {
|
|
timeoutFrames++;
|
|
if (timeoutFrames >= TIMEOUT_INIT_FRAMES)
|
|
return abort(Error::Type::ADAPTER_NOT_CONNECTED);
|
|
}
|
|
|
|
processNewFrame();
|
|
}
|
|
|
|
void _onSerial() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
linkSPI->_onSerial();
|
|
u32 newData = linkSPI->getAsyncData();
|
|
|
|
if (asyncCommand.isActive) {
|
|
if (asyncCommand.state == AsyncCommand::State::PENDING) {
|
|
if (isSIO32Mode()) {
|
|
if (asyncCommand.direction == AsyncCommand::Direction::SENDING)
|
|
sendAsyncCommandSIO32(newData);
|
|
else
|
|
receiveAsyncCommandSIO32(newData);
|
|
} else {
|
|
if (asyncCommand.direction == AsyncCommand::Direction::SENDING)
|
|
sendAsyncCommandSIO8(newData);
|
|
else
|
|
receiveAsyncCommandSIO8(newData);
|
|
}
|
|
|
|
if (asyncCommand.state == AsyncCommand::State::COMPLETED) {
|
|
asyncCommand.isActive = false;
|
|
processAsyncCommand();
|
|
}
|
|
}
|
|
} else {
|
|
processLoosePacket(newData);
|
|
}
|
|
}
|
|
|
|
void _onTimer() {
|
|
if (!isEnabled || !hasPendingTransfer)
|
|
return;
|
|
|
|
linkSPI->transferAsync(pendingTransfer);
|
|
stopTimer();
|
|
hasPendingTransfer = false;
|
|
}
|
|
|
|
struct Config {
|
|
u32 timerId;
|
|
};
|
|
|
|
/**
|
|
* @brief LinkMobile configuration.
|
|
* \warning `deactivate()` first, change the config, and `activate()` again!
|
|
*/
|
|
Config config;
|
|
|
|
private:
|
|
union AdapterConfiguration {
|
|
ConfigurationData fields;
|
|
char bytes[CONFIGURATION_DATA_SIZE];
|
|
|
|
bool isValid() { return calculatedChecksum() == reportedChecksum(); }
|
|
|
|
u16 calculatedChecksum() {
|
|
u16 result = 0;
|
|
for (u32 i = 0; i < CONFIGURATION_DATA_SIZE - 2; i++)
|
|
result += bytes[i];
|
|
return result;
|
|
}
|
|
|
|
u16 reportedChecksum() {
|
|
return buildU16(fields.checksumHigh, fields.checksumLow);
|
|
}
|
|
};
|
|
|
|
struct MagicBytes {
|
|
u8 magic1 = COMMAND_MAGIC_VALUE1;
|
|
u8 magic2 = COMMAND_MAGIC_VALUE2;
|
|
} __attribute__((packed));
|
|
|
|
struct PacketData {
|
|
u8 bytes[LINK_MOBILE_COMMAND_TRANSFER_BUFFER] = {};
|
|
} __attribute__((packed));
|
|
|
|
struct PacketHeader {
|
|
u8 commandId = 0;
|
|
u8 _unused_ = 0;
|
|
u8 _unusedSizeHigh_ = 0;
|
|
u8 size = 0;
|
|
|
|
u16 sum() { return commandId + _unused_ + _unusedSizeHigh_ + size; }
|
|
} __attribute__((packed));
|
|
|
|
struct PacketChecksum {
|
|
u8 high = 0;
|
|
u8 low = 0;
|
|
} __attribute__((packed));
|
|
|
|
struct Command {
|
|
MagicBytes magicBytes;
|
|
PacketHeader header;
|
|
PacketData data;
|
|
PacketChecksum checksum;
|
|
};
|
|
|
|
struct CommandResponse {
|
|
CommandResult result = CommandResult::PENDING;
|
|
Command command;
|
|
};
|
|
|
|
struct AsyncCommand {
|
|
enum State { PENDING, COMPLETED };
|
|
enum Direction { SENDING, RECEIVING };
|
|
|
|
State state;
|
|
CommandResult result;
|
|
u32 transferred;
|
|
Command cmd;
|
|
Direction direction;
|
|
u16 expectedChecksum;
|
|
bool expectsResponse;
|
|
u8 errorCode;
|
|
bool isActive = false;
|
|
|
|
void reset() {
|
|
state = AsyncCommand::State::PENDING;
|
|
result = CommandResult::PENDING;
|
|
transferred = 0;
|
|
cmd = Command{};
|
|
direction = AsyncCommand::Direction::SENDING;
|
|
expectedChecksum = 0;
|
|
expectsResponse = false;
|
|
errorCode = 0;
|
|
isActive = false;
|
|
}
|
|
|
|
bool respondsTo(u8 commandId) {
|
|
return direction == AsyncCommand::Direction::RECEIVING &&
|
|
cmd.header.commandId == (commandId | OR_VALUE);
|
|
}
|
|
|
|
void finish() {
|
|
if (cmd.header.commandId == COMMAND_ERROR_STATUS) {
|
|
if (cmd.header.size != 2) {
|
|
result = CommandResult::WEIRD_ERROR;
|
|
errorCode = cmd.data.bytes[1];
|
|
} else {
|
|
result = CommandResult::ERROR;
|
|
}
|
|
} else {
|
|
result = CommandResult::SUCCESS;
|
|
}
|
|
|
|
state = AsyncCommand::State::COMPLETED;
|
|
}
|
|
|
|
void fail(CommandResult _result) {
|
|
result = _result;
|
|
state = AsyncCommand::State::COMPLETED;
|
|
}
|
|
};
|
|
|
|
static constexpr u32 PREAMBLE_SIZE =
|
|
sizeof(MagicBytes) + sizeof(PacketHeader);
|
|
static constexpr u32 CHECKSUM_SIZE = sizeof(PacketChecksum);
|
|
|
|
AdapterConfiguration adapterConfiguration;
|
|
AsyncCommand asyncCommand;
|
|
u32 waitFrames = 0;
|
|
u32 timeoutFrames = 0;
|
|
LinkSPI* linkSPI = new LinkSPI();
|
|
State state = NEEDS_RESET;
|
|
PacketData nextCommandData;
|
|
u32 nextCommandDataSize = 0;
|
|
bool hasPendingTransfer = false;
|
|
u32 pendingTransfer = 0;
|
|
Error error = {};
|
|
volatile bool isEnabled = false;
|
|
|
|
void processNewFrame() {
|
|
switch (state) {
|
|
case WAITING_TO_START: {
|
|
waitFrames--;
|
|
|
|
if (waitFrames == 0) {
|
|
setState(ENDING_SESSION);
|
|
cmdEndSession();
|
|
}
|
|
break;
|
|
}
|
|
case WAITING_TO_SWITCH: {
|
|
waitFrames--;
|
|
|
|
if (waitFrames == 0) {
|
|
linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS,
|
|
LinkSPI::DataSize::SIZE_32BIT);
|
|
setState(READING_CONFIGURATION);
|
|
cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
}
|
|
|
|
void processAsyncCommand() {
|
|
if (asyncCommand.result != CommandResult::SUCCESS) {
|
|
if (state >= STARTING_SESSION && state < SESSION_ACTIVE)
|
|
return abort(Error::Type::UNEXPECTED_FAILURE);
|
|
}
|
|
|
|
_LMLOG_("%s $%X [%d]",
|
|
asyncCommand.direction == AsyncCommand::Direction::SENDING ? ">!"
|
|
: "<!",
|
|
asyncCommand.cmd.header.commandId & (~OR_VALUE),
|
|
asyncCommand.cmd.header.size);
|
|
|
|
if (asyncCommand.direction == AsyncCommand::Direction::SENDING) {
|
|
if (asyncCommand.expectsResponse) {
|
|
if (asyncCommand.result != CommandResult::SUCCESS)
|
|
return;
|
|
receiveCommandAsync();
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (state) {
|
|
case ENDING_SESSION: {
|
|
if (!asyncCommand.respondsTo(COMMAND_END_SESSION))
|
|
return;
|
|
|
|
setState(STARTING_SESSION);
|
|
cmdBeginSession();
|
|
break;
|
|
}
|
|
case STARTING_SESSION: {
|
|
if (!asyncCommand.respondsTo(COMMAND_BEGIN_SESSION))
|
|
return;
|
|
if (asyncCommand.cmd.header.size != LOGIN_PARTS_SIZE)
|
|
return abort(Error::Type::WEIRD_RESPONSE);
|
|
|
|
for (u32 i = 0; i < LOGIN_PARTS_SIZE; i++) {
|
|
if (asyncCommand.cmd.data.bytes[i] != LOGIN_PARTS[i])
|
|
return abort(Error::Type::WEIRD_RESPONSE);
|
|
}
|
|
|
|
setState(ACTIVATING_SIO32);
|
|
cmdSIO32();
|
|
break;
|
|
}
|
|
case ACTIVATING_SIO32: {
|
|
if (asyncCommand.respondsTo(COMMAND_RESET)) {
|
|
// If the adapter responds to a 0x16 instead of 0x18,
|
|
// it's libmobile telling us that SIO32 is not supported.
|
|
// In that case, we continue using SIO8.
|
|
setState(READING_CONFIGURATION);
|
|
cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK);
|
|
return;
|
|
}
|
|
if (!asyncCommand.respondsTo(COMMAND_SIO32))
|
|
return;
|
|
|
|
setState(WAITING_TO_SWITCH);
|
|
waitFrames = PING_WAIT_FRAMES;
|
|
break;
|
|
}
|
|
case READING_CONFIGURATION: {
|
|
if (!asyncCommand.respondsTo(COMMAND_READ_CONFIGURATION_DATA))
|
|
return;
|
|
|
|
u32 offset = asyncCommand.cmd.data.bytes[0];
|
|
u32 sizeWithOffsetByte = asyncCommand.cmd.header.size;
|
|
if (asyncCommand.result != CommandResult::SUCCESS ||
|
|
sizeWithOffsetByte != CONFIGURATION_DATA_CHUNK + 1 ||
|
|
(offset != 0 && offset != CONFIGURATION_DATA_CHUNK))
|
|
return abort(Error::Type::WEIRD_RESPONSE);
|
|
|
|
for (u32 i = 0; i < CONFIGURATION_DATA_CHUNK; i++)
|
|
adapterConfiguration.bytes[offset + i] =
|
|
asyncCommand.cmd.data.bytes[1 + i];
|
|
|
|
if (offset == CONFIGURATION_DATA_CHUNK &&
|
|
!adapterConfiguration.isValid())
|
|
return abort(Error::Type::BAD_CONFIGURATION_CHECKSUM);
|
|
|
|
if (offset == 0)
|
|
cmdReadConfigurationData(CONFIGURATION_DATA_CHUNK,
|
|
CONFIGURATION_DATA_CHUNK);
|
|
else
|
|
setState(SESSION_ACTIVE);
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
}
|
|
|
|
void processLoosePacket(u32 newData) {
|
|
switch (state) {
|
|
case PINGING: {
|
|
setState(WAITING_TO_START);
|
|
waitFrames = PING_WAIT_FRAMES;
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
}
|
|
|
|
void cmdBeginSession() {
|
|
for (u32 i = 0; i < LOGIN_PARTS_SIZE; i++)
|
|
addData(LOGIN_PARTS[i], i == 0);
|
|
sendCommandAsync(buildCommand(COMMAND_BEGIN_SESSION, true), true);
|
|
}
|
|
|
|
void cmdEndSession() {
|
|
sendCommandAsync(buildCommand(COMMAND_END_SESSION, true), true);
|
|
}
|
|
|
|
void cmdSIO32() {
|
|
addData(1, true);
|
|
sendCommandAsync(buildCommand(COMMAND_SIO32, true), true);
|
|
}
|
|
|
|
void cmdReadConfigurationData(u8 offset, u8 size) {
|
|
addData(offset, true);
|
|
addData(CONFIGURATION_DATA_CHUNK);
|
|
sendCommandAsync(buildCommand(COMMAND_READ_CONFIGURATION_DATA, true), true);
|
|
}
|
|
|
|
void addData(u8 value, bool start = false) {
|
|
if (start) {
|
|
nextCommandDataSize = 0;
|
|
nextCommandData = PacketData{};
|
|
}
|
|
nextCommandData.bytes[nextCommandDataSize] = value;
|
|
nextCommandDataSize++;
|
|
}
|
|
|
|
void setState(State newState) {
|
|
State oldState = state;
|
|
state = newState;
|
|
timeoutFrames = 0;
|
|
_LMLOG_("!! New state: %d -> %d", oldState, newState);
|
|
(void)oldState;
|
|
}
|
|
|
|
void abort(Error::Type errorType) {
|
|
error =
|
|
Error{.type = errorType,
|
|
.state = state,
|
|
.cmdId = (u8)(asyncCommand.cmd.header.commandId & (~OR_VALUE)),
|
|
.cmdResult = asyncCommand.result,
|
|
.cmdErrorCode = asyncCommand.errorCode,
|
|
.cmdIsSending =
|
|
asyncCommand.direction == AsyncCommand::Direction::SENDING};
|
|
|
|
_LMLOG_(
|
|
"!! aborted:\n error: %d\n cmdId: %s$%X\n cmdResult: %d\n "
|
|
"cmdErrorCode: %d",
|
|
error.type, error.cmdIsSending ? ">" : "<", error.cmdId,
|
|
error.cmdResult, error.cmdErrorCode);
|
|
|
|
resetState();
|
|
stop();
|
|
}
|
|
|
|
void reset() {
|
|
resetState();
|
|
stop();
|
|
start();
|
|
}
|
|
|
|
void resetState() {
|
|
setState(NEEDS_RESET);
|
|
|
|
this->adapterConfiguration = AdapterConfiguration{};
|
|
this->asyncCommand.reset();
|
|
this->waitFrames = 0;
|
|
this->timeoutFrames = 0;
|
|
this->nextCommandDataSize = 0;
|
|
this->hasPendingTransfer = false;
|
|
this->pendingTransfer = 0;
|
|
}
|
|
|
|
void stop() {
|
|
stopTimer();
|
|
linkSPI->deactivate();
|
|
}
|
|
|
|
void start() {
|
|
linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS,
|
|
LinkSPI::DataSize::SIZE_8BIT);
|
|
|
|
setState(PINGING);
|
|
transferAsync(0);
|
|
}
|
|
|
|
void stopTimer() {
|
|
Link::_REG_TM[config.timerId].cnt =
|
|
Link::_REG_TM[config.timerId].cnt & (~Link::_TM_ENABLE);
|
|
}
|
|
|
|
void startTimer(u16 interval) {
|
|
Link::_REG_TM[config.timerId].start = -interval;
|
|
Link::_REG_TM[config.timerId].cnt =
|
|
Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY;
|
|
}
|
|
|
|
void sendCommandAsync(Command command, bool expectsResponse = false) {
|
|
_LMLOG_(">> $%X [%d]%s", command.header.commandId, command.header.size,
|
|
expectsResponse ? " (...)" : "");
|
|
asyncCommand.reset();
|
|
asyncCommand.cmd = command;
|
|
asyncCommand.expectsResponse = expectsResponse;
|
|
asyncCommand.isActive = true;
|
|
|
|
if (isSIO32Mode()) // Magic+Header
|
|
advance32(buildU32(command.magicBytes.magic1, command.magicBytes.magic2,
|
|
command.header.commandId, command.header._unused_));
|
|
else // Magic Bytes (1)
|
|
advance8(command.magicBytes.magic1);
|
|
}
|
|
|
|
void receiveCommandAsync() {
|
|
_LMLOG_("<< ...");
|
|
asyncCommand.reset();
|
|
asyncCommand.direction = AsyncCommand::Direction::RECEIVING;
|
|
asyncCommand.isActive = true;
|
|
|
|
if (isSIO32Mode())
|
|
transferAsync(GBA_WAITING_32BIT);
|
|
else
|
|
transferAsync(GBA_WAITING);
|
|
}
|
|
|
|
void sendAsyncCommandSIO8(u32 newData) {
|
|
const u8* commandBytes = (const u8*)&asyncCommand.cmd;
|
|
u32 mainSize = PREAMBLE_SIZE + asyncCommand.cmd.header.size;
|
|
|
|
bool isAcknowledgement =
|
|
asyncCommand.transferred >= mainSize + CHECKSUM_SIZE + 1;
|
|
if (!isAcknowledgement && newData != ADAPTER_WAITING)
|
|
return asyncCommand.fail(CommandResult::NOT_WAITING);
|
|
|
|
if (asyncCommand.transferred < mainSize) {
|
|
// Magic Bytes (2) + Packet Header + Packet Data
|
|
advance8(commandBytes[asyncCommand.transferred]);
|
|
} else if (asyncCommand.transferred < mainSize + CHECKSUM_SIZE) {
|
|
// Packet Checksum
|
|
commandBytes += PREAMBLE_SIZE + LINK_MOBILE_COMMAND_TRANSFER_BUFFER;
|
|
advance8(commandBytes[asyncCommand.transferred - mainSize]);
|
|
} else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE) {
|
|
// Acknowledgement Signal (1)
|
|
advance8(DEVICE_GBA | OR_VALUE);
|
|
} else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 1) {
|
|
// Acknowledgement Signal (2)
|
|
if (!isSupportedAdapter(newData))
|
|
return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
|
|
advance8(ACK_SENDER);
|
|
} else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 2) {
|
|
// Acknowledgement Signal (3)
|
|
if (newData != (asyncCommand.cmd.header.commandId ^ OR_VALUE))
|
|
return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK);
|
|
asyncCommand.finish();
|
|
}
|
|
}
|
|
|
|
void sendAsyncCommandSIO32(u32 newData) {
|
|
u32 dataSize = asyncCommand.cmd.header.size;
|
|
u32 alignment = dataSize % 4;
|
|
u32 padding = alignment != 0 ? 4 - alignment : 0;
|
|
u32 mainSize = PREAMBLE_SIZE + dataSize + padding;
|
|
|
|
bool isAcknowledgement = asyncCommand.transferred >= mainSize + 4;
|
|
if (!isAcknowledgement && newData != ADAPTER_WAITING &&
|
|
newData != ADAPTER_WAITING_32BIT)
|
|
return asyncCommand.fail(CommandResult::NOT_WAITING);
|
|
|
|
if (asyncCommand.transferred == 4) {
|
|
// Header+Data || Header+Checksum
|
|
advance32(dataSize > 0
|
|
? buildU32(asyncCommand.cmd.header._unusedSizeHigh_,
|
|
asyncCommand.cmd.header.size,
|
|
asyncCommand.cmd.data.bytes[0],
|
|
asyncCommand.cmd.data.bytes[1])
|
|
: buildU32(asyncCommand.cmd.header._unusedSizeHigh_,
|
|
asyncCommand.cmd.header.size,
|
|
asyncCommand.cmd.checksum.high,
|
|
asyncCommand.cmd.checksum.low));
|
|
} else if (asyncCommand.transferred < mainSize) {
|
|
// Data || Data+Checksum
|
|
u32 transferredDataCount = asyncCommand.transferred - PREAMBLE_SIZE;
|
|
u32 pendingDataCount = (dataSize + padding) - transferredDataCount;
|
|
advance32(
|
|
pendingDataCount > 2
|
|
? buildU32(asyncCommand.cmd.data.bytes[transferredDataCount],
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 1],
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 2],
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 3])
|
|
: buildU32(asyncCommand.cmd.data.bytes[transferredDataCount],
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 1],
|
|
asyncCommand.cmd.checksum.high,
|
|
asyncCommand.cmd.checksum.low));
|
|
} else if (!isAcknowledgement) {
|
|
// Acknowledgement Signal (1)
|
|
advance32(buildU32(DEVICE_GBA | OR_VALUE, ACK_SENDER, 0, 0));
|
|
} else {
|
|
// Acknowledgement Signal (2)
|
|
u16 ackData = msB32(newData);
|
|
if (!isSupportedAdapter(msB16(ackData)))
|
|
return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
|
|
if (lsB16(ackData) != (asyncCommand.cmd.header.commandId ^ OR_VALUE))
|
|
return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK);
|
|
asyncCommand.finish();
|
|
}
|
|
}
|
|
|
|
void receiveAsyncCommandSIO8(u32 newData) {
|
|
u8* commandBytes = (u8*)&asyncCommand.cmd;
|
|
u32 mainSize = PREAMBLE_SIZE + asyncCommand.cmd.header.size;
|
|
|
|
if (asyncCommand.transferred == 0) {
|
|
// Magic Bytes (1)
|
|
if (newData == ADAPTER_WAITING)
|
|
return transferAsync(GBA_WAITING);
|
|
if (newData != COMMAND_MAGIC_VALUE1)
|
|
return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES);
|
|
advance8(GBA_WAITING);
|
|
} else if (asyncCommand.transferred == 1) {
|
|
// Magic Bytes (1)
|
|
if (newData != COMMAND_MAGIC_VALUE2)
|
|
return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES);
|
|
advance8(GBA_WAITING);
|
|
} else if (asyncCommand.transferred < PREAMBLE_SIZE) {
|
|
// Packet Header
|
|
commandBytes[asyncCommand.transferred] = newData;
|
|
if (asyncCommand.cmd.header._unusedSizeHigh_ != 0)
|
|
return asyncCommand.fail(CommandResult::WEIRD_DATA_SIZE);
|
|
advance8(GBA_WAITING);
|
|
if (asyncCommand.transferred == PREAMBLE_SIZE)
|
|
asyncCommand.expectedChecksum = asyncCommand.cmd.header.sum();
|
|
} else if (asyncCommand.transferred < mainSize) {
|
|
// Packet Data
|
|
commandBytes[asyncCommand.transferred] = newData;
|
|
asyncCommand.expectedChecksum += newData;
|
|
advance8(GBA_WAITING);
|
|
} else if (asyncCommand.transferred == mainSize) {
|
|
// Packet Checksum (1)
|
|
if (newData != msB16(asyncCommand.expectedChecksum))
|
|
return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
|
|
advance8(GBA_WAITING);
|
|
} else if (asyncCommand.transferred == mainSize + 1) {
|
|
// Packet Checksum (2)
|
|
if (newData != lsB16(asyncCommand.expectedChecksum))
|
|
return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
|
|
advance8(DEVICE_GBA | OR_VALUE);
|
|
} else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE) {
|
|
// Acknowledgement Signal (1)
|
|
if (!isSupportedAdapter(newData))
|
|
return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
|
|
advance8(asyncCommand.cmd.header.commandId ^ OR_VALUE);
|
|
} else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 1) {
|
|
// Acknowledgement Signal (2)
|
|
if (newData != ACK_SENDER)
|
|
return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK);
|
|
asyncCommand.finish();
|
|
}
|
|
}
|
|
|
|
void receiveAsyncCommandSIO32(u32 newData) {
|
|
u32 dataSize = asyncCommand.cmd.header.size;
|
|
u32 alignment = dataSize % 4;
|
|
u32 padding = alignment != 0 ? 4 - alignment : 0;
|
|
u32 mainSize = PREAMBLE_SIZE + dataSize + padding;
|
|
|
|
if (asyncCommand.transferred == 0) {
|
|
// Magic+Header
|
|
if (newData == ADAPTER_WAITING || newData == ADAPTER_WAITING_32BIT)
|
|
return transferAsync(GBA_WAITING_32BIT);
|
|
u16 magic = msB32(newData);
|
|
u16 firstHalfHeader = lsB32(newData);
|
|
if (msB16(magic) != COMMAND_MAGIC_VALUE1 ||
|
|
lsB16(magic) != COMMAND_MAGIC_VALUE2)
|
|
return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES);
|
|
asyncCommand.cmd.header.commandId = msB16(firstHalfHeader);
|
|
asyncCommand.cmd.header._unused_ = lsB16(firstHalfHeader);
|
|
advance32(GBA_WAITING_32BIT);
|
|
} else if (asyncCommand.transferred == 4) {
|
|
// Header+Data || Header+Checksum
|
|
u16 secondHalfHeader = msB32(newData);
|
|
asyncCommand.cmd.header._unusedSizeHigh_ = msB16(secondHalfHeader);
|
|
asyncCommand.cmd.header.size = lsB16(secondHalfHeader);
|
|
asyncCommand.expectedChecksum = asyncCommand.cmd.header.sum();
|
|
if (asyncCommand.cmd.header.size > 0) {
|
|
u16 firstData = lsB32(newData);
|
|
u8 b0 = msB16(firstData), b1 = lsB16(firstData);
|
|
asyncCommand.cmd.data.bytes[0] = b0;
|
|
asyncCommand.cmd.data.bytes[1] = b1;
|
|
asyncCommand.expectedChecksum += b0 + b1;
|
|
} else {
|
|
u16 checksum = lsB32(newData);
|
|
if (msB16(checksum) != msB16(asyncCommand.expectedChecksum) ||
|
|
lsB16(checksum) != msB16(asyncCommand.expectedChecksum))
|
|
return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
|
|
asyncCommand.cmd.checksum.high = msB16(checksum);
|
|
asyncCommand.cmd.checksum.low = lsB16(checksum);
|
|
}
|
|
advance32(GBA_WAITING_32BIT);
|
|
} else if (asyncCommand.transferred < mainSize) {
|
|
// Data || Data+Checksum
|
|
u32 transferredDataCount = asyncCommand.transferred - PREAMBLE_SIZE;
|
|
u32 pendingDataCount = (dataSize + padding) - transferredDataCount;
|
|
if (pendingDataCount > 2) {
|
|
u16 dataHigh = msB32(newData);
|
|
u16 dataLow = lsB32(newData);
|
|
u8 b0 = msB16(dataHigh), b1 = lsB16(dataHigh), b2 = msB16(dataLow),
|
|
b3 = lsB16(dataLow);
|
|
asyncCommand.cmd.data.bytes[transferredDataCount] = b0;
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 1] = b1;
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 2] = b2;
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 3] = b3;
|
|
asyncCommand.expectedChecksum += b0 + b1 + b2 + b3;
|
|
advance32(GBA_WAITING_32BIT);
|
|
} else {
|
|
u16 lastData = msB32(newData);
|
|
u8 b0 = msB16(lastData), b1 = lsB16(lastData);
|
|
asyncCommand.cmd.data.bytes[transferredDataCount] = b0;
|
|
asyncCommand.cmd.data.bytes[transferredDataCount + 1] = b1;
|
|
asyncCommand.expectedChecksum += b0 + b1;
|
|
u16 checksum = lsB32(newData);
|
|
if (msB16(checksum) != msB16(asyncCommand.expectedChecksum) ||
|
|
lsB16(checksum) != lsB16(asyncCommand.expectedChecksum))
|
|
return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
|
|
asyncCommand.cmd.checksum.high = msB16(checksum);
|
|
asyncCommand.cmd.checksum.low = lsB16(checksum);
|
|
advance32(buildU32(DEVICE_GBA | OR_VALUE,
|
|
asyncCommand.cmd.header.commandId ^ OR_VALUE, 0, 0));
|
|
}
|
|
} else {
|
|
// Acknowledgement Signal
|
|
u32 ackData = msB32(newData);
|
|
if (!isSupportedAdapter(msB16(ackData)) || lsB16(ackData) != ACK_SENDER)
|
|
return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
|
|
asyncCommand.finish();
|
|
}
|
|
}
|
|
|
|
bool isSupportedAdapter(u8 ack) {
|
|
for (u32 i = 0; i < SUPPORTED_DEVICES_SIZE; i++) {
|
|
if ((SUPPORTED_DEVICES[i] | OR_VALUE) == ack)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Command buildCommand(u8 type, bool withData = false) {
|
|
Command command;
|
|
command.header.commandId = type;
|
|
command.header._unused_ = 0;
|
|
command.header._unusedSizeHigh_ = 0;
|
|
command.header.size = withData ? (u8)nextCommandDataSize : 0;
|
|
if (withData)
|
|
command.data = nextCommandData;
|
|
u16 checksum = command.header.sum();
|
|
for (u32 i = 0; i < command.header.size; i++)
|
|
checksum += command.data.bytes[i];
|
|
command.checksum.high = msB16(checksum);
|
|
command.checksum.low = lsB16(checksum);
|
|
|
|
return command;
|
|
}
|
|
|
|
void advance8(u32 data) {
|
|
transferAsync(data);
|
|
asyncCommand.transferred++;
|
|
}
|
|
|
|
void advance32(u32 data) {
|
|
transferAsync(data);
|
|
asyncCommand.transferred += 4;
|
|
}
|
|
|
|
void transferAsync(u32 data) {
|
|
hasPendingTransfer = true;
|
|
pendingTransfer = data;
|
|
startTimer(WAIT_TICKS[isSIO32Mode()]);
|
|
}
|
|
|
|
bool isSIO32Mode() {
|
|
return linkSPI->getDataSize() == LinkSPI::DataSize::SIZE_32BIT;
|
|
}
|
|
|
|
static u32 buildU32(u8 msB, u8 byte2, u8 byte3, u8 lsB) {
|
|
return ((msB & 0xFF) << 24) | ((byte2 & 0xFF) << 16) |
|
|
((byte3 & 0xFF) << 8) | (lsB & 0xFF);
|
|
}
|
|
static u16 buildU16(u8 msB, u8 lsB) { return (msB << 8) | lsB; }
|
|
static u16 msB32(u32 value) { return value >> 16; }
|
|
static u16 lsB32(u32 value) { return value & 0xffff; }
|
|
static u8 msB16(u16 value) { return value >> 8; }
|
|
static u8 lsB16(u16 value) { return value & 0xff; }
|
|
};
|
|
|
|
extern LinkMobile* linkMobile;
|
|
|
|
inline void LINK_MOBILE_ISR_VBLANK() {
|
|
linkMobile->_onVBlank();
|
|
}
|
|
|
|
inline void LINK_MOBILE_ISR_SERIAL() {
|
|
linkMobile->_onSerial();
|
|
}
|
|
|
|
inline void LINK_MOBILE_ISR_TIMER() {
|
|
linkMobile->_onTimer();
|
|
}
|
|
|
|
#undef _LMLOG_
|
|
|
|
#endif // LINK_MOBILE_H
|
|
|
|
// TODO: Error screen
|
|
// TODO: Keep adapter alive with Wait For Telephone Call
|