-
Notifications
You must be signed in to change notification settings - Fork 6
/
LinkCableMultiboot.hpp
390 lines (331 loc) · 12.6 KB
/
LinkCableMultiboot.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#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:
// - stop DMA before sending the ROM! (you might need to stop your audio player)
// --------------------------------------------------------------------------
#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif
#include "LinkRawCable.hpp"
#include "LinkSPI.hpp"
#ifndef LINK_CABLE_MULTIBOOT_PALETTE_DATA
/**
* @brief Palette data (controls how the logo is displayed).
* Format: 0b1CCCDSS1, where C=color, D=direction, S=speed.
* Default: 0b10010011
*/
#define LINK_CABLE_MULTIBOOT_PALETTE_DATA 0b10010011
#endif
static volatile char LINK_CABLE_MULTIBOOT_VERSION[] =
"LinkCableMultiboot/v7.0.1";
#define LINK_CABLE_MULTIBOOT_TRY(CALL) \
partialResult = CALL; \
if (partialResult == ABORTED) \
return error(CANCELED); \
else if (partialResult == NEEDS_RETRY) \
goto retry;
/**
* @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 FRAME_LINES = 228;
static constexpr int WAIT_BEFORE_RETRY = FRAME_LINES * 4;
static constexpr int DETECTION_TRIES = 16;
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 auto MAX_BAUD_RATE = LinkRawCable::BaudRate::BAUD_RATE_3;
static constexpr u8 CLIENT_IDS[] = {0b0010, 0b0100, 0b1000};
struct Response {
u32 data[LINK_RAW_CABLE_MAX_PLAYERS];
int playerId = -1; // (-1 = unknown)
};
public:
enum Result { SUCCESS, INVALID_SIZE, CANCELED, FAILURE_DURING_TRANSFER };
enum TransferMode {
SPI = 0,
MULTI_PLAY = 1
}; // (used in SWI call, do not swap)
/**
* @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.
* @param mode Either `TransferMode::MULTI_PLAY` for GBA cable (default value)
* or `TransferMode::SPI` for GBC cable.
* \warning Blocks the system until completion or cancellation.
*/
template <typename F>
Result sendRom(const u8* rom,
u32 romSize,
F cancel,
TransferMode mode = TransferMode::MULTI_PLAY) {
this->_mode = mode;
if (romSize < MIN_ROM_SIZE)
return INVALID_SIZE;
if (romSize > MAX_ROM_SIZE)
return INVALID_SIZE;
if ((romSize % 0x10) != 0)
return INVALID_SIZE;
retry:
deactivate();
// (*) instead of 1/16s, waiting a random number of frames works better
wait(WAIT_BEFORE_RETRY + FRAME_LINES * _qran_range(1, 30));
// 1. Prepare a "Multiboot Parameter Structure" in RAM.
PartialResult partialResult = NEEDS_RETRY;
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 = LINK_CABLE_MULTIBOOT_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(sendHeader(multiBootParameters, rom, cancel))
LINK_CABLE_MULTIBOOT_TRY(sendPalette(multiBootParameters, cancel))
LINK_CABLE_MULTIBOOT_TRY(confirmHandshakeData(multiBootParameters, cancel))
// 9. Call SWI 0x25, with r0 set to the address of the multiboot parameter
// structure and r1 set to the communication mode (0 for normal, 1 for
// MultiPlay).
int result = Link::_MultiBoot(&multiBootParameters, (int)_mode);
deactivate();
// 10. Upon return, r0 will be either 0 for success, or 1 for failure. If
// successful, all clients have received the multiboot program successfully
// and are now executing it - you can begin either further data transfer or
// a multiplayer game from here.
return result == 1 ? FAILURE_DURING_TRANSFER : SUCCESS;
}
~LinkCableMultiboot() {
delete linkRawCable;
delete linkSPI;
}
private:
LinkRawCable* linkRawCable = new LinkRawCable();
LinkSPI* linkSPI = new LinkSPI();
TransferMode _mode;
int randomSeed = 123;
enum PartialResult { NEEDS_RETRY, FINISHED, ABORTED };
struct Responses {
u16 d[CLIENTS];
};
template <typename F>
PartialResult detectClients(Link::_MultiBootParam& multiBootParameters,
F cancel) {
// 2. Initiate a multiplayer communication session, using either Normal mode
// for a single client or MultiPlay mode for multiple clients.
activate();
// 3. Send the word 0x6200 repeatedly until all detected clients respond
// with 0x720X, where X is their client number (1-3). If they fail to do
// this after 16 tries, delay 1/16s and go back to step 2. (*)
bool success = false;
for (u32 t = 0; t < DETECTION_TRIES; t++) {
auto response = transfer(HANDSHAKE, cancel);
if (cancel())
return ABORTED;
multiBootParameters.client_bit = 0;
success =
validateResponse(response, [&multiBootParameters](u32 i, u16 value) {
if ((value & 0xfff0) == HANDSHAKE_RESPONSE) {
auto clientId = value & 0xf;
if (clientId == CLIENT_IDS[i]) {
multiBootParameters.client_bit |= clientId;
return true;
}
}
return false;
});
if (success)
break;
}
if (!success)
return NEEDS_RETRY;
// 4. Fill in client_bit in the multiboot parameter structure (with
// bits 1-3 set according to which clients responded). Send the word
// 0x610Y, where Y is that same set of set bits.
transfer(CONFIRM_CLIENTS | multiBootParameters.client_bit, cancel);
return FINISHED;
}
template <typename F>
PartialResult sendHeader(Link::_MultiBootParam& multiBootParameters,
const u8* rom,
F cancel) {
// 5. Send the cartridge header, 16 bits at a time, in little endian order.
// After each 16-bit send, the clients will respond with 0xNN0X, where NN is
// the number of words remaining and X is the client number. (Note that if
// transferring in the single-client 32-bit mode, you still need to send
// only 16 bits at a time).
u16* headerOut = (u16*)rom;
u32 remaining = HEADER_SIZE / 2;
while (remaining > 0) {
auto response = transfer(*(headerOut++), cancel);
if (cancel())
return ABORTED;
bool success = validateResponse(response, [&remaining](u32 i, u16 value) {
u8 clientId = CLIENT_IDS[i];
u16 expectedValue = (remaining << 8) | clientId;
return value == expectedValue;
});
if (!success)
return NEEDS_RETRY;
remaining--;
}
// 6. Send 0x6200, followed by 0x620Y again.
transfer(HANDSHAKE, cancel);
if (cancel())
return ABORTED;
transfer(HANDSHAKE | multiBootParameters.client_bit, cancel);
if (cancel())
return ABORTED;
return FINISHED;
}
template <typename F>
PartialResult sendPalette(Link::_MultiBootParam& multiBootParameters,
F cancel) {
// 7. Send 0x63PP repeatedly, where PP is the palette_data you have picked
// earlier. Do this until the clients respond with 0x73CC, where CC is a
// random byte. Store these bytes in client_data in the parameter structure.
auto data = SEND_PALETTE | LINK_CABLE_MULTIBOOT_PALETTE_DATA;
bool success = false;
for (u32 i = 0; i < DETECTION_TRIES; i++) {
auto response = transfer(data, cancel);
if (cancel())
return ABORTED;
success =
validateResponse(response, [&multiBootParameters](u32 i, u16 value) {
if ((value >> 8) == ACK_RESPONSE) {
multiBootParameters.client_data[i] = value & 0xff;
return true;
}
return false;
});
if (success)
break;
}
if (!success)
return NEEDS_RETRY;
return FINISHED;
}
template <typename F>
PartialResult confirmHandshakeData(Link::_MultiBootParam& multiBootParameters,
F cancel) {
// 8. Calculate the handshake_data byte and store it in the parameter
// structure. This should be calculated as 0x11 + the sum of the three
// client_data bytes. Send 0x64HH, where HH is the handshake_data.
multiBootParameters.handshake_data =
(HANDSHAKE_DATA + multiBootParameters.client_data[0] +
multiBootParameters.client_data[1] +
multiBootParameters.client_data[2]) %
256;
u16 data = CONFIRM_HANDSHAKE_DATA | multiBootParameters.handshake_data;
auto response = transfer(data, cancel);
if (cancel())
return ABORTED;
return (response.data[1] >> 8) == ACK_RESPONSE ? FINISHED : NEEDS_RETRY;
}
template <typename F>
bool validateResponse(Response response, F check) {
u32 count = 0;
for (u32 i = 0; i < CLIENTS; i++) {
auto value = response.data[1 + i];
if (value == LINK_RAW_CABLE_DISCONNECTED) {
// Note that throughout this process, any clients that are not
// connected will always respond with 0xFFFF - be sure to ignore them.
continue;
}
if (!check(i, value))
return false;
count++;
}
return count > 0;
}
template <typename F>
Response transfer(u32 data, F cancel) {
if (_mode == TransferMode::MULTI_PLAY) {
Response response;
auto response16bit = linkRawCable->transfer(data, cancel);
for (u32 i = 0; i < LINK_RAW_CABLE_MAX_PLAYERS; i++)
response.data[i] = response16bit.data[i];
response.playerId = response16bit.playerId;
return response;
} else {
Response response = {
.data = {LINK_RAW_CABLE_DISCONNECTED, LINK_RAW_CABLE_DISCONNECTED,
LINK_RAW_CABLE_DISCONNECTED, LINK_RAW_CABLE_DISCONNECTED}};
response.data[1] = linkSPI->transfer(data, cancel) >> 16;
response.playerId = 0;
return response;
}
}
public:
void activate() {
if (_mode == TransferMode::MULTI_PLAY)
linkRawCable->activate(MAX_BAUD_RATE);
else
linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS);
}
void deactivate() {
if (_mode == TransferMode::MULTI_PLAY)
linkRawCable->deactivate();
else
linkSPI->deactivate();
}
Result error(Result error) {
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;
}
};
}
int _qran() {
randomSeed = 1664525 * randomSeed + 1013904223;
return (randomSeed >> 16) & 0x7FFF;
}
int _qran_range(int min, int max) {
return (_qran() * (max - min) >> 15) + min;
}
};
extern LinkCableMultiboot* linkCableMultiboot;
#endif // LINK_CABLE_MULTIBOOT_H