mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-04-17 06:35:58 -05:00
290 lines
9.3 KiB
C++
290 lines
9.3 KiB
C++
#ifndef LINK_CABLE_MULTIBOOT_H
|
|
#define LINK_CABLE_MULTIBOOT_H
|
|
|
|
// --------------------------------------------------------------------------
|
|
// A Multiboot tool to send small programs from one GBA to up to 3 slaves.
|
|
// --------------------------------------------------------------------------
|
|
// Usage:
|
|
// - 1) Include this header in your main.cpp file and add:
|
|
// LinkCableMultiboot* linkCableMultiboot = new LinkCableMultiboot();
|
|
// - 2) Send the ROM:
|
|
// LinkCableMultiboot::Result result = linkCableMultiboot->sendRom(
|
|
// romBytes, // for current ROM, use: ((const u8*)MEM_EWRAM)
|
|
// romLength, // in bytes, should be multiple of 0x10
|
|
// []() {
|
|
// u16 keys = ~REG_KEYS & KEY_ANY;
|
|
// return keys & KEY_START;
|
|
// // (when this returns true, the transfer will be canceled)
|
|
// }
|
|
// );
|
|
// // `result` should be LinkCableMultiboot::Result::SUCCESS
|
|
// --------------------------------------------------------------------------
|
|
// considerations:
|
|
// - for better results, turn on the GBAs after calling the `sendRom` method!
|
|
// - stop DMA before sending the ROM! (you might need to stop your audio player)
|
|
// --------------------------------------------------------------------------
|
|
|
|
#include "LinkRawCable.hpp"
|
|
|
|
static volatile char LINK_CABLE_MULTIBOOT_VERSION[] =
|
|
"LinkCableMultiboot/v7.0.0";
|
|
|
|
#define LINK_CABLE_MULTIBOOT_TRY(CALL) \
|
|
do { \
|
|
partialResult = CALL; \
|
|
} while (partialResult == NEEDS_RETRY); \
|
|
if (partialResult == ABORTED) \
|
|
return error(CANCELED); \
|
|
else if (partialResult == ERROR) \
|
|
return error(FAILURE_DURING_HANDSHAKE);
|
|
|
|
/**
|
|
* @brief A Multiboot tool to send small programs from one GBA to up to 3
|
|
* slaves.
|
|
*/
|
|
class LinkCableMultiboot {
|
|
private:
|
|
using u32 = unsigned int;
|
|
using u16 = unsigned short;
|
|
using u8 = unsigned char;
|
|
|
|
static constexpr int MIN_ROM_SIZE = 0x100 + 0xc0;
|
|
static constexpr int MAX_ROM_SIZE = 256 * 1024;
|
|
static constexpr int WAIT_BEFORE_RETRY = (160 + 68) * 60;
|
|
static constexpr int WAIT_BEFORE_TRANSFER = 50;
|
|
static constexpr int DETECTION_TRIES = 16;
|
|
static constexpr int PALETTE_DATA = 0x93;
|
|
static constexpr int CLIENTS = 3;
|
|
static constexpr int CLIENT_NO_DATA = 0xff;
|
|
static constexpr int HANDSHAKE = 0x6200;
|
|
static constexpr int HANDSHAKE_RESPONSE = 0x7200;
|
|
static constexpr int CONFIRM_CLIENTS = 0x6100;
|
|
static constexpr int SEND_PALETTE = 0x6300;
|
|
static constexpr int HANDSHAKE_DATA = 0x11;
|
|
static constexpr int CONFIRM_HANDSHAKE_DATA = 0x6400;
|
|
static constexpr int ACK_RESPONSE = 0x73;
|
|
static constexpr int HEADER_SIZE = 0xC0;
|
|
static constexpr int SWI_MULTIPLAYER_MODE = 1;
|
|
static constexpr auto MAX_BAUD_RATE = LinkRawCable::BaudRate::BAUD_RATE_3;
|
|
static constexpr u8 CLIENT_IDS[] = {0b0010, 0b0100, 0b1000};
|
|
|
|
public:
|
|
enum Result {
|
|
SUCCESS,
|
|
INVALID_SIZE,
|
|
CANCELED,
|
|
FAILURE_DURING_HANDSHAKE,
|
|
FAILURE_DURING_TRANSFER
|
|
};
|
|
|
|
/**
|
|
* @brief Sends the `rom`. Once completed, the return value should be
|
|
* `LinkCableMultiboot::Result::SUCCESS`.
|
|
* @param rom A pointer to ROM data.
|
|
* @param romSize Size of the ROM in bytes. It must be a number between `448`
|
|
* and `262144`, and a multiple of `16`.
|
|
* @param cancel A function that will be continuously invoked. If it returns
|
|
* `true`, the transfer will be aborted.
|
|
*/
|
|
template <typename F>
|
|
[[nodiscard]] Result sendRom(const u8* rom, u32 romSize, F cancel) {
|
|
if (romSize < MIN_ROM_SIZE)
|
|
return INVALID_SIZE;
|
|
if (romSize > MAX_ROM_SIZE)
|
|
return INVALID_SIZE;
|
|
if ((romSize % 0x10) != 0)
|
|
return INVALID_SIZE;
|
|
|
|
PartialResult partialResult;
|
|
Link::_MultiBootParam multiBootParameters;
|
|
multiBootParameters.client_data[0] = CLIENT_NO_DATA;
|
|
multiBootParameters.client_data[1] = CLIENT_NO_DATA;
|
|
multiBootParameters.client_data[2] = CLIENT_NO_DATA;
|
|
multiBootParameters.palette_data = PALETTE_DATA;
|
|
multiBootParameters.client_bit = 0;
|
|
multiBootParameters.boot_srcp = (u8*)rom + HEADER_SIZE;
|
|
multiBootParameters.boot_endp = (u8*)rom + romSize;
|
|
|
|
LINK_CABLE_MULTIBOOT_TRY(detectClients(multiBootParameters, cancel))
|
|
LINK_CABLE_MULTIBOOT_TRY(confirmClients(multiBootParameters, cancel))
|
|
LINK_CABLE_MULTIBOOT_TRY(sendHeader(rom, cancel))
|
|
LINK_CABLE_MULTIBOOT_TRY(confirmHeader(multiBootParameters, cancel))
|
|
LINK_CABLE_MULTIBOOT_TRY(reconfirm(multiBootParameters, cancel))
|
|
LINK_CABLE_MULTIBOOT_TRY(sendPalette(multiBootParameters, cancel))
|
|
|
|
multiBootParameters.handshake_data =
|
|
(HANDSHAKE_DATA + multiBootParameters.client_data[0] +
|
|
multiBootParameters.client_data[1] +
|
|
multiBootParameters.client_data[2]) %
|
|
256;
|
|
|
|
LINK_CABLE_MULTIBOOT_TRY(confirmHandshakeData(multiBootParameters, cancel))
|
|
|
|
int result = Link::_MultiBoot(&multiBootParameters, SWI_MULTIPLAYER_MODE);
|
|
|
|
linkRawCable->deactivate();
|
|
|
|
return result == 1 ? FAILURE_DURING_TRANSFER : SUCCESS;
|
|
}
|
|
|
|
~LinkCableMultiboot() { delete linkRawCable; }
|
|
|
|
private:
|
|
LinkRawCable* linkRawCable = new LinkRawCable();
|
|
|
|
enum PartialResult { NEEDS_RETRY, FINISHED, ABORTED, ERROR };
|
|
|
|
struct Responses {
|
|
u16 d[CLIENTS];
|
|
};
|
|
|
|
template <typename F>
|
|
PartialResult detectClients(Link::_MultiBootParam& multiBootParameters,
|
|
F cancel) {
|
|
linkRawCable->activate(MAX_BAUD_RATE);
|
|
|
|
for (u32 t = 0; t < DETECTION_TRIES; t++) {
|
|
auto response = linkRawCable->transfer(HANDSHAKE, cancel);
|
|
if (cancel())
|
|
return ABORTED;
|
|
|
|
for (u32 i = 0; i < CLIENTS; i++) {
|
|
if ((response.data[1 + i] & 0xfff0) == HANDSHAKE_RESPONSE) {
|
|
auto clientId = response.data[1 + i] & 0xf;
|
|
|
|
switch (clientId) {
|
|
case 0b0010:
|
|
case 0b0100:
|
|
case 0b1000: {
|
|
multiBootParameters.client_bit |= clientId;
|
|
break;
|
|
}
|
|
default:
|
|
return NEEDS_RETRY;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (multiBootParameters.client_bit == 0) {
|
|
linkRawCable->deactivate();
|
|
wait(WAIT_BEFORE_RETRY);
|
|
return NEEDS_RETRY;
|
|
}
|
|
|
|
return FINISHED;
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult confirmClients(Link::_MultiBootParam& multiBootParameters,
|
|
F cancel) {
|
|
return compare(multiBootParameters,
|
|
CONFIRM_CLIENTS | multiBootParameters.client_bit,
|
|
HANDSHAKE_RESPONSE, cancel);
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult confirmHeader(Link::_MultiBootParam& multiBootParameters,
|
|
F cancel) {
|
|
return compare(multiBootParameters, HANDSHAKE, 0, cancel);
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult reconfirm(Link::_MultiBootParam& multiBootParameters,
|
|
F cancel) {
|
|
return compare(multiBootParameters, HANDSHAKE, HANDSHAKE_RESPONSE, cancel);
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult sendHeader(const u8* rom, F cancel) {
|
|
u16* headerOut = (u16*)rom;
|
|
|
|
for (int i = 0; i < HEADER_SIZE; i += 2) {
|
|
linkRawCable->transfer(*(headerOut++), cancel);
|
|
if (cancel())
|
|
return ABORTED;
|
|
}
|
|
|
|
return FINISHED;
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult sendPalette(Link::_MultiBootParam& multiBootParameters,
|
|
F cancel) {
|
|
auto data = SEND_PALETTE | PALETTE_DATA;
|
|
|
|
auto response = linkRawCable->transfer(data, cancel);
|
|
if (cancel())
|
|
return ABORTED;
|
|
|
|
for (u32 i = 0; i < CLIENTS; i++) {
|
|
if (response.data[1 + i] >> 8 == ACK_RESPONSE)
|
|
multiBootParameters.client_data[i] = response.data[1 + i] & 0xff;
|
|
}
|
|
|
|
for (u32 i = 0; i < CLIENTS; i++) {
|
|
u8 clientId = CLIENT_IDS[i];
|
|
bool isClientConnected = multiBootParameters.client_bit & clientId;
|
|
|
|
if (isClientConnected &&
|
|
multiBootParameters.client_data[i] == CLIENT_NO_DATA)
|
|
return NEEDS_RETRY;
|
|
}
|
|
|
|
return FINISHED;
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult confirmHandshakeData(Link::_MultiBootParam& multiBootParameters,
|
|
F cancel) {
|
|
u16 data = CONFIRM_HANDSHAKE_DATA | multiBootParameters.handshake_data;
|
|
auto response = linkRawCable->transfer(data, cancel);
|
|
if (cancel())
|
|
return ABORTED;
|
|
|
|
return (response.data[1] >> 8) == ACK_RESPONSE ? FINISHED : ERROR;
|
|
}
|
|
|
|
template <typename F>
|
|
PartialResult compare(Link::_MultiBootParam& multiBootParameters,
|
|
u16 data,
|
|
u16 expectedResponse,
|
|
F cancel) {
|
|
auto response = linkRawCable->transfer(data, cancel);
|
|
if (cancel())
|
|
return ABORTED;
|
|
|
|
for (u32 i = 0; i < CLIENTS; i++) {
|
|
u8 clientId = CLIENT_IDS[i];
|
|
u16 expectedResponseWithId = expectedResponse | clientId;
|
|
bool isClientConnected = multiBootParameters.client_bit & clientId;
|
|
|
|
if (isClientConnected && response.data[1 + i] != expectedResponseWithId)
|
|
return ERROR;
|
|
}
|
|
|
|
return FINISHED;
|
|
}
|
|
|
|
Result error(Result error) {
|
|
linkRawCable->deactivate();
|
|
return error;
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
extern LinkCableMultiboot* linkCableMultiboot;
|
|
|
|
#endif // LINK_CABLE_MULTIBOOT_H
|