diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6d360c1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index 2815716..567aff5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ # Directories -build -.vscode +build/ +.vscode/ +node_modules/ # Files +examples/multiboot +.DS_Store *.elf *.gba *.sav *.sa1 *.sa2 *.sa3 -*.sa4 \ No newline at end of file +*.sa4 diff --git a/.licenses/gba-hpp.md b/.licenses/gba-hpp.md new file mode 100644 index 0000000..22bdd64 --- /dev/null +++ b/.licenses/gba-hpp.md @@ -0,0 +1,17 @@ +gba-hpp is available under the [zlib license](https://www.zlib.net/zlib_license.html) : + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/README.md b/README.md index 3677315..3443dc7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gba-link-connection -A set of Game Boy Advance (GBA) C++ libraries to interact with the Serial Port. Its main purpose is to provide multiplayer support to homebrew games. +A set of Game Boy Advance (GBA) C++ libraries to interact with the Serial Port. Its main purpose is to provide multiplayer support to homebrew games. [C bindings](#c-bindings) are also included for compatibility. - [👾](#-LinkCable) [LinkCable.hpp](lib/LinkCable.hpp): The classic 16-bit **Multi-Play mode** (up to 4 players) using a GBA Link Cable! - [💻](#-LinkCableMultiboot) [LinkCableMultiboot.hpp](lib/LinkCableMultiboot.hpp): ‍Send **Multiboot software** (small 256KiB ROMs) to other GBAs with no cartridge! @@ -13,6 +13,8 @@ A set of Game Boy Advance (GBA) C++ libraries to interact with the Serial Port. - [🔌](#-LinkGPIO) [LinkGPIO.hpp](lib/LinkGPIO.hpp): Use the Link Port however you want to control **any device** (like LEDs, rumble motors, and that kind of stuff)! - [🔗](#-LinkSPI) [LinkSPI.hpp](lib/LinkSPI.hpp): Connect with a PC (like a **Raspberry Pi**) or another GBA (with a GBC Link Cable) using this mode. Transfer up to 2Mbit/s! - [⏱️](#%EF%B8%8F-LinkUART) [LinkUART.hpp](lib/LinkUART.hpp): Easily connect to **any PC** using a USB to UART cable! +- [🟪](#-LinkCube) [LinkCube.hpp](lib/LinkCube.hpp): Exchange data with a *Wii* or a *GameCube* using the classic **Joybus** protocol! +- [📱](#-LinkMobile) [LinkMobile.hpp](lib/LinkMobile.hpp): Connect to **the internet** using the *Mobile Adapter GB*, brought back to life thanks to the [REON](https://github.com/REONTeam) project! - [🖱️](#%EF%B8%8F-LinkPS2Mouse) [LinkPS2Mouse.hpp](lib/LinkPS2Mouse.hpp): Connect a **PS/2 mouse** to the GBA for extended controls! - [⌨️](#%EF%B8%8F-LinkPS2Keyboard) [LinkPS2Keyboard.hpp](lib/LinkPS2Keyboard.hpp): Connect a **PS/2 keyboard** to the GBA for extended controls! @@ -24,20 +26,55 @@ A set of Game Boy Advance (GBA) C++ libraries to interact with the Serial Port. ## Usage -- Include the library you want (e.g. [LinkCable.hpp](lib/LinkCable.hpp)) in your game code, and refer to its comments for instructions. Most of these libraries are provided as single header files for simplicity. The only external dependency is **libtonc**, which comes preinstalled with *devkitPro*. -- Check out the [examples](examples) folder. - * Builds are available in [Releases](https://github.com/afska/gba-link-connection/releases). - * They can be tested on real GBAs or using emulators. - * For `LinkCable`/`LinkWireless`/`LinkUniversal` there are stress tests that you can use to tweak your configuration. +- Copy the contents of the [lib/](lib/) folder into a directory that is part of your project's include path. Then, `#include` the library you need, such as [LinkCable.hpp](lib/LinkCable.hpp), in your project. No external dependencies are required. +- For initial instructions and setup details, refer to the large comment block at the beginning of each file, the documentation included here, and the provided examples. +- Check out the [examples/](examples/) folder. + * **Compiled ROMs are available** in [Releases](https://github.com/afska/gba-link-connection/releases). + * The example code uses [libtonc](https://github.com/gbadev-org/libtonc) (and [libugba](https://github.com/AntonioND/libugba) for interrupts), but any library can be used. + * The examples can be tested on real GBAs or using emulators. + * For `LinkCable`/`LinkWireless`/`LinkUniversal`, stress tests are provided to help you tweak your configuration. The [LinkUniversal_real](https://github.com/afska/gba-link-universal-test) ROM tests a more real scenario using an audio player, a background video, text and sprites. + * The `LinkCableMultiboot_demo` and `LinkWirelessMultiboot_demo` examples can bootstrap all other examples, allowing you to test with multiple units even if you only have one flashcart. -To learn implementation details, you might also want to check out the [docs](docs) folder, which contains important documentation. +> The files use some compiler extensions, so using **GCC** is required. -### Makefile actions (for all examples) +> The example ROMs were compiled with [devkitPro](https://devkitpro.org), using GCC `14.1.0` with `-std=c++17` as the standard and `-Ofast` as the optimization level. + +> To learn implementation details, you might also want to check out the [docs/](docs/) folder, which contains important documentation. + +### Compiling the examples + +Running `./compile.sh` builds all the examples with the right configuration. + +The project must be in a path without spaces; devkitARM and some *nix commands are required. + +All the projects understand these Makefile actions: ```bash make [ clean | build | start | rebuild | restart ] ``` +### C bindings + +- To use the libraries in a C project, include the files from the [lib/c_bindings/](lib/c_bindings/) directory. +- For documentation, use this `README.md` file or comments inside the main C++ files. +- Some libraries may not be available in C. +- Some methods/overloads may not be available in the C implementations. +- Unlike the main libraries, C bindings depend on *libtonc*. + +```cpp +// Instantiating +LinkSomething* linkSomething = new LinkSomething(a, b); // C++ +LinkSomethingHandle cLinkSomething = C_LinkSomething_create(a, b); // C + +// Calling methods +linkSomething->method(a, b); // C++ +C_LinkSomething_method(cLinkSomething, a, b); // C + +// Destroying +delete linkSomething; // C++ +C_LinkSomething_destroy(cLinkSomething); // C +``` + # 👾 LinkCable *(aka Multi-Play Mode)* @@ -55,9 +92,8 @@ The library uses message queues to send/receive data and transmits when it's pos Name | Type | Default | Description --- | --- | --- | --- `baudRate` | **BaudRate** | `BAUD_RATE_1` | Sets a specific baud rate. -`timeout` | **u32** | `3` | Number of *frames* without an `II_SERIAL` IRQ to reset the connection. -`remoteTimeout` | **u32** | `5` | Number of *messages* with `0xFFFF` to mark a player as disconnected. -`interval` | **u16** | `50` | Number of *1024-cycle ticks* (61.04μs) between transfers *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values will transfer faster but also consume more CPU. +`timeout` | **u32** | `3` | Maximum number of *frames* without receiving data from other player before marking them as disconnected or resetting the connection. +`interval` | **u16** | `50` | Number of *1024-cycle ticks* (61.04μs) between transfers *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values will transfer faster but also consume more CPU. You can use `Link::perFrame(...)` to convert from *packets per frame* to *interval values*. `sendTimerId` | **u8** *(0~3)* | `3` | GBA Timer to use for sending. You can update these values at any time without creating a new instance: @@ -67,6 +103,9 @@ You can update these values at any time without creating a new instance: You can also change these compile-time constants: - `LINK_CABLE_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing messages the queues can store at max **per player**). The default value is `15`, which seems fine for most games. + - This affects how much memory is allocated. With the default value, it's around `390` bytes. There's a double-buffered pending queue (to avoid data races), `1` incoming queue and `1` outgoing queue. + - You can approximate the memory usage with: + - `(LINK_CABLE_QUEUE_SIZE * sizeof(u16) * LINK_CABLE_MAX_PLAYERS) * 3 + LINK_CABLE_QUEUE_SIZE * sizeof(u16)` <=> `LINK_CABLE_QUEUE_SIZE * 26` ## Methods @@ -77,13 +116,13 @@ Name | Return type | Description `deactivate()` | - | Deactivates the library. `isConnected()` | **bool** | Returns `true` if there are at least 2 connected players. `playerCount()` | **u8** *(0~4)* | Returns the number of connected players. -`currentPlayerId()` | **u8** *(0~3)* | Returns the current player id. +`currentPlayerId()` | **u8** *(0~3)* | Returns the current player ID. `sync()` | - | Call this method every time you need to fetch new data. `waitFor(playerId)` | **bool** | Waits for data from player #`playerId`. Returns `true` on success, or `false` on disconnection. `waitFor(playerId, cancel)` | **bool** | Like `waitFor(playerId)` but accepts a `cancel()` function. The library will continuously invoke it, and abort the wait if it returns `true`. `canRead(playerId)` | **bool** | Returns `true` if there are pending messages from player #`playerId`. Keep in mind that if this returns `false`, it will keep doing so until you *fetch new data* with `sync()`. -`read(playerId)` | **u16** | Dequeues and returns the next message from player #`playerId`. -`peek(playerId)` | **u16** | Returns the next message from player #`playerId` without dequeuing it. +`read(playerId)` | **u16** | Dequeues and returns the next message from player #`playerId`. If there's no data from that player, a `0` will be returned. +`peek(playerId)` | **u16** | Returns the next message from player #`playerId` without dequeuing it. If there's no data from that player, a `0` will be returned. `send(data)` | - | Sends `data` to all connected players. ⚠️ `0xFFFF` and `0x0` are reserved values, so don't send them! @@ -94,15 +133,22 @@ Name | Return type | Description This tool allows sending Multiboot ROMs (small 256KiB programs that fit in EWRAM) from one GBA to up to 3 slaves, using a single cartridge. +Its demo (`LinkCableMultiboot_demo`) has all the other gba-link-connection ROMs bundled with it, so it can be used to quickly test the library. + ![screenshot](https://github.com/afska/gba-link-connection/assets/1631752/6ff55944-5437-436f-bcc7-a89b05dc5486) +You can change these compile-time constants: +- `LINK_CABLE_MULTIBOOT_PALETTE_DATA`: to control how the logo is displayed. + - Format: `0b1CCCDSS1`, where `C`=color, `D`=direction, `S`=speed. + - Default: `0b10010011`. + ## Methods Name | Return type | Description --- | --- | --- -`sendRom(rom, romSize, cancel)` | **LinkCableMultiboot::Result** | Sends the `rom`. During the handshake process, the library will continuously invoke `cancel`, and abort the transfer if it returns `true`. The `romSize` must be a number between `448` and `262144`, and a multiple of `16`. Once completed, the return value should be `LinkCableMultiboot::Result::SUCCESS`. +`sendRom(rom, romSize, cancel, [mode])` | **LinkCableMultiboot::Result** | Sends the `rom`. During the handshake process, the library will continuously invoke `cancel`, and abort the transfer if it returns `true`. The `romSize` must be a number between `448` and `262144`, and a multiple of `16`. The `mode` can be either `LinkCableMultiboot::TransferMode::MULTI_PLAY` for GBA cable (default value) or `LinkCableMultiboot::TransferMode::SPI` for GBC cable. Once completed, the return value should be `LinkCableMultiboot::Result::SUCCESS`. -⚠️ 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)_ # 🔧👾 LinkRawCable @@ -119,14 +165,14 @@ Name | Return type | Description `isActive()` | **bool** | Returns whether the library is active or not. `activate(baudRate = BAUD_RATE_1)` | - | Activates the library in a specific `baudRate` (`LinkRawCable::BaudRate`). `deactivate()` | - | Deactivates the library. -`transfer(data)` | **LinkRawCable::Response** | Exchanges `data` with the connected consoles. Returns the received data, including the assigned player id. +`transfer(data)` | **LinkRawCable::Response** | Exchanges `data` with the connected consoles. Returns the received data, including the assigned player ID. `transfer(data, cancel)` | **LinkRawCable::Response** | Like `transfer(data)` but accepts a `cancel()` function. The library will continuously invoke it, and abort the transfer if it returns `true`. `transferAsync(data)` | - | 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! `getAsyncState()` | **LinkRawCable::AsyncState** | Returns the state of the last async transfer (one of `LinkRawCable::AsyncState::IDLE`, `LinkRawCable::AsyncState::WAITING`, or `LinkRawCable::AsyncState::READY`). -`getAsyncData()` | **LinkRawCable::Response** | If the async state is `READY`, returns the remote data and switches the state back to `IDLE`. +`getAsyncData()` | **LinkRawCable::Response** | If the async state is `READY`, returns the remote data and switches the state back to `IDLE`. If not, returns an empty response. +`getBaudRate()` | **LinkRawCable::BaudRate** | Returns the current `baudRate`. `isMaster()` | **bool** | Returns whether the console is connected as master or not. Returns garbage when the cable is not properly connected. `isReady()` | **bool** | Returns whether all connected consoles have entered the multiplayer mode. Returns garbage when the cable is not properly connected. -`getBaudRate()` | **LinkRawCable::BaudRate** | Returns the current `baudRate`. - don't send `0xFFFF`, it's a reserved value that means *disconnected client* - only `transfer(...)` if `isReady()` @@ -149,12 +195,10 @@ Name | Type | Default | Description --- | --- | --- | --- `forwarding` | **bool** | `true` | If `true`, the server forwards all messages to the clients. Otherwise, clients only see messages sent from the server (ignoring other peers). `retransmission` | **bool** | `true` | If `true`, the library handles retransmission for you, so there should be no packet loss. -`maxPlayers` | **u8** *(2~5)* | `5` | Maximum number of allowed players. The adapter will accept connections after reaching the limit, but the library will ignore them. If your game only supports -for example- two players, set this to `2` as it will make transfers faster. -`timeout` | **u32** | `10` | Number of *frames* without receiving *any* data to reset the connection. -`remoteTimeout` | **u32** | `10` | Number of *successful transfers* without a message from a client to mark the player as disconnected. -`interval` | **u16** | `50` | Number of *1024-cycle ticks* (61.04μs) between transfers *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values will transfer faster but also consume more CPU. +`maxPlayers` | **u8** *(2~5)* | `5` | Maximum number of allowed players. +`timeout` | **u32** | `10` | Maximum number of *frames* without receiving data from other player before resetting the connection. +`interval` | **u16** | `50` | Number of *1024-cycle ticks* (61.04μs) between transfers *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values will transfer faster but also consume more CPU. You can use `Link::perFrame(...)` to convert from *packets per frame* to *interval values*. `sendTimerId` | **u8** *(0~3)* | `3` | GBA Timer to use for sending. -`asyncACKTimerId` | **s8** *(0~3 or -1)* | `-1` | GBA Timer to use for ACKs. If you have free timers, use one here to reduce CPU usage. You can update these values at any time without creating a new instance: - Call `deactivate()`. @@ -163,35 +207,42 @@ You can update these values at any time without creating a new instance: You can also change these compile-time constants: - `LINK_WIRELESS_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing messages the queues can store at max). The default value is `30`, which seems fine for most games. + - This affects how much memory is allocated. With the default value, it's around `960` bytes. There's a double-buffered incoming queue and a double-buffered outgoing queue (to avoid data races). + - You can approximate the memory usage with: + - `LINK_WIRELESS_QUEUE_SIZE * sizeof(Message) * 4` <=> `LINK_WIRELESS_QUEUE_SIZE * 32` - `LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH` and `LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH`: to set the biggest allowed transfer per timer tick. Transfers contain retransmission headers and multiple user messages. These values must be in the range `[6;20]` for servers and `[2;4]` for clients. The default values are `20` and `4`, but you might want to set them a bit lower to reduce CPU usage. -- `LINK_WIRELESS_PUT_ISR_IN_IWRAM`: to put critical functions (~3.5KB) in IWRAM, which can significantly improve performance due to its faster access. This is disabled by default to conserve IWRAM space, which is limited, but it's enabled in demos to showcase its performance benefits. -- `LINK_WIRELESS_USE_SEND_RECEIVE_LATCH`: to alternate between sends and receives on each timer tick (instead of doing both things). This is disabled by default. Enabling it will introduce some latency but reduce overall CPU usage. +- `LINK_WIRELESS_PUT_ISR_IN_IWRAM`: to put critical functions in IWRAM, which can significantly improve performance due to its faster access. This is disabled by default to conserve IWRAM space, which is limited, but it's enabled in demos to showcase its performance benefits. + - If you enable this, make sure that `LinkWireless.cpp` gets compiled! For example, in a Makefile-based project, verify that the file is in your `SRCDIRS` list. +- `LINK_WIRELESS_ENABLE_NESTED_IRQ`: to allow `LINK_WIRELESS_ISR_*` functions to be interrupted. This can be useful, for example, if your audio engine requires calling a VBlank handler with precise timing. + - This won't produce any effect if `LINK_WIRELESS_PUT_ISR_IN_IWRAM` is disabled. +- `LINK_WIRELESS_USE_SEND_RECEIVE_LATCH`: to alternate between sends and receives on each timer tick (instead of doing both things). This is disabled by default. Enabling it will introduce a bit of latency but also reduce _a lot_ the overall CPU usage. It's enabled in the `LinkWireless_demo` example, but disabled in the `LinkUniversal_*` examples. +- `LINK_WIRELESS_TWO_PLAYERS_ONLY`: to optimize the library for two players. This will make the code smaller and use less CPU. It will also let you "misuse" `5` bits from the packet header to send small packets really fast (e.g. pressed keys) without confirmation, using the `QUICK_SEND` and `QUICK_RECEIVE` properties. When this option is enabled, the `maxPlayers` constructor parameter will be ignored. ## Methods - Most of these methods return a boolean, indicating if the action was successful. If not, you can call `getLastError()` to know the reason. Usually, unless it's a trivial error (like buffers being full), the connection with the adapter is reset and the game needs to start again. - You can check the connection state at any time with `getState()`. -- Until a session starts, all actions are synchronic. -- During sessions (when the state is `SERVING` or `CONNECTED`), the message transfers are IRQ-driven, so `send(...)` and `receive(...)` won't waste extra cycles. +- Until a session starts, all actions are synchronous. +- During sessions (when the state is `SERVING` or `CONNECTED`), the message transfers are IRQ-driven, so `send(...)` and `receive(...)` won't waste extra cycles. The only synchronous method that can be called during a session is `serve(...)`, which can be used to update the broadcast data. Name | Return type | Description --- | --- | --- `isActive()` | **bool** | Returns whether the library is active or not. `activate()` | **bool** | Activates the library. When an adapter is connected, it changes the state to `AUTHENTICATED`. It can also be used to disconnect or reset the adapter. -`deactivate()` | **bool** | Puts the adapter into a low consumption mode and then deactivates the library. It returns a boolean indicating whether the transition to low consumption mode was successful. -`serve([gameName], [userName], [gameId])` | **bool** | Starts broadcasting a server and changes the state to `SERVING`. You can, optionally, provide a `gameName` (max `14` characters), a `userName` (max `8` characters), and a `gameId` *(0 ~ 0x7FFF)* that games will be able to read. The strings must be null-terminated character arrays. If the adapter is already serving, this method only updates the broadcast data. +`deactivate()` | **bool** | Puts the adapter into a low consumption mode and then deactivates the library. It returns a boolean indicating whether the transition to low consumption mode was successful. You can disable the transition and deactivate directly by setting `turnOff` to `true`. +`serve([gameName], [userName], [gameId])` | **bool** | Starts broadcasting a server and changes the state to `SERVING`. You can, optionally, provide a `gameName` (max `14` characters), a `userName` (max `8` characters), and a `gameId` *(0 ~ 0x7FFF)* that games will be able to read. The strings must be null-terminated character arrays. If the adapter is already serving, this method only updates the broadcast data. Updating broadcast data while serving can fail if the adapter is busy. In that case, this will return `false` and `getLastError()` will be `BUSY_TRY_AGAIN`. `getServers(servers, [onWait])` | **bool** | Fills the `servers` array with all the currently broadcasting servers. This action takes 1 second to complete, but you can optionally provide an `onWait()` function which will be invoked each time VBlank starts. `getServersAsyncStart()` | **bool** | Starts looking for broadcasting servers and changes the state to `SEARCHING`. After this, call `getServersAsyncEnd(...)` 1 second later. `getServersAsyncEnd(servers)` | **bool** | Fills the `servers` array with all the currently broadcasting servers. Changes the state to `AUTHENTICATED` again. `connect(serverId)` | **bool** | Starts a connection with `serverId` and changes the state to `CONNECTING`. -`keepConnecting()` | **bool** | When connecting, this needs to be called until the state is `CONNECTED`. It assigns a player id. Keep in mind that `isConnected()` and `playerCount()` won't be updated until the first message from server arrives. +`keepConnecting()` | **bool** | When connecting, this needs to be called until the state is `CONNECTED`. It assigns a player ID. Keep in mind that `isConnected()` and `playerCount()` won't be updated until the first message from the server arrives. `send(data)` | **bool** | Enqueues `data` to be sent to other nodes. `receive(messages)` | **bool** | Fills the `messages` array with incoming messages, forwarding if needed. `getState()` | **LinkWireless::State** | Returns the current state (one of `LinkWireless::State::NEEDS_RESET`, `LinkWireless::State::AUTHENTICATED`, `LinkWireless::State::SEARCHING`, `LinkWireless::State::SERVING`, `LinkWireless::State::CONNECTING`, or `LinkWireless::State::CONNECTED`). -`isConnected()` | **bool** | Returns true if the player count is higher than 1. -`isSessionActive()` | **bool** | Returns true if the state is `SERVING` or `CONNECTED`. +`isConnected()` | **bool** | Returns `true` if the player count is higher than `1`. +`isSessionActive()` | **bool** | Returns `true` if the state is `SERVING` or `CONNECTED`. `playerCount()` | **u8** *(1~5)* | Returns the number of connected players. -`currentPlayerId()` | **u8** *(0~4)* | Returns the current player id. +`currentPlayerId()` | **u8** *(0~4)* | Returns the current player ID. `getLastError([clear])` | **LinkWireless::Error** | If one of the other methods returns `false`, you can inspect this to know the cause. After this call, the last error is cleared if `clear` is `true` (default behavior). ⚠️ `0xFFFF` is a reserved value, so don't send it! @@ -202,6 +253,8 @@ Name | Return type | Description This tool allows sending Multiboot ROMs (small 256KiB programs that fit in EWRAM) from one GBA to up to 4 slaves, wirelessly, using a single cartridge. +Its demo (`LinkWirelessMultiboot_demo`) has all the other gba-link-connection ROMs bundled with it, so it can be used to quickly test the library. + https://github.com/afska/gba-link-connection/assets/1631752/9a648bff-b14f-4a85-92d4-ccf366adce2d ## Methods @@ -220,7 +273,7 @@ Name | Return type | Description ## Methods -- There's one method for every supported wireless adapter command. +- There's one method for every supported Wireless Adapter command. - Use `sendCommand(...)` to send arbitrary commands. # 🔧🏛 LinkWirelessOpenSDK @@ -254,10 +307,11 @@ Name | Type | Default | Description `gameName` | **const char\*** | `""` | The game name that will be broadcasted in wireless sessions (max `14` characters). The string must be a null-terminated character array. The library uses this to only connect to servers from the same game. `cableOptions` | **LinkUniversal::CableOptions** | *same as LinkCable* | All the [👾 LinkCable](#-LinkCable) constructor parameters in one *struct*. `wirelessOptions` | **LinkUniversal::WirelessOptions** | *same as LinkWireless* | All the [📻 LinkWireless](#-LinkWireless) constructor parameters in one *struct*. +`randomSeed` | **int** | `123` | Random seed used for waits to prevent livelocks. If you use _libtonc_, pass `__qran_seed`. You can also change these compile-time constants: - `LINK_UNIVERSAL_MAX_PLAYERS`: to set a maximum number of players. The default value is `5`, but since LinkCable's limit is `4`, you might want to decrease it. -- `LINK_UNIVERSAL_GAME_ID_FILTER`: to restrict wireless connections to rooms with a specific game ID (`0x0000` - `0x7fff`). The default value (`0`) connects to any game ID and uses `0x7fff` when serving. +- `LINK_UNIVERSAL_GAME_ID_FILTER`: to restrict wireless connections to rooms with a specific game ID (`0x0000` ~ `0x7fff`). The default value (`0`) connects to any game ID and uses `0x7fff` when serving. ## Methods @@ -268,8 +322,8 @@ Name | Return type | Description `getState()` | **LinkUniversal::State** | Returns the current state (one of `LinkUniversal::State::INITIALIZING`, `LinkUniversal::State::WAITING`, or `LinkUniversal::State::CONNECTED`). `getMode()` | **LinkUniversal::Mode** | Returns the active mode (one of `LinkUniversal::Mode::LINK_CABLE`, or `LinkUniversal::Mode::LINK_WIRELESS`). `getProtocol()` | **LinkUniversal::Protocol** | Returns the active protocol (one of `LinkUniversal::Protocol::AUTODETECT`, `LinkUniversal::Protocol::CABLE`, `LinkUniversal::Protocol::WIRELESS_AUTO`, `LinkUniversal::Protocol::WIRELESS_SERVER`, or `LinkUniversal::Protocol::WIRELESS_CLIENT`). -`setProtocol(protocol)` | - | Sets the active `protocol`. `getWirelessState()` | **LinkWireless::State** | Returns the wireless state (same as [📻 LinkWireless](#-LinkWireless)'s `getState()`). +`setProtocol(protocol)` | - | Sets the active `protocol`. # 🔌 LinkGPIO @@ -298,7 +352,7 @@ Name | Return type | Description *(aka Normal Mode)* -This is the GBA's implementation of SPI. You can use this to interact with other GBAs or computers that know SPI. By default, it uses 32-bit packets, but you can switch to 8-bit by enabling the compile-time constant `LINK_SPI_8BIT_MODE`. +This is the GBA's implementation of SPI. You can use this to interact with other GBAs or computers that know SPI. ![screenshot](https://user-images.githubusercontent.com/1631752/213068614-875049f6-bb01-41b6-9e30-98c73cc69b25.png) @@ -307,26 +361,29 @@ This is the GBA's implementation of SPI. You can use this to interact with other Name | Return type | Description --- | --- | --- `isActive()` | **bool** | Returns whether the library is active or not. -`activate(mode)` | - | Activates the library in a specific `mode` (one of `LinkSPI::Mode::SLAVE`, `LinkSPI::Mode::MASTER_256KBPS`, or `LinkSPI::Mode::MASTER_2MBPS`). +`activate(mode, [dataSize])` | - | Activates the library in a specific `mode` (one of `LinkSPI::Mode::SLAVE`, `LinkSPI::Mode::MASTER_256KBPS`, or `LinkSPI::Mode::MASTER_2MBPS`). By default, the `dataSize` is 32-bit, but can be changed to `LinkSPI::DataSize::SIZE_8BIT`. `deactivate()` | - | Deactivates the library. `transfer(data)` | **u32** | Exchanges `data` with the other end. Returns the received data. `transfer(data, cancel)` | **u32** | Like `transfer(data)` but accepts a `cancel()` function. The library will continuously invoke it, and abort the transfer if it returns `true`. `transferAsync(data, [cancel])` | - | 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! `getAsyncState()` | **LinkSPI::AsyncState** | Returns the state of the last async transfer (one of `LinkSPI::AsyncState::IDLE`, `LinkSPI::AsyncState::WAITING`, or `LinkSPI::AsyncState::READY`). -`getAsyncData()` | **u32** | If the async state is `READY`, returns the remote data and switches the state back to `IDLE`. +`getAsyncData()` | **u32** | If the async state is `READY`, returns the remote data and switches the state back to `IDLE`. If not, returns an empty response. `getMode()` | **LinkSPI::Mode** | Returns the current `mode`. +`getDataSize()` | **LinkSPI::DataSize** | Returns the current `dataSize`. `setWaitModeActive(isActive)` | - | Enables or disables `waitMode` (*). `isWaitModeActive()` | **bool** | Returns whether `waitMode` (*) is active or not. -> (*) `waitMode`: The GBA adds an extra feature over SPI. When working as master, it can check whether the other terminal is ready to receive, and wait if it's not. 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. +> (*) `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. > > `waitMode` is disabled by default. +> +> `MISO` means `SO` on the slave side and `SI` on the master side. ⚠️ when using Normal Mode between two GBAs, use a GBC Link Cable! ⚠️ only use the 2Mbps mode with custom hardware (very short wires)! -⚠️ don't send `0xFFFFFFFF`, it's reserved for errors! +⚠️ returns `0xFFFFFFFF` (or `0xFF`) on misuse or cancelled transfers! ## SPI Configuration @@ -377,17 +434,110 @@ Name | Return type | Description ## UART Configuration -The GBA operates using 1 stop bit, but everything else can be configured. By default, the library uses `8N1`, which means 8-bit data and no parity bit. RTS/CTS is disabled by default. +The GBA operates using `1` stop bit, but everything else can be configured. By default, the library uses `8N1`, which means 8-bit data and no parity bit. RTS/CTS is disabled by default. ![diagram](https://github.com/afska/gba-link-connection/assets/1631752/a6a58f94-da24-4fd9-9603-9c7c9a493f93) -- Black wire (GND) -> GBA GND. -- Green wire (TX) -> GBA SI. -- White wire (RX) -> GBA SO. +- Black wire (`GND`) -> GBA `GND`. +- Green wire (`TX`) -> GBA `SI`. +- White wire (`RX`) -> GBA `SO`. + +# 🟪 LinkCube + +*(aka JOYBUS Mode)* + +This is the GBA's implementation of JOYBUS, in which users connect the console to a *GameCube* (or *Wii* with GC ports) using an official adapter. The library can be tested using *Dolphin/mGBA* and [gba-joybus-tester](https://github.com/afska/gba-joybus-tester). + +![screenshot](https://github.com/user-attachments/assets/93c11c9a-bdbf-4726-a070-895465739789) + +You can change these compile-time constants: +- `LINK_CUBE_QUEUE_SIZE`: to set a custom 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. + - 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. + - You can approximate the memory usage with: + - `LINK_CUBE_QUEUE_SIZE * sizeof(u32) * 3` <=> `LINK_CUBE_QUEUE_SIZE * 12` + +## Methods + +Name | Return type | Description +--- | --- | --- +`isActive()` | **bool** | Returns whether the library is active or not. +`activate()` | - | Activates the library. +`deactivate()` | - | Deactivates the library. +`wait()` | **bool** | Waits for data. Returns `true` on success, or `false` on JOYBUS reset. +`wait(cancel)` | **bool** | Like `wait()` but accepts a `cancel()` function. The library will invoke it after every SERIAL interrupt, and abort the wait if it returns `true`. +`canRead()` | **bool** | Returns `true` if there are pending received values to read. +`read()` | **u32** | Dequeues and returns the next received value. If there's no received data, a `0` will be returned. +`peek()` | **u32** | Returns the next received value without dequeuing it. If there's no received data, a `0` will be returned. +`send(data)` | - | Sends 32-bit `data`. If the other end asks for data at the same time you call this method, a `0x00000000` will be sent. +`pendingCount()` | **u32** | Returns the number of pending outgoing transfers. +`didReset([clear])` | **bool** | Returns whether a JOYBUS reset was requested or not. After this call, the reset flag is cleared if `clear` is `true` (default behavior). + +# 📱 LinkMobile + +*(aka Mobile Adapter GB)* + +This is a driver for an accessory that enables online conectivity on the GB and GBA. The protocol was reverse-engineered by the *REON Team*. + +The original accessory was sold in Japan only and using it nowadays is hard since it relies on old tech, but REON has created an open-source implementation called [libmobile](https://github.com/REONTeam/libmobile), as well as support for emulators and microcontrollers. + +It has two modes of operation: +- Direct call (P2P): Calling someone directly for a 2-player session, using the other person's IP address or the phone number provided by the [relay server](https://github.com/REONTeam/mobile-relay). +- ISP call (PPP): Calling an ISP number for internet access. In this mode, the adapter can open up to 2 TCP/UDP sockets and transfer arbitrary data. + +![screenshot](https://github.com/user-attachments/assets/fcc1488b-4955-4a1b-8ffa-dd660175a45c) + +## Constructor + +`new LinkMobile(...)` accepts these **optional** parameters: + +Name | Type | Default | Description +--- | --- | --- | --- +`timeout` | **u32** | `600` | Number of *frames* without completing a request to reset a connection. +`timerId` | **u8** *(0~3)* | `3` | GBA Timer to use for sending. + +You can update these values at any time without creating a new instance: +- Call `deactivate()`. +- Mutate the `config` property. +- Call `activate()`. + +You can also change these compile-time constants: +- `LINK_MOBILE_QUEUE_SIZE`: to set a custom request queue size (how many commands can be queued at the same time). The default value is `10`, which seems fine for most games. + - This affects how much memory is allocated. With the default value, it's around `3` KB. + +## Methods + +- All actions are asynchronous/nonblocking. That means, they will return `true` if nothing is awfully wrong, but the actual consequence will occur some frames later. You can call `getState()` at any time to know what it's doing. +- On fatal errors, the library will transition to a `NEEDS_RESET` state. In that case, you can call `getError()` to know more details on what happened, and then `activate()` to restart. +- When calling `deactivate()`, the adapter automatically turns itself off after `3` seconds of inactivity. However, to gracefully turn it off, it's recommended to call `shutdown()` first, wait until the state is `SHUTDOWN`, and then `deactivate()`. + +Name | Return type | Description +--- | --- | --- +`isActive()` | **bool** | Returns whether the library is active or not. +`activate()` | - | Activates the library. After some time, if an adapter is connected, the state will be changed to `SESSION_ACTIVE`. If not, the state will be `NEEDS_RESET`, and you can retrieve the error with `getError()`. +`deactivate()` | - | Deactivates the library, resetting the serial mode to GPIO. Calling `shutdown()` first is recommended, but the adapter will put itself in sleep mode after 3 seconds anyway. +`shutdown()` | **bool** | Gracefully shuts down the adapter, closing all connections. After some time, the state will be changed to `SHUTDOWN`, and only then it's safe to call `deactivate()`. +`call(phoneNumber)` | **bool** | Initiates a P2P connection with a `phoneNumber`. After some time, the state will be `CALL_ESTABLISHED` (or `ACTIVE_SESSION` if the connection fails or ends). In REON/libmobile the phone number can be a number assigned by the relay server, or a 12-digit IPv4 address (for example, `"127000000001"` would be `127.0.0.1`). +`callISP(password, loginId)` | **bool** | Calls the ISP number registered in the adapter configuration, or a default number if the adapter hasn't been configured. Then, performs a login operation using the provided `password` and `loginId`. After some time, the state will be `PPP_ACTIVE`. If `loginId` is empty and the adapter has been configured, it will use the one stored in the configuration. Both parameters are null-terminated strings (max `32` characters). +`dnsQuery(domainName, result)` | **bool** | Looks up the IPv4 address for a `domainName` (a null-terminated string, max `253` characters). It also accepts an ASCII IPv4 address, converting it into a 4-byte address instead of querying the DNS server. The `result` is a pointer to a `LinkMobile::DNSQuery` struct that will be filled with the result. When the request is completed, the `completed` field will be `true`. If an IP address was found, the `success` field will be `true` and the `ipv4` field can be read as a 4-byte address. +`openConnection(ip, port, type, result)` | **bool** | Opens a TCP/UDP (`type`) connection at the given `ip` (4-byte address) on the given `port`. The `result` is a pointer to a `LinkMobile::OpenConn` struct that will be filled with the result. When the request is completed, the `completed` field will be `true`. If the connection was successful, the `success` field will be `true` and the `connectionId` field can be used when calling the `transfer(...)` method. Only `2` connections can be opened at the same time. +`closeConnection(connectionId, type, result)` | **bool** | Closes an active TCP/UDP (`type`) connection. The `result` is a pointer to a `LinkMobile::CloseConn` struct that will be filled with the result. When the request is completed, the `completed` field will be `true`. If the connection was closed correctly, the `success` field will be `true`. +`transfer(dataToSend, result, [connectionId])` | **bool** | Requests a data transfer (up to `254` bytes) and responds the received data. The transfer can be done with the other node in a P2P connection, or with any open TCP/UDP connection if a PPP session is active. In the case of a TCP/UDP connection, the `connectionId` must be provided. The `result` is a pointer to a `LinkMobile::DataTransfer` struct that will be filled with the received data. It can also point to `dataToSend` to reuse the struct. When the request is completed, the `completed` field will be `true`. If the transfer was successful, the `success` field will be `true`. If not, you can assume that the connection was closed. +`waitFor(asyncRequest)` | **bool** | Waits for `asyncRequest` to be completed. Returns `true` if the request was completed && successful, and the adapter session is still alive. Otherwise, it returns `false`. The `asyncRequest` is a pointer to a `LinkMobile::DNSQuery`, `LinkMobile::OpenConn`, `LinkMobile::CloseConn`, or `LinkMobile::DataTransfer`. +`hangUp()` | **bool** | Hangs up the current P2P or PPP call. Closes all connections. +`readConfiguration(configurationData)` | **bool** | Retrieves the adapter configuration, and puts it in the `configurationData` struct. If the adapter has an active session, the data is already loaded, so it's instantaneous. +`getState()` | **LinkMobile::State** | Returns the current state (one of `LinkMobile::State::NEEDS_RESET`, `LinkMobile::State::PINGING`, `LinkMobile::State::WAITING_TO_START`, `LinkMobile::State::STARTING_SESSION`, `LinkMobile::State::ACTIVATING_SIO32`, `LinkMobile::State::WAITING_32BIT_SWITCH`, `LinkMobile::State::READING_CONFIGURATION`, `LinkMobile::State::SESSION_ACTIVE`, `LinkMobile::State::CALL_REQUESTED`, `LinkMobile::State::CALLING`, `LinkMobile::State::CALL_ESTABLISHED`, `LinkMobile::State::ISP_CALL_REQUESTED`, `LinkMobile::State::ISP_CALLING`, `LinkMobile::State::PPP_LOGIN`, `LinkMobile::State::PPP_ACTIVE`, `LinkMobile::State::SHUTDOWN_REQUESTED`, `LinkMobile::State::ENDING_SESSION`, `LinkMobile::State::WAITING_8BIT_SWITCH`, or `LinkMobile::State::SHUTDOWN`). +`getRole()` | **LinkMobile::Role** | Returns the current role in the P2P connection (one of `LinkMobile::Role::NO_P2P_CONNECTION`, `LinkMobile::Role::CALLER`, or `LinkMobile::Role::RECEIVER`). +`isConfigurationValid()` | **int** | Returns whether the adapter has been configured or not. Returns `1` = yes, `0` = no, `-1` = unknown (no session active). +`isConnectedP2P()` | **bool** | Returns `true` if a P2P call is established (the state is `CALL_ESTABLISHED`). +`isConnectedPPP()` | **bool** | Returns `true` if a PPP session is active (the state is `PPP_ACTIVE`). +`isSessionActive()` | **bool** | Returns `true` if the session is active. +`canShutdown()` | **bool** | Returns `true` if there's an active session and there's no previous shutdown requests. +`getDataSize()` | **LinkSPI::DataSize** | Returns the current operation mode (`LinkSPI::DataSize`). +`getError()` | **LinkMobile::Error** | Returns details about the last error that caused the connection to be aborted. # 🖱️ LinkPS2Mouse -A PS/2 mouse driver for the GBA. Use it to add mouse support to your homebrew games. +A PS/2 mouse driver for the GBA. Use it to add mouse support to your homebrew games. It's a straight port from [this library](https://github.com/kristopher/PS2-Mouse-Arduino). ![photo](https://github.com/afska/gba-link-connection/assets/1631752/6856ff0d-0f06-4a9d-8ded-280052e02b8d) @@ -402,7 +552,21 @@ Name | Return type | Description `isActive()` | **bool** | Returns whether the library is active or not. `activate()` | - | Activates the library. `deactivate()` | - | Deactivates the library. -`report(data[3])` | - | Fills the `data` int array with a report. The first int contains _clicks_ that you can check against the bitmasks `LINK_PS2_MOUSE_LEFT_CLICK`, `LINK_PS2_MOUSE_MIDDLE_CLICK`, and `LINK_PS2_MOUSE_RIGHT_CLICK`. The second int is the _X movement_, and the third int is the _Y movement_. +`report(data[3])` | - | Fills the `data` int array with a report. The first int contains *clicks* that you can check against the bitmasks `LINK_PS2_MOUSE_LEFT_CLICK`, `LINK_PS2_MOUSE_MIDDLE_CLICK`, and `LINK_PS2_MOUSE_RIGHT_CLICK`. The second int is the *X movement*, and the third int is the *Y movement*. + +⚠️ calling `activate()` or `report(...)` could freeze the system if nothing is connected: detecting timeouts using interrupts is the user's responsibility. + +## Pinout + +``` + ____________ +|PS/2 --- GBA| +|------------| +|CLOCK -> SI | +|DATA --> SO | +|VCC ---> VCC| +|GND ---> GND| +``` # ⌨️ LinkPS2Keyboard @@ -412,7 +576,7 @@ A PS/2 keyboard driver for the GBA. Use it to add keyboard support to your homeb ## Constructor -`new LinkPS2Keyboard(onEvent)`, where `onEvent` is a function pointer that will receive the scan codes (`u8`). You should check a PS/2 scan code list online, but there are some examples included like `LINK_PS2_KEYBOARD_KEY_ENTER` and `LINK_PS2_KEYBOARD_KEY_RELEASE`. +`new LinkPS2Keyboard(onEvent)`, where `onEvent` is a function pointer that will receive the scan codes (`u8`). You should check a PS/2 scan code list online, but most common keys/events are included in enums like `LINK_PS2_KEYBOARD_KEY::ENTER` and `LINK_PS2_KEYBOARD_EVENT::RELEASE`. ## Methods @@ -420,4 +584,16 @@ Name | Return type | Description --- | --- | --- `isActive()` | **bool** | Returns whether the library is active or not. `activate()` | - | Activates the library. -`deactivate()` | - | Deactivates the library. \ No newline at end of file +`deactivate()` | - | Deactivates the library. + +## Pinout + +``` + ____________ +|PS/2 --- GBA| +|------------| +|CLOCK -> SI | +|DATA --> SO | +|VCC ---> VCC| +|GND ---> GND| +``` diff --git a/docs/gbatek.md b/docs/gbatek.md index 9c6eacf..4e8d42c 100644 --- a/docs/gbatek.md +++ b/docs/gbatek.md @@ -4,8 +4,8 @@ The GBAs Serial Port may be used in various different communication modes. Normal mode may exchange data between two GBAs (or to transfer data from master GBA to several slave GBAs in one-way direction). Multi-player mode may exchange data between up to four GBAs. UART mode works much like a RS232 interface. JOY Bus mode uses a standardized Nintendo protocol. And General Purpose mode allows to mis-use the 'serial' port as bi-directional 4bit parallel port. -Note: The Nintendo DS does not include a Serial Port. - +Note: The Nintendo DS does not include a Serial Port. + - SIO Normal Mode - SIO Multi-Player Mode - SIO General-Purpose Mode @@ -13,12 +13,12 @@ Note: The Nintendo DS does not include a Serial Port. - GBA Wireless Adapter # SIO Normal Mode - + This mode is used to communicate between two units. Transfer rates of 256Kbit/s or 2Mbit/s can be selected, however, the fast 2Mbit/s is intended ONLY for special hardware expansions that are DIRECTLY connected to the GBA link port (ie. without a cable being located between the GBA and expansion hardware). In normal cases, always use 256Kbit/s transfer rate which provides stable results. -Transfer lengths of 8bit or 32bit may be used, the 8bit mode is the same as for older DMG/CGB gameboys, however, the voltages for "GBA cartridges in GBAs" are different as for "DMG/CGB cartridges in DMG/CGB/GBAs", ie. it is not possible to communicate between DMG/CGB games and GBA games. +Transfer lengths of 8bit or 32bit may be used, the 8bit mode is the same as for older DMG/CGB gameboys, however, the voltages for "GBA cartridges in GBAs" are different as for "DMG/CGB cartridges in DMG/CGB/GBAs", ie. it is not possible to communicate between DMG/CGB games and GBA games. -**4000134h - RCNT (R) - Mode Selection, in Normal/Multiplayer/UART modes (R/W)** +**4000134h - RCNT (R) - Mode Selection, in Normal/Multiplayer/UART modes (R/W)** ``` Bit Expl. @@ -29,7 +29,7 @@ Transfer lengths of 8bit or 32bit may be used, the 8bit mode is the same as for 15 Must be zero (0) for Normal/Multiplayer/UART modes ``` -**4000128h - SIOCNT - SIO Control, usage in NORMAL Mode (R/W)** +**4000128h - SIOCNT - SIO Control, usage in NORMAL Mode (R/W)** ``` Bit Expl. @@ -46,23 +46,23 @@ Transfer lengths of 8bit or 32bit may be used, the 8bit mode is the same as for 15 Not used (Read only, always 0) ``` -The Start bit is automatically reset when the transfer completes, ie. when all 8 or 32 bits are transferred, at that time an IRQ may be generated. - -**400012Ah - SIODATA8 - SIO Normal Communication 8bit Data (R/W)** +The Start bit is automatically reset when the transfer completes, ie. when all 8 or 32 bits are transferred, at that time an IRQ may be generated. -For 8bit normal mode. Contains 8bit data (only lower 8bit are used). Outgoing data should be written to this register before starting the transfer. During transfer, transmitted bits are shifted-out (MSB first), and received bits are shifted-in simultaneously. Upon transfer completion, the register contains the received 8bit value. - -**4000120h - SIODATA32\_L - SIO Normal Communication lower 16bit data (R/W)** -**4000122h - SIODATA32\_H - SIO Normal Communication upper 16bit data (R/W)** +**400012Ah - SIODATA8 - SIO Normal Communication 8bit Data (R/W)** + +For 8bit normal mode. Contains 8bit data (only lower 8bit are used). Outgoing data should be written to this register before starting the transfer. During transfer, transmitted bits are shifted-out (MSB first), and received bits are shifted-in simultaneously. Upon transfer completion, the register contains the received 8bit value. + +**4000120h - SIODATA32_L - SIO Normal Communication lower 16bit data (R/W)** +**4000122h - SIODATA32_H - SIO Normal Communication upper 16bit data (R/W)** Same as above SIODATA8, for 32bit normal transfer mode respectively. -SIOCNT/RCNT must be set to 32bit normal mode writing to SIODATA32. - +SIOCNT/RCNT must be set to 32bit normal mode writing to SIODATA32. + **Initialization** First, initialize RCNT register. Second, set mode/clock bits in SIOCNT with startbit cleared. For master: select internal clock, and (in most cases) specify 256KHz as transfer rate. For slave: select external clock, the local transfer rate selection is then ignored, as the transfer rate is supplied by the remote GBA (or other computer, which might supply custom transfer rates). -Third, set the startbit in SIOCNT with mode/clock bits unchanged. - +Third, set the startbit in SIOCNT with mode/clock bits unchanged. + **Recommended Communication Procedure for SLAVE unit (external clock)** \- Initialize data which is to be sent to master. \- Set Start flag. @@ -71,41 +71,41 @@ Third, set the startbit in SIOCNT with mode/clock bits unchanged. \- Set SO to HIGH to indicate that we are not ready. \- Process received data. \- Repeat procedure if more data is to be transferred. -(or is so=high done automatically? would be fine - more stable - otherwise master may still need delay) - +(or is so=high done automatically? would be fine - more stable - otherwise master may still need delay) + **Recommended Communication Procedure for SLAVE unit (external clock)** \- Initialize data which is to be sent to master. \- Set Start=0 and SO=0 (SO=LOW indicates that slave is (almost) ready). -\- Set Start=1 and SO=1 (SO=HIGH indicates not ready, applied after transfer). +\- Set Start=1 and SO=1 (SO=HIGH indicates not ready, applied after transfer). -> (Expl. Old SO=LOW kept output until 1st clock bit received). +> (Expl. Old SO=LOW kept output until 1st clock bit received). -> (Expl. New SO=HIGH is automatically output at transfer completion). +> (Expl. New SO=HIGH is automatically output at transfer completion). \- Set SO to LOW to indicate that master may start now. \- Wait for IRQ (or for Start bit to become zero). (Check timeout here!) \- Process received data. -\- Repeat procedure if more data is to be transferred. - +\- Repeat procedure if more data is to be transferred. + **Recommended Communication Procedure for MASTER unit (internal clock)** \- Initialize data which is to be sent to slave. \- Wait for SI to become LOW (slave ready). (Check timeout here!) \- Set Start flag. \- Wait for IRQ (or for Start bit to become zero). \- Process received data. -\- Repeat procedure if more data is to be transferred. - +\- Repeat procedure if more data is to be transferred. + **Cable Protocol** During inactive transfer, the shift clock (SC) is high. The transmit (SO) and receive (SI) data lines may be manually controlled as described above. -When master sends SC=LOW, each master and slave must output the next outgoing data bit to SO. When master sends SC=HIGH, each master and slave must read out the opponents data bit from SI. This is repeated for each of the 8 or 32 bits, and when completed SC will be kept high again. - +When master sends SC=LOW, each master and slave must output the next outgoing data bit to SO. When master sends SC=HIGH, each master and slave must read out the opponents data bit from SI. This is repeated for each of the 8 or 32 bits, and when completed SC will be kept high again. + **Transfer Rates** Either 256KHz or 2MHz rates can be selected for SC, so max 32KBytes (256Kbit) or 128KBytes (2Mbit) can be transferred per second. However, the software must process each 8bit or 32bit of transmitted data separately, so the actual transfer rate will be reduced by the time spent on handling each data unit. -Only 256KHz provides stable results in most cases (such like when linking between two GBAs). The 2MHz rate is intended for special expansion hardware (with very short wires) only. - +Only 256KHz provides stable results in most cases (such like when linking between two GBAs). The 2MHz rate is intended for special expansion hardware (with very short wires) only. + **Using Normal mode for One-Way Multiplayer communication** When using normal mode with multiplay-cables, data isn't exchanged between first and second GBA as usually. Instead, data is shifted from first to last GBA (the first GBA receives zero, because master SI is shortcut to GND). -This behaviour may be used for fast ONE-WAY data transfer from master to all other GBAs. For example (3 GBAs linked): +This behaviour may be used for fast ONE-WAY data transfer from master to all other GBAs. For example (3 GBAs linked): ``` Step Sender 1st Recipient 2nd Recipient @@ -116,13 +116,13 @@ This behaviour may be used for fast ONE-WAY data transfer from master to all oth ``` The recipients should not output any own data, instead they should forward the previously received data to the next recipient during next transfer (just keep the incoming data unmodified in the data register). -Due to the delayed forwarding, 2nd recipient should ignore the first incoming data. After the last transfer, the sender must send one (or more) dummy data unit(s), so that the last data is forwarded to the 2nd (or further) recipient(s). +Due to the delayed forwarding, 2nd recipient should ignore the first incoming data. After the last transfer, the sender must send one (or more) dummy data unit(s), so that the last data is forwarded to the 2nd (or further) recipient(s). # SIO Multi-Player Mode - -Multi-Player mode can be used to communicate between up to 4 units. - -**4000134h - RCNT (R) - Mode Selection, in Normal/Multiplayer/UART modes (R/W)** + +Multi-Player mode can be used to communicate between up to 4 units. + +**4000134h - RCNT (R) - Mode Selection, in Normal/Multiplayer/UART modes (R/W)** ``` Bit Expl. @@ -133,9 +133,9 @@ Multi-Player mode can be used to communicate between up to 4 units. 15 Must be zero (0) for Normal/Multiplayer/UART modes ``` -Note: Even though undocumented, many Nintendo games are using Bit 0 to test current SC state in multiplay mode. - -**4000128h - SIOCNT - SIO Control, usage in MULTI-PLAYER Mode (R/W)** +Note: Even though undocumented, many Nintendo games are using Bit 0 to test current SC state in multiplay mode. + +**4000128h - SIOCNT - SIO Control, usage in MULTI-PLAYER Mode (R/W)** ``` Bit Expl. @@ -152,34 +152,34 @@ Note: Even though undocumented, many Nintendo games are using Bit 0 to test curr 15 Not used (Read only, always 0) ``` -The ID Bits are undefined until the first transfer has completed. - -**400012Ah - SIOMLT\_SEND - Data Send Register (R/W)** -Outgoing data (16 bit) which is to be sent to the other GBAs. - +The ID Bits are undefined until the first transfer has completed. + +**400012Ah - SIOMLT_SEND - Data Send Register (R/W)** +Outgoing data (16 bit) which is to be sent to the other GBAs. + **4000120h - SIOMULTI0 - SIO Multi-Player Data 0 (Parent) (R/W)** **4000122h - SIOMULTI1 - SIO Multi-Player Data 1 (1st child) (R/W)** **4000124h - SIOMULTI2 - SIO Multi-Player Data 2 (2nd child) (R/W)** **4000126h - SIOMULTI3 - SIO Multi-Player Data 3 (3rd child) (R/W)** These registers are automatically reset to FFFFh upon transfer start. -After transfer, these registers contain incoming data (16bit each) from all remote GBAs (if any / otherwise still FFFFh), as well as the local outgoing SIOMLT\_SEND data. -Ie. after the transfer, all connected GBAs will contain the same values in their SIOMULTI0-3 registers. - +After transfer, these registers contain incoming data (16bit each) from all remote GBAs (if any / otherwise still FFFFh), as well as the local outgoing SIOMLT_SEND data. +Ie. after the transfer, all connected GBAs will contain the same values in their SIOMULTI0-3 registers. + **Initialization** \- Initialize RCNT Bit 14-15 and SIOCNT Bit 12-13 to select Multi-Player mode. \- Read SIOCNT Bit 3 to verify that all GBAs are in Multi-Player mode. -\- Read SIOCNT Bit 2 to detect whether this is the Parent/Master unit. - +\- Read SIOCNT Bit 2 to detect whether this is the Parent/Master unit. + **Recommended Transmission Procedure** -\- Write outgoing data to SIODATA\_SEND. +\- Write outgoing data to SIODATA_SEND. \- Master must set Start bit. \- All units must process received data in SIOMULTI0-3 when transfer completed. \- After the first successful transfer, ID Bits in SIOCNT are valid. \- If more data is to be transferred, repeat procedure. The parent unit blindly sends data regardless of whether childs have already processed old data/supplied new data. So, parent unit might be required to insert delays between each transfer, and/or perform error checking. -Also, slave units may signalize that they are not ready by temporarily switching into another communication mode (which does not output SD High, as Multi-Player mode does during inactivity). - +Also, slave units may signalize that they are not ready by temporarily switching into another communication mode (which does not output SD High, as Multi-Player mode does during inactivity). + **Transfer Protocol** Beginning \- The masters SI pin is always LOW. @@ -211,15 +211,15 @@ Step D Transfer end \- Master sets SC=HIGH, all GBAs set SO=HIGH. \- The Start/Busy bits of all GBAs are automatically cleared. -\- Interrupts are requested in all GBAs (as far as enabled). - +\- Interrupts are requested in all GBAs (as far as enabled). + **Error Bit** This bit is set when a slave did not receive SI=LOW even though SC=LOW signalized a transfer (this might happen when connecting more than 4 GBAs, or when the previous child is not connected). Also, the bit is set when a Stopbit wasn't HIGH. The error bit may be undefined during active transfer - read only after transfer completion (the transfer continues and completes as normal even if errors have occurred for some or all GBAs). -Don't know: The bit is automatically reset/initialized with each transfer, or must be manually reset? - +Don't know: The bit is automatically reset/initialized with each transfer, or must be manually reset? + **Transmission Time** -The transmission time depends on the selected Baud rate. And on the amount of Bits (16 data bits plus start/stop bits for each GBA), delays between data for each GBA, plus final timeout (if less than 4 GBAs). That is, depending on the number of connected GBAs: +The transmission time depends on the selected Baud rate. And on the amount of Bits (16 data bits plus start/stop bits for each GBA), delays between data for each GBA, plus final timeout (if less than 4 GBAs). That is, depending on the number of connected GBAs: ``` GBAs Bits Delays Timeout @@ -230,17 +230,17 @@ The transmission time depends on the selected Baud rate. And on the amount of Bi ``` (The average Delay and Timeout periods are unknown?) -Above is not counting the additional CPU time that must be spent on initiating and processing each transfer. - +Above is not counting the additional CPU time that must be spent on initiating and processing each transfer. + **Fast One-Way Transmission** -Beside for the actual SIO Multiplayer mode, you can also use SIO Normal mode for fast one-way data transfer from Master unit to all Child unit(s). See chapter about SIO Normal mode for details. - +Beside for the actual SIO Multiplayer mode, you can also use SIO Normal mode for fast one-way data transfer from Master unit to all Child unit(s). See chapter about SIO Normal mode for details. + # SIO General-Purpose Mode -In this mode, the SIO is 'misused' as a 4bit bi-directional parallel port, each of the SI,SO,SC,SD pins may be directly controlled, each can be separately declared as input (with internal pull-up) or as output signal. - +In this mode, the SIO is 'misused' as a 4bit bi-directional parallel port, each of the SI,SO,SC,SD pins may be directly controlled, each can be separately declared as input (with internal pull-up) or as output signal. + **4000134h - RCNT (R) - SIO Mode, usage in GENERAL-PURPOSE Mode (R/W)** -Interrupts can be requested when SI changes from HIGH to LOW, as General Purpose mode does not require a serial shift clock, this interrupt may be produced even when the GBA is in Stop (low power standby) state. +Interrupts can be requested when SI changes from HIGH to LOW, as General Purpose mode does not require a serial shift clock, this interrupt may be produced even when the GBA is in Stop (low power standby) state. ``` Bit Expl. @@ -258,14 +258,14 @@ Interrupts can be requested when SI changes from HIGH to LOW, as General Purpose 15 Must be "1" for General-Purpose or JOYBUS Mode ``` -SI should be always used as Input to avoid problems with other hardware which does not expect data to be output there. - +SI should be always used as Input to avoid problems with other hardware which does not expect data to be output there. + **4000128h - SIOCNT - SIO Control, not used in GENERAL-PURPOSE Mode** -This register is not used in general purpose mode. That is, the separate bits of SIOCNT still exist and are read- and/or write-able in the same manner as for Normal, Multiplay, or UART mode (depending on SIOCNT Bit 12,13), but are having no effect on data being output to the link port. - +This register is not used in general purpose mode. That is, the separate bits of SIOCNT still exist and are read- and/or write-able in the same manner as for Normal, Multiplay, or UART mode (depending on SIOCNT Bit 12,13), but are having no effect on data being output to the link port. + # SIO Control Registers Summary - -**Mode Selection (by RCNT.15-14 and SIOCNT.13-12)** + +**Mode Selection (by RCNT.15-14 and SIOCNT.13-12)** ``` R.15 R.14 S.13 S.12 Mode @@ -276,8 +276,8 @@ This register is not used in general purpose mode. That is, the separate bits of 1 0 x x General Purpose 1 1 x x JOY BUS ``` - -**SIOCNT** + +**SIOCNT** ``` Bit 0 1 2 3 4 5 6 7 8 9 10 11 @@ -287,16 +287,17 @@ This register is not used in general purpose mode. That is, the separate bits of ``` # GBA Wireless Adapter - -**GBA Wireless Adapter (AGB-015 or OXY-004)** + +**GBA Wireless Adapter (AGB-015 or OXY-004)** + - GBA Wireless Adapter Games - GBA Wireless Adapter Login - GBA Wireless Adapter Commands - GBA Wireless Adapter Component Lists - + ## GBA Wireless Adapter Games -**GBA Wireless Adapter compatible Games** +**GBA Wireless Adapter compatible Games** ``` bit Generations series (Japan only) @@ -341,11 +342,11 @@ This register is not used in general purpose mode. That is, the separate bits of Sennen Kazoku (Japan only) Shrek SuperSlam Sonic Advance 3 -``` +``` ## GBA Wireless Adapter Login -**GBA Wireless Adapter Login** +**GBA Wireless Adapter Login** ``` rcnt=8000h ;\\ @@ -373,8 +374,8 @@ This register is not used in general purpose mode. That is, the separate bits of ret @@key\_string db 'NINTENDO',01h,80h ;10 bytes (5 halfwords; index=0..4) ``` - -**Data exchanged during Login** + +**Data exchanged during Login** ``` GBA ADAPTER @@ -394,11 +395,11 @@ This register is not used in general purpose mode. That is, the separate bits of \\ \\ MSBs=Inverse of MSBs=Own Prev.Data.From.Adapter Data.From.Adapter -``` +``` # GBA Wireless Adapter Commands - -**Wireless Command/Parameter Transmission** + +**Wireless Command/Parameter Transmission** ``` GBA Adapter @@ -412,7 +413,7 @@ This register is not used in general purpose mode. That is, the separate bits of ... ... ;/ ``` -Wireless 32bit Transfers +Wireless 32bit Transfers ``` wait until \[4000128h\].Bit2=0 ;want SI=0 @@ -421,9 +422,9 @@ Wireless 32bit Transfers set \[4000128h\].Bit3=0,Bit7=1 ;set SO=0 and start 32bit transfer ``` -All command/param/reply transfers should be done at Internal Clock (except, Response Words for command 25h,27h,35h,37h should use External Clock). - -**Wireless Commands** +All command/param/reply transfers should be done at Internal Clock (except, Response Words for command 25h,27h,35h,37h should use External Clock). + +**Wireless Commands** ``` Cmd Para Reply Name @@ -444,7 +445,7 @@ All command/param/reply transfers should be done at Internal Clock (except, Resp 1Eh - NN Get Directory? (receive list of game/user names?) 1Fh 1 - Select Game for Download (send 16bit Game\_ID) - + 20h - 1 21h - 1 Good/Bad response to cmd 1Fh ? @@ -463,7 +464,7 @@ All command/param/reply transfers should be done at Internal Clock (except, Resp 2Eh 2Fh - + 30h 1 - 31h @@ -483,11 +484,11 @@ All command/param/reply transfers should be done at Internal Clock (except, Resp 3Fh ``` -Special Response 996601EEh for error or so? (only at software side?) - +Special Response 996601EEh for error or so? (only at software side?) + ## GBA Wireless Adapter Component Lists - -Main Chipset + +Main Chipset ``` U1 32pin Freescale MC13190 (2.4 GHz ISM band transceiver) @@ -497,9 +498,9 @@ Main Chipset The MC13190 is a Short-Range, Low-Power 2.4 GHz ISM band transceiver. The processor is Motorola's 32-bit M-Core RISC engine. (?) MCT3000 (?) -See also: [http://www.eetimes.com/document.asp?doc\_id=1271943](http://www.eetimes.com/document.asp?doc_id=1271943) - -Version with GERMAN Postal Code on sticker: +See also: [http://www.eetimes.com/document.asp?doc_id=1271943](http://www.eetimes.com/document.asp?doc_id=1271943) + +Version with GERMAN Postal Code on sticker: ``` Sticker on Case: @@ -514,7 +515,7 @@ Version with GERMAN Postal Code on sticker: X3 2pin "D959L4I" (9.5MHz) (top side) (ca. 19 clks per 2us) ``` -Further components... top side (A-7) +Further components... top side (A-7) ``` D1 5pin "D6F, 44" (top side, below X3) @@ -531,7 +532,7 @@ Further components... top side (A-7) CN1 6pin connector to GBA link port (top side) ``` -Further components... bottom side (B-7) +Further components... bottom side (B-7) ``` U201 5pin "LXVB" (bottom side, near CN1) @@ -540,8 +541,8 @@ Further components... bottom side (B-7) B70 6pin "\[\]" (bottom side, near ANT, small white chip) ``` -Plus, resistors and capacitors (without any markings). - +Plus, resistors and capacitors (without any markings). + Version WITHOUT sticker: ``` @@ -554,7 +555,7 @@ Version WITHOUT sticker: X3 2pin "9.5SKSS4GT" (top side) ``` -Further components... top side (A-1) +Further components... top side (A-1) ``` D1 5pin "D6F, 31" (top side, below X3) @@ -570,7 +571,7 @@ Further components... top side (A-1) CN1 6pin connector to GBA link port (top side) ``` -Further components... bottom side (B-1) +Further components... bottom side (B-1) ``` U201 5pin "LXV2" (bottom side, near CN1) @@ -579,9 +580,9 @@ Further components... bottom side (B-1) B70 6pin "\[\]" (bottom side, near ANT, small white chip) ``` -Plus, resistors and capacitors (without any markings). - -Major Differences +Plus, resistors and capacitors (without any markings). + +Major Differences ``` Sticker "N/A" vs "Grossosteim P/AGB-A-WA-EUR-2 E3" @@ -591,4 +592,4 @@ Major Differences U70/U72 U70 "AAG" (6pin) vs U72 "BMs" (4pin) ``` -Purpose of the changes is unknown (either older/newer revisions, or different regions with different FCC regulations). \ No newline at end of file +Purpose of the changes is unknown (either older/newer revisions, or different regions with different FCC regulations). diff --git a/docs/img/link-cable-multiboot.gif b/docs/img/link-cable-multiboot.gif new file mode 100644 index 0000000..5892326 Binary files /dev/null and b/docs/img/link-cable-multiboot.gif differ diff --git a/docs/img/link-cable.png b/docs/img/link-cable.png new file mode 100644 index 0000000..9c14228 Binary files /dev/null and b/docs/img/link-cable.png differ diff --git a/docs/img/link-cube.gif b/docs/img/link-cube.gif new file mode 100644 index 0000000..8915791 Binary files /dev/null and b/docs/img/link-cube.gif differ diff --git a/docs/img/link-gpio.gif b/docs/img/link-gpio.gif new file mode 100644 index 0000000..956afcc Binary files /dev/null and b/docs/img/link-gpio.gif differ diff --git a/docs/img/link-mobile.gif b/docs/img/link-mobile.gif new file mode 100644 index 0000000..550794d Binary files /dev/null and b/docs/img/link-mobile.gif differ diff --git a/docs/img/link-raw-cable.gif b/docs/img/link-raw-cable.gif new file mode 100644 index 0000000..781a4bd Binary files /dev/null and b/docs/img/link-raw-cable.gif differ diff --git a/docs/img/link-raw-wireless.gif b/docs/img/link-raw-wireless.gif new file mode 100644 index 0000000..b244bc0 Binary files /dev/null and b/docs/img/link-raw-wireless.gif differ diff --git a/docs/img/link-spi.png b/docs/img/link-spi.png new file mode 100644 index 0000000..9e5c178 Binary files /dev/null and b/docs/img/link-spi.png differ diff --git a/docs/img/link-uart.gif b/docs/img/link-uart.gif new file mode 100644 index 0000000..7932f3a Binary files /dev/null and b/docs/img/link-uart.gif differ diff --git a/docs/img/pinout-raspberry-pi.png b/docs/img/pinout-raspberry-pi.png new file mode 100644 index 0000000..cc4429c Binary files /dev/null and b/docs/img/pinout-raspberry-pi.png differ diff --git a/docs/img/pinout-uart.png b/docs/img/pinout-uart.png new file mode 100644 index 0000000..99ea691 Binary files /dev/null and b/docs/img/pinout-uart.png differ diff --git a/docs/img/pinout.png b/docs/img/pinout.png new file mode 100644 index 0000000..b42e55d Binary files /dev/null and b/docs/img/pinout.png differ diff --git a/docs/img/0x10.png b/docs/img/wireless/0x10.png similarity index 100% rename from docs/img/0x10.png rename to docs/img/wireless/0x10.png diff --git a/docs/img/0x11.png b/docs/img/wireless/0x11.png similarity index 100% rename from docs/img/0x11.png rename to docs/img/wireless/0x11.png diff --git a/docs/img/0x16.png b/docs/img/wireless/0x16.png similarity index 100% rename from docs/img/0x16.png rename to docs/img/wireless/0x16.png diff --git a/docs/img/0x17.png b/docs/img/wireless/0x17.png similarity index 100% rename from docs/img/0x17.png rename to docs/img/wireless/0x17.png diff --git a/docs/img/0x1d.png b/docs/img/wireless/0x1d.png similarity index 100% rename from docs/img/0x1d.png rename to docs/img/wireless/0x1d.png diff --git a/docs/img/0x1f.png b/docs/img/wireless/0x1f.png similarity index 100% rename from docs/img/0x1f.png rename to docs/img/wireless/0x1f.png diff --git a/docs/img/0x20.png b/docs/img/wireless/0x20.png similarity index 100% rename from docs/img/0x20.png rename to docs/img/wireless/0x20.png diff --git a/docs/img/0x21.png b/docs/img/wireless/0x21.png similarity index 100% rename from docs/img/0x21.png rename to docs/img/wireless/0x21.png diff --git a/docs/img/0x25.png b/docs/img/wireless/0x25.png similarity index 100% rename from docs/img/0x25.png rename to docs/img/wireless/0x25.png diff --git a/docs/img/0x26.png b/docs/img/wireless/0x26.png similarity index 100% rename from docs/img/0x26.png rename to docs/img/wireless/0x26.png diff --git a/docs/img/0x27.png b/docs/img/wireless/0x27.png similarity index 100% rename from docs/img/0x27.png rename to docs/img/wireless/0x27.png diff --git a/docs/img/0x30.png b/docs/img/wireless/0x30.png similarity index 100% rename from docs/img/0x30.png rename to docs/img/wireless/0x30.png diff --git a/docs/img/broadcast.png b/docs/img/wireless/broadcast.png similarity index 100% rename from docs/img/broadcast.png rename to docs/img/wireless/broadcast.png diff --git a/docs/img/first_nintendo_32.png b/docs/img/wireless/first_nintendo_32.png similarity index 100% rename from docs/img/first_nintendo_32.png rename to docs/img/wireless/first_nintendo_32.png diff --git a/docs/img/first_single_u32.png b/docs/img/wireless/first_single_u32.png similarity index 100% rename from docs/img/first_single_u32.png rename to docs/img/wireless/first_single_u32.png diff --git a/docs/img/full_initialisation.png b/docs/img/wireless/full_initialisation.png similarity index 100% rename from docs/img/full_initialisation.png rename to docs/img/wireless/full_initialisation.png diff --git a/docs/img/init.png b/docs/img/wireless/init.png similarity index 100% rename from docs/img/init.png rename to docs/img/wireless/init.png diff --git a/docs/img/logic2.png b/docs/img/wireless/logic2.png similarity index 100% rename from docs/img/logic2.png rename to docs/img/wireless/logic2.png diff --git a/docs/img/multiboot.jpg b/docs/img/wireless/multiboot.jpg similarity index 100% rename from docs/img/multiboot.jpg rename to docs/img/wireless/multiboot.jpg diff --git a/docs/img/wake-up.png b/docs/img/wireless/wake-up.png similarity index 100% rename from docs/img/wake-up.png rename to docs/img/wireless/wake-up.png diff --git a/docs/img/wirelessadapter.jpg b/docs/img/wireless/wirelessadapter.jpg similarity index 100% rename from docs/img/wirelessadapter.jpg rename to docs/img/wireless/wirelessadapter.jpg diff --git a/docs/mobile_adapter.md b/docs/mobile_adapter.md new file mode 100644 index 0000000..710d92d --- /dev/null +++ b/docs/mobile_adapter.md @@ -0,0 +1,454 @@ +🌎 From: https://shonumi.github.io/dandocs.html#magb 🌎 + +# Mobile Adapter GB + +- [General Hardware Information](#mobile-adapter-gb--general-hardware-information) +- [Compatible Games](#mobile-adapter-gb--compatible-games) +- [Protocol - Packet Format](#mobile-adapter-gb--protocol---packet-format) +- [Protocol - Flow of Communication](#mobile-adapter-gb--protocol---flow-of-communication) +- [Protocol - Commands](#mobile-adapter-gb--protocol---commands) +- [Configuration Data](#mobile-adapter-gb--protocol---configuration-data) + +## \[Mobile Adapter GB\] : General Hardware Information + +The Mobile Adapter GB was an accessory designed to allow the Game Boy Color, and later the Game Boy Advance, to connect online via cellular networks in Japan. Released on January 27, 2001, it supported a limited number of games before service was shutdown on December 14, 2002. Many of the compatible games supported features such on mail clients, downloadable bonus content, player-versus-player modes, and even online tournaments. It represented Nintendo's first official attempt at online gaming for its handhelds. + +* The Mobile Adapter is a small device that essentially allows a Japanese phone to connect to the Game Boy's link port +* Model number is CGB-005 +* Officially released with 3 different versions of the Mobile Adapter. Each featured distinct colors to work with different type of phones +* Each Mobile Adapter came packaged with a cartridge called the Mobile Trainer to help configure and setup the device +* Servers were formally hosted at gameboy.datacenter.ne.jp + +Below, the Mobile Adapter variants are explained in further detail: + +Blue -> Used to connect PDC phones. +Yellow -> Used to connect cdmaOne phones. +Red -> Used to connect DDI phones. +Green -> Would have been used to connect PHS phones, but this version was never released. + +## \[Mobile Adapter GB\] : Compatible Games + +There are currently 22 known games that are compatible with the Mobile Adapter: + +Game Boy Color : 6 Total + +* Game Boy Wars 3 +* Hello Kitty: Happy House +* Mobile Golf +* Mobile Trainer +* Net de Get Minigames @ 100 +* Pocket Monsters Crystal Version + +Game Boy Advance : 16 Total + +* All-Japan GT Championship +* Daisenryaku For Game Boy Advance +* Doraemon: Midori no Wakusei Doki Doki Daikyuushuutsu! +* Exciting Bass +* EX Monopoly +* JGTO Licensed: Golfmaster Mobile +* Kinniku Banzuke ~Kongou-kun no Daibouken!~ +* Mail de Cute +* Mario Kart Advance +* Mobile Pro Baseball: Control Baton +* Monster Guardians +* Morita Shougi Advance +* Napoleon +* Play Novel: Silent Hill +* Starcom: Star Communicator +* Zero-Tours + +Two games were planned but later cancelled: **beatmaniaGB Net Jam** for the GBC and **Horse Racing Creating Derby** for the GBA. + +The GBA game Yu-Gi-Oh! Duel Monsters 5 Expert 1 contains code for the Mobile Adapter, but despite being built with the library it does not appear to use it. This functionality may have been planned and later abandoned. + +## \[Mobile Adapter GB\] : Protocol - Packet Format + +On the GBC, the Mobile Adapter operates using the fastest available setting (64KB/s) by setting Bits 0 and 1 of the SC register (0xFF02) high. It also uses an internal clock for all transfers. Communication is comparable to that of the Game Boy Printer, where the Game Boy sends packets with header, data, command, and checksum sections. On the GBA, the Mobile Adapter operates in NORMAL8 mode using a shift clock of 256KHz. Below is a chart breaking down the Mobile Adapter packet format used by the Game Boy or Mobile Adapter when acting as the sender. For response data sent by the receiver, refer to the next section. + +``` +------------------------------------------------- +Section | Length +------------------------------------------------- +Magic Bytes : 0x99 0x66 | 2 bytes +Packet Header | 4 bytes +Packet Data | 0-254 bytes +Packet Checksum | 2 bytes +Acknowledgement Signal | 2 bytes +------------------------------------------------- + + +------------------------------------------------- +Packet Header +------------------------------------------------- +Byte 1 | Command ID +Byte 2 | Unknown/Unused (0x00) +Byte 3 | High byte of Packet Data length +Byte 4 | Low byte of Packet Data length +------------------------------------------------- + + +------------------------------------------------- +Packet Data +------------------------------------------------- +Bytes 0-254 | Arbitrary data +------------------------------------------------- + + +------------------------------------------------- +Packet Checksum +------------------------------------------------- +Byte 1 | High byte of 16-bit sum +Byte 2 | Low byte of 16-bit sum +------------------------------------------------- + + +------------------------------------------------- +Acknowledgement Signal +------------------------------------------------- +Byte 1 | Device ID +Byte 2 | Command ID +------------------------------------------------- +``` + +The magic bytes are simply a pair of bytes used to identify the start of a Mobile Adapter packet. + +Packet Data is arbitrary data and varies in length and content. On the Game Boy Color, it has a maximum size of 254 bytes. This restriction may be applied via software and appears to come from the fact that the Packet Data and Packet Checksum are lumped together, thus their total lengths must not exceed 256 bytes. Attempting to send more than 254 bytes of packet data causes communications errors in all supported GBC games. Evidence suggests GBA games can use Bytes 3 and 4 of the Packet Header to specify Packet Data size (possibly up to 64KB). The Mobile Adapter discards any packets bigger than 255 bytes, effectively forcing the high byte of the packet data length to be 0. + +Data greater than the maximum packet length may be broken up into multiple packets, however. For example, when sending a large binary file such as an image or executable code, multiple packets are transferred from the Mobile Adapter to the Game Boy while the TCP transfer is ongoing. + +The Packet Checksum is simply the 16-bit sum of all previous header bytes and all previous packet data bytes. It does not include the magic bytes. The checksum is transmitted big-endian. + +After the checksum, a simple 2-byte Acknowledgement Signal is sent. The first byte is the Device ID OR'ed with the value 0x80. The second byte is 0x00 for the sender. The receiver transfers the Command ID from the Packet Header XOR'ed by 0x80. This essentially confirms what role the Game Boy is acting in. If it is the receiver, it is expecting to read information from the Packet Data from the Mobile Adapter. If it is the sender, it is pushing information from its own Packet Data to the Mobile Adapter. For example, with Command 0x19, the Game Boy is explicitly requesting data from the adapter, and with Command 0x1A the Game Boy is explicitly sending data to the adapter. + +The Command ID byte in the Acknowledgement Signal may also be used for the receiver to indicate an error. If the checksum verification fails, the receiving side will send error 0xF1. This causes the sender to immediately re-attempt sending the packet up to 4 times. If the command isn't implemented/supported by the receiving Mobile Adapter, error 0xF0 will be sent. Error 0xF2 indicates an internal error, such as the Mobile Adapter's TCP/telephone transfer buffer being full. + +The device ID determines what kind of hardware each side is communicating with. Below are the possible values and their meaning: + +``` +------------------------------------------------- +Device ID | OR Value | Device Type +------------------------------------------------- +0x00 | 0x80 | Game Boy Color +0x01 | 0x81 | Game Boy Advance +0x08 | 0x88 | PDC Mobile Adapter (Blue) +0x09 | 0x89 | cdmaOne Mobile Adapter (Yellow) +0x0A | 0x8A | PHS Mobile Adapter (Green) +0x0B | 0x8B | DDI Mobile Adapter (Red) +------------------------------------------------- +``` + +## \[Mobile Adapter GB\] : Protocol - Flow of Communication + +Even though the protocol effectively enables 2-way communication between the Game Boy and a remote server, the handheld is expected to oversee all transmissions to the adapter itself. That is to say, the typical "master-slave" model often used for Game Boy serial I/O still applies in some sense. Once the server starts responding, the Game Boy has to continually initiate another transfer to the adapter (setting Bit 7 of 0xFF02 high) to keep reading any additional bytes that were sent. + +It is up to the game software itself to handle secondary protocols (such as HTTP, POP3, or SMTP) which involve one side specifically acting as the sender or receiver. For example, after opening a TCP connection to an HTTP server and issuing the 0x15 command (Data Transfer), the software will determine whether the Game Boy is acting as a sender (making an HTTP request) or a receiver (receiving an HTTP response). Generally, this goes back and forth. The Game Boy sends information via its Packet Data, while the Mobile Adapter responds with 0xD2 "wait" bytes until the Game Boy finishes its TCP transfer. When the Game Boy's TCP transfer is done, the adapter sends any information from the server in its Packet Data while the Game Boy responds with 0x4B "wait" bytes. The chart below illustrates this concept and details what bytes are transferred by each side depending on their current role: + +Device | Role | Magic Bytes | Packet Header | Packet Checksum | Packet Data | Acknowledgement Signal +--- | --- | --- | --- | --- | --- | --- +Game Boy | Sender | 0x96 0x66 | Arbitrary | Arbitrary | Arbitrary | Device ID OR 0x80 + 0x00 +Mobile Adapter | Receiver | 0xD2 0xD2 | 0xD2 0xD2 ... | 0xD2 0xD2 ... ... ... | 0xD2 0xD2 ... | Device ID OR 0x80 + Command ID XOR 0x80 +--- | --- | --- | --- | --- | --- | --- +Game Boy | Receiver | 0x4B 0x4B | 0x4B 0x4B ... | 0x4B 0x4B ... ... ... | 0x4B 0x4B ... | Device ID OR 0x80 + Command ID XOR 0x80 +Mobile Adapter | Sender | 0x96 0x66 | Arbitrary | Arbitrary | Arbitrary | Device ID OR 0x80 + 0x00 +------------------------------------------------------------------------------------------------------------------------------------------------- + +When beginning communications with the Mobile Adapter, the Game Boy typically assumes the role of sender first. + +Many games appear to follow a certain order of commands initially. This may have been part of some kind of standard library available to developers in order to connect to an ISP. The commands most commonly look like this: + +``` +------------ +Command 0x10 Begin Session. First is perhaps to test the presence of the Mobile Adapter +Command 0x11 Close Session. +Command 0x10 Begin Session. Open session for configuration data +------------ +Command 0x19 Read Configuration Data. Grab first 96 bytes +Command 0x19 Read Configuration Data. Grab second 96 bytes +Command 0x11 Close Session. +Command 0x10 Begin Session. Open session to read configuration data again +Command 0x19 Read Configuration Data. Grab first 96 bytes +Command 0x19 Read Configuration Data. Grab second 96 bytes +------------ +Command 0x17 Check Telephone Status if not busy +Command 0x12 Dial Telephone. Should be the ISP's number stored in configuration data +Command 0x21 ISP Login +Command 0x28 DNS Query +------------ +``` + +From there, the software decides what next (if anything) needs to be done after successfully connecting to the internet. + +When the GBC or GBA first start communicating with the Mobile Adapter, the first byte sent in response will be garbage data. Sending this first byte causes the Mobile Adapter to exit sleep mode, and the GBC or GBA will then have to wait a short interval (around 100ms) before starting communications proper. If it doesn't, the Mobile Adapter might send more garbage. Afterwards, however, it will reply with 0xD2 as its "idle" byte until a command is finished being sent. The Mobile Adapter enters sleep mode after 3 seconds since the last serial byte was transmitted. This implicitly cancels the command currently being processed, closes all connections currently open and ends the session. + +## \[Mobile Adapter GB\] : Protocol - Commands + +**Command 0x0F - Empty** + +Data Sent: N/A. Empty Packet Data + +Doesn't incite a reply from the adapter at all, aside from the Acknowledgement Signal. Presumably used to ping the adapter, not seen in any games in the wild. + +**Command 0x10 - Begin Session** + +Data Sent: "NINTENDO" ASCII string. 8 bytes only, not null-terminated + +Data Received: "NINTENDO" ASCII string. 8 bytes only, not null-terminated + +Sent to the adapter at the beginning of a session. The Game Boy sends an ASCII string containing "NINTENDO" and the adapter replies with a packet containing the same data. It must be noted that the adapter will not respond to other commands until it receives this command. If this command is sent twice, it returns an error. + +**Command 0x11 - End Session** + +Data Sent: N/A. Empty Packet Data + +Data Received: N/A. Empty Packet Data + +Sent to the adapter at the end of a session. The Packet Data is empty, and the length is zero bytes. This command causes all connections to be closed, and the phone to be hung up. + +**Command 0x12 - Dial Telephone** + +Data Sent: 1 unknown byte + telephone number + +Data Received: N/A. Empty Packet Data + +Instructs the adapter to dial a telephone number. The first byte's purpose is unknown, but seems to vary depending on the adapter type. 0 is sent for the blue/PDC adapter, 1 is sent for the green/PHS or red/DDI adapters, and 2 for the yellow/cdmaOne adapter. For unknown reasons, the blue adapter also accepts 16, the red adapter also accepts 9, and the yellow adapter doesn't actually verify this value. The following data is the telephone number represented in ASCII values, consisting of decimal numbers "0" through "9", as well as "#" and "\*". Any ASCII values not within this range are ignored. The maximum length of the phone number is 32 bytes. + +**Command 0x13 - Hang Up Telephone** + +Data Sent: N/A. Empty Packet Data + +Data Received: N/A. Empty Packet Data + +Instructs the adapter to close a telephone connection. This implicitly disconnects any open TCP/UDP connections. The Packet Data is empty, and the length is zero bytes. + +**Command 0x14 - Wait For Telephone Call** + +Data Sent: N/A. Empty Packet Data + +Data Received: N/A. Empty Packet Data + +Instructs the adapter to wait for and pick up an incoming call. This returns inmediately if there is no call to pick up, with an error packet with code 0. The Packet Data is empty, and the length is zero bytes. + +**Command 0x15 - Transfer Data** + +Data Sent: Connection ID + Arbitrary Data (optional) + +Data Received: Connection ID + Arbitrary Data (optional) + +Used to transfer data over TCP after command 0x23 (Open TCP Connection), transfer data over UDP after command 0x25 (Open UDP connection), or transfer data over the phone line after either 0x12 (Dial Telephone) or 0x14 (Wait For Telephone Call) have successfully been called. Only TCP/UDP communication is possible after a 0x21 (ISP Login) command, and the first byte indicates the connection that's being transferred over, as multiple can be opened simultaneously. If it's a mobile connection, the first byte is ignored, usually being set to 0xFF. + +Generally, additional data is appended, although it is not required, such as when waiting for the server/other phone to send a reply. Large chunks of data greater than 254 bytes must be broken down into separate packets. While a connection is active, the Command ID in the Reply is 0x15 for the sender and 0x95 for the receiver. When a TCP connection is closed by the remote server (e.g. when an HTTP response has finished), and there's no leftover data to be received by the Game Boy, the Command ID in the Reply becomes 0x1F for the sender and 0x9F for the receiver, with a packet length of 0. Additionally, for TCP connections, if no data is sent, this command will wait for data to be received up to 1 second, before sending a reply. + +During a phone-to-phone communication, no disconnection is detected, instead being indicated by the 0x17 (Telephone Status) command. However, most games implement this instead through a timeout during which no data has been received. + +**Command 0x16 - Reset** + +Data Sent: N/A. Empty Packet Data + +Data Received: N/A. Empty Packet Data + +This command does the same as sending commands 0x11 (End Session), followed by 0x10 (Begin Session). Additionally, it resets SIO32 Mode to the default state. Presumably used to reset the adapter, though not seen in any games in the wild. + +**Command 0x17 - Telephone Status** + +Data Sent: N/A. Empty Packet Data + +Data Received: 3 bytes + +Typically sent to the adapter before dialing. Also used to test the telephone status before opening a TCP connection, or to constantly ping the phone to make sure it's still active. + +The reply is 3 bytes. The first byte indicates the phone's status, where 0xFF is returned if the phone is disconnected. If the phone isn't disconnected, bit 2 indicates whether the phone line is "busy" (i.e. in a call/picked up), and bit 0 indicates the presence of an incoming call (this remains if the incoming call is picked up). As such, the valid values are 0, 1, 4 and 5. Software may check bit 2 to know if the phone line is still connected. Most software doesn't seem to care about bit 0, but Net de Get: Mini Game @ 100 refuses to work with bit 0 set (value 5). + +The second byte is related to the adapter type, where the blue/PDC adapter returns 0x4D, and the red/DDI and yellow/cdmaOne adapters return 0x48, though the actual meaning is unknown. The third byte is unknown, and usually hardcoded to 0. However, Pokemon Crystal reacts to the third byte being 0xF0 by allowing the player to bypass the 10 min/day battle time limit. + +**Command 0x18 - SIO32 Mode** + +Data Sent: 1 byte + +Data Received: N/A. Empty Packet Data + +This command is generally sent after Command 0x10. It enables/disables SIO32 Mode, which is useful for GBA games to be able to send more data, faster. The sent byte must be 1 to enable SIO32 mode, and 0 to disable it. SIO32 mode implies that any transmission will happen in chunks of 4 bytes instead of 1, which has implications with respect to the alignment of the communication. + +When SIO32 mode is on, the packet data will be aligned to a multiple of 4 bytes, padding the remaining bytes with 0, and this won't be reflected in the packet length field. Similarly, the Acknowledgement Signal gains 2 padding bytes (hardcoded 0, not verified) at the end. Since the entire transmission (including Magic Bytes) is sent in chunks of 4, this means that the checksum is sent along with either the packet length (if length is 0) or the last 2 bytes of the packet (if length is not 0), and the acknowledgement signal is sent in the next chunk. + +SIO32 Mode will only be toggled after the reply to this packet has been sent. The adapter should be allowed at least 100ms to toggle, as it might otherwise start sending garbage. + +**Command 0x19 - Read Configuration Data** + +Data Sent: 1 byte offset + 1 byte read length + +Data Received: 1 byte offset + Requested Configuration Data + +Requests data from the adapter's 256-byte configuration memory. The first byte sent to the adapter is the offset. The second byte sent is the length of data to read. The adapter responds with the same offset byte followed by configuration data from the adapter's internal memory. The maximum amount of data that can be requested at once is 128 bytes, and the adapter may return an error if the game requests more data. Most software send 2 of these commands to read 96-byte chunks, for a total of 192 bytes, which is the area of this memory that is actually used. + +**Command 0x1A - Write Configuration Data** + +Data Sent: 1 byte offset + Configuration Data to Write + +Data Received: 1 byte offset + +Writes data to the adapter's 256-byte configuration memory. The first byte sent to the adapter is the offset. The following bytes are the data to be written in the adapters internal memory. A maximum of 128 bytes may be written at once. + +**Command 0x21 - ISP Login** + +Data Sent: 1 byte Login ID Length + Login ID + 1 byte Password Length + Password + 4 bytes DNS Address #1 + 4 bytes DNS Address #2 + +Data Received: 4 bytes assigned IP + 4 bytes assigned DNS Address #1 + 4 bytes assigned DNS Address #2 /p> + +Logs into the DION dial-up service, after calling it with command 0x12 (Dial Telephone), allowing the adapter to connect to the internet. Both the Login ID and Password are prefixed with bytes declaring their lengths, with a maximum length of 0x20. The IPv4 DNS addresses are 4 bytes each, with a single byte representing one octet. The reply contains the assigned IP address and DNS addresses. If the game sets either of the DNS addresses to 0, the adapter may assign the DNS address on its own, and return that in the reply, otherwise, the reply's DNS addresses are 0.0.0.0. + +**Command 0x22 - ISP Logout** + +Data Sent: N/A. Empty Packet Data + +Data Received: N/A. Empty Packet Data + +Logs out of the DION service. This command causes all connections to be closed. + +**Command 0x23 - Open TCP Connection** + +Data Sent: 4 bytes for IP Address + 2 Bytes for Port Number + +Data Received: 1 byte Connection ID + +Opens a TCP connection at the given IP address on the given port, after logging into the DION dial-up service. The IPv4 IP address is 4 bytes, with a single byte representing one octet. The port number is big-endian. Depending on which port the TCP connection opens (25, 80, 110), different protocols can be accessed on a server (SMTP, HTTP, and POP respectively). Handling the details of the protocol itself depends on the software and the server. The Mobile Adapter is merely responsible for opening the connection and handling TCP transfers such as when using Command 0x15. The reply contains the Connection ID, which must be used when using Command 0x15 (Transfer Data). The maximum amount of connections on a real adapter is 2. + +**Command 0x24 - Close TCP Connection** + +Data Sent: 1 byte Connection ID + +Data Received: 1 byte Connection ID + +Closes an active TCP connection. + +**Command 0x25 - Open UDP Connection** + +Data Sent: 4 bytes for IP Address + 2 Bytes for Port Number + +Data Received: 1 byte Connection ID + +Opens a UDP connection at the given IP address on the given port, after logging into the DION dial-up service. It's an analog of Command 0x23 (Open TCP Connection), but opens a UDP connection instead. This UDP connection is bound to the specified IP address and Port until it's closed. When using Command 0x15 (Transfer Data) with a UDP connection, it's impossible to know the sender of any received data, as it isn't verified. + +**Command 0x26 - Close UDP Connection** + +Data Sent: 1 byte Connection ID + +Data Received: 1 byte Connection ID + +Closes an active UDP connection. + +**Command 0x28 - DNS Query** + +Data Sent: Domain Name + +Data Received: 4 bytes for IP Address + +Looks up the IP address for a domain name, using the DNS server addresses sent in Command 0x21. This command also accepts an ASCII IPv4 address (as parsed by the inet\_addr(3) function of POSIX), converting it into a 4-byte IPv4 address instead of querying the DNS server. The domain name is in ASCII and may contain zeroes, which truncate the name. + +**Command 0x3F - Firmware Version** + +Data Sent: N/A. Empty Packet Data + +Data Received: N/A. Empty Packet Data + +On a real Mobile Adapter, this causes it to send firmware version information through the serial pins on the phone connector, and enter a state in which no other commands can be used. Presumably, this enters a test mode of some kind. Likely not used by any games. + +This command may not be used if the phone line is in use. The only way to resume sending commands after this one is sent, is sending Command 0x16 (Reset), or, exclusively on the blue adapter, Command 0x11 (End Session) may also be used. + +**Command 0x6E - Error Status** + +Data Sent: N/A. Adapter sends this in response to a failed command + +Data Received: 1 byte for command that failed + 1 byte for error status + +If a previously sent command fails, the adapter will respond with this instead, indicating the command that failed as well as a brief status code. The error statuses for one command do not indicate the same error for another command, so context matters when parsing the codes. The following commands and their known error status codes are listed below: + +``` +0x10: Error Code 0x01 - Sent twice +0x10: Error Code 0x02 - Invalid contents + +0x11: Error Code 0x02 - Still connected/failed to disconnect(?) + +0x12: Error Code 0x00 - Telephone line is busy +0x12: Error Code 0x01 - Invalid use (already connected) +0x12: Error Code 0x02 - Invalid contents (first byte isn't correct) +0x12: Error Code 0x03 - Communication failed/phone not connected +0x12: Error Code 0x04 - Call not established, redial + +0x13: Error Code 0x01 - Invalid use (already hung up/phone not connected) + +0x14: Error Code 0x00 - No call received/phone not connected +0x14: Error Code 0x01 - Invalid use (already calling) +0x14: Error Code 0x03 - Internal error (ringing but picking up fails) + +0x15: Error Code 0x00 - Invalid connection/communication failed +0x15: Error Code 0x01 - Invalid use (Call was ended/never made) + +0x16: Error Code 0x00 - Still connected/failed to disconnect(?) + +0x18: Error Code 0x02 - Invalid contents (first byte not either 1 or 0) + +0x19: Error Code 0x00 - Internal error (Failed to read config) +0x19: Error Code 0x02 - Read outside of config area/too big a chunk + +0x1A: Error Code 0x00 - Internal error (Failed to write config) +0x1A: Error Code 0x02 - Write outside of config area/too big a chunk + +0x21: Error Code 0x01 - Invalid use (Not in a call) +0x21: Error Code 0x02 - Unknown error (some kind of timeout?) +0x21: Error Code 0x03 - Unknown error (internal error?) + +0x22: Error Code 0x00 - Invalid use (Not logged in) +0x22: Error Code 0x01 - Invalid use (Not in a call) +0x22: Error Code 0x02 - Unknown error (some kind of timeout?) + +0x23: Error Code 0x00 - Too many connections +0x23: Error Code 0x01 - Invalid use (Not logged in) +0x23: Error Code 0x03 - Connection failed + +0x24: Error Code 0x00 - Invalid connection (Not connected) +0x24: Error Code 0x01 - Invalid use (Not logged in) +0x24: Error Code 0x02 - Unknown error (???) + +0x25: Error Code 0x00 - Too many connections +0x25: Error Code 0x01 - Invalid use (Not logged in) +0x25: Error Code 0x03 - Connection failed (though this can't happen) + +0x26: Error Code 0x00 - Invalid connection (Not connected) +0x26: Error Code 0x01 - Invalid use (Not logged in) +0x26: Error Code 0x02 - Unknown error (???) + +0x28: Error Code 0x01 - Invalid use (not logged in) +0x28: Error Code 0x02 - Invalid contents/lookup failed +``` + +## \[Mobile Adapter GB\] : Protocol - Configuration Data + +The Mobile Adapter has small area of built-in memory designed to store various settings for its configuration. It only uses 192 bytes but data is readable and writable via the Commands 0x19 and 0x1A respectively. These fields are filled out when running the initial setup on Mobile Trainer. The memory is laid out as describe below: + +``` +-------------------------- +0x00 - 0x01 :: "MA" in ASCII. The "Mobile Adapter" header. +0x02 :: Set to 0x1 during Mobile Trainer registration and 0x81 when registration is complete +0x04 - 0x07 :: Primary DNS server (210.196.3.183) +0x08 - 0x0B :: Secondary DNS server (210.141.112.163) +0x0C - 0x15 :: Login ID in the format gXXXXXXXXX. Mobile Trainer only allows 9 editable characters +0x2C - 0x43 :: User email address in the format XXXXXXXX@YYYY.dion.ne.jp +0x4A - 0x5D :: SMTP server in the format mail.XXXX.dion.ne.jp +0x5E - 0x70 :: POP server in the format pop.XXXX.dion.ne.jp +0x76 - 0x8D :: Configuration Slot #1 +0x8E - 0xA5 :: Configuration Slot #2 +0xA6 - 0xBD :: Configuration Slot #3 +0xBE - 0xBF :: 16-bit big-endian checksum +-------------------------- +``` + +Each configuration slot may contain an 8-byte telephone number to be used to connect to the ISP and a 16-byte ID string. The telephone number is stored in a variant of binary-coded decimal, where 0x0A represents the "#" key, 0x0B represents the "\*" key, and 0x0F marks the end of the telephone number. These slots may have been intended to allow users to connect online using ISPs besides DION at some point, however, Nintendo never implemented any such plans. + +If the Mobile Adapter is connected to a PDC or CDMA device, the telephone number defaults to #9677 with an ID string of "DION PDC/CDMAONE". If the Mobile Adapter is connected to a PHS or DDI device, the telephone number defaults to 0077487751 with an ID string of "DION DDI-POCKET". Only the first slot is configured by Mobile Trainer; it fills the rest with 0xFF and 0x00 bytes. An unidentified device (as reported by the Device ID in the Acknowledgement Signal of a packet) causes the Mobile Adapter to overwrite all configuration data with garbage values. + +The checksum is simply the 16-bit sum of bytes 0x00 - 0xBD. + +All software compatible with the Mobile Adapter appears to read the configuration data first and foremost. If the data cannot be read or if there is a problem with the data, they will refuse to even attempt logging in to the DION dial-up service. Generally, they return the error code 25-000 in that situation. + +If any compatible software attempts to read or write configuration data outside the allotted 256 bytes via commands 0x19 and 0x1A, the entire I/O operation is cancelled. No data is written even if the initial offset is within the 256 bytes. No data is returned either, as both commands respond with Error Status packets. diff --git a/docs/multiboot.md b/docs/multiboot.md index ece70b4..88e01ff 100644 --- a/docs/multiboot.md +++ b/docs/multiboot.md @@ -137,4 +137,4 @@
  • MultiPlay mode: https://www.problemkaputt.de/gbatek.htm#siomultiplayermode
  • - \ No newline at end of file + diff --git a/docs/wireless_adapter.md b/docs/wireless_adapter.md index 911f9b5..1283c9b 100644 --- a/docs/wireless_adapter.md +++ b/docs/wireless_adapter.md @@ -1,50 +1,45 @@ -Game Boy Advance Wireless Adapter -------------------------------------------------- +## Game Boy Advance Wireless Adapter - 🌎 **Original post**: https://blog.kuiper.dev/gba-wireless-adapter 🌎 - ✏️ **Updates**: [@davidgfnet](https://github.com/davidgfnet) and I were discovering new things and we added them here! > You can learn more details by reading [LinkRawWireless.hpp](../lib/LinkRawWireless.hpp)'s code. -The Wireless Adapter -==================== +# The Wireless Adapter -[![The Game Boy Advance Wireless Adapter](img/wirelessadapter.jpg)](img/wirelessadapter.jpg) +[![The Game Boy Advance Wireless Adapter](img/wireless/wirelessadapter.jpg)](img/wireless/wirelessadapter.jpg) -*The Game Boy Advance Wireless Adapter* +_The Game Boy Advance Wireless Adapter_ The wireless adapter is a piece of hardware that connects to the link cable port of a GBA that then communicates wirelessly with other adapters. It also contains a multibootable[1](#fn:multiboot) rom for playing games only one player has a copy of (although I am not aware of many games that use it, some NES classic games use this). However, the most notable games to use it is probably the Pokémon games Fire Red, Leaf Green and Emerald (Sapphire and Ruby do _not_ have wireless adapter support)[2](#fn:list_of_games). [![The multiboot rom from the wireless adapter showing a game title of AGB.RS and a -username of CORWIN](img/multiboot.jpg)](img/multiboot.jpg) +username of CORWIN](img/wireless/multiboot.jpg)](img/wireless/multiboot.jpg) -*You can make this screen display any game* +_You can make this screen display any game_ -Communicating with the adapter -============================== +# Communicating with the adapter When I started, I used the following resources to start being able to talk with the wireless adapter: -* [This Gist contains some details](wireless.txt) -* [GBATEK has a section on the wireless adapter](gbatek.md) +- [This Gist contains some details](wireless.txt) +- [GBATEK has a section on the wireless adapter](gbatek.md) -Pinout ------- +## Pinout The wireless adapter connects using the link cable port to the GBA. It uses -* 3.3V -* Serial In -* Serial out -* SD -* Clock -* Ground +- 3.3V +- Serial In +- Serial out +- SD +- Clock +- Ground which is all 6 of the pins. If you are going to mess with interfacing with the link cable yourself, make sure you know which pin is which. If you just want to use the wireless adapter as part of the GBA this isn’t relevant. -Serial Peripheral Interface ---------------------------- +## Serial Peripheral Interface Broadly speaking the GBA communicates with the wireless adapter using the Serial Peripheral Interface (SPI), however it can be somewhat weird. In the case of the GBA this is a three or four wire protocol depending on how you count. The clock, two data wires, and what is normally chip select but operates more as a reset. @@ -52,24 +47,23 @@ Broadly speaking the GBA communicates with the wireless adapter using the Serial [![A logic analyser displaying an SPI trace from the GBA and wireless adapter -communications](img/init.png)](img/init.png) +communications](img/wireless/init.png)](img/wireless/init.png) -*A logic analyser can be used to probe the link cable protocol between the GBA and a Wireless Adapter* +_A logic analyser can be used to probe the link cable protocol between the GBA and a Wireless Adapter_ I will break up the ways in which you communicate into three parts: -* Initialisation -* Commands -* Waiting for data +- Initialisation +- Commands +- Waiting for data One thing to make note of is that when I have screenshots showing the logic analyser traces, these all come from Pokémon Emerald as it is what I had at the time I did a lot of this. -Initialisation --------------- +## Initialisation -[![The initialisation sequence captured using a logic analyser](img/full_initialisation.png)](img/full_initialisation.png) +[![The initialisation sequence captured using a logic analyser](img/wireless/full_initialisation.png)](img/wireless/full_initialisation.png) -*The initialisation sequence captured using a logic analyser* +_The initialisation sequence captured using a logic analyser_ Before starting sending and receiving commands, a handshake with the adapter needs to be done. During this, the clocks runs at 256 kHz. Real games start this process by resetting the adapter. @@ -84,24 +78,24 @@ Next is the Nintendo Exchange. The GBA and the adapter exchange the word “NINTENDO” with each other in quite a strange way. [![GBA -sends `0x7FFF494E` and wireless adapter sends `0x00000000`.](img/first_single_u32.png)](img/first_single_u32.png) +sends `0x7FFF494E` and wireless adapter sends `0x00000000`.](img/wireless/first_single_u32.png)](img/wireless/first_single_u32.png) -*GBA sends `0x7FFF494E` and wireless adapter sends `0x00000000`.* +_GBA sends `0x7FFF494E` and wireless adapter sends `0x00000000`._ The GBA here sends `0x7FFF494E`, of this the relevant part is the `0x494E`. If we look up what the bytes `0x49, 0x4E` are you will find them to be the letters `NI`. As exchanges happen simultaneously, at this point the adapter doesn’t know what to respond with and so responds with all zeros. -[![GBA sends `0xFFFF494E` and wireless adapter sends `0x494EB6B1`.](img/first_nintendo_32.png)](img/first_nintendo_32.png) +[![GBA sends `0xFFFF494E` and wireless adapter sends `0x494EB6B1`.](img/wireless/first_nintendo_32.png)](img/wireless/first_nintendo_32.png) -*GBA sends `0xFFFF494E` and wireless adapter sends `0x494EB6B1`.* +_GBA sends `0xFFFF494E` and wireless adapter sends `0x494EB6B1`._ Next the GBA sends `0xFFFF494E` and now the wireless adapter does respond and responds with `0x494EB6B1`. I can assure you there is a pattern here: -* GBA: - * Two _most_ significant bytes are the inverse of the adapters previous _most_ significant bytes. - * Two _least_ significant bytes are the GBA’s own data. -* Adapter: - * Two _least_ significant bytes are the inverse of the GBA’s previous _least_ significant bytes. - * Two most significant bytes are the adapters own data. +- GBA: + - Two _most_ significant bytes are the inverse of the adapters previous _most_ significant bytes. + - Two _least_ significant bytes are the GBA’s own data. +- Adapter: + - Two _least_ significant bytes are the inverse of the GBA’s previous _least_ significant bytes. + - Two most significant bytes are the adapters own data. The “own” data are the bytes of the string “NINTENDO”, and you advance to the next pair when the most significant bytes equal the inverse of the least significant bytes. @@ -160,48 +154,48 @@ Following these rules the transfer looks like Although note that due to the rules, the first few transfers may contain some junk data and be different to this in practice. And after this, you can start sending commands. -Commands --------- +## Commands [![A command being sent by the GBA and acknowledged by the -adapter](img/0x17.png)](img/0x17.png) +adapter](img/wireless/0x17.png)](img/wireless/0x17.png) -*A command being sent by the GBA and acknowledged by the adapter* +_A command being sent by the GBA and acknowledged by the adapter_ Commands are how you tell the adapter to do things. When in command mode the clock operates at 2 mHz. Some examples of commands include connect to adapter, send message, and receive message. All commands follow the same form: -* Command - - The command is a 32 bit value of the form `0x9966LLCC`: - - * LL - * The length of the data payload in number of 32 bit values. For example here it is `0x01`, so one value is transmitted after this. - * CC - * The command type, there are a bunch of these! In this case the command type is `0x17`. -* Data - - All the data along with the command, must transmit the number given in the command - -* Acknowledge - - The adapter responds with a command, the length is the number of 32 bit values and the command type is always what you send + `0x80`. In this case the length is zero and the command is `0x17` + `0x80` = `0x97`. - - * ⚠️ When you send invalid commands or a one you're not supposed to send in the current state (like sending a `0x1d` before a `0x1c`), the adapter responds `0x996601ee`. If you read the next word (as the response size is `01`), it gives you an error code (`2` when using an invalid command, `1` when using a valid command in an invalid state, or `0`). +- Command -* Response - - The data that the adapter responds with. Equal to the length given in the acknowledgement. + The command is a 32 bit value of the form `0x9966LLCC`: -* Ready - - In the figure, you’ll see that after exchanging any 32 bit value using SPI, some out of clock communication happens. This is the GBA and the Adapter signalling to each other that they are ready to communicate. This happens over the following stages: - - 1. The GBA goes low as soon as it can. - 2. The adapter goes high. - 3. The GBA goes high. - 4. The adapter goes low _when it’s ready_. - 5. The GBA goes low when it’s ready. - 6. The GBA starts a transfer, clock starts pulsing, and both sides exchange the next 32 bit value. + - LL + - The length of the data payload in number of 32 bit values. For example here it is `0x01`, so one value is transmitted after this. + - CC + - The command type, there are a bunch of these! In this case the command type is `0x17`. + +- Data + + All the data along with the command, must transmit the number given in the command + +- Acknowledge + + The adapter responds with a command, the length is the number of 32 bit values and the command type is always what you send + `0x80`. In this case the length is zero and the command is `0x17` + `0x80` = `0x97`. + + - ⚠️ When you send invalid commands or a one you're not supposed to send in the current state (like sending a `0x1d` before a `0x1c`), the adapter responds `0x996601ee`. If you read the next word (as the response size is `01`), it gives you an error code (`2` when using an invalid command, `1` when using a valid command in an invalid state, or `0`). + +- Response + + The data that the adapter responds with. Equal to the length given in the acknowledgement. + +- Ready + + In the figure, you’ll see that after exchanging any 32 bit value using SPI, some out of clock communication happens. This is the GBA and the Adapter signalling to each other that they are ready to communicate. This happens over the following stages: + + 1. The GBA goes low as soon as it can. + 2. The adapter goes high. + 3. The GBA goes high. + 4. The adapter goes low _when it’s ready_. + 5. The GBA goes low when it’s ready. + 6. The GBA starts a transfer, clock starts pulsing, and both sides exchange the next 32 bit value. ⌛ If this acknowledge procedure doesn't complete, the adapter "gives up" after ~800μs and start listening again for commands. That means that if a game doesn't implement this logic, it has to wait almost 1 millisecond between transfers (vs ~40μs in normal scenarios). @@ -211,23 +205,22 @@ Whenever either side expects something to be sent from the other (as SPI is alwa #### Hello - `0x10` -[![Image without alt text or caption](img/0x10.png)](img/0x10.png) +[![Image without alt text or caption](img/wireless/0x10.png)](img/wireless/0x10.png) -* Send length: 0, Response length: 0 - -* First thing to be called after finishing the initialisation sequence. +- Send length: 0, Response length: 0 +- First thing to be called after finishing the initialisation sequence. #### Setup - `0x17` -[![Image without alt text or caption](img/0x17.png)](img/0x17.png) +[![Image without alt text or caption](img/wireless/0x17.png)](img/wireless/0x17.png) -* Send length: 1, response length: 0 - -* Games set this. It seems to setup the adapter's configuration. +- Send length: 1, response length: 0 +- Games set this. It seems to setup the adapter's configuration. Both Pokemon games and the multiboot ROM that the adapter sends when no cartridge is inserted use `0x003C0420`. 🔝 For a game, the most important bits are bits `16-17` (let's call this `maxPlayers`), which specify the maximum number of allowed players: + - `00`: 5 players (1 host and 4 clients) - `01`: 4 players - `10`: 3 players @@ -241,15 +234,14 @@ Both Pokemon games and the multiboot ROM that the adapter sends when no cartridg #### Broadcast - `0x16` -[![Image without alt text or caption](img/0x16.png)](img/0x16.png) +[![Image without alt text or caption](img/wireless/0x16.png)](img/wireless/0x16.png) -* Send length: 6, response length: 0 - -* The data to be broadcast out to all adapters. Examples of use include the union room, broadcasting game name and username in download play, and the username in direct multiplayer in Pokémon. +- Send length: 6, response length: 0 +- The data to be broadcast out to all adapters. Examples of use include the union room, broadcasting game name and username in download play, and the username in direct multiplayer in Pokémon. 💻 This is the first command used to start a server. The 6 parameters are the ASCII characters of the game and user name, plus some bytes indicating whether the server should appear in the Download Play list or not. Here's a byte by byte explanation: -[![Image without alt text or caption](img/broadcast.png)](img/broadcast.png) +[![Image without alt text or caption](img/wireless/broadcast.png)](img/wireless/broadcast.png) (if you read from right to left, it says `ICE CLIMBER` - `NINTENDO`) @@ -259,38 +251,35 @@ Both Pokemon games and the multiboot ROM that the adapter sends when no cartridg #### StartHost - `0x19` -* Send length: 0, response length: 0 - -* This uses the broadcast data given by the broadcast command and actually does the broadcasting. +- Send length: 0, response length: 0 +- This uses the broadcast data given by the broadcast command and actually does the broadcasting. #### EndHost - `0x1b` -* Send length: 0, response length: 2+ - -* This command stops host broadcast. This allows to "close" the session and stop allowing new clients, but also **keeping the existing connections alive**. Sends and Receives still work, but: - - Clients cannot connect, even if they already know the host ID (`FinishConnection` will fail). - - Calls to `AcceptConnections` on the host side will fail, unless `StartHost` is called again. - +- Send length: 0, response length: 2+ +- This command stops host broadcast. This allows to "close" the session and stop allowing new clients, but also **keeping the existing connections alive**. Sends and Receives still work, but: + - Clients cannot connect, even if they already know the host ID (`FinishConnection` will fail). + - Calls to `AcceptConnections` on the host side will fail, unless `StartHost` is called again. + #### BroadcastRead - `0x1c`, `0x1d` and `0x1e` -[![Image without alt text or caption](img/0x1d.png)](img/0x1d.png) +[![Image without alt text or caption](img/wireless/0x1d.png)](img/wireless/0x1d.png) -* Send length: 0, response length: 7 \* number of broadcasts (maximum: 4) - -* All currently broadcasting devices are returned here along with a word of **metadata** (the metadata word first, then 6 words with broadcast data). -* The metadata contains: - * First 2 bytes: Server ID. IDs have 16 bits. - * 3rd byte: Next available slot. This can be used to check whether a player can join a room or not. - * `0b00`: If you join this room, your `clientNumber` will be 0. - * `0b01`: If you join this room, your `clientNumber` will be 1. - * `0b10`: If you join this room, your `clientNumber` will be 2. - * `0b11`: If you join this room, your `clientNumber` will be 3. - * `0xff`: The server is full. You cannot join this room. - * Although `LinkWireless` uses this to know the number of connected players, that only works because -by design- rooms are closed when a player disconnects. The hardware allows disconnecting specific clients, so if the next available slot is e.g. 2, it can mean that there are 3 connected players (1 host + 2 clients) or that there are more players, but the third client (`clientNumber` = 2) has disconnected and the slot is now free. - * The number of available slots depends on `maxPlayers` (see [Setup](#setup---0x17)) and/or [EndHost](#endhost---0x1b). - * 4th byte: Zero. +- Send length: 0, response length: 7 \* number of broadcasts (maximum: 4) +- All currently broadcasting devices are returned here along with a word of **metadata** (the metadata word first, then 6 words with broadcast data). +- The metadata contains: + - First 2 bytes: Server ID. IDs have 16 bits. + - 3rd byte: Next available slot. This can be used to check whether a player can join a room or not. + - `0b00`: If you join this room, your `clientNumber` will be 0. + - `0b01`: If you join this room, your `clientNumber` will be 1. + - `0b10`: If you join this room, your `clientNumber` will be 2. + - `0b11`: If you join this room, your `clientNumber` will be 3. + - `0xff`: The server is full. You cannot join this room. + - Although `LinkWireless` uses this to know the number of connected players, that only works because -by design- rooms are closed when a player disconnects. The hardware allows disconnecting specific clients, so if the next available slot is e.g. 2, it can mean that there are 3 connected players (1 host + 2 clients) or that there are more players, but the third client (`clientNumber` = 2) has disconnected and the slot is now free. + - The number of available slots depends on `maxPlayers` (see [Setup](#setup---0x17)) and/or [EndHost](#endhost---0x1b). + - 4th byte: Zero. -🆔 IDs are randomly generated. Each time you broadcast or connect, the adapter assigns you a new id. +🆔 IDs are randomly generated. Each time you broadcast or connect, the adapter assigns you a new ID. ✅ Reading broadcasts is a three-step process: First, you send `0x1c` (you will get an ACK instantly), and start waiting until the adapter retrieves data (games usually wait 1 full second). Then, send a `0x1d` and it will return what's described above. Lastly, send a `0x1e` to finish the process (you can ignore what the adapter returns here). If you don't send that last `0x1e`, the next command will fail. @@ -300,105 +289,101 @@ Both Pokemon games and the multiboot ROM that the adapter sends when no cartridg #### AcceptConnections - `0x1a` -* Send length: 0, response length: 0+ - -* Accepts new connections and returns a list with the connected adapters. The length of the response is zero if there are no connected adapters. -* It includes one value per connected client, in which the most significant byte is the `clientNumber` (see [IsFinishedConnect](#isfinishedconnect---0x20)) and the least significant byte is the ID. +- Send length: 0, response length: 0+ +- Accepts new connections and returns a list with the connected adapters. The length of the response is zero if there are no connected adapters. +- It includes one value per connected client, in which the most significant byte is the `clientNumber` (see [IsFinishedConnect](#isfinishedconnect---0x20)) and the least significant byte is the ID. 🔗 If this command reports 3 connected consoles, after turning off one of them, it will still report 3 consoles. Servers need to detect timeouts in another way. #### Connect - `0x1f` -[![Image without alt text or caption](img/0x1f.png)](img/0x1f.png) +[![Image without alt text or caption](img/wireless/0x1f.png)](img/wireless/0x1f.png) -* Send length: 1, response length: 0 - -* Send the ID of the adapter you want to connect to from [BroadcastRead](#broadcastread---0x1c-0x1d-and-0x1e). +- Send length: 1, response length: 0 +- Send the ID of the adapter you want to connect to from [BroadcastRead](#broadcastread---0x1c-0x1d-and-0x1e). #### IsFinishedConnect - `0x20` -[![Image without alt text or caption](img/0x20.png)](img/0x20.png) +[![Image without alt text or caption](img/wireless/0x20.png)](img/wireless/0x20.png) + +- Send length: 0, response length: 1 +- Responds with a 16 bit ID as lower 16 bits if finished, otherwise responds with `0x01000000`. -* Send length: 0, response length: 1 - -* Responds with a 16 bit ID as lower 16 bits if finished, otherwise responds with `0x01000000`. - 👆 It also responds in its bits 16 and 17 a number that represents the `clientNumber` (0 to 3). Lets say our ID is `abcd`, it will respond `0x0000abcd` if we are the first client that connects to that server, `0x0001abcd` if we are the second one, `0x0002abcd` third, and `0x0003abcd` fourth. Rooms allow 5 simultaneous adapters at max. 💥 If the connection failed, the `clientNumber` will be a number higher than `3`. #### FinishConnection - `0x21` -[![Image without alt text or caption](img/0x21.png)](img/0x21.png) +[![Image without alt text or caption](img/wireless/0x21.png)](img/wireless/0x21.png) -* Send length: 0, response length: 1 - -* Called after [IsFinishedConnect](#isfinishedconnect---0x20), responds with the final device ID (which tends to be equal to the ID from the previous command), the `clientNumber` in bits 16 and 17, and if all went well, zeros in its remaining bits. +- Send length: 0, response length: 1 +- Called after [IsFinishedConnect](#isfinishedconnect---0x20), responds with the final device ID (which tends to be equal to the ID from the previous command), the `clientNumber` in bits 16 and 17, and if all went well, zeros in its remaining bits. #### SendData - `0x24` -* Send length: N, response length: 0 - -* Send N 32 bit values to connected adapter. - +- Send length: N, response length: 0 +- Send N 32 bit values to connected adapter. + ⚠️ The first value **is a header**, and has to be correct. Otherwise, the adapter will ignore the command and won't send any data. The header is as follows: + - For hosts: the number of `bytes` that come next. For example, if we want to send `0xaabbccdd` and `0x12345678` in the same command, we need to send: - * `0x00000008`, `0xaabbccdd`, `0x12345678`. + - `0x00000008`, `0xaabbccdd`, `0x12345678`. - For clients: `(bytes << (3 + (1+clientNumber) * 5))`. The `clientNumber` is what I described in [IsFinishedConnect](#isfinishedconnect---0x20). For example, if we want to send a single 4-byte value (`0xaabbccdd`): - * The first client should send: `0x400`, `0xaabbccdd` - * The second client should send: `0x8000`, `0xaabbccdd` - * The third client should send: `0x100000`, `0xaabbccdd` - * The fourth client should send: `0x2000000`, `0xaabbccdd` + - The first client should send: `0x400`, `0xaabbccdd` + - The second client should send: `0x8000`, `0xaabbccdd` + - The third client should send: `0x100000`, `0xaabbccdd` + - The fourth client should send: `0x2000000`, `0xaabbccdd` 🔝 Each `SendData` can send up to: + - **Host:** 87 bytes (or 21.75 values) - **Clients:** 16 bytes (or 4 values) -- *(the header doesn't count)* +- _(the header doesn't count)_ 🗂️ Any non-multiple of 4 byte count will send LSB bytes first. For example, a host sending `0x00000003`, `0xaabbccdd` will result in bytes `0xbb`, `0xcc` and `0xdd` being received by clients (the clients will receive `0x00bbccdd`). 🤝 Note that when having more than 2 connected adapters, data is not transferred between different clients. If a client wants to tell something to another client, it has to talk first with the host with `SendData`, and then the host needs to relay that information to the other client. 👑 Internally, data is only sent when **the host** calls `SendData`: + - The send/receive buffer size is 1 packet, so calling `SendData` multiple times on either side (before the other side calls `ReceiveData`) will result in data loss. - Clients only **schedule** the data transfer, but they don't do it until the host sends something. This is problematic because the command overrides previously scheduled transfers, so calling `SendData` multiple times on the client side before the host calls `SendData` would also result in data loss. I believe this is why most games use `SendDataWait` on the client side. - Here's an example of this behavior: - - **Client**: `SendData` `{sndHeader}`, `10` - - **Host**: `SendData` `{sndHeader}`, `1` **(\*)** - - *(here, the adapter internally receives the 10 from the client)* - - **Host**: `SendData` `{sndHeader}`, `2` - - *(here, the previous packet with 1 is lost since nobody received it yet)* - - **Client**: `ReceiveData` - - Receives `{rcvHeader}`, `2` - - **Client**: `SendData` `{sndHeader}`, `20` - - **Host**: `ReceiveData` - - Receives `{rcvHeader}`, `10` *(pending from **(\*)**)* - - **Host**: `ReceiveData` - - Receives nothing - - **Host**: `SendData` `{sndHeader}`, `3` - - *(here, the adapter internally receives the 20 from the client)* - - **Host**: `ReceiveData` - - Receives `{rcvHeader}`, 20 + - **Client**: `SendData` `{sndHeader}`, `10` + - **Host**: `SendData` `{sndHeader}`, `1` **(\*)** + - _(here, the adapter internally receives the 10 from the client)_ + - **Host**: `SendData` `{sndHeader}`, `2` + - _(here, the previous packet with 1 is lost since nobody received it yet)_ + - **Client**: `ReceiveData` + - Receives `{rcvHeader}`, `2` + - **Client**: `SendData` `{sndHeader}`, `20` + - **Host**: `ReceiveData` + - Receives `{rcvHeader}`, `10` _(pending from **(\*)**)_ + - **Host**: `ReceiveData` + - Receives nothing + - **Host**: `SendData` `{sndHeader}`, `3` + - _(here, the adapter internally receives the 20 from the client)_ + - **Host**: `ReceiveData` + - Receives `{rcvHeader}`, 20 🔁 This command can also be used with one header and **no data**. In this case, it will resend the last N bytes (based on the header) of the last packet. Until we have a better name, we'll call this **ghost sends**. #### SendDataWait - `0x25` -[![Image without alt text or caption](img/0x25.png)](img/0x25.png) +[![Image without alt text or caption](img/wireless/0x25.png)](img/wireless/0x25.png) -* Send length: N, response length: 0 - -* The same as [SendData](#senddata---0x24) but with the additional effect of [Wait](#wait---0x27) -* See [Waiting](#waiting) for more details on this. +- Send length: N, response length: 0 +- The same as [SendData](#senddata---0x24) but with the additional effect of [Wait](#wait---0x27) +- See [Waiting](#waiting) for more details on this. #### ReceiveData - `0x26` -[![Image without alt text or caption](img/0x26.png)](img/0x26.png) +[![Image without alt text or caption](img/wireless/0x26.png)](img/wireless/0x26.png) -* Send length: 0, response length: N - -* Responds with all the data from all adapters. No IDs are included, this is just what was sent concatenated together. -* Once data has been pulled out, it clears the data buffer, so calling this again can only get new data. +- Send length: 0, response length: N +- Responds with all the data from all adapters. No IDs are included, this is just what was sent concatenated together. +- Once data has been pulled out, it clears the data buffer, so calling this again can only get new data. 🧩 The data is only concatenated on the host side, and its order is based on the `clientNumber`. It doesn't matter who called `SendData` first. @@ -412,91 +397,91 @@ Both Pokemon games and the multiboot ROM that the adapter sends when no cartridg - The rest of the bits are `0`. 🧱 Concatenation is done at **byte** level. So, for example, if client 3 sends 3 bytes (`0xAABBCC`) and client 1 sends 2 bytes (`0xDDEE`), the host would receive 3 words: - - (header) `0b0000_00011_00000_00000_00010_0_0000000`, `0xEEAABBCC`, `0x000000DD` + +- (header) `0b0000_00011_00000_00000_00010_0_0000000`, `0xEEAABBCC`, `0x000000DD` #### Wait - `0x27` -[![Image without alt text or caption](img/0x27.png)](img/0x27.png) +[![Image without alt text or caption](img/wireless/0x27.png)](img/wireless/0x27.png) + +- Send length: 0, response length: 0 +- See [Waiting](#waiting) for more details on this. -* Send length: 0, response length: 0 - -* See [Waiting](#waiting) for more details on this. - #### DisconnectClient - `0x30` -[![Image without alt text or caption](img/0x30.png)](img/0x30.png) +[![Image without alt text or caption](img/wireless/0x30.png)](img/wireless/0x30.png) -* Send length 1, reponse length: 0 - -* This command disconnects clients. The argument is a bitmask of the client ID to disconnect. Sending `0x1` means "disconnect client number 0", sending `0x2` means "disconnect client number 1", and sending `0xF` would disconnect all the clients. After disconnecting a client, its ID won't appear on `AcceptConnection` calls and its `clientNumber` will be liberated, so other peers can connect. +- Send length 1, reponse length: 0 +- This command disconnects clients. The argument is a bitmask of the client ID to disconnect. Sending `0x1` means "disconnect client number 0", sending `0x2` means "disconnect client number 1", and sending `0xF` would disconnect all the clients. After disconnecting a client, its ID won't appear on `AcceptConnection` calls and its `clientNumber` will be liberated, so other peers can connect. ⚡ The clients also are able to disconnect themselves using this command, but they can only send its corresponding bit or `0xF`, other bits are ignored (they cannot disconnect other clients). Also, the host won't know if a client disconnects itself, so this feature is not very useful: - * The host still needs to monitor clients to ensure they are still alive (ie. through some PING like mechanism) and disconnect them if they are not, to allow new clients to connect. [4](#pokered) + +- The host still needs to monitor clients to ensure they are still alive (ie. through some PING like mechanism) and disconnect them if they are not, to allow new clients to connect. [4](#pokered) #### Bye - `0x3d` -* Send length: 0, Response length: 0 - -* This sets the adapter in a low power consumption mode. Games use it when the player exits the multiplayer mode. To use the adapter again after this command, a new reset/initialization is needed. +- Send length: 0, Response length: 0 +- This sets the adapter in a low power consumption mode. Games use it when the player exits the multiplayer mode. To use the adapter again after this command, a new reset/initialization is needed. ### Other commands #### SignalLevel - `0x11` -[![Image without alt text or caption](img/0x11.png)](img/0x11.png) +[![Image without alt text or caption](img/wireless/0x11.png)](img/wireless/0x11.png) -* Send length: 0, response length: 1 - -* This returns the signal level of the other adapters from `0` to `0xFF` (`0` means disconnected). -* The levels are returned in a single value, the first byte being the signal level of client 0, and the last byte being the signal level of client 3. -* When called from a client, it only returns the signal level of that client in its corresponding byte. The rest of the bytes will be `0`. +- Send length: 0, response length: 1 +- This returns the signal level of the other adapters from `0` to `0xFF` (`0` means disconnected). +- The levels are returned in a single value, the first byte being the signal level of client 0, and the last byte being the signal level of client 3. +- When called from a client, it only returns the signal level of that client in its corresponding byte. The rest of the bytes will be `0`. #### VersionStatus - `0x12` -* Send length: 0, Response length: 1 +- Send length: 0, Response length: 1 -* In my adapter, it always returns `8585495` (decimal). It contains the hardware and firmware version of the adapter. +- In my adapter, it always returns `8585495` (decimal). It contains the hardware and firmware version of the adapter. #### SystemStatus - `0x13` -* Send length: 0, Response length: 1 +- Send length: 0, Response length: 1 -* Returns some information about the current connection and device state. The returned word contains: -- Bits `0-15`: The device ID (or zero if the device is not connected nor hosting). -- Bits `16-23`: A 4-bit array with slots. If the console is a client, it'll have a 1 in the position assigned to that slot (e.g. the one with `clientNumber` 3 will have `0100`). The host will always have `0000` here. -- Bits `24-31`: A number indicating the state of the adapter - - `0` = idle - - `1`/`2` = serving (host) - - `3` = searching - - `4` = connecting - - `5` = connected (client) +- Returns some information about the current connection and device state. The returned word contains: + +* Bits `0-15`: The device ID (or zero if the device is not connected nor hosting). +* Bits `16-23`: A 4-bit array with slots. If the console is a client, it'll have a 1 in the position assigned to that slot (e.g. the one with `clientNumber` 3 will have `0100`). The host will always have `0000` here. +* Bits `24-31`: A number indicating the state of the adapter + - `0` = idle + - `1`/`2` = serving (host) + - `3` = searching + - `4` = connecting + - `5` = connected (client) #### SlotStatus - `0x14` -* Send length: 0, Response length: 1+ +- Send length: 0, Response length: 1+ -* It's returns a list of the connected adapters, similar to what `AcceptConnections` responds, but also: +- It's returns a list of the connected adapters, similar to what `AcceptConnections` responds, but also: - - `SlotStatus` has an extra word at the start of the response, indicating the `clientNumber` that the next connection will have (or `0xFF` if the room is not accepting new clients). - - `SlotStatus` can be called after `EndHost`, while `AcceptConnections` fails. + - `SlotStatus` has an extra word at the start of the response, indicating the `clientNumber` that the next connection will have (or `0xFF` if the room is not accepting new clients). + - `SlotStatus` can be called after `EndHost`, while `AcceptConnections` fails. #### ConfigStatus - `0x15` -* Send length: 0, Response length: 7 (as client), or 8 (as host) -* Returns the adapter configuration. +- Send length: 0, Response length: 7 (as client), or 8 (as host) +- Returns the adapter configuration. 🤔 In my tests... + - As client, it returned: `0, 0, 0, 0, 0, 0, 257`. - As host, it returned: `1, 2, 3, 4, 5, 6, 3933216, 257`. - * `1, 2, 3, 4, 5, 6` would be the broadcast data. - * `3933216` is the value used in the [Setup](#setup---0x17) command (`0x003C0420`). - * No idea what `257` this means. + - `1, 2, 3, 4, 5, 6` would be the broadcast data. + - `3933216` is the value used in the [Setup](#setup---0x17) command (`0x003C0420`). + - No idea what `257` this means. #### RetransmitAndWait - `0x37` -* Send length: 0, Response length: 0 -* Retransmits the last data from a host to all clients, with the additional effect of [Wait](#wait---0x27) -* See [Waiting](#waiting) for more details on this. +- Send length: 0, Response length: 0 +- Retransmits the last data from a host to all clients, with the additional effect of [Wait](#wait---0x27) +- See [Waiting](#waiting) for more details on this. ### Unknown commands @@ -510,16 +495,15 @@ If we analyze whether a command ID throws an 'invalid command' error (`0x996601e - `0x38` - `0x39` -Waiting -------- +## Waiting -[![Image without alt text or caption](img/wake-up.png)](img/wake-up.png) +[![Image without alt text or caption](img/wireless/wake-up.png)](img/wireless/wake-up.png) -* After either [SendDataWait](#senddatawait---0x25) or [Wait](#wait---0x27), clock control switches to the wireless adapter. -* Once the adapter has something to tell the GBA about, the _adapter_ sends a command to the GBA (usually `0x99660028`). -* These transfers are dealt with in much the same way as before but with the roles of the GBA and the adapter reversed, see the figure! -* The GBA then sends the response back (e.g. `0x996600A8` as `0x28` + `0x80` = `0xA8`). -* After this, control of the clock returns to the GBA, and it can start sending commands back again. For example this might be receiving the command sent by the other device using [ReceiveData](#receivedata---0x26). +- After either [SendDataWait](#senddatawait---0x25) or [Wait](#wait---0x27), clock control switches to the wireless adapter. +- Once the adapter has something to tell the GBA about, the _adapter_ sends a command to the GBA (usually `0x99660028`). +- These transfers are dealt with in much the same way as before but with the roles of the GBA and the adapter reversed, see the figure! +- The GBA then sends the response back (e.g. `0x996600A8` as `0x28` + `0x80` = `0xA8`). +- After this, control of the clock returns to the GBA, and it can start sending commands back again. For example this might be receiving the command sent by the other device using [ReceiveData](#receivedata---0x26). ⌚ This timeouts after 500ms of the adapter not having anything to tell the GBA about. In this case, the adapter sends `0x99660027`. **This is only true if the console has used the [Setup](#setup---0x17) command before**. The value that most games use (`0x003C0420`) contains this timeout value, but the default is zero (no timeout). @@ -528,14 +512,16 @@ Waiting 💨 Clients receive the `0x28` when new data from the host is available, but the host receives it immediately (well, after the transfer completes), as it can be used to know which clients received data or are disconnected. ⚠️ If some children didn't receive the data, the adapter sends to the host GBA a `0x99660128`. - - The extra parameter has two bitarrays: - * Bits `0-4`: The clients that _received_ data. - * Bits `8-11`: The clients marked as _inactive_. This depends on the # of maximum transmissions configured with the [Setup](#setup---0x17) command. + +- The extra parameter has two bitarrays: + - Bits `0-4`: The clients that _received_ data. + - Bits `8-11`: The clients marked as _inactive_. This depends on the # of maximum transmissions configured with the [Setup](#setup---0x17) command. 🔗 When the adapter is disconnected from the host, it sends a `0x99660029`. - - Bit 8 of the response indicates the reason: - * `0` = manual disconnect (aka the host used [DisconnectClient](#disconnectclient---0x30)) - * `1` = the connection was lost + +- Bit 8 of the response indicates the reason: + - `0` = manual disconnect (aka the host used [DisconnectClient](#disconnectclient---0x30)) + - `1` = the connection was lost ◀ **Inverted ACKs** @@ -548,20 +534,19 @@ While the clock is inverted, the acknowledge procedure is 'standard' but with th 5. The adapter goes low when it's ready. 6. The adapter starts a transfer, clock starts pulsing, and both sides exchange the next 32 bit value. -Wireless Multiboot ------------------- +## Wireless Multiboot > You can learn more details by reading [LinkWirelessMultiboot.hpp](../lib/LinkWirelessMultiboot.hpp)'s code. To host a 'multiboot' room, a host sets the **multiboot flag** (bit 15) in its game ID (inside broadcast data) and starts serving. -- 1) For each new client that connects, it runs a small handshake where the client sends their 'game name' and 'player name'. The bootloader always sends `RFU-MB-DL` as game name and `PLAYER A` (or `B`, `C`, `D`) as player name. +- 1. For each new client that connects, it runs a small handshake where the client sends their 'game name' and 'player name'. The bootloader always sends `RFU-MB-DL` as game name and `PLAYER A` (or `B`, `C`, `D`) as player name. -- 2) When the host player confirms that all players are ready, it sends a 'rom start' command. +- 2. When the host player confirms that all players are ready, it sends a 'rom start' command. -- 3) The host sends the rom bytes in 84-byte chunks. +- 3. The host sends the rom bytes in 84-byte chunks. -- 4) The host sends a 'rom end' command and the games boot. +- 4. The host sends a 'rom end' command and the games boot. ### Valid header @@ -573,6 +558,8 @@ The bootloader will only accept ROMs with valid headers: they must contain this When the bootloader accepts the ROM, it will run the jump instruction located at the first byte. No extra headers are required apart from these 16 bytes. +`LinkWirelessMultiboot` patches the header on the fly, so users can compile their ROMs for cabled Multiboot and also use them wirelessly. + ### Official protocol > You can learn more details by reading [LinkWirelessOpenSDK.hpp](../lib/LinkWirelessOpenSDK.hpp)'s code. @@ -635,37 +622,37 @@ enum CommState : unsigned int { - Payload: `0x01`, `0x06`, `0x00`, `0x1A`, `0x00`, `0x00` - Server: ACKs the packet (`size=0, n=1, ph=0, ack=1, commState=1`) - Client: sends `0x00000501` - - Header: `0x0501` (`size=1, n=2, ph=0, ack=0, commState=1`) - - Payload: `0x00` + - Header: `0x0501` (`size=1, n=2, ph=0, ack=0, commState=1`) + - Payload: `0x00` - Server: ACKs the packet - Client: sends `0x00000886`, `0x2D554652` - - Header: `0x0886` (`size=6, n=1, ph=0, ack=0, commState=2`) (`2 = COMMUNICATING`) - - Payload: `0x00`, `0x00`, `0x52`, `0x46`, `0x55`, `0x2D` - - => `RFU-` + - Header: `0x0886` (`size=6, n=1, ph=0, ack=0, commState=2`) (`2 = COMMUNICATING`) + - Payload: `0x00`, `0x00`, `0x52`, `0x46`, `0x55`, `0x2D` + - => `RFU-` - Server: ACKs the packet - Client: sends `0x424D08A6`, `0x004C442D` - - Header: `0x08A6` (`size=6, n=1, ph=1, ack=0, commState=2`) - - Payload: `MB-DL` + - Header: `0x08A6` (`size=6, n=1, ph=1, ack=0, commState=2`) + - Payload: `MB-DL` - Server: ACKs the packet - Client: sends `0x000008C6`, `0x50000000` - - Header: `0x08C6` (`size=6, n=1, ph=2, ack=0, commState=2`) - - Payload: `P` + - Header: `0x08C6` (`size=6, n=1, ph=2, ack=0, commState=2`) + - Payload: `P` - Server: ACKs the packet - Client: sends `0x414C08E6`, `0x20524559` - - Header: `0x08E6` (`size=6, n=1, ph=3, ack=0, commState=2`) - - Payload: `LAYER` + - Header: `0x08E6` (`size=6, n=1, ph=3, ack=0, commState=2`) + - Payload: `LAYER` - Server: ACKs the packet - Client: sends `0x00410902` - - Header: `0x0902` (`size=2, n=2, ph=0, ack=0, commState=2`) - - Payload: `A` + - Header: `0x0902` (`size=2, n=2, ph=0, ack=0, commState=2`) + - Payload: `A` - Server: ACKs the packet - Client: sends `0x00000C00` - - Header: `0x0C00` (`size=0, n=0, ph=0, ack=0, commState=3`) (`3 = ENDING`) - - No payload + - Header: `0x0C00` (`size=0, n=0, ph=0, ack=0, commState=3`) (`3 = ENDING`) + - No payload - Server: ACKs the packet - Client: sends `0x00000080` - - Header: `0x0080` (`size=0, n=1, ph=0, ack=0, commState=0`) (`0 = OFF`) - - No payload + - Header: `0x0080` (`size=0, n=1, ph=0, ack=0, commState=0`) (`0 = OFF`) + - No payload - Server: ACKs the packet ## (2) ROM start command @@ -689,22 +676,18 @@ After all ROM chunks are ACK'd, the last transfers are: - `size=0, n=0, ph=0, ack=0, commState=3` (`3 = ENDING`) - `size=0, n=1, ph=0, ack=0, commState=0` (`0 = OFF`) -SPI config ----------- +## SPI config Here's how SPI works on the GBA: -[![Image without alt text or caption](img/logic2.png)](img/logic2.png) +[![Image without alt text or caption](img/wireless/logic2.png)](img/wireless/logic2.png) -I know more! -============ +# I know more! If you know any extra details about the wireless adapter, get in touch!. For specific details I’ve left footnotes around if you happen to know that piece of information. 1. Multiboot is what we call a rom that can be booted over link cable. This can be used for something akin to download play software for the DS. [↩︎](#fnref:multiboot) - 2. [Games compatible with the wireless adapter](https://en.wikipedia.org/wiki/Game_Boy_Advance_Wireless_Adapter#Compatible_games) [↩︎](#fnref:list_of_games) - 3. [Send me an email if you know more about this](https://blog.kuiper.dev/contact) 4. Some interesting data about the RFU adapter can be found in Pokemon Games, see the [FireRed Decompilation](https://github.com/pret/pokefirered/blob/49ea462d7f421e75a76b25d7e85c92494c0a9798/include/librfu.h#L44) for more information. diff --git a/examples/LinkCableMultiboot_demo/Makefile b/examples/LinkCableMultiboot_demo/Makefile index 862164e..0246cfb 100644 --- a/examples/LinkCableMultiboot_demo/Makefile +++ b/examples/LinkCableMultiboot_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -134,22 +134,22 @@ TITLE := $(PROJ) LIBS := -ltonc -lugba BUILD := build -SRCDIRS := src ../_lib ../../lib +SRCDIRS := src ../_lib ../../lib ../_lib/libgbfs DATADIRS := data INCDIRS := src LIBDIRS := $(TONCLIB) $(PWD)/../_lib/libugba # --- switches --- -bMB := 1 # Multiboot build +bMB := 0 # Multiboot build bTEMPS := 0 # Save gcc temporaries (.i and .s files) bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- @@ -247,9 +248,6 @@ $(BUILD): @[ -d $@ ] || mkdir -p $@ @make --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile arm-none-eabi-nm -Sn $(OUTPUT).elf > $(BUILD)/$(TARGET).map - mv $(OUTPUT).gba tmp.gba - ./pad16.sh tmp.gba - mv tmp.gba $(OUTPUT).gba all : $(BUILD) @@ -278,10 +276,13 @@ endif # End BUILD switch .PHONY: clean rebuild start -rebuild: clean $(BUILD) +rebuild: clean $(BUILD) package -start: - start "$(TARGET).gba" +package: $(BUILD) + ../gbfs.sh "$(TARGET).gba" content.gbfs "$(TARGET).out.gba" + +start: package + start "$(TARGET).out.gba" restart: rebuild start diff --git a/examples/LinkCableMultiboot_demo/content.gbfs b/examples/LinkCableMultiboot_demo/content.gbfs new file mode 100644 index 0000000..9518bd6 Binary files /dev/null and b/examples/LinkCableMultiboot_demo/content.gbfs differ diff --git a/examples/LinkCableMultiboot_demo/src/main.cpp b/examples/LinkCableMultiboot_demo/src/main.cpp index 9ed5b9a..42bc9c7 100644 --- a/examples/LinkCableMultiboot_demo/src/main.cpp +++ b/examples/LinkCableMultiboot_demo/src/main.cpp @@ -1,128 +1,138 @@ -#include -#include -#include "../../_lib/interrupt.h" - -#include "../../../lib/LinkCable.hpp" // (0) Include the header #include "../../../lib/LinkCableMultiboot.hpp" -void log(std::string text); +#include +#include +#include + +extern "C" { +#include "../../_lib/libgbfs/gbfs.h" +} + +static const GBFS_FILE* fs = find_first_gbfs_file(0); +static u32 selectedFile = 0; +static bool spi = false; + +void log(std::string text); +void waitFor(u16 key); +bool didPress(u16 key, bool& pressed); -LinkCable* linkCable = new LinkCable(); // (1) Create a LinkCableMultiboot instance LinkCableMultiboot* linkCableMultiboot = new LinkCableMultiboot(); +void selectLeft() { + if (selectedFile == 0) + return; + selectedFile--; +} + +void selectRight() { + if ((int)selectedFile >= fs->dir_nmemb - 1) + return; + selectedFile++; +} + void init() { REG_DISPCNT = DCNT_MODE0 | DCNT_BG0; tte_init_se_default(0, BG_CBB(0) | BG_SBB(31)); - interrupt_init(); - interrupt_set_handler(INTR_VBLANK, LINK_CABLE_ISR_VBLANK); - interrupt_enable(INTR_VBLANK); - interrupt_set_handler(INTR_SERIAL, LINK_CABLE_ISR_SERIAL); - interrupt_enable(INTR_SERIAL); - interrupt_set_handler(INTR_TIMER3, LINK_CABLE_ISR_TIMER); - interrupt_enable(INTR_TIMER3); + irq_init(NULL); + irq_add(II_VBLANK, NULL); } int main() { init(); - // Hardcoded ROM length - // This is optional, you could also use `LINK_CABLE_MULTIBOOT_MAX_ROM_SIZE` - // (but the transfer will be painfully slow) - u32 romSize = 39264; - // Note that this project's Makefile pads the ROM to a 0x10 boundary - // (as required for Multiboot). + // Ensure there are GBFS files + if (fs == NULL) { + log("! GBFS file not found"); + while (true) + ; + } else if (gbfs_get_nth_obj(fs, 0, NULL, NULL) == NULL) { + log("! No files found (GBFS)"); + while (true) + ; + } - // Each player's input - u16 data[LINK_CABLE_MAX_PLAYERS]; - for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) - data[i] = 0; - - bool isSenderMode = true; - LinkCableMultiboot::Result result = LinkCableMultiboot::Result::CANCELED; + bool left = false, right = false, a = false, b = false, l = false; while (true) { - u16 keys = ~REG_KEYS & KEY_ANY; - - // Sender options - if (isSenderMode) { - if (result != LinkCableMultiboot::Result::SUCCESS) - log("LinkCableMultiboot_demo\n (v6.3.0)\n\nPress START to send the " - "ROM...\nPress B to set client mode..."); - - if (keys & KEY_START) { - log("Sending... (SELECT to cancel)"); - linkCable->deactivate(); - - // (3) Send the ROM - result = - linkCableMultiboot->sendRom((const u8*)MEM_EWRAM, romSize, []() { - u16 keys = ~REG_KEYS & KEY_ANY; - return keys & KEY_SELECT; - }); - - // Print results and wait - log("Result: " + std::to_string(result) + "\n" + - "Press A to continue..."); - do { - keys = ~REG_KEYS & KEY_ANY; - } while (!(keys & KEY_A)); - - // Switch to client mode if it worked - if (result == LinkCableMultiboot::Result::SUCCESS) { - linkCable->activate(); - VBlankIntrWait(); - continue; - } - } - - // Switch to client mode manually (for slaves) - if (keys & KEY_B) { - isSenderMode = false; - linkCable->activate(); - VBlankIntrWait(); - continue; - } - - // In sender mode, don't continue until the ROM is sent successfully - if (result != LinkCableMultiboot::Result::SUCCESS) { - VBlankIntrWait(); - continue; + // Get selected ROM name + char name[32]; + u32 romSize; + const u8* romToSend = + (const u8*)gbfs_get_nth_obj(fs, selectedFile, name, &romSize); + for (u32 i = 0; i < 32; i++) { + if (name[i] == '.') { + name[i] = '\0'; + break; } } - // --- - // Client mode - // --- + // Toggle mode + if (didPress(KEY_L, l)) + spi = !spi; - linkCable->sync(); - linkCable->send(keys + 1); + // Select ROM + if (didPress(KEY_LEFT, left)) + selectLeft(); + if (didPress(KEY_RIGHT, right)) + selectRight(); - std::string output = ""; - if (linkCable->isConnected()) { - u8 playerCount = linkCable->playerCount(); - u8 currentPlayerId = linkCable->currentPlayerId(); + // Menu + log("LinkCableMultiboot_demo\n (v7.0.0)\n\n" + "Press A to send the ROM...\n" + "Press B to launch the ROM...\nLEFT/RIGHT: select ROM\nL: toggle " + "mode\n\nSelected ROM:\n " + + std::string(name) + "\n\nMode:\n " + + std::string(spi ? "SPI (GBC cable)" : "MULTI_PLAY (GBA cable)")); - output += "Players: " + std::to_string(playerCount) + "\n"; + // Send ROM + if (didPress(KEY_A, a)) { + log("Sending... (SELECT to cancel)"); - output += "("; - for (u32 i = 0; i < playerCount; i++) { - while (linkCable->canRead(i)) - data[i] = linkCable->read(i) - 1; + // (2) Send the ROM + auto result = linkCableMultiboot->sendRom( + romToSend, romSize, + []() { + u16 keys = ~REG_KEYS & KEY_ANY; + return keys & KEY_SELECT; + }, + spi ? LinkCableMultiboot::TransferMode::SPI + : LinkCableMultiboot::TransferMode::MULTI_PLAY); - output += std::to_string(data[i]) + (i + 1 == playerCount ? "" : ", "); - } - output += ")\n"; - output += "_keys: " + std::to_string(keys) + "\n"; - output += "_pID: " + std::to_string(currentPlayerId); - } else { - output += std::string("Waiting... "); + // Print results and wait + log("Result: " + std::to_string(result) + "\n" + + "Press DOWN to continue..."); + waitFor(KEY_DOWN); + } + + // Launch ROM + if (didPress(KEY_B, b)) { + log("Launching..."); + VBlankIntrWait(); + + REG_IME = 0; + + u32 fileLength; + const u8* romToSend = + (const u8*)gbfs_get_nth_obj(fs, selectedFile, NULL, &fileLength); + + void* EWRAM = (void*)0x02000000; + memcpy(EWRAM, romToSend, fileLength); + + asm volatile( + "mov r0, %0\n" + "bx r0\n" + : + : "r"(EWRAM) + : "r0"); + + while (true) + ; } VBlankIntrWait(); - log(output); } return 0; @@ -132,4 +142,23 @@ void log(std::string text) { tte_erase_screen(); tte_write("#{P:0,0}"); tte_write(text.c_str()); -} \ No newline at end of file +} + +void waitFor(u16 key) { + u16 keys; + do { + keys = ~REG_KEYS & KEY_ANY; + } while (!(keys & key)); +} + +bool didPress(u16 key, bool& pressed) { + u16 keys = ~REG_KEYS & KEY_ANY; + bool isPressedNow = false; + if ((keys & key) && !pressed) { + pressed = true; + isPressedNow = true; + } + if (pressed && !(keys & key)) + pressed = false; + return isPressedNow; +} diff --git a/examples/LinkCable_basic/Makefile b/examples/LinkCable_basic/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkCable_basic/Makefile +++ b/examples/LinkCable_basic/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkCable_basic/src/main.cpp b/examples/LinkCable_basic/src/main.cpp index 9aecd15..dd109b3 100644 --- a/examples/LinkCable_basic/src/main.cpp +++ b/examples/LinkCable_basic/src/main.cpp @@ -1,13 +1,13 @@ -#include -#include -#include "../../_lib/interrupt.h" - // BASIC: // This example sends the pressed buttons to other players. // (0) Include the header #include "../../../lib/LinkCable.hpp" +#include +#include +#include "../../_lib/interrupt.h" + void log(std::string text); // (1) Create a LinkCable instance @@ -45,7 +45,7 @@ int main() { u16 keys = ~REG_KEYS & KEY_ANY; linkCable->send(keys + 1); // (avoid using 0) - std::string output = "LinkCable_basic (v6.3.0)\n\n"; + std::string output = "LinkCable_basic (v7.0.0)\n\n"; if (linkCable->isConnected()) { u8 playerCount = linkCable->playerCount(); u8 currentPlayerId = linkCable->currentPlayerId(); @@ -77,4 +77,4 @@ void log(std::string text) { tte_erase_screen(); tte_write("#{P:0,0}"); tte_write(text.c_str()); -} \ No newline at end of file +} diff --git a/examples/LinkCable_full/Makefile b/examples/LinkCable_full/Makefile index 9ad0b2f..8664077 100644 --- a/examples/LinkCable_full/Makefile +++ b/examples/LinkCable_full/Makefile @@ -126,7 +126,6 @@ BUILD := build SRCDIRS := src ../_lib ../../lib \ src/scenes \ src/utils - DATADIRS := INCDIRS := src LIBDIRS := $(TONCLIB) $(PWD)/../_lib/libugba $(PWD)/../_lib/libgba-sprite-engine @@ -156,10 +155,11 @@ CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- diff --git a/examples/LinkCable_full/src/main.cpp b/examples/LinkCable_full/src/main.cpp index a4e47c1..34cfbed 100644 --- a/examples/LinkCable_full/src/main.cpp +++ b/examples/LinkCable_full/src/main.cpp @@ -1,12 +1,12 @@ +// FULL: +// This example has a menu and lets the user send data in different ways. + #include "main.h" #include #include "../../_lib/interrupt.h" #include "scenes/TestScene.h" #include "utils/SceneUtils.h" -// FULL: -// This example has a menu and lets the user send data in different ways. - void setUpInterrupts(); void printTutorial(); static std::shared_ptr engine{new GBAEngine()}; @@ -15,8 +15,7 @@ static std::unique_ptr testScene{new TestScene(engine)}; #ifndef USE_LINK_UNIVERSAL LinkCable* linkCable = new LinkCable(); LinkCable* linkConnection = linkCable; -#endif -#ifdef USE_LINK_UNIVERSAL +#else LinkUniversal* linkUniversal = new LinkUniversal(); LinkUniversal* linkConnection = linkUniversal; #endif @@ -41,13 +40,17 @@ int main() { DEBULOG("! started"); } - // log player id/count and important flags + static constexpr int BIT_READY = 3; + static constexpr int BIT_ERROR = 6; + static constexpr int BIT_START = 7; + + // log player ID/count and important flags TextStream::instance().setText( "P" + asStr(linkConnection->currentPlayerId()) + "/" + asStr(linkConnection->playerCount()) + "-R" + - asStr(isBitHigh(REG_SIOCNT, LINK_CABLE_BIT_READY)) + "-S" + - asStr(isBitHigh(REG_SIOCNT, LINK_CABLE_BIT_START)) + "-E" + - asStr(isBitHigh(REG_SIOCNT, LINK_CABLE_BIT_ERROR)), + asStr(isBitHigh(REG_SIOCNT, BIT_READY)) + "-S" + + asStr(isBitHigh(REG_SIOCNT, BIT_ERROR)) + "-E" + + asStr(isBitHigh(REG_SIOCNT, BIT_START)), 0, 14); engine->update(); @@ -74,8 +77,7 @@ inline void setUpInterrupts() { interrupt_enable(INTR_SERIAL); interrupt_set_handler(INTR_TIMER3, LINK_CABLE_ISR_TIMER); interrupt_enable(INTR_TIMER3); -#endif -#ifdef USE_LINK_UNIVERSAL +#else // LinkUniversal interrupt_set_handler(INTR_VBLANK, LINK_UNIVERSAL_ISR_VBLANK); interrupt_enable(INTR_VBLANK); @@ -93,10 +95,9 @@ inline void setUpInterrupts() { void printTutorial() { #ifndef USE_LINK_UNIVERSAL - DEBULOG("LinkCable_full (v6.3.0)"); -#endif -#ifdef USE_LINK_UNIVERSAL - DEBULOG("LinkUniversal_full (v6.3.0)"); + DEBULOG("LinkCable_full (v7.0.0)"); +#else + DEBULOG("LinkUniversal_full (v7.0.0)"); #endif DEBULOG(""); diff --git a/examples/LinkCable_full/src/main.h b/examples/LinkCable_full/src/main.h index b7001b1..f194cae 100644 --- a/examples/LinkCable_full/src/main.h +++ b/examples/LinkCable_full/src/main.h @@ -1,16 +1,19 @@ #ifndef MAIN_H #define MAIN_H -#include -#include "../../../lib/LinkCable.hpp" -#include "../../../lib/LinkUniversal.hpp" - // #define USE_LINK_UNIVERSAL #ifndef USE_LINK_UNIVERSAL -extern LinkCable* linkConnection; +#include "../../../lib/LinkCable.hpp" +#else +#include "../../../lib/LinkUniversal.hpp" #endif -#ifdef USE_LINK_UNIVERSAL + +#include + +#ifndef USE_LINK_UNIVERSAL +extern LinkCable* linkConnection; +#else extern LinkUniversal* linkConnection; #endif diff --git a/examples/LinkCable_full/src/utils/InputHandler.h b/examples/LinkCable_full/src/utils/InputHandler.h index 163e02e..ebe25fe 100644 --- a/examples/LinkCable_full/src/utils/InputHandler.h +++ b/examples/LinkCable_full/src/utils/InputHandler.h @@ -36,4 +36,4 @@ class InputHandler { bool isWaiting = false; }; -#endif // INPUT_HANDLER_H \ No newline at end of file +#endif // INPUT_HANDLER_H diff --git a/examples/LinkCable_stress/Makefile b/examples/LinkCable_stress/Makefile index 1f143ca..e96aa55 100644 --- a/examples/LinkCable_stress/Makefile +++ b/examples/LinkCable_stress/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkCable_stress/src/main.cpp b/examples/LinkCable_stress/src/main.cpp index c6ce241..4e87685 100644 --- a/examples/LinkCable_stress/src/main.cpp +++ b/examples/LinkCable_stress/src/main.cpp @@ -1,7 +1,3 @@ -#include "main.h" -#include -#include "../../_lib/interrupt.h" - // STRESS: // This example can perform multiple stress tests. // A) Packet loss test: @@ -18,6 +14,10 @@ // R) Measure ping-pong latency: // - Like (L), but adding a validation response and adding that time. +#include "main.h" +#include +#include "../../_lib/interrupt.h" + #define FINAL_VALUE 65534 void test(bool withSync); @@ -34,25 +34,22 @@ u32 toMs(u32 cycles); #ifndef USE_LINK_UNIVERSAL LinkCable* linkCable = new LinkCable(); LinkCable* linkConnection = linkCable; -#endif -#ifdef USE_LINK_UNIVERSAL +#else LinkUniversal* linkUniversal = new LinkUniversal(LinkUniversal::Protocol::AUTODETECT, "LinkUniversal", (LinkUniversal::CableOptions){ .baudRate = LinkCable::BAUD_RATE_1, .timeout = LINK_CABLE_DEFAULT_TIMEOUT, - .remoteTimeout = LINK_CABLE_DEFAULT_REMOTE_TIMEOUT, .interval = LINK_CABLE_DEFAULT_INTERVAL, .sendTimerId = LINK_CABLE_DEFAULT_SEND_TIMER_ID}, (LinkUniversal::WirelessOptions){ .retransmission = true, .maxPlayers = 2, .timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, - .remoteTimeout = LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, .interval = LINK_WIRELESS_DEFAULT_INTERVAL, - .sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID, - .asyncACKTimerId = 0}); + .sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID}, + __qran_seed); LinkUniversal* linkConnection = linkUniversal; #endif @@ -70,8 +67,7 @@ void init() { interrupt_enable(INTR_SERIAL); interrupt_set_handler(INTR_TIMER3, LINK_CABLE_ISR_TIMER); interrupt_enable(INTR_TIMER3); -#endif -#ifdef USE_LINK_UNIVERSAL +#else // LinkUniversal interrupt_set_handler(INTR_VBLANK, LINK_UNIVERSAL_ISR_VBLANK); interrupt_enable(INTR_VBLANK); @@ -79,8 +75,6 @@ void init() { interrupt_enable(INTR_SERIAL); interrupt_set_handler(INTR_TIMER3, LINK_UNIVERSAL_ISR_TIMER); interrupt_enable(INTR_TIMER3); - interrupt_set_handler(INTR_TIMER0, LINK_UNIVERSAL_ISR_ACK_TIMER); - interrupt_enable(INTR_TIMER0); #endif } @@ -89,10 +83,9 @@ int main() { while (true) { #ifndef USE_LINK_UNIVERSAL - std::string output = "LinkCable_stress (v6.3.0)\n\n"; -#endif -#ifdef USE_LINK_UNIVERSAL - std::string output = "LinkUniversal_stress (v6.3.0)\n\n"; + std::string output = "LinkCable_stress (v7.0.0)\n\n"; +#else + std::string output = "LinkUniversal_stress (v7.0.0)\n\n"; #endif linkConnection->deactivate(); @@ -117,8 +110,7 @@ int main() { interval = 10; #ifndef USE_LINK_UNIVERSAL linkConnection->config.interval = interval; -#endif -#ifdef USE_LINK_UNIVERSAL +#else linkConnection->linkCable->config.interval = interval; linkConnection->linkWireless->config.interval = interval; #endif @@ -292,11 +284,13 @@ void measureLatency(bool withPong) { totalMs += elapsedMilliseconds; u32 average = Div(totalMs, samples); + std::string output = "Ping latency: \n " + + std::to_string(elapsedCycles) + " cycles\n " + + std::to_string(elapsedMilliseconds) + " ms\n " + + std::to_string(average) + " ms avg" + + "\nValue sent:\n " + std::to_string(sentPacket); VBlankIntrWait(); - log("Ping latency: \n " + std::to_string(elapsedCycles) + " cycles\n " + - std::to_string(elapsedMilliseconds) + " ms\n " + - std::to_string(average) + " ms avg" + "\nValue sent:\n " + - std::to_string(sentPacket)); + log(output); } else { VBlankIntrWait(); log("Waiting..."); @@ -366,4 +360,4 @@ u32 toMs(u32 cycles) { // CPU Frequency * time per frame = cycles per frame // 16780000 * (1/60) ~= 279666 return (cycles * 1000) / (279666 * 60); -} \ No newline at end of file +} diff --git a/examples/LinkCable_stress/src/main.h b/examples/LinkCable_stress/src/main.h index b7001b1..f194cae 100644 --- a/examples/LinkCable_stress/src/main.h +++ b/examples/LinkCable_stress/src/main.h @@ -1,16 +1,19 @@ #ifndef MAIN_H #define MAIN_H -#include -#include "../../../lib/LinkCable.hpp" -#include "../../../lib/LinkUniversal.hpp" - // #define USE_LINK_UNIVERSAL #ifndef USE_LINK_UNIVERSAL -extern LinkCable* linkConnection; +#include "../../../lib/LinkCable.hpp" +#else +#include "../../../lib/LinkUniversal.hpp" #endif -#ifdef USE_LINK_UNIVERSAL + +#include + +#ifndef USE_LINK_UNIVERSAL +extern LinkCable* linkConnection; +#else extern LinkUniversal* linkConnection; #endif diff --git a/examples/LinkCube_demo/Makefile b/examples/LinkCube_demo/Makefile new file mode 100644 index 0000000..05e014f --- /dev/null +++ b/examples/LinkCube_demo/Makefile @@ -0,0 +1,285 @@ +# +# Template tonc makefile +# +# Yoinked mostly from DKP's template +# + +# === SETUP =========================================================== + +# --- No implicit rules --- +.SUFFIXES: + +# --- Paths --- +export TONCLIB := ${DEVKITPRO}/libtonc + +# === TONC RULES ====================================================== +# +# Yes, this is almost, but not quite, completely like to +# DKP's base_rules and gba_rules +# + +export PATH := $(DEVKITARM)/bin:$(PATH) + + +# --- Executable names --- + +PREFIX ?= arm-none-eabi- + +export CC := $(PREFIX)gcc +export CXX := $(PREFIX)g++ +export AS := $(PREFIX)as +export AR := $(PREFIX)ar +export NM := $(PREFIX)nm +export OBJCOPY := $(PREFIX)objcopy + +# LD defined in Makefile + + +# === LINK / TRANSLATE ================================================ + +%.gba : %.elf + @$(OBJCOPY) -O binary $< $@ + @echo built ... $(notdir $@) + @gbafix $@ -t$(TITLE) + +#---------------------------------------------------------------------- + +%.mb.elf : + @echo Linking multiboot + $(LD) -specs=gba_mb.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(NM) -Sn $@ > $(basename $(notdir $@)).map + +#---------------------------------------------------------------------- + +%.elf : + @echo Linking cartridge + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(NM) -Sn $@ > $(basename $(notdir $@)).map + +#---------------------------------------------------------------------- + +%.a : + @echo $(notdir $@) + @rm -f $@ + $(AR) -crs $@ $^ + + +# === OBJECTIFY ======================================================= + +%.iwram.o : %.iwram.cpp + @echo $(notdir $<) + $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ + +#---------------------------------------------------------------------- +%.iwram.o : %.iwram.c + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(IARCH) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.cpp + @echo $(notdir $<) + $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(RARCH) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.c + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(RARCH) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.s + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.S + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@ + + +#---------------------------------------------------------------------- +# canned command sequence for binary data +#---------------------------------------------------------------------- + +define bin2o + bin2s $< | $(AS) -o $(@) + echo "extern const u8" `(echo $( `(echo $(> `(echo $(> `(echo $( $(BUILD)/$(TARGET).map + +all : $(BUILD) + +clean: + @echo clean ... + @rm -rf $(BUILD) $(TARGET).elf $(TARGET).gba $(TARGET).sav + + +else # If we're here, we should be in the BUILD dir + +DEPENDS := $(OFILES:.o=.d) + +# --- Main targets ---- + +$(OUTPUT).gba : $(OUTPUT).elf + +$(OUTPUT).elf : $(OFILES) + +-include $(DEPENDS) + + +endif # End BUILD switch + +# --- More targets ---------------------------------------------------- + +.PHONY: clean rebuild start + +rebuild: clean $(BUILD) + +start: + start "$(TARGET).gba" + +restart: rebuild start + +# EOF diff --git a/examples/LinkCube_demo/src/main.cpp b/examples/LinkCube_demo/src/main.cpp new file mode 100644 index 0000000..0ad996e --- /dev/null +++ b/examples/LinkCube_demo/src/main.cpp @@ -0,0 +1,120 @@ +// (0) Include the header +#include "../../../lib/LinkCube.hpp" + +#include +#include +#include "../../_lib/interrupt.h" + +void log(std::string text); +void wait(u32 verticalLines); +bool didPress(unsigned short key, bool& pressed); +inline void VBLANK() {} + +bool a = false, b = false, l = false; + +// (1) Create a LinkCube instance +LinkCube* linkCube = new LinkCube(); + +void init() { + REG_DISPCNT = DCNT_MODE0 | DCNT_BG0; + tte_init_se_default(0, BG_CBB(0) | BG_SBB(31)); + + // (2) Add the interrupt service routines + interrupt_init(); + interrupt_set_handler(INTR_VBLANK, VBLANK); + interrupt_enable(INTR_VBLANK); + interrupt_set_handler(INTR_SERIAL, LINK_CUBE_ISR_SERIAL); + interrupt_enable(INTR_SERIAL); +} + +int main() { + init(); + + // (3) Initialize the library + linkCube->activate(); + + u32 counter = 0; + std::string received = ""; + bool reset = false; + u32 vblanks = 0; + + while (true) { + // Title + std::string output = + "LinkCube_demo (v7.0.0)" + std::string(reset ? " !RESET!" : "") + + "\n\nPress A to send\nPress B to clear\n (L = " + "+1024)\n\nLast sent: " + + std::to_string(counter) + + "\n(pending = " + std::to_string(linkCube->pendingCount()) + + ")\n\nReceived:\n" + received; + + // (4) Send 32-bit values + if (didPress(KEY_A, a)) { + counter++; + linkCube->send(counter); + } + + // +1024 + if (didPress(KEY_L, l)) { + counter += 1024; + linkCube->send(counter); + } + + // (5) Read 32-bit values + while (linkCube->canRead()) { + received += std::to_string(linkCube->read()) + ", "; + } + + // Clear + if (didPress(KEY_B, b)) + received = ""; + + // Reset warning + if (linkCube->didReset()) { + counter = 0; + reset = true; + vblanks = 0; + } + if (reset) { + vblanks++; + if (vblanks > 60) + reset = false; + } + + // Print + VBlankIntrWait(); + log(output); + } + + return 0; +} + +void log(std::string text) { + tte_erase_screen(); + tte_write("#{P:0,0}"); + tte_write(text.c_str()); +} + +void wait(u32 verticalLines) { + u32 count = 0; + u32 vCount = REG_VCOUNT; + + while (count < verticalLines) { + if (REG_VCOUNT != vCount) { + count++; + vCount = REG_VCOUNT; + } + }; +} + +bool didPress(u16 key, bool& pressed) { + u16 keys = ~REG_KEYS & KEY_ANY; + bool isPressedNow = false; + if ((keys & key) && !pressed) { + pressed = true; + isPressedNow = true; + } + if (pressed && !(keys & key)) + pressed = false; + return isPressedNow; +} diff --git a/examples/LinkGPIO_demo/Makefile b/examples/LinkGPIO_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkGPIO_demo/Makefile +++ b/examples/LinkGPIO_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkGPIO_demo/src/main.cpp b/examples/LinkGPIO_demo/src/main.cpp index 1f9cb63..5d598b8 100644 --- a/examples/LinkGPIO_demo/src/main.cpp +++ b/examples/LinkGPIO_demo/src/main.cpp @@ -1,9 +1,9 @@ -#include -#include - // (0) Include the header #include "../../../lib/LinkGPIO.hpp" +#include +#include + void log(std::string text); std::string mode(std::string name, LinkGPIO::Pin pin); std::string value(std::string name, LinkGPIO::Pin pin, bool isHigh); @@ -27,7 +27,7 @@ int main() { while (true) { // (3) Use the pins - std::string output = "LinkGPIO_demo (v6.3.0)\n\n"; + std::string output = "LinkGPIO_demo (v7.0.0)\n\n"; // Commands u16 keys = ~REG_KEYS & KEY_ANY; @@ -104,4 +104,4 @@ std::string value(std::string name, LinkGPIO::Pin pin, bool isHigh) { return linkGPIO->getMode(pin) == LinkGPIO::Direction::INPUT ? "< " + title + std::to_string(linkGPIO->readPin(pin)) + "\n" : "> " + title + std::to_string(isHigh) + "\n"; -} \ No newline at end of file +} diff --git a/examples/LinkMobile_demo/Makefile b/examples/LinkMobile_demo/Makefile new file mode 100644 index 0000000..05e014f --- /dev/null +++ b/examples/LinkMobile_demo/Makefile @@ -0,0 +1,285 @@ +# +# Template tonc makefile +# +# Yoinked mostly from DKP's template +# + +# === SETUP =========================================================== + +# --- No implicit rules --- +.SUFFIXES: + +# --- Paths --- +export TONCLIB := ${DEVKITPRO}/libtonc + +# === TONC RULES ====================================================== +# +# Yes, this is almost, but not quite, completely like to +# DKP's base_rules and gba_rules +# + +export PATH := $(DEVKITARM)/bin:$(PATH) + + +# --- Executable names --- + +PREFIX ?= arm-none-eabi- + +export CC := $(PREFIX)gcc +export CXX := $(PREFIX)g++ +export AS := $(PREFIX)as +export AR := $(PREFIX)ar +export NM := $(PREFIX)nm +export OBJCOPY := $(PREFIX)objcopy + +# LD defined in Makefile + + +# === LINK / TRANSLATE ================================================ + +%.gba : %.elf + @$(OBJCOPY) -O binary $< $@ + @echo built ... $(notdir $@) + @gbafix $@ -t$(TITLE) + +#---------------------------------------------------------------------- + +%.mb.elf : + @echo Linking multiboot + $(LD) -specs=gba_mb.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(NM) -Sn $@ > $(basename $(notdir $@)).map + +#---------------------------------------------------------------------- + +%.elf : + @echo Linking cartridge + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(NM) -Sn $@ > $(basename $(notdir $@)).map + +#---------------------------------------------------------------------- + +%.a : + @echo $(notdir $@) + @rm -f $@ + $(AR) -crs $@ $^ + + +# === OBJECTIFY ======================================================= + +%.iwram.o : %.iwram.cpp + @echo $(notdir $<) + $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ + +#---------------------------------------------------------------------- +%.iwram.o : %.iwram.c + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(IARCH) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.cpp + @echo $(notdir $<) + $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(RARCH) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.c + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) $(RARCH) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.s + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@ + +#---------------------------------------------------------------------- + +%.o : %.S + @echo $(notdir $<) + $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@ + + +#---------------------------------------------------------------------- +# canned command sequence for binary data +#---------------------------------------------------------------------- + +define bin2o + bin2s $< | $(AS) -o $(@) + echo "extern const u8" `(echo $( `(echo $(> `(echo $(> `(echo $( $(BUILD)/$(TARGET).map + +all : $(BUILD) + +clean: + @echo clean ... + @rm -rf $(BUILD) $(TARGET).elf $(TARGET).gba $(TARGET).sav + + +else # If we're here, we should be in the BUILD dir + +DEPENDS := $(OFILES:.o=.d) + +# --- Main targets ---- + +$(OUTPUT).gba : $(OUTPUT).elf + +$(OUTPUT).elf : $(OFILES) + +-include $(DEPENDS) + + +endif # End BUILD switch + +# --- More targets ---------------------------------------------------- + +.PHONY: clean rebuild start + +rebuild: clean $(BUILD) + +start: + start "$(TARGET).gba" + +restart: rebuild start + +# EOF diff --git a/examples/LinkMobile_demo/src/main.cpp b/examples/LinkMobile_demo/src/main.cpp new file mode 100644 index 0000000..ea8e1e7 --- /dev/null +++ b/examples/LinkMobile_demo/src/main.cpp @@ -0,0 +1,635 @@ +#define LINK_ENABLE_DEBUG_LOGS 0 + +// (0) Include the header +#include "../../../lib/LinkMobile.hpp" + +#include "main.h" + +#include +#include +#include "../../_lib/interrupt.h" + +// One transfer for every N frames +constexpr static int TRANSFER_FREQUENCY = 30; + +bool isConnected = false; +bool hasError = false; +u16 keys = 0; +std::string output = ""; + +LinkMobile::DataTransfer dataTransfer; +LinkMobile::DataTransfer lastCompletedTransfer; +LinkMobile::DNSQuery dnsQuery; +bool waitingDNS = false; +std::string outgoingData = ""; +u32 counter = 0; +u32 frameCounter = 0; + +bool left = false, right = false, up = false, down = false; +bool a = false, b = false, l = false, r = false; +bool start = false, select = false; +std::string selectedNumber = ""; +std::string selectedPassword = ""; +std::string selectedDomain = ""; + +LinkMobile* linkMobile = nullptr; + +void init() { + REG_DISPCNT = DCNT_MODE0 | DCNT_BG0; + tte_init_se_default(0, BG_CBB(0) | BG_SBB(31)); +} + +int main() { + init(); + +start: + log("LinkMobile_demo (v7.0.0)\n\nPress A to start"); + waitForA(); + + // (1) Create a LinkMobile instance + linkMobile = new LinkMobile(); + + // (2) Add the required interrupt service routines + interrupt_init(); + interrupt_set_handler(INTR_VBLANK, LINK_MOBILE_ISR_VBLANK); + interrupt_enable(INTR_VBLANK); + interrupt_set_handler(INTR_SERIAL, LINK_MOBILE_ISR_SERIAL); + interrupt_enable(INTR_SERIAL); + interrupt_set_handler(INTR_TIMER3, LINK_MOBILE_ISR_TIMER); + interrupt_enable(INTR_TIMER3); + + // (3) Initialize the library + linkMobile->activate(); + + while (true) { + keys = ~REG_KEYS & KEY_ANY; + hasError = linkMobile->getError().type != LinkMobile::Error::Type::NONE; + output = "State = " + getStateString(linkMobile->getState()) + "\n"; + + printMenu(); + + if (linkMobile->isConnectedP2P()) { + handleP2P(); + } else if (linkMobile->isConnectedPPP()) { + handlePPP(); + } else if (isConnected) { + cleanup(); + } + + // SELECT = stop + if (didPress(KEY_SELECT, select)) { + bool didShutdown = linkMobile->getState() == LinkMobile::State::SHUTDOWN; + + if (hasError || didShutdown) { + linkMobile->deactivate(); + interrupt_disable(INTR_VBLANK); + interrupt_disable(INTR_SERIAL); + interrupt_disable(INTR_TIMER3); + delete linkMobile; + linkMobile = nullptr; + + if (!didShutdown) { + log("Waiting..."); + wait(228 * 60 * 3); + } + + goto start; + } else if (linkMobile->canShutdown()) { + // (12) Turn off the adapter + linkMobile->shutdown(); + } + } + + switch (linkMobile->getState()) { + case LinkMobile::State::SESSION_ACTIVE: { + // L = Read Configuration + if (didPress(KEY_L, l)) { + readConfiguration(); + waitForA(); + } + + // R = Call someone + if (didPress(KEY_R, r)) { + std::string number = getNumberInput(); + if (number != "") { + // (4) Call someone + linkMobile->call(number.c_str()); + } + } + + // START = Call the ISP + if (didPress(KEY_START, start)) { + std::string password = getPasswordInput(); + if (password != "") { + // (7) Connect to the internet + linkMobile->callISP(password.c_str()); + } + } + break; + } + case LinkMobile::State::CALL_ESTABLISHED: { + // L = hang up + if (didPress(KEY_L, l)) { + // (6) Hang up + linkMobile->hangUp(); + } + break; + } + case LinkMobile::State::PPP_ACTIVE: { + // A = DNS query + if (didPress(KEY_A, a) && !waitingDNS) { + std::string domain = getDomainInput(); + if (domain != "") { + // (8) Run DNS queries + linkMobile->dnsQuery(domain.c_str(), &dnsQuery); + waitingDNS = true; + } + } + // L = hang up + if (didPress(KEY_L, l)) { + // (6) Hang up + linkMobile->hangUp(); + } + break; + } + default: { + } + } + + VBlankIntrWait(); + log(output); + } + + return 0; +} + +void handleP2P() { + if (!isConnected) { + // First transfer + isConnected = true; + outgoingData = linkMobile->getRole() == LinkMobile::Role::CALLER + ? "caller!!!" + : "receiver!!!"; + transfer(dataTransfer, outgoingData, 0xff, true); + } + + if (dataTransfer.completed) { + // Save a copy of last received data + if (dataTransfer.size > 0) + lastCompletedTransfer = dataTransfer; + dataTransfer.completed = false; + } + + if (keys & KEY_A) { + // `A` increments the counter + counter++; + outgoingData = + (linkMobile->getRole() == LinkMobile::Role::CALLER ? "caller: " + : "receiver: ") + + std::to_string(counter); + } + + frameCounter++; + if (frameCounter >= TRANSFER_FREQUENCY) { + // Transfer every N frames + frameCounter = 0; + transfer(dataTransfer, outgoingData, 0xff, true); + } + + if (lastCompletedTransfer.completed) { + // Show received data + output += "\n\n>> " + std::string(outgoingData); + output += "\n<< " + std::string((char*)lastCompletedTransfer.data); + // (LinkMobile zero-pads an extra byte, so this is safe) + } +} + +void handlePPP() { + if (!isConnected) + isConnected = true; + + if (waitingDNS && dnsQuery.completed) { + waitingDNS = false; + log("DNS Response:\n " + std::to_string(dnsQuery.ipv4[0]) + "." + + std::to_string(dnsQuery.ipv4[1]) + "." + + std::to_string(dnsQuery.ipv4[2]) + "." + + std::to_string(dnsQuery.ipv4[3]) + "\n\n" + + (dnsQuery.success ? "OK!\nLet's connect to it on TCP 80!" + : "DNS query failed!")); + waitForA(); + if (!dnsQuery.success) + return; + + // (9) Open connections + log("Connecting..."); + LinkMobile::OpenConn openConn; + linkMobile->openConnection(dnsQuery.ipv4, 80, + LinkMobile::ConnectionType::TCP, &openConn); + if (!linkMobile->waitFor(&openConn)) { + log("Connection failed!"); + waitForA(); + return; + } + + // HTTP request + + LinkMobile::DataTransfer http; + std::string request = + std::string("GET / HTTP/1.1\r\nHost: ") + selectedDomain + "\r\n\r\n"; + std::string output = ""; + u32 chunk = 1; + u32 retry = 1; + do { + log("Downloading... (" + std::to_string(chunk) + ", " + + std::to_string(retry) + ")\n (hold START = close conn)\n\n" + output); + + if (didPress(KEY_START, start)) { + log("Closing..."); + LinkMobile::CloseConn closeConn; + linkMobile->closeConnection( + openConn.connectionId, LinkMobile::ConnectionType::TCP, &closeConn); + linkMobile->waitFor(&closeConn); + return; + } + + transfer(http, request, openConn.connectionId); + if (!linkMobile->waitFor(&http)) { + log("Connection closed:\n " + std::to_string(chunk) + " packets!\n\n" + + output); + waitForA(); + return; + } + + if (http.size > 0) { + chunk++; + output += std::string((char*)http.data); + // (LinkMobile zero-pads an extra byte, so this is safe) + } + + http = {}; + request = ""; + retry++; + } while (true); + } + + output += waitingDNS ? "\n\nWaiting DNS..." : ""; +} + +void cleanup() { + isConnected = false; + dataTransfer = {}; + lastCompletedTransfer = {}; + dnsQuery = {}; + waitingDNS = false; + counter = 0; + frameCounter = 0; + outgoingData = ""; +} + +void readConfiguration() { + LinkMobile::ConfigurationData data; + if (!linkMobile->readConfiguration(data)) + log("Read failed :("); + + log("Magic:\n " + toStr(data.magic, 2) + ", $" + + toHex(data.registrationState) + "\nPrimary DNS:\n " + + std::to_string(data.primaryDNS[0]) + "." + + std::to_string(data.primaryDNS[1]) + "." + + std::to_string(data.primaryDNS[2]) + "." + + std::to_string(data.primaryDNS[3]) + "\nSecondary DNS:\n " + + std::to_string(data.secondaryDNS[0]) + "." + + std::to_string(data.secondaryDNS[1]) + "." + + std::to_string(data.secondaryDNS[2]) + "." + + std::to_string(data.secondaryDNS[3]) + "\nLoginID:\n " + + toStr(data.loginId, 10) + "\nEmail:\n " + toStr(data.email, 24) + + "\nSMTP Server:\n " + toStr(data.smtpServer, 20) + "\nPOP Server:\n " + + toStr(data.popServer, 19) + "\nISP Number #1:\n " + + std::string(data._ispNumber1) + "\n\nIs Valid: " + + std::to_string(linkMobile->isConfigurationValid()) + "\nMode: " + + (linkMobile->getDataSize() == LinkSPI::DataSize::SIZE_32BIT ? "SIO32" + : "SIO8")); +} + +void printMenu() { + auto error = linkMobile->getError(); + + if (hasError) { + output += getErrorString(error); + output += "\n (SELECT = stop)"; + } else if (linkMobile->getState() == LinkMobile::State::SESSION_ACTIVE) { + output += "\nL = Read configuration"; + output += "\nR = Call someone"; + output += "\nSTART = Call the ISP"; + output += "\n\n (A = ok)\n (SELECT = stop)"; + } else { + if (linkMobile->isConnectedP2P()) { + output += "\n (A = send)"; + output += "\n (L = hang up)"; + } else if (linkMobile->isConnectedPPP()) { + output += "\n (A = DNS query)"; + output += "\n (L = hang up)"; + } + output += "\n (SELECT = stop)"; + } +} + +void transfer(LinkMobile::DataTransfer& dataTransfer, + std::string text, + u8 connectionId, + bool addNullTerminator) { + // (5) Send/receive data + for (u32 i = 0; i < text.size(); i++) + dataTransfer.data[i] = text[i]; + if (addNullTerminator) + dataTransfer.data[text.size()] = '\0'; + dataTransfer.size = text.size() + addNullTerminator; + linkMobile->transfer(dataTransfer, &dataTransfer, connectionId); +} + +std::string getNumberInput() { + std::vector> rows; + rows.push_back({"1", "2", "3"}); + rows.push_back({"4", "5", "6"}); + rows.push_back({"7", "8", "9"}); + rows.push_back({"*", "0", "#"}); + std::vector> altRows; + + return getInput(selectedNumber, LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH, + "a number", rows, altRows, {{"localhost", "127000000001"}}, + ""); +} + +std::string getPasswordInput() { + return getTextInput(selectedPassword, LINK_MOBILE_MAX_PASSWORD_LENGTH, + "your password", {{"pass123", "pass123"}}); +} + +std::string getDomainInput() { + return getTextInput( + selectedDomain, LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH, "a domain name", + {{"something.com", "something.com"}, {"localhost", "localhost"}}); +} + +std::string getTextInput(std::string& field, + u32 maxChars, + std::string inputName, + std::vector defaultValues) { + std::vector> rows; + rows.push_back({"a", "b", "c", "d", "e"}); + rows.push_back({"f", "g", "h", "i", "j"}); + rows.push_back({"k", "l", "m", "n", "o"}); + rows.push_back({"p", "q", "r", "s", "t"}); + rows.push_back({"u", "v", "w", "x", "y"}); + rows.push_back({"z", "1", "2", "3", "4"}); + rows.push_back({"5", "6", "7", "8", "9"}); + rows.push_back({"0", ".", "#", "/", "?"}); + + std::vector> altRows; + altRows.push_back({"A", "B", "C", "D", "E"}); + altRows.push_back({"F", "G", "H", "I", "J"}); + altRows.push_back({"K", "L", "M", "N", "O"}); + altRows.push_back({"P", "Q", "R", "S", "T"}); + altRows.push_back({"U", "V", "W", "X", "Y"}); + altRows.push_back({"Z", "1", "2", "3", "4"}); + altRows.push_back({"5", "6", "7", "8", "9"}); + altRows.push_back({"0", ".", "#", "/", "?"}); + + return getInput(field, maxChars, inputName, rows, altRows, defaultValues, + "caps lock"); +} + +std::string getInput(std::string& field, + u32 maxChars, + std::string inputName, + std::vector> rows, + std::vector> altRows, + std::vector defaultValues, + std::string altName) { + VBlankIntrWait(); + + int selectedX = 0; + int selectedY = 0; + int selectedDefaultValue = 0; + bool altActive = false; + + while (true) { + auto renderRows = altActive ? altRows : rows; + + std::string output = "Type " + inputName + ":\n\n"; + output += ">> " + field + "\n\n"; + + if (didPress(KEY_RIGHT, right)) { + selectedX++; + if (selectedX >= (int)renderRows[0].size()) + selectedX = renderRows[0].size() - 1; + } + if (didPress(KEY_LEFT, left)) { + selectedX--; + if (selectedX < 0) + selectedX = 0; + } + if (didPress(KEY_UP, up)) { + selectedY--; + if (selectedY < 0) + selectedY = 0; + } + if (didPress(KEY_DOWN, down)) { + selectedY++; + if (selectedY >= (int)renderRows.size()) + selectedY = renderRows.size() - 1; + } + if (didPress(KEY_B, b)) { + if (field.size() > 0) + field = field.substr(0, field.size() - 1); + else + return ""; + } + if (didPress(KEY_A, a)) { + if (field.size() < maxChars) + field += renderRows[selectedY][selectedX]; + } + if (didPress(KEY_SELECT, select)) { + field = defaultValues[selectedDefaultValue].value; + selectedDefaultValue = (selectedDefaultValue + 1) % defaultValues.size(); + } + if (didPress(KEY_START, start)) + return field; + if (altName != "" && didPress(KEY_L, l)) + altActive = !altActive; + + for (int y = 0; y < (int)renderRows.size(); y++) { + for (int x = 0; x < (int)renderRows[y].size(); x++) { + bool isSelected = selectedX == x && selectedY == y; + output += std::string("") + "|" + (isSelected ? "<" : " ") + + renderRows[y][x] + (isSelected ? ">" : " ") + "|"; + output += " "; + } + output += "\n"; + } + + output += "\n (B = back)\n (A = select)\n (SELECT = " + + defaultValues[selectedDefaultValue].name + + ")\n (START = confirm)"; + + if (altName != "") + output += "\n\n (L = " + altName + ")"; + + VBlankIntrWait(); + log(output); + } +} + +std::string getStateString(LinkMobile::State state) { + switch (state) { + case LinkMobile::State::NEEDS_RESET: + return "NEEDS_RESET"; + case LinkMobile::State::PINGING: + return "PINGING"; + case LinkMobile::State::WAITING_TO_START: + return "WAITING_TO_START"; + case LinkMobile::State::STARTING_SESSION: + return "STARTING_SESSION"; + case LinkMobile::State::ACTIVATING_SIO32: + return "ACTIVATING_SIO32"; + case LinkMobile::State::WAITING_32BIT_SWITCH: + return "WAITING_32BIT_SWITCH"; + case LinkMobile::State::READING_CONFIGURATION: + return "READING_CONFIGURATION"; + case LinkMobile::State::SESSION_ACTIVE: + return "SESSION_ACTIVE"; + case LinkMobile::State::CALL_REQUESTED: + return "CALL_REQUESTED"; + case LinkMobile::State::CALLING: + return "CALLING"; + case LinkMobile::State::CALL_ESTABLISHED: + return "CALL_ESTABLISHED"; + case LinkMobile::State::ISP_CALL_REQUESTED: + return "ISP_CALL_REQUESTED"; + case LinkMobile::State::ISP_CALLING: + return "ISP_CALLING"; + case LinkMobile::State::PPP_LOGIN: + return "PPP_LOGIN"; + case LinkMobile::State::PPP_ACTIVE: + return "PPP_ACTIVE"; + case LinkMobile::State::SHUTDOWN_REQUESTED: + return "SHUTDOWN_REQUESTED"; + case LinkMobile::State::ENDING_SESSION: + return "ENDING_SESSION"; + case LinkMobile::State::WAITING_8BIT_SWITCH: + return "WAITING_8BIT_SWITCH"; + case LinkMobile::State::SHUTDOWN: + return "SHUTDOWN"; + default: + return "?"; + } +} + +std::string getErrorString(LinkMobile::Error error) { + return "ERROR" + "\n Type: " + + getErrorTypeString(error.type) + + "\n State: " + getStateString(error.state) + + "\n CmdID: " + std::string(error.cmdIsSending ? ">" : "<") + "$" + + toHex(error.cmdId) + + "\n CmdResult: " + getResultString(error.cmdResult) + + "\n CmdErrorCode: " + std::to_string(error.cmdErrorCode) + + "\n ReqType: " + std::to_string(error.reqType) + "\n"; +} + +std::string getErrorTypeString(LinkMobile::Error::Type errorType) { + switch (errorType) { + case LinkMobile::Error::Type::ADAPTER_NOT_CONNECTED: + return "ADAPTER_NOT_CONNECTED"; + case LinkMobile::Error::Type::PPP_LOGIN_FAILED: + return "PPP_LOGIN_FAILED"; + case LinkMobile::Error::Type::COMMAND_FAILED: + return "COMMAND_FAILED"; + case LinkMobile::Error::Type::WEIRD_RESPONSE: + return "WEIRD_RESPONSE"; + case LinkMobile::Error::Type::TIMEOUT: + return "TIMEOUT"; + case LinkMobile::Error::Type::WTF: + return "WTF"; + default: + return "?"; + } +} + +std::string getResultString(LinkMobile::CommandResult cmdResult) { + switch (cmdResult) { + case LinkMobile::CommandResult::PENDING: + return "PENDING"; + case LinkMobile::CommandResult::SUCCESS: + return "SUCCESS"; + case LinkMobile::CommandResult::INVALID_DEVICE_ID: + return "INVALID_DEVICE_ID"; + case LinkMobile::CommandResult::INVALID_COMMAND_ACK: + return "INVALID_COMMAND_ACK"; + case LinkMobile::CommandResult::INVALID_MAGIC_BYTES: + return "INVALID_MAGIC_BYTES"; + case LinkMobile::CommandResult::WEIRD_DATA_SIZE: + return "WEIRD_DATA_SIZE"; + case LinkMobile::CommandResult::WRONG_CHECKSUM: + return "WRONG_CHECKSUM"; + case LinkMobile::CommandResult::ERROR_CODE: + return "ERROR_CODE"; + case LinkMobile::CommandResult::WEIRD_ERROR_CODE: + return "WEIRD_ERROR_CODE"; + default: + return "?"; + } +} + +void log(std::string text) { + if (linkMobile != nullptr) + VBlankIntrWait(); + tte_erase_screen(); + tte_write("#{P:0,0}"); + tte_write(text.c_str()); +} + +std::string toStr(char* chars, int size) { + char copiedChars[255]; + for (int i = 0; i < size; i++) + copiedChars[i] = chars[i]; + copiedChars[size] = '\0'; + return std::string(copiedChars); +} + +void wait(u32 verticalLines) { + u32 count = 0; + u32 vCount = REG_VCOUNT; + + while (count < verticalLines) { + if (REG_VCOUNT != vCount) { + count++; + vCount = REG_VCOUNT; + } + }; +} + +bool didPress(u16 key, bool& pressed) { + u16 keys = ~REG_KEYS & KEY_ANY; + bool isPressedNow = false; + if ((keys & key) && !pressed) { + pressed = true; + isPressedNow = true; + } + if (pressed && !(keys & key)) + pressed = false; + return isPressedNow; +} + +void waitForA() { + while (!didPress(KEY_A, a)) + ; +} + +template +[[nodiscard]] std::string toHex(I w, size_t hex_len) { + static const char* digits = "0123456789ABCDEF"; + std::string rc(hex_len, '0'); + for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4) + rc[i] = digits[(w >> j) & 0x0f]; + return rc; +} diff --git a/examples/LinkMobile_demo/src/main.h b/examples/LinkMobile_demo/src/main.h new file mode 100644 index 0000000..911515c --- /dev/null +++ b/examples/LinkMobile_demo/src/main.h @@ -0,0 +1,54 @@ +#ifndef MAIN_H +#define MAIN_H + +#include "../../../lib/LinkMobile.hpp" + +#include +#include + +struct DefaultValue { + std::string name; + std::string value; +}; + +void handleP2P(); +void handlePPP(); +void cleanup(); +void readConfiguration(); + +void printMenu(); +void transfer(LinkMobile::DataTransfer& dataTransfer, + std::string text, + unsigned char connectionId, + bool addNullTerminator = false); + +std::string getNumberInput(); +std::string getPasswordInput(); +std::string getDomainInput(); +std::string getTextInput(std::string& field, + unsigned int maxChars, + std::string inputName, + std::vector defaultValues); +std::string getInput(std::string& field, + unsigned int maxChars, + std::string inputName, + std::vector> rows, + std::vector> altRows, + std::vector defaultValues, + std::string altName); + +std::string getStateString(LinkMobile::State state); +std::string getErrorString(LinkMobile::Error error); +std::string getErrorTypeString(LinkMobile::Error::Type errorType); +std::string getResultString(LinkMobile::CommandResult cmdResult); + +void log(std::string text); +std::string toStr(char* chars, int size); +void wait(unsigned int verticalLines); +bool didPress(unsigned short key, bool& pressed); +void waitForA(); + +template +[[nodiscard]] std::string toHex(I w, size_t hex_len = sizeof(I) << 1); + +#endif // MAIN_H diff --git a/examples/LinkPS2Keyboard_demo/Makefile b/examples/LinkPS2Keyboard_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkPS2Keyboard_demo/Makefile +++ b/examples/LinkPS2Keyboard_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkPS2Keyboard_demo/src/main.cpp b/examples/LinkPS2Keyboard_demo/src/main.cpp index 3822a07..c75e2c7 100644 --- a/examples/LinkPS2Keyboard_demo/src/main.cpp +++ b/examples/LinkPS2Keyboard_demo/src/main.cpp @@ -37,11 +37,13 @@ int main() { init(); while (true) { - std::string output = "LinkPS2Keyboard_demo (v6.3.0)\n\n"; + std::string output = "LinkPS2Keyboard_demo (v7.0.0)\n\n"; u16 keys = ~REG_KEYS & KEY_ANY; if (!linkPS2Keyboard->isActive()) { - output += "Press A to read keyboard input"; + output += + "Press A to read keyboard input\n" + "Press B to clear logs"; if (keys & KEY_A) { // (3) Initialize the library @@ -51,6 +53,10 @@ int main() { continue; } } else { + if (keys & KEY_B) { + scanCodes = ""; + irqs = 0; + } output += std::to_string(irqs) + " - " + scanCodes; } diff --git a/examples/LinkPS2Mouse_demo/Makefile b/examples/LinkPS2Mouse_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkPS2Mouse_demo/Makefile +++ b/examples/LinkPS2Mouse_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkPS2Mouse_demo/src/main.cpp b/examples/LinkPS2Mouse_demo/src/main.cpp index bf84ea5..e3b7840 100644 --- a/examples/LinkPS2Mouse_demo/src/main.cpp +++ b/examples/LinkPS2Mouse_demo/src/main.cpp @@ -1,10 +1,10 @@ +// (0) Include the header +#include "../../../lib/LinkPS2Mouse.hpp" + #include #include #include "../../_lib/interrupt.h" -// (0) Include the header -#include "../../../lib/LinkPS2Mouse.hpp" - void log(std::string text); inline void VBLANK() {} inline void TIMER() {} @@ -12,6 +12,10 @@ inline void TIMER() {} // (1) Create a LinkPS2Mouse instance LinkPS2Mouse* linkPS2Mouse = new LinkPS2Mouse(2); +inline void KEYPAD() { + SoftReset(); +} + void init() { REG_DISPCNT = DCNT_MODE0 | DCNT_BG0; tte_init_se_default(0, BG_CBB(0) | BG_SBB(31)); @@ -22,17 +26,24 @@ void init() { interrupt_enable(INTR_VBLANK); interrupt_set_handler(INTR_TIMER2, TIMER); interrupt_enable(INTR_TIMER2); + + // Interrupt to handle B event (to reset) + REG_KEYCNT = 0b10 | (1 << 14); + interrupt_set_handler(INTR_KEYPAD, KEYPAD); + interrupt_enable(INTR_KEYPAD); } int main() { init(); while (true) { - std::string output = "LinkPS2Mouse_demo (v6.3.0)\n\n"; + std::string output = "LinkPS2Mouse_demo (v7.0.0)\n\n"; u16 keys = ~REG_KEYS & KEY_ANY; if (!linkPS2Mouse->isActive()) { - output += "Press A to read mouse input"; + output += + "Press A to read mouse input\n" + "Press B to cancel"; if (keys & KEY_A) { // (3) Initialize the library diff --git a/examples/LinkRawCable_demo/Makefile b/examples/LinkRawCable_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkRawCable_demo/Makefile +++ b/examples/LinkRawCable_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkRawCable_demo/src/main.cpp b/examples/LinkRawCable_demo/src/main.cpp index 8afe7eb..466d870 100644 --- a/examples/LinkRawCable_demo/src/main.cpp +++ b/examples/LinkRawCable_demo/src/main.cpp @@ -1,10 +1,10 @@ +// (0) Include the header +#include "../../../lib/LinkRawCable.hpp" + #include #include #include "../../_lib/interrupt.h" -// (0) Include the header -#include "../../../lib/LinkRawCable.hpp" - void log(std::string text); void wait(u32 verticalLines); inline void VBLANK() {} @@ -33,7 +33,7 @@ int main() { u16 prevKeys = 0; while (true) { - std::string output = "LinkRawCable_demo (v6.3.0)\n\n"; + std::string output = "LinkRawCable_demo (v7.0.0)\n\n"; u16 keys = ~REG_KEYS & KEY_ANY; if (!linkRawCable->isActive()) { @@ -131,4 +131,4 @@ void wait(u32 verticalLines) { vCount = REG_VCOUNT; } }; -} \ No newline at end of file +} diff --git a/examples/LinkRawWireless_demo/Makefile b/examples/LinkRawWireless_demo/Makefile index 9ad0b2f..8664077 100644 --- a/examples/LinkRawWireless_demo/Makefile +++ b/examples/LinkRawWireless_demo/Makefile @@ -126,7 +126,6 @@ BUILD := build SRCDIRS := src ../_lib ../../lib \ src/scenes \ src/utils - DATADIRS := INCDIRS := src LIBDIRS := $(TONCLIB) $(PWD)/../_lib/libugba $(PWD)/../_lib/libgba-sprite-engine @@ -156,10 +155,11 @@ CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- diff --git a/examples/LinkRawWireless_demo/src/main.cpp b/examples/LinkRawWireless_demo/src/main.cpp index b44ec52..260432a 100644 --- a/examples/LinkRawWireless_demo/src/main.cpp +++ b/examples/LinkRawWireless_demo/src/main.cpp @@ -1,6 +1,7 @@ +#include "../../../lib/LinkRawWireless.hpp" + #include #include -#include "../../../lib/LinkRawWireless.hpp" #include "../../_lib/interrupt.h" #include "scenes/DebugScene.h" #include "utils/SceneUtils.h" diff --git a/examples/LinkRawWireless_demo/src/scenes/DebugScene.cpp b/examples/LinkRawWireless_demo/src/scenes/DebugScene.cpp index 34651d8..11cf04b 100644 --- a/examples/LinkRawWireless_demo/src/scenes/DebugScene.cpp +++ b/examples/LinkRawWireless_demo/src/scenes/DebugScene.cpp @@ -1,3 +1,5 @@ +#include "../../../../lib/LinkRawWireless.hpp" + #include "DebugScene.h" #include @@ -5,7 +7,6 @@ #include #include -#include "../../../../lib/LinkRawWireless.hpp" #include "utils/InputHandler.h" #include "utils/SceneUtils.h" @@ -139,7 +140,7 @@ void DebugScene::load() { log("---"); log("LinkRawWireless demo"); - log(" (v6.3.0)"); + log(" (v7.0.0)"); log(""); log("START: reset wireless adapter"); log("A: send command"); @@ -771,7 +772,7 @@ genericWait: } int DebugScene::selectServerId() { - switch (selectOption("Which server id?", std::vector{ + switch (selectOption("Which server ID?", std::vector{ "", "", "", "", ""})) { case 0: { diff --git a/examples/LinkRawWireless_demo/src/utils/InputHandler.h b/examples/LinkRawWireless_demo/src/utils/InputHandler.h index 163e02e..ebe25fe 100644 --- a/examples/LinkRawWireless_demo/src/utils/InputHandler.h +++ b/examples/LinkRawWireless_demo/src/utils/InputHandler.h @@ -36,4 +36,4 @@ class InputHandler { bool isWaiting = false; }; -#endif // INPUT_HANDLER_H \ No newline at end of file +#endif // INPUT_HANDLER_H diff --git a/examples/LinkSPI_demo/Makefile b/examples/LinkSPI_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkSPI_demo/Makefile +++ b/examples/LinkSPI_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkSPI_demo/src/main.cpp b/examples/LinkSPI_demo/src/main.cpp index 82312fc..feb52c7 100644 --- a/examples/LinkSPI_demo/src/main.cpp +++ b/examples/LinkSPI_demo/src/main.cpp @@ -1,10 +1,10 @@ +// (0) Include the header +#include "../../../lib/LinkSPI.hpp" + #include #include #include "../../_lib/interrupt.h" -// (0) Include the header -#include "../../../lib/LinkSPI.hpp" - void log(std::string text); void wait(u32 verticalLines); inline void VBLANK() {} @@ -32,7 +32,7 @@ int main() { u32 counter = 0; while (true) { - std::string output = "LinkSPI_demo (v6.3.0)\n\n"; + std::string output = "LinkSPI_demo (v7.0.0)\n\n"; u16 keys = ~REG_KEYS & KEY_ANY; if (!linkSPI->isActive()) { @@ -42,14 +42,17 @@ int main() { output += "\n(stop: press L+R)\n"; output += "(hold A on start for async)\n"; output += "(hold B on start for waitMode)\n"; + output += "(hold UP for 8-bit mode)\n"; output += - "\n\n\n\n\n\n\n\n\n\n\n\n[!] to test this demo...\n " + "\n\n\n\n\n\n\n\n\n[!] to test this demo...\n " "...use a GBC Link Cable!"; if ((keys & KEY_START) | (keys & KEY_SELECT)) { // (3) Initialize the library linkSPI->activate((keys & KEY_START) ? LinkSPI::Mode::MASTER_256KBPS - : LinkSPI::Mode::SLAVE); + : LinkSPI::Mode::SLAVE, + (keys & KEY_UP) ? LinkSPI::DataSize::SIZE_8BIT + : LinkSPI::DataSize::SIZE_32BIT); linkSPI->setWaitModeActive(keys & KEY_B); // see `waitMode` in README.md if (keys & KEY_A) @@ -123,4 +126,4 @@ void wait(u32 verticalLines) { vCount = REG_VCOUNT; } }; -} \ No newline at end of file +} diff --git a/examples/LinkUART_demo/Makefile b/examples/LinkUART_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkUART_demo/Makefile +++ b/examples/LinkUART_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkUART_demo/Test.js b/examples/LinkUART_demo/Test.js index 1c2ed82..4b77f35 100644 --- a/examples/LinkUART_demo/Test.js +++ b/examples/LinkUART_demo/Test.js @@ -1,13 +1,14 @@ -const { SerialPort, ReadlineParser } = require('serialport') // "^12.0.0" +// node v14 +const { SerialPort, ReadlineParser } = require("serialport"); // "^12.0.0" var serialPort = new SerialPort({ path: "COM9", // (*nix: /dev/ttyACMX, Windows: COMX) - baudRate: 9600 + baudRate: 9600, }); const parser = serialPort.pipe(new ReadlineParser()); -parser.on('data', (it) => console.log(it)) +parser.on("data", (it) => console.log(it)); setInterval(() => { - serialPort.write('<< node\n') -}, 1000); \ No newline at end of file + serialPort.write("<< node\n"); +}, 1000); diff --git a/examples/LinkUART_demo/src/main.cpp b/examples/LinkUART_demo/src/main.cpp index cef56a6..394d085 100644 --- a/examples/LinkUART_demo/src/main.cpp +++ b/examples/LinkUART_demo/src/main.cpp @@ -1,10 +1,10 @@ +// (0) Include the header +#include "../../../lib/LinkUART.hpp" + #include #include #include "../../_lib/interrupt.h" -// (0) Include the header -#include "../../../lib/LinkUART.hpp" - void log(std::string text); inline void VBLANK() {} @@ -32,7 +32,7 @@ int main() { bool firstTransfer = false; while (true) { - std::string output = "LinkUART_demo (v6.3.0)\n\n"; + std::string output = "LinkUART_demo (v7.0.0)\n\n"; u16 keys = ~REG_KEYS & KEY_ANY; if (!linkUART->isActive()) { diff --git a/examples/LinkUniversal_basic/Makefile b/examples/LinkUniversal_basic/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkUniversal_basic/Makefile +++ b/examples/LinkUniversal_basic/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkUniversal_basic/src/main.cpp b/examples/LinkUniversal_basic/src/main.cpp index 62049bf..f806ef3 100644 --- a/examples/LinkUniversal_basic/src/main.cpp +++ b/examples/LinkUniversal_basic/src/main.cpp @@ -1,17 +1,17 @@ -#include -#include -#include "../../_lib/interrupt.h" - // BASIC: // This example sends the pressed buttons to other players. // (0) Include the header #include "../../../lib/LinkUniversal.hpp" +#include +#include +#include "../../_lib/interrupt.h" + void log(std::string text); void waitFor(u16 key); -LinkUniversal* linkUniversal = NULL; +LinkUniversal* linkUniversal = nullptr; void init() { REG_DISPCNT = DCNT_MODE0 | DCNT_BG0; @@ -21,11 +21,13 @@ void init() { int main() { init(); - log("LinkUniversal_basic (v6.3.0)\n\n\nPress A to start\n\n\nhold LEFT on " - "start:\n -> force cable\n\nhold RIGHT on start:\n -> force " - "wireless\n\nhold UP on start:\n -> force wireless server\n\nhold DOWN " - "on start:\n -> force wireless client\n\nhold B on start:\n -> set 2 " - "players (wireless)"); + log("LinkUniversal_basic (v7.0.0)\n\n\n" + "Press A to start\n\n\n" + "hold LEFT on start:\n -> force cable\n\n" + "hold RIGHT on start:\n -> force wireless\n\n" + "hold UP on start:\n -> force wireless server\n\n" + "hold DOWN on start:\n -> force wireless client\n\n" + "hold B on start:\n -> set 2 players (wireless)"); waitFor(KEY_A); u16 initialKeys = ~REG_KEYS & KEY_ANY; bool forceCable = initialKeys & KEY_LEFT; @@ -41,22 +43,20 @@ int main() { u32 maxPlayers = (initialKeys & KEY_B) ? 2 : LINK_UNIVERSAL_MAX_PLAYERS; // (1) Create a LinkUniversal instance - linkUniversal = new LinkUniversal( - protocol, "LinkUNI", - (LinkUniversal::CableOptions){ - .baudRate = LinkCable::BAUD_RATE_1, - .timeout = LINK_CABLE_DEFAULT_TIMEOUT, - .remoteTimeout = LINK_CABLE_DEFAULT_REMOTE_TIMEOUT, - .interval = LINK_CABLE_DEFAULT_INTERVAL, - .sendTimerId = LINK_CABLE_DEFAULT_SEND_TIMER_ID}, - (LinkUniversal::WirelessOptions){ - .retransmission = true, - .maxPlayers = maxPlayers, - .timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, - .remoteTimeout = LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, - .interval = LINK_WIRELESS_DEFAULT_INTERVAL, - .sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID, - .asyncACKTimerId = LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID}); + linkUniversal = + new LinkUniversal(protocol, "LinkUNI", + (LinkUniversal::CableOptions){ + .baudRate = LinkCable::BAUD_RATE_1, + .timeout = LINK_CABLE_DEFAULT_TIMEOUT, + .interval = LINK_CABLE_DEFAULT_INTERVAL, + .sendTimerId = LINK_CABLE_DEFAULT_SEND_TIMER_ID}, + (LinkUniversal::WirelessOptions){ + .retransmission = true, + .maxPlayers = maxPlayers, + .timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, + .interval = LINK_WIRELESS_DEFAULT_INTERVAL, + .sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID}, + __qran_seed); // (2) Add the required interrupt service routines interrupt_init(); @@ -131,4 +131,4 @@ void waitFor(u16 key) { do { keys = ~REG_KEYS & KEY_ANY; } while (!(keys & key)); -} \ No newline at end of file +} diff --git a/examples/LinkUniversal_full/README.md b/examples/LinkUniversal_full/README.md index 6053a46..222e3ec 100644 --- a/examples/LinkUniversal_full/README.md +++ b/examples/LinkUniversal_full/README.md @@ -2,4 +2,4 @@ - Reuse code from `examples/LinkCable_full` - Uncomment `USE_LINK_UNIVERSAL` in `src/main.h` -- Compile! \ No newline at end of file +- Compile! diff --git a/examples/LinkUniversal_stress/README.md b/examples/LinkUniversal_stress/README.md index 8db2b42..6434f38 100644 --- a/examples/LinkUniversal_stress/README.md +++ b/examples/LinkUniversal_stress/README.md @@ -2,4 +2,4 @@ - Reuse code from `examples/LinkCable_stress` - Uncomment `USE_LINK_UNIVERSAL` in `src/main.h` -- Compile! \ No newline at end of file +- Compile! diff --git a/examples/LinkWirelessMultiboot_demo/Makefile b/examples/LinkWirelessMultiboot_demo/Makefile index 5b2ffc2..754f919 100644 --- a/examples/LinkWirelessMultiboot_demo/Makefile +++ b/examples/LinkWirelessMultiboot_demo/Makefile @@ -123,11 +123,9 @@ TITLE := $(PROJ) LIBS := -ltonc -lugba -lgba-sprite-engine BUILD := build -SRCDIRS := src ../_lib ../../lib \ +SRCDIRS := src ../_lib ../../lib ../_lib/libgbfs \ src/scenes \ - src/utils \ - src/utils/gbfs - + src/utils DATADIRS := INCDIRS := src LIBDIRS := $(TONCLIB) $(PWD)/../_lib/libugba $(PWD)/../_lib/libgba-sprite-engine @@ -157,10 +155,11 @@ CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -283,7 +282,7 @@ endif # End BUILD switch rebuild: clean $(BUILD) package package: $(BUILD) - ./scripts/package.sh + ../gbfs.sh "$(TARGET).gba" content.gbfs "$(TARGET).out.gba" start: package start "$(TARGET).out.gba" diff --git a/examples/LinkWirelessMultiboot_demo/content.gbfs b/examples/LinkWirelessMultiboot_demo/content.gbfs index 0bfec9e..9518bd6 100644 Binary files a/examples/LinkWirelessMultiboot_demo/content.gbfs and b/examples/LinkWirelessMultiboot_demo/content.gbfs differ diff --git a/examples/LinkWirelessMultiboot_demo/src/main.cpp b/examples/LinkWirelessMultiboot_demo/src/main.cpp index a326e8f..003a153 100644 --- a/examples/LinkWirelessMultiboot_demo/src/main.cpp +++ b/examples/LinkWirelessMultiboot_demo/src/main.cpp @@ -1,6 +1,7 @@ +#include "../../../lib/LinkWirelessMultiboot.hpp" + #include #include -#include "../../../lib/LinkWirelessMultiboot.hpp" #include "../../_lib/interrupt.h" #include "scenes/MultibootScene.h" #include "utils/SceneUtils.h" @@ -16,8 +17,6 @@ LinkWirelessMultiboot* linkWirelessMultiboot = new LinkWirelessMultiboot(); int main() { setUpInterrupts(); - REG_WAITCNT = 0x4317; // (3,1 waitstates, prefetch ON) - engine->setScene(multibootScene.get()); while (true) { diff --git a/examples/LinkWirelessMultiboot_demo/src/scenes/MultibootScene.cpp b/examples/LinkWirelessMultiboot_demo/src/scenes/MultibootScene.cpp index c9c22d0..889bfca 100644 --- a/examples/LinkWirelessMultiboot_demo/src/scenes/MultibootScene.cpp +++ b/examples/LinkWirelessMultiboot_demo/src/scenes/MultibootScene.cpp @@ -1,21 +1,22 @@ +// (0) Include the header +#include "../../../../lib/LinkWirelessMultiboot.hpp" + #include "MultibootScene.h" #include +#include #include #include -#include "../../../../lib/LinkWirelessMultiboot.hpp" #include "utils/InputHandler.h" #include "utils/SceneUtils.h" extern "C" { -#include "utils/gbfs/gbfs.h" +#include "../../_lib/libgbfs/gbfs.h" } static const GBFS_FILE* fs = find_first_gbfs_file(0); -const char* ROM_FILE_NAME = "rom-to-transfer.gba"; - MultibootScene::MultibootScene(std::shared_ptr engine) : Scene(engine) {} @@ -23,6 +24,10 @@ static std::unique_ptr aHandler = std::unique_ptr(new InputHandler()); static std::unique_ptr bHandler = std::unique_ptr(new InputHandler()); +static std::unique_ptr leftHandler = + std::unique_ptr(new InputHandler()); +static std::unique_ptr rightHandler = + std::unique_ptr(new InputHandler()); static std::unique_ptr upHandler = std::unique_ptr(new InputHandler()); static std::unique_ptr downHandler = @@ -33,9 +38,12 @@ static std::unique_ptr rHandler = std::unique_ptr(new InputHandler()); static std::unique_ptr selectHandler = std::unique_ptr(new InputHandler()); +static std::unique_ptr startHandler = + std::unique_ptr(new InputHandler()); static std::vector logLines; static u32 currentLogLine = 0; +static u32 selectedFile = 0; static u32 players = 5; #define MAX_LINES 20 @@ -109,6 +117,59 @@ void log(std::string string) { scrollPageDown(); } +void printFile() { + char name[32]; + gbfs_get_nth_obj(fs, selectedFile, name, NULL); + for (u32 i = 0; i < 32; i++) { + if (name[i] == '.') { + name[i] = '\0'; + break; + } + } + + log("! selected file: " + std::to_string(selectedFile + 1) + "/" + + std::to_string(fs->dir_nmemb)); + log(" " + std::string(name)); +} + +void selectLeft() { + if (selectedFile == 0) + return printFile(); + selectedFile--; + printFile(); +} + +void selectRight() { + if ((int)selectedFile >= fs->dir_nmemb - 1) + return printFile(); + selectedFile++; + printFile(); +} + +void launchROM() { + log("Launching..."); + VBlankIntrWait(); + + REG_IME = 0; + + u32 fileLength; + const u8* romToSend = + (const u8*)gbfs_get_nth_obj(fs, selectedFile, NULL, &fileLength); + + void* EWRAM = (void*)0x02000000; + memcpy(EWRAM, romToSend, fileLength); + + asm volatile( + "mov r0, %0\n" + "bx r0\n" + : + : "r"(EWRAM) + : "r0"); + + while (true) + ; +} + std::vector MultibootScene::backgrounds() { return {}; } @@ -125,6 +186,7 @@ void MultibootScene::load() { #ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING linkWirelessMultiboot->logger = [](std::string string) { log(string); }; #endif + #ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING linkWirelessMultiboot->linkRawWireless->logger = [](std::string string) { log(string); @@ -133,27 +195,31 @@ void MultibootScene::load() { log("---"); log("LinkWirelessMultiboot demo"); - log(" (v6.3.0)"); + log(" (v7.0.0)"); log(""); if (fs == NULL) { log("! GBFS file not found"); while (true) ; - } else if (gbfs_get_obj(fs, ROM_FILE_NAME, NULL) == NULL) { - log("! File not found in GBFS:"); - log(" " + std::string(ROM_FILE_NAME)); + } else if (gbfs_get_nth_obj(fs, 0, NULL, NULL) == NULL) { + log("! No files found (GBFS)"); while (true) ; } log("A: send ROM"); - log("B: toggle players"); + log("B: launch ROM"); + log("START: toggle players"); + log("LEFT/RIGHT: select ROM"); log("UP/DOWN: scroll up/down"); log("L/R: scroll page up/down"); - log("UP+L/DOWN+R: scroll to top/bottom"); + log("UP+L/DOWN+R: scroll top/bottom"); + log("L+R: cancel transfer"); log("SELECT: clear"); log("---"); log(""); togglePlayers(); + selectedFile = fs->dir_nmemb - 1; + printFile(); } void MultibootScene::tick(u16 keys) { @@ -171,26 +237,34 @@ void MultibootScene::tick(u16 keys) { void MultibootScene::processKeys(u16 keys) { aHandler->setIsPressed(keys & KEY_A); bHandler->setIsPressed(keys & KEY_B); + leftHandler->setIsPressed(keys & KEY_LEFT); + rightHandler->setIsPressed(keys & KEY_RIGHT); upHandler->setIsPressed(keys & KEY_UP); downHandler->setIsPressed(keys & KEY_DOWN); lHandler->setIsPressed(keys & KEY_L); rHandler->setIsPressed(keys & KEY_R); selectHandler->setIsPressed(keys & KEY_SELECT); + startHandler->setIsPressed(keys & KEY_START); } void MultibootScene::processButtons() { - if (bHandler->hasBeenPressedNow()) + if (startHandler->hasBeenPressedNow()) togglePlayers(); if (aHandler->hasBeenPressedNow()) { u32 fileLength; const u8* romToSend = - (const u8*)gbfs_get_obj(fs, ROM_FILE_NAME, &fileLength); + (const u8*)gbfs_get_nth_obj(fs, selectedFile, NULL, &fileLength); + clear(); + printFile(); + + // (2) Send the ROM auto result = linkWirelessMultiboot->sendRom( romToSend, fileLength, "Multiboot", "Test", 0xffff, players, [](LinkWirelessMultiboot::MultibootProgress progress) { - return false; + u16 keys = ~REG_KEYS & KEY_ANY; + return (keys & KEY_L) && (keys & KEY_R); }); log("-> result: " + std::to_string(result)); print(); @@ -210,6 +284,12 @@ void MultibootScene::processButtons() { scrollPageDown(); } + if (leftHandler->hasBeenPressedNow()) + selectLeft(); + + if (rightHandler->hasBeenPressedNow()) + selectRight(); + if (upHandler->getIsPressed()) scrollBack(); @@ -218,6 +298,9 @@ void MultibootScene::processButtons() { if (selectHandler->hasBeenPressedNow()) clear(); + + if (bHandler->hasBeenPressedNow()) + launchROM(); } void MultibootScene::togglePlayers() { diff --git a/examples/LinkWirelessMultiboot_demo/src/utils/InputHandler.h b/examples/LinkWirelessMultiboot_demo/src/utils/InputHandler.h index 163e02e..ebe25fe 100644 --- a/examples/LinkWirelessMultiboot_demo/src/utils/InputHandler.h +++ b/examples/LinkWirelessMultiboot_demo/src/utils/InputHandler.h @@ -36,4 +36,4 @@ class InputHandler { bool isWaiting = false; }; -#endif // INPUT_HANDLER_H \ No newline at end of file +#endif // INPUT_HANDLER_H diff --git a/examples/LinkWireless_demo/Makefile b/examples/LinkWireless_demo/Makefile index 7cff424..05e014f 100644 --- a/examples/LinkWireless_demo/Makefile +++ b/examples/LinkWireless_demo/Makefile @@ -14,7 +14,7 @@ export TONCLIB := ${DEVKITPRO}/libtonc # === TONC RULES ====================================================== # -# Yes, this is almost, but not quite, completely like to +# Yes, this is almost, but not quite, completely like to # DKP's base_rules and gba_rules # @@ -53,7 +53,7 @@ export OBJCOPY := $(PREFIX)objcopy %.elf : @echo Linking cartridge - $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ + $(LD) -specs=gba.specs $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ $(NM) -Sn $@ > $(basename $(notdir $@)).map #---------------------------------------------------------------------- @@ -69,7 +69,7 @@ export OBJCOPY := $(PREFIX)objcopy %.iwram.o : %.iwram.cpp @echo $(notdir $<) $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) $(IARCH) -c $< -o $@ - + #---------------------------------------------------------------------- %.iwram.o : %.iwram.c @echo $(notdir $<) @@ -148,8 +148,8 @@ bDEBUG2 := 0 # Generate debug info (bDEBUG2? Not a full DEBUG flag. Yet) # === BUILD FLAGS ===================================================== # This is probably where you can stop editing -# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck -# up things (gcse seems fond of building masks inside a loop instead of +# NOTE: I've noticed that -fgcse and -ftree-loop-optimize sometimes muck +# up things (gcse seems fond of building masks inside a loop instead of # outside them for example). Removing them sometimes helps # --- Architecture --- @@ -160,15 +160,16 @@ IARCH := -mthumb-interwork -marm -mlong-calls # --- Main flags --- -CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -O2 +CFLAGS := -mcpu=arm7tdmi -mtune=arm7tdmi -Ofast CFLAGS += -Wall CFLAGS += $(INCLUDE) CFLAGS += -ffast-math -fno-strict-aliasing -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +USERFLAGS ?= +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=c++17 -DLINK_DEVELOPMENT $(USERFLAGS) ASFLAGS := $(ARCH) $(INCLUDE) -LDFLAGS := $(ARCH) -Wl,-Map,$(PROJ).map +LDFLAGS := $(ARCH) -Wl,--print-memory-usage,-Map,$(PROJ).map # --- switched additions ---------------------------------------------- @@ -203,7 +204,7 @@ endif ifneq ($(BUILD),$(notdir $(CURDIR))) -# Still in main dir: +# Still in main dir: # * Define/export some extra variables # * Invoke this file again from the build dir # PONDER: what happens if BUILD == "" ? @@ -238,7 +239,7 @@ export OFILES := $(addsuffix .o, $(BINFILES)) \ export INCLUDE := $(foreach dir,$(INCDIRS),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) - + export LIBPATHS := -L$(CURDIR) $(foreach dir,$(LIBDIRS),-L$(dir)/lib) # --- Create BUILD if necessary, and run this makefile from there --- diff --git a/examples/LinkWireless_demo/src/main.cpp b/examples/LinkWireless_demo/src/main.cpp index 40dc9bb..909ff78 100644 --- a/examples/LinkWireless_demo/src/main.cpp +++ b/examples/LinkWireless_demo/src/main.cpp @@ -1,12 +1,13 @@ +// (0) Include the header +#include "../../../lib/LinkWireless.hpp" + #include +#include #include #include #include #include "../../_lib/interrupt.h" -// (0) Include the header -#include "../../../lib/LinkWireless.hpp" - #ifdef PROFILING_ENABLED #include #endif @@ -27,12 +28,13 @@ void connect(); void messageLoop(); void log(std::string text); void waitFor(u16 key); +bool didPress(u16 key, bool& pressed); void wait(u32 verticalLines); void hang(); LinkWireless::Error lastError; -LinkWireless* linkWireless = NULL; -bool forwarding, retransmission, asyncACK; +LinkWireless* linkWireless = nullptr; +bool forwarding, retransmission; u32 maxPlayers; void init() { @@ -43,44 +45,51 @@ void init() { int main() { init(); - bool firstTime = true; + std::string buildSettings = ""; +#ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM + buildSettings += " + irq_iwram\n"; +#endif +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + buildSettings += " + irq_nested\n"; +#endif +#ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH + buildSettings += " + s/r_latch\n"; +#endif +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + buildSettings += " + 2players\n"; +#endif +#ifdef PROFILING_ENABLED + buildSettings += " + profiler\n"; +#endif start: // Options - log("LinkWireless_demo (v6.3.0)\n\n\n\nPress A to start\n\n\n\n\nhold LEFT " - "on start:\n -> disable forwarding\n\nhold UP on start:\n -> disable " - "retransmission\n\nhold B on start:\n -> set 2 players\n\nhold START on " - "start:\n -> async ACK"); + log("LinkWireless_demo (v7.0.0)\n" + buildSettings + + "\n" + "Press A to start\n\n" + "hold LEFT on start:\n -> disable forwarding\n\n" + "hold UP on start:\n -> disable retransmission\n\n" + "hold B on start:\n -> set 2 players"); waitFor(KEY_A); u16 initialKeys = ~REG_KEYS & KEY_ANY; forwarding = !(initialKeys & KEY_LEFT); retransmission = !(initialKeys & KEY_UP); maxPlayers = (initialKeys & KEY_B) ? 2 : LINK_WIRELESS_MAX_PLAYERS; - asyncACK = initialKeys & KEY_START; // (1) Create a LinkWireless instance linkWireless = new LinkWireless( forwarding, retransmission, maxPlayers, LINK_WIRELESS_DEFAULT_TIMEOUT, - LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, LINK_WIRELESS_DEFAULT_INTERVAL, - LINK_WIRELESS_DEFAULT_SEND_TIMER_ID, asyncACK ? 0 : -1); + LINK_WIRELESS_DEFAULT_INTERVAL, LINK_WIRELESS_DEFAULT_SEND_TIMER_ID); // linkWireless->debug = [](std::string str) { log(str); }; - if (firstTime) { - // (2) Add the required interrupt service routines - interrupt_init(); - interrupt_set_handler(INTR_VBLANK, LINK_WIRELESS_ISR_VBLANK); - interrupt_enable(INTR_VBLANK); - interrupt_set_handler(INTR_SERIAL, LINK_WIRELESS_ISR_SERIAL); - interrupt_enable(INTR_SERIAL); - interrupt_set_handler(INTR_TIMER3, LINK_WIRELESS_ISR_TIMER); - interrupt_enable(INTR_TIMER3); - - // (only required when using async ACK) - interrupt_set_handler(INTR_TIMER0, LINK_WIRELESS_ISR_ACK_TIMER); - interrupt_enable(INTR_TIMER0); - - firstTime = false; - } + // (2) Add the required interrupt service routines + interrupt_init(); + interrupt_set_handler(INTR_VBLANK, LINK_WIRELESS_ISR_VBLANK); + interrupt_enable(INTR_VBLANK); + interrupt_set_handler(INTR_SERIAL, LINK_WIRELESS_ISR_SERIAL); + interrupt_enable(INTR_SERIAL); + interrupt_set_handler(INTR_TIMER3, LINK_WIRELESS_ISR_TIMER); + interrupt_enable(INTR_TIMER3); // (3) Initialize the library linkWireless->activate(); @@ -98,40 +107,31 @@ start: "(SELECT = cancel)\n (START = activate)\n\n-> forwarding: " + (forwarding ? "ON" : "OFF") + "\n-> retransmission: " + (retransmission ? "ON" : "OFF") + - "\n-> max players: " + std::to_string(maxPlayers) + - "\n-> async ACK: " + (asyncACK ? "ON" : "OFF")); + "\n-> max players: " + std::to_string(maxPlayers)); // SELECT = back if (keys & KEY_SELECT) { linkWireless->deactivate(); + interrupt_disable(INTR_VBLANK); + interrupt_disable(INTR_SERIAL); + interrupt_disable(INTR_TIMER3); + interrupt_disable(INTR_TIMER0); delete linkWireless; - linkWireless = NULL; + linkWireless = nullptr; goto start; } // START = Activate - if ((keys & KEY_START) && !activating) { - activating = true; + if (didPress(KEY_START, activating)) activate(); - } - if (activating && !(keys & KEY_START)) - activating = false; // L = Serve - if ((keys & KEY_L) && !serving) { - serving = true; + if (didPress(KEY_L, serving)) serve(); - } - if (serving && !(keys & KEY_L)) - serving = false; // R = Connect - if (!connecting && (keys & KEY_R)) { - connecting = true; + if (didPress(KEY_R, connecting)) connect(); - } - if (connecting && !(keys & KEY_R)) - connecting = false; VBlankIntrWait(); } @@ -294,6 +294,10 @@ void messageLoop() { u16 newValue = counters[linkWireless->currentPlayerId()] + 1; bool success = linkWireless->send(newValue); +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + linkWireless->QUICK_SEND = newValue % 32; +#endif + if (success) { counters[linkWireless->currentPlayerId()] = newValue; } else { @@ -316,29 +320,27 @@ void messageLoop() { // (7) Receive data LinkWireless::Message messages[LINK_WIRELESS_QUEUE_SIZE]; linkWireless->receive(messages); - if (messages[0].packetId != LINK_WIRELESS_END) { - for (u32 i = 0; i < LINK_WIRELESS_QUEUE_SIZE; i++) { - auto message = messages[i]; - if (message.packetId == LINK_WIRELESS_END) - break; + for (u32 i = 0; i < LINK_WIRELESS_QUEUE_SIZE; i++) { + auto message = messages[i]; + if (message.packetId == LINK_WIRELESS_END) + break; #ifndef PROFILING_ENABLED - u32 expected = counters[message.playerId] + 1; + u32 expected = counters[message.playerId] + 1; #endif - counters[message.playerId] = message.data; + counters[message.playerId] = message.data; #ifndef PROFILING_ENABLED - // Check for packet loss - if (altView && message.data != expected) { - lostPackets++; - lastLostPacketPlayerId = message.playerId; - lastLostPacketExpected = expected; - lastLostPacketReceived = message.data; - lastLostPacketReceivedPacketId = message.packetId; - } -#endif + // Check for packet loss + if (altView && message.data != expected) { + lostPackets++; + lastLostPacketPlayerId = message.playerId; + lastLostPacketExpected = expected; + lastLostPacketReceived = message.data; + lastLostPacketReceivedPacketId = message.packetId; } +#endif } // (8) Disconnect @@ -348,8 +350,21 @@ void messageLoop() { } // Packet loss check setting - if (!switching && (keys & KEY_UP)) { - switching = true; + if (didPress(KEY_UP, switching)) { +#ifdef PROFILING_ENABLED + // In the profiler ROM, pressing UP will update the broadcast data + if (linkWireless->getState() == LinkWireless::State::SERVING) { + linkWireless->serve("LinkWireless", + ("N = " + std::to_string(counters[0])).c_str(), + counters[0]); + if (linkWireless->getLastError() == + LinkWireless::Error::BUSY_TRY_AGAIN) { + log("Busy!"); + waitFor(KEY_DOWN); + } + } +#endif + altView = !altView; #ifndef PROFILING_ENABLED if (!altView) { @@ -360,8 +375,6 @@ void messageLoop() { } #endif } - if (switching && (!(keys & KEY_UP))) - switching = false; // Normal output std::string output = @@ -381,7 +394,12 @@ void messageLoop() { "p" + std::to_string(i) + ": " + std::to_string(counters[i]) + "\n"; } - // Debug output +// Debug output +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + output += "\n>> " + std::to_string(linkWireless->QUICK_SEND); + output += "\n<< " + std::to_string(linkWireless->QUICK_RECEIVE) + "\n"; +#endif + output += "\n_buffer: " + std::to_string(linkWireless->_getPendingCount()); if (retransmission && !altView) { output += @@ -405,24 +423,17 @@ void messageLoop() { output += "\n_onVBlank: " + std::to_string(linkWireless->lastVBlankTime); output += "\n_onSerial: " + std::to_string(linkWireless->lastSerialTime); output += "\n_onTimer: " + std::to_string(linkWireless->lastTimerTime); - if (asyncACK) - output += " | " + std::to_string(linkWireless->lastACKTimerTime); output += "\n_serialIRQs: " + std::to_string(linkWireless->lastFrameSerialIRQs); output += "\n_timerIRQs: " + std::to_string(linkWireless->lastFrameTimerIRQs); - if (asyncACK) - output += " | " + std::to_string(linkWireless->lastFrameACKTimerIRQs); output += "\n_ms: " + std::to_string(linkWireless->toMs( linkWireless->lastVBlankTime + linkWireless->lastSerialTime * linkWireless->lastFrameSerialIRQs + - linkWireless->lastTimerTime * linkWireless->lastFrameTimerIRQs + - linkWireless->lastACKTimerTime * - linkWireless->lastFrameACKTimerIRQs)); -#endif -#ifndef PROFILING_ENABLED + linkWireless->lastTimerTime * linkWireless->lastFrameTimerIRQs)); +#else if (lostPackets > 0) { output += "\n\n_lostPackets: " + std::to_string(lostPackets) + "\n"; output += "_last: (" + std::to_string(lastLostPacketPlayerId) + ":" + @@ -456,6 +467,18 @@ void waitFor(u16 key) { } while (!(keys & key)); } +bool didPress(u16 key, bool& pressed) { + u16 keys = ~REG_KEYS & KEY_ANY; + bool isPressedNow = false; + if ((keys & key) && !pressed) { + pressed = true; + isPressedNow = true; + } + if (pressed && !(keys & key)) + pressed = false; + return isPressedNow; +} + void wait(u32 verticalLines) { u32 count = 0; u32 vCount = REG_VCOUNT; @@ -470,4 +493,4 @@ void wait(u32 verticalLines) { void hang() { waitFor(KEY_DOWN); -} \ No newline at end of file +} diff --git a/examples/_lib/interrupt.cpp b/examples/_lib/interrupt.cpp index ab575d9..57f3797 100644 --- a/examples/_lib/interrupt.cpp +++ b/examples/_lib/interrupt.cpp @@ -1,5 +1,5 @@ #include "interrupt.h" -#include +#include void interrupt_init(void) { IRQ_Init(); @@ -19,4 +19,4 @@ void interrupt_disable(interrupt_index index) { void interrupt_set_reference_vcount(unsigned long y) { IRQ_SetReferenceVCOUNT(y); -} \ No newline at end of file +} diff --git a/examples/_lib/interrupt.h b/examples/_lib/interrupt.h index 47c5b27..1974864 100644 --- a/examples/_lib/interrupt.h +++ b/examples/_lib/interrupt.h @@ -27,4 +27,4 @@ void interrupt_enable(interrupt_index index); void interrupt_disable(interrupt_index index); void interrupt_set_reference_vcount(unsigned long y); -#endif // INTERRUPT_H \ No newline at end of file +#endif // INTERRUPT_H diff --git a/examples/LinkWirelessMultiboot_demo/src/utils/gbfs/gbfs.h b/examples/_lib/libgbfs/gbfs.h similarity index 100% rename from examples/LinkWirelessMultiboot_demo/src/utils/gbfs/gbfs.h rename to examples/_lib/libgbfs/gbfs.h diff --git a/examples/LinkWirelessMultiboot_demo/src/utils/gbfs/libgbfs.c b/examples/_lib/libgbfs/libgbfs.c similarity index 100% rename from examples/LinkWirelessMultiboot_demo/src/utils/gbfs/libgbfs.c rename to examples/_lib/libgbfs/libgbfs.c diff --git a/examples/_lib/libugba/include/background.h b/examples/_lib/libugba/include/ugba/background.h similarity index 100% rename from examples/_lib/libugba/include/background.h rename to examples/_lib/libugba/include/ugba/background.h diff --git a/examples/_lib/libugba/include/bios.h b/examples/_lib/libugba/include/ugba/bios.h similarity index 100% rename from examples/_lib/libugba/include/bios.h rename to examples/_lib/libugba/include/ugba/bios.h diff --git a/examples/_lib/libugba/include/bios_wrappers.h b/examples/_lib/libugba/include/ugba/bios_wrappers.h similarity index 100% rename from examples/_lib/libugba/include/bios_wrappers.h rename to examples/_lib/libugba/include/ugba/bios_wrappers.h diff --git a/examples/_lib/libugba/include/console.h b/examples/_lib/libugba/include/ugba/console.h similarity index 100% rename from examples/_lib/libugba/include/console.h rename to examples/_lib/libugba/include/ugba/console.h diff --git a/examples/_lib/libugba/include/debug.h b/examples/_lib/libugba/include/ugba/debug.h similarity index 100% rename from examples/_lib/libugba/include/debug.h rename to examples/_lib/libugba/include/ugba/debug.h diff --git a/examples/_lib/libugba/include/definitions.h b/examples/_lib/libugba/include/ugba/definitions.h similarity index 97% rename from examples/_lib/libugba/include/definitions.h rename to examples/_lib/libugba/include/ugba/definitions.h index dfa1e0c..ce8111b 100644 --- a/examples/_lib/libugba/include/definitions.h +++ b/examples/_lib/libugba/include/ugba/definitions.h @@ -34,6 +34,7 @@ # define EXPORT_API __declspec(dllexport) # else # define EXPORT_API __attribute__((visibility("default"))) +// Is this one below needed in MinGW? //# define EXPORT_API __attribute__((dllexport)) # endif #endif diff --git a/examples/_lib/libugba/include/display.h b/examples/_lib/libugba/include/ugba/display.h similarity index 100% rename from examples/_lib/libugba/include/display.h rename to examples/_lib/libugba/include/ugba/display.h diff --git a/examples/_lib/libugba/include/dma.h b/examples/_lib/libugba/include/ugba/dma.h similarity index 100% rename from examples/_lib/libugba/include/dma.h rename to examples/_lib/libugba/include/ugba/dma.h diff --git a/examples/_lib/libugba/include/fp_math.h b/examples/_lib/libugba/include/ugba/fp_math.h similarity index 100% rename from examples/_lib/libugba/include/fp_math.h rename to examples/_lib/libugba/include/ugba/fp_math.h diff --git a/examples/_lib/libugba/include/hardware.h b/examples/_lib/libugba/include/ugba/hardware.h similarity index 99% rename from examples/_lib/libugba/include/hardware.h rename to examples/_lib/libugba/include/ugba/hardware.h index 519bcb8..beffe5c 100644 --- a/examples/_lib/libugba/include/hardware.h +++ b/examples/_lib/libugba/include/ugba/hardware.h @@ -529,6 +529,14 @@ EXPORT_API uintptr_t *UGBA_RegDMA3DAD(void); // 3) When starting a timer by writing to: // // REG_TM0CNT_H, REG_TM1CNT_H, REG_TM2CNT_H, REG_TM3CNT_H +// +// 4) When IME or IE have been 0 for some time, IF isn't 0, and IME or IE are +// set to a value that would trigger an interrupt: +// +// REG_IE, REG_IME +// +// Note that writing to IF doesn't work on the SDL2 port. On the GBA, writing +// a 1 to a bit sets it to 0. On the SDL2 port, it sets the bit to 1. #ifdef __GBA__ # define UGBA_RegisterUpdatedOffset(offset) do { (void)(offset); } while (0) diff --git a/examples/_lib/libugba/include/input.h b/examples/_lib/libugba/include/ugba/input.h similarity index 100% rename from examples/_lib/libugba/include/input.h rename to examples/_lib/libugba/include/ugba/input.h diff --git a/examples/_lib/libugba/include/interrupts.h b/examples/_lib/libugba/include/ugba/interrupts.h similarity index 72% rename from examples/_lib/libugba/include/interrupts.h rename to examples/_lib/libugba/include/ugba/interrupts.h index 3c0d7a9..5761146 100644 --- a/examples/_lib/libugba/include/interrupts.h +++ b/examples/_lib/libugba/include/ugba/interrupts.h @@ -6,26 +6,25 @@ #define INTERRUPTS_H__ #include -#include #include "definitions.h" typedef enum { - IRQ_VBLANK = 0, - IRQ_HBLANK = 1, - IRQ_VCOUNT = 2, - IRQ_TIMER0 = 3, - IRQ_TIMER1 = 4, - IRQ_TIMER2 = 5, - IRQ_TIMER3 = 6, - IRQ_SERIAL = 7, - IRQ_DMA0 = 8, - IRQ_DMA1 = 9, - IRQ_DMA2 = 10, - IRQ_DMA3 = 11, - IRQ_KEYPAD = 12, - IRQ_GAMEPAK = 13, - IRQ_NUMBER + IRQ_VBLANK = 0, + IRQ_HBLANK = 1, + IRQ_VCOUNT = 2, + IRQ_TIMER0 = 3, + IRQ_TIMER1 = 4, + IRQ_TIMER2 = 5, + IRQ_TIMER3 = 6, + IRQ_SERIAL = 7, + IRQ_DMA0 = 8, + IRQ_DMA1 = 9, + IRQ_DMA2 = 10, + IRQ_DMA3 = 11, + IRQ_KEYPAD = 12, + IRQ_GAMEPAK = 13, + IRQ_NUMBER } irq_index; typedef void (*irq_vector)(void); @@ -47,4 +46,4 @@ EXPORT_API void IRQ_Disable(irq_index index); // Set the reference VCOUNT that triggers the VCOUNT interrupt. EXPORT_API void IRQ_SetReferenceVCOUNT(uint32_t y); -#endif // INTERRUPTS_H__ +#endif // INTERRUPTS_H__ diff --git a/examples/_lib/libugba/include/obj.h b/examples/_lib/libugba/include/ugba/obj.h similarity index 100% rename from examples/_lib/libugba/include/obj.h rename to examples/_lib/libugba/include/ugba/obj.h diff --git a/examples/_lib/libugba/include/sound.h b/examples/_lib/libugba/include/ugba/sound.h similarity index 100% rename from examples/_lib/libugba/include/sound.h rename to examples/_lib/libugba/include/ugba/sound.h diff --git a/examples/_lib/libugba/include/sram.h b/examples/_lib/libugba/include/ugba/sram.h similarity index 100% rename from examples/_lib/libugba/include/sram.h rename to examples/_lib/libugba/include/ugba/sram.h diff --git a/examples/_lib/libugba/include/timer.h b/examples/_lib/libugba/include/ugba/timer.h similarity index 100% rename from examples/_lib/libugba/include/timer.h rename to examples/_lib/libugba/include/ugba/timer.h diff --git a/examples/_lib/libugba/include/ugba.h b/examples/_lib/libugba/include/ugba/ugba.h similarity index 87% rename from examples/_lib/libugba/include/ugba.h rename to examples/_lib/libugba/include/ugba/ugba.h index 8c86941..aeba47a 100644 --- a/examples/_lib/libugba/include/ugba.h +++ b/examples/_lib/libugba/include/ugba/ugba.h @@ -13,10 +13,10 @@ extern "C" { #include "bios.h" #include "bios_wrappers.h" #include "console.h" +#include "dma.h" +#include "display.h" #include "debug.h" #include "definitions.h" -#include "display.h" -#include "dma.h" #include "fp_math.h" #include "hardware.h" #include "input.h" @@ -25,16 +25,17 @@ extern "C" { #include "sound.h" #include "sram.h" #include "timer.h" +#include "version.h" #include "vram.h" // Initialize library. This function needs to be called at the start of main(). -EXPORT_API void UGBA_Init(int* argc, char** argv[]); +EXPORT_API void UGBA_Init(int *argc, char **argv[]); #ifndef __GBA__ // Initialize library with no video output (for testing). This function needs to // be called at the start of main(). Not implemented in GBA as it isn't usedul // there. -EXPORT_API void UGBA_InitHeadless(int* argc, char** argv[]); +EXPORT_API void UGBA_InitHeadless(int *argc, char **argv[]); #endif // This function tries to detect specific flashcarts with special needs and @@ -46,4 +47,4 @@ EXPORT_API uint16_t UGBA_FlashcartOptimizedWaitstates(void); } #endif -#endif // UGBA_H__ +#endif // UGBA_H__ diff --git a/examples/_lib/libugba/include/version.h b/examples/_lib/libugba/include/ugba/version.h similarity index 92% rename from examples/_lib/libugba/include/version.h rename to examples/_lib/libugba/include/ugba/version.h index c90e198..525b030 100644 --- a/examples/_lib/libugba/include/version.h +++ b/examples/_lib/libugba/include/ugba/version.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (c) 2020 Antonio Niño Díaz +// Copyright (c) 2020-2022 Antonio Niño Díaz #ifndef VERSION_H__ #define VERSION_H__ @@ -10,7 +10,7 @@ // This library follows the Semantic Versioning rules: https://semver.org/ #define LIBUGBA_VERSION_MAJOR (0U) -#define LIBUGBA_VERSION_MINOR (2U) +#define LIBUGBA_VERSION_MINOR (3U) #define LIBUGBA_VERSION_PATCH (0U) #define LIBUGBA_VERSION ((LIBUGBA_VERSION_MAJOR << 16) | \ diff --git a/examples/_lib/libugba/include/vram.h b/examples/_lib/libugba/include/ugba/vram.h similarity index 100% rename from examples/_lib/libugba/include/vram.h rename to examples/_lib/libugba/include/ugba/vram.h diff --git a/examples/_lib/libugba/lib/libugba.a b/examples/_lib/libugba/lib/libugba.a index b0f3793..991c090 100644 Binary files a/examples/_lib/libugba/lib/libugba.a and b/examples/_lib/libugba/lib/libugba.a differ diff --git a/examples/compile.sh b/examples/compile.sh index debd85d..e9596e8 100755 --- a/examples/compile.sh +++ b/examples/compile.sh @@ -1,112 +1,167 @@ #!/bin/bash -cd LinkCable_basic/ -make rebuild -cp LinkCable_basic.gba ../ +set -e + +compile() { + if [ "$1" = "multiboot" ]; then + args="bMB=1" + suffix=".mb" + folder="multiboot" + else + args="bMB=0" + suffix="" + folder="." + fi + + # LinkCable_basic + cd LinkCable_basic/ + make rebuild $args + cp LinkCable_basic$suffix.gba ../$folder/ + cd .. + + # LinkCable_full + cd LinkCable_full/ + make rebuild $args + cp LinkCable_full$suffix.gba ../$folder/ + cd .. + + # LinkCable_stress + cd LinkCable_stress/ + make rebuild $args + cp LinkCable_stress$suffix.gba ../$folder/ + cd .. + + # LinkGPIO_demo + cd LinkGPIO_demo/ + make rebuild $args + cp LinkGPIO_demo$suffix.gba ../$folder/ + cd .. + + # LinkMobile_demo + cd LinkMobile_demo/ + make rebuild $args + cp LinkMobile_demo$suffix.gba ../$folder/ + cd .. + + # LinkPS2Keyboard_demo + cd LinkPS2Keyboard_demo/ + make rebuild $args + cp LinkPS2Keyboard_demo$suffix.gba ../$folder/ + cd .. + + # LinkPS2Mouse_demo + cd LinkPS2Mouse_demo/ + make rebuild $args + cp LinkPS2Mouse_demo$suffix.gba ../$folder/ + cd .. + + # LinkRawCable_demo + cd LinkRawCable_demo/ + make rebuild $args + cp LinkRawCable_demo$suffix.gba ../$folder/ + cd .. + + # LinkRawWireless_demo + cd LinkRawWireless_demo/ + make rebuild $args USERFLAGS="-DLINK_RAW_WIRELESS_ENABLE_LOGGING=1" + cp LinkRawWireless_demo$suffix.gba ../$folder/ + cd .. + + # LinkSPI_demo + cd LinkSPI_demo/ + make rebuild $args + cp LinkSPI_demo$suffix.gba ../$folder/ + cd .. + + # LinkUART_demo + cd LinkUART_demo/ + make rebuild $args + cp LinkUART_demo$suffix.gba ../$folder/ + cd .. + + # LinkUniversal_basic + cd LinkUniversal_basic/ + make rebuild $args USERFLAGS="-DLINK_WIRELESS_PUT_ISR_IN_IWRAM=1" + cp LinkUniversal_basic$suffix.gba ../$folder/ + cd .. + + # LinkUniversal_full + cd LinkCable_full/ + mv LinkCable_full$suffix.gba backup.gba + make rebuild $args USERFLAGS="-DUSE_LINK_UNIVERSAL=1 -DLINK_WIRELESS_PUT_ISR_IN_IWRAM=1" + cp LinkCable_full$suffix.gba ../$folder/LinkUniversal_full$suffix.gba + mv backup.gba LinkCable_full$suffix.gba + cd .. + + # LinkUniversal_stress + cd LinkCable_stress/ + mv LinkCable_stress$suffix.gba backup.gba + make rebuild $args USERFLAGS="-DUSE_LINK_UNIVERSAL=1 -DLINK_WIRELESS_PUT_ISR_IN_IWRAM=1" + cp LinkCable_stress$suffix.gba ../$folder/LinkUniversal_stress$suffix.gba + mv backup.gba LinkCable_stress$suffix.gba + cd .. + + # LinkWireless_demo + cd LinkWireless_demo/ + make rebuild $args USERFLAGS="-DLINK_WIRELESS_PUT_ISR_IN_IWRAM=1 -DLINK_WIRELESS_USE_SEND_RECEIVE_LATCH=1" + cp LinkWireless_demo$suffix.gba ../$folder/ + cd .. + + # LinkWireless_demo_2players + cd LinkWireless_demo/ + mv LinkWireless_demo$suffix.gba backup.gba + make rebuild $args USERFLAGS="-DLINK_WIRELESS_PUT_ISR_IN_IWRAM=1 -DLINK_WIRELESS_ENABLE_NESTED_IRQ=1 -DLINK_WIRELESS_USE_SEND_RECEIVE_LATCH=1 -DLINK_WIRELESS_TWO_PLAYERS_ONLY=1" + cp LinkWireless_demo$suffix.gba ../$folder/LinkWireless_demo_2players$suffix.gba + mv backup.gba LinkWireless_demo$suffix.gba + cd .. + + # LinkWireless_demo_profiler + if [ "$1" != "multiboot" ]; then + cd LinkWireless_demo/ + mv LinkWireless_demo.gba backup.gba + make rebuild USERFLAGS="-DLINK_WIRELESS_PUT_ISR_IN_IWRAM=1 -DLINK_WIRELESS_USE_SEND_RECEIVE_LATCH=1 -DPROFILING_ENABLED=1" + cp LinkWireless_demo.gba ../LinkWireless_demo_profiler.gba + mv backup.gba LinkWireless_demo.gba + cd .. + fi +} + +# Cleanup +rm -rf multiboot/ +mkdir -p multiboot/ +rm -f *.gba *.sav *.sa2 + +# Compile all ROMs as multiboot +compile "multiboot" +cd multiboot/ +cp ../hello.gbfs . +ungbfs hello.gbfs +rm hello.gbfs +for file in *.gba; do + ../pad16.sh "$file" +done +gbfs roms.gbfs * cd .. -cd LinkCable_full/ -make rebuild -cp LinkCable_full.gba ../ -cd .. - -cd LinkCable_stress/ -make rebuild -cp LinkCable_stress.gba ../ -cd .. +# Bundle all multiboot ROMs in the multiboot launchers +cp multiboot/roms.gbfs LinkCableMultiboot_demo/content.gbfs +cp multiboot/roms.gbfs LinkWirelessMultiboot_demo/content.gbfs +# LinkCableMultiboot_demo cd LinkCableMultiboot_demo/ make rebuild -cp LinkCableMultiboot_demo.mb.gba ../ -cd .. - -cd LinkGPIO_demo/ -make rebuild -cp LinkGPIO_demo.gba ../ -cd .. - -cd LinkPS2Keyboard_demo/ -make rebuild -cp LinkPS2Keyboard_demo.gba ../ -cd .. - -cd LinkPS2Mouse_demo/ -make rebuild -cp LinkPS2Mouse_demo.gba ../ -cd .. - -cd LinkRawCable_demo/ -make rebuild -cp LinkRawCable_demo.gba ../ -cd .. - -cd LinkRawWireless_demo/ -sed -i -e "s/\/\/ #define LINK_RAW_WIRELESS_ENABLE_LOGGING/#define LINK_RAW_WIRELESS_ENABLE_LOGGING/g" ../../lib/LinkRawWireless.hpp -make rebuild -sed -i -e "s/#define LINK_RAW_WIRELESS_ENABLE_LOGGING/\/\/ #define LINK_RAW_WIRELESS_ENABLE_LOGGING/g" ../../lib/LinkRawWireless.hpp -cp LinkRawWireless_demo.gba ../ -cd .. - -cd LinkSPI_demo/ -make rebuild -cp LinkSPI_demo.gba ../ -cd .. - -cd LinkUART_demo/ -make rebuild -cp LinkUART_demo.gba ../ -cd .. - -cd LinkUniversal_basic/ -sed -i -e "s/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -make rebuild -cp LinkUniversal_basic.gba ../ -sed -i -e "s/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -cd .. - -cd LinkCable_full/ -sed -i -e "s/\/\/ #define USE_LINK_UNIVERSAL/#define USE_LINK_UNIVERSAL/g" src/main.h -sed -i -e "s/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -mv LinkCable_full.gba backup.gba -make rebuild -cp LinkCable_full.gba ../LinkUniversal_full.gba -mv backup.gba LinkCable_full.gba -sed -i -e "s/#define USE_LINK_UNIVERSAL/\/\/ #define USE_LINK_UNIVERSAL/g" src/main.h -sed -i -e "s/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -cd .. - -cd LinkCable_stress/ -sed -i -e "s/\/\/ #define USE_LINK_UNIVERSAL/#define USE_LINK_UNIVERSAL/g" src/main.h -sed -i -e "s/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -mv LinkCable_stress.gba backup.gba -make rebuild -cp LinkCable_stress.gba ../LinkUniversal_stress.gba -mv backup.gba LinkCable_stress.gba -sed -i -e "s/#define USE_LINK_UNIVERSAL/\/\/ #define USE_LINK_UNIVERSAL/g" src/main.h -sed -i -e "s/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -cd .. - -cd LinkWireless_demo/ -sed -i -e "s/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -make rebuild -sed -i -e "s/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -cp LinkWireless_demo.gba ../ -cd .. - -cd LinkWireless_demo/ -sed -i -e "s/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -sed -i -e "s/\/\/ #define PROFILING_ENABLED/#define PROFILING_ENABLED/g" ../../lib/LinkWireless.hpp -mv LinkWireless_demo.gba backup.gba -make rebuild -cp LinkWireless_demo.gba ../LinkWireless_demo_profiler.gba -mv backup.gba LinkWireless_demo.gba -sed -i -e "s/#define LINK_WIRELESS_PUT_ISR_IN_IWRAM/\/\/ #define LINK_WIRELESS_PUT_ISR_IN_IWRAM/g" ../../lib/LinkWireless.hpp -sed -i -e "s/#define PROFILING_ENABLED/\/\/ #define PROFILING_ENABLED/g" ../../lib/LinkWireless.hpp +cp LinkCableMultiboot_demo.out.gba ../LinkCableMultiboot_demo.gba +cp ../hello.gbfs content.gbfs cd .. +# LinkWirelessMultiboot_demo cd LinkWirelessMultiboot_demo/ -sed -i -e "s/\/\/ #define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/#define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/g" ../../lib/LinkWirelessMultiboot.hpp +sed $sed_inplace_option -e "s/\/\/ #define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/#define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/g" ../../lib/LinkWirelessMultiboot.hpp make rebuild cp LinkWirelessMultiboot_demo.out.gba ../LinkWirelessMultiboot_demo.gba -sed -i -e "s/#define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/\/\/ #define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/g" ../../lib/LinkWirelessMultiboot.hpp +cp ../hello.gbfs content.gbfs +sed $sed_inplace_option -e "s/#define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/\/\/ #define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING/g" ../../lib/LinkWirelessMultiboot.hpp cd .. + +# Compile all ROMs as normal +compile diff --git a/examples/LinkWirelessMultiboot_demo/scripts/package.sh b/examples/gbfs.sh similarity index 68% rename from examples/LinkWirelessMultiboot_demo/scripts/package.sh rename to examples/gbfs.sh index cf6c2e6..7f60b80 100755 --- a/examples/LinkWirelessMultiboot_demo/scripts/package.sh +++ b/examples/gbfs.sh @@ -1,9 +1,9 @@ #!/bin/bash -FILE_INPUT="LinkWirelessMultiboot_demo.gba" -FILE_TMP="LinkWirelessMultiboot_demo.tmp.gba" -FILE_OUTPUT="LinkWirelessMultiboot_demo.out.gba" -DATA="content.gbfs" +FILE_INPUT="$1" +FILE_TMP="tmp.gba" +DATA="$2" +FILE_OUTPUT="$3" if [ ! -f "$DATA" ]; then echo "" @@ -30,7 +30,7 @@ MAX_REQUIRED_SIZE_KB=$(($MAX_ROM_SIZE_KB - $GBFS_SIZE_KB)) if (( $MAX_REQUIRED_SIZE_KB < $ROM_SIZE_KB )); then echo "" echo "[!] ERROR:" - echo "GBFS file too big." + echo "ROM/GBFS file too big." echo "" echo "GBFS_SIZE_KB=$GBFS_SIZE_KB" echo "ROM_SIZE_KB=$ROM_SIZE_KB" @@ -38,14 +38,17 @@ if (( $MAX_REQUIRED_SIZE_KB < $ROM_SIZE_KB )); then echo "" exit 1 fi -REQUIRED_SIZE_KB=$(($INITIAL_REQUIRED_SIZE_KB > $MAX_REQUIRED_SIZE_KB ? $MAX_REQUIRED_SIZE_KB : $INITIAL_REQUIRED_SIZE_KB)) -PAD_NEEDED=$((($REQUIRED_SIZE_KB * $KB) - $ROM_SIZE)) cp $FILE_INPUT $FILE_TMP if [ $? -ne 0 ]; then exit 1 fi -dd if=/dev/zero bs=1 count=$PAD_NEEDED >> $FILE_TMP +SIZE=$(wc -c < $FILE_TMP) +DIFF=$(($SIZE % 1024)) +if (($DIFF > 0)); then + PAD_NEEDED=$((1024 - $DIFF)) + dd if=/dev/zero bs=1 count=$PAD_NEEDED >> $FILE_TMP +fi if [ $? -ne 0 ]; then exit 1 fi diff --git a/examples/hello.gbfs b/examples/hello.gbfs new file mode 100644 index 0000000..9518bd6 Binary files /dev/null and b/examples/hello.gbfs differ diff --git a/examples/LinkCableMultiboot_demo/pad16.sh b/examples/pad16.sh similarity index 56% rename from examples/LinkCableMultiboot_demo/pad16.sh rename to examples/pad16.sh index 1dfce6e..f0673df 100755 --- a/examples/LinkCableMultiboot_demo/pad16.sh +++ b/examples/pad16.sh @@ -1,8 +1,8 @@ #!/bin/bash -SIZE=$(wc -c < $1) +SIZE=$(wc -c < "$1") DIFF=$(($SIZE % 16)) if (($DIFF > 0)); then PAD_NEEDED=$((16 - $DIFF)) - dd if=/dev/zero bs=1 count=$PAD_NEEDED >> $1 + dd if=/dev/zero bs=1 count=$PAD_NEEDED >> "$1" fi diff --git a/lib/LinkCable.hpp b/lib/LinkCable.hpp index 74a705d..ac3789d 100644 --- a/lib/LinkCable.hpp +++ b/lib/LinkCable.hpp @@ -31,51 +31,70 @@ // That causes packet loss. You REALLY want to use libugba's instead. // (see examples) // -------------------------------------------------------------------------- -// (*2) The hardware is very sensitive to timing. Make sure your interrupt -// handlers are short, so `LINK_CABLE_ISR_SERIAL()` is called on time. -// Another option would be activating nested interrupts by setting -// `REG_IME=1` at the start of your interrupt handler. +// (*2) The hardware is very sensitive to timing. Make sure that +// `LINK_CABLE_ISR_SERIAL()` is handled on time. That means: +// Be careful with DMA usage (which stops the CPU), and write short +// interrupt handlers (or activate nested interrupts by setting +// `REG_IME=1` at the start of your handlers). // -------------------------------------------------------------------------- // `send(...)` restrictions: // - 0xFFFF and 0x0 are reserved values, so don't send them! // (they mean 'disconnected' and 'no data' respectively) // -------------------------------------------------------------------------- -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif -// Buffer size +#include "_link_common.hpp" + +#ifndef LINK_CABLE_QUEUE_SIZE +/** + * @brief Buffer size (how many incoming and outgoing messages the queues can + * store at max **per player**). The default value is `15`, which seems fine for + * most games. + * \warning This affects how much memory is allocated. With the default value, + * it's around `390` bytes. There's a double-buffered pending queue (to avoid + * data races), 1 incoming queue and 1 outgoing queue. \warning You can + * approximate the usage with `LINK_CABLE_QUEUE_SIZE * 26`. + */ #define LINK_CABLE_QUEUE_SIZE 15 +#endif + +static volatile char LINK_CABLE_VERSION[] = "LinkCable/v7.0.0"; #define LINK_CABLE_MAX_PLAYERS 4 -#define LINK_CABLE_DISCONNECTED 0xffff -#define LINK_CABLE_NO_DATA 0x0 #define LINK_CABLE_DEFAULT_TIMEOUT 3 -#define LINK_CABLE_DEFAULT_REMOTE_TIMEOUT 5 #define LINK_CABLE_DEFAULT_INTERVAL 50 #define LINK_CABLE_DEFAULT_SEND_TIMER_ID 3 -#define LINK_CABLE_BASE_FREQUENCY TM_FREQ_1024 -#define LINK_CABLE_REMOTE_TIMEOUT_OFFLINE -1 -#define LINK_CABLE_BIT_SLAVE 2 -#define LINK_CABLE_BIT_READY 3 -#define LINK_CABLE_BITS_PLAYER_ID 4 -#define LINK_CABLE_BIT_ERROR 6 -#define LINK_CABLE_BIT_START 7 -#define LINK_CABLE_BIT_MULTIPLAYER 13 -#define LINK_CABLE_BIT_IRQ 14 -#define LINK_CABLE_BIT_GENERAL_PURPOSE_LOW 14 -#define LINK_CABLE_BIT_GENERAL_PURPOSE_HIGH 15 +#define LINK_CABLE_DISCONNECTED 0xffff +#define LINK_CABLE_NO_DATA 0x0 #define LINK_CABLE_BARRIER asm volatile("" ::: "memory") -static volatile char LINK_CABLE_VERSION[] = "LinkCable/v6.3.0"; - -void LINK_CABLE_ISR_VBLANK(); -void LINK_CABLE_ISR_SERIAL(); -void LINK_CABLE_ISR_TIMER(); -const u16 LINK_CABLE_TIMER_IRQ_IDS[] = {IRQ_TIMER0, IRQ_TIMER1, IRQ_TIMER2, - IRQ_TIMER3}; - +/** + * @brief A Link Cable connection for Multi-Play mode. + */ class LinkCable { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + using vu32 = volatile unsigned int; + using vs32 = volatile signed int; + using U16Queue = Link::Queue; + + static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024; + static constexpr int REMOTE_TIMEOUT_OFFLINE = -1; + static constexpr int BIT_SLAVE = 2; + static constexpr int BIT_READY = 3; + static constexpr int BITS_PLAYER_ID = 4; + static constexpr int BIT_ERROR = 6; + static constexpr int BIT_START = 7; + static constexpr int BIT_MULTIPLAYER = 13; + static constexpr int BIT_IRQ = 14; + static constexpr int BIT_GENERAL_PURPOSE_LOW = 14; + static constexpr int BIT_GENERAL_PURPOSE_HIGH = 15; + public: enum BaudRate { BAUD_RATE_0, // 9600 bps @@ -84,65 +103,36 @@ class LinkCable { BAUD_RATE_3 // 115200 bps }; - class U16Queue { - public: - void push(u16 item) { - if (isFull()) - pop(); - - rear = (rear + 1) % LINK_CABLE_QUEUE_SIZE; - arr[rear] = item; - count++; - } - - u16 pop() { - if (isEmpty()) - return LINK_CABLE_NO_DATA; - - auto x = arr[front]; - front = (front + 1) % LINK_CABLE_QUEUE_SIZE; - count--; - - return x; - } - - u16 peek() { - if (isEmpty()) - return LINK_CABLE_NO_DATA; - - return arr[front]; - } - - void clear() { - front = count = 0; - rear = -1; - } - - int size() { return count; } - bool isEmpty() { return size() == 0; } - bool isFull() { return size() == LINK_CABLE_QUEUE_SIZE; } - - private: - u16 arr[LINK_CABLE_QUEUE_SIZE]; - vs32 front = 0; - vs32 rear = -1; - vu32 count = 0; - }; - + /** + * @brief Constructs a new LinkCable object. + * @param baudRate Sets a specific baud rate. + * @param timeout Number of *frames* without a `SERIAL` IRQ to reset the + * connection. + * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers + * *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values + * will transfer faster but also consume more CPU. + * @param sendTimerId `(0~3)` GBA Timer to use for sending. + * \warning You can use `Link::perFrame(...)` to convert from *packets per + * frame* to *interval values*. + */ explicit LinkCable(BaudRate baudRate = BAUD_RATE_1, u32 timeout = LINK_CABLE_DEFAULT_TIMEOUT, - u32 remoteTimeout = LINK_CABLE_DEFAULT_REMOTE_TIMEOUT, u16 interval = LINK_CABLE_DEFAULT_INTERVAL, u8 sendTimerId = LINK_CABLE_DEFAULT_SEND_TIMER_ID) { this->config.baudRate = baudRate; this->config.timeout = timeout; - this->config.remoteTimeout = remoteTimeout; this->config.interval = interval; this->config.sendTimerId = sendTimerId; } - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + /** + * @brief Activates the library. + */ void activate() { LINK_CABLE_BARRIER; isEnabled = false; @@ -156,6 +146,9 @@ class LinkCable { LINK_CABLE_BARRIER; } + /** + * @brief Deactivates the library. + */ void deactivate() { LINK_CABLE_BARRIER; isEnabled = false; @@ -166,13 +159,26 @@ class LinkCable { clearIncomingMessages(); } - bool isConnected() { + /** + * @brief Returns `true` if there are at least 2 connected players. + */ + [[nodiscard]] bool isConnected() { return state.playerCount > 1 && state.currentPlayerId < state.playerCount; } - u8 playerCount() { return state.playerCount; } - u8 currentPlayerId() { return state.currentPlayerId; } + /** + * @brief Returns the number of connected players (`0~4`). + */ + [[nodiscard]] u8 playerCount() { return state.playerCount; } + /** + * @brief Returns the current player ID (`0~3`). + */ + [[nodiscard]] u8 currentPlayerId() { return state.currentPlayerId; } + + /** + * @brief Call this method every time you need to fetch new data. + */ void sync() { if (!isEnabled) return; @@ -182,7 +188,7 @@ class LinkCable { LINK_CABLE_BARRIER; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) - move(_state.pendingMessages[i], state.incomingMessages[i]); + move(_state.readyToSyncMessages[i], state.syncedIncomingMessages[i]); LINK_CABLE_BARRIER; isReadingMessages = false; @@ -192,62 +198,104 @@ class LinkCable { clearIncomingMessages(); } + /** + * @brief Waits for data from player #`playerId`. Returns `true` on success, + * or `false` on disconnection. + * @param playerId A player ID. + */ bool waitFor(u8 playerId) { return waitFor(playerId, []() { return false; }); } + /** + * @brief Waits for data from player #`playerId`. Returns `true` on success, + * or `false` on disconnection. + * @param playerId ID of player to wait data from. + * @param cancel A function that will be continuously invoked. If it returns + * `true`, the wait be aborted. + */ template bool waitFor(u8 playerId, F cancel) { sync(); while (isConnected() && !canRead(playerId) && !cancel()) { - IntrWait(1, IRQ_SERIAL | LINK_CABLE_TIMER_IRQ_IDS[config.sendTimerId]); + Link::_IntrWait( + 1, Link::_IRQ_SERIAL | Link::_TIMER_IRQ_IDS[config.sendTimerId]); sync(); } return isConnected() && canRead(playerId); } - bool canRead(u8 playerId) { - return !state.incomingMessages[playerId].isEmpty(); + /** + * @brief Returns `true` if there are pending messages from player + * #`playerId`. + * @param playerId A player ID. + * \warning Keep in mind that if this returns `false`, it will keep doing so + * until you *fetch new data* with `sync()`. + */ + [[nodiscard]] bool canRead(u8 playerId) { + return !state.syncedIncomingMessages[playerId].isEmpty(); } - u16 read(u8 playerId) { return state.incomingMessages[playerId].pop(); } + /** + * @brief Dequeues and returns the next message from player #`playerId`. + * @param playerId A player ID. + * \warning If there's no data from that player, a `0` will be returned. + */ + u16 read(u8 playerId) { return state.syncedIncomingMessages[playerId].pop(); } - u16 peek(u8 playerId) { return state.incomingMessages[playerId].peek(); } + /** + * @brief Returns the next message from player #`playerId` without dequeuing + * it. + * @param playerId A player ID. + * \warning If there's no data from that player, a `0` will be returned. + */ + [[nodiscard]] u16 peek(u8 playerId) { + return state.syncedIncomingMessages[playerId].peek(); + } + /** + * @brief Sends `data` to all connected players. + * @param data The value to be sent. + */ void send(u16 data) { if (data == LINK_CABLE_DISCONNECTED || data == LINK_CABLE_NO_DATA) return; - LINK_CABLE_BARRIER; - isAddingMessage = true; - LINK_CABLE_BARRIER; - - _state.outgoingMessages.push(data); - - LINK_CABLE_BARRIER; - isAddingMessage = false; - LINK_CABLE_BARRIER; - - if (isAddingWhileResetting) { - _state.outgoingMessages.clear(); - isAddingWhileResetting = false; - } + _state.outgoingMessages.syncPush(data); } + /** + * @brief This method is called by the VBLANK interrupt handler. + * \warning This is internal API! + */ void _onVBlank() { if (!isEnabled) return; if (!_state.IRQFlag) _state.IRQTimeout++; - _state.IRQFlag = false; + for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { + if (isOnline(i) && !_state.msgFlags[i]) + _state.msgTimeouts[i]++; + _state.msgFlags[i] = false; + } + + if (didTimeout()) { + reset(); + return; + } + copyState(); } + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ void _onSerial() { if (!isEnabled) return; @@ -262,7 +310,7 @@ class LinkCable { u8 newPlayerCount = 0; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { - u16 data = REG_SIOMULTI[i]; + u16 data = Link::_REG_SIOMULTI[i]; if (data != LINK_CABLE_DISCONNECTED) { if (data != LINK_CABLE_NO_DATA && i != state.currentPlayerId) @@ -270,9 +318,7 @@ class LinkCable { newPlayerCount++; setOnline(i); } else if (isOnline(i)) { - _state.timeouts[i]++; - - if (_state.timeouts[i] >= (int)config.remoteTimeout) { + if (_state.msgTimeouts[i] >= (int)config.timeout) { _state.newMessages[i].clear(); setOffline(i); } else { @@ -283,8 +329,9 @@ class LinkCable { state.playerCount = newPlayerCount; state.currentPlayerId = - (REG_SIOCNT & (0b11 << LINK_CABLE_BITS_PLAYER_ID)) >> - LINK_CABLE_BITS_PLAYER_ID; + (Link::_REG_SIOCNT & (0b11 << BITS_PLAYER_ID)) >> BITS_PLAYER_ID; + + Link::_REG_SIOMLT_SEND = LINK_CABLE_NO_DATA; if (!isMaster()) sendPendingData(); @@ -292,15 +339,14 @@ class LinkCable { copyState(); } + /** + * @brief This method is called by the TIMER interrupt handler. + * \warning This is internal API! + */ void _onTimer() { if (!isEnabled) return; - if (didTimeout()) { - reset(); - return; - } - if (isMaster() && isReady() && !isSending()) sendPendingData(); @@ -310,44 +356,46 @@ class LinkCable { struct Config { BaudRate baudRate; u32 timeout; - u32 remoteTimeout; u32 interval; u8 sendTimerId; }; + /** + * @brief LinkCable configuration. + * \warning `deactivate()` first, change the config, and `activate()` again! + */ Config config; private: struct ExternalState { - U16Queue incomingMessages[LINK_CABLE_MAX_PLAYERS]; + U16Queue syncedIncomingMessages[LINK_CABLE_MAX_PLAYERS]; u8 playerCount; u8 currentPlayerId; }; struct InternalState { U16Queue outgoingMessages; - U16Queue pendingMessages[LINK_CABLE_MAX_PLAYERS]; + U16Queue readyToSyncMessages[LINK_CABLE_MAX_PLAYERS]; U16Queue newMessages[LINK_CABLE_MAX_PLAYERS]; - int timeouts[LINK_CABLE_MAX_PLAYERS]; - bool IRQFlag; u32 IRQTimeout; + int msgTimeouts[LINK_CABLE_MAX_PLAYERS]; + bool msgFlags[LINK_CABLE_MAX_PLAYERS]; + bool IRQFlag; }; ExternalState state; InternalState _state; volatile bool isEnabled = false; volatile bool isReadingMessages = false; - volatile bool isAddingMessage = false; - volatile bool isAddingWhileResetting = false; - bool isMaster() { return !isBitHigh(LINK_CABLE_BIT_SLAVE); } - bool isReady() { return isBitHigh(LINK_CABLE_BIT_READY); } - bool hasError() { return isBitHigh(LINK_CABLE_BIT_ERROR); } - bool isSending() { return isBitHigh(LINK_CABLE_BIT_START); } + bool isMaster() { return !isBitHigh(BIT_SLAVE); } + bool isReady() { return isBitHigh(BIT_READY); } + bool hasError() { return isBitHigh(BIT_ERROR); } + bool isSending() { return isBitHigh(BIT_START); } bool didTimeout() { return _state.IRQTimeout >= config.timeout; } void sendPendingData() { - if (isAddingMessage) + if (_state.outgoingMessages.isWriting()) return; LINK_CABLE_BARRIER; @@ -356,10 +404,10 @@ class LinkCable { } void transfer(u16 data) { - REG_SIOMLT_SEND = data; + Link::_REG_SIOMLT_SEND = data; if (isMaster()) - setBitHigh(LINK_CABLE_BIT_START); + setBitHigh(BIT_START); } void reset() { @@ -372,14 +420,11 @@ class LinkCable { state.playerCount = 0; state.currentPlayerId = 0; - if (isAddingMessage || isAddingWhileResetting) - isAddingWhileResetting = true; - else - _state.outgoingMessages.clear(); + _state.outgoingMessages.syncClear(); for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { if (!isReadingMessages) - _state.pendingMessages[i].clear(); + _state.readyToSyncMessages[i].clear(); _state.newMessages[i].clear(); setOffline(i); @@ -389,6 +434,7 @@ class LinkCable { } void stop() { + Link::_REG_SIOMLT_SEND = LINK_CABLE_NO_DATA; stopTimer(); setGeneralPurposeMode(); } @@ -400,19 +446,19 @@ class LinkCable { } void stopTimer() { - REG_TM[config.sendTimerId].cnt = - REG_TM[config.sendTimerId].cnt & (~TM_ENABLE); + Link::_REG_TM[config.sendTimerId].cnt = + Link::_REG_TM[config.sendTimerId].cnt & (~Link::_TM_ENABLE); } void startTimer() { - REG_TM[config.sendTimerId].start = -config.interval; - REG_TM[config.sendTimerId].cnt = - TM_ENABLE | TM_IRQ | LINK_CABLE_BASE_FREQUENCY; + Link::_REG_TM[config.sendTimerId].start = -config.interval; + Link::_REG_TM[config.sendTimerId].cnt = + Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY; } void clearIncomingMessages() { for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) - state.incomingMessages[i].clear(); + state.syncedIncomingMessages[i].clear(); } void copyState() { @@ -421,9 +467,9 @@ class LinkCable { for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { if (isOnline(i)) - move(_state.newMessages[i], _state.pendingMessages[i]); + move(_state.newMessages[i], _state.readyToSyncMessages[i]); else - _state.pendingMessages[i].clear(); + _state.readyToSyncMessages[i].clear(); } } @@ -433,44 +479,77 @@ class LinkCable { } bool isOnline(u8 playerId) { - return _state.timeouts[playerId] != LINK_CABLE_REMOTE_TIMEOUT_OFFLINE; - } - void setOnline(u8 playerId) { _state.timeouts[playerId] = 0; } - void setOffline(u8 playerId) { - _state.timeouts[playerId] = LINK_CABLE_REMOTE_TIMEOUT_OFFLINE; + return _state.msgTimeouts[playerId] != REMOTE_TIMEOUT_OFFLINE; } - void setInterruptsOn() { setBitHigh(LINK_CABLE_BIT_IRQ); } + void setOnline(u8 playerId) { + _state.msgTimeouts[playerId] = 0; + _state.msgFlags[playerId] = true; + } + + void setOffline(u8 playerId) { + _state.msgTimeouts[playerId] = REMOTE_TIMEOUT_OFFLINE; + _state.msgFlags[playerId] = false; + } + + void setInterruptsOn() { setBitHigh(BIT_IRQ); } void setMultiPlayMode() { - REG_RCNT = REG_RCNT & ~(1 << LINK_CABLE_BIT_GENERAL_PURPOSE_HIGH); - REG_SIOCNT = (1 << LINK_CABLE_BIT_MULTIPLAYER); - REG_SIOCNT |= config.baudRate; - REG_SIOMLT_SEND = 0; + Link::_REG_RCNT = Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_SIOCNT = (1 << BIT_MULTIPLAYER); + Link::_REG_SIOCNT |= config.baudRate; + Link::_REG_SIOMLT_SEND = 0; } void setGeneralPurposeMode() { - REG_RCNT = (REG_RCNT & ~(1 << LINK_CABLE_BIT_GENERAL_PURPOSE_LOW)) | - (1 << LINK_CABLE_BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) | + (1 << BIT_GENERAL_PURPOSE_HIGH); } - bool isBitHigh(u8 bit) { return (REG_SIOCNT >> bit) & 1; } - void setBitHigh(u8 bit) { REG_SIOCNT |= 1 << bit; } - void setBitLow(u8 bit) { REG_SIOCNT &= ~(1 << bit); } + 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 LinkCable* linkCable; +/** + * @brief VBLANK interrupt handler. + */ inline void LINK_CABLE_ISR_VBLANK() { linkCable->_onVBlank(); } +/** + * @brief SERIAL interrupt handler. + */ inline void LINK_CABLE_ISR_SERIAL() { linkCable->_onSerial(); } +/** + * @brief TIMER interrupt handler. + */ inline void LINK_CABLE_ISR_TIMER() { linkCable->_onTimer(); } +/** + * NOTES: + * For end users: + * - `sync()` fills an incoming queue (`syncedIncomingMessages`). + * - `read(...)` pops one message from that queue. + * - `send(...)` pushes one message to an outgoing queue (`outgoingMessages`). + * Behind the curtains: + * - On each SERIAL IRQ: + * -> Each new message is pushed to `newMessages`. + * - On each VBLANK, SERIAL, or TIMER IRQ: + * -> **If the user is not syncing**: + * -> All `newMessages` are moved to `readyToSyncMessages`. + * - If (playerId == 0 && TIMER_IRQ) || (playerId > 0 && SERIAL_IRQ): + * -> **If the user is not sending**: + * -> Pops one message from `outgoingMessages` and transfers it. + * - `sync()` moves all `readyToSyncMessages` to `syncedIncomingMessages`. + */ + #endif // LINK_CABLE_H diff --git a/lib/LinkCableMultiboot.hpp b/lib/LinkCableMultiboot.hpp index 6702b66..1b03a0a 100644 --- a/lib/LinkCableMultiboot.hpp +++ b/lib/LinkCableMultiboot.hpp @@ -20,256 +20,369 @@ // // `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 -#include -#include "LinkRawCable.hpp" +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif -#define LINK_CABLE_MULTIBOOT_MIN_ROM_SIZE (0x100 + 0xc0) -#define LINK_CABLE_MULTIBOOT_MAX_ROM_SIZE (256 * 1024) -#define LINK_CABLE_MULTIBOOT_WAIT_BEFORE_RETRY ((160 + 68) * 60) -#define LINK_CABLE_MULTIBOOT_WAIT_BEFORE_TRANSFER 50 -#define LINK_CABLE_MULTIBOOT_DETECTION_TRIES 16 -#define LINK_CABLE_MULTIBOOT_PALETTE_DATA 0x93 -#define LINK_CABLE_MULTIBOOT_CLIENTS 3 -#define LINK_CABLE_MULTIBOOT_CLIENT_NO_DATA 0xff -#define LINK_CABLE_MULTIBOOT_HANDSHAKE 0x6200 -#define LINK_CABLE_MULTIBOOT_HANDSHAKE_RESPONSE 0x7200 -#define LINK_CABLE_MULTIBOOT_CONFIRM_CLIENTS 0x6100 -#define LINK_CABLE_MULTIBOOT_SEND_PALETTE 0x6300 -#define LINK_CABLE_MULTIBOOT_HANDSHAKE_DATA 0x11 -#define LINK_CABLE_MULTIBOOT_CONFIRM_HANDSHAKE_DATA 0x6400 -#define LINK_CABLE_MULTIBOOT_ACK_RESPONSE 0x73 -#define LINK_CABLE_MULTIBOOT_HEADER_SIZE 0xC0 -#define LINK_CABLE_MULTIBOOT_SWI_MULTIPLAYER_MODE 1 -#define LINK_CABLE_MULTIBOOT_MAX_BAUD_RATE LinkRawCable::BaudRate::BAUD_RATE_3 -#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); +#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/v6.3.0"; + "LinkCableMultiboot/v7.0.0"; -const u8 LINK_CABLE_MULTIBOOT_CLIENT_IDS[] = {0b0010, 0b0100, 0b1000}; +#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 { - public: - enum Result { - SUCCESS, - INVALID_SIZE, - CANCELED, - FAILURE_DURING_HANDSHAKE, - FAILURE_DURING_TRANSFER + 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 - Result sendRom(const u8* rom, u32 romSize, F cancel) { - if (romSize < LINK_CABLE_MULTIBOOT_MIN_ROM_SIZE) + 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 > LINK_CABLE_MULTIBOOT_MAX_ROM_SIZE) + if (romSize > MAX_ROM_SIZE) return INVALID_SIZE; if ((romSize % 0x10) != 0) return INVALID_SIZE; - PartialResult partialResult; - MultiBootParam multiBootParameters; - multiBootParameters.client_data[0] = LINK_CABLE_MULTIBOOT_CLIENT_NO_DATA; - multiBootParameters.client_data[1] = LINK_CABLE_MULTIBOOT_CLIENT_NO_DATA; - multiBootParameters.client_data[2] = LINK_CABLE_MULTIBOOT_CLIENT_NO_DATA; + 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 + LINK_CABLE_MULTIBOOT_HEADER_SIZE; + 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(sendHeader(multiBootParameters, rom, cancel)) LINK_CABLE_MULTIBOOT_TRY(sendPalette(multiBootParameters, cancel)) - - multiBootParameters.handshake_data = (LINK_CABLE_MULTIBOOT_HANDSHAKE_DATA + - multiBootParameters.client_data[0] + - multiBootParameters.client_data[1] + - multiBootParameters.client_data[2]) % - 256; - LINK_CABLE_MULTIBOOT_TRY(confirmHandshakeData(multiBootParameters, cancel)) - int result = MultiBoot(&multiBootParameters, - LINK_CABLE_MULTIBOOT_SWI_MULTIPLAYER_MODE); + // 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); - linkRawCable->deactivate(); + 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; } + ~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, ERROR }; + enum PartialResult { NEEDS_RETRY, FINISHED, ABORTED }; struct Responses { - u16 d[LINK_CABLE_MULTIBOOT_CLIENTS]; + u16 d[CLIENTS]; }; template - PartialResult detectClients(MultiBootParam& multiBootParameters, F cancel) { - linkRawCable->activate(LINK_CABLE_MULTIBOOT_MAX_BAUD_RATE); + 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(); - for (u32 t = 0; t < LINK_CABLE_MULTIBOOT_DETECTION_TRIES; t++) { - auto response = - linkRawCable->transfer(LINK_CABLE_MULTIBOOT_HANDSHAKE, cancel); + // 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; - for (u32 i = 0; i < LINK_CABLE_MULTIBOOT_CLIENTS; i++) { - if ((response.data[1 + i] & 0xfff0) == - LINK_CABLE_MULTIBOOT_HANDSHAKE_RESPONSE) { - auto clientId = response.data[1 + i] & 0xf; + multiBootParameters.client_bit = 0; - switch (clientId) { - case 0b0010: - case 0b0100: - case 0b1000: { - multiBootParameters.client_bit |= clientId; - break; + 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; + } } - default: - return NEEDS_RETRY; - } - } - } + return false; + }); + + if (success) + break; } - if (multiBootParameters.client_bit == 0) { - linkRawCable->deactivate(); - wait(LINK_CABLE_MULTIBOOT_WAIT_BEFORE_RETRY); + 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 - PartialResult confirmClients(MultiBootParam& multiBootParameters, F cancel) { - return compare( - multiBootParameters, - LINK_CABLE_MULTIBOOT_CONFIRM_CLIENTS | multiBootParameters.client_bit, - LINK_CABLE_MULTIBOOT_HANDSHAKE_RESPONSE, cancel); - } - - template - PartialResult confirmHeader(MultiBootParam& multiBootParameters, F cancel) { - return compare(multiBootParameters, LINK_CABLE_MULTIBOOT_HANDSHAKE, 0, - cancel); - } - - template - PartialResult reconfirm(MultiBootParam& multiBootParameters, F cancel) { - return compare(multiBootParameters, LINK_CABLE_MULTIBOOT_HANDSHAKE, - LINK_CABLE_MULTIBOOT_HANDSHAKE_RESPONSE, cancel); - } - - template - PartialResult sendHeader(const u8* rom, F cancel) { + 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; - - for (int i = 0; i < LINK_CABLE_MULTIBOOT_HEADER_SIZE; i += 2) { - linkRawCable->transfer(*(headerOut++), cancel); + u32 remaining = HEADER_SIZE / 2; + while (remaining > 0) { + auto response = transfer(*(headerOut++), cancel); if (cancel()) return ABORTED; - } - return FINISHED; - } + bool success = validateResponse(response, [&remaining](u32 i, u16 value) { + u8 clientId = CLIENT_IDS[i]; + u16 expectedValue = (remaining << 8) | clientId; + return value == expectedValue; + }); - template - PartialResult sendPalette(MultiBootParam& multiBootParameters, F cancel) { - auto data = - LINK_CABLE_MULTIBOOT_SEND_PALETTE | LINK_CABLE_MULTIBOOT_PALETTE_DATA; - - auto response = linkRawCable->transfer(data, cancel); - if (cancel()) - return ABORTED; - - for (u32 i = 0; i < LINK_CABLE_MULTIBOOT_CLIENTS; i++) { - if (response.data[1 + i] >> 8 == LINK_CABLE_MULTIBOOT_ACK_RESPONSE) - multiBootParameters.client_data[i] = response.data[1 + i] & 0xff; - } - - for (u32 i = 0; i < LINK_CABLE_MULTIBOOT_CLIENTS; i++) { - u8 clientId = LINK_CABLE_MULTIBOOT_CLIENT_IDS[i]; - bool isClientConnected = multiBootParameters.client_bit & clientId; - - if (isClientConnected && multiBootParameters.client_data[i] == - LINK_CABLE_MULTIBOOT_CLIENT_NO_DATA) + 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 - PartialResult confirmHandshakeData(MultiBootParam& multiBootParameters, + 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 + PartialResult confirmHandshakeData(Link::_MultiBootParam& multiBootParameters, F cancel) { - u16 data = LINK_CABLE_MULTIBOOT_CONFIRM_HANDSHAKE_DATA | - multiBootParameters.handshake_data; - auto response = linkRawCable->transfer(data, 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) == LINK_CABLE_MULTIBOOT_ACK_RESPONSE - ? FINISHED - : ERROR; + return (response.data[1] >> 8) == ACK_RESPONSE ? FINISHED : NEEDS_RETRY; } template - PartialResult compare(MultiBootParam& multiBootParameters, - u16 data, - u16 expectedResponse, - F cancel) { - auto response = linkRawCable->transfer(data, cancel); - if (cancel()) - return ABORTED; + 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; + } - for (u32 i = 0; i < LINK_CABLE_MULTIBOOT_CLIENTS; i++) { - u8 clientId = LINK_CABLE_MULTIBOOT_CLIENT_IDS[i]; - u16 expectedResponseWithId = expectedResponse | clientId; - bool isClientConnected = multiBootParameters.client_bit & clientId; - - if (isClientConnected && response.data[1 + i] != expectedResponseWithId) - return ERROR; + if (!check(i, value)) + return false; + count++; } - return FINISHED; + return count > 0; + } + + template + 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) { - linkRawCable->deactivate(); + deactivate(); return error; } void wait(u32 verticalLines) { u32 count = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; while (count < verticalLines) { - if (REG_VCOUNT != vCount) { + if (Link::_REG_VCOUNT != vCount) { count++; - vCount = REG_VCOUNT; + 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; diff --git a/lib/LinkCube.hpp b/lib/LinkCube.hpp new file mode 100644 index 0000000..318d7e3 --- /dev/null +++ b/lib/LinkCube.hpp @@ -0,0 +1,281 @@ +#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: (*) +// irq_init(NULL); +// irq_add(II_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 + +static volatile char LINK_CUBE_VERSION[] = "LinkCube/v7.0.0"; + +#define LINK_CUBE_BARRIER asm volatile("" ::: "memory") + +/** + * @brief A JOYBUS handler for the Link Port. + */ +class LinkCube { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + using U32Queue = Link::Queue; + + 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_CUBE_BARRIER; + isEnabled = false; + LINK_CUBE_BARRIER; + + resetState(); + stop(); + + LINK_CUBE_BARRIER; + isEnabled = true; + LINK_CUBE_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 + bool wait(F cancel) { + 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) { outgoingQueue.syncPush(data); } + + /** + * @brief Returns the number of pending outgoing transfers. + */ + [[nodiscard]] u32 pendingCount() { return outgoingQueue.size(); } + + /** + * @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.push(newIncomingQueue.pop()); + } + + void resetState() { + needsClear = false; + newIncomingQueue.clear(); + if (incomingQueue.isReading()) + needsClear = true; + else + incomingQueue.clear(); + outgoingQueue.syncClear(); + resetFlag = false; + } + + void setPendingData() { + setData(outgoingQueue.isWriting() ? 0 : outgoingQueue.pop()); + } + + void setData(u32 data) { + Link::_REG_JOY_TRANS_H = msB32(data); + Link::_REG_JOY_TRANS_L = lsB32(data); + } + + u32 getData() { + return 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); } + + u32 buildU32(u16 msB, u16 lsB) { return (msB << 16) | lsB; } + u16 msB32(u32 value) { return value >> 16; } + u16 lsB32(u32 value) { return value & 0xffff; } + 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 diff --git a/lib/LinkGPIO.hpp b/lib/LinkGPIO.hpp index 7966266..36293e6 100644 --- a/lib/LinkGPIO.hpp +++ b/lib/LinkGPIO.hpp @@ -24,52 +24,91 @@ // - call reset() when you finish doing GPIO stuff! // -------------------------------------------------------------------------- -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif -#define LINK_GPIO_RCNT_GENERAL_PURPOSE (1 << 15) -#define LINK_GPIO_SIOCNT_GENERAL_PURPOSE 0 -#define LINK_GPIO_BIT_SI_INTERRUPT 8 -#define LINK_GPIO_GET(REG, BIT) ((REG >> BIT) & 1) -#define LINK_GPIO_SET(REG, BIT, DATA) \ - if (DATA) \ - REG |= 1 << BIT; \ - else \ - REG &= ~(1 << BIT); - -static volatile char LINK_GPIO_VERSION[] = "LinkGPIO/v6.3.0"; - -const u8 LINK_GPIO_DATA_BITS[] = {2, 3, 1, 0}; -const u8 LINK_GPIO_DIRECTION_BITS[] = {6, 7, 5, 4}; +#include "_link_common.hpp" +/** + * @brief A General Purpose Input-Output handler for the Link Port. + */ class LinkGPIO { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + static constexpr int RCNT_GENERAL_PURPOSE = (1 << 15); + static constexpr int SIOCNT_GENERAL_PURPOSE = 0; + static constexpr int BIT_SI_INTERRUPT = 8; + static constexpr u8 DATA_BITS[] = {2, 3, 1, 0}; + static constexpr u8 DIRECTION_BITS[] = {6, 7, 5, 4}; + public: enum Pin { SI, SO, SD, SC }; enum Direction { INPUT, OUTPUT }; + /** + * @brief Resets communication mode to General Purpose. + * \warning Required to initialize the library! + */ void reset() { - REG_RCNT = LINK_GPIO_RCNT_GENERAL_PURPOSE; - REG_SIOCNT = LINK_GPIO_SIOCNT_GENERAL_PURPOSE; + Link::_REG_RCNT = RCNT_GENERAL_PURPOSE; + Link::_REG_SIOCNT = SIOCNT_GENERAL_PURPOSE; } + /** + * @brief Configures a `pin` to use a `direction` (input or output). + * @param pin One of the enum values from `LinkGPIO::Pin`. + * @param direction One of the enum values from `LinkGPIO::Direction`. + */ void setMode(Pin pin, Direction direction) { - LINK_GPIO_SET(REG_RCNT, LINK_GPIO_DIRECTION_BITS[pin], - direction == Direction::OUTPUT); + setBit(Link::_REG_RCNT, DIRECTION_BITS[pin], + direction == Direction::OUTPUT); } - Direction getMode(Pin pin) { - return Direction(LINK_GPIO_GET(REG_RCNT, LINK_GPIO_DIRECTION_BITS[pin])); + /** + * @brief Returns the direction set at `pin`. + */ + [[nodiscard]] Direction getMode(Pin pin) { + return Direction(getBit(Link::_REG_RCNT, DIRECTION_BITS[pin])); } - bool readPin(Pin pin) { - return (REG_RCNT & (1 << LINK_GPIO_DATA_BITS[pin])) != 0; + /** + * @brief Returns whether a `pin` is *HIGH* or not (when set as an input). + * @param pin One of the enum values from `LinkGPIO::Pin`. + */ + [[nodiscard]] bool readPin(Pin pin) { + return (Link::_REG_RCNT & (1 << DATA_BITS[pin])) != 0; } + /** + * @brief Sets a `pin` to be high or not (when set as an output). + * @param pin One of the enum values from `LinkGPIO::Pin`. + * @param isHigh `true` = HIGH, `false` = LOW. + */ void writePin(Pin pin, bool isHigh) { - LINK_GPIO_SET(REG_RCNT, LINK_GPIO_DATA_BITS[pin], isHigh); + setBit(Link::_REG_RCNT, DATA_BITS[pin], isHigh); } + /** + * @brief If it `isEnabled`, an IRQ will be generated when `SI` changes from + * HIGH to LOW. + * @param isEnabled Enable SI-falling interrupts. + */ void setSIInterrupts(bool isEnabled) { - LINK_GPIO_SET(REG_RCNT, LINK_GPIO_BIT_SI_INTERRUPT, isEnabled); + setBit(Link::_REG_RCNT, BIT_SI_INTERRUPT, isEnabled); + } + + private: + int getBit(u16 reg, int bit) { return (reg >> bit) & 1; } + + void setBit(volatile u16& reg, int bit, bool data) { + if (data) + reg |= 1 << bit; + else + reg &= ~(1 << bit); } }; diff --git a/lib/LinkMobile.hpp b/lib/LinkMobile.hpp new file mode 100644 index 0000000..2e042e7 --- /dev/null +++ b/lib/LinkMobile.hpp @@ -0,0 +1,1919 @@ +#ifndef LINK_MOBILE_H +#define LINK_MOBILE_H + +// -------------------------------------------------------------------------- +// A high level driver for the Mobile Adapter GB. +// Check out the REON project -> https://github.com/REONTeam +// -------------------------------------------------------------------------- +// 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(); +// // (do something until `linkMobile->isSessionActive()` returns `true`) +// - 4) Call someone: +// linkMobile->call("127000000001"); +// // (do something until `linkMobile->isConnectedP2P()` returns `true`) +// - 5) Send/receive data: +// LinkMobile::DataTransfer dataTransfer = { .size = 5 }; +// for (u32 i = 0; i < 5; i++) +// dataTransfer.data[i] = ((u8*)"hello")[i]; +// linkMobile->transfer(dataTransfer, &dataTransfer); +// // (do something until `dataTransfer.completed` is `true`) +// // (use `dataTransfer` as the received data) +// - 6) Hang up: +// linkMobile->hangUp(); +// - 7) Connect to the internet: +// linkMobile->callISP("REON password"); +// // (do something until `linkMobile->isConnectedPPP()` returns `true`) +// - 8) Run DNS queries: +// LinkMobile::DNSQuery dnsQuery; +// linkMobile->dnsQuery("something.com", &dnsQuery); +// // (do something until `dnsQuery.completed` is `true`) +// // (use `dnsQuery.success` and `dnsQuery.ipv4`) +// - 9) Open connections: +// auto type = LinkMobile::ConnectionType::TCP; +// LinkMobile::OpenConn openConn; +// linkMobile->openConnection(dnsQuery.ipv4, connType, &openConn); +// // (do something until `openConn.completed` is `true`) +// // (use `openConn.connectionId` as last argument of `transfer(...)`) +// - 10) Close connections: +// LinkMobile::CloseConn closeConn; +// linkMobile->closeConnection(openConn.connectionId, type, &closeConn); +// // (do something until `closeConn.completed` is `true`) +// - 11) Synchronously wait for an action to be completed: +// linkMobile->waitFor(&dnsQuery); +// - 12) Turn off the adapter: +// linkMobile->shutdown(); +// -------------------------------------------------------------------------- +// (*) 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" + +#include +#include "LinkGPIO.hpp" +#include "LinkSPI.hpp" + +#ifndef LINK_MOBILE_QUEUE_SIZE +/** + * @brief Request queue size (how many commands can be queued at the same time). + * 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 3 KB. + */ +#define LINK_MOBILE_QUEUE_SIZE 10 +#endif + +static volatile char LINK_MOBILE_VERSION[] = "LinkMobile/v7.0.0"; + +#define LINK_MOBILE_MAX_USER_TRANSFER_LENGTH 254 +#define LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH 255 +#define LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH 32 +#define LINK_MOBILE_MAX_LOGIN_ID_LENGTH 32 +#define LINK_MOBILE_MAX_PASSWORD_LENGTH 32 +#define LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH 253 +#define LINK_MOBILE_COMMAND_TRANSFER_BUFFER \ + (LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH + 4) +#define LINK_MOBILE_DEFAULT_TIMEOUT (60 * 10) +#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 + +/** + * @brief A high level driver for the Mobile Adapter GB. + */ +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 INIT_WAIT_FRAMES = 7; + static constexpr int INIT_TIMEOUT_FRAMES = 60 * 3; + static constexpr int PING_FREQUENCY_FRAMES = 60; + 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 const char* FALLBACK_ISP_NUMBER = "#9677"; + 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_TELEPHONE_STATUS = 0x17; + 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_CONNECTION_CLOSED = 0x1f; + 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}; + static constexpr u8 DIAL_PHONE_FIRST_BYTE[] = {0, 2, 1, 1}; + + public: + enum State { + NEEDS_RESET = 0, + PINGING = 1, + WAITING_TO_START = 2, + STARTING_SESSION = 3, + ACTIVATING_SIO32 = 4, + WAITING_32BIT_SWITCH = 5, + READING_CONFIGURATION = 6, + SESSION_ACTIVE = 7, + CALL_REQUESTED = 8, + CALLING = 9, + CALL_ESTABLISHED = 10, + ISP_CALL_REQUESTED = 11, + ISP_CALLING = 12, + PPP_LOGIN = 13, + PPP_ACTIVE = 14, + SHUTDOWN_REQUESTED = 15, + ENDING_SESSION = 16, + WAITING_8BIT_SWITCH = 17, + SHUTDOWN = 18 + }; + + enum Role { NO_P2P_CONNECTION, CALLER, RECEIVER }; + + enum ConnectionType { TCP, UDP }; + + struct ConfigurationData { + char magic[2]; + u8 registrationState; + 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; + + char _ispNumber1[16 + 1]; // (parsed from `configurationSlot1`) + } __attribute__((packed)); + + struct AsyncRequest { + volatile bool completed = false; + bool success = false; + + bool fail() { + success = false; + completed = true; + return false; + } + }; + + struct DNSQuery : public AsyncRequest { + u8 ipv4[4] = {}; + }; + + struct OpenConn : public AsyncRequest { + u8 connectionId = 0; + }; + + struct CloseConn : public AsyncRequest {}; + + struct DataTransfer : public AsyncRequest { + u8 data[LINK_MOBILE_MAX_USER_TRANSFER_LENGTH] = {}; + u8 size = 0; + }; + + enum CommandResult { + PENDING, + SUCCESS, + INVALID_DEVICE_ID, + INVALID_COMMAND_ACK, + INVALID_MAGIC_BYTES, + WEIRD_DATA_SIZE, + WRONG_CHECKSUM, + ERROR_CODE, + WEIRD_ERROR_CODE + }; + + struct Error { + enum Type { + NONE, + ADAPTER_NOT_CONNECTED, + PPP_LOGIN_FAILED, + COMMAND_FAILED, + WEIRD_RESPONSE, + TIMEOUT, + WTF + }; + + Error::Type type = Error::Type::NONE; + State state = State::NEEDS_RESET; + u8 cmdId = 0; + CommandResult cmdResult = CommandResult::PENDING; + u8 cmdErrorCode = 0; + bool cmdIsSending = false; + int reqType = -1; + }; + + /** + * @brief Constructs a new LinkMobile object. + * @param timeout Number of *frames* without completing a request to reset a + * connection. Defaults to 600 (10 seconds). + * @param timerId GBA Timer to use for waiting. + */ + explicit LinkMobile(u32 timeout = LINK_MOBILE_DEFAULT_TIMEOUT, + u8 timerId = LINK_MOBILE_DEFAULT_TIMER_ID) { + this->config.timeout = timeout; + this->config.timerId = timerId; + } + + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + + /** + * @brief Activates the library. After some time, if an adapter is connected, + * the state will be changed to `SESSION_ACTIVE`. If not, the state will be + * `NEEDS_RESET`, and you can retrieve the error with `getError()`. + */ + void activate() { + error = {}; + + LINK_MOBILE_BARRIER; + isEnabled = false; + LINK_MOBILE_BARRIER; + + resetState(); + stop(); + + LINK_MOBILE_BARRIER; + isEnabled = true; + LINK_MOBILE_BARRIER; + + start(); + } + + /** + * @brief Deactivates the library, resetting the serial mode to GPIO. + * \warning Calling `shutdown()` first is recommended, but the adapter will + * put itself in sleep mode after 3 seconds anyway. + */ + void deactivate() { + error = {}; + isEnabled = false; + resetState(); + stop(); + } + + /** + * @brief Gracefully shuts down the adapter, closing all connections. + * After some time, the state will be changed to `SHUTDOWN`, and only then + * it's safe to call `deactivate()`. + * \warning Non-blocking. Returns `true` immediately, or `false` if there's no + * active session or available request slots. + */ + bool shutdown() { + if (!canShutdown() || userRequests.isFull()) + return false; + + pushRequest(UserRequest{.type = UserRequest::Type::SHUTDOWN}); + return true; + } + + /** + * @brief Initiates a P2P connection with a `phoneNumber`. After some time, + * the state will be `CALL_ESTABLISHED` (or `ACTIVE_SESSION` if the + * connection fails or ends). + * @param phoneNumber The phone number to call. In REON/libmobile this can be + * a number assigned by the relay server, or a 12-digit IPv4 address (for + * example, "127000000001" would be 127.0.0.1). + * \warning Non-blocking. Returns `true` immediately, or `false` if there's no + * active session or available request slots. + */ + bool call(const char* phoneNumber) { + if (state != SESSION_ACTIVE || userRequests.isFull()) + return false; + + auto request = UserRequest{.type = UserRequest::Type::CALL}; + copyString(request.phoneNumber, phoneNumber, + LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH); + pushRequest(request); + return true; + } + + /** + * @brief Calls the ISP number registered in the adapter configuration, or a + * default number if the adapter hasn't been configured. Then, performs a + * login operation using the provided REON `password` and `loginId`. After + * some time, the state will be `PPP_ACTIVE`. If `loginId` is empty and the + * adapter has been configured, it will use the one stored in the + * configuration. + * @param password The password, as a null-terminated string (max `32` + * characters). + * @param loginId The login ID, as a null-terminated string (max `32` + * characters). It can be empty if it's already stored in the configuration. + * \warning Non-blocking. Returns `true` immediately, or `false` + * if there's no active session, no available request slots, or no login ID. + */ + bool callISP(const char* password, const char* loginId = "") { + if (state != SESSION_ACTIVE || userRequests.isFull()) + return false; + + auto request = UserRequest{.type = UserRequest::Type::PPP_LOGIN}; + copyString(request.password, password, LINK_MOBILE_MAX_PASSWORD_LENGTH); + + if (std::strlen(loginId) > 0) + copyString(request.loginId, loginId, LINK_MOBILE_MAX_LOGIN_ID_LENGTH); + else if (adapterConfiguration.isValid()) + copyString(request.loginId, adapterConfiguration.fields._ispNumber1, + LINK_MOBILE_MAX_LOGIN_ID_LENGTH); + else + return false; + + pushRequest(request); + return true; + } + + /** + * @brief Looks up the IPv4 address for a domain name. + * @param domain A null-terminated string for the domain name (max `253` + * characters). It also accepts a ASCII IPv4 address, converting it into a + * 4-byte address instead of querying the DNS server. + * @param result A pointer to a `LinkMobile::DNSQuery` struct that + * will be filled with the result. When the request is completed, the + * `completed` field will be `true`. If an IP address was found, the `success` + * field will be `true` and the `ipv4` field can be read as a 4-byte address. + * \warning Non-blocking. Returns `true` immediately, or `false` if there's no + * active PPP session or available request slots. + */ + bool dnsQuery(const char* domainName, DNSQuery* result) { + if (state != PPP_ACTIVE || userRequests.isFull()) + return result->fail(); + + result->completed = false; + result->success = false; + u32 size = std::strlen(domainName); + if (size > LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH) + size = LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH; + + auto request = UserRequest{.type = UserRequest::Type::DNS_QUERY, + .dns = result, + .send = {.data = {}}, + .commandSent = false}; + for (u32 i = 0; i < size; i++) + request.send.data[i] = domainName[i]; + request.send.size = size; + + pushRequest(request); + return true; + } + + /** + * @brief Opens a TCP/UDP (`type`) connection at the given `ip` (4-byte + * address) on the given `port`. + * @param ip The 4-byte address. + * @param port The port. + * @param type One of the enum values from `LinkMobile::ConnectionType`. + * @param result A pointer to a `LinkMobile::OpenConn` struct that + * will be filled with the result. When the request is completed, the + * `completed` field will be `true`. If the connection was successful, the + * `success` field will be `true` and the `connectionId` field can be used + * when calling the `transfer(...)` method. If not, you can assume that the + * connection was closed. + * \warning Only `2` connections can be opened at the same time. + * \warning Non-blocking. Returns `true` immediately, or `false` if there's no + * active PPP session, no available request slots. + */ + bool openConnection(const u8* ip, + u16 port, + ConnectionType type, + OpenConn* result) { + if (state != PPP_ACTIVE || userRequests.isFull()) + return result->fail(); + + result->completed = false; + result->success = false; + + auto request = UserRequest{.type = UserRequest::Type::OPEN_CONNECTION, + .open = result, + .connectionType = type, + .commandSent = false}; + for (u32 i = 0; i < 4; i++) + request.ip[i] = ip[i]; + request.port = port; + + pushRequest(request); + return true; + } + + /** + * @brief Closes an active TCP/UDP (`type`) connection. + * @param connectionId The ID of the connection. + * @param type One of the enum values from `LinkMobile::ConnectionType`. + * @param result A pointer to a `LinkMobile::CloseConn` struct that + * will be filled with the result. When the request is completed, the + * `completed` field will be `true`. If the connection was closed correctly, + * the `success` field will be `true`. + * \warning Non-blocking. Returns `true` immediately, or `false` if there's no + * active PPP session, no available request slots. + */ + bool closeConnection(u8 connectionId, + ConnectionType type, + CloseConn* result) { + if (state != PPP_ACTIVE || userRequests.isFull()) + return result->fail(); + + result->completed = false; + result->success = false; + + auto request = UserRequest{.type = UserRequest::Type::CLOSE_CONNECTION, + .close = result, + .connectionType = type, + .commandSent = false}; + request.connectionId = connectionId; + + pushRequest(request); + return true; + } + + /** + * @brief Requests a data transfer and responds the received data. The + * transfer can be done with the other node in a P2P connection, or with any + * open TCP/UDP connection if a PPP session is active. In the case of a + * TCP/UDP connection, the `connectionId` must be provided. + * @param dataToSend The data to send, up to 254 bytes. + * @param result A pointer to a `LinkMobile::DataTransfer` struct that + * will be filled with the received data. It can also point to `dataToSend` to + * reuse the struct. When the transfer is completed, the `completed` field + * will be `true`. If the transfer was successful, the `success` field will be + * `true`. + * \warning Non-blocking. Returns `true` immediately, or `false` if + * there's no active call or available request slots. + */ + bool transfer(DataTransfer dataToSend, + DataTransfer* result, + u8 connectionId = 0xff) { + if ((state != CALL_ESTABLISHED && state != PPP_ACTIVE) || + userRequests.isFull()) + return result->fail(); + + result->completed = false; + result->success = false; + + auto request = UserRequest{.type = UserRequest::Type::TRANSFER, + .connectionId = connectionId, + .send = {.data = {}, .size = dataToSend.size}, + .receive = result, + .commandSent = false}; + for (u32 i = 0; i < dataToSend.size; i++) + request.send.data[i] = dataToSend.data[i]; + pushRequest(request); + return true; + } + + /** + * @brief Waits for `asyncRequest` to be completed. Returns `true` if the + * request was completed && successful, and the adapter session is still + * alive. Otherwise, it returns `false`. + * @param asyncRequest A pointer to a `LinkMobile::DNSQuery`, + * `LinkMobile::OpenConn`, `LinkMobile::CloseConn`, or + * `LinkMobile::DataTransfer`. + */ + bool waitFor(AsyncRequest* asyncRequest) { + while (isSessionActive() && !asyncRequest->completed) + Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_IRQ_VBLANK); + + return isSessionActive() && asyncRequest->completed && + asyncRequest->success; + } + + /** + * @brief Hangs up the current P2P or PPP call. Closes all connections. + * \warning Non-blocking. Returns `true` immediately, or `false` if there's no + * active call or available request slots. + */ + bool hangUp() { + if ((state != CALL_ESTABLISHED && state != PPP_ACTIVE) || + userRequests.isFull()) + return false; + + pushRequest(UserRequest{.type = UserRequest::Type::HANG_UP}); + return true; + } + + /** + * @brief Retrieves the adapter configuration. + * @param configurationData A structure that will be filled with the + * configuration data. If the adapter has an active session, the data is + * already loaded, so it's instantaneous. + * \warning Returns `true` if `configurationData` has been filled, or `false` + * if there's no active session. + */ + [[nodiscard]] bool readConfiguration(ConfigurationData& configurationData) { + if (!isSessionActive()) + return false; + + configurationData = adapterConfiguration.fields; + return true; + } + + /** + * @brief Returns the current state. + * @return One of the enum values from `LinkMobile::State`. + */ + [[nodiscard]] State getState() { return state; } + + /** + * @brief Returns the current role in the P2P connection. + * @return One of the enum values from `LinkMobile::Role`. + */ + [[nodiscard]] Role getRole() { return role; } + + /** + * @brief Returns whether the adapter has been configured or not. + * @return 1 = yes, 0 = no, -1 = unknown (no session active). + */ + [[nodiscard]] int isConfigurationValid() { + if (!isSessionActive()) + return -1; + + return (int)adapterConfiguration.isValid(); + } + + /** + * @brief Returns `true` if a P2P call is established (the state is + * `CALL_ESTABLISHED`). + */ + [[nodiscard]] bool isConnectedP2P() { return state == CALL_ESTABLISHED; } + + /** + * @brief Returns `true` if a PPP session is active (the state is + * `PPP_ACTIVE`). + */ + [[nodiscard]] bool isConnectedPPP() { return state == PPP_ACTIVE; } + + /** + * @brief Returns `true` if the session is active. + */ + [[nodiscard]] bool isSessionActive() { + return state >= SESSION_ACTIVE && state <= SHUTDOWN_REQUESTED; + } + + /** + * @brief Returns `true` if there's an active session and there's no previous + * shutdown requests. + */ + [[nodiscard]] bool canShutdown() { + return isSessionActive() && state != SHUTDOWN_REQUESTED; + } + + /** + * @brief Returns the current operation mode (`LinkSPI::DataSize`). + */ + [[nodiscard]] LinkSPI::DataSize getDataSize() { + return linkSPI->getDataSize(); + } + + /** + * @brief Returns details about the last error that caused the connection to + * be aborted. + */ + [[nodiscard]] Error getError() { return error; } + + ~LinkMobile() { delete linkSPI; } + + /** + * @brief This method is called by the VBLANK interrupt handler. + * \warning This is internal API! + */ + void _onVBlank() { + if (!isEnabled) + return; + + if (shouldAbortOnStateTimeout()) { + timeoutStateFrames++; + if (timeoutStateFrames >= INIT_TIMEOUT_FRAMES) + return abort(Error::Type::ADAPTER_NOT_CONNECTED); + } + + pingFrameCount++; + if (pingFrameCount >= PING_FREQUENCY_FRAMES && isSessionActive() && + !asyncCommand.isActive) { + pingFrameCount = 0; + cmdTelephoneStatus(); + } + + processUserRequests(); + processNewFrame(); + } + + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ + void _onSerial() { + if (!isEnabled) + return; + + linkSPI->_onSerial(); + u32 newData = linkSPI->getAsyncData(); + + if (state == NEEDS_RESET) + return; + + 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); + } + } + + /** + * @brief This method is called by the TIMER interrupt handler. + * \warning This is internal API! + */ + void _onTimer() { + if (!isEnabled || !hasPendingTransfer) + return; + + linkSPI->transferAsync(pendingTransfer); + stopTimer(); + hasPendingTransfer = false; + } + + struct Config { + u32 timeout; + u32 timerId; + }; + + /** + * @brief LinkMobile configuration. + * \warning `deactivate()` first, change the config, and `activate()` again! + */ + Config config; + + private: + enum AdapterType { BLUE, YELLOW, GREEN, RED, UNKNOWN }; + + struct UserRequest { + enum Type { + CALL, + PPP_LOGIN, + DNS_QUERY, + OPEN_CONNECTION, + CLOSE_CONNECTION, + TRANSFER, + HANG_UP, + SHUTDOWN + }; + + Type type; + char phoneNumber[LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH + 1]; + char loginId[LINK_MOBILE_MAX_LOGIN_ID_LENGTH + 1]; + char password[LINK_MOBILE_MAX_PASSWORD_LENGTH + 1]; + DNSQuery* dns; + OpenConn* open; + CloseConn* close; + u8 ip[4]; + u16 port; + ConnectionType connectionType; + u8 connectionId; + DataTransfer send; + DataTransfer* receive; + bool commandSent; + u32 timeout; + bool finished; + + void cleanup() { + if (finished) + return; + AsyncRequest* metadata = type == DNS_QUERY ? (AsyncRequest*)dns + : type == OPEN_CONNECTION ? (AsyncRequest*)open + : type == CLOSE_CONNECTION ? (AsyncRequest*)close + : type == TRANSFER ? (AsyncRequest*)receive + : nullptr; + if (metadata != nullptr) { + metadata->success = false; + metadata->completed = true; + } + } + }; + + union AdapterConfiguration { + ConfigurationData fields; + char bytes[CONFIGURATION_DATA_SIZE]; + + bool isValid() { + return fields.magic[0] == 'M' && fields.magic[1] == 'A' && + (fields.registrationState & 1) == 1 && + 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; } + u8 pureCommandId() { return commandId & (~OR_VALUE); } + } __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; + u8 errorCommandId; + u8 errorCode; + bool isActive = false; + + void reset() { + state = AsyncCommand::State::PENDING; + result = CommandResult::PENDING; + transferred = 0; + cmd = Command{}; + direction = AsyncCommand::Direction::SENDING; + expectedChecksum = 0; + errorCommandId = 0; + errorCode = 0; + isActive = false; + } + + u8 relatedCommandId() { + return result == CommandResult::ERROR_CODE ? errorCommandId + : cmd.header.pureCommandId(); + } + + bool respondsTo(u8 commandId) { + return direction == AsyncCommand::Direction::RECEIVING && + (result == CommandResult::ERROR_CODE + ? errorCommandId == commandId + : cmd.header.commandId == (commandId | OR_VALUE)); + } + + void finish() { + if (cmd.header.commandId == COMMAND_ERROR_STATUS) { + if (cmd.header.size != 2) { + result = CommandResult::WEIRD_ERROR_CODE; + } else { + result = CommandResult::ERROR_CODE; + errorCommandId = cmd.data.bytes[0]; + errorCode = cmd.data.bytes[1]; + } + } 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); + + using RequestQueue = Link::Queue; + + RequestQueue userRequests; + AdapterConfiguration adapterConfiguration; + AsyncCommand asyncCommand; + u32 waitFrames = 0; + u32 timeoutStateFrames = 0; + u32 pingFrameCount = 0; + Role role = Role::NO_P2P_CONNECTION; + LinkSPI* linkSPI = new LinkSPI(); + State state = NEEDS_RESET; + PacketData nextCommandData; + u32 nextCommandDataSize = 0; + bool hasPendingTransfer = false; + u32 pendingTransfer = 0; + AdapterType adapterType = AdapterType::UNKNOWN; + Error error = {}; + volatile bool isEnabled = false; + + void processUserRequests() { + if (!userRequests.canMutate() || userRequests.isEmpty()) + return; + + if (!isSessionActive()) { + userRequests.clear(); + return; + } + + if (userRequests.peek().finished) + userRequests.pop(); + if (userRequests.isEmpty()) + return; + + auto request = userRequests.peek(); + request.timeout++; + if (shouldAbortOnRequestTimeout() && request.timeout >= config.timeout) + return abort(Error::Type::TIMEOUT); + + switch (request.type) { + case UserRequest::Type::CALL: { + if (state != SESSION_ACTIVE && state != CALL_REQUESTED) { + popRequest(); + return; + } + if (state != CALL_REQUESTED) + setState(CALL_REQUESTED); + + if (!asyncCommand.isActive) { + setState(CALLING); + cmdDialTelephone(request.phoneNumber); + popRequest(); + } + break; + } + case UserRequest::Type::PPP_LOGIN: { + if (state != SESSION_ACTIVE && state != ISP_CALL_REQUESTED && + state != ISP_CALLING) { + popRequest(); + return; + } + if (state == SESSION_ACTIVE) + setState(ISP_CALL_REQUESTED); + + if (!asyncCommand.isActive && state == ISP_CALL_REQUESTED) { + setState(ISP_CALLING); + cmdDialTelephone(adapterConfiguration.isValid() + ? adapterConfiguration.fields._ispNumber1 + : FALLBACK_ISP_NUMBER); + } + break; + } + case UserRequest::Type::DNS_QUERY: { + if (state != PPP_ACTIVE) { + popRequest(); + return; + } + + if (!asyncCommand.isActive && !request.commandSent) { + cmdDNSQuery(request.send.data, request.send.size); + request.commandSent = true; + } + break; + } + case UserRequest::Type::OPEN_CONNECTION: { + if (state != PPP_ACTIVE) { + popRequest(); + return; + } + + if (!asyncCommand.isActive && !request.commandSent) { + if (request.connectionType == ConnectionType::TCP) + cmdOpenTCPConnection(request.ip, request.port); + else + cmdOpenUDPConnection(request.ip, request.port); + request.commandSent = true; + } + break; + } + case UserRequest::Type::CLOSE_CONNECTION: { + if (state != PPP_ACTIVE) { + popRequest(); + return; + } + + if (!asyncCommand.isActive && !request.commandSent) { + if (request.connectionType == ConnectionType::TCP) + cmdCloseTCPConnection(request.connectionId); + else + cmdCloseUDPConnection(request.connectionId); + request.commandSent = true; + } + break; + } + case UserRequest::Type::TRANSFER: { + if (state != CALL_ESTABLISHED && state != PPP_ACTIVE) { + popRequest(); + return; + } + + if (!asyncCommand.isActive && !request.commandSent) { + cmdTransferData(request.connectionId, request.send.data, + request.send.size); + request.commandSent = true; + } + break; + } + case UserRequest::Type::HANG_UP: { + if (state != CALL_ESTABLISHED && state != PPP_ACTIVE) { + popRequest(); + return; + } + if (!asyncCommand.isActive) + cmdHangUpTelephone(); + break; + } + case UserRequest::Type::SHUTDOWN: { + if (state != SHUTDOWN_REQUESTED) + setState(SHUTDOWN_REQUESTED); + + if (!asyncCommand.isActive) { + setState(ENDING_SESSION); + cmdEndSession(); + popRequest(); + } + break; + } + default: { + } + } + } + + void processNewFrame() { + switch (state) { + case WAITING_TO_START: { + waitFrames--; + + if (waitFrames == 0) { + setState(STARTING_SESSION); + cmdBeginSession(); + } + break; + } + case WAITING_32BIT_SWITCH: { + waitFrames--; + + if (waitFrames == 0) { + setState(READING_CONFIGURATION); + cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK); + } + break; + } + case SESSION_ACTIVE: { + if (!asyncCommand.isActive) + cmdWaitForTelephoneCall(); + + break; + } + case WAITING_8BIT_SWITCH: { + waitFrames--; + + if (waitFrames == 0) { + error = {}; + setState(SHUTDOWN); + } + break; + } + default: { + } + } + } + + void processAsyncCommand() { + if (asyncCommand.result != CommandResult::SUCCESS) { + if (shouldAbortOnCommandFailure()) + return abort(Error::Type::COMMAND_FAILED); + else + abort(Error::Type::COMMAND_FAILED, false); // (log the error) + } + + _LMLOG_("%s $%X [%d]", + asyncCommand.direction == AsyncCommand::Direction::SENDING ? ">!" + : "activate(LinkSPI::Mode::MASTER_256KBPS, + LinkSPI::DataSize::SIZE_32BIT); + 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 == 0) { + cmdReadConfigurationData(CONFIGURATION_DATA_CHUNK, + CONFIGURATION_DATA_CHUNK); + } else { + setISPNumber(); + setState(SESSION_ACTIVE); + } + break; + } + case SESSION_ACTIVE: { + if (!asyncCommand.respondsTo(COMMAND_WAIT_FOR_TELEPHONE_CALL)) + return; + + if (asyncCommand.result == CommandResult::SUCCESS) { + setState(CALL_ESTABLISHED); + role = Role::RECEIVER; + } else { + // (no call received) + } + break; + } + case CALLING: { + if (!asyncCommand.respondsTo(COMMAND_DIAL_TELEPHONE)) + return; + + if (asyncCommand.result == CommandResult::SUCCESS) { + setState(CALL_ESTABLISHED); + role = Role::CALLER; + } else { + // (call failed) + setState(SESSION_ACTIVE); + } + break; + } + case CALL_ESTABLISHED: { + if (asyncCommand.respondsTo(COMMAND_HANG_UP_TELEPHONE)) { + setState(SESSION_ACTIVE); + return; + } + if (!asyncCommand.respondsTo(COMMAND_TRANSFER_DATA)) + return; + if (userRequests.isEmpty()) + return abort(Error::Type::WTF); + auto request = userRequests.peekRef(); + + handleTransferDataResponse(request); + + break; + } + case ISP_CALLING: { + if (!asyncCommand.respondsTo(COMMAND_DIAL_TELEPHONE)) + return; + if (userRequests.isEmpty()) + return abort(Error::Type::WTF); + auto request = userRequests.peekRef(); + if (request->type != UserRequest::PPP_LOGIN) + return abort(Error::Type::WTF); + + if (asyncCommand.result == CommandResult::SUCCESS) { + setState(PPP_LOGIN); + cmdISPLogin(request->loginId, request->password); + } else { + // (ISP call failed) + setState(SESSION_ACTIVE); + } + request->finished = true; + + break; + } + case PPP_LOGIN: { + if (!asyncCommand.respondsTo(COMMAND_ISP_LOGIN)) + return; + if (asyncCommand.result != CommandResult::SUCCESS) + return abort(Error::Type::PPP_LOGIN_FAILED); + + setState(PPP_ACTIVE); + break; + } + case PPP_ACTIVE: { + if (asyncCommand.respondsTo(COMMAND_HANG_UP_TELEPHONE)) { + setState(SESSION_ACTIVE); + return; + } + + if (userRequests.isEmpty()) + return; + auto request = userRequests.peekRef(); + + if (asyncCommand.respondsTo(COMMAND_DNS_QUERY)) { + if (request->type != UserRequest::DNS_QUERY) + return abort(Error::Type::WTF); + + if (asyncCommand.result == CommandResult::SUCCESS) { + if (asyncCommand.cmd.header.size != 4) + return abort(Error::Type::WEIRD_RESPONSE); + for (u32 i = 0; i < 4; i++) + request->dns->ipv4[i] = asyncCommand.cmd.data.bytes[i]; + request->dns->success = true; + } else { + request->dns->success = false; + } + + request->dns->completed = true; + request->finished = true; + } else if (asyncCommand.respondsTo(COMMAND_OPEN_TCP_CONNECTION) || + asyncCommand.respondsTo(COMMAND_OPEN_UDP_CONNECTION)) { + if (request->type != UserRequest::OPEN_CONNECTION) + return abort(Error::Type::WTF); + + if (asyncCommand.result == CommandResult::SUCCESS) { + if (asyncCommand.cmd.header.size != 1) + return abort(Error::Type::WEIRD_RESPONSE); + request->open->connectionId = asyncCommand.cmd.data.bytes[0]; + request->open->success = true; + } else { + request->open->success = false; + } + + request->open->completed = true; + request->finished = true; + } else if (asyncCommand.respondsTo(COMMAND_CLOSE_TCP_CONNECTION) || + asyncCommand.respondsTo(COMMAND_CLOSE_UDP_CONNECTION)) { + if (request->type != UserRequest::CLOSE_CONNECTION) + return abort(Error::Type::WTF); + + request->close->success = + asyncCommand.result == CommandResult::SUCCESS; + request->close->completed = true; + request->finished = true; + } else if (asyncCommand.respondsTo(COMMAND_TRANSFER_DATA)) { + if (request->type != UserRequest::TRANSFER) + return abort(Error::Type::WTF); + + handleTransferDataResponse(request); + } else if (asyncCommand.respondsTo(COMMAND_CONNECTION_CLOSED)) { + if (request->type != UserRequest::TRANSFER) + return abort(Error::Type::WTF); + + // (connection closed) + request->receive->success = false; + request->receive->completed = true; + request->finished = true; + } + break; + } + case ENDING_SESSION: { + if (!asyncCommand.respondsTo(COMMAND_END_SESSION)) + return; + + setState(WAITING_8BIT_SWITCH); + waitFrames = INIT_WAIT_FRAMES; + linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS, + LinkSPI::DataSize::SIZE_8BIT); + break; + } + default: { + } + } + } + + void handleTransferDataResponse(UserRequest* request) { + if (request->type != UserRequest::TRANSFER) + return abort(Error::Type::WTF); + + if (asyncCommand.result == CommandResult::SUCCESS) { + if (asyncCommand.cmd.header.size == 0) + return abort(Error::Type::WEIRD_RESPONSE); + + u32 size = asyncCommand.cmd.header.size - 1; + for (u32 i = 0; i < size; i++) + request->receive->data[i] = asyncCommand.cmd.data.bytes[1 + i]; + + request->receive->data[size] = '\0'; + // (just for convenience when using strings, the buffer is big enough) + + request->receive->size = size; + request->receive->success = true; + } else { + request->receive->success = false; + } + + request->receive->completed = true; + request->finished = true; + } + + void processLoosePacket(u32 newData) { + switch (state) { + case PINGING: { + setState(WAITING_TO_START); + waitFrames = INIT_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)); + } + + void cmdEndSession() { sendCommandAsync(buildCommand(COMMAND_END_SESSION)); } + + void cmdDialTelephone(const char* phoneNumber) { + addData(DIAL_PHONE_FIRST_BYTE[adapterType], true); + for (u32 i = 0; i < std::strlen(phoneNumber); i++) + addData(phoneNumber[i]); + sendCommandAsync(buildCommand(COMMAND_DIAL_TELEPHONE, true)); + } + + void cmdHangUpTelephone() { + sendCommandAsync(buildCommand(COMMAND_HANG_UP_TELEPHONE, true)); + } + + void cmdWaitForTelephoneCall() { + sendCommandAsync(buildCommand(COMMAND_WAIT_FOR_TELEPHONE_CALL)); + } + + void cmdTransferData(u8 connectionId, const u8* data, u8 size) { + addData(connectionId, true); + for (u32 i = 0; i < size; i++) + addData(data[i]); + sendCommandAsync(buildCommand(COMMAND_TRANSFER_DATA, true)); + } + + void cmdTelephoneStatus() { + sendCommandAsync(buildCommand(COMMAND_TELEPHONE_STATUS)); + } + + void cmdSIO32(bool enabled) { + addData(enabled, true); + sendCommandAsync(buildCommand(COMMAND_SIO32, true)); + } + + void cmdReadConfigurationData(u8 offset, u8 size) { + addData(offset, true); + addData(CONFIGURATION_DATA_CHUNK); + sendCommandAsync(buildCommand(COMMAND_READ_CONFIGURATION_DATA, true)); + } + + void cmdISPLogin(const char* loginId, const char* password) { + u32 loginIdLength = std::strlen(loginId); + addData(loginIdLength, true); + for (u32 i = 0; i < loginIdLength; i++) + addData(loginId[i]); + + u32 passwordLength = std::strlen(password); + addData(passwordLength); + for (u32 i = 0; i < passwordLength; i++) + addData(password[i]); + + bool isConfigured = isConfigurationValid(); + for (u32 i = 0; i < 4; i++) + addData(isConfigured ? adapterConfiguration.fields.primaryDNS[i] : 0); + for (u32 i = 0; i < 4; i++) + addData(isConfigured ? adapterConfiguration.fields.secondaryDNS[i] : 0); + + sendCommandAsync(buildCommand(COMMAND_ISP_LOGIN, true)); + } + + void cmdOpenTCPConnection(const u8* ip, u16 port) { + for (u32 i = 0; i < 4; i++) + addData(ip[i], i == 0); + addData(msB16(port)); + addData(lsB16(port)); + sendCommandAsync(buildCommand(COMMAND_OPEN_TCP_CONNECTION, true)); + } + + void cmdCloseTCPConnection(u8 connectionId) { + addData(connectionId); + sendCommandAsync(buildCommand(COMMAND_CLOSE_TCP_CONNECTION, true)); + } + + void cmdOpenUDPConnection(const u8* ip, u16 port) { + for (u32 i = 0; i < 4; i++) + addData(ip[i], i == 0); + addData(msB16(port)); + addData(lsB16(port)); + sendCommandAsync(buildCommand(COMMAND_OPEN_UDP_CONNECTION, true)); + } + + void cmdCloseUDPConnection(u8 connectionId) { + addData(connectionId); + sendCommandAsync(buildCommand(COMMAND_CLOSE_UDP_CONNECTION, true)); + } + + void cmdDNSQuery(const u8* data, u8 size) { + for (int i = 0; i < size; i++) + addData(data[i], i == 0); + sendCommandAsync(buildCommand(COMMAND_DNS_QUERY, true)); + } + + void setISPNumber() { + static const char BCD[16] = "0123456789#*cde"; + + for (u32 i = 0; i < 8; i++) { + u8 b = adapterConfiguration.fields.configurationSlot1[i]; + adapterConfiguration.fields._ispNumber1[i * 2] = BCD[b >> 4]; + adapterConfiguration.fields._ispNumber1[i * 2 + 1] = BCD[b & 0xF]; + } + } + + void pushRequest(UserRequest request) { + request.timeout = 0; + request.finished = false; + userRequests.syncPush(request); + } + + void popRequest() { + auto request = userRequests.peekRef(); + request->cleanup(); + request->finished = true; + userRequests.pop(); + } + + bool shouldAbortOnStateTimeout() { + return state > NEEDS_RESET && state < SESSION_ACTIVE; + } + + bool shouldAbortOnRequestTimeout() { return true; } + + bool shouldAbortOnCommandFailure() { + u8 commandId = asyncCommand.relatedCommandId(); + return asyncCommand.direction == AsyncCommand::Direction::SENDING || + (commandId != COMMAND_WAIT_FOR_TELEPHONE_CALL && + commandId != COMMAND_DIAL_TELEPHONE && !isAsyncRequest(commandId)); + } + + bool isAsyncRequest(u8 commandId) { + return commandId == COMMAND_DNS_QUERY || + commandId == COMMAND_OPEN_TCP_CONNECTION || + commandId == COMMAND_CLOSE_TCP_CONNECTION || + commandId == COMMAND_OPEN_UDP_CONNECTION || + commandId == COMMAND_CLOSE_UDP_CONNECTION || + commandId == COMMAND_TRANSFER_DATA; + } + + void addData(u8 value, bool start = false) { + if (start) { + nextCommandDataSize = 0; + nextCommandData = PacketData{}; + } + nextCommandData.bytes[nextCommandDataSize] = value; + nextCommandDataSize++; + } + + void copyString(char* target, const char* source, u32 length) { + u32 len = std::strlen(source); + + for (u32 i = 0; i < length + 1; i++) + if (i < len) + target[i] = source[i]; + else + target[i] = '\0'; + } + + void setState(State newState) { + role = Role::NO_P2P_CONNECTION; + State oldState = state; + state = newState; + timeoutStateFrames = 0; + pingFrameCount = 0; + _LMLOG_("!! new state: %d -> %d", oldState, newState); + (void)oldState; + } + + void abort(Error::Type errorType, bool fatal = true) { + auto newError = Error{ + .type = errorType, + .state = state, + .cmdId = asyncCommand.relatedCommandId(), + .cmdResult = asyncCommand.result, + .cmdErrorCode = asyncCommand.errorCode, + .cmdIsSending = + asyncCommand.direction == AsyncCommand::Direction::SENDING, + + .reqType = userRequests.isEmpty() ? -1 : userRequests.peek().type}; + + _LMLOG_( + "!! %s:\n error: %d\n cmdId: %s$%X\n cmdResult: %d\n " + "cmdErrorCode: %d", + fatal ? "aborted" : "failed", newError.type, + newError.cmdIsSending ? ">" : "<", newError.cmdId, newError.cmdResult, + newError.cmdErrorCode); + (void)newError; + + if (fatal) { + error = newError; + resetState(); + stop(); + } + } + + void reset() { + resetState(); + stop(); + start(); + } + + void resetState() { + setState(NEEDS_RESET); + + this->adapterConfiguration = AdapterConfiguration{}; + this->userRequests.clear(); + this->asyncCommand.reset(); + this->waitFrames = 0; + this->timeoutStateFrames = 0; + this->role = Role::NO_P2P_CONNECTION; + this->nextCommandDataSize = 0; + this->hasPendingTransfer = false; + this->pendingTransfer = 0; + this->adapterType = AdapterType::UNKNOWN; + + userRequests.syncClear(); + } + + 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) { + _LMLOG_(">> $%X [%d] (...)", command.header.commandId, command.header.size); + asyncCommand.reset(); + asyncCommand.cmd = command; + 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; + + 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; + + 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 (asyncCommand.transferred < mainSize + 4) { + // 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 || + asyncCommand.cmd.header.size > + LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH) + 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); + if (asyncCommand.cmd.header._unusedSizeHigh_ != 0 || + asyncCommand.cmd.header.size > + LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH) + return asyncCommand.fail(CommandResult::WEIRD_DATA_SIZE); + 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 (checksum != 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 (checksum != 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) { + if (adapterType == AdapterType::UNKNOWN) + adapterType = static_cast(i); + + 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; } + bool isBitHigh(u8 byte, u8 bit) { return (byte >> bit) & 1; } +}; + +extern LinkMobile* linkMobile; + +/** + * @brief VBLANK interrupt handler. + */ +inline void LINK_MOBILE_ISR_VBLANK() { + linkMobile->_onVBlank(); +} + +/** + * @brief SERIAL interrupt handler. + */ +inline void LINK_MOBILE_ISR_SERIAL() { + linkMobile->_onSerial(); +} + +/** + * @brief TIMER interrupt handler. + */ +inline void LINK_MOBILE_ISR_TIMER() { + linkMobile->_onTimer(); +} + +#undef _LMLOG_ + +#endif // LINK_MOBILE_H diff --git a/lib/LinkPS2Keyboard.hpp b/lib/LinkPS2Keyboard.hpp index d93e23d..21b7611 100644 --- a/lib/LinkPS2Keyboard.hpp +++ b/lib/LinkPS2Keyboard.hpp @@ -2,7 +2,7 @@ #define LINK_PS2_KEYBOARD_H // -------------------------------------------------------------------------- -// A PS/2 Keyboard Adapter for the GBA +// A PS/2 Keyboard Adapter for the GBA. // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: @@ -17,6 +17,16 @@ // linkPS2Keyboard->activate(); // - 4) Handle events in the callback sent to LinkPS2Keyboard's constructor! // -------------------------------------------------------------------------- +// (*1) 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) +// -------------------------------------------------------------------------- +// (*2) The hardware is very sensitive to timing. Make sure that +// `LINK_PS2_KEYBOARD_ISR_SERIAL()` is handled on time. That means: +// Be careful with DMA usage (which stops the CPU), and write short +// interrupt handlers (or activate nested interrupts by setting +// `REG_IME=1` at the start of your handlers). +// -------------------------------------------------------------------------- // ____________ // | Pinout | // |PS/2 --- GBA| @@ -27,106 +37,142 @@ // |GND ---> GND| // -------------------------------------------------------------------------- -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif -// Example Scan Codes: (Num Lock OFF) -#define LINK_PS2_KEYBOARD_KEY_Z 26 // Z -#define LINK_PS2_KEYBOARD_KEY_Q 21 // Q -#define LINK_PS2_KEYBOARD_KEY_S 27 // S -#define LINK_PS2_KEYBOARD_KEY_E 36 // E -#define LINK_PS2_KEYBOARD_KEY_C 33 // C -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_1 105 // Numpad 1 -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_7 108 // Numpad 7 -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_5 115 // Numpad 5 -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_9 125 // Numpad 9 -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_3 122 // Numpad 3 -#define LINK_PS2_KEYBOARD_KEY_ENTER 90 // Enter -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_PLUS 121 // Numpad + -#define LINK_PS2_KEYBOARD_KEY_BACKSPACE 102 // Backspace -#define LINK_PS2_KEYBOARD_KEY_NUMPAD_MINUS 123 // Numpad - -#define LINK_PS2_KEYBOARD_KEY_LEFT 107 // Left -#define LINK_PS2_KEYBOARD_KEY_RIGHT 116 // Right -#define LINK_PS2_KEYBOARD_KEY_UP 117 // Up -#define LINK_PS2_KEYBOARD_KEY_ESC 118 // ESC -#define LINK_PS2_KEYBOARD_KEY_SUPR 113 // Supr -// --- -#define LINK_PS2_KEYBOARD_KEY_RELEASE 240 // Triggered before each key release -#define LINK_PS2_KEYBOARD_KEY_SPECIAL 224 // Triggered before special keys +#include "_link_common.hpp" -#define LINK_PS2_KEYBOARD_SI_DIRECTION 0b1000000 -#define LINK_PS2_KEYBOARD_SO_DIRECTION 0b10000000 -#define LINK_PS2_KEYBOARD_SI_DATA 0b100 -#define LINK_PS2_KEYBOARD_SO_DATA 0b1000 -#define LINK_PS2_KEYBOARD_TIMEOUT_FRAMES 15 // (~250ms) - -static volatile char LINK_PS2_KEYBOARD_VERSION[] = "LinkPS2Keyboard/v6.3.0"; +static volatile char LINK_PS2_KEYBOARD_VERSION[] = "LinkPS2Keyboard/v7.0.0"; +/** + * @brief A PS/2 Keyboard Adapter for the GBA. + */ class LinkPS2Keyboard { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + static constexpr int RCNT_GPIO_AND_SI_IRQ = 0b1000000100000000; + static constexpr int RCNT_GPIO = 0b1000000000000000; + static constexpr int SI_DIRECTION = 0b1000000; + static constexpr int SO_DIRECTION = 0b10000000; + static constexpr int SI_DATA = 0b100; + static constexpr int SO_DATA = 0b1000; + static constexpr int TIMEOUT_FRAMES = 15; // (~250ms) + + LinkPS2Keyboard() = delete; + public: - explicit LinkPS2Keyboard(std::function onEvent) { - this->onEvent = onEvent; - } + using EventCallback = void (*)(u8 event); - bool isActive() { return isEnabled; } + /** + * @brief Constructs a new LinkPS2Keyboard object. + * @param onEvent Function pointer that will receive the scan codes (`u8`). + * Check out `LINK_PS2_KEYBOARD_KEY` and `LINK_PS2_KEYBOARD_EVENT` for codes. + */ + explicit LinkPS2Keyboard(EventCallback onEvent) { this->onEvent = onEvent; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + + /** + * @brief Activates the library. + */ void activate() { deactivate(); - REG_RCNT = 0b1000000100000000; // General Purpose Mode + SI interrupts - REG_SIOCNT = 0; // Unused + Link::_REG_RCNT = RCNT_GPIO_AND_SI_IRQ; + Link::_REG_SIOCNT = 0; bitcount = 0; incoming = 0; + parityBit = 0; prevFrame = 0; frameCounter = 0; isEnabled = true; } + /** + * @brief Deactivates the library. + */ void deactivate() { isEnabled = false; - REG_RCNT = 0b1000000000000000; // General Purpose Mode - REG_SIOCNT = 0; // Unused + Link::_REG_RCNT = RCNT_GPIO; + Link::_REG_SIOCNT = 0; } + /** + * @brief This method is called by the VBLANK interrupt handler. + * \warning This is internal API! + */ void _onVBlank() { frameCounter++; } + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ void _onSerial() { - u8 val = (REG_RCNT & LINK_PS2_KEYBOARD_SO_DATA) != 0; + if (!isEnabled) + return; + + u8 val = (Link::_REG_RCNT & SO_DATA) != 0; u32 nowFrame = frameCounter; - if (nowFrame - prevFrame > LINK_PS2_KEYBOARD_TIMEOUT_FRAMES) { + if (nowFrame - prevFrame > TIMEOUT_FRAMES) { bitcount = 0; incoming = 0; + parityBit = 0; } prevFrame = nowFrame; - u8 n = bitcount - 1; - if (n <= 7) - incoming |= (val << n); - bitcount++; - - if (bitcount == 11) { - onEvent(incoming); + if (bitcount == 0 && val == 0) { // start bit detected + // start bit is always 0, so only proceed if val is 0 + bitcount++; + } else if (bitcount >= 1 && bitcount <= 8) { // data bits + incoming |= (val << (bitcount - 1)); + bitcount++; + } else if (bitcount == 9) { // parity bit + // store parity bit for later check + parityBit = val; + bitcount++; + } else if (bitcount == 10) { // stop bit + if (val == 1) { // stop bit should be 1 + // calculate parity (including the stored parity bit from previous IRQ) + u8 parity = 0; + for (u8 i = 0; i < 8; i++) + parity += (incoming >> i) & 1; + parity += parityBit; + if (parity % 2 != 0) // odd parity as expected + onEvent(incoming); + } bitcount = 0; incoming = 0; + parityBit = 0; } } private: bool isEnabled = false; - uint8_t bitcount = 0; - uint8_t incoming = 0; - uint32_t prevFrame = 0; + u8 bitcount = 0; + u8 incoming = 0; + u8 parityBit = 0; + u32 prevFrame = 0; u32 frameCounter = 0; - std::function onEvent; + EventCallback onEvent; }; extern LinkPS2Keyboard* linkPS2Keyboard; +/** + * @brief VBLANK interrupt handler. + */ inline void LINK_PS2_KEYBOARD_ISR_VBLANK() { if (!linkPS2Keyboard->isActive()) return; @@ -134,11 +180,105 @@ inline void LINK_PS2_KEYBOARD_ISR_VBLANK() { linkPS2Keyboard->_onVBlank(); } +/** + * @brief SERIAL interrupt handler. + */ inline void LINK_PS2_KEYBOARD_ISR_SERIAL() { - if (!linkPS2Keyboard->isActive()) - return; - linkPS2Keyboard->_onSerial(); } -#endif // LINK_PS2_KEYBOARD_H \ No newline at end of file +/** + * @brief Key Scan Code list. + */ +enum LINK_PS2_KEYBOARD_KEY { + ESC = 118, + F1 = 5, + F2 = 6, + F3 = 4, + F4 = 12, + F5 = 3, + F6 = 11, + F7 = 131, + F8 = 10, + F9 = 1, + F10 = 9, + F11 = 120, + F12 = 7, + BACKSPACE = 102, + TAB = 13, + ENTER = 90, + SHIFT_L = 18, + SHIFT_R = 89, + SUPER = 97, + CTRL_L = 20, + SPECIAL_CTRL_R = 224 + 20, + ALT_L = 17, + SPECIAL_ALT_R = 224 + 17, + SPACE = 41, + CAPS_LOCK = 88, + NUM_LOCK = 119, + SCROLL_LOCK = 126, + SPECIAL_INSERT = 224 + 112, + SPECIAL_DELETE = 224 + 113, + SPECIAL_HOME = 224 + 108, + SPECIAL_END = 224 + 105, + SPECIAL_PAGE_UP = 224 + 125, + SPECIAL_PAGE_DOWN = 224 + 122, + SPECIAL_UP = 224 + 117, + SPECIAL_DOWN = 224 + 114, + SPECIAL_LEFT = 224 + 107, + SPECIAL_RIGHT = 224 + 116, + A = 28, + B = 50, + C = 33, + D = 35, + E = 36, + F = 43, + G = 52, + H = 51, + I = 67, + J = 59, + K = 66, + L = 75, + M = 58, + N = 49, + O = 68, + P = 77, + Q = 21, + R = 45, + S = 27, + T = 44, + U = 60, + V = 42, + W = 29, + X = 34, + Y = 53, + Z = 26, + NUMPAD_0 = 112, + NUMPAD_1 = 105, + NUMPAD_2 = 114, + NUMPAD_3 = 122, + NUMPAD_4 = 107, + NUMPAD_5 = 115, + NUMPAD_6 = 116, + NUMPAD_7 = 108, + NUMPAD_8 = 117, + NUMPAD_9 = 125, + NUMPAD_PLUS = 121, + NUMPAD_MINUS = 123, + SPECIAL_NUMPAD_ENTER = 224 + 90, + NUMPAD_DOT = 113, + NUMPAD_ASTERISK = 124, + NUMPAD_SLASH = 74 +}; + +/** + * @brief Event Scan Code list. + */ +enum LINK_PS2_KEYBOARD_EVENT { + SELF_TEST_PASSED = 0xAA, // Triggered when hot-plugging the keyboard + RELEASE = 240, // Triggered before each key release + SPECIAL = 224 // Triggered before special keys +}; + +#endif // LINK_PS2_KEYBOARD_H diff --git a/lib/LinkPS2Mouse.hpp b/lib/LinkPS2Mouse.hpp index 6bd64e5..9a71609 100644 --- a/lib/LinkPS2Mouse.hpp +++ b/lib/LinkPS2Mouse.hpp @@ -2,13 +2,13 @@ #define LINK_PS2_MOUSE_H // -------------------------------------------------------------------------- -// A PS/2 Mouse Adapter for the GBA +// A PS/2 Mouse Adapter for the GBA. // (Based on https://github.com/kristopher/PS2-Mouse-Arduino, MIT license) // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: // LinkPS2Mouse* linkPS2Mouse = new LinkPS2Mouse(); -// - 2) Add the required interrupt service routines: (*) +// - 2) Add the required interrupt service routines: // irq_init(NULL); // irq_add(II_TIMER2, NULL); // - 3) Initialize the library with: @@ -21,6 +21,10 @@ // data[1] // X movement // data[2] // Y movement // -------------------------------------------------------------------------- +// considerations: +// - `activate()` or `report(...)` could freeze the system if not connected! +// - detecting timeouts using interrupts is the user's responsibility! +// -------------------------------------------------------------------------- // ____________ // | Pinout | // |PS/2 --- GBA| @@ -31,30 +35,54 @@ // |GND ---> GND| // -------------------------------------------------------------------------- -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + +static volatile char LINK_PS2_MOUSE_VERSION[] = "LinkPS2Mouse/v7.0.0"; #define LINK_PS2_MOUSE_LEFT_CLICK 0b001 #define LINK_PS2_MOUSE_RIGHT_CLICK 0b010 #define LINK_PS2_MOUSE_MIDDLE_CLICK 0b100 -#define LINK_PS2_MOUSE_SI_DIRECTION 0b1000000 -#define LINK_PS2_MOUSE_SO_DIRECTION 0b10000000 -#define LINK_PS2_MOUSE_SI_DATA 0b100 -#define LINK_PS2_MOUSE_SO_DATA 0b1000 -#define LINK_PS2_MOUSE_TO_TICKS 17 - -const u16 LINK_PS2_MOUSE_IRQ_IDS[] = {IRQ_TIMER0, IRQ_TIMER1, IRQ_TIMER2, - IRQ_TIMER3}; - -static volatile char LINK_PS2_MOUSE_VERSION[] = "LinkPS2Mouse/v6.3.0"; - +/** + * @brief A PS/2 Mouse Adapter for the GBA. + */ class LinkPS2Mouse { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + using s16 = signed short; + + static constexpr int RCNT_GPIO = 0b1000000000000000; + static constexpr int SI_DIRECTION = 0b1000000; + static constexpr int SO_DIRECTION = 0b10000000; + static constexpr int SI_DATA = 0b100; + static constexpr int SO_DATA = 0b1000; + static constexpr int TO_TICKS = 17; + + LinkPS2Mouse() = delete; + public: + /** + * @brief Constructs a new LinkPS2Mouse object. + * @param waitTimerId `(0~3)` GBA Timer used for delays. + */ explicit LinkPS2Mouse(u8 waitTimerId) { this->waitTimerId = waitTimerId; } - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + /** + * @brief Activates the library. + * \warning Could freeze the system if nothing is connected! + * \warning Detect timeouts using timer interrupts! + */ void activate() { deactivate(); @@ -73,13 +101,24 @@ class LinkPS2Mouse { isEnabled = true; } + /** + * @brief Deactivates the library. + */ void deactivate() { isEnabled = false; - REG_RCNT = 0b1000000000000000; // General Purpose Mode - REG_SIOCNT = 0; // Unused + Link::_REG_RCNT = RCNT_GPIO; + Link::_REG_SIOCNT = 0; } + /** + * @brief Fills the `data` int array with a report. The first int contains + * *clicks* that you can check against the bitmasks + * `LINK_PS2_MOUSE_LEFT_CLICK`, `LINK_PS2_MOUSE_MIDDLE_CLICK`, and + * `LINK_PS2_MOUSE_RIGHT_CLICK`. The second int is the *X movement*, and the + * third int is the *Y movement*. + * @param data The array to be filled with data. + */ void report(int (&data)[3]) { write(0xeb); // send read data readByte(); // read ack byte @@ -101,7 +140,7 @@ class LinkPS2Mouse { s16 x = readByte(); if ((status & (1 << 4)) != 0) // negative - for (int i = 8; i < 16; i++) + for (u32 i = 8; i < 16; i++) x |= 1 << i; return x; } @@ -110,7 +149,7 @@ class LinkPS2Mouse { s16 y = readByte(); if ((status & (1 << 5)) != 0) // negative - for (int i = 8; i < 16; i++) + for (u32 i = 8; i < 16; i++) y |= 1 << i; return y; } @@ -168,7 +207,7 @@ class LinkPS2Mouse { ; while (!getClock()) ; // eat start bit - for (int i = 0; i < 8; i++) { + for (u32 i = 0; i < 8; i++) { data |= readBit() << i; } readBit(); // parity bit @@ -178,61 +217,63 @@ class LinkPS2Mouse { return data; } - bool readBit() { + volatile bool readBit() { while (getClock()) ; - bool bit = getData(); + volatile bool bit = getData(); while (!getClock()) ; return bit; } void waitMilliseconds(u16 milliseconds) { - u16 ticksOf1024Cycles = milliseconds * LINK_PS2_MOUSE_TO_TICKS; - REG_TM[waitTimerId].start = -ticksOf1024Cycles; - REG_TM[waitTimerId].cnt = TM_ENABLE | TM_IRQ | TM_FREQ_1024; - IntrWait(1, LINK_PS2_MOUSE_IRQ_IDS[waitTimerId]); - REG_TM[waitTimerId].cnt = 0; + u16 ticksOf1024Cycles = milliseconds * TO_TICKS; + Link::_REG_TM[waitTimerId].start = -ticksOf1024Cycles; + Link::_REG_TM[waitTimerId].cnt = + Link::_TM_ENABLE | Link::_TM_IRQ | Link::_TM_FREQ_1024; + Link::_IntrWait(1, Link::_TIMER_IRQ_IDS[waitTimerId]); + Link::_REG_TM[waitTimerId].cnt = 0; } void waitMicroseconds(u16 microseconds) { - u16 cycles = microseconds * LINK_PS2_MOUSE_TO_TICKS; - REG_TM[waitTimerId].start = -cycles; - REG_TM[waitTimerId].cnt = TM_ENABLE | TM_IRQ | TM_FREQ_1; - IntrWait(1, LINK_PS2_MOUSE_IRQ_IDS[waitTimerId]); - REG_TM[waitTimerId].cnt = 0; + u16 cycles = microseconds * TO_TICKS; + Link::_REG_TM[waitTimerId].start = -cycles; + Link::_REG_TM[waitTimerId].cnt = + Link::_TM_ENABLE | Link::_TM_IRQ | Link::_TM_FREQ_1; + Link::_IntrWait(1, Link::_TIMER_IRQ_IDS[waitTimerId]); + Link::_REG_TM[waitTimerId].cnt = 0; } - bool getClock() { - REG_RCNT &= ~LINK_PS2_MOUSE_SI_DIRECTION; - return (REG_RCNT & LINK_PS2_MOUSE_SI_DATA) >> 0; + volatile bool getClock() { + Link::_REG_RCNT &= ~SI_DIRECTION; + return (Link::_REG_RCNT & SI_DATA) >> 0; } - bool getData() { - REG_RCNT &= ~LINK_PS2_MOUSE_SO_DIRECTION; - return (REG_RCNT & LINK_PS2_MOUSE_SO_DATA) >> 1; + volatile bool getData() { + Link::_REG_RCNT &= ~SO_DIRECTION; + return (Link::_REG_RCNT & SO_DATA) >> 1; } void setClockHigh() { - REG_RCNT |= LINK_PS2_MOUSE_SI_DIRECTION; - REG_RCNT |= LINK_PS2_MOUSE_SI_DATA; + Link::_REG_RCNT |= SI_DIRECTION; + Link::_REG_RCNT |= SI_DATA; } void setClockLow() { - REG_RCNT |= LINK_PS2_MOUSE_SI_DIRECTION; - REG_RCNT &= ~LINK_PS2_MOUSE_SI_DATA; + Link::_REG_RCNT |= SI_DIRECTION; + Link::_REG_RCNT &= ~SI_DATA; } void setDataHigh() { - REG_RCNT |= LINK_PS2_MOUSE_SO_DIRECTION; - REG_RCNT |= LINK_PS2_MOUSE_SO_DATA; + Link::_REG_RCNT |= SO_DIRECTION; + Link::_REG_RCNT |= SO_DATA; } void setDataLow() { - REG_RCNT |= LINK_PS2_MOUSE_SO_DIRECTION; - REG_RCNT &= ~LINK_PS2_MOUSE_SO_DATA; + Link::_REG_RCNT |= SO_DIRECTION; + Link::_REG_RCNT &= ~SO_DATA; } }; extern LinkPS2Mouse* linkPS2Mouse; -#endif // LINK_PS2_MOUSE_H \ No newline at end of file +#endif // LINK_PS2_MOUSE_H diff --git a/lib/LinkRawCable.hpp b/lib/LinkRawCable.hpp index 646cf7b..5172109 100644 --- a/lib/LinkRawCable.hpp +++ b/lib/LinkRawCable.hpp @@ -7,7 +7,7 @@ // Usage: // - 1) Include this header in your main.cpp file and add: // LinkRawCable* linkRawCable = new LinkRawCable(); -// - 2) (Optional) Add the interrupt service routines: +// - 2) (Optional) Add the interrupt service routines: (*) // irq_init(NULL); // irq_add(II_SERIAL, LINK_RAW_CABLE_ISR_SERIAL); // // (this is only required for `transferAsync`) @@ -17,7 +17,7 @@ // LinkRawCable::Response data = linkRawCable->transfer(0x1234); // // (this blocks the console indefinitely) // - 5) Exchange data with a cancellation callback: -// u16 data = linkRawCable->transfer(0x1234, []() { +// LinkRawCable::Response data = linkRawCable->transfer(0x1234, []() { // u16 keys = ~REG_KEYS & KEY_ANY; // return keys & KEY_START; // }); @@ -25,39 +25,49 @@ // linkRawCable->transferAsync(0x1234); // // ... // if (linkRawCable->getAsyncState() == LinkRawCable::AsyncState::READY) { -// u16 data = linkRawCable->getAsyncData(); +// LinkRawCable::Response data = linkRawCable->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: // - don't send 0xFFFF, it's a reserved value that means // - only transfer(...) if isReady() // -------------------------------------------------------------------------- -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + +static volatile char LINK_RAW_CABLE_VERSION[] = "LinkRawCable/v7.0.0"; #define LINK_RAW_CABLE_MAX_PLAYERS 4 #define LINK_RAW_CABLE_DISCONNECTED 0xffff -#define LINK_RAW_CABLE_BIT_SLAVE 2 -#define LINK_RAW_CABLE_BIT_READY 3 -#define LINK_RAW_CABLE_BITS_PLAYER_ID 4 -#define LINK_RAW_CABLE_BIT_ERROR 6 -#define LINK_RAW_CABLE_BIT_START 7 -#define LINK_RAW_CABLE_BIT_MULTIPLAYER 13 -#define LINK_RAW_CABLE_BIT_IRQ 14 -#define LINK_RAW_CABLE_BIT_GENERAL_PURPOSE_LOW 14 -#define LINK_RAW_CABLE_BIT_GENERAL_PURPOSE_HIGH 15 -#define LINK_RAW_CABLE_EMPTY_RESPONSE \ - { \ - { \ - LINK_RAW_CABLE_DISCONNECTED, LINK_RAW_CABLE_DISCONNECTED, \ - LINK_RAW_CABLE_DISCONNECTED, LINK_RAW_CABLE_DISCONNECTED \ - } \ - } - -static volatile char LINK_RAW_CABLE_VERSION[] = "LinkRawCable/v6.3.0"; +/** + * @brief A low level handler for the Link Port (Multi-Play Mode). + */ class LinkRawCable { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + static constexpr int BIT_SLAVE = 2; + static constexpr int BIT_READY = 3; + static constexpr int BITS_PLAYER_ID = 4; + static constexpr int BIT_ERROR = 6; + static constexpr int BIT_START = 7; + static constexpr int BIT_MULTIPLAYER = 13; + static constexpr int BIT_IRQ = 14; + static constexpr int BIT_GENERAL_PURPOSE_LOW = 14; + static constexpr int BIT_GENERAL_PURPOSE_HIGH = 15; + public: enum BaudRate { BAUD_RATE_0, // 9600 bps @@ -71,34 +81,66 @@ class LinkRawCable { }; enum AsyncState { IDLE, WAITING, READY }; - bool isActive() { return isEnabled; } + private: + static constexpr Response EMPTY_RESPONSE = { + {LINK_RAW_CABLE_DISCONNECTED, LINK_RAW_CABLE_DISCONNECTED, + LINK_RAW_CABLE_DISCONNECTED, LINK_RAW_CABLE_DISCONNECTED}, + -1}; + public: + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + + /** + * @brief Activates the library in a specific `baudRate`. + * @param baudRate One of the enum values from `LinkRawCable::BaudRate`. + * Defaults to `LinkRawCable::BaudRate::BAUD_RATE_1` (38400 bps). + */ void activate(BaudRate baudRate = BaudRate::BAUD_RATE_1) { this->baudRate = baudRate; this->asyncState = IDLE; - this->asyncData = LINK_RAW_CABLE_EMPTY_RESPONSE; + this->asyncData = EMPTY_RESPONSE; setMultiPlayMode(); isEnabled = true; } + /** + * @brief Deactivates the library. + */ void deactivate() { isEnabled = false; setGeneralPurposeMode(); baudRate = BaudRate::BAUD_RATE_1; asyncState = IDLE; - asyncData = LINK_RAW_CABLE_EMPTY_RESPONSE; + asyncData = EMPTY_RESPONSE; } + /** + * @brief Exchanges `data` with the connected consoles. Returns the received + * data from each player, including the assigned player ID. + * @param data The value to be sent. + * \warning Blocks the system until completion. + */ Response transfer(u16 data) { return transfer(data, []() { return false; }); } + /** + * @brief Exchanges `data` with the connected consoles. Returns the received + * data from each player, including the assigned player ID. + * @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 Response transfer(u16 data, F cancel, bool _async = false) { if (asyncState != IDLE) - return LINK_RAW_CABLE_EMPTY_RESPONSE; + return EMPTY_RESPONSE; setData(data); @@ -112,46 +154,77 @@ class LinkRawCable { startTransfer(); if (_async) - return LINK_RAW_CABLE_EMPTY_RESPONSE; + return EMPTY_RESPONSE; while (isSending()) if (cancel()) { stopTransfer(); - return LINK_RAW_CABLE_EMPTY_RESPONSE; + return EMPTY_RESPONSE; } if (isReady() && !hasError()) return getData(); - return LINK_RAW_CABLE_EMPTY_RESPONSE; + return EMPTY_RESPONSE; } + /** + * @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. + */ void transferAsync(u16 data) { - transfer( - data, []() { return false; }, true); + transfer(data, []() { return false; }, true); } - Response getAsyncData() { + /** + * @brief Returns the state of the last async transfer + * @return One of the enum values from `LinkRawCable::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]] Response getAsyncData() { if (asyncState != READY) - return LINK_RAW_CABLE_EMPTY_RESPONSE; + return EMPTY_RESPONSE; Response data = asyncData; asyncState = IDLE; return data; } - BaudRate getBaudRate() { return baudRate; } - bool isMaster() { return !isBitHigh(LINK_RAW_CABLE_BIT_SLAVE); } - bool isReady() { return isBitHigh(LINK_RAW_CABLE_BIT_READY); } - AsyncState getAsyncState() { return asyncState; } + /** + * @brief Returns the current `baudRate`. + */ + [[nodiscard]] BaudRate getBaudRate() { return baudRate; } + /** + * @brief Returns whether the console is connected as master or not. Returns + * garbage when the cable is not properly connected. + */ + [[nodiscard]] bool isMaster() { return !isBitHigh(BIT_SLAVE); } + + /** + * @brief Returns whether all connected consoles have entered the multiplayer + * mode. Returns garbage when the cable is not properly connected. + */ + [[nodiscard]] bool isReady() { return isBitHigh(BIT_READY); } + + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ void _onSerial() { if (!isEnabled || asyncState != WAITING) return; setInterruptsOff(); asyncState = READY; - asyncData = LINK_RAW_CABLE_EMPTY_RESPONSE; + asyncData = EMPTY_RESPONSE; if (isReady() && !hasError()) asyncData = getData(); } @@ -159,51 +232,53 @@ class LinkRawCable { private: BaudRate baudRate = BaudRate::BAUD_RATE_1; AsyncState asyncState = IDLE; - Response asyncData = LINK_RAW_CABLE_EMPTY_RESPONSE; + Response asyncData = EMPTY_RESPONSE; volatile bool isEnabled = false; void setMultiPlayMode() { - REG_RCNT = REG_RCNT & ~(1 << LINK_RAW_CABLE_BIT_GENERAL_PURPOSE_HIGH); - REG_SIOCNT = (1 << LINK_RAW_CABLE_BIT_MULTIPLAYER); - REG_SIOCNT |= baudRate; - REG_SIOMLT_SEND = 0; + Link::_REG_RCNT = Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_SIOCNT = (1 << BIT_MULTIPLAYER); + Link::_REG_SIOCNT |= baudRate; + Link::_REG_SIOMLT_SEND = 0; } void setGeneralPurposeMode() { - REG_RCNT = (REG_RCNT & ~(1 << LINK_RAW_CABLE_BIT_GENERAL_PURPOSE_LOW)) | - (1 << LINK_RAW_CABLE_BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) | + (1 << BIT_GENERAL_PURPOSE_HIGH); } - void setData(u16 data) { REG_SIOMLT_SEND = data; } + void setData(u16 data) { Link::_REG_SIOMLT_SEND = data; } Response getData() { - Response response = LINK_RAW_CABLE_EMPTY_RESPONSE; + Response response = EMPTY_RESPONSE; for (u32 i = 0; i < LINK_RAW_CABLE_MAX_PLAYERS; i++) - response.data[i] = REG_SIOMULTI[i]; + response.data[i] = Link::_REG_SIOMULTI[i]; response.playerId = - (REG_SIOCNT & (0b11 << LINK_RAW_CABLE_BITS_PLAYER_ID)) >> - LINK_RAW_CABLE_BITS_PLAYER_ID; + (Link::_REG_SIOCNT & (0b11 << BITS_PLAYER_ID)) >> BITS_PLAYER_ID; return response; } - bool hasError() { return isBitHigh(LINK_RAW_CABLE_BIT_ERROR); } - bool isSending() { return isBitHigh(LINK_RAW_CABLE_BIT_START); } + bool hasError() { return isBitHigh(BIT_ERROR); } + bool isSending() { return isBitHigh(BIT_START); } - void startTransfer() { setBitHigh(LINK_RAW_CABLE_BIT_START); } - void stopTransfer() { setBitLow(LINK_RAW_CABLE_BIT_START); } + void startTransfer() { setBitHigh(BIT_START); } + void stopTransfer() { setBitLow(BIT_START); } - void setInterruptsOn() { setBitHigh(LINK_RAW_CABLE_BIT_IRQ); } - void setInterruptsOff() { setBitLow(LINK_RAW_CABLE_BIT_IRQ); } + void setInterruptsOn() { setBitHigh(BIT_IRQ); } + void setInterruptsOff() { setBitLow(BIT_IRQ); } - bool isBitHigh(u8 bit) { return (REG_SIOCNT >> bit) & 1; } - void setBitHigh(u8 bit) { REG_SIOCNT |= 1 << bit; } - void setBitLow(u8 bit) { REG_SIOCNT &= ~(1 << bit); } + 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 LinkRawCable* linkRawCable; +/** + * @brief SERIAL interrupt handler. + */ inline void LINK_RAW_CABLE_ISR_SERIAL() { linkRawCable->_onSerial(); } diff --git a/lib/LinkRawWireless.hpp b/lib/LinkRawWireless.hpp index 40432cf..1464fc4 100644 --- a/lib/LinkRawWireless.hpp +++ b/lib/LinkRawWireless.hpp @@ -8,16 +8,33 @@ // - If you're building a game, use `LinkWireless`. // -------------------------------------------------------------------------- -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + #include #include #include "LinkGPIO.hpp" #include "LinkSPI.hpp" -// Enable logging (set `linkRawWireless->logger` and uncomment to enable) +/** + * @brief Enable logging. + * \warning Set `linkRawWireless->logger` and uncomment to enable! + */ // #define LINK_RAW_WIRELESS_ENABLE_LOGGING +static volatile char LINK_RAW_WIRELESS_VERSION[] = "LinkRawWireless/v7.0.0"; + +#define LINK_RAW_WIRELESS_MAX_PLAYERS 5 +#define LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30 +#define LINK_RAW_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4 +#define LINK_RAW_WIRELESS_MAX_GAME_ID 0x7fff +#define LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH 14 +#define LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH 8 +#define LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 23 + #ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING #include #define LRWLOG(str) logger(str) @@ -25,53 +42,51 @@ #define LRWLOG(str) #endif -#define LINK_RAW_WIRELESS_MAX_PLAYERS 5 -#define LINK_RAW_WIRELESS_PING_WAIT 50 -#define LINK_RAW_WIRELESS_TRANSFER_WAIT 15 -#define LINK_RAW_WIRELESS_CMD_TIMEOUT 100 -#define LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30 -#define LINK_RAW_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4 -#define LINK_RAW_WIRELESS_MAX_GAME_ID 0x7fff -#define LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH 14 -#define LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH 8 -#define LINK_RAW_WIRELESS_LOGIN_STEPS 9 -#define LINK_RAW_WIRELESS_COMMAND_HEADER 0x9966 -#define LINK_RAW_WIRELESS_RESPONSE_ACK 0x80 -#define LINK_RAW_WIRELESS_DATA_REQUEST 0x80000000 -#define LINK_RAW_WIRELESS_SETUP_MAGIC 0x003c0000 -#define LINK_RAW_WIRELESS_STILL_CONNECTING 0x01000000 -#define LINK_RAW_WIRELESS_BROADCAST_LENGTH 6 -#define LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH \ - (1 + LINK_RAW_WIRELESS_BROADCAST_LENGTH) -#define LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 23 -#define LINK_RAW_WIRELESS_MAX_SERVERS \ - (LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / \ - LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH) -#define LINK_RAW_WIRELESS_COMMAND_HELLO 0x10 -#define LINK_RAW_WIRELESS_COMMAND_SETUP 0x17 -#define LINK_RAW_WIRELESS_COMMAND_BROADCAST 0x16 -#define LINK_RAW_WIRELESS_COMMAND_START_HOST 0x19 -#define LINK_RAW_WIRELESS_COMMAND_SLOT_STATUS 0x14 -#define LINK_RAW_WIRELESS_COMMAND_ACCEPT_CONNECTIONS 0x1a -#define LINK_RAW_WIRELESS_COMMAND_END_HOST 0x1b -#define LINK_RAW_WIRELESS_COMMAND_BROADCAST_READ_START 0x1c -#define LINK_RAW_WIRELESS_COMMAND_BROADCAST_READ_POLL 0x1d -#define LINK_RAW_WIRELESS_COMMAND_BROADCAST_READ_END 0x1e -#define LINK_RAW_WIRELESS_COMMAND_CONNECT 0x1f -#define LINK_RAW_WIRELESS_COMMAND_IS_FINISHED_CONNECT 0x20 -#define LINK_RAW_WIRELESS_COMMAND_FINISH_CONNECTION 0x21 -#define LINK_RAW_WIRELESS_COMMAND_SEND_DATA 0x24 -#define LINK_RAW_WIRELESS_COMMAND_SEND_DATA_AND_WAIT 0x25 -#define LINK_RAW_WIRELESS_COMMAND_RECEIVE_DATA 0x26 -#define LINK_RAW_WIRELESS_COMMAND_WAIT 0x27 -#define LINK_RAW_WIRELESS_COMMAND_BYE 0x3d - -static volatile char LINK_RAW_WIRELESS_VERSION[] = "LinkRawWireless/v6.3.0"; - -const u16 LINK_RAW_WIRELESS_LOGIN_PARTS[] = { - 0x494e, 0x494e, 0x544e, 0x544e, 0x4e45, 0x4e45, 0x4f44, 0x4f44, 0x8001}; - +/** + * @brief A low level driver for the GBA Wireless Adapter. + * \warning Advanced usage only! + * \warning If you're building a game, use `LinkWireless`. + */ class LinkRawWireless { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + static constexpr int PING_WAIT = 50; + static constexpr int TRANSFER_WAIT = 15; + static constexpr int CMD_TIMEOUT = 10; + static constexpr int LOGIN_STEPS = 9; + static constexpr int COMMAND_HEADER = 0x9966; + static constexpr int RESPONSE_ACK = 0x80; + static constexpr u32 DATA_REQUEST = 0x80000000; + static constexpr int SETUP_MAGIC = 0x003c0000; + static constexpr int WAIT_STILL_CONNECTING = 0x01000000; + static constexpr int BROADCAST_LENGTH = 6; + static constexpr int BROADCAST_RESPONSE_LENGTH = 1 + BROADCAST_LENGTH; + static constexpr int MAX_SERVERS = + LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / BROADCAST_RESPONSE_LENGTH; + static constexpr int COMMAND_HELLO = 0x10; + static constexpr int COMMAND_SETUP = 0x17; + static constexpr int COMMAND_BROADCAST = 0x16; + static constexpr int COMMAND_START_HOST = 0x19; + static constexpr int COMMAND_SLOT_STATUS = 0x14; + static constexpr int COMMAND_ACCEPT_CONNECTIONS = 0x1a; + static constexpr int COMMAND_END_HOST = 0x1b; + static constexpr int COMMAND_BROADCAST_READ_START = 0x1c; + static constexpr int COMMAND_BROADCAST_READ_POLL = 0x1d; + static constexpr int COMMAND_BROADCAST_READ_END = 0x1e; + static constexpr int COMMAND_CONNECT = 0x1f; + static constexpr int COMMAND_IS_FINISHED_CONNECT = 0x20; + static constexpr int COMMAND_FINISH_CONNECTION = 0x21; + static constexpr int COMMAND_SEND_DATA = 0x24; + static constexpr int COMMAND_SEND_DATA_AND_WAIT = 0x25; + static constexpr int COMMAND_RECEIVE_DATA = 0x26; + static constexpr int COMMAND_WAIT = 0x27; + static constexpr int COMMAND_BYE = 0x3d; + static constexpr u16 LOGIN_PARTS[] = {0x494e, 0x494e, 0x544e, 0x544e, 0x4e45, + 0x4e45, 0x4f44, 0x4f44, 0x8001}; + public: #ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING typedef void (*Logger)(std::string); @@ -129,7 +144,7 @@ class LinkRawWireless { }; struct BroadcastReadPollResponse { - std::array servers; + std::array servers; u32 serversSize = 0; }; @@ -146,8 +161,15 @@ class LinkRawWireless { u32 dataSize = 0; }; - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + /** + * @brief Activates the library. + * Returns whether initialization was successful or not. + */ bool activate() { isEnabled = false; @@ -157,8 +179,11 @@ class LinkRawWireless { return success; } + /** + * @brief Deactivates the library. + */ bool deactivate() { - bool success = sendCommand(LINK_RAW_WIRELESS_COMMAND_BYE).success; + bool success = sendCommand(COMMAND_BYE).success; isEnabled = false; resetState(); @@ -167,17 +192,34 @@ class LinkRawWireless { return success; } + /** + * @brief Calls the Setup (`0x17`) command. + * @param maxPlayers `(2~5)` Maximum players in hosted rooms. Clients should + * set this to `0`. + * @param maxTransmissions Number of transmissions before marking a player as + * disconnected. `0` means infinite retransmissions. + * @param waitTimeout Timeout of the *waiting commands*, in frames (16.6ms). + * `0` means no timeout. + * @param magic A part of the protocol that hasn't been reverse-engineered + * yet. For now, it's magic (`0x003c0000`). + */ bool setup(u8 maxPlayers = LINK_RAW_WIRELESS_MAX_PLAYERS, u8 maxTransmissions = 4, u8 waitTimeout = 32, - u32 magic = LINK_RAW_WIRELESS_SETUP_MAGIC) { + u32 magic = SETUP_MAGIC) { u32 config = (u32)(magic | (((LINK_RAW_WIRELESS_MAX_PLAYERS - maxPlayers) & 0b11) << 16) | (maxTransmissions << 8) | waitTimeout); - return sendCommand(LINK_RAW_WIRELESS_COMMAND_SETUP, {config}, 1).success; + return sendCommand(COMMAND_SETUP, {config}, 1).success; } + /** + * @brief Calls the Broadcast (`0x16`) command. + * @param gameName Game name. Maximum `14` characters + null terminator. + * @param userName User name. Maximum `8` characters + null terminator. + * @param gameId `(0 ~ 0x7FFF)` Game ID. + */ bool broadcast(const char* gameName = "", const char* userName = "", u16 gameId = LINK_RAW_WIRELESS_MAX_GAME_ID) { @@ -197,7 +239,7 @@ class LinkRawWireless { bool success = sendCommand( - LINK_RAW_WIRELESS_COMMAND_BROADCAST, + COMMAND_BROADCAST, {buildU32(buildU16(finalGameName[1], finalGameName[0]), gameId), buildU32(buildU16(finalGameName[5], finalGameName[4]), buildU16(finalGameName[3], finalGameName[2])), @@ -209,7 +251,7 @@ class LinkRawWireless { buildU16(finalUserName[1], finalUserName[0])), buildU32(buildU16(finalUserName[7], finalUserName[6]), buildU16(finalUserName[5], finalUserName[4]))}, - LINK_RAW_WIRELESS_BROADCAST_LENGTH) + BROADCAST_LENGTH) .success; if (!success) { @@ -220,23 +262,30 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the StartHost (`0x19`) command. + */ bool startHost() { - bool success = sendCommand(LINK_RAW_WIRELESS_COMMAND_START_HOST).success; + bool success = sendCommand(COMMAND_START_HOST).success; if (!success) { reset(); return false; } - wait(LINK_RAW_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); LRWLOG("state = SERVING"); state = SERVING; return true; } + /** + * @brief Calls the SlotStatus (`0x14`) command. + * @param response A structure that will be filled with the response data. + */ bool getSlotStatus(SlotStatusResponse& response) { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_SLOT_STATUS); + auto result = sendCommand(COMMAND_SLOT_STATUS); if (!result.success) { reset(); @@ -257,8 +306,12 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the AcceptConnections (`0x1a`) command. + * @param response A structure that will be filled with the response data. + */ bool acceptConnections(AcceptConnectionsResponse& response) { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_ACCEPT_CONNECTIONS); + auto result = sendCommand(COMMAND_ACCEPT_CONNECTIONS); if (!result.success) { reset(); @@ -280,8 +333,12 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the EndHost (`0x1b`) command. + * @param response A structure that will be filled with the response data. + */ bool endHost(AcceptConnectionsResponse& response) { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_END_HOST); + auto result = sendCommand(COMMAND_END_HOST); if (!result.success) { reset(); @@ -303,9 +360,11 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the BroadcastRead1 (`0x1c`) command. + */ bool broadcastReadStart() { - bool success = - sendCommand(LINK_RAW_WIRELESS_COMMAND_BROADCAST_READ_START).success; + bool success = sendCommand(COMMAND_BROADCAST_READ_START).success; if (!success) { reset(); @@ -318,23 +377,24 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the BroadcastRead2 (`0x1d`) command. + */ bool broadcastReadPoll(BroadcastReadPollResponse& response) { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_BROADCAST_READ_POLL); + auto result = sendCommand(COMMAND_BROADCAST_READ_POLL); bool success = - result.success && - result.responsesSize % LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH == 0; + result.success && result.responsesSize % BROADCAST_RESPONSE_LENGTH == 0; if (!success) { reset(); return false; } - u32 totalBroadcasts = - result.responsesSize / LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH; + u32 totalBroadcasts = result.responsesSize / BROADCAST_RESPONSE_LENGTH; response.serversSize = 0; for (u32 i = 0; i < totalBroadcasts; i++) { - u32 start = LINK_RAW_WIRELESS_BROADCAST_RESPONSE_LENGTH * i; + u32 start = BROADCAST_RESPONSE_LENGTH * i; Server server; server.id = (u16)result.responses[start]; @@ -357,9 +417,11 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the BroadcastRead3 (`0x1e`) command. + */ bool broadcastReadEnd() { - bool success = - sendCommand(LINK_RAW_WIRELESS_COMMAND_BROADCAST_READ_END).success; + bool success = sendCommand(COMMAND_BROADCAST_READ_END).success; if (!success) { reset(); @@ -372,9 +434,12 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the Connect (`0x1f`) command. + * @param serverId Device ID of the server. + */ bool connect(u16 serverId) { - bool success = - sendCommand(LINK_RAW_WIRELESS_COMMAND_CONNECT, {serverId}, 1).success; + bool success = sendCommand(COMMAND_CONNECT, {serverId}, 1).success; if (!success) { reset(); @@ -387,8 +452,12 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the IsFinishedConnect (`0x20`) command. + * @param response A structure that will be filled with the response data. + */ bool keepConnecting(ConnectionStatus& response) { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_IS_FINISHED_CONNECT); + auto result = sendCommand(COMMAND_IS_FINISHED_CONNECT); if (!result.success || result.responsesSize == 0) { if (result.responsesSize == 0) LRWLOG("! empty response"); @@ -396,7 +465,7 @@ class LinkRawWireless { return false; } - if (result.responses[0] == LINK_RAW_WIRELESS_STILL_CONNECTING) { + if (result.responses[0] == WAIT_STILL_CONNECTING) { response.phase = STILL_CONNECTING; return true; } @@ -415,8 +484,11 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the FinishConnection (`0x21`) command. + */ bool finishConnection() { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_FINISH_CONNECTION); + auto result = sendCommand(COMMAND_FINISH_CONNECTION); if (!result.success || result.responsesSize == 0) { if (result.responsesSize == 0) LRWLOG("! empty response"); @@ -439,6 +511,13 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the SendData (`0x24`) command. + * @param data The values to be sent. + * @param dataSize The number of 32-bit values in the `data` array. + * @param _bytes The number of BYTES to send. If `0`, the method will use + * `dataSize * 4` instead. + */ bool sendData( std::array data, u32 dataSize, @@ -453,9 +532,7 @@ class LinkRawWireless { dataSize++; LRWLOG("using header " + toHex(header)); - bool success = - sendCommand(LINK_RAW_WIRELESS_COMMAND_SEND_DATA, data, dataSize) - .success; + bool success = sendCommand(COMMAND_SEND_DATA, data, dataSize).success; if (!success) { reset(); @@ -465,6 +542,15 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the SendDataAndWait (`0x25`) command. + * @param data The values to be sent. + * @param dataSize The number of 32-bit values in the `data` array. + * @param remoteCommand A structure that will be filled with the remote + * command from the adapter. + * @param _bytes The number of BYTES to send. If `0`, the method will use + * `dataSize * 4` instead. + */ bool sendDataAndWait( std::array data, u32 dataSize, @@ -480,8 +566,7 @@ class LinkRawWireless { dataSize++; LRWLOG("using header " + toHex(header)); - if (!sendCommand(LINK_RAW_WIRELESS_COMMAND_SEND_DATA_AND_WAIT, data, - dataSize, true) + if (!sendCommand(COMMAND_SEND_DATA_AND_WAIT, data, dataSize, true) .success) { reset(); return false; @@ -492,8 +577,12 @@ class LinkRawWireless { return remoteCommand.success; } + /** + * @brief Calls the ReceiveData (`0x26`) command. + * @param response A structure that will be filled with the response data. + */ bool receiveData(ReceiveDataResponse& response) { - auto result = sendCommand(LINK_RAW_WIRELESS_COMMAND_RECEIVE_DATA); + auto result = sendCommand(COMMAND_RECEIVE_DATA); for (u32 i = 0; i < result.responsesSize; i++) response.data[i] = result.responses[i]; response.dataSize = result.responsesSize; @@ -521,8 +610,13 @@ class LinkRawWireless { return true; } + /** + * @brief Calls the Wait (`0x27`) command. + * @param remoteCommand A structure that will be filled with the remote + * command from the adapter. + */ bool wait(RemoteCommand& remoteCommand) { - if (!sendCommand(LINK_RAW_WIRELESS_COMMAND_WAIT, {}, 0, true).success) { + if (!sendCommand(COMMAND_WAIT, {}, 0, true).success) { reset(); return false; } @@ -532,6 +626,13 @@ class LinkRawWireless { return remoteCommand.success; } + /** + * @brief Calls an arbitrary command and returns the response. + * @param type The ID of the command. + * @param params The command parameters. + * @param length The number of 32-bit values in the `params` array. + * @param invertsClock Whether this command inverts the clock or not (Wait). + */ CommandResult sendCommand( u8 type, std::array params = @@ -543,8 +644,8 @@ class LinkRawWireless { u32 r; LRWLOG("sending command 0x" + toHex(command)); - if ((r = transfer(command)) != LINK_RAW_WIRELESS_DATA_REQUEST) { - logExpectedButReceived(LINK_RAW_WIRELESS_DATA_REQUEST, r); + if ((r = transfer(command)) != DATA_REQUEST) { + logExpectedButReceived(DATA_REQUEST, r); return result; } @@ -553,37 +654,34 @@ class LinkRawWireless { u32 param = params[i]; LRWLOG("sending param" + std::to_string(parameterCount) + ": 0x" + toHex(param)); - if ((r = transfer(param)) != LINK_RAW_WIRELESS_DATA_REQUEST) { - logExpectedButReceived(LINK_RAW_WIRELESS_DATA_REQUEST, r); + if ((r = transfer(param)) != DATA_REQUEST) { + logExpectedButReceived(DATA_REQUEST, r); return result; } parameterCount++; } LRWLOG("sending response request"); - u32 response = - invertsClock - ? transferAndStartClockInversionACK(LINK_RAW_WIRELESS_DATA_REQUEST) - : transfer(LINK_RAW_WIRELESS_DATA_REQUEST); + u32 response = invertsClock + ? transferAndStartClockInversionACK(DATA_REQUEST) + : transfer(DATA_REQUEST); u16 header = msB32(response); u16 data = lsB32(response); u8 responses = msB16(data); u8 ack = lsB16(data); - if (header != LINK_RAW_WIRELESS_COMMAND_HEADER) { + if (header != COMMAND_HEADER) { LRWLOG("! expected HEADER 0x9966"); LRWLOG("! but received 0x" + toHex(header)); return result; } - if (ack != type + LINK_RAW_WIRELESS_RESPONSE_ACK) { + if (ack != type + RESPONSE_ACK) { if (ack == 0xee && responses == 1 && !invertsClock) { - u8 __attribute__((unused)) code = - (u8)transfer(LINK_RAW_WIRELESS_DATA_REQUEST); + u8 __attribute__((unused)) code = (u8)transfer(DATA_REQUEST); LRWLOG("! error received"); LRWLOG(code == 1 ? "! invalid state" : "! unknown cmd"); } else { - LRWLOG("! expected ACK 0x" + - toHex(type + LINK_RAW_WIRELESS_RESPONSE_ACK)); + LRWLOG("! expected ACK 0x" + toHex(type + RESPONSE_ACK)); LRWLOG("! but received 0x" + toHex(ack)); } return result; @@ -594,7 +692,7 @@ class LinkRawWireless { for (u32 i = 0; i < responses; i++) { LRWLOG("response " + std::to_string(i + 1) + "/" + std::to_string(responses) + ":"); - u32 responseData = transfer(LINK_RAW_WIRELESS_DATA_REQUEST); + u32 responseData = transfer(DATA_REQUEST); result.responses[result.responsesSize++] = responseData; LRWLOG("<< " + toHex(responseData)); } @@ -604,6 +702,10 @@ class LinkRawWireless { return result; } + /** + * @brief Inverts the clock and waits until the adapter sends a command. + * Returns the remote command. + */ RemoteCommand receiveCommandFromAdapter() { RemoteCommand remoteCommand; @@ -611,8 +713,8 @@ class LinkRawWireless { linkSPI->activate(LinkSPI::Mode::SLAVE); LRWLOG("WAITING for adapter cmd"); - u32 command = linkSPI->transfer( - LINK_RAW_WIRELESS_DATA_REQUEST, []() { return false; }, false, true); + u32 command = + linkSPI->transfer(DATA_REQUEST, []() { return false; }, false, true); if (!reverseAcknowledge()) { linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); reset(); @@ -623,7 +725,7 @@ class LinkRawWireless { u16 data = lsB32(command); u8 params = msB16(data); u8 commandId = lsB16(data); - if (header != LINK_RAW_WIRELESS_COMMAND_HEADER) { + if (header != COMMAND_HEADER) { LRWLOG("! expected HEADER 0x9966"); LRWLOG("! but received 0x" + toHex(header)); linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); @@ -636,8 +738,8 @@ class LinkRawWireless { for (u32 i = 0; i < params; i++) { LRWLOG("param " + std::to_string(i + 1) + "/" + std::to_string(params) + ":"); - u32 paramData = linkSPI->transfer( - LINK_RAW_WIRELESS_DATA_REQUEST, []() { return false; }, false, true); + u32 paramData = + linkSPI->transfer(DATA_REQUEST, []() { return false; }, false, true); if (!reverseAcknowledge()) { linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); reset(); @@ -647,17 +749,19 @@ class LinkRawWireless { LRWLOG("<< " + toHex(paramData)); } + wait(TRANSFER_WAIT); + LRWLOG("sending ack"); command = linkSPI->transfer( - 0x99660000 | ((commandId + 0x80) & 0xff), []() { return false; }, false, - true); + (COMMAND_HEADER << 16) | ((commandId + RESPONSE_ACK) & 0xff), + []() { return false; }, false, true); if (!reverseAcknowledge(true)) { linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); reset(); return remoteCommand; } - if (command != LINK_RAW_WIRELESS_DATA_REQUEST) { + if (command != DATA_REQUEST) { LRWLOG("! expected CMD request"); LRWLOG("! but received 0x" + toHex(command)); linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); @@ -665,10 +769,10 @@ class LinkRawWireless { return remoteCommand; } - LRWLOG("setting SPI to 2Mbps"); + LRWLOG("setting SPI to MASTER"); linkSPI->activate(LinkSPI::Mode::MASTER_2MBPS); - wait(LINK_RAW_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); remoteCommand.success = true; remoteCommand.commandId = commandId; @@ -676,16 +780,41 @@ class LinkRawWireless { return remoteCommand; } - u32 getDeviceTransferLength() { + /** + * @brief Returns the maximum number of transferrable 32-bit values. + * It's 23 for servers and 4 for clients. + */ + [[nodiscard]] u32 getDeviceTransferLength() { return state == SERVING ? LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH : LINK_RAW_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH; } - State getState() { return state; } - bool isConnected() { return sessionState.playerCount > 1; } - bool isSessionActive() { return state == SERVING || state == CONNECTED; } - u8 playerCount() { return sessionState.playerCount; } - u8 currentPlayerId() { return sessionState.currentPlayerId; } + /** + * @brief Returns the current state. + */ + [[nodiscard]] State getState() { return state; } + + /** + * @brief Returns `true` if the player count is higher than `1`. + */ + [[nodiscard]] bool isConnected() { return sessionState.playerCount > 1; } + + /** + * @brief Returns `true` if the state is `SERVING` or `CONNECTED`. + */ + [[nodiscard]] bool isSessionActive() { + return state == SERVING || state == CONNECTED; + } + + /** + * @brief Returns the number of connected players. + */ + [[nodiscard]] u8 playerCount() { return sessionState.playerCount; } + + /** + * @brief Returns the current player ID. + */ + [[nodiscard]] u8 currentPlayerId() { return sessionState.currentPlayerId; } ~LinkRawWireless() { delete linkSPI; @@ -708,6 +837,13 @@ class LinkRawWireless { State state = NEEDS_RESET; volatile bool isEnabled = false; + /** + * @brief Copies a null-terminated `source` string to a `target` destination + * (up to `length` characters). + * @param target Target string. + * @param source Source string. + * @param length Number of characters. + */ void copyName(char* target, const char* source, u32 length) { u32 len = std::strlen(source); @@ -718,6 +854,14 @@ class LinkRawWireless { target[i] = '\0'; } + /** + * @brief Recovers parts of the `name` of a Wireless Adapter room. + * @param name Target string. + * @param nameCursor Current position within `name`. + * @param word Current value. + * @param includeFirstTwoBytes Whether the first two bytes from `word` should + * be used. + */ void recoverName(char* name, u32& nameCursor, u32 word, @@ -739,6 +883,10 @@ class LinkRawWireless { name[nameCursor++] = character; } + /** + * @brief Resets the adapter + * @param initialize Whether it's an initialization (first time) or not. + */ bool reset(bool initialize = false) { resetState(); if (initialize) @@ -746,6 +894,9 @@ class LinkRawWireless { return initialize && start(); } + /** + * @brief Resets all the state. + */ void resetState() { LRWLOG("state = NEEDS_RESET"); this->state = NEEDS_RESET; @@ -753,8 +904,14 @@ class LinkRawWireless { this->sessionState.currentPlayerId = 0; } + /** + * @brief Stops the communication. + */ void stop() { linkSPI->deactivate(); } + /** + * @brief Starts the communication. + */ bool start() { pingAdapter(); LRWLOG("setting SPI to 256Kbps"); @@ -763,10 +920,10 @@ class LinkRawWireless { if (!login()) return false; - wait(LINK_RAW_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); LRWLOG("sending HELLO command"); - if (!sendCommand(LINK_RAW_WIRELESS_COMMAND_HELLO).success) + if (!sendCommand(COMMAND_HELLO).success) return false; LRWLOG("setting SPI to 2Mbps"); @@ -777,6 +934,9 @@ class LinkRawWireless { return true; } + /** + * @brief Sends the signal to reset the adapter. + */ void pingAdapter() { LRWLOG("setting SO as OUTPUT"); linkGPIO->setMode(LinkGPIO::Pin::SO, LinkGPIO::Direction::OUTPUT); @@ -784,29 +944,37 @@ class LinkRawWireless { linkGPIO->setMode(LinkGPIO::Pin::SD, LinkGPIO::Direction::OUTPUT); LRWLOG("setting SD = HIGH"); linkGPIO->writePin(LinkGPIO::SD, true); - wait(LINK_RAW_WIRELESS_PING_WAIT); + wait(PING_WAIT); LRWLOG("setting SD = LOW"); linkGPIO->writePin(LinkGPIO::SD, false); } + /** + * @brief Sends the login sequence to the adapter. + */ bool login() { LoginMemory memory; LRWLOG("sending initial login packet"); - if (!exchangeLoginPacket(LINK_RAW_WIRELESS_LOGIN_PARTS[0], 0, memory)) + if (!exchangeLoginPacket(LOGIN_PARTS[0], 0, memory)) return false; - for (u32 i = 0; i < LINK_RAW_WIRELESS_LOGIN_STEPS; i++) { + for (u32 i = 0; i < LOGIN_STEPS; i++) { LRWLOG("sending login packet " + std::to_string(i + 1) + "/" + - std::to_string(LINK_RAW_WIRELESS_LOGIN_STEPS)); - if (!exchangeLoginPacket(LINK_RAW_WIRELESS_LOGIN_PARTS[i], - LINK_RAW_WIRELESS_LOGIN_PARTS[i], memory)) + std::to_string(LOGIN_STEPS)); + if (!exchangeLoginPacket(LOGIN_PARTS[i], LOGIN_PARTS[i], memory)) return false; } return true; } + /** + * @brief Exchanges part of the login sequence with the adapter. + * @param data The value to be sent. + * @param expectedResponse The expected response. + * @param memory A structure that holds memory of the previous values. + */ bool exchangeLoginPacket(u16 data, u16 expectedResponse, LoginMemory& memory) { @@ -826,42 +994,61 @@ class LinkRawWireless { return true; } + /** + * @brief Builds a 32-bit value representing the command. + * @param type The ID of the command. + * @param length The number of 32-bit values that will be sent. + */ u32 buildCommand(u8 type, u8 length = 0) { - return buildU32(LINK_RAW_WIRELESS_COMMAND_HEADER, buildU16(length, type)); + return buildU32(COMMAND_HEADER, buildU16(length, type)); } + /** + * @brief Transfers `data` via SPI and performs the adapter's ACK procedure. + * Returns the received value. + * @param data The value to be sent. + * @param customAck Whether the adapter's ACK procedure should be used or not. + */ u32 transfer(u32 data, bool customAck = true) { if (!customAck) - wait(LINK_RAW_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; u32 receivedData = linkSPI->transfer( data, [this, &lines, &vCount]() { return cmdTimeout(lines, vCount); }, false, customAck); if (customAck && !acknowledge()) - return LINK_SPI_NO_DATA; + return LINK_SPI_NO_DATA_32; return receivedData; } + /** + * @brief Transfers `data` via SPI and performs the inverted adapter's ACK + * procedure. Returns the received value. + * @param data The value to be sent. + */ u32 transferAndStartClockInversionACK(u32 data) { u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; u32 receivedData = linkSPI->transfer( data, [this, &lines, &vCount]() { return cmdTimeout(lines, vCount); }, false, true); if (!reverseAcknowledgeStart()) - return LINK_SPI_NO_DATA; + return LINK_SPI_NO_DATA_32; return receivedData; } + /** + * @brief Performs the adapter's ACK procedure. + */ bool acknowledge() { u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; linkSPI->_setSOLow(); while (!linkSPI->_isSIHigh()) { @@ -884,9 +1071,12 @@ class LinkRawWireless { return true; } + /** + * @brief Starts performing the inverted adapter's ACK procedure. + */ bool reverseAcknowledgeStart() { u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; linkSPI->_setSOLow(); wait(1); @@ -903,9 +1093,15 @@ class LinkRawWireless { return true; } + /** + * @brief Performs the inverted adapter's ACK procedure. + * @param isLastPart Whether it's the last part of the procedure or not. + * \warning `isLastPart` is required when there's no subsequent + * `linkSPI->transfer(...)` call. + */ bool reverseAcknowledge(bool isLastPart = false) { u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; linkSPI->_setSOLow(); while (linkSPI->_isSIHigh()) { @@ -939,39 +1135,62 @@ class LinkRawWireless { return true; } + /** + * @brief Evaluates a timeout defined by `CMD_TIMEOUT`. + * @param lines A line counter that will be updated. + * @param vCount Starting `VCOUNT`. + */ bool cmdTimeout(u32& lines, u32& vCount) { - return timeout(LINK_RAW_WIRELESS_CMD_TIMEOUT, lines, vCount); + return timeout(CMD_TIMEOUT, lines, vCount); } + /** + * @brief Evaluates a timeout defined by `limit`. + * @param limit Maximum number of lines to wait. + * @param lines A line counter that will be updated. + * @param vCount Starting `VCOUNT`. + */ bool timeout(u32 limit, u32& lines, u32& vCount) { - if (REG_VCOUNT != vCount) { - lines += max((int)REG_VCOUNT - (int)vCount, 0); - vCount = REG_VCOUNT; + if (Link::_REG_VCOUNT != vCount) { + lines += Link::_max((int)Link::_REG_VCOUNT - (int)vCount, 0); + vCount = Link::_REG_VCOUNT; } return lines > limit; } + /** + * @brief Waits a number of `verticalLines`. + * @param verticalLines Number of lines to wait. + */ void wait(u32 verticalLines) { u32 count = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; while (count < verticalLines) { - if (REG_VCOUNT != vCount) { + if (Link::_REG_VCOUNT != vCount) { count++; - vCount = REG_VCOUNT; + vCount = Link::_REG_VCOUNT; } }; } + /** + * @brief Logs an error message (expected vs received). + * @param expected The expected number. + * @param received The received number. + */ void logExpectedButReceived(u32 expected, u32 received) { LRWLOG("! expected 0x" + toHex(expected)); LRWLOG("! but received 0x" + toHex(received)); } #ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING + /** + * @brief Converts `w` to an hexadecimal string. + */ template - std::string toHex(I w, size_t hex_len = sizeof(I) << 1) { + [[nodiscard]] std::string toHex(I w, size_t hex_len = sizeof(I) << 1) { static const char* digits = "0123456789ABCDEF"; std::string rc(hex_len, '0'); for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4) @@ -980,12 +1199,35 @@ class LinkRawWireless { } #endif - u32 buildU32(u16 msB, u16 lsB) { return (msB << 16) | lsB; } - u16 buildU16(u8 msB, u8 lsB) { return (msB << 8) | lsB; } - u16 msB32(u32 value) { return value >> 16; } - u16 lsB32(u32 value) { return value & 0xffff; } - u8 msB16(u16 value) { return value >> 8; } - u8 lsB16(u16 value) { return value & 0xff; } + /** + * @brief Builds a u32 numbers from `msB` and `lsB` + */ + [[nodiscard]] u32 buildU32(u16 msB, u16 lsB) { return (msB << 16) | lsB; } + + /** + * @brief Builds a u16 numbers from `msB` and `lsB` + */ + [[nodiscard]] u16 buildU16(u8 msB, u8 lsB) { return (msB << 8) | lsB; } + + /** + * @brief Returns the higher 16 bits of `value`. + */ + [[nodiscard]] u16 msB32(u32 value) { return value >> 16; } + + /** + * @brief Returns the lower 16 bits of `value`. + */ + [[nodiscard]] u16 lsB32(u32 value) { return value & 0xffff; } + + /** + * @brief Returns the higher 8 bits of `value`. + */ + [[nodiscard]] u8 msB16(u16 value) { return value >> 8; } + + /** + * @brief Returns the lower 8 bits of `value`. + */ + [[nodiscard]] u8 lsB16(u16 value) { return value & 0xff; } }; extern LinkRawWireless* linkRawWireless; diff --git a/lib/LinkSPI.hpp b/lib/LinkSPI.hpp index 05fb5f9..c66a883 100644 --- a/lib/LinkSPI.hpp +++ b/lib/LinkSPI.hpp @@ -2,7 +2,7 @@ #define LINK_SPI_H // -------------------------------------------------------------------------- -// An SPI handler for the Link Port (Normal Mode, 32bits). +// 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: @@ -37,64 +37,63 @@ // considerations: // - when using Normal Mode between two GBAs, use a GBC Link Cable! // - only use the 2Mbps mode with custom hardware (very short wires)! -// - don't send 0xFFFFFFFF, it's reserved for errors! +// - returns 0xFFFFFFFF (or 0xFF) on misuse or cancelled transfers! // -------------------------------------------------------------------------- -#include - -// 8-bit mode (uncomment to enable) -// #define LINK_SPI_8BIT_MODE - -#ifdef LINK_SPI_8BIT_MODE -#define LINK_SPI_DATA_TYPE u8 -#endif -#ifndef LINK_SPI_8BIT_MODE -#define LINK_SPI_DATA_TYPE u32 +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header #endif -#ifdef LINK_SPI_8BIT_MODE -#define LINK_SPI_DATA_REG REG_SIODATA8 -#endif -#ifndef LINK_SPI_8BIT_MODE -#define LINK_SPI_DATA_REG REG_SIODATA32 -#endif +#include "_link_common.hpp" -#ifdef LINK_SPI_8BIT_MODE -#define LINK_SPI_NO_DATA 0xff -#endif -#ifndef LINK_SPI_8BIT_MODE -#define LINK_SPI_NO_DATA 0xffffffff -#endif +static volatile char LINK_SPI_VERSION[] = "LinkSPI/v7.0.0"; -#define LINK_SPI_BIT_CLOCK 0 -#define LINK_SPI_BIT_CLOCK_SPEED 1 -#define LINK_SPI_BIT_SI 2 -#define LINK_SPI_BIT_SO 3 -#define LINK_SPI_BIT_START 7 -#define LINK_SPI_BIT_LENGTH 12 -#define LINK_SPI_BIT_IRQ 14 -#define LINK_SPI_BIT_GENERAL_PURPOSE_LOW 14 -#define LINK_SPI_BIT_GENERAL_PURPOSE_HIGH 15 - -static volatile char LINK_SPI_VERSION[] = "LinkSPI/v6.3.0"; - -const u32 LINK_SPI_MASK_CLEAR_SO_BIT = ~(1 << LINK_SPI_BIT_SO); -const u32 LINK_SPI_MASK_SET_START_BIT = (1 << LINK_SPI_BIT_START); +#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 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + 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 Mode { SLAVE, MASTER_256KBPS, MASTER_2MBPS }; + enum DataSize { SIZE_32BIT, SIZE_8BIT }; enum AsyncState { IDLE, WAITING, READY }; - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } - void activate(Mode mode) { + /** + * @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 = SIZE_32BIT) { this->mode = mode; + this->dataSize = dataSize; this->waitMode = false; this->asyncState = IDLE; this->asyncData = 0; - setNormalMode32Bit(); + setNormalMode(); disableTransfer(); if (mode == SLAVE) @@ -111,6 +110,9 @@ class LinkSPI { isEnabled = true; } + /** + * @brief Deactivates the library. + */ void deactivate() { isEnabled = false; setGeneralPurposeMode(); @@ -121,17 +123,29 @@ class LinkSPI { asyncData = 0; } - LINK_SPI_DATA_TYPE transfer(LINK_SPI_DATA_TYPE data) { + /** + * @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 - LINK_SPI_DATA_TYPE transfer(LINK_SPI_DATA_TYPE data, - F cancel, - bool _async = false, - bool _customAck = false) { + u32 transfer(u32 data, + F cancel, + bool _async = false, + bool _customAck = false) { if (asyncState != IDLE) - return LINK_SPI_NO_DATA; + return noData(); setData(data); @@ -147,20 +161,20 @@ class LinkSPI { disableTransfer(); setInterruptsOff(); asyncState = IDLE; - return LINK_SPI_NO_DATA; + return noData(); } enableTransfer(); startTransfer(); if (_async) - return LINK_SPI_NO_DATA; + return noData(); while (!isReady()) if (cancel()) { stopTransfer(); disableTransfer(); - return LINK_SPI_NO_DATA; + return noData(); } if (!_customAck) @@ -169,30 +183,84 @@ class LinkSPI { return getData(); } - void transferAsync(LINK_SPI_DATA_TYPE 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. + * \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 - void transferAsync(LINK_SPI_DATA_TYPE data, F cancel) { + void transferAsync(u32 data, F cancel) { transfer(data, cancel, true); } - LINK_SPI_DATA_TYPE getAsyncData() { - if (asyncState != READY) - return LINK_SPI_NO_DATA; + /** + * @brief Returns the state of the last async transfer. + * @return One of the enum values from `LinkSPI::AsyncState`. + */ + [[nodiscard]] AsyncState getAsyncState() { return asyncState; } - LINK_SPI_DATA_TYPE data = asyncData; + /** + * @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 != READY) + return noData(); + + u32 data = asyncData; asyncState = IDLE; return data; } - Mode getMode() { return mode; } - void setWaitModeActive(bool isActive) { waitMode = isActive; } - bool isWaitModeActive() { return waitMode; } - AsyncState getAsyncState() { return asyncState; } + /** + * @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) { 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 != WAITING) return; @@ -205,57 +273,87 @@ class LinkSPI { asyncData = getData(); } - void _setSOHigh() { setBitHigh(LINK_SPI_BIT_SO); } - void _setSOLow() { setBitLow(LINK_SPI_BIT_SO); } - bool _isSIHigh() { return isBitHigh(LINK_SPI_BIT_SI); } + /** + * @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; AsyncState asyncState = IDLE; - LINK_SPI_DATA_TYPE asyncData = 0; + u32 asyncData = 0; volatile bool isEnabled = false; - void setNormalMode32Bit() { - REG_RCNT = REG_RCNT & ~(1 << LINK_SPI_BIT_GENERAL_PURPOSE_HIGH); -#ifdef LINK_SPI_8BIT_MODE - REG_SIOCNT = 0; -#endif -#ifndef LINK_SPI_8BIT_MODE - REG_SIOCNT = 1 << LINK_SPI_BIT_LENGTH; -#endif + void setNormalMode() { + Link::_REG_RCNT = Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_HIGH); + + if (dataSize == SIZE_32BIT) + Link::_REG_SIOCNT = 1 << BIT_LENGTH; + else + Link::_REG_SIOCNT = 0; } void setGeneralPurposeMode() { - REG_RCNT = (REG_RCNT & ~(1 << LINK_SPI_BIT_GENERAL_PURPOSE_LOW)) | - (1 << LINK_SPI_BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) | + (1 << BIT_GENERAL_PURPOSE_HIGH); } - void setData(LINK_SPI_DATA_TYPE data) { LINK_SPI_DATA_REG = data; } - LINK_SPI_DATA_TYPE getData() { return LINK_SPI_DATA_REG; } + void setData(u32 data) { + if (dataSize == SIZE_32BIT) + Link::_REG_SIODATA32 = data; + else + Link::_REG_SIODATA8 = data & 0xff; + } + + u32 getData() { + return dataSize == SIZE_32BIT ? Link::_REG_SIODATA32 + : Link::_REG_SIODATA8 & 0xff; + } + + u32 noData() { + return dataSize == SIZE_32BIT ? LINK_SPI_NO_DATA_32 : LINK_SPI_NO_DATA_8; + } void enableTransfer() { _setSOLow(); } void disableTransfer() { _setSOHigh(); } - void startTransfer() { setBitHigh(LINK_SPI_BIT_START); } - void stopTransfer() { setBitLow(LINK_SPI_BIT_START); } - bool isReady() { return !isBitHigh(LINK_SPI_BIT_START); } + void startTransfer() { setBitHigh(BIT_START); } + void stopTransfer() { setBitLow(BIT_START); } + bool isReady() { return !isBitHigh(BIT_START); } bool isSlaveReady() { return !_isSIHigh(); } - void setMasterMode() { setBitHigh(LINK_SPI_BIT_CLOCK); } - void setSlaveMode() { setBitLow(LINK_SPI_BIT_CLOCK); } - void set256KbpsSpeed() { setBitLow(LINK_SPI_BIT_CLOCK_SPEED); } - void set2MbpsSpeed() { setBitHigh(LINK_SPI_BIT_CLOCK_SPEED); } - void setInterruptsOn() { setBitHigh(LINK_SPI_BIT_IRQ); } - void setInterruptsOff() { setBitLow(LINK_SPI_BIT_IRQ); } + 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 != SLAVE; } - bool isBitHigh(u8 bit) { return (REG_SIOCNT >> bit) & 1; } - void setBitHigh(u8 bit) { REG_SIOCNT |= 1 << bit; } - void setBitLow(u8 bit) { REG_SIOCNT &= ~(1 << bit); } + 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(); } diff --git a/lib/LinkUART.hpp b/lib/LinkUART.hpp index 9423195..829f2c1 100644 --- a/lib/LinkUART.hpp +++ b/lib/LinkUART.hpp @@ -2,7 +2,7 @@ #define LINK_UART_H // -------------------------------------------------------------------------- -// An UART handler for the Link Port (8N1, 7N1, 8E1, 7E1, 8O1, 7E1). +// A UART handler for the Link Port (8N1, 7N1, 8E1, 7E1, 8O1, 7E1). // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: @@ -24,33 +24,51 @@ // (see examples) // -------------------------------------------------------------------------- -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif -// Buffer size +#include "_link_common.hpp" + +#ifndef LINK_UART_QUEUE_SIZE +/** + * @brief Buffer size in bytes. + */ #define LINK_UART_QUEUE_SIZE 256 +#endif + +static volatile char LINK_UART_VERSION[] = "LinkUART/v7.0.0"; -#define LINK_UART_BIT_CTS 2 -#define LINK_UART_BIT_PARITY_CONTROL 3 -#define LINK_UART_BIT_SEND_DATA_FLAG 4 -#define LINK_UART_BIT_RECEIVE_DATA_FLAG 5 -#define LINK_UART_BIT_ERROR_FLAG 6 -#define LINK_UART_BIT_DATA_LENGTH 7 -#define LINK_UART_BIT_FIFO_ENABLE 8 -#define LINK_UART_BIT_PARITY_ENABLE 9 -#define LINK_UART_BIT_SEND_ENABLE 10 -#define LINK_UART_BIT_RECEIVE_ENABLE 11 -#define LINK_UART_BIT_UART_1 12 -#define LINK_UART_BIT_UART_2 13 -#define LINK_UART_BIT_IRQ 14 -#define LINK_UART_BIT_GENERAL_PURPOSE_LOW 14 -#define LINK_UART_BIT_GENERAL_PURPOSE_HIGH 15 #define LINK_UART_BARRIER asm volatile("" ::: "memory") -static volatile char LINK_UART_VERSION[] = "LinkUART/v6.3.0"; - -void LINK_UART_ISR_SERIAL(); - +/** + * @brief A UART handler for the Link Port (8N1, 7N1, 8E1, 7E1, 8O1, 7E1). + */ class LinkUART { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + using vu32 = volatile unsigned int; + using vs32 = volatile signed int; + using U8Queue = Link::Queue; + + static constexpr int BIT_CTS = 2; + static constexpr int BIT_PARITY_CONTROL = 3; + static constexpr int BIT_SEND_DATA_FLAG = 4; + static constexpr int BIT_RECEIVE_DATA_FLAG = 5; + static constexpr int BIT_ERROR_FLAG = 6; + static constexpr int BIT_DATA_LENGTH = 7; + static constexpr int BIT_FIFO_ENABLE = 8; + static constexpr int BIT_PARITY_ENABLE = 9; + static constexpr int BIT_SEND_ENABLE = 10; + static constexpr int BIT_RECEIVE_ENABLE = 11; + static constexpr int BIT_UART_1 = 12; + static constexpr int BIT_UART_2 = 13; + static constexpr int BIT_IRQ = 14; + static constexpr int BIT_GENERAL_PURPOSE_LOW = 14; + static constexpr int BIT_GENERAL_PURPOSE_HIGH = 15; + public: enum BaudRate { BAUD_RATE_0, // 9600 bps @@ -61,6 +79,9 @@ class LinkUART { enum DataSize { SIZE_7_BITS, SIZE_8_BITS }; enum Parity { NO, EVEN, ODD }; + /** + * @brief Constructs a new LinkUART object. + */ explicit LinkUART() { this->config.baudRate = BAUD_RATE_0; this->config.dataSize = SIZE_8_BITS; @@ -68,8 +89,19 @@ class LinkUART { this->config.useCTS = false; } - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + /** + * @brief Activates the library using a specific UART mode. + * Defaults: 9600bps, 8-bit data, no parity bit, no CTS_ + * @param baudRate One of the enum values from `LinkUART::BaudRate`. + * @param dataSize One of the enum values from `LinkUART::DataSize`. + * @param parity One of the enum values from `LinkUART::Parity`. + * @param useCTS Enable RTS/CTS flow. + */ void activate(BaudRate baudRate = BAUD_RATE_0, DataSize dataSize = SIZE_8_BITS, Parity parity = NO, @@ -90,6 +122,9 @@ class LinkUART { LINK_UART_BARRIER; } + /** + * @brief Deactivates the library. + */ void deactivate() { LINK_UART_BARRIER; isEnabled = false; @@ -99,10 +134,24 @@ class LinkUART { stop(); } + /** + * @brief Takes a null-terminated `string`, and sends it followed by a `'\n'` + * character. The null character is not sent. + * @param string The null-terminated string. + * \warning Blocks the system until completion. + */ void sendLine(const char* string) { sendLine(string, []() { return false; }); } + /** + * @brief Takes a null-terminated `string`, and sends it followed by a `'\n'` + * character. The null character is not sent. + * @param string The null-terminated string. + * @param cancel A function that will be continuously invoked. If it returns + * `true`, the transfer will be aborted. + * \warning Blocks the system until completion or cancellation. + */ template void sendLine(const char* string, F cancel) { for (u32 i = 0; string[i] != '\0'; i++) { @@ -114,11 +163,30 @@ class LinkUART { send('\n'); } + /** + * @brief Reads characters into `string` until finding a `'\n'` character or a + * character `limit` is reached. A null terminator is added at the end. + * Returns `false` if the limit has been reached without finding a newline + * character. + * @param string The output string buffer. + * @param limit The character limit. + * \warning Blocks the system until completion. + */ bool readLine(char* string, u32 limit = LINK_UART_QUEUE_SIZE) { - return readLine( - string, []() { return false; }, limit); + return readLine(string, []() { return false; }, limit); } + /** + * @brief Reads characters into `string` until finding a `'\n'` character or a + * character `limit` is reached. A null terminator is added at the end. + * Returns `false` if the limit has been reached without finding a newline + * character. + * @param string The output string buffer. + * @param cancel A function that will be continuously invoked. If it returns + * `true`, the transfer will be aborted. + * @param limit The character limit. + * \warning Blocks the system until completion or cancellation. + */ template bool readLine(char* string, F cancel, u32 limit = LINK_UART_QUEUE_SIZE) { u32 readBytes = 0; @@ -140,11 +208,24 @@ class LinkUART { return !aborted && readBytes > 1; } + /** + * @brief Sends `size` bytes from `buffer`, starting at byte `offset`. + * @param buffer The source buffer. + * @param size The size in bytes. + * @param offset The starting offset. + */ void send(const u8* buffer, u32 size, u32 offset = 0) { for (u32 i = 0; i < size; i++) send(buffer[offset + i]); } + /** + * @brief Tries to read `size` bytes into `(u8*)(buffer + offset)`. Returns + * the number of read bytes. + * @param buffer The target buffer. + * @param size The size in bytes. + * @param offset The offset from target buffer. + */ u32 read(u8* buffer, u32 size, u32 offset = 0) { for (u32 i = 0; i < size; i++) { if (!canRead()) @@ -155,94 +236,56 @@ class LinkUART { return size; } - bool canRead() { return !incomingQueue.isEmpty(); } - bool canSend() { return !outgoingQueue.isFull(); } - u32 availableForRead() { return incomingQueue.size(); } - u32 availableForSend() { return LINK_UART_QUEUE_SIZE - outgoingQueue.size(); } + /** + * @brief Returns whether there are bytes to read or not. + */ + [[nodiscard]] bool canRead() { return !incomingQueue.isEmpty(); } - u8 read() { - LINK_UART_BARRIER; - isReading = true; - LINK_UART_BARRIER; + /** + * @brief Returns whether there is room to send new messages or not. + */ + [[nodiscard]] bool canSend() { return !outgoingQueue.isFull(); } - u8 data = incomingQueue.pop(); + /** + * @brief Returns the number of bytes available for read. + */ + [[nodiscard]] u32 availableForRead() { return incomingQueue.size(); } - LINK_UART_BARRIER; - isReading = false; - LINK_UART_BARRIER; - - return data; + /** + * @brief Returns the number of bytes available for send (buffer size - queued + * bytes). + */ + [[nodiscard]] u32 availableForSend() { + return LINK_UART_QUEUE_SIZE - outgoingQueue.size(); } - void send(u8 data) { - LINK_UART_BARRIER; - isAdding = true; - LINK_UART_BARRIER; + /** + * @brief Reads a byte. Returns 0 if nothing is found. + */ + u8 read() { return incomingQueue.syncPop(); } - outgoingQueue.push(data); - - LINK_UART_BARRIER; - isAdding = false; - LINK_UART_BARRIER; - } + /** + * @brief Sends a `data` byte. + * @param data The value to be sent. + */ + void send(u8 data) { outgoingQueue.syncPush(data); } + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ void _onSerial() { if (!isEnabled || hasError()) return; - if (!isReading && canReceive()) - incomingQueue.push((u8)REG_SIODATA8); + if (!incomingQueue.isReading() && canReceive()) + incomingQueue.push((u8)Link::_REG_SIODATA8); - if (!isAdding && canTransfer() && needsTransfer()) - REG_SIODATA8 = outgoingQueue.pop(); + if (!outgoingQueue.isWriting() && canTransfer() && needsTransfer()) + Link::_REG_SIODATA8 = outgoingQueue.pop(); } private: - class U8Queue { - public: - void push(u8 item) { - if (isFull()) - pop(); - - rear = (rear + 1) % LINK_UART_QUEUE_SIZE; - arr[rear] = item; - count++; - } - - u16 pop() { - if (isEmpty()) - return 0; - - auto x = arr[front]; - front = (front + 1) % LINK_UART_QUEUE_SIZE; - count--; - - return x; - } - - u16 peek() { - if (isEmpty()) - return 0; - - return arr[front]; - } - - void clear() { - front = count = 0; - rear = -1; - } - - u32 size() { return count; } - bool isEmpty() { return size() == 0; } - bool isFull() { return size() == LINK_UART_QUEUE_SIZE; } - - private: - u8 arr[LINK_UART_QUEUE_SIZE]; - vs32 front = 0; - vs32 rear = -1; - vu32 count = 0; - }; - struct Config { BaudRate baudRate; DataSize dataSize; @@ -254,13 +297,11 @@ class LinkUART { U8Queue incomingQueue; U8Queue outgoingQueue; volatile bool isEnabled = false; - volatile bool isReading = false; - volatile bool isAdding = false; - bool canReceive() { return !isBitHigh(LINK_UART_BIT_RECEIVE_DATA_FLAG); } - bool canTransfer() { return !isBitHigh(LINK_UART_BIT_SEND_DATA_FLAG); } - bool hasError() { return isBitHigh(LINK_UART_BIT_ERROR_FLAG); } - bool needsTransfer() { return outgoingQueue.size() > 0; } + bool canReceive() { return !isBitHigh(BIT_RECEIVE_DATA_FLAG); } + bool canTransfer() { return !isBitHigh(BIT_SEND_DATA_FLAG); } + bool hasError() { return isBitHigh(BIT_ERROR_FLAG); } + bool needsTransfer() { return !outgoingQueue.isEmpty(); } void reset() { resetState(); @@ -292,34 +333,37 @@ class LinkUART { setReceiveOn(); } - void set8BitData() { setBitHigh(LINK_UART_BIT_DATA_LENGTH); } - void setParityOn() { setBitHigh(LINK_UART_BIT_PARITY_ENABLE); } - void setOddParity() { setBitHigh(LINK_UART_BIT_PARITY_CONTROL); } - void setCTSOn() { setBitHigh(LINK_UART_BIT_CTS); } - void setFIFOOn() { setBitHigh(LINK_UART_BIT_FIFO_ENABLE); } - void setInterruptsOn() { setBitHigh(LINK_UART_BIT_IRQ); } - void setSendOn() { setBitHigh(LINK_UART_BIT_SEND_ENABLE); } - void setReceiveOn() { setBitHigh(LINK_UART_BIT_RECEIVE_ENABLE); } + void set8BitData() { setBitHigh(BIT_DATA_LENGTH); } + void setParityOn() { setBitHigh(BIT_PARITY_ENABLE); } + void setOddParity() { setBitHigh(BIT_PARITY_CONTROL); } + void setCTSOn() { setBitHigh(BIT_CTS); } + void setFIFOOn() { setBitHigh(BIT_FIFO_ENABLE); } + void setInterruptsOn() { setBitHigh(BIT_IRQ); } + void setSendOn() { setBitHigh(BIT_SEND_ENABLE); } + void setReceiveOn() { setBitHigh(BIT_RECEIVE_ENABLE); } void setUARTMode() { - REG_RCNT = REG_RCNT & ~(1 << LINK_UART_BIT_GENERAL_PURPOSE_HIGH); - REG_SIOCNT = (1 << LINK_UART_BIT_UART_1) | (1 << LINK_UART_BIT_UART_2); - REG_SIOCNT |= config.baudRate; - REG_SIOMLT_SEND = 0; + Link::_REG_RCNT = Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_SIOCNT = (1 << BIT_UART_1) | (1 << BIT_UART_2); + Link::_REG_SIOCNT |= config.baudRate; + Link::_REG_SIOMLT_SEND = 0; } void setGeneralPurposeMode() { - REG_RCNT = (REG_RCNT & ~(1 << LINK_UART_BIT_GENERAL_PURPOSE_LOW)) | - (1 << LINK_UART_BIT_GENERAL_PURPOSE_HIGH); + Link::_REG_RCNT = (Link::_REG_RCNT & ~(1 << BIT_GENERAL_PURPOSE_LOW)) | + (1 << BIT_GENERAL_PURPOSE_HIGH); } - bool isBitHigh(u8 bit) { return (REG_SIOCNT >> bit) & 1; } - void setBitHigh(u8 bit) { REG_SIOCNT |= 1 << bit; } - void setBitLow(u8 bit) { REG_SIOCNT &= ~(1 << bit); } + 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 LinkUART* linkUART; +/** + * @brief SERIAL interrupt handler. + */ inline void LINK_UART_ISR_SERIAL() { linkUART->_onSerial(); } diff --git a/lib/LinkUniversal.hpp b/lib/LinkUniversal.hpp index a2c1371..267bdd2 100644 --- a/lib/LinkUniversal.hpp +++ b/lib/LinkUniversal.hpp @@ -2,7 +2,7 @@ #define LINK_UNIVERSAL_H // -------------------------------------------------------------------------- -// A multiplayer connection for the Link Cable and the Wireless Adapter. +// A multiplayer connection for the Link Cable and the Wireless Adapter. // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: @@ -12,8 +12,6 @@ // irq_add(II_VBLANK, LINK_UNIVERSAL_ISR_VBLANK); // irq_add(II_SERIAL, LINK_UNIVERSAL_ISR_SERIAL); // irq_add(II_TIMER3, LINK_UNIVERSAL_ISR_TIMER); -// irq_add(II_TIMER2, LINK_UNIVERSAL_ISR_ACK_TIMER); // (*) -// // optional, for `LinkWireless::asyncACKTimerId` -----^ // - 3) Initialize the library with: // linkUniversal->activate(); // - 4) Sync: @@ -34,46 +32,68 @@ // (see examples) // -------------------------------------------------------------------------- // (*2) For CABLE mode: -// The hardware is very sensitive to timing. Make sure your interrupt -// handlers are short, so `LINK_UNIVERSAL_ISR_SERIAL()` is called on time. -// Another option would be activating nested interrupts by setting -// `REG_IME=1` at the start of your interrupt handler. +// The hardware is very sensitive to timing. Make sure that +// `LINK_CABLE_ISR_SERIAL()` is handled on time. That means: +// Be careful with DMA usage (which stops the CPU), and write short +// interrupt handlers (or activate nested interrupts by setting +// `REG_IME=1` at the start of your handlers). // -------------------------------------------------------------------------- // `send(...)` restrictions: // - 0xFFFF and 0x0 are reserved values, so don't use them! // (they mean 'disconnected' and 'no data' respectively) // -------------------------------------------------------------------------- -#include -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + #include #include "LinkCable.hpp" #include "LinkWireless.hpp" -// Max players. Default = 5 (keep in mind that LinkCable's limit is 4) +#ifndef LINK_UNIVERSAL_MAX_PLAYERS +/** + * @brief Maximum number of players. Default = 5 + * \warning Keep in mind that LinkCable's limit is 4. + */ #define LINK_UNIVERSAL_MAX_PLAYERS LINK_WIRELESS_MAX_PLAYERS +#endif -// Game ID Filter. Default = 0 (no filter) +#ifndef LINK_UNIVERSAL_GAME_ID_FILTER +/** + * @brief Game ID Filter (`0x0000` ~ `0x7fff`). Default = 0 (no filter) + * This restricts wireless connections to rooms with a specific game ID. + * When disabled, it connects to any game ID and uses `0x7fff` when serving. + */ #define LINK_UNIVERSAL_GAME_ID_FILTER 0 +#endif + +static volatile char LINK_UNIVERSAL_VERSION[] = "LinkUniversal/v7.0.0"; #define LINK_UNIVERSAL_DISCONNECTED LINK_CABLE_DISCONNECTED #define LINK_UNIVERSAL_NO_DATA LINK_CABLE_NO_DATA -#define LINK_UNIVERSAL_MAX_ROOM_NUMBER 32000 -#define LINK_UNIVERSAL_INIT_WAIT_FRAMES 10 -#define LINK_UNIVERSAL_SWITCH_WAIT_FRAMES 25 -#define LINK_UNIVERSAL_SWITCH_WAIT_FRAMES_RANDOM 10 -#define LINK_UNIVERSAL_BROADCAST_SEARCH_WAIT_FRAMES 10 -#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES 60 -#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM 30 - -static volatile char LINK_UNIVERSAL_VERSION[] = "LinkUniversal/v6.3.0"; - -void LINK_UNIVERSAL_ISR_VBLANK(); -void LINK_UNIVERSAL_ISR_SERIAL(); -void LINK_UNIVERSAL_ISR_TIMER(); +/** + * @brief A multiplayer connection for the Link Cable and the Wireless Adapter. + */ class LinkUniversal { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + using s8 = signed char; + using U16Queue = Link::Queue; + + static constexpr int MAX_ROOM_NUMBER = 32000; + static constexpr int INIT_WAIT_FRAMES = 10; + static constexpr int SWITCH_WAIT_FRAMES = 25; + static constexpr int SWITCH_WAIT_FRAMES_RANDOM = 10; + static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 10; + static constexpr int SERVE_WAIT_FRAMES = 60; + static constexpr int SERVE_WAIT_FRAMES_RANDOM = 30; + public: enum State { INITIALIZING, WAITING, CONNECTED }; enum Mode { LINK_CABLE, LINK_WIRELESS }; @@ -88,7 +108,6 @@ class LinkUniversal { struct CableOptions { LinkCable::BaudRate baudRate; u32 timeout; - u32 remoteTimeout; u16 interval; u8 sendTimerId; }; @@ -97,46 +116,65 @@ class LinkUniversal { bool retransmission; u32 maxPlayers; u32 timeout; - u32 remoteTimeout; u16 interval; u8 sendTimerId; - s8 asyncACKTimerId; }; - explicit LinkUniversal( - Protocol protocol = AUTODETECT, - const char* gameName = "", - CableOptions cableOptions = - CableOptions{ - LinkCable::BaudRate::BAUD_RATE_1, LINK_CABLE_DEFAULT_TIMEOUT, - LINK_CABLE_DEFAULT_REMOTE_TIMEOUT, LINK_CABLE_DEFAULT_INTERVAL, - LINK_CABLE_DEFAULT_SEND_TIMER_ID}, - WirelessOptions wirelessOptions = WirelessOptions{ - true, LINK_UNIVERSAL_MAX_PLAYERS, LINK_WIRELESS_DEFAULT_TIMEOUT, - LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, LINK_WIRELESS_DEFAULT_INTERVAL, - LINK_WIRELESS_DEFAULT_SEND_TIMER_ID, - LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID}) { - this->linkCable = new LinkCable( - cableOptions.baudRate, cableOptions.timeout, cableOptions.remoteTimeout, - cableOptions.interval, cableOptions.sendTimerId); + /** + * @brief Constructs a new LinkUniversal object. + * @param protocol One of the enum values from `LinkUniversal::Protocol`. + * @param gameName The game name that will be broadcasted in wireless sessions + * (max `14` characters). The string must be a null-terminated character + * array. The library uses this to only connect to servers from the same game. + * @param cableOptions All the LinkCable constructor parameters in one struct. + * @param wirelessOptions All the LinkWireless constructor parameters in one + * struct. + * @param randomSeed Random seed used for waits to prevent livelocks. If you + * use _libtonc_, pass `__qran_seed`. + */ + explicit LinkUniversal(Protocol protocol = AUTODETECT, + const char* gameName = "", + CableOptions cableOptions = + CableOptions{LinkCable::BaudRate::BAUD_RATE_1, + LINK_CABLE_DEFAULT_TIMEOUT, + LINK_CABLE_DEFAULT_INTERVAL, + LINK_CABLE_DEFAULT_SEND_TIMER_ID}, + WirelessOptions wirelessOptions = + WirelessOptions{ + true, LINK_UNIVERSAL_MAX_PLAYERS, + LINK_WIRELESS_DEFAULT_TIMEOUT, + LINK_WIRELESS_DEFAULT_INTERVAL, + LINK_WIRELESS_DEFAULT_SEND_TIMER_ID}, + int randomSeed = 123) { + this->linkCable = + new LinkCable(cableOptions.baudRate, cableOptions.timeout, + cableOptions.interval, cableOptions.sendTimerId); this->linkWireless = new LinkWireless( wirelessOptions.retransmission, true, - min(wirelessOptions.maxPlayers, LINK_UNIVERSAL_MAX_PLAYERS), - wirelessOptions.timeout, wirelessOptions.remoteTimeout, - wirelessOptions.interval, wirelessOptions.sendTimerId, - wirelessOptions.asyncACKTimerId); + Link::_min(wirelessOptions.maxPlayers, LINK_UNIVERSAL_MAX_PLAYERS), + wirelessOptions.timeout, wirelessOptions.interval, + wirelessOptions.sendTimerId); this->config.protocol = protocol; this->config.gameName = gameName; } - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + /** + * @brief Activates the library. + */ void activate() { reset(); isEnabled = true; } + /** + * @brief Deactivates the library. + */ void deactivate() { isEnabled = false; linkCable->deactivate(); @@ -144,29 +182,38 @@ class LinkUniversal { resetState(); } - void setProtocol(Protocol protocol) { this->config.protocol = protocol; } - Protocol getProtocol() { return this->config.protocol; } + /** + * @brief Returns `true` if there are at least 2 connected players. + */ + [[nodiscard]] bool isConnected() { return state == CONNECTED; } - bool isConnected() { return state == CONNECTED; } - - u8 playerCount() { + /** + * @brief Returns the number of connected players (`0~5`). + */ + [[nodiscard]] u8 playerCount() { return mode == LINK_CABLE ? linkCable->playerCount() : linkWireless->playerCount(); } - u8 currentPlayerId() { + /** + * @brief Returns the current player ID (`0~4`). + */ + [[nodiscard]] u8 currentPlayerId() { return mode == LINK_CABLE ? linkCable->currentPlayerId() : linkWireless->currentPlayerId(); } + /** + * @brief Call this method every time you need to fetch new data. + */ void sync() { if (!isEnabled) return; - u16 keys = ~REG_KEYS & KEY_ANY; - __qran_seed += keys; - __qran_seed += REG_RCNT; - __qran_seed += REG_SIOCNT; + u16 keys = ~Link::_REG_KEYS & Link::_KEY_ANY; + randomSeed += keys; + randomSeed += Link::_REG_RCNT; + randomSeed += Link::_REG_SIOCNT; if (mode == LINK_CABLE) linkCable->sync(); @@ -174,7 +221,7 @@ class LinkUniversal { switch (state) { case INITIALIZING: { waitCount++; - if (waitCount > LINK_UNIVERSAL_INIT_WAIT_FRAMES) + if (waitCount > INIT_WAIT_FRAMES) start(); break; }; @@ -226,13 +273,27 @@ class LinkUniversal { break; } + default: { + } } } + /** + * @brief Waits for data from player #`playerId`. Returns `true` on success, + * or `false` on disconnection. + * @param playerId A player ID. + */ bool waitFor(u8 playerId) { return waitFor(playerId, []() { return false; }); } + /** + * @brief Waits for data from player #`playerId`. Returns `true` on success, + * or `false` on disconnection. + * @param playerId ID of player to wait data from. + * @param cancel A function that will be continuously invoked. If it returns + * `true`, the wait be aborted. + */ template bool waitFor(u8 playerId, F cancel) { sync(); @@ -241,19 +302,47 @@ class LinkUniversal { : linkWireless->config.sendTimerId; while (isConnected() && !canRead(playerId) && !cancel()) { - IntrWait(1, IRQ_SERIAL | LINK_CABLE_TIMER_IRQ_IDS[timerId]); + Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_TIMER_IRQ_IDS[timerId]); sync(); } return isConnected() && canRead(playerId); } - bool canRead(u8 playerId) { return !incomingMessages[playerId].isEmpty(); } + /** + * @brief Returns `true` if there are pending messages from player + * #`playerId`. + * @param playerId A player ID. + * \warning Keep in mind that if this returns `false`, it will keep doing so + * until you *fetch new data* with `sync()`. + */ + [[nodiscard]] bool canRead(u8 playerId) { + return !incomingMessages[playerId].isEmpty(); + } + /** + * @brief Dequeues and returns the next message from player #`playerId`. + * @param playerId A player ID. + * \warning If there's no data from that player, a `0` will be returned. + */ u16 read(u8 playerId) { return incomingMessages[playerId].pop(); } - u16 peek(u8 playerId) { return incomingMessages[playerId].peek(); } + /** + * @brief Returns the next message from player #`playerId` without dequeuing + * it. + * @param playerId A player ID. + * \warning If there's no data from that player, a `0` will be returned. + */ + [[nodiscard]] u16 peek(u8 playerId) { + return incomingMessages[playerId].peek(); + } + /** + * @brief Sends `data` to all connected players. + * If the buffers are full, it either drops the oldest message (on cable mode) + * or ignores it returning `false` (on wireless mode). + * @param data The value to be sent. + */ bool send(u16 data) { if (data == LINK_CABLE_DISCONNECTED || data == LINK_CABLE_NO_DATA) return false; @@ -266,18 +355,58 @@ class LinkUniversal { } } - State getState() { return state; } - Mode getMode() { return mode; } - LinkWireless::State getWirelessState() { return linkWireless->getState(); } + /** + * @brief Returns the current state. + * @return One of the enum values from `LinkUniversal::State`. + */ + [[nodiscard]] State getState() { return state; } + + /** + * @brief Returns the active mode. + * @return One of the enum values from `LinkUniversal::Mode`. + */ + [[nodiscard]] Mode getMode() { return mode; } + + /** + * @brief Returns the active protocol + * @return One of the enum values from `LinkUniversal::Protocol`. + */ + [[nodiscard]] Protocol getProtocol() { return this->config.protocol; } + + /** + * @brief Returns the wireless state (same as `LinkWireless::getState()`). + */ + [[nodiscard]] LinkWireless::State getWirelessState() { + return linkWireless->getState(); + } + + /** + * @brief Sets the active `protocol`. + * @param protocol One of the enum values from `LinkUniversal::Protocol`. + */ + void setProtocol(Protocol protocol) { this->config.protocol = protocol; } ~LinkUniversal() { delete linkCable; delete linkWireless; } - u32 _getWaitCount() { return waitCount; } - u32 _getSubWaitCount() { return subWaitCount; } + /** + * @brief Returns the wait count. + * \warning This is internal API! + */ + [[nodiscard]] u32 _getWaitCount() { return waitCount; } + /** + * @brief Returns the sub-wait count. + * \warning This is internal API! + */ + [[nodiscard]] u32 _getSubWaitCount() { return subWaitCount; } + + /** + * @brief This method is called by the VBLANK interrupt handler. + * \warning This is internal API! + */ void _onVBlank() { if (mode == LINK_CABLE) linkCable->_onVBlank(); @@ -285,6 +414,10 @@ class LinkUniversal { linkWireless->_onVBlank(); } + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ void _onSerial() { if (mode == LINK_CABLE) linkCable->_onSerial(); @@ -292,6 +425,10 @@ class LinkUniversal { linkWireless->_onSerial(); } + /** + * @brief This method is called by the TIMER interrupt handler. + * \warning This is internal API! + */ void _onTimer() { if (mode == LINK_CABLE) linkCable->_onTimer(); @@ -299,11 +436,6 @@ class LinkUniversal { linkWireless->_onTimer(); } - void _onACKTimer() { - if (mode == LINK_WIRELESS) - linkWireless->_onACKTimer(); - } - LinkCable* linkCable; LinkWireless* linkWireless; @@ -313,7 +445,7 @@ class LinkUniversal { const char* gameName; }; - LinkCable::U16Queue incomingMessages[LINK_UNIVERSAL_MAX_PLAYERS]; + U16Queue incomingMessages[LINK_UNIVERSAL_MAX_PLAYERS]; Config config; State state = INITIALIZING; Mode mode = LINK_CABLE; @@ -321,10 +453,16 @@ class LinkUniversal { u32 switchWait = 0; u32 subWaitCount = 0; u32 serveWait = 0; + int randomSeed = 0; volatile bool isEnabled = false; void receiveCableMessages() { - for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { + static constexpr u32 MAX_PLAYERS = + LINK_UNIVERSAL_MAX_PLAYERS < LINK_CABLE_MAX_PLAYERS + ? LINK_UNIVERSAL_MAX_PLAYERS + : LINK_CABLE_MAX_PLAYERS; + + for (u32 i = 0; i < MAX_PLAYERS; i++) { while (linkCable->canRead(i)) incomingMessages[i].push(linkCable->read(i)); } @@ -339,7 +477,8 @@ class LinkUniversal { if (message.packetId == LINK_WIRELESS_END) break; - incomingMessages[message.playerId].push(message.data); + if (message.playerId < LINK_UNIVERSAL_MAX_PLAYERS) + incomingMessages[message.playerId].push(message.data); } } @@ -355,7 +494,7 @@ class LinkUniversal { waitCount = 0; subWaitCount++; - if (subWaitCount >= LINK_UNIVERSAL_BROADCAST_SEARCH_WAIT_FRAMES) { + if (subWaitCount >= BROADCAST_SEARCH_WAIT_FRAMES) { if (!tryConnectOrServeWirelessSession()) return false; } @@ -380,6 +519,8 @@ class LinkUniversal { // (should not happen) break; } + default: { + } } return true; @@ -402,8 +543,7 @@ class LinkUniversal { (LINK_UNIVERSAL_GAME_ID_FILTER == 0 || server.gameId == LINK_UNIVERSAL_GAME_ID_FILTER)) { u32 randomNumber = safeStoi(server.userName); - if (randomNumber > maxRandomNumber && - randomNumber < LINK_UNIVERSAL_MAX_ROOM_NUMBER) { + if (randomNumber > maxRandomNumber && randomNumber < MAX_ROOM_NUMBER) { maxRandomNumber = randomNumber; serverIndex = i; } @@ -418,9 +558,8 @@ class LinkUniversal { return false; subWaitCount = 0; - serveWait = LINK_UNIVERSAL_SERVE_WAIT_FRAMES + - qran_range(1, LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM); - u32 randomNumber = qran_range(1, LINK_UNIVERSAL_MAX_ROOM_NUMBER); + serveWait = SERVE_WAIT_FRAMES + _qran_range(1, SERVE_WAIT_FRAMES_RANDOM); + u32 randomNumber = _qran_range(1, MAX_ROOM_NUMBER); char randomNumberStr[6]; std::snprintf(randomNumberStr, sizeof(randomNumberStr), "%d", randomNumber); @@ -450,6 +589,8 @@ class LinkUniversal { setMode(LINK_WIRELESS); break; } + default: { + } } } @@ -457,7 +598,7 @@ class LinkUniversal { if (mode == LINK_CABLE) linkCable->deactivate(); else - linkWireless->deactivate(); + linkWireless->deactivate(false); } void toggleMode() { @@ -476,6 +617,8 @@ class LinkUniversal { setMode(LINK_WIRELESS); break; } + default: { + } } } @@ -489,8 +632,12 @@ class LinkUniversal { void start() { if (mode == LINK_CABLE) linkCable->activate(); - else - linkWireless->activate(); + else { + if (!linkWireless->activate()) { + toggleMode(); + return; + } + } state = WAITING; resetState(); @@ -498,8 +645,7 @@ class LinkUniversal { void resetState() { waitCount = 0; - switchWait = LINK_UNIVERSAL_SWITCH_WAIT_FRAMES + - qran_range(1, LINK_UNIVERSAL_SWITCH_WAIT_FRAMES_RANDOM); + switchWait = SWITCH_WAIT_FRAMES + _qran_range(1, SWITCH_WAIT_FRAMES_RANDOM); subWaitCount = 0; serveWait = 0; for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++) @@ -519,24 +665,38 @@ class LinkUniversal { return num; } + + int _qran() { + randomSeed = 1664525 * randomSeed + 1013904223; + return (randomSeed >> 16) & 0x7FFF; + } + + int _qran_range(int min, int max) { + return (_qran() * (max - min) >> 15) + min; + } }; extern LinkUniversal* linkUniversal; +/** + * @brief VBLANK interrupt handler. + */ inline void LINK_UNIVERSAL_ISR_VBLANK() { linkUniversal->_onVBlank(); } +/** + * @brief SERIAL interrupt handler. + */ inline void LINK_UNIVERSAL_ISR_SERIAL() { linkUniversal->_onSerial(); } +/** + * @brief TIMER interrupt handler used for sending. + */ inline void LINK_UNIVERSAL_ISR_TIMER() { linkUniversal->_onTimer(); } -inline void LINK_UNIVERSAL_ISR_ACK_TIMER() { - linkUniversal->_onACKTimer(); -} - #endif // LINK_UNIVERSAL_H diff --git a/lib/LinkWireless.cpp b/lib/LinkWireless.cpp index 1e4858f..57b6a55 100644 --- a/lib/LinkWireless.cpp +++ b/lib/LinkWireless.cpp @@ -1,13 +1,35 @@ #include "LinkWireless.hpp" #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM + LINK_WIRELESS_CODE_IWRAM void LinkWireless::_onSerial() { +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + interrupt = true; + LINK_WIRELESS_BARRIER; + Link::_REG_IME = 1; +#endif + __onSerial(); + +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + irqEnd(); +#endif } LINK_WIRELESS_CODE_IWRAM void LinkWireless::_onTimer() { +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + if (interrupt) + return; + + interrupt = true; + LINK_WIRELESS_BARRIER; + Link::_REG_IME = 1; +#endif + __onTimer(); + +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + irqEnd(); +#endif } -LINK_WIRELESS_CODE_IWRAM void LinkWireless::_onACKTimer() { - __onACKTimer(); -} -#endif \ No newline at end of file + +#endif diff --git a/lib/LinkWireless.hpp b/lib/LinkWireless.hpp index c3c620b..23feb1f 100644 --- a/lib/LinkWireless.hpp +++ b/lib/LinkWireless.hpp @@ -12,8 +12,6 @@ // irq_add(II_VBLANK, LINK_WIRELESS_ISR_VBLANK); // irq_add(II_SERIAL, LINK_WIRELESS_ISR_SERIAL); // irq_add(II_TIMER3, LINK_WIRELESS_ISR_TIMER); -// irq_add(II_TIMER2, LINK_WIRELESS_ISR_ACK_TIMER); // --v -// // optional, for `LinkWireless::asyncACKTimerId` -----^ // - 3) Initialize the library with: // linkWireless->activate(); // - 4) Start a server: @@ -54,8 +52,12 @@ // - 0xFFFF is a reserved value, so don't use it! // -------------------------------------------------------------------------- -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + #include #include "LinkGPIO.hpp" #include "LinkSPI.hpp" @@ -63,70 +65,100 @@ // #include // #include -// Buffer size +#ifndef LINK_WIRELESS_QUEUE_SIZE +/** + * @brief Buffer size (how many incoming and outgoing messages the queues can + * store at max). The default value is `30`, which seems fine for most games. + * \warning This affects how much memory is allocated. With the default value, + * it's around `960` bytes. There's a double-buffered incoming queue and a + * double-buffered outgoing queue (to avoid data races). + * \warning You can approximate the usage with `LINK_WIRELESS_QUEUE_SIZE * 32`. + */ #define LINK_WIRELESS_QUEUE_SIZE 30 +#endif -// Max server transfer length +#ifndef LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH +/** + * @brief Max server transfer length per timer tick. Must be in the range + * `[6;20]`. The default value is `20`, but you might want to set it a bit lower + * to reduce CPU usage. + */ #define LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH 20 +#endif -// Max client transfer length +#ifndef LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH +/** + * @brief Max client transfer length per timer tick. Must be in the range + * `[2;4]`. The default value is `4`. Changing this is not recommended, it's + * already too low. + */ #define LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4 +#endif -// Put Interrupt Service Routines (ISR) in IWRAM (uncomment to enable) +#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM +/** + * @brief Put Interrupt Service Routines (ISR) in IWRAM (uncomment to enable). + * This can significantly improve performance due to its faster access, but it's + * disabled by default to conserve IWRAM space, which is limited. + * \warning If you enable this, make sure that `LinkWireless.cpp` gets compiled! + * For example, in a Makefile-based project, verify that the file is in your + * `SRCDIRS` list. + */ // #define LINK_WIRELESS_PUT_ISR_IN_IWRAM +#endif -// Use send/receive latch (uncomment to enable) +#ifndef LINK_WIRELESS_ENABLE_NESTED_IRQ +/** + * @brief Allow LINK_WIRELESS_ISR_* functions to be interrupted (uncomment to + * enable). + * This can be useful, for example, if your audio engine requires calling a + * VBlank handler with precise timing. + * \warning This won't produce any effect if `LINK_WIRELESS_PUT_ISR_IN_IWRAM` is + * disabled. + */ +// #define LINK_WIRELESS_ENABLE_NESTED_IRQ +#endif + +#ifndef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH +/** + * @brief Use send/receive latch (uncomment to enable). + * This makes it alternate between sends and receives on each timer tick + * (instead of doing both things). Enabling it will introduce some latency but + * also reduce overall CPU usage. + */ // #define LINK_WIRELESS_USE_SEND_RECEIVE_LATCH +#endif + +#ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY +/** + * @brief Optimize the library for two players (uncomment to enable). + * This will make the code smaller and use less CPU. It will also let you + * "misuse" 5 bits from the packet header to send small packets really fast + * (e.g. pressed keys) without confirmation, using the `QUICK_SEND` and + * `QUICK_RECEIVE` properties. + */ +// #define LINK_WIRELESS_TWO_PLAYERS_ONLY +#endif + +static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v7.0.0"; #define LINK_WIRELESS_MAX_PLAYERS 5 #define LINK_WIRELESS_MIN_PLAYERS 2 #define LINK_WIRELESS_END 0 -#define LINK_WIRELESS_DEFAULT_TIMEOUT 10 -#define LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT 10 -#define LINK_WIRELESS_DEFAULT_INTERVAL 50 -#define LINK_WIRELESS_DEFAULT_SEND_TIMER_ID 3 -#define LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID -1 -#define LINK_WIRELESS_BASE_FREQUENCY TM_FREQ_1024 -#define LINK_WIRELESS_PACKET_ID_BITS 6 -#define LINK_WIRELESS_MAX_PACKET_IDS (1 << LINK_WIRELESS_PACKET_ID_BITS) -#define LINK_WIRELESS_PACKET_ID_MASK (LINK_WIRELESS_MAX_PACKET_IDS - 1) -#define LINK_WIRELESS_MSG_PING 0xffff -#define LINK_WIRELESS_PING_WAIT 50 -#define LINK_WIRELESS_TRANSFER_WAIT 15 -#define LINK_WIRELESS_BROADCAST_SEARCH_WAIT_FRAMES 60 -#define LINK_WIRELESS_CMD_TIMEOUT 100 +#define LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 22 #define LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30 -#define LINK_WIRELESS_MAX_GAME_ID 0x7fff -#define LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14 -#define LINK_WIRELESS_MAX_USER_NAME_LENGTH 8 -#define LINK_WIRELESS_LOGIN_STEPS 9 -#define LINK_WIRELESS_COMMAND_HEADER 0x9966 -#define LINK_WIRELESS_RESPONSE_ACK 0x80 -#define LINK_WIRELESS_DATA_REQUEST 0x80000000 -#define LINK_WIRELESS_SETUP_MAGIC 0x003c0420 -#define LINK_WIRELESS_SETUP_MAX_PLAYERS_BIT 16 -#define LINK_WIRELESS_STILL_CONNECTING 0x01000000 #define LINK_WIRELESS_BROADCAST_LENGTH 6 #define LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH \ (1 + LINK_WIRELESS_BROADCAST_LENGTH) -#define LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 22 #define LINK_WIRELESS_MAX_SERVERS \ (LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / \ LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH) -#define LINK_WIRELESS_COMMAND_HELLO 0x10 -#define LINK_WIRELESS_COMMAND_SETUP 0x17 -#define LINK_WIRELESS_COMMAND_BROADCAST 0x16 -#define LINK_WIRELESS_COMMAND_START_HOST 0x19 -#define LINK_WIRELESS_COMMAND_ACCEPT_CONNECTIONS 0x1a -#define LINK_WIRELESS_COMMAND_BROADCAST_READ_START 0x1c -#define LINK_WIRELESS_COMMAND_BROADCAST_READ_POLL 0x1d -#define LINK_WIRELESS_COMMAND_BROADCAST_READ_END 0x1e -#define LINK_WIRELESS_COMMAND_CONNECT 0x1f -#define LINK_WIRELESS_COMMAND_IS_FINISHED_CONNECT 0x20 -#define LINK_WIRELESS_COMMAND_FINISH_CONNECTION 0x21 -#define LINK_WIRELESS_COMMAND_SEND_DATA 0x24 -#define LINK_WIRELESS_COMMAND_RECEIVE_DATA 0x26 -#define LINK_WIRELESS_COMMAND_BYE 0x3d +#define LINK_WIRELESS_MAX_GAME_ID 0x7fff +#define LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14 +#define LINK_WIRELESS_MAX_USER_NAME_LENGTH 8 +#define LINK_WIRELESS_DEFAULT_TIMEOUT 10 +#define LINK_WIRELESS_DEFAULT_INTERVAL 50 +#define LINK_WIRELESS_DEFAULT_SEND_TIMER_ID 3 #define LINK_WIRELESS_BARRIER asm volatile("" ::: "memory") #define LINK_WIRELESS_CODE_IWRAM \ __attribute__((section(".iwram"), target("arm"), noinline)) @@ -139,31 +171,71 @@ if (!reset()) \ return false; -static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v6.3.0"; - -void LINK_WIRELESS_ISR_VBLANK(); -void LINK_WIRELESS_ISR_SERIAL(); -void LINK_WIRELESS_ISR_TIMER(); -const u16 LINK_WIRELESS_LOGIN_PARTS[] = {0x494e, 0x494e, 0x544e, 0x544e, 0x4e45, - 0x4e45, 0x4f44, 0x4f44, 0x8001}; -const u16 LINK_WIRELESS_TIMER_IRQ_IDS[] = {IRQ_TIMER0, IRQ_TIMER1, IRQ_TIMER2, - IRQ_TIMER3}; - +/** + * @brief A high level driver for the GBA Wireless Adapter. + */ class LinkWireless { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + using vu32 = volatile unsigned int; + using vs32 = volatile signed int; + using s8 = signed char; + + static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024; +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + static constexpr int PACKET_ID_BITS = 5; +#else + static constexpr int PACKET_ID_BITS = 6; +#endif + static constexpr int MAX_PACKET_IDS = (1 << PACKET_ID_BITS); + static constexpr int PACKET_ID_MASK = (MAX_PACKET_IDS - 1); + static constexpr int MSG_PING = 0xffff; + static constexpr int PING_WAIT = 50; + static constexpr int TRANSFER_WAIT = 15; + static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 60; + static constexpr int CMD_TIMEOUT = 10; + static constexpr int LOGIN_STEPS = 9; + static constexpr int COMMAND_HEADER_VALUE = 0x9966; + static constexpr int RESPONSE_ACK_VALUE = 0x80; + static constexpr u32 DATA_REQUEST_VALUE = 0x80000000; + static constexpr int SETUP_MAGIC = 0x003c0420; + static constexpr int SETUP_MAX_PLAYERS_BIT = 16; + static constexpr int WAIT_STILL_CONNECTING = 0x01000000; + static constexpr int COMMAND_HELLO = 0x10; + static constexpr int COMMAND_SETUP = 0x17; + static constexpr int COMMAND_BROADCAST = 0x16; + static constexpr int COMMAND_START_HOST = 0x19; + static constexpr int COMMAND_ACCEPT_CONNECTIONS = 0x1a; + static constexpr int COMMAND_BROADCAST_READ_START = 0x1c; + static constexpr int COMMAND_BROADCAST_READ_POLL = 0x1d; + static constexpr int COMMAND_BROADCAST_READ_END = 0x1e; + static constexpr int COMMAND_CONNECT = 0x1f; + static constexpr int COMMAND_IS_FINISHED_CONNECT = 0x20; + static constexpr int COMMAND_FINISH_CONNECTION = 0x21; + static constexpr int COMMAND_SEND_DATA = 0x24; + static constexpr int COMMAND_RECEIVE_DATA = 0x26; + static constexpr int COMMAND_BYE = 0x3d; + static constexpr u16 LOGIN_PARTS[] = {0x494e, 0x494e, 0x544e, 0x544e, 0x4e45, + 0x4e45, 0x4f44, 0x4f44, 0x8001}; + public: +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + u32 QUICK_SEND = 0; + u32 QUICK_RECEIVE = 0; +#endif + // std::function debug; // #define PROFILING_ENABLED #ifdef PROFILING_ENABLED u32 lastVBlankTime = 0; u32 lastSerialTime = 0; u32 lastTimerTime = 0; - u32 lastACKTimerTime = 0; u32 lastFrameSerialIRQs = 0; u32 lastFrameTimerIRQs = 0; - u32 lastFrameACKTimerIRQs = 0; u32 serialIRQCount = 0; u32 timerIRQCount = 0; - u32 ackTimerIRQCount = 0; #endif enum State { @@ -189,7 +261,8 @@ class LinkWireless { RECEIVE_DATA_FAILED = 8, ACKNOWLEDGE_FAILED = 9, TIMEOUT = 10, - REMOTE_TIMEOUT = 11 + REMOTE_TIMEOUT = 11, + BUSY_TRY_AGAIN = 12, }; struct Message { @@ -209,27 +282,53 @@ class LinkWireless { bool isFull() { return currentPlayerCount == 0; } }; - explicit LinkWireless( - bool forwarding = true, - bool retransmission = true, - u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS, - u32 timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, - u32 remoteTimeout = LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, - u16 interval = LINK_WIRELESS_DEFAULT_INTERVAL, - u8 sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID, - s8 asyncACKTimerId = LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID) { + /** + * @brief Constructs a new LinkWireless object. + * @param forwarding If `true`, the server forwards all messages to the + * clients. Otherwise, clients only see messages sent from the server + * (ignoring other peers). + * @param retransmission If `true`, the library handles retransmission for + * you, so there should be no packet loss. + * @param maxPlayers Maximum number of allowed players. If your game only + * supports -for example- two players, set this to `2` as it will make + * transfers faster. + * @param timeout Number of *frames* without receiving *any* data to reset the + * connection. + * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers + * *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values + * will transfer faster but also consume more CPU. + * @param sendTimerId GBA Timer to use for sending. + * \warning You can use `Link::perFrame(...)` to convert from *packets per + * frame* to *interval values*. + */ + explicit LinkWireless(bool forwarding = true, + bool retransmission = true, + u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS, + u32 timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, + u16 interval = LINK_WIRELESS_DEFAULT_INTERVAL, + u8 sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID) { +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + maxPlayers = 2; +#endif + this->config.forwarding = forwarding; this->config.retransmission = retransmission; this->config.maxPlayers = maxPlayers; this->config.timeout = timeout; - this->config.remoteTimeout = remoteTimeout; this->config.interval = interval; this->config.sendTimerId = sendTimerId; - this->config.asyncACKTimerId = asyncACKTimerId; } - bool isActive() { return isEnabled; } + /** + * @brief Returns whether the library is active or not. + */ + [[nodiscard]] bool isActive() { return isEnabled; } + /** + * @brief Activates the library. When an adapter is connected, it changes the + * state to `AUTHENTICATED`. It can also be used to disconnect or reset the + * adapter. + */ bool activate() { lastError = NONE; isEnabled = false; @@ -242,9 +341,20 @@ class LinkWireless { return success; } - bool deactivate() { - activate(); - bool success = sendCommand(LINK_WIRELESS_COMMAND_BYE).success; + /** + * @brief Puts the adapter into a low consumption mode and then deactivates + * the library. It returns a boolean indicating whether the transition to low + * consumption mode was successful. + * @param turnOff Whether the library should put the adapter in the low + * consumption mode or not before deactivation. Defaults to `true`. + */ + bool deactivate(bool turnOff = true) { + bool success = true; + + if (turnOff) { + activate(); + success = sendCommand(COMMAND_BYE).success; + } lastError = NONE; isEnabled = false; @@ -254,6 +364,17 @@ class LinkWireless { return success; } + /** + * @brief Starts broadcasting a server and changes the state to `SERVING`. You + * can optionally provide data that games will be able to read. If the adapter + * is already serving, this method only updates the broadcast data. + * @param gameName Game name. Maximum `14` characters + null terminator. + * @param userName User name. Maximum `8` characters + null terminator. + * @param gameId `(0 ~ 0x7FFF)` Game ID. + * \warning Updating broadcast data while serving can fail if the adapter is + * busy. In that case, this will return `false` and `getLastError()` will be + * `BUSY_TRY_AGAIN`. + */ bool serve(const char* gameName = "", const char* userName = "", u16 gameId = LINK_WIRELESS_MAX_GAME_ID) { @@ -271,6 +392,13 @@ class LinkWireless { return false; } + isSendingSyncCommand = true; + if (asyncCommand.isActive) { + lastError = BUSY_TRY_AGAIN; + isSendingSyncCommand = false; + return false; + } + char finalGameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1]; char finalUserName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1]; copyName(finalGameName, gameName, LINK_WIRELESS_MAX_GAME_NAME_LENGTH); @@ -293,12 +421,10 @@ class LinkWireless { addData(buildU32(buildU16(finalUserName[7], finalUserName[6]), buildU16(finalUserName[5], finalUserName[4]))); - bool success = sendCommand(LINK_WIRELESS_COMMAND_BROADCAST, true).success; + bool success = sendCommand(COMMAND_BROADCAST, true).success; - if (state != SERVING) { - success = - success && sendCommand(LINK_WIRELESS_COMMAND_START_HOST).success; - } + if (state != SERVING) + success = success && sendCommand(COMMAND_START_HOST).success; if (!success) { reset(); @@ -306,22 +432,37 @@ class LinkWireless { return false; } - wait(LINK_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); state = SERVING; return true; } + /** + * @brief Fills the `servers` array with all the currently broadcasting + * servers. + * @param servers The array to be filled with data. + * \warning This action takes 1 second to complete. + * \warning For an async version, see `getServersAsyncStart()`. + */ bool getServers(Server servers[]) { return getServers(servers, []() {}); } + /** + * @brief Fills the `servers` array with all the currently broadcasting + * servers. + * @param servers The array to be filled with data. + * @param onWait A function which will be invoked each time VBlank starts. + * \warning This action takes 1 second to complete. + * \warning For an async version, see `getServersAsyncStart()`. + */ template bool getServers(Server servers[], F onWait) { if (!getServersAsyncStart()) return false; - waitVBlanks(LINK_WIRELESS_BROADCAST_SEARCH_WAIT_FRAMES, onWait); + waitVBlanks(BROADCAST_SEARCH_WAIT_FRAMES, onWait); if (!getServersAsyncEnd(servers)) return false; @@ -329,6 +470,10 @@ class LinkWireless { return true; } + /** + * @brief Starts looking for broadcasting servers and changes the state to + * `SEARCHING`. After this, call `getServersAsyncEnd(...)` 1 second later. + */ bool getServersAsyncStart() { LINK_WIRELESS_RESET_IF_NEEDED if (state != AUTHENTICATED) { @@ -336,8 +481,7 @@ class LinkWireless { return false; } - bool success = - sendCommand(LINK_WIRELESS_COMMAND_BROADCAST_READ_START).success; + bool success = sendCommand(COMMAND_BROADCAST_READ_START).success; if (!success) { reset(); @@ -350,6 +494,11 @@ class LinkWireless { return true; } + /** + * @brief Fills the `servers` array with all the currently broadcasting + * servers. Changes the state to `AUTHENTICATED` again. + * @param servers The array to be filled with data. + */ bool getServersAsyncEnd(Server servers[]) { LINK_WIRELESS_RESET_IF_NEEDED if (state != SEARCHING) { @@ -357,7 +506,7 @@ class LinkWireless { return false; } - auto result = sendCommand(LINK_WIRELESS_COMMAND_BROADCAST_READ_POLL); + auto result = sendCommand(COMMAND_BROADCAST_READ_POLL); bool success1 = result.success && result.responsesSize % LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH == 0; @@ -368,8 +517,7 @@ class LinkWireless { return false; } - bool success2 = - sendCommand(LINK_WIRELESS_COMMAND_BROADCAST_READ_END).success; + bool success2 = sendCommand(COMMAND_BROADCAST_READ_END).success; if (!success2) { reset(); @@ -407,6 +555,11 @@ class LinkWireless { return true; } + /** + * @brief Starts a connection with `serverId` and changes the state to + * `CONNECTING`. + * @param serverId Device ID of the server. + */ bool connect(u16 serverId) { LINK_WIRELESS_RESET_IF_NEEDED if (state != AUTHENTICATED) { @@ -415,7 +568,7 @@ class LinkWireless { } addData(serverId, true); - bool success = sendCommand(LINK_WIRELESS_COMMAND_CONNECT, true).success; + bool success = sendCommand(COMMAND_CONNECT, true).success; if (!success) { reset(); @@ -428,6 +581,12 @@ class LinkWireless { return true; } + /** + * @brief When connecting, this needs to be called until the state is + * `CONNECTED`. It assigns a player ID. Keep in mind that `isConnected()` and + * `playerCount()` won't be updated until the first message from the server + * arrives. + */ bool keepConnecting() { LINK_WIRELESS_RESET_IF_NEEDED if (state != CONNECTING) { @@ -435,14 +594,14 @@ class LinkWireless { return false; } - auto result1 = sendCommand(LINK_WIRELESS_COMMAND_IS_FINISHED_CONNECT); + auto result1 = sendCommand(COMMAND_IS_FINISHED_CONNECT); if (!result1.success || result1.responsesSize == 0) { reset(); lastError = COMMAND_FAILED; return false; } - if (result1.responses[0] == LINK_WIRELESS_STILL_CONNECTING) + if (result1.responses[0] == WAIT_STILL_CONNECTING) return true; u8 assignedPlayerId = 1 + (u8)msB32(result1.responses[0]); @@ -452,7 +611,7 @@ class LinkWireless { return false; } - auto result2 = sendCommand(LINK_WIRELESS_COMMAND_FINISH_CONNECTION); + auto result2 = sendCommand(COMMAND_FINISH_CONNECTION); if (!result2.success) { reset(); lastError = COMMAND_FAILED; @@ -465,6 +624,10 @@ class LinkWireless { return true; } + /** + * @brief Enqueues `data` to be sent to other nodes. + * @param data The value to be sent. + */ bool send(u16 data, int _author = -1) { LINK_WIRELESS_RESET_IF_NEEDED if (!isSessionActive()) { @@ -482,52 +645,75 @@ class LinkWireless { message.playerId = _author >= 0 ? _author : sessionState.currentPlayerId; message.data = data; - LINK_WIRELESS_BARRIER; - isAddingMessage = true; - LINK_WIRELESS_BARRIER; - - sessionState.tmpMessagesToSend.push(message); - - LINK_WIRELESS_BARRIER; - isAddingMessage = false; - LINK_WIRELESS_BARRIER; - - if (isPendingClearActive) { - sessionState.tmpMessagesToSend.clear(); - isPendingClearActive = false; - } + sessionState.newOutgoingMessages.syncPush(message); return true; } + /** + * @brief Fills the `messages` array with incoming messages, forwarding if + * needed. + * @param messages The array to be filled with data. + */ bool receive(Message messages[]) { if (!isEnabled || state == NEEDS_RESET || !isSessionActive()) return false; LINK_WIRELESS_BARRIER; - isReadingMessages = true; + sessionState.incomingMessages.startReading(); LINK_WIRELESS_BARRIER; u32 i = 0; while (!sessionState.incomingMessages.isEmpty()) { auto message = sessionState.incomingMessages.pop(); messages[i] = message; +#ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY forwardMessageIfNeeded(message); +#endif i++; } LINK_WIRELESS_BARRIER; - isReadingMessages = false; + sessionState.incomingMessages.stopReading(); LINK_WIRELESS_BARRIER; return true; } - State getState() { return state; } - bool isConnected() { return sessionState.playerCount > 1; } - bool isSessionActive() { return state == SERVING || state == CONNECTED; } - u8 playerCount() { return sessionState.playerCount; } - u8 currentPlayerId() { return sessionState.currentPlayerId; } + /** + * @brief Returns the current state. + * @return One of the enum values from `LinkWireless::State`. + */ + [[nodiscard]] State getState() { return state; } + + /** + * @brief Returns `true` if the player count is higher than `1`. + */ + [[nodiscard]] bool isConnected() { return sessionState.playerCount > 1; } + + /** + * @brief Returns `true` if the state is `SERVING` or `CONNECTED`. + */ + [[nodiscard]] bool isSessionActive() { + return state == SERVING || state == CONNECTED; + } + + /** + * @brief Returns the number of connected players. + */ + [[nodiscard]] u8 playerCount() { return sessionState.playerCount; } + + /** + * @brief Returns the current player ID. + */ + [[nodiscard]] u8 currentPlayerId() { return sessionState.currentPlayerId; } + + /** + * @brief If one of the other methods returns `false`, you can inspect this to + * know the cause. After this call, the last error is cleared if `clear` is + * `true` (default behavior). + * @param clear Whether it should clear the error or not. + */ Error getLastError(bool clear = true) { Error error = lastError; if (clear) @@ -540,30 +726,97 @@ class LinkWireless { delete linkGPIO; } - bool _hasActiveAsyncCommand() { return asyncCommand.isActive; } - bool _canSend() { return !sessionState.outgoingMessages.isFull(); } - u32 _getPendingCount() { return sessionState.outgoingMessages.size(); } - u32 _lastPacketId() { return sessionState.lastPacketId; } - u32 _lastConfirmationFromClient1() { + /** + * @brief Returns whether it's running an async command or not. + * \warning This is internal API! + */ + [[nodiscard]] bool _hasActiveAsyncCommand() { return asyncCommand.isActive; } + + /** + * @brief Returns whether there's room for new outgoing messages or not. + * \warning This is internal API! + */ + [[nodiscard]] bool _canSend() { + return !sessionState.outgoingMessages.isFull(); + } + + /** + * @brief Returns the number of pending outgoing messages. + * \warning This is internal API! + */ + [[nodiscard]] u32 _getPendingCount() { + return sessionState.outgoingMessages.size(); + } + + /** + * @brief Returns the last packet ID. + * \warning This is internal API! + */ + [[nodiscard]] u32 _lastPacketId() { return sessionState.lastPacketId; } + + /** + * @brief Returns the last confirmation received from player ID 1. + * \warning This is internal API! + */ + [[nodiscard]] u32 _lastConfirmationFromClient1() { return sessionState.lastConfirmationFromClients[1]; } - u32 _lastPacketIdFromClient1() { + + /** + * @brief Returns the last packet ID received from player ID 1. + * \warning This is internal API! + */ + [[nodiscard]] u32 _lastPacketIdFromClient1() { return sessionState.lastPacketIdFromClients[1]; } - u32 _lastConfirmationFromServer() { + + /** + * @brief Returns the last confirmation received from the server. + * \warning This is internal API! + */ + [[nodiscard]] u32 _lastConfirmationFromServer() { return sessionState.lastConfirmationFromServer; } - u32 _lastPacketIdFromServer() { return sessionState.lastPacketIdFromServer; } - u32 _nextPendingPacketId() { + + /** + * @brief Returns the last packet ID received from the server. + * \warning This is internal API! + */ + [[nodiscard]] u32 _lastPacketIdFromServer() { + return sessionState.lastPacketIdFromServer; + } + + /** + * @brief Returns the next pending packet ID. + * \warning This is internal API! + */ + [[nodiscard]] u32 _nextPendingPacketId() { return sessionState.outgoingMessages.isEmpty() ? 0 : sessionState.outgoingMessages.peek().packetId; } + /** + * @brief This method is called by the VBLANK interrupt handler. + * \warning This is internal API! + */ +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + __attribute__((noinline)) void _onVBlank() { +#else void _onVBlank() { +#endif if (!isEnabled) return; +#ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + if (interrupt) { + pendingVBlank = true; + return; + } +#endif +#endif + #ifdef PROFILING_ENABLED profileStart(); #endif @@ -571,22 +824,24 @@ class LinkWireless { if (!isSessionActive()) return; - if (isConnected() && sessionState.frameRecvCount == 0) + if (isConnected() && !sessionState.recvFlag) sessionState.recvTimeout++; - if (sessionState.recvTimeout >= config.timeout) { reset(); lastError = TIMEOUT; return; } +#ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY + trackRemoteTimeouts(); if (!checkRemoteTimeouts()) { reset(); lastError = REMOTE_TIMEOUT; return; } +#endif - sessionState.frameRecvCount = 0; + sessionState.recvFlag = false; sessionState.acceptCalled = false; sessionState.pingSent = false; @@ -594,24 +849,23 @@ class LinkWireless { lastVBlankTime = profileStop(); lastFrameSerialIRQs = serialIRQCount; lastFrameTimerIRQs = timerIRQCount; - lastFrameACKTimerIRQs = ackTimerIRQCount; serialIRQCount = 0; timerIRQCount = 0; - ackTimerIRQCount = 0; #endif } #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM void _onSerial(); void _onTimer(); - void _onACKTimer(); -#endif -#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM +#else void _onSerial() { __onSerial(); } void _onTimer() { __onTimer(); } - void _onACKTimer() { __onACKTimer(); } #endif + /** + * @brief This method is called by the SERIAL interrupt handler. + * \warning This is internal API! + */ LINK_WIRELESS_ALWAYS_INLINE void __onSerial() { if (!isEnabled) return; @@ -623,40 +877,25 @@ class LinkWireless { linkSPI->_onSerial(true); bool hasNewData = linkSPI->getAsyncState() == LinkSPI::AsyncState::READY; - if (!usesAsyncACK()) { - if (hasNewData) { - if (!acknowledge()) { - reset(); - lastError = ACKNOWLEDGE_FAILED; - return; - } - } else + if (hasNewData) { + if (!acknowledge()) { + reset(); + lastError = ACKNOWLEDGE_FAILED; return; - } + } + } else + return; u32 newData = linkSPI->getAsyncData(); if (!isSessionActive()) return; if (asyncCommand.isActive) { - if (usesAsyncACK()) { - if (asyncCommand.ackStep != AsyncCommand::ACKStep::READY) - return; + if (asyncCommand.state == AsyncCommand::State::PENDING) { + updateAsyncCommand(newData); - if (hasNewData) { - linkSPI->_setSOLow(); - asyncCommand.ackStep = AsyncCommand::ACKStep::WAITING_FOR_HIGH; - asyncCommand.pendingData = newData; - startACKTimer(); - } else - return; - } else { - if (asyncCommand.state == AsyncCommand::State::PENDING) { - updateAsyncCommand(newData); - - if (asyncCommand.state == AsyncCommand::State::COMPLETED) - processAsyncCommand(); - } + if (asyncCommand.state == AsyncCommand::State::COMPLETED) + processAsyncCommand(); } } @@ -666,6 +905,10 @@ class LinkWireless { #endif } + /** + * @brief This method is called by the TIMER interrupt handler. + * \warning This is internal API! + */ LINK_WIRELESS_ALWAYS_INLINE void __onTimer() { if (!isEnabled) return; @@ -686,120 +929,35 @@ class LinkWireless { #endif } - LINK_WIRELESS_ALWAYS_INLINE void __onACKTimer() { - if (!isEnabled || !asyncCommand.isActive || - asyncCommand.ackStep == AsyncCommand::ACKStep::READY) - return; - - if (asyncCommand.ackStep == AsyncCommand::ACKStep::WAITING_FOR_HIGH) { - if (!linkSPI->_isSIHigh()) - return; - - linkSPI->_setSOHigh(); - asyncCommand.ackStep = AsyncCommand::ACKStep::WAITING_FOR_LOW; - } else if (asyncCommand.ackStep == AsyncCommand::ACKStep::WAITING_FOR_LOW) { - if (linkSPI->_isSIHigh()) - return; - -#ifdef PROFILING_ENABLED - profileStart(); -#endif - - linkSPI->_setSOLow(); - asyncCommand.ackStep = AsyncCommand::ACKStep::READY; - stopACKTimer(); - - if (asyncCommand.state == AsyncCommand::State::PENDING) { - updateAsyncCommand(asyncCommand.pendingData); - - if (asyncCommand.state == AsyncCommand::State::COMPLETED) - processAsyncCommand(); - } - -#ifdef PROFILING_ENABLED - lastACKTimerTime = profileStop(); - ackTimerIRQCount++; -#endif - } - } - struct Config { bool forwarding; bool retransmission; u8 maxPlayers; u32 timeout; - u32 remoteTimeout; u32 interval; u32 sendTimerId; - s8 asyncACKTimerId; }; + /** + * @brief LinkWireless configuration. + * \warning `deactivate()` first, change the config, and `activate()` again! + */ Config config; private: - class MessageQueue { - public: - void push(Message item) { - if (isFull()) - return; - - rear = (rear + 1) % LINK_WIRELESS_QUEUE_SIZE; - arr[rear] = item; - count++; - } - - Message pop() { - if (isEmpty()) - return Message{}; - - auto x = arr[front]; - front = (front + 1) % LINK_WIRELESS_QUEUE_SIZE; - count--; - - return x; - } - - Message peek() { - if (isEmpty()) - return Message{}; - return arr[front]; - } - - template - void forEach(F action) { - int currentFront = front; - - for (u32 i = 0; i < count; i++) { - if (!action(arr[currentFront])) - return; - currentFront = (currentFront + 1) % LINK_WIRELESS_QUEUE_SIZE; - } - } - - void clear() { - front = count = 0; - rear = -1; - } - - int size() { return count; } - bool isEmpty() { return size() == 0; } - bool isFull() { return size() == LINK_WIRELESS_QUEUE_SIZE; } - - private: - Message arr[LINK_WIRELESS_QUEUE_SIZE]; - vs32 front = 0; - vs32 rear = -1; - vu32 count = 0; - }; + using MessageQueue = Link::Queue; struct SessionState { - MessageQueue incomingMessages; // read by user, write by irq&user - MessageQueue outgoingMessages; // read and write by irq - MessageQueue tmpMessagesToReceive; // read and write by irq - MessageQueue tmpMessagesToSend; // read by irq, write by user&irq - u32 timeouts[LINK_WIRELESS_MAX_PLAYERS]; - u32 recvTimeout = 0; - u32 frameRecvCount = 0; + MessageQueue incomingMessages; // read by user, write by irq&user + MessageQueue outgoingMessages; // read and write by irq + MessageQueue newIncomingMessages; // read and write by irq + MessageQueue newOutgoingMessages; // read by irq, write by user&irq + + u32 recvTimeout = 0; // (~= LinkCable::IRQTimeout) + u32 msgTimeouts[LINK_WIRELESS_MAX_PLAYERS]; // (~= LinkCable::msgTimeouts) + bool recvFlag = false; // (~= LinkCable::IRQFlag) + bool msgFlags[LINK_WIRELESS_MAX_PLAYERS]; // (~= LinkCable::msgFlags) + bool acceptCalled = false; bool pingSent = false; #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH @@ -819,10 +977,15 @@ class LinkWireless { }; struct MessageHeader { - unsigned int partialPacketId : LINK_WIRELESS_PACKET_ID_BITS; + unsigned int partialPacketId : PACKET_ID_BITS; unsigned int isConfirmation : 1; +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + unsigned int playerId : 1; + unsigned int quickData : 5; +#else unsigned int playerId : 3; unsigned int clientCount : 2; +#endif unsigned int dataChecksum : 4; }; @@ -852,8 +1015,6 @@ class LinkWireless { DATA_REQUEST }; - enum ACKStep { READY, WAITING_FOR_HIGH, WAITING_FOR_LOW }; - u8 type; u32 parameters[LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; u32 responses[LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH]; @@ -862,8 +1023,6 @@ class LinkWireless { Step step; u32 sentParameters, totalParameters; u32 receivedResponses, totalResponses; - u32 pendingData; - ACKStep ackStep; bool isActive; }; @@ -874,22 +1033,43 @@ class LinkWireless { State state = NEEDS_RESET; u32 nextCommandData[LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; u32 nextCommandDataSize = 0; - volatile bool isReadingMessages = false; - volatile bool isAddingMessage = false; - volatile bool isPendingClearActive = false; + u32 nextAsyncCommandData[LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; + u32 nextAsyncCommandDataSize = 0; + volatile bool isSendingSyncCommand = false; Error lastError = NONE; volatile bool isEnabled = false; +#ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + volatile bool interrupt = false, pendingVBlank = false; +#endif +#endif + +#ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY void forwardMessageIfNeeded(Message& message) { if (state == SERVING && config.forwarding && sessionState.playerCount > 2) send(message.data, message.playerId); } +#endif + +#ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + void irqEnd() { + interrupt = false; + LINK_WIRELESS_BARRIER; + if (pendingVBlank) { + _onVBlank(); + pendingVBlank = false; + } + } +#endif +#endif void processAsyncCommand() { // (irq only) if (!asyncCommand.result.success) { - if (asyncCommand.type == LINK_WIRELESS_COMMAND_SEND_DATA) + if (asyncCommand.type == COMMAND_SEND_DATA) lastError = SEND_DATA_FAILED; - else if (asyncCommand.type == LINK_WIRELESS_COMMAND_RECEIVE_DATA) + else if (asyncCommand.type == COMMAND_RECEIVE_DATA) lastError = RECEIVE_DATA_FAILED; else lastError = COMMAND_FAILED; @@ -901,31 +1081,30 @@ class LinkWireless { asyncCommand.isActive = false; switch (asyncCommand.type) { - case LINK_WIRELESS_COMMAND_ACCEPT_CONNECTIONS: { + case COMMAND_ACCEPT_CONNECTIONS: { // AcceptConnections (end) - sessionState.playerCount = - min(1 + asyncCommand.result.responsesSize, config.maxPlayers); + sessionState.playerCount = Link::_min( + 1 + asyncCommand.result.responsesSize, config.maxPlayers); break; } - case LINK_WIRELESS_COMMAND_SEND_DATA: { + case COMMAND_SEND_DATA: { // SendData (end) #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH if (state == CONNECTED) sessionState.shouldWaitForServer = true; sessionState.sendReceiveLatch = !sessionState.sendReceiveLatch; -#endif -#ifndef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH +#else if (state == SERVING) { // ReceiveData (start) - sendCommandAsync(LINK_WIRELESS_COMMAND_RECEIVE_DATA); + sendCommandAsync(COMMAND_RECEIVE_DATA); } #endif break; } - case LINK_WIRELESS_COMMAND_RECEIVE_DATA: { + case COMMAND_RECEIVE_DATA: { // ReceiveData (end) #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH @@ -936,14 +1115,13 @@ class LinkWireless { if (asyncCommand.result.responsesSize == 0) break; - sessionState.frameRecvCount++; + sessionState.recvFlag = true; sessionState.recvTimeout = 0; #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH sessionState.shouldWaitForServer = false; #endif - trackRemoteTimeouts(); addIncomingMessagesFromData(asyncCommand.result); #ifndef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH @@ -964,20 +1142,19 @@ class LinkWireless { if (state == SERVING && !sessionState.acceptCalled && sessionState.playerCount < config.maxPlayers) { // AcceptConnections (start) - sendCommandAsync(LINK_WIRELESS_COMMAND_ACCEPT_CONNECTIONS); - sessionState.acceptCalled = true; + if (sendCommandAsync(COMMAND_ACCEPT_CONNECTIONS)) + sessionState.acceptCalled = true; } else if (state == CONNECTED || isConnected()) { #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH bool shouldReceive = !sessionState.sendReceiveLatch || sessionState.shouldWaitForServer; -#endif -#ifndef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH +#else bool shouldReceive = state == CONNECTED; #endif if (shouldReceive) { // ReceiveData (start) - sendCommandAsync(LINK_WIRELESS_COMMAND_RECEIVE_DATA); + sendCommandAsync(COMMAND_RECEIVE_DATA); } else { // SendData (start) sendPendingData(); @@ -988,14 +1165,14 @@ class LinkWireless { void sendPendingData() { // (irq only) copyOutgoingState(); int lastPacketId = setDataFromOutgoingMessages(); - sendCommandAsync(LINK_WIRELESS_COMMAND_SEND_DATA, true); - clearOutgoingMessagesIfNeeded(lastPacketId); + if (sendCommandAsync(COMMAND_SEND_DATA, true)) + clearOutgoingMessagesIfNeeded(lastPacketId); } int setDataFromOutgoingMessages() { // (irq only) u32 maxTransferLength = getDeviceTransferLength(); - addData(0, true); + addAsyncData(0, true); if (config.retransmission) addConfirmations(); @@ -1004,27 +1181,28 @@ class LinkWireless { int lastPacketId = -1; - sessionState.outgoingMessages.forEach( - [this, maxTransferLength, &lastPacketId](Message message) { - u16 header = buildMessageHeader(message.playerId, message.packetId, - buildChecksum(message.data)); - u32 rawMessage = buildU32(header, message.data); + sessionState.outgoingMessages.forEach([this, maxTransferLength, + &lastPacketId](Message message) { + u16 header = buildMessageHeader(message.playerId, message.packetId, + buildChecksum(message.data)); + u32 rawMessage = buildU32(header, message.data); - if (nextCommandDataSize /* -1 (wireless header) + 1 (rawMessage) */ > - maxTransferLength) - return false; + if (nextAsyncCommandDataSize /* -1 (wireless header) + 1 (rawMessage) */ > + maxTransferLength) + return false; - addData(rawMessage); - lastPacketId = message.packetId; + addAsyncData(rawMessage); + lastPacketId = message.packetId; - return true; - }); + return true; + }); // (add wireless header) - u32 bytes = (nextCommandDataSize - 1) * 4; - nextCommandData[0] = sessionState.currentPlayerId == 0 - ? bytes - : bytes << (3 + sessionState.currentPlayerId * 5); + u32 bytes = (nextAsyncCommandDataSize - 1) * 4; + nextAsyncCommandData[0] = + sessionState.currentPlayerId == 0 + ? bytes + : bytes << (3 + sessionState.currentPlayerId * 5); return lastPacketId; } @@ -1041,13 +1219,20 @@ class LinkWireless { MessageHeader header = serializer.asStruct; u32 partialPacketId = header.partialPacketId; bool isConfirmation = header.isConfirmation; - u8 remotePlayerId = header.playerId; + u8 remotePlayerId = Link::_min(header.playerId, config.maxPlayers - 1); +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + QUICK_RECEIVE = header.quickData; + u8 remotePlayerCount = 2; +#else u8 remotePlayerCount = LINK_WIRELESS_MIN_PLAYERS + header.clientCount; +#endif u32 checksum = header.dataChecksum; - bool isPing = data == LINK_WIRELESS_MSG_PING; + bool isPing = data == MSG_PING; - sessionState.timeouts[0] = 0; - sessionState.timeouts[remotePlayerId] = 0; + sessionState.msgTimeouts[0] = 0; + sessionState.msgTimeouts[remotePlayerId] = 0; + sessionState.msgFlags[0] = true; + sessionState.msgFlags[remotePlayerId] = true; if (checksum != buildChecksum(data)) continue; @@ -1064,7 +1249,7 @@ class LinkWireless { if (!handleConfirmation(message)) continue; } else { - sessionState.tmpMessagesToReceive.push(message); + sessionState.newIncomingMessages.push(message); } } copyIncomingState(); @@ -1076,7 +1261,7 @@ class LinkWireless { if (state == SERVING) { u32 expectedPacketId = (sessionState.lastPacketIdFromClients[message.playerId] + 1) % - LINK_WIRELESS_MAX_PACKET_IDS; + MAX_PACKET_IDS; if (config.retransmission && !isConfirmation && message.packetId != expectedPacketId) @@ -1086,8 +1271,8 @@ class LinkWireless { message.packetId = ++sessionState.lastPacketIdFromClients[message.playerId]; } else { - u32 expectedPacketId = (sessionState.lastPacketIdFromServer + 1) % - LINK_WIRELESS_MAX_PACKET_IDS; + u32 expectedPacketId = + (sessionState.lastPacketIdFromServer + 1) % MAX_PACKET_IDS; if (config.retransmission && !isConfirmation && message.packetId != expectedPacketId) @@ -1115,7 +1300,7 @@ class LinkWireless { Message pingMessage; pingMessage.packetId = newPacketId(); pingMessage.playerId = sessionState.currentPlayerId; - pingMessage.data = LINK_WIRELESS_MSG_PING; + pingMessage.data = MSG_PING; sessionState.outgoingMessages.push(pingMessage); sessionState.pingSent = true; } @@ -1123,6 +1308,7 @@ class LinkWireless { void addConfirmations() { // (irq only) if (state == SERVING) { +#ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY if (config.maxPlayers > 2 && (sessionState.lastPacketIdFromClients[1] == 0 || sessionState.lastPacketIdFromClients[2] == 0 || @@ -1131,21 +1317,22 @@ class LinkWireless { u32 lastPacketId = sessionState.lastPacketId; u16 header = buildConfirmationHeader(0, lastPacketId); u32 rawMessage = buildU32(header, lastPacketId & 0xffff); - addData(rawMessage); + addAsyncData(rawMessage); } +#endif for (int i = 0; i < config.maxPlayers - 1; i++) { u32 confirmationData = sessionState.lastPacketIdFromClients[1 + i]; u16 header = buildConfirmationHeader(1 + i, confirmationData); u32 rawMessage = buildU32(header, confirmationData & 0xffff); - addData(rawMessage); + addAsyncData(rawMessage); } } else { u32 confirmationData = sessionState.lastPacketIdFromServer; u16 header = buildConfirmationHeader(sessionState.currentPlayerId, confirmationData); u32 rawMessage = buildU32(header, confirmationData & 0xffff); - addData(rawMessage); + addAsyncData(rawMessage); } } @@ -1180,9 +1367,9 @@ class LinkWireless { u32 min = 0xffffffff; for (int i = 0; i < config.maxPlayers - 1; i++) { - u32 confirmationData = sessionState.lastConfirmationFromClients[1 + i]; - if (confirmationData > 0 && confirmationData < min) - min = confirmationData; + u32 _confirmationData = sessionState.lastConfirmationFromClients[1 + i]; + if (_confirmationData > 0 && _confirmationData < min) + min = _confirmationData; } if (min < 0xffffffff) removeConfirmedMessages(min); @@ -1199,7 +1386,7 @@ class LinkWireless { // confirmation messages "repurpose" some message header fields: // packetId => high 6 bits of confirmation // data => low 16 bits of confirmation - u8 highPart = (confirmationData >> 16) & LINK_WIRELESS_PACKET_ID_MASK; + u8 highPart = (confirmationData >> 16) & PACKET_ID_MASK; u16 lowPart = confirmationData & 0xffff; return buildMessageHeader(playerId, highPart, buildChecksum(lowPart), true); } @@ -1209,10 +1396,14 @@ class LinkWireless { u8 dataChecksum, bool isConfirmation = false) { // (irq only) MessageHeader header; - header.partialPacketId = packetId % LINK_WIRELESS_MAX_PACKET_IDS; + header.partialPacketId = packetId % MAX_PACKET_IDS; header.isConfirmation = isConfirmation; header.playerId = playerId; +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + header.quickData = QUICK_SEND; +#else header.clientCount = sessionState.playerCount - LINK_WIRELESS_MIN_PLAYERS; +#endif header.dataChecksum = dataChecksum; MessageHeaderSerializer serializer; @@ -1225,21 +1416,25 @@ class LinkWireless { return __builtin_popcount(data) % 16; } +#ifndef LINK_WIRELESS_TWO_PLAYERS_ONLY void trackRemoteTimeouts() { // (irq only) - for (u32 i = 0; i < sessionState.playerCount; i++) - if (i != sessionState.currentPlayerId) - sessionState.timeouts[i]++; + for (u32 i = 0; i < sessionState.playerCount; i++) { + if (i != sessionState.currentPlayerId && !sessionState.msgFlags[i]) + sessionState.msgTimeouts[i]++; + sessionState.msgFlags[i] = false; + } } bool checkRemoteTimeouts() { // (irq only) for (u32 i = 0; i < sessionState.playerCount; i++) { if ((i == 0 || state == SERVING) && - sessionState.timeouts[i] > config.remoteTimeout) + sessionState.msgTimeouts[i] > config.timeout) return false; } return true; } +#endif u32 getDeviceTransferLength() { // (irq only) return state == SERVING ? LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH @@ -1247,25 +1442,25 @@ class LinkWireless { } void copyOutgoingState() { // (irq only) - if (isAddingMessage) + if (sessionState.newOutgoingMessages.isWriting()) return; - while (!sessionState.tmpMessagesToSend.isEmpty()) { + while (!sessionState.newOutgoingMessages.isEmpty()) { if (!_canSend()) break; - auto message = sessionState.tmpMessagesToSend.pop(); + auto message = sessionState.newOutgoingMessages.pop(); message.packetId = newPacketId(); sessionState.outgoingMessages.push(message); } } void copyIncomingState() { // (irq only) - if (isReadingMessages) + if (sessionState.newIncomingMessages.isReading()) return; - while (!sessionState.tmpMessagesToReceive.isEmpty()) { - auto message = sessionState.tmpMessagesToReceive.pop(); + while (!sessionState.newIncomingMessages.isEmpty()) { + auto message = sessionState.newIncomingMessages.pop(); sessionState.incomingMessages.push(message); } } @@ -1281,15 +1476,11 @@ class LinkWireless { nextCommandDataSize++; } - void startACKTimer() { - REG_TM[config.asyncACKTimerId].start = -1; - REG_TM[config.asyncACKTimerId].cnt = - TM_ENABLE | TM_IRQ | LINK_WIRELESS_BASE_FREQUENCY; - } - - void stopACKTimer() { - REG_TM[config.asyncACKTimerId].cnt = - REG_TM[config.asyncACKTimerId].cnt & (~TM_ENABLE); + void addAsyncData(u32 value, bool start = false) { + if (start) + nextAsyncCommandDataSize = 0; + nextAsyncCommandData[nextAsyncCommandDataSize] = value; + nextAsyncCommandDataSize++; } void copyName(char* target, const char* source, u32 length) { @@ -1324,17 +1515,37 @@ class LinkWireless { } bool reset() { + bool wasEnabled = isEnabled; + + LINK_WIRELESS_BARRIER; + isEnabled = false; + LINK_WIRELESS_BARRIER; + resetState(); stop(); - return start(); + bool success = start(); + + if (!success) + stop(); + + LINK_WIRELESS_BARRIER; + isEnabled = wasEnabled; + LINK_WIRELESS_BARRIER; + + return success; } void resetState() { this->state = NEEDS_RESET; + this->asyncCommand.isActive = false; +#ifdef LINK_WIRELESS_TWO_PLAYERS_ONLY + QUICK_SEND = 0; + QUICK_RECEIVE = 0; +#endif this->sessionState.playerCount = 1; this->sessionState.currentPlayerId = 0; + this->sessionState.recvFlag = false; this->sessionState.recvTimeout = 0; - this->sessionState.frameRecvCount = 0; this->sessionState.acceptCalled = false; this->sessionState.pingSent = false; #ifdef LINK_WIRELESS_USE_SEND_RECEIVE_LATCH @@ -1346,29 +1557,25 @@ class LinkWireless { this->sessionState.lastPacketIdFromServer = 0; this->sessionState.lastConfirmationFromServer = 0; for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++) { - this->sessionState.timeouts[i] = 0; + this->sessionState.msgTimeouts[i] = 0; + this->sessionState.msgFlags[i] = 0; this->sessionState.lastPacketIdFromClients[i] = 0; this->sessionState.lastConfirmationFromClients[i] = 0; } - this->asyncCommand.isActive = false; this->nextCommandDataSize = 0; + this->nextAsyncCommandDataSize = 0; - if (!isReadingMessages) - this->sessionState.incomingMessages.clear(); + this->sessionState.incomingMessages.syncClear(); this->sessionState.outgoingMessages.clear(); - this->sessionState.tmpMessagesToReceive.clear(); - if (!isAddingMessage) - this->sessionState.tmpMessagesToSend.clear(); - else - isPendingClearActive = true; + this->sessionState.newIncomingMessages.clear(); + this->sessionState.newOutgoingMessages.syncClear(); + + isSendingSyncCommand = false; } void stop() { stopTimer(); - if (usesAsyncACK()) - stopACKTimer(); - linkSPI->deactivate(); } @@ -1381,9 +1588,9 @@ class LinkWireless { if (!login()) return false; - wait(LINK_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); - if (!sendCommand(LINK_WIRELESS_COMMAND_HELLO).success) + if (!sendCommand(COMMAND_HELLO).success) return false; if (!setup()) @@ -1396,33 +1603,32 @@ class LinkWireless { } void stopTimer() { - REG_TM[config.sendTimerId].cnt = - REG_TM[config.sendTimerId].cnt & (~TM_ENABLE); + Link::_REG_TM[config.sendTimerId].cnt = + Link::_REG_TM[config.sendTimerId].cnt & (~Link::_TM_ENABLE); } void startTimer() { - REG_TM[config.sendTimerId].start = -config.interval; - REG_TM[config.sendTimerId].cnt = - TM_ENABLE | TM_IRQ | LINK_WIRELESS_BASE_FREQUENCY; + Link::_REG_TM[config.sendTimerId].start = -config.interval; + Link::_REG_TM[config.sendTimerId].cnt = + Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY; } void pingAdapter() { linkGPIO->setMode(LinkGPIO::Pin::SO, LinkGPIO::Direction::OUTPUT); linkGPIO->setMode(LinkGPIO::Pin::SD, LinkGPIO::Direction::OUTPUT); linkGPIO->writePin(LinkGPIO::SD, true); - wait(LINK_WIRELESS_PING_WAIT); + wait(PING_WAIT); linkGPIO->writePin(LinkGPIO::SD, false); } bool login() { LoginMemory memory; - if (!exchangeLoginPacket(LINK_WIRELESS_LOGIN_PARTS[0], 0, memory)) + if (!exchangeLoginPacket(LOGIN_PARTS[0], 0, memory)) return false; - for (u32 i = 0; i < LINK_WIRELESS_LOGIN_STEPS; i++) { - if (!exchangeLoginPacket(LINK_WIRELESS_LOGIN_PARTS[i], - LINK_WIRELESS_LOGIN_PARTS[i], memory)) + for (u32 i = 0; i < LOGIN_STEPS; i++) { + if (!exchangeLoginPacket(LOGIN_PARTS[i], LOGIN_PARTS[i], memory)) return false; } @@ -1446,74 +1652,83 @@ class LinkWireless { } bool setup(u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS) { - addData(LINK_WIRELESS_SETUP_MAGIC | - (((LINK_WIRELESS_MAX_PLAYERS - maxPlayers) & 0b11) - << LINK_WIRELESS_SETUP_MAX_PLAYERS_BIT), + addData(SETUP_MAGIC | (((LINK_WIRELESS_MAX_PLAYERS - maxPlayers) & 0b11) + << SETUP_MAX_PLAYERS_BIT), true); - return sendCommand(LINK_WIRELESS_COMMAND_SETUP, true).success; + return sendCommand(COMMAND_SETUP, true).success; } CommandResult sendCommand(u8 type, bool withData = false) { CommandResult result; u32 command = buildCommand(type, withData ? (u16)nextCommandDataSize : 0); - if (transfer(command) != LINK_WIRELESS_DATA_REQUEST) + if (transfer(command) != DATA_REQUEST_VALUE) { + isSendingSyncCommand = false; return result; + } if (withData) { for (u32 i = 0; i < nextCommandDataSize; i++) { - if (transfer(nextCommandData[i]) != LINK_WIRELESS_DATA_REQUEST) + if (transfer(nextCommandData[i]) != DATA_REQUEST_VALUE) { + isSendingSyncCommand = false; return result; + } } } - u32 response = transfer(LINK_WIRELESS_DATA_REQUEST); + u32 response = transfer(DATA_REQUEST_VALUE); u16 header = msB32(response); u16 data = lsB32(response); u8 responses = msB16(data); u8 ack = lsB16(data); - if (header != LINK_WIRELESS_COMMAND_HEADER || - ack != type + LINK_WIRELESS_RESPONSE_ACK || - responses > LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH) + if (header != COMMAND_HEADER_VALUE || ack != type + RESPONSE_ACK_VALUE || + responses > LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH) { + isSendingSyncCommand = false; return result; + } for (u32 i = 0; i < responses; i++) - result.responses[i] = transfer(LINK_WIRELESS_DATA_REQUEST); + result.responses[i] = transfer(DATA_REQUEST_VALUE); result.responsesSize = responses; result.success = true; + + LINK_WIRELESS_BARRIER; + isSendingSyncCommand = false; + LINK_WIRELESS_BARRIER; + return result; } - void sendCommandAsync(u8 type, bool withData = false) { // (irq only) - if (asyncCommand.isActive) - return; + bool sendCommandAsync(u8 type, bool withData = false) { // (irq only) + if (asyncCommand.isActive || isSendingSyncCommand) + return false; asyncCommand.type = type; if (withData) { - for (u32 i = 0; i < nextCommandDataSize; i++) - asyncCommand.parameters[i] = nextCommandData[i]; + for (u32 i = 0; i < nextAsyncCommandDataSize; i++) + asyncCommand.parameters[i] = nextAsyncCommandData[i]; } asyncCommand.result.success = false; asyncCommand.state = AsyncCommand::State::PENDING; asyncCommand.step = AsyncCommand::Step::COMMAND_HEADER; asyncCommand.sentParameters = 0; - asyncCommand.totalParameters = withData ? nextCommandDataSize : 0; + asyncCommand.totalParameters = withData ? nextAsyncCommandDataSize : 0; asyncCommand.receivedResponses = 0; asyncCommand.totalResponses = 0; - asyncCommand.pendingData = 0; - asyncCommand.ackStep = AsyncCommand::ACKStep::READY; asyncCommand.isActive = true; u32 command = buildCommand(type, asyncCommand.totalParameters); transferAsync(command); + + return true; } void updateAsyncCommand(u32 newData) { // (irq only) switch (asyncCommand.step) { case AsyncCommand::Step::COMMAND_HEADER: { - if (newData != LINK_WIRELESS_DATA_REQUEST) { + if (newData != DATA_REQUEST_VALUE) { asyncCommand.state = AsyncCommand::State::COMPLETED; return; } @@ -1522,7 +1737,7 @@ class LinkWireless { break; } case AsyncCommand::Step::COMMAND_PARAMETERS: { - if (newData != LINK_WIRELESS_DATA_REQUEST) { + if (newData != DATA_REQUEST_VALUE) { asyncCommand.state = AsyncCommand::State::COMPLETED; return; } @@ -1536,8 +1751,8 @@ class LinkWireless { u8 responses = msB16(data); u8 ack = lsB16(data); - if (header != LINK_WIRELESS_COMMAND_HEADER || - ack != asyncCommand.type + LINK_WIRELESS_RESPONSE_ACK || + if (header != COMMAND_HEADER_VALUE || + ack != asyncCommand.type + RESPONSE_ACK_VALUE || responses > LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH) { asyncCommand.state = AsyncCommand::State::COMPLETED; return; @@ -1556,6 +1771,8 @@ class LinkWireless { receiveAsyncCommandResponseOrFinish(); break; } + default: { + } } } @@ -1566,14 +1783,14 @@ class LinkWireless { asyncCommand.sentParameters++; } else { asyncCommand.step = AsyncCommand::Step::RESPONSE_REQUEST; - transferAsync(LINK_WIRELESS_DATA_REQUEST); + transferAsync(DATA_REQUEST_VALUE); } } void receiveAsyncCommandResponseOrFinish() { // (irq only) if (asyncCommand.receivedResponses < asyncCommand.totalResponses) { asyncCommand.step = AsyncCommand::Step::DATA_REQUEST; - transferAsync(LINK_WIRELESS_DATA_REQUEST); + transferAsync(DATA_REQUEST_VALUE); } else { asyncCommand.result.success = true; asyncCommand.state = AsyncCommand::State::COMPLETED; @@ -1581,33 +1798,38 @@ class LinkWireless { } u32 buildCommand(u8 type, u8 length = 0) { - return buildU32(LINK_WIRELESS_COMMAND_HEADER, buildU16(length, type)); + return buildU32(COMMAND_HEADER_VALUE, buildU16(length, type)); } - void transferAsync(u32 data) { - linkSPI->transfer( - data, []() { return false; }, true, true); + void transferAsync(u32 data) { // (irq only) +#ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM +#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ + Link::_REG_IME = 0; +#endif +#endif + + linkSPI->transfer(data, []() { return false; }, true, true); } u32 transfer(u32 data, bool customAck = true) { if (!customAck) - wait(LINK_WIRELESS_TRANSFER_WAIT); + wait(TRANSFER_WAIT); u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; u32 receivedData = linkSPI->transfer( data, [this, &lines, &vCount]() { return cmdTimeout(lines, vCount); }, false, customAck); if (customAck && !acknowledge()) - return LINK_SPI_NO_DATA; + return LINK_SPI_NO_DATA_32; return receivedData; } bool acknowledge() { u32 lines = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; linkSPI->_setSOLow(); while (!linkSPI->_isSIHigh()) @@ -1622,16 +1844,14 @@ class LinkWireless { return true; } - bool usesAsyncACK() { return config.asyncACKTimerId > -1; } - bool cmdTimeout(u32& lines, u32& vCount) { - return timeout(LINK_WIRELESS_CMD_TIMEOUT, lines, vCount); + return timeout(CMD_TIMEOUT, lines, vCount); } bool timeout(u32 limit, u32& lines, u32& vCount) { - if (REG_VCOUNT != vCount) { - lines += max((int)REG_VCOUNT - (int)vCount, 0); - vCount = REG_VCOUNT; + if (Link::_REG_VCOUNT != vCount) { + lines += Link::_max((int)Link::_REG_VCOUNT - (int)vCount, 0); + vCount = Link::_REG_VCOUNT; } return lines > limit; @@ -1639,12 +1859,12 @@ class LinkWireless { void wait(u32 verticalLines) { u32 count = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; while (count < verticalLines) { - if (REG_VCOUNT != vCount) { + if (Link::_REG_VCOUNT != vCount) { count++; - vCount = REG_VCOUNT; + vCount = Link::_REG_VCOUNT; } }; } @@ -1652,11 +1872,11 @@ class LinkWireless { template void waitVBlanks(u32 vBlanks, F onVBlank) { u32 count = 0; - u32 vCount = REG_VCOUNT; + u32 vCount = Link::_REG_VCOUNT; while (count < vBlanks) { - if (REG_VCOUNT != vCount) { - vCount = REG_VCOUNT; + if (Link::_REG_VCOUNT != vCount) { + vCount = Link::_REG_VCOUNT; if (vCount == 160) { onVBlank(); @@ -1675,21 +1895,21 @@ class LinkWireless { #ifdef PROFILING_ENABLED void profileStart() { - REG_TM1CNT_L = 0; - REG_TM2CNT_L = 0; + Link::_REG_TM1CNT_L = 0; + Link::_REG_TM2CNT_L = 0; - REG_TM1CNT_H = 0; - REG_TM2CNT_H = 0; + Link::_REG_TM1CNT_H = 0; + Link::_REG_TM2CNT_H = 0; - REG_TM2CNT_H = TM_ENABLE | TM_CASCADE; - REG_TM1CNT_H = TM_ENABLE | TM_FREQ_1; + Link::_REG_TM2CNT_H = Link::_TM_ENABLE | Link::_TM_CASCADE; + Link::_REG_TM1CNT_H = Link::_TM_ENABLE | Link::_TM_FREQ_1; } u32 profileStop() { - REG_TM1CNT_H = 0; - REG_TM2CNT_H = 0; + Link::_REG_TM1CNT_H = 0; + Link::_REG_TM2CNT_H = 0; - return (REG_TM1CNT_L | (REG_TM2CNT_L << 16)); + return (Link::_REG_TM1CNT_L | (Link::_REG_TM2CNT_L << 16)); } public: @@ -1703,20 +1923,25 @@ class LinkWireless { extern LinkWireless* linkWireless; +/** + * @brief VBLANK interrupt handler. + */ inline void LINK_WIRELESS_ISR_VBLANK() { linkWireless->_onVBlank(); } +/** + * @brief SERIAL interrupt handler. + */ inline void LINK_WIRELESS_ISR_SERIAL() { linkWireless->_onSerial(); } +/** + * @brief TIMER interrupt handler used for sending. + */ inline void LINK_WIRELESS_ISR_TIMER() { linkWireless->_onTimer(); } -inline void LINK_WIRELESS_ISR_ACK_TIMER() { - linkWireless->_onACKTimer(); -} - #endif // LINK_WIRELESS_H diff --git a/lib/LinkWirelessMultiboot.hpp b/lib/LinkWirelessMultiboot.hpp index 6649109..ed7203e 100644 --- a/lib/LinkWirelessMultiboot.hpp +++ b/lib/LinkWirelessMultiboot.hpp @@ -1,9 +1,6 @@ #ifndef LINK_WIRELESS_MULTIBOOT_H #define LINK_WIRELESS_MULTIBOOT_H -#pragma GCC push_options -#pragma GCC optimize("O2") - // -------------------------------------------------------------------------- // A Wireless Multiboot tool to send small ROMs from a GBA to up to 4 slaves. // -------------------------------------------------------------------------- @@ -15,9 +12,9 @@ // LinkWirelessMultiboot::Result result = linkWirelessMultiboot->sendRom( // romBytes, // for current ROM, use: ((const u8*)MEM_EWRAM) // romLength, // in bytes -// Multiboot", // game name +// "Multiboot", // game name // "Test", // user name -// 0xffff, // game id +// 0xffff, // game ID // 2, // number of players // [](LinkWirelessMultiboot::MultibootProgress progress) { // // check progress.[state,connectedClients,percentage] @@ -30,52 +27,80 @@ // // `result` should be LinkWirelessMultiboot::Result::SUCCESS // -------------------------------------------------------------------------- -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + #include "LinkRawWireless.hpp" #include "LinkWirelessOpenSDK.hpp" -// Enable logging (set `linkWirelessMultiboot->logger` and uncomment to enable) +#ifndef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING +/** + * @brief Enable logging. + * \warning Set `linkWirelessMultiboot->logger` and uncomment to enable! + */ // #define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING - -#ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING -#include -#define LWMLOG(str) logger(str) -#else -#define LWMLOG(str) #endif +static volatile char LINK_WIRELESS_MULTIBOOT_VERSION[] = + "LinkWirelessMultiboot/v7.0.0"; + #define LINK_WIRELESS_MULTIBOOT_MIN_ROM_SIZE (0x100 + 0xc0) #define LINK_WIRELESS_MULTIBOOT_MAX_ROM_SIZE (256 * 1024) #define LINK_WIRELESS_MULTIBOOT_MIN_PLAYERS 2 #define LINK_WIRELESS_MULTIBOOT_MAX_PLAYERS 5 -#define LINK_WIRELESS_MULTIBOOT_HEADER_SIZE 0xC0 -#define LINK_WIRELESS_MULTIBOOT_SETUP_MAGIC 0x003c0000 -#define LINK_WIRELESS_MULTIBOOT_SETUP_TX 1 -#define LINK_WIRELESS_MULTIBOOT_SETUP_WAIT_TIMEOUT 32 -#define LINK_WIRELESS_MULTIBOOT_GAME_ID_MULTIBOOT_FLAG (1 << 15) -#define LINK_WIRELESS_MULTIBOOT_FRAME_LINES 228 -#define LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS 4 +#define LINK_WIRELESS_MULTIBOOT_BARRIER asm volatile("" ::: "memory") #define LINK_WIRELESS_MULTIBOOT_TRY(CALL) \ + LINK_WIRELESS_MULTIBOOT_BARRIER; \ if ((lastResult = CALL) != SUCCESS) { \ return finish(lastResult); \ } -const u8 LINK_WIRELESS_MULTIBOOT_CMD_START[] = {0x00, 0x54, 0x00, 0x00, - 0x00, 0x02, 0x00}; -const u8 LINK_WIRELESS_MULTIBOOT_CMD_START_SIZE = 7; -const u8 LINK_WIRELESS_MULTIBOOT_BOOTLOADER_HANDSHAKE[][6] = { - {0x00, 0x00, 0x52, 0x46, 0x55, 0x2d}, - {0x4d, 0x42, 0x2d, 0x44, 0x4c, 0x00}}; -const u8 LINK_WIRELESS_MULTIBOOT_BOOTLOADER_HANDSHAKE_SIZE = 6; -const u8 LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH[] = { - 0x52, 0x46, 0x55, 0x2d, 0x4d, 0x42, 0x4f, 0x4f, 0x54, 0x00, 0x00, 0x00}; -const u8 LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH_OFFSET = 4; -const u8 LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH_SIZE = 12; - -static volatile char LINK_WIRELESS_MULTIBOOT_VERSION[] = - "LinkWirelessMultiboot/v6.3.0"; +#ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING +#include +#define _LWMLOG_(str) logger(str) +#else +#define _LWMLOG_(str) +#endif +/** + * @brief A Wireless Multiboot tool to send small ROMs from a GBA to up to 4 + * slaves. + */ class LinkWirelessMultiboot { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + using CommState = LinkWirelessOpenSDK::CommState; + using Sequence = LinkWirelessOpenSDK::SequenceNumber; + using ClientHeader = LinkWirelessOpenSDK::ClientSDKHeader; + using ClientPacket = LinkWirelessOpenSDK::ClientPacket; + using ChildrenData = LinkWirelessOpenSDK::ChildrenData; + using SendBuffer = + LinkWirelessOpenSDK::SendBuffer; + + static constexpr int HEADER_SIZE = 0xC0; + static constexpr int SETUP_MAGIC = 0x003c0000; + static constexpr int SETUP_TX = 1; + static constexpr int SETUP_WAIT_TIMEOUT = 32; + static constexpr int GAME_ID_MULTIBOOT_FLAG = (1 << 15); + static constexpr int FRAME_LINES = 228; + static constexpr int MAX_INFLIGHT_PACKETS = 4; + static constexpr u8 CMD_START[] = {0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x00}; + static constexpr u8 CMD_START_SIZE = 7; + static constexpr u8 BOOTLOADER_HANDSHAKE[][6] = { + {0x00, 0x00, 0x52, 0x46, 0x55, 0x2d}, + {0x4d, 0x42, 0x2d, 0x44, 0x4c, 0x00}}; + static constexpr u8 BOOTLOADER_HANDSHAKE_SIZE = 6; + static constexpr u8 ROM_HEADER_PATCH[] = {0x52, 0x46, 0x55, 0x2d, 0x4d, 0x42, + 0x4f, 0x4f, 0x54, 0x00, 0x00, 0x00}; + static constexpr u8 ROM_HEADER_PATCH_OFFSET = 4; + static constexpr u8 ROM_HEADER_PATCH_SIZE = 12; + public: #ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING typedef void (*Logger)(std::string); @@ -89,6 +114,7 @@ class LinkWirelessMultiboot { CANCELED, ADAPTER_NOT_DETECTED, BAD_HANDSHAKE, + CLIENT_DISCONNECTED, FAILURE }; enum State { STOPPED, INITIALIZING, WAITING, PREPARING, SENDING, CONFIRMING }; @@ -99,14 +125,31 @@ class LinkWirelessMultiboot { u32 percentage = 0; }; + /** + * @brief Sends the `rom`. Once completed, the return value should be + * `LinkWirelessMultiboot::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`. It's recommended to use a ROM size that is a multiple + * of `16`, as this also ensures compatibility with Multiboot via Link Cable. + * @param gameName Game name. Maximum `14` characters + null terminator. + * @param userName User name. Maximum `8` characters + null terminator. + * @param gameId `(0 ~ 0x7FFF)` Game ID. + * @param players The exact number of consoles that will download the ROM. + * Once this number of players is reached, the code will start transmitting + * the ROM bytes. + * @param cancel A function that will be continuously invoked. If it returns + * `true`, the transfer will be aborted. + * \warning Blocks the system until completion or cancellation. + */ template - Result sendRom(const u8* rom, - u32 romSize, - const char* gameName, - const char* userName, - const u16 gameId, - u8 players, - C cancel) { + __attribute__((noinline)) Result sendRom(const u8* rom, + u32 romSize, + const char* gameName, + const char* userName, + const u16 gameId, + u8 players, + C cancel) { if (romSize < LINK_WIRELESS_MULTIBOOT_MIN_ROM_SIZE) return INVALID_SIZE; if (romSize > LINK_WIRELESS_MULTIBOOT_MAX_ROM_SIZE) @@ -115,30 +158,31 @@ class LinkWirelessMultiboot { players > LINK_WIRELESS_MULTIBOOT_MAX_PLAYERS) return INVALID_PLAYERS; - LWMLOG("starting..."); + _LWMLOG_("starting..."); LINK_WIRELESS_MULTIBOOT_TRY(activate()) progress.state = INITIALIZING; LINK_WIRELESS_MULTIBOOT_TRY(initialize(gameName, userName, gameId, players)) - LWMLOG("waiting for connections..."); + _LWMLOG_("waiting for connections..."); progress.state = WAITING; LINK_WIRELESS_MULTIBOOT_TRY(waitForClients(players, cancel)) - LWMLOG("all players are connected"); + _LWMLOG_("all players are connected"); progress.state = PREPARING; - linkRawWireless->wait(LINK_WIRELESS_MULTIBOOT_FRAME_LINES); + linkRawWireless->wait(FRAME_LINES); - LWMLOG("rom start command..."); + _LWMLOG_("rom start command..."); LINK_WIRELESS_MULTIBOOT_TRY(sendRomStartCommand(cancel)) - LWMLOG("SENDING ROM!"); + _LWMLOG_("SENDING ROM!"); progress.state = SENDING; LINK_WIRELESS_MULTIBOOT_TRY(sendRomBytes(rom, romSize, cancel)) + linkRawWireless->wait(FRAME_LINES * 10); progress.state = CONFIRMING; LINK_WIRELESS_MULTIBOOT_TRY(confirm(cancel)) - LWMLOG("SUCCESS!"); + _LWMLOG_("SUCCESS!"); return finish(SUCCESS); } @@ -158,41 +202,40 @@ class LinkWirelessMultiboot { }; struct PendingTransferList { - std::array - transfers; + std::array transfers; - PendingTransfer* max(bool ack = false) { + __attribute__((noinline)) PendingTransfer* max(bool ack = false) { int maxCursor = -1; int maxI = -1; - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) { + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) { if (transfers[i].isActive && (int)transfers[i].cursor > maxCursor && (!ack || transfers[i].ack)) { maxCursor = transfers[i].cursor; maxI = i; } } - return maxI > -1 ? &transfers[maxI] : NULL; + return maxI > -1 ? &transfers[maxI] : nullptr; } - PendingTransfer* minWithoutAck() { + __attribute__((noinline)) PendingTransfer* minWithoutAck() { u32 minCursor = 0xffffffff; int minI = -1; - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) { + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) { if (transfers[i].isActive && transfers[i].cursor < minCursor && !transfers[i].ack) { minCursor = transfers[i].cursor; minI = i; } } - return minI > -1 ? &transfers[minI] : NULL; + return minI > -1 ? &transfers[minI] : nullptr; } - void addIfNeeded(u32 newCursor) { + __attribute__((noinline)) void addIfNeeded(u32 newCursor) { auto maxTransfer = max(); - if (maxTransfer != NULL && newCursor <= maxTransfer->cursor) + if (maxTransfer != nullptr && newCursor <= maxTransfer->cursor) return; - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) { + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) { if (!transfers[i].isActive) { transfers[i].cursor = newCursor; transfers[i].ack = false; @@ -202,7 +245,7 @@ class LinkWirelessMultiboot { } } - int ack(LinkWirelessOpenSDK::SequenceNumber sequence) { + __attribute__((noinline)) int ack(Sequence sequence) { int index = findIndex(sequence); if (index == -1) return -1; @@ -210,8 +253,8 @@ class LinkWirelessMultiboot { transfers[index].ack = true; auto maxAckTransfer = max(true); - bool canUpdateCursor = - maxAckTransfer != NULL && isAckCompleteUpTo(maxAckTransfer->cursor); + bool canUpdateCursor = maxAckTransfer != nullptr && + isAckCompleteUpTo(maxAckTransfer->cursor); if (canUpdateCursor) cleanup(); @@ -219,39 +262,38 @@ class LinkWirelessMultiboot { return canUpdateCursor ? maxAckTransfer->cursor + 1 : -1; } - void cleanup() { - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) { + __attribute__((noinline)) void cleanup() { + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) { if (transfers[i].isActive && transfers[i].ack) transfers[i].isActive = false; } } - bool isFull() { - return size() == LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; + __attribute__((noinline)) bool isFull() { + return size() == MAX_INFLIGHT_PACKETS; } - u32 size() { + __attribute__((noinline)) u32 size() { u32 size = 0; - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) if (transfers[i].isActive) size++; return size; } private: - bool isAckCompleteUpTo(u32 cursor) { - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) + __attribute__((noinline)) bool isAckCompleteUpTo(u32 cursor) { + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) if (transfers[i].isActive && !transfers[i].ack && transfers[i].cursor < cursor) return false; return true; } - int findIndex(LinkWirelessOpenSDK::SequenceNumber sequence) { - for (u32 i = 0; i < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS; i++) { + __attribute__((noinline)) int findIndex(Sequence sequence) { + for (u32 i = 0; i < MAX_INFLIGHT_PACKETS; i++) { if (transfers[i].isActive && - LinkWirelessOpenSDK::SequenceNumber::fromPacketId( - transfers[i].cursor) == sequence) { + Sequence::fromPacketId(transfers[i].cursor) == sequence) { return i; } } @@ -264,42 +306,45 @@ class LinkWirelessMultiboot { u32 cursor = 0; PendingTransferList pendingTransferList; - u32 nextCursor(bool canSendInflightPackets) { + __attribute__((noinline)) u32 nextCursor(bool canSendInflightPackets) { u32 pendingCount = pendingTransferList.size(); if (canSendInflightPackets && pendingCount > 0 && - pendingCount < LINK_WIRELESS_MULTIBOOT_MAX_INFLIGHT_PACKETS) { + pendingCount < MAX_INFLIGHT_PACKETS) { return pendingTransferList.max()->cursor + 1; } else { auto minWithoutAck = pendingTransferList.minWithoutAck(); - return minWithoutAck != NULL ? minWithoutAck->cursor : cursor; + return minWithoutAck != nullptr ? minWithoutAck->cursor : cursor; } } - void addIfNeeded(u32 newCursor) { + __attribute__((noinline)) void addIfNeeded(u32 newCursor) { if (newCursor >= cursor) pendingTransferList.addIfNeeded(newCursor); } - u32 transferred() { - return cursor * LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER; + __attribute__((noinline)) u32 transferred() { + return cursor * LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER; } - LinkWirelessOpenSDK::SequenceNumber sequence() { - return LinkWirelessOpenSDK::SequenceNumber::fromPacketId(cursor); + __attribute__((noinline)) Sequence sequence() { + return Sequence::fromPacketId(cursor); } }; MultibootProgress progress; - Result lastResult; - LinkWirelessOpenSDK::ClientSDKHeader lastValidHeader; + volatile Result lastResult; + ClientHeader lastValidHeader; + + using TransferArray = + std::array; __attribute__((noinline)) Result activate() { if (!linkRawWireless->activate()) { - LWMLOG("! adapter not detected"); + _LWMLOG_("! adapter not detected"); return ADAPTER_NOT_DETECTED; } - LWMLOG("activated"); + _LWMLOG_("activated"); return SUCCESS; } @@ -308,27 +353,25 @@ class LinkWirelessMultiboot { const char* userName, const u16 gameId, u8 players) { - if (!linkRawWireless->setup(players, LINK_WIRELESS_MULTIBOOT_SETUP_TX, - LINK_WIRELESS_MULTIBOOT_SETUP_WAIT_TIMEOUT, - LINK_WIRELESS_MULTIBOOT_SETUP_MAGIC)) { - LWMLOG("! setup failed"); + if (!linkRawWireless->setup(players, SETUP_TX, SETUP_WAIT_TIMEOUT, + SETUP_MAGIC)) { + _LWMLOG_("! setup failed"); return FAILURE; } - LWMLOG("setup ok"); + _LWMLOG_("setup ok"); - if (!linkRawWireless->broadcast( - gameName, userName, - gameId | LINK_WIRELESS_MULTIBOOT_GAME_ID_MULTIBOOT_FLAG)) { - LWMLOG("! broadcast failed"); + if (!linkRawWireless->broadcast(gameName, userName, + gameId | GAME_ID_MULTIBOOT_FLAG)) { + _LWMLOG_("! broadcast failed"); return FAILURE; } - LWMLOG("broadcast data set"); + _LWMLOG_("broadcast data set"); if (!linkRawWireless->startHost()) { - LWMLOG("! start host failed"); + _LWMLOG_("! start host failed"); return FAILURE; } - LWMLOG("host started"); + _LWMLOG_("host started"); return SUCCESS; } @@ -360,38 +403,36 @@ class LinkWirelessMultiboot { } template - Result handshakeClient(u8 clientNumber, C cancel) { - LinkWirelessOpenSDK::ClientPacket handshakePackets[2]; - bool hasReceivedName = false; + __attribute__((noinline)) Result handshakeClient(u8 clientNumber, C cancel) { + ClientPacket handshakePackets[2]; + volatile bool hasReceivedName = false; - LWMLOG("new client: " + std::to_string(clientNumber)); + _LWMLOG_("new client: " + std::to_string(clientNumber)); LINK_WIRELESS_MULTIBOOT_TRY(exchangeData( clientNumber, [this](LinkRawWireless::ReceiveDataResponse& response) { return sendAndExpectData(toArray(), 0, 1, response); }, - [](LinkWirelessOpenSDK::ClientPacket packet) { return true; }, cancel)) + [](ClientPacket packet) { return true; }, cancel)) // (initial client packet received) - LWMLOG("handshake (1/2)..."); + _LWMLOG_("handshake (1/2)..."); LINK_WIRELESS_MULTIBOOT_TRY(exchangeACKData( clientNumber, - [](LinkWirelessOpenSDK::ClientPacket packet) { + [](ClientPacket packet) { auto header = packet.header; - return header.n == 2 && - header.commState == LinkWirelessOpenSDK::CommState::STARTING; + return header.n == 2 && header.commState == CommState::STARTING; }, cancel)) // (n = 2, commState = 1) - LWMLOG("handshake (2/2)..."); + _LWMLOG_("handshake (2/2)..."); LINK_WIRELESS_MULTIBOOT_TRY(exchangeACKData( clientNumber, - [&handshakePackets](LinkWirelessOpenSDK::ClientPacket packet) { + [&handshakePackets](ClientPacket packet) { auto header = packet.header; - bool isValid = - header.n == 1 && header.phase == 0 && - header.commState == LinkWirelessOpenSDK::CommState::COMMUNICATING; + bool isValid = header.n == 1 && header.phase == 0 && + header.commState == CommState::COMMUNICATING; if (isValid) handshakePackets[0] = packet; return isValid; @@ -399,40 +440,37 @@ class LinkWirelessMultiboot { cancel)) // (n = 1, commState = 2) - LWMLOG("receiving name..."); + _LWMLOG_("receiving name..."); LINK_WIRELESS_MULTIBOOT_TRY(exchangeACKData( clientNumber, - [this, &handshakePackets, - &hasReceivedName](LinkWirelessOpenSDK::ClientPacket packet) { + [this, &handshakePackets, &hasReceivedName](ClientPacket packet) { auto header = packet.header; lastValidHeader = header; if (header.n == 1 && header.phase == 1 && - header.commState == - LinkWirelessOpenSDK::CommState::COMMUNICATING) { + header.commState == CommState::COMMUNICATING) { handshakePackets[1] = packet; hasReceivedName = true; } - return header.commState == LinkWirelessOpenSDK::CommState::OFF; + return header.commState == CommState::OFF; }, cancel)) // (commState = 0) - LWMLOG("validating name..."); + _LWMLOG_("validating name..."); for (u32 i = 0; i < 2; i++) { auto receivedPayload = handshakePackets[i].payload; - auto expectedPayload = LINK_WIRELESS_MULTIBOOT_BOOTLOADER_HANDSHAKE[i]; + auto expectedPayload = BOOTLOADER_HANDSHAKE[i]; - for (u32 j = 0; j < LINK_WIRELESS_MULTIBOOT_BOOTLOADER_HANDSHAKE_SIZE; - j++) { + for (u32 j = 0; j < BOOTLOADER_HANDSHAKE_SIZE; j++) { if (!hasReceivedName || receivedPayload[j] != expectedPayload[j]) { - LWMLOG("! bad payload"); + _LWMLOG_("! bad payload"); return finish(BAD_HANDSHAKE); } } } - LWMLOG("draining queue..."); - bool hasFinished = false; + _LWMLOG_("draining queue..."); + volatile bool hasFinished = false; while (!hasFinished) { if (cancel(progress)) return finish(CANCELED); @@ -444,7 +482,7 @@ class LinkWirelessMultiboot { } // (no more client packets) - LWMLOG("client " + std::to_string(clientNumber) + " accepted"); + _LWMLOG_("client " + std::to_string(clientNumber) + " accepted"); return SUCCESS; } @@ -455,9 +493,7 @@ class LinkWirelessMultiboot { LINK_WIRELESS_MULTIBOOT_TRY(exchangeNewData( i, linkWirelessOpenSDK->createServerBuffer( - LINK_WIRELESS_MULTIBOOT_CMD_START, - LINK_WIRELESS_MULTIBOOT_CMD_START_SIZE, - {1, 0, LinkWirelessOpenSDK::CommState::STARTING}, 1 << i), + CMD_START, CMD_START_SIZE, {1, 0, CommState::STARTING}, 1 << i), cancel)) } @@ -468,16 +504,14 @@ class LinkWirelessMultiboot { __attribute__((noinline)) Result sendRomBytes(const u8* rom, u32 romSize, C cancel) { - LinkWirelessOpenSDK::ChildrenData childrenData; - std::array transfers; - u8 firstPagePatch[LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER]; - for (u32 i = 0; i < LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER; i++) { + ChildrenData childrenData; + TransferArray transfers; + u8 firstPagePatch[LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER]; + for (u32 i = 0; i < LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER; i++) { firstPagePatch[i] = - i >= LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH_OFFSET && - i < LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH_OFFSET + - LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH_SIZE - ? LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH - [i - LINK_WIRELESS_MULTIBOOT_ROM_HEADER_PATCH_OFFSET] + i >= ROM_HEADER_PATCH_OFFSET && + i < ROM_HEADER_PATCH_OFFSET + ROM_HEADER_PATCH_SIZE + ? ROM_HEADER_PATCH[i - ROM_HEADER_PATCH_OFFSET] : rom[i]; } @@ -489,9 +523,11 @@ class LinkWirelessMultiboot { if (cancel(progress)) return finish(CANCELED); + LINK_WIRELESS_MULTIBOOT_TRY(ensureAllClientsAreStillAlive()) + u32 cursor = findMinCursor(transfers); - u32 offset = cursor * LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER; - auto sequence = LinkWirelessOpenSDK::SequenceNumber::fromPacketId(cursor); + u32 offset = cursor * LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER; + auto sequence = Sequence::fromPacketId(cursor); const u8* bufferToSend = cursor == 0 ? (const u8*)firstPagePatch : rom; auto sendBuffer = linkWirelessOpenSDK->createServerBuffer( @@ -517,21 +553,19 @@ class LinkWirelessMultiboot { } } - u32 newPercentage = - min(transfers[findMinClient(transfers)].transferred() * 100 / romSize, - 100); + u32 newPercentage = Link::_min( + transfers[findMinClient(transfers)].transferred() * 100 / romSize, + 100); if (newPercentage != progress.percentage) { progress.percentage = newPercentage; - LWMLOG("-> " + std::to_string(newPercentage)); + _LWMLOG_("-> " + std::to_string(newPercentage)); } } return SUCCESS; } - __attribute__((noinline)) u32 findMinClient( - std::array& - transfers) { + __attribute__((noinline)) u32 findMinClient(TransferArray& transfers) { u32 minTransferredBytes = 0xffffffff; u32 minClient = 0; @@ -546,9 +580,7 @@ class LinkWirelessMultiboot { return minClient; } - __attribute__((noinline)) u32 findMinCursor( - std::array& - transfers) { + __attribute__((noinline)) u32 findMinCursor(TransferArray& transfers) { u32 minNextCursor = 0xffffffff; bool canSendInflightPackets = true; @@ -568,39 +600,39 @@ class LinkWirelessMultiboot { template __attribute__((noinline)) Result confirm(C cancel) { - LWMLOG("confirming (1/2)..."); + _LWMLOG_("confirming (1/2)..."); for (u32 i = 0; i < progress.connectedClients; i++) { - LINK_WIRELESS_MULTIBOOT_TRY(exchangeNewData( - i, - linkWirelessOpenSDK->createServerBuffer( - {}, 0, {0, 0, LinkWirelessOpenSDK::CommState::ENDING}, 1 << i), - cancel)) + LINK_WIRELESS_MULTIBOOT_TRY( + exchangeNewData(i, + linkWirelessOpenSDK->createServerBuffer( + {}, 0, {0, 0, CommState::ENDING}, 1 << i), + cancel)) } - LWMLOG("confirming (2/2)..."); + LINK_WIRELESS_MULTIBOOT_BARRIER; + + _LWMLOG_("confirming (2/2)..."); for (u32 i = 0; i < progress.connectedClients; i++) { LinkRawWireless::ReceiveDataResponse response; - LINK_WIRELESS_MULTIBOOT_TRY(sendAndExpectData( - linkWirelessOpenSDK->createServerBuffer( - {}, 0, {1, 0, LinkWirelessOpenSDK::CommState::OFF}, 1 << i), - response)) + LINK_WIRELESS_MULTIBOOT_TRY( + sendAndExpectData(linkWirelessOpenSDK->createServerBuffer( + {}, 0, {1, 0, CommState::OFF}, 1 << i), + response)) } return SUCCESS; } template - __attribute__((noinline)) Result exchangeNewData( - u8 clientNumber, - LinkWirelessOpenSDK::SendBuffer - sendBuffer, - C cancel) { + __attribute__((noinline)) Result exchangeNewData(u8 clientNumber, + SendBuffer sendBuffer, + C cancel) { LINK_WIRELESS_MULTIBOOT_TRY(exchangeData( clientNumber, [this, &sendBuffer](LinkRawWireless::ReceiveDataResponse& response) { return sendAndExpectData(sendBuffer, response); }, - [&sendBuffer](LinkWirelessOpenSDK::ClientPacket packet) { + [&sendBuffer](ClientPacket packet) { auto header = packet.header; return header.isACK == 1 && header.sequence() == sendBuffer.header.sequence(); @@ -627,11 +659,9 @@ class LinkWirelessMultiboot { } template - __attribute__((noinline)) Result exchangeData(u8 clientNumber, - F sendAction, - V validatePacket, - C cancel) { - bool hasFinished = false; + __attribute__((noinline)) Result + exchangeData(u8 clientNumber, F sendAction, V validatePacket, C cancel) { + volatile bool hasFinished = false; while (!hasFinished) { if (cancel(progress)) return finish(CANCELED); @@ -655,10 +685,9 @@ class LinkWirelessMultiboot { return SUCCESS; } - __attribute__((noinline)) Result sendAndExpectData( - LinkWirelessOpenSDK::SendBuffer - sendBuffer, - LinkRawWireless::ReceiveDataResponse& response) { + __attribute__((noinline)) Result + sendAndExpectData(SendBuffer sendBuffer, + LinkRawWireless::ReceiveDataResponse& response) { return sendAndExpectData(sendBuffer.data, sendBuffer.dataSize, sendBuffer.totalByteCount, response); } @@ -669,21 +698,28 @@ class LinkWirelessMultiboot { u32 _bytes, LinkRawWireless::ReceiveDataResponse& response) { LinkRawWireless::RemoteCommand remoteCommand; - bool success = false; + volatile bool success = false; success = linkRawWireless->sendDataAndWait(data, dataSize, remoteCommand, _bytes); + + LINK_WIRELESS_MULTIBOOT_BARRIER; + if (!success) { - LWMLOG("! sendDataAndWait failed"); + _LWMLOG_("! sendDataAndWait failed"); return FAILURE; } + LINK_WIRELESS_MULTIBOOT_BARRIER; + if (remoteCommand.commandId != 0x28) { - LWMLOG("! expected EVENT 0x28"); - LWMLOG("! but got " + toHex(remoteCommand.commandId)); + _LWMLOG_("! expected EVENT 0x28"); + _LWMLOG_("! but got " + toHex(remoteCommand.commandId)); return FAILURE; } + LINK_WIRELESS_MULTIBOOT_BARRIER; + if (remoteCommand.paramsSize > 0) { u8 expectedActiveChildren = 0; for (u32 i = 0; i < progress.connectedClients; i++) @@ -692,22 +728,38 @@ class LinkWirelessMultiboot { (remoteCommand.params[0] >> 8) & expectedActiveChildren; if (activeChildren != expectedActiveChildren) { - LWMLOG("! client timeout [" + std::to_string(activeChildren) + "]"); - LWMLOG("! vs expected: [" + std::to_string(expectedActiveChildren) + - "]"); + _LWMLOG_("! client timeout [" + std::to_string(activeChildren) + "]"); + _LWMLOG_("! vs expected: [" + std::to_string(expectedActiveChildren) + + "]"); return FAILURE; } } + LINK_WIRELESS_MULTIBOOT_BARRIER; + success = linkRawWireless->receiveData(response); + + LINK_WIRELESS_MULTIBOOT_BARRIER; + if (!success) { - LWMLOG("! receiveData failed"); + _LWMLOG_("! receiveData failed"); return FAILURE; } return SUCCESS; } + __attribute__((noinline)) Result ensureAllClientsAreStillAlive() { + LinkRawWireless::SlotStatusResponse slotStatusResponse; + if (!linkRawWireless->getSlotStatus(slotStatusResponse)) + return FAILURE; + + if (slotStatusResponse.connectedClientsSize < progress.connectedClients) + return CLIENT_DISCONNECTED; + + return SUCCESS; + } + __attribute__((noinline)) Result finish(Result result) { linkRawWireless->deactivate(); progress.state = STOPPED; @@ -736,8 +788,6 @@ class LinkWirelessMultiboot { extern LinkWirelessMultiboot* linkWirelessMultiboot; -#undef LWMLOG +#undef _LWMLOG_ -#pragma GCC pop_options - -#endif // LINK_WIRELESS_MULTIBOOT_H \ No newline at end of file +#endif // LINK_WIRELESS_MULTIBOOT_H diff --git a/lib/LinkWirelessOpenSDK.hpp b/lib/LinkWirelessOpenSDK.hpp index 955243b..abb9521 100644 --- a/lib/LinkWirelessOpenSDK.hpp +++ b/lib/LinkWirelessOpenSDK.hpp @@ -4,44 +4,54 @@ // -------------------------------------------------------------------------- // An open-source implementation of the "official" Wireless Adapter protocol. // -------------------------------------------------------------------------- -// - Check out README.md for documentation. +// - Advanced usage only! +// - You only need this if you want to interact with N software. // -------------------------------------------------------------------------- -#include -#include +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +#include "_link_common.hpp" + #include "LinkRawWireless.hpp" -#define LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_WORDS 23 -#define LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_BYTES_SERVER 87 -#define LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_BYTES_CLIENT 16 -#define LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER 3 -#define LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT 2 -#define LINK_WIRELESS_OPEN_SDK_HEADER_MASK_SERVER \ - ((1 << (LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER * 8)) - 1) -#define LINK_WIRELESS_OPEN_SDK_HEADER_MASK_CLIENT \ - ((1 << (LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT * 8)) - 1) -#define LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER \ - (LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_BYTES_SERVER - \ - LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER) -#define LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_CLIENT \ - (LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_BYTES_CLIENT - \ - LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT) -#define LINK_WIRELESS_OPEN_SDK_MAX_PACKETS_SERVER \ - (LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_BYTES_SERVER / \ - LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER) -#define LINK_WIRELESS_OPEN_SDK_MAX_PACKETS_CLIENT \ - (LINK_WIRELESS_OPEN_SDK_MAX_TRANSFER_BYTES_CLIENT / \ - LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT) - -static volatile char LINK_WIRELESS_OPEN_SDK_VERSION[] = - "LinkWirelessOpenSDK/v6.3.0"; +static volatile char VERSION[] = "LinkWirelessOpenSDK/v7.0.0"; +/** + * @brief An open-source implementation of the "official" Wireless Adapter + * protocol. + * \warning Advanced usage only! + * \warning You only need this if you want to interact with N software. + */ class LinkWirelessOpenSDK { + private: + using u32 = unsigned int; + using u16 = unsigned short; + using u8 = unsigned char; + + public: + static constexpr int MAX_TRANSFER_WORDS = 23; + static constexpr int MAX_TRANSFER_BYTES_SERVER = 87; + static constexpr int MAX_TRANSFER_BYTES_CLIENT = 16; + static constexpr int HEADER_SIZE_SERVER = 3; + static constexpr int HEADER_SIZE_CLIENT = 2; + static constexpr int HEADER_MASK_SERVER = (1 << (HEADER_SIZE_SERVER * 8)) - 1; + static constexpr int HEADER_MASK_CLIENT = (1 << (HEADER_SIZE_CLIENT * 8)) - 1; + static constexpr int MAX_PAYLOAD_SERVER = + MAX_TRANSFER_BYTES_SERVER - HEADER_SIZE_SERVER; + static constexpr int MAX_PAYLOAD_CLIENT = + MAX_TRANSFER_BYTES_CLIENT - HEADER_SIZE_CLIENT; + static constexpr int MAX_PACKETS_SERVER = + MAX_TRANSFER_BYTES_SERVER / HEADER_SIZE_SERVER; + static constexpr int MAX_PACKETS_CLIENT = + MAX_TRANSFER_BYTES_CLIENT / HEADER_SIZE_CLIENT; + public: template struct SendBuffer { T header; - std::array data; + std::array data; u32 dataSize = 0; u32 totalByteCount = 0; }; @@ -90,10 +100,10 @@ class LinkWirelessOpenSDK { }; struct ServerPacket { ServerSDKHeader header; - u8 payload[LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER]; + u8 payload[MAX_PAYLOAD_SERVER]; }; struct ServerResponse { - ServerPacket packets[LINK_WIRELESS_OPEN_SDK_MAX_PACKETS_SERVER]; + ServerPacket packets[MAX_PACKETS_SERVER]; u32 packetsSize = 0; }; struct ParentData { @@ -117,16 +127,22 @@ class LinkWirelessOpenSDK { }; struct ClientPacket { ClientSDKHeader header; - u8 payload[LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_CLIENT]; + u8 payload[MAX_PAYLOAD_CLIENT]; }; struct ClientResponse { - ClientPacket packets[LINK_WIRELESS_OPEN_SDK_MAX_PACKETS_CLIENT]; + ClientPacket packets[MAX_PACKETS_CLIENT]; u32 packetsSize = 0; }; struct ChildrenData { ClientResponse responses[4]; }; + /** + * @brief Parses the `response` and returns a struct containing all the + * received packets from the connected clients. + * @param response The response to be parsed. + */ + [[nodiscard]] ChildrenData getChildrenData(LinkRawWireless::ReceiveDataResponse response) { u8* buffer = (u8*)response.data; u32 cursor = 0; @@ -141,18 +157,17 @@ class LinkWirelessOpenSDK { ClientResponse* clientResponse = &childrenData.responses[i - 1]; u32 remainingBytes = response.sentBytes[i]; - while (remainingBytes >= LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT) { + while (remainingBytes >= HEADER_SIZE_CLIENT) { ClientPacket* packet = &clientResponse->packets[clientResponse->packetsSize]; u32 headerInt = *((u16*)(buffer + cursor)); packet->header = parseClientHeader(headerInt); - cursor += LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT; - remainingBytes -= LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT; + cursor += HEADER_SIZE_CLIENT; + remainingBytes -= HEADER_SIZE_CLIENT; if (packet->header.payloadSize > 0 && - packet->header.payloadSize <= - LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_CLIENT && + packet->header.payloadSize <= MAX_PAYLOAD_CLIENT && remainingBytes >= packet->header.payloadSize) { for (u32 j = 0; j < packet->header.payloadSize; j++) packet->payload[j] = buffer[cursor++]; @@ -166,6 +181,12 @@ class LinkWirelessOpenSDK { return childrenData; } + /** + * @brief Parses the `response` and returns a struct containing all the + * received packets from the host. + * @param response The response to be parsed. + */ + [[nodiscard]] ParentData getParentData(LinkRawWireless::ReceiveDataResponse response) { u8* buffer = (u8*)response.data; u32 cursor = 0; @@ -177,19 +198,18 @@ class LinkWirelessOpenSDK { ServerResponse* serverResponse = &parentData.response; u32 remainingBytes = response.sentBytes[0]; - while (remainingBytes >= LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER) { + while (remainingBytes >= HEADER_SIZE_SERVER) { ServerPacket* packet = &serverResponse->packets[serverResponse->packetsSize]; u32 headerInt = (*((u16*)(buffer + cursor))) | (((*((u8*)(buffer + cursor + 2)))) << 16); packet->header = parseServerHeader(headerInt); - cursor += LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER; - remainingBytes -= LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER; + cursor += HEADER_SIZE_SERVER; + remainingBytes -= HEADER_SIZE_SERVER; if (packet->header.payloadSize > 0 && - packet->header.payloadSize <= - LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER && + packet->header.payloadSize <= MAX_PAYLOAD_SERVER && remainingBytes >= packet->header.payloadSize) { for (u32 j = 0; j < packet->header.payloadSize; j++) packet->payload[j] = buffer[cursor++]; @@ -202,14 +222,30 @@ class LinkWirelessOpenSDK { return parentData; } + /** + * @brief Creates a buffer for the host to send a `fullPayload` with a valid + * header. If `fullPayloadSize` is higher than `84` (the maximum payload + * size), the buffer will only contain the **first** `84` bytes (unless an + * `offset` > 0 is used). A `sequence` number must be created by using + * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. Optionally, a + * `targetSlots` bit array can be used to exclude some clients from the + * transmissions (the default is `0b1111`). + * @param fullPayload A pointer to the payload buffer. + * @param fullPayloadSize Total size of the payload. + * @param sequence A sequence number created using + * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. + * @param targetSlots A bit array that can be used to exclude some clients + * (the default is `0b1111`). + * @param offset The offset within the `fullPayload` pointer. Defaults to `0`. + */ + [[nodiscard]] SendBuffer createServerBuffer(const u8* fullPayload, u32 fullPayloadSize, SequenceNumber sequence, u8 targetSlots = 0b1111, u32 offset = 0) { SendBuffer buffer; - u32 payloadSize = - min(fullPayloadSize, LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER); + u32 payloadSize = Link::_min(fullPayloadSize, MAX_PAYLOAD_SERVER); buffer.header.isACK = 0; buffer.header.targetSlots = targetSlots; @@ -226,8 +262,7 @@ class LinkWirelessOpenSDK { for (u32 i = 1; i < payloadSize; i += 4) { u32 word = 0; for (u32 j = 0; j < 4; j++) { - if (offset + i + j < fullPayloadSize && - i + j < LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_SERVER) { + if (offset + i + j < fullPayloadSize && i + j < MAX_PAYLOAD_SERVER) { u8 byte = fullPayload[offset + i + j]; word |= byte << (j * 8); } @@ -235,12 +270,18 @@ class LinkWirelessOpenSDK { buffer.data[buffer.dataSize++] = word; } - buffer.totalByteCount = - LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER + payloadSize; + buffer.totalByteCount = HEADER_SIZE_SERVER + payloadSize; return buffer; } + /** + * @brief Creates a buffer for the host to acknowledge a header received from + * a certain `clientNumber`. + * @param clientHeader The header of the received packet. + * @param clientNumber `(0~3)` The client number that sent the packet. + */ + [[nodiscard]] SendBuffer createServerACKBuffer( ClientSDKHeader clientHeader, u8 clientNumber) { @@ -250,18 +291,30 @@ class LinkWirelessOpenSDK { u32 headerInt = serializeServerHeader(buffer.header); buffer.data[buffer.dataSize++] = headerInt; - buffer.totalByteCount = LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_SERVER; + buffer.totalByteCount = HEADER_SIZE_SERVER; return buffer; } + /** + * @brief Creates a buffer for the client to send a `fullPayload` with a valid + * header. If `fullPayloadSize` is higher than `14` (the maximum payload + * size), the buffer will only contain the **first** `14` bytes (unless an + * `offset` > 0 is used). A `sequence` number must be created by using + * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. + * @param fullPayload A pointer to the payload buffer. + * @param fullPayloadSize Total size of the payload. + * @param sequence A sequence number created using + * `LinkWirelessOpenSDK::SequenceNumber::fromPacketId(...)`. + * @param offset The offset within the `fullPayload` pointer. Defaults to `0`. + */ + [[nodiscard]] SendBuffer createClientBuffer(const u8* fullPayload, u32 fullPayloadSize, SequenceNumber sequence, u32 offset = 0) { SendBuffer buffer; - u32 payloadSize = - min(fullPayloadSize, LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_CLIENT); + u32 payloadSize = Link::_min(fullPayloadSize, MAX_PAYLOAD_CLIENT); buffer.header.isACK = 0; buffer.header.payloadSize = payloadSize; @@ -279,8 +332,7 @@ class LinkWirelessOpenSDK { for (u32 i = 2; i < payloadSize; i += 4) { u32 word = 0; for (u32 j = 0; j < 4; j++) { - if (offset + i + j < fullPayloadSize && - i + j < LINK_WIRELESS_OPEN_SDK_MAX_PAYLOAD_CLIENT) { + if (offset + i + j < fullPayloadSize && i + j < MAX_PAYLOAD_CLIENT) { u8 byte = fullPayload[offset + i + j]; word |= byte << (j * 8); } @@ -288,12 +340,17 @@ class LinkWirelessOpenSDK { buffer.data[buffer.dataSize++] = word; } - buffer.totalByteCount = - LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT + payloadSize; + buffer.totalByteCount = HEADER_SIZE_CLIENT + payloadSize; return buffer; } + /** + * @brief Creates a buffer for the client to acknowledge a header received + * from the host. + * @param serverHeader The header of the received packet. + */ + [[nodiscard]] SendBuffer createClientACKBuffer( ServerSDKHeader serverHeader) { SendBuffer buffer; @@ -302,11 +359,13 @@ class LinkWirelessOpenSDK { u16 headerInt = serializeClientHeader(buffer.header); buffer.data[buffer.dataSize++] = headerInt; - buffer.totalByteCount = LINK_WIRELESS_OPEN_SDK_HEADER_SIZE_CLIENT; + buffer.totalByteCount = HEADER_SIZE_CLIENT; return buffer; } + private: + [[nodiscard]] ServerSDKHeader createACKHeaderFor(ClientSDKHeader clientHeader, u8 clientNumber) { ServerSDKHeader serverHeader; @@ -320,6 +379,7 @@ class LinkWirelessOpenSDK { return serverHeader; } + [[nodiscard]] ClientSDKHeader createACKHeaderFor(ServerSDKHeader serverHeader) { ClientSDKHeader clientHeader; clientHeader.isACK = 1; @@ -331,30 +391,32 @@ class LinkWirelessOpenSDK { return clientHeader; } + [[nodiscard]] ClientSDKHeader parseClientHeader(u32 clientHeaderInt) { ClientSDKHeaderSerializer clientSerializer; - clientSerializer.asInt = - clientHeaderInt & LINK_WIRELESS_OPEN_SDK_HEADER_MASK_CLIENT; + clientSerializer.asInt = clientHeaderInt & HEADER_MASK_CLIENT; return clientSerializer.asStruct; } + [[nodiscard]] u16 serializeClientHeader(ClientSDKHeader clientHeader) { ClientSDKHeaderSerializer clientSerializer; clientSerializer.asStruct = clientHeader; - return clientSerializer.asInt & LINK_WIRELESS_OPEN_SDK_HEADER_MASK_CLIENT; + return clientSerializer.asInt & HEADER_MASK_CLIENT; } + [[nodiscard]] ServerSDKHeader parseServerHeader(u32 serverHeaderInt) { ServerSDKHeaderSerializer serverSerializer; - serverSerializer.asInt = - serverHeaderInt & LINK_WIRELESS_OPEN_SDK_HEADER_MASK_SERVER; + serverSerializer.asInt = serverHeaderInt & HEADER_MASK_SERVER; return serverSerializer.asStruct; } + [[nodiscard]] u32 serializeServerHeader(ServerSDKHeader serverHeader) { ServerSDKHeaderSerializer serverSerializer; serverSerializer.asStruct = serverHeader; - return serverSerializer.asInt & LINK_WIRELESS_OPEN_SDK_HEADER_MASK_SERVER; + return serverSerializer.asInt & HEADER_MASK_SERVER; } }; diff --git a/lib/_link_common.hpp b/lib/_link_common.hpp new file mode 100644 index 0000000..77a5681 --- /dev/null +++ b/lib/_link_common.hpp @@ -0,0 +1,297 @@ +#ifndef LINK_COMMON_H +#define LINK_COMMON_H + +#ifndef LINK_DEVELOPMENT +#pragma GCC system_header +#endif + +/** + * @brief Enable mGBA debug logging. + */ +#ifndef LINK_ENABLE_DEBUG_LOGS +#define LINK_ENABLE_DEBUG_LOGS 0 +#endif + +#if LINK_ENABLE_DEBUG_LOGS != 0 +#include +#include +#endif + +/** + * @brief This namespace contains shared code between all libraries. + * \warning Most of these things are borrowed from libtonc and gba-hpp. + */ +namespace Link { + +// Types + +using u32 = unsigned int; +using u16 = unsigned short; +using u8 = unsigned char; + +using s16 = signed short; +using s8 = signed char; + +using vu32 = volatile unsigned int; +using vs32 = volatile signed int; +using vu16 = volatile unsigned short; +using vs16 = volatile signed short; + +// Structs + +struct _TMR_REC { + union { + u16 start; + u16 count; + } __attribute__((packed)); + + u16 cnt; +} __attribute__((aligned(4))); + +typedef struct { + u32 reserved1[5]; + u8 handshake_data; + u8 padding; + u16 handshake_timeout; + u8 probe_count; + u8 client_data[3]; + u8 palette_data; + u8 response_bit; + u8 client_bit; + u8 reserved2; + u8* boot_srcp; + u8* boot_endp; + u8* masterp; + u8* reserved3[3]; + u32 system_work2[4]; + u8 sendflag; + u8 probe_target_bit; + u8 check_wait; + u8 server_type; +} _MultiBootParam; + +// I/O Registers + +constexpr u32 _REG_BASE = 0x04000000; + +inline vu16& _REG_RCNT = *reinterpret_cast(_REG_BASE + 0x0134); +inline vu16& _REG_SIOCNT = *reinterpret_cast(_REG_BASE + 0x0128); +inline vu32& _REG_SIODATA32 = *reinterpret_cast(_REG_BASE + 0x0120); +inline vu16& _REG_SIODATA8 = *reinterpret_cast(_REG_BASE + 0x012A); +inline vu16& _REG_SIOMLT_SEND = *reinterpret_cast(_REG_BASE + 0x012A); +inline vu16* const _REG_SIOMULTI = reinterpret_cast(_REG_BASE + 0x0120); +inline vu16& _REG_JOYCNT = *reinterpret_cast(_REG_BASE + 0x0140); +inline vu16& _REG_JOY_RECV_L = *reinterpret_cast(_REG_BASE + 0x0150); +inline vu16& _REG_JOY_RECV_H = *reinterpret_cast(_REG_BASE + 0x0152); +inline vu16& _REG_JOY_TRANS_L = *reinterpret_cast(_REG_BASE + 0x0154); +inline vu16& _REG_JOY_TRANS_H = *reinterpret_cast(_REG_BASE + 0x0156); +inline vu16& _REG_JOYSTAT = *reinterpret_cast(_REG_BASE + 0x0158); +inline vu16& _REG_VCOUNT = *reinterpret_cast(_REG_BASE + 0x0006); +inline vu16& _REG_KEYS = *reinterpret_cast(_REG_BASE + 0x0130); +inline vu16& _REG_TM1CNT_L = *reinterpret_cast(_REG_BASE + 0x0104); +inline vu16& _REG_TM1CNT_H = *reinterpret_cast(_REG_BASE + 0x0106); +inline vu16& _REG_TM2CNT_L = *reinterpret_cast(_REG_BASE + 0x0108); +inline vu16& _REG_TM2CNT_H = *reinterpret_cast(_REG_BASE + 0x010a); +inline vu16& _REG_IME = *reinterpret_cast(_REG_BASE + 0x0208); + +inline volatile _TMR_REC* const _REG_TM = + reinterpret_cast(_REG_BASE + 0x0100); + +static constexpr u16 _KEY_ANY = 0x03FF; //!< Here's the Any key :) +static constexpr u16 _TM_FREQ_1 = 0; //!< 1 cycle/tick (16.7 MHz) +static constexpr u16 _TM_FREQ_64 = 0x0001; //!< 64 cycles/tick (262 kHz) +static constexpr u16 _TM_FREQ_256 = 0x0002; //!< 256 cycles/tick (66 kHz) +static constexpr u16 _TM_FREQ_1024 = 0x0003; //!< 1024 cycles/tick (16 kHz) +static constexpr u16 _TM_CASCADE = + 0x0004; //!< Increment when preceding timer overflows +static constexpr u16 _TM_IRQ = 0x0040; //!< Enable timer irq +static constexpr u16 _TM_ENABLE = 0x0080; //!< Enable timer + +static constexpr u16 _IRQ_VBLANK = 0x0001; //!< Catch VBlank irq +static constexpr u16 _IRQ_TIMER0 = 0x0008; //!< Catch timer 0 irq +static constexpr u16 _IRQ_TIMER1 = 0x0010; //!< Catch timer 1 irq +static constexpr u16 _IRQ_TIMER2 = 0x0020; //!< Catch timer 2 irq +static constexpr u16 _IRQ_TIMER3 = 0x0040; //!< Catch timer 3 irq +static constexpr u16 _IRQ_SERIAL = 0x0080; //!< Catch serial comm irq +static constexpr u16 _TIMER_IRQ_IDS[] = {_IRQ_TIMER0, _IRQ_TIMER1, _IRQ_TIMER2, + _IRQ_TIMER3}; + +// SWI + +static inline __attribute__((always_inline)) void _IntrWait( + bool clearCurrent, + u32 flags) noexcept { + register auto r0 asm("r0") = clearCurrent; + register auto r1 asm("r1") = flags; + asm volatile inline("swi 0x4 << ((1f - . == 4) * -16); 1:" + : "+r"(r0), "+r"(r1)::"r3"); +} + +static inline auto _MultiBoot(const _MultiBootParam* param, + u32 mbmode) noexcept { + register union { + const _MultiBootParam* ptr; + int res; + } r0 asm("r0") = {param}; + register auto r1 asm("r1") = mbmode; + asm volatile inline("swi 0x25 << ((1f - . == 4) * -16); 1:" + : "+r"(r0), "+r"(r1)::"r3"); + return r0.res; +} + +// Helpers + +static inline int _max(int a, int b) { + return (a > b) ? (a) : (b); +} + +static inline int _min(int a, int b) { + return (a < b) ? (a) : (b); +} + +// Queue + +template +class Queue { + public: + void push(T item) { + if (isFull()) { + if constexpr (Overwrite) { + pop(); + } else { + return; + } + } + + rear = (rear + 1) % Size; + arr[rear] = item; + count = count + 1; + } + + T pop() { + if (isEmpty()) + return T{}; + + auto x = arr[front]; + front = (front + 1) % Size; + count = count - 1; + + return x; + } + + T peek() { + if (isEmpty()) + return T{}; + return arr[front]; + } + + T* peekRef() { + if (isEmpty()) + return nullptr; + return &arr[front]; + } + + template + void forEach(F action) { + vs32 currentFront = front; + + for (u32 i = 0; i < count; i++) { + if (!action(arr[currentFront])) + return; + currentFront = (currentFront + 1) % Size; + } + } + + void clear() { + front = 0; + count = 0; + rear = -1; + } + + void startReading() { _isReading = true; } + void stopReading() { _isReading = false; } + + void syncPush(T item) { + _isWriting = true; + asm volatile("" ::: "memory"); + + push(item); + + asm volatile("" ::: "memory"); + _isWriting = false; + asm volatile("" ::: "memory"); + + if (_needsClear) { + clear(); + _needsClear = false; + } + } + + T syncPop() { + _isReading = true; + asm volatile("" ::: "memory"); + + auto value = pop(); + + asm volatile("" ::: "memory"); + _isReading = false; + asm volatile("" ::: "memory"); + + return value; + } + + void syncClear() { + if (_isReading) + return; // (it will be cleared later anyway) + + if (!_isWriting) + clear(); + else + _needsClear = true; + } + + u32 size() { return count; } + bool isEmpty() { return count == 0; } + bool isFull() { return count == Size; } + bool isReading() { return _isReading; } + bool isWriting() { return _isWriting; } + bool canMutate() { return !_isReading && !_isWriting; } + + private: + T arr[Size]; + vs32 front = 0; + vs32 rear = -1; + vu32 count = 0; + volatile bool _isReading = false; + volatile bool _isWriting = false; + volatile bool _needsClear = false; +}; + +// Packets per frame -> Timer interval +static inline u16 perFrame(u16 packets) { + return (1667 * 1024) / (packets * 6104); +} + +// mGBA Logging + +#if LINK_ENABLE_DEBUG_LOGS != 0 +inline vu16& _REG_LOG_ENABLE = *reinterpret_cast(0x4FFF780); +inline vu16& _REG_LOG_LEVEL = *reinterpret_cast(0x4FFF700); + +static inline void log(const char* fmt, ...) { + _REG_LOG_ENABLE = 0xC0DE; + + va_list args; + va_start(args, fmt); + + char* const log = (char*)0x4FFF600; + vsnprintf(log, 0x100, fmt, args); + _REG_LOG_LEVEL = 0x102; // Level: WARN + + va_end(args); +} +#endif + +} // namespace Link + +#endif // LINK_COMMON_H diff --git a/lib/c_bindings/C_LinkCable.cpp b/lib/c_bindings/C_LinkCable.cpp new file mode 100644 index 0000000..3894b13 --- /dev/null +++ b/lib/c_bindings/C_LinkCable.cpp @@ -0,0 +1,87 @@ +#include "C_LinkCable.h" +#include "../LinkCable.hpp" + +extern "C" { + +C_LinkCableHandle C_LinkCable_createDefault() { + return new LinkCable(); +} + +C_LinkCableHandle C_LinkCable_create(C_LinkCable_BaudRate baudRate, + u32 timeout, + u16 interval, + u8 sendTimerId) { + return new LinkCable(static_cast(baudRate), timeout, + interval, sendTimerId); +} + +void C_LinkCable_destroy(C_LinkCableHandle handle) { + delete static_cast(handle); +} + +bool C_LinkCable_isActive(C_LinkCableHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkCable_activate(C_LinkCableHandle handle) { + static_cast(handle)->activate(); +} + +void C_LinkCable_deactivate(C_LinkCableHandle handle) { + static_cast(handle)->deactivate(); +} + +bool C_LinkCable_isConnected(C_LinkCableHandle handle) { + return static_cast(handle)->isConnected(); +} + +u8 C_LinkCable_playerCount(C_LinkCableHandle handle) { + return static_cast(handle)->playerCount(); +} + +u8 C_LinkCable_currentPlayerId(C_LinkCableHandle handle) { + return static_cast(handle)->currentPlayerId(); +} + +void C_LinkCable_sync(C_LinkCableHandle handle) { + static_cast(handle)->sync(); +} + +bool C_LinkCable_waitFor(C_LinkCableHandle handle, u8 playerId) { + return static_cast(handle)->waitFor(playerId); +} + +bool C_LinkCable_waitForWithCancel(C_LinkCableHandle handle, + u8 playerId, + bool (*cancel)()) { + return static_cast(handle)->waitFor(playerId, cancel); +} + +bool C_LinkCable_canRead(C_LinkCableHandle handle, u8 playerId) { + return static_cast(handle)->canRead(playerId); +} + +u16 C_LinkCable_read(C_LinkCableHandle handle, u8 playerId) { + return static_cast(handle)->read(playerId); +} + +u16 C_LinkCable_peek(C_LinkCableHandle handle, u8 playerId) { + return static_cast(handle)->peek(playerId); +} + +void C_LinkCable_send(C_LinkCableHandle handle, u16 data) { + static_cast(handle)->send(data); +} + +void C_LinkCable_onVBlank(C_LinkCableHandle handle) { + static_cast(handle)->_onVBlank(); +} + +void C_LinkCable_onSerial(C_LinkCableHandle handle) { + static_cast(handle)->_onSerial(); +} + +void C_LinkCable_onTimer(C_LinkCableHandle handle) { + static_cast(handle)->_onTimer(); +} +} diff --git a/lib/c_bindings/C_LinkCable.h b/lib/c_bindings/C_LinkCable.h new file mode 100644 index 0000000..578debf --- /dev/null +++ b/lib/c_bindings/C_LinkCable.h @@ -0,0 +1,75 @@ +#ifndef C_BINDINGS_LINK_CABLE_H +#define C_BINDINGS_LINK_CABLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkCableHandle; + +#define C_LINK_CABLE_MAX_PLAYERS 4 +#define C_LINK_CABLE_DEFAULT_TIMEOUT 3 +#define C_LINK_CABLE_DEFAULT_INTERVAL 50 +#define C_LINK_CABLE_DEFAULT_SEND_TIMER_ID 3 +#define C_LINK_CABLE_DISCONNECTED 0xffff +#define C_LINK_CABLE_NO_DATA 0x0 + +typedef enum { + C_LINK_CABLE_BAUD_RATE_0, // 9600 bps + C_LINK_CABLE_BAUD_RATE_1, // 38400 bps + C_LINK_CABLE_BAUD_RATE_2, // 57600 bps + C_LINK_CABLE_BAUD_RATE_3 // 115200 bps +} C_LinkCable_BaudRate; + +C_LinkCableHandle C_LinkCable_createDefault(); +C_LinkCableHandle C_LinkCable_create(C_LinkCable_BaudRate baudRate, + u32 timeout, + u16 interval, + u8 sendTimerId); +void C_LinkCable_destroy(C_LinkCableHandle handle); + +bool C_LinkCable_isActive(C_LinkCableHandle handle); +void C_LinkCable_activate(C_LinkCableHandle handle); +void C_LinkCable_deactivate(C_LinkCableHandle handle); + +bool C_LinkCable_isConnected(C_LinkCableHandle handle); +u8 C_LinkCable_playerCount(C_LinkCableHandle handle); +u8 C_LinkCable_currentPlayerId(C_LinkCableHandle handle); + +void C_LinkCable_sync(C_LinkCableHandle handle); +bool C_LinkCable_waitFor(C_LinkCableHandle handle, u8 playerId); +bool C_LinkCable_waitForWithCancel(C_LinkCableHandle handle, + u8 playerId, + bool (*cancel)()); + +bool C_LinkCable_canRead(C_LinkCableHandle handle, u8 playerId); +u16 C_LinkCable_read(C_LinkCableHandle handle, u8 playerId); +u16 C_LinkCable_peek(C_LinkCableHandle handle, u8 playerId); + +void C_LinkCable_send(C_LinkCableHandle handle, u16 data); + +void C_LinkCable_onVBlank(C_LinkCableHandle handle); +void C_LinkCable_onSerial(C_LinkCableHandle handle); +void C_LinkCable_onTimer(C_LinkCableHandle handle); + +extern C_LinkCableHandle cLinkCable; + +inline void C_LINK_CABLE_ISR_VBLANK() { + C_LinkCable_onVBlank(cLinkCable); +} + +inline void C_LINK_CABLE_ISR_SERIAL() { + C_LinkCable_onSerial(cLinkCable); +} + +inline void C_LINK_CABLE_ISR_TIMER() { + C_LinkCable_onTimer(cLinkCable); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_CABLE_H diff --git a/lib/c_bindings/C_LinkCableMultiboot.cpp b/lib/c_bindings/C_LinkCableMultiboot.cpp new file mode 100644 index 0000000..d5251f5 --- /dev/null +++ b/lib/c_bindings/C_LinkCableMultiboot.cpp @@ -0,0 +1,25 @@ +#include "C_LinkCableMultiboot.h" +#include "../LinkCableMultiboot.hpp" + +extern "C" { + +C_LinkCableMultibootHandle C_LinkCableMultiboot_create() { + return new LinkCableMultiboot(); +} + +void C_LinkCableMultiboot_destroy(C_LinkCableMultibootHandle handle) { + delete static_cast(handle); +} + +C_LinkCableMultiboot_Result C_LinkCableMultiboot_sendRom( + C_LinkCableMultibootHandle handle, + const u8* rom, + u32 romSize, + bool (*cancel)(void), + C_LinkCableMultiboot_TransferMode mode) { + return static_cast( + static_cast(handle)->sendRom( + rom, romSize, cancel, + static_cast(mode))); +} +} diff --git a/lib/c_bindings/C_LinkCableMultiboot.h b/lib/c_bindings/C_LinkCableMultiboot.h new file mode 100644 index 0000000..fc9cc8b --- /dev/null +++ b/lib/c_bindings/C_LinkCableMultiboot.h @@ -0,0 +1,40 @@ +#ifndef C_BINDINGS_LINK_CABLE_MULTIBOOT_H +#define C_BINDINGS_LINK_CABLE_MULTIBOOT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef enum { + C_LINK_CABLE_MULTIBOOT_SUCCESS, + C_LINK_CABLE_MULTIBOOT_INVALID_SIZE, + C_LINK_CABLE_MULTIBOOT_CANCELED, + C_LINK_CABLE_MULTIBOOT_FAILURE_DURING_TRANSFER +} C_LinkCableMultiboot_Result; + +typedef enum { + C_LINK_CABLE_MULTIBOOT_TRANSFER_MODE_SPI = 0, + C_LINK_CABLE_MULTIBOOT_TRANSFER_MODE_MULTI_PLAY = 1 +} C_LinkCableMultiboot_TransferMode; + +typedef void* C_LinkCableMultibootHandle; + +C_LinkCableMultibootHandle C_LinkCableMultiboot_create(); +void C_LinkCableMultiboot_destroy(C_LinkCableMultibootHandle handle); + +C_LinkCableMultiboot_Result C_LinkCableMultiboot_sendRom( + C_LinkCableMultibootHandle handle, + const u8* rom, + u32 romSize, + bool (*cancel)(void), + C_LinkCableMultiboot_TransferMode mode); + +extern C_LinkCableMultibootHandle cLinkCableMultiboot; + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_CABLE_MULTIBOOT_H diff --git a/lib/c_bindings/C_LinkCube.cpp b/lib/c_bindings/C_LinkCube.cpp new file mode 100644 index 0000000..ee1a7cc --- /dev/null +++ b/lib/c_bindings/C_LinkCube.cpp @@ -0,0 +1,61 @@ +#include "C_LinkCube.h" +#include "../LinkCube.hpp" + +extern "C" { + +C_LinkCubeHandle C_LinkCube_create() { + return new LinkCube(); +} + +void C_LinkCube_destroy(C_LinkCubeHandle handle) { + delete static_cast(handle); +} + +bool C_LinkCube_isActive(C_LinkCubeHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkCube_activate(C_LinkCubeHandle handle) { + static_cast(handle)->activate(); +} + +void C_LinkCube_deactivate(C_LinkCubeHandle handle) { + static_cast(handle)->deactivate(); +} + +bool C_LinkCube_wait(C_LinkCubeHandle handle) { + return static_cast(handle)->wait(); +} + +bool C_LinkCube_waitWithCancel(C_LinkCubeHandle handle, bool (*cancel)()) { + return static_cast(handle)->wait(cancel); +} + +bool C_LinkCube_canRead(C_LinkCubeHandle handle) { + return static_cast(handle)->canRead(); +} + +u32 C_LinkCube_read(C_LinkCubeHandle handle) { + return static_cast(handle)->read(); +} + +u32 C_LinkCube_peek(C_LinkCubeHandle handle) { + return static_cast(handle)->peek(); +} + +void C_LinkCube_send(C_LinkCubeHandle handle, u32 data) { + static_cast(handle)->send(data); +} + +u32 C_LinkCube_pendingCount(C_LinkCubeHandle handle) { + return static_cast(handle)->pendingCount(); +} + +bool C_LinkCube_didReset(C_LinkCubeHandle handle, bool clear) { + return static_cast(handle)->didReset(clear); +} + +void C_LinkCube_onSerial(C_LinkCubeHandle handle) { + static_cast(handle)->_onSerial(); +} +} diff --git a/lib/c_bindings/C_LinkCube.h b/lib/c_bindings/C_LinkCube.h new file mode 100644 index 0000000..aeaa1d4 --- /dev/null +++ b/lib/c_bindings/C_LinkCube.h @@ -0,0 +1,43 @@ +#ifndef C_BINDINGS_LINK_CUBE_H +#define C_BINDINGS_LINK_CUBE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkCubeHandle; + +C_LinkCubeHandle C_LinkCube_create(); +void C_LinkCube_destroy(C_LinkCubeHandle handle); + +bool C_LinkCube_isActive(C_LinkCubeHandle handle); +void C_LinkCube_activate(C_LinkCubeHandle handle); +void C_LinkCube_deactivate(C_LinkCubeHandle handle); + +bool C_LinkCube_wait(C_LinkCubeHandle handle); +bool C_LinkCube_waitWithCancel(C_LinkCubeHandle handle, bool (*cancel)()); + +bool C_LinkCube_canRead(C_LinkCubeHandle handle); +u32 C_LinkCube_read(C_LinkCubeHandle handle); +u32 C_LinkCube_peek(C_LinkCubeHandle handle); + +void C_LinkCube_send(C_LinkCubeHandle handle, u32 data); +u32 C_LinkCube_pendingCount(C_LinkCubeHandle handle); + +bool C_LinkCube_didReset(C_LinkCubeHandle handle, bool clear); + +void C_LinkCube_onSerial(C_LinkCubeHandle handle); + +extern C_LinkCubeHandle cLinkCube; + +inline void C_LINK_CUBE_ISR_SERIAL() { + C_LinkCube_onSerial(cLinkCube); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_CUBE_H diff --git a/lib/c_bindings/C_LinkGPIO.cpp b/lib/c_bindings/C_LinkGPIO.cpp new file mode 100644 index 0000000..545358b --- /dev/null +++ b/lib/c_bindings/C_LinkGPIO.cpp @@ -0,0 +1,47 @@ +#include "C_LinkGPIO.h" +#include "../LinkGPIO.hpp" + +extern "C" { + +C_LinkGPIOHandle C_LinkGPIO_create() { + return new LinkGPIO(); +} + +void C_LinkGPIO_destroy(C_LinkGPIOHandle handle) { + delete static_cast(handle); +} + +void C_LinkGPIO_reset(C_LinkGPIOHandle handle) { + static_cast(handle)->reset(); +} + +void C_LinkGPIO_setMode(C_LinkGPIOHandle handle, + C_LinkGPIO_Pin pin, + C_LinkGPIO_Direction direction) { + static_cast(handle)->setMode( + static_cast(pin), + static_cast(direction)); +} + +C_LinkGPIO_Direction C_LinkGPIO_getMode(C_LinkGPIOHandle handle, + C_LinkGPIO_Pin pin) { + return static_cast( + static_cast(handle)->getMode(static_cast(pin))); +} + +bool C_LinkGPIO_readPin(C_LinkGPIOHandle handle, C_LinkGPIO_Pin pin) { + return static_cast(handle)->readPin( + static_cast(pin)); +} + +void C_LinkGPIO_writePin(C_LinkGPIOHandle handle, + C_LinkGPIO_Pin pin, + bool isHigh) { + static_cast(handle)->writePin(static_cast(pin), + isHigh); +} + +void C_LinkGPIO_setSIInterrupts(C_LinkGPIOHandle handle, bool isEnabled) { + static_cast(handle)->setSIInterrupts(isEnabled); +} +} diff --git a/lib/c_bindings/C_LinkGPIO.h b/lib/c_bindings/C_LinkGPIO.h new file mode 100644 index 0000000..2afd919 --- /dev/null +++ b/lib/c_bindings/C_LinkGPIO.h @@ -0,0 +1,47 @@ +#ifndef C_BINDINGS_LINK_GPIO_H +#define C_BINDINGS_LINK_GPIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkGPIOHandle; + +typedef enum { + C_LINK_GPIO_PIN_SI, + C_LINK_GPIO_PIN_SO, + C_LINK_GPIO_PIN_SD, + C_LINK_GPIO_PIN_SC +} C_LinkGPIO_Pin; + +typedef enum { + C_LINK_GPIO_DIRECTION_INPUT, + C_LINK_GPIO_DIRECTION_OUTPUT +} C_LinkGPIO_Direction; + +C_LinkGPIOHandle C_LinkGPIO_create(); +void C_LinkGPIO_destroy(C_LinkGPIOHandle handle); + +void C_LinkGPIO_reset(C_LinkGPIOHandle handle); +void C_LinkGPIO_setMode(C_LinkGPIOHandle handle, + C_LinkGPIO_Pin pin, + C_LinkGPIO_Direction direction); +C_LinkGPIO_Direction C_LinkGPIO_getMode(C_LinkGPIOHandle handle, + C_LinkGPIO_Pin pin); + +bool C_LinkGPIO_readPin(C_LinkGPIOHandle handle, C_LinkGPIO_Pin pin); +void C_LinkGPIO_writePin(C_LinkGPIOHandle handle, + C_LinkGPIO_Pin pin, + bool isHigh); + +void C_LinkGPIO_setSIInterrupts(C_LinkGPIOHandle handle, bool isEnabled); + +extern C_LinkGPIOHandle cLinkGPIO; + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_GPIO_H diff --git a/lib/c_bindings/C_LinkMobile.cpp b/lib/c_bindings/C_LinkMobile.cpp new file mode 100644 index 0000000..ab23b92 --- /dev/null +++ b/lib/c_bindings/C_LinkMobile.cpp @@ -0,0 +1,152 @@ +#include "C_LinkMobile.h" +#include "../LinkMobile.hpp" + +extern "C" { + +C_LinkMobileHandle C_LinkMobile_createDefault() { + return new LinkMobile(); +} + +C_LinkMobileHandle C_LinkMobile_create(u32 timeout, u8 timerId) { + return new LinkMobile(timeout, timerId); +} + +void C_LinkMobile_destroy(C_LinkMobileHandle handle) { + delete static_cast(handle); +} + +bool C_LinkMobile_isActive(C_LinkMobileHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkMobile_activate(C_LinkMobileHandle handle) { + static_cast(handle)->activate(); +} + +void C_LinkMobile_deactivate(C_LinkMobileHandle handle) { + static_cast(handle)->deactivate(); +} + +bool C_LinkMobile_shutdown(C_LinkMobileHandle handle) { + return static_cast(handle)->shutdown(); +} + +bool C_LinkMobile_call(C_LinkMobileHandle handle, const char* phoneNumber) { + return static_cast(handle)->call(phoneNumber); +} + +bool C_LinkMobile_callISP(C_LinkMobileHandle handle, + const char* password, + const char* loginId) { + return static_cast(handle)->callISP(password, loginId); +} + +bool C_LinkMobile_dnsQuery(C_LinkMobileHandle handle, + const char* domainName, + C_LinkMobile_DNSQuery* result) { + return static_cast(handle)->dnsQuery( + domainName, reinterpret_cast(result)); +} + +bool C_LinkMobile_openConnection(C_LinkMobileHandle handle, + const u8* ip, + u16 port, + C_LinkMobile_ConnectionType connectionType, + C_LinkMobile_OpenConn* result) { + return static_cast(handle)->openConnection( + ip, port, static_cast(connectionType), + reinterpret_cast(result)); +} + +bool C_LinkMobile_closeConnection(C_LinkMobileHandle handle, + u8 connectionId, + C_LinkMobile_ConnectionType connectionType, + C_LinkMobile_CloseConn* result) { + return static_cast(handle)->closeConnection( + connectionId, static_cast(connectionType), + reinterpret_cast(result)); +} + +bool C_LinkMobile_transfer(C_LinkMobileHandle handle, + C_LinkMobile_DataTransfer dataToSend, + C_LinkMobile_DataTransfer* result, + u8 connectionId) { + return static_cast(handle)->transfer( + *reinterpret_cast(&dataToSend), + reinterpret_cast(result), connectionId); +} + +bool C_LinkMobile_waitFor(C_LinkMobileHandle handle, void* asyncRequest) { + return static_cast(handle)->waitFor( + static_cast(asyncRequest)); +} + +bool C_LinkMobile_hangUp(C_LinkMobileHandle handle) { + return static_cast(handle)->hangUp(); +} + +bool C_LinkMobile_readConfiguration( + C_LinkMobileHandle handle, + C_LinkMobile_ConfigurationData* configurationData) { + return static_cast(handle)->readConfiguration( + *reinterpret_cast(configurationData)); +} + +C_LinkMobile_State C_LinkMobile_getState(C_LinkMobileHandle handle) { + return static_cast( + static_cast(handle)->getState()); +} + +C_LinkMobile_Role C_LinkMobile_getRole(C_LinkMobileHandle handle) { + return static_cast( + static_cast(handle)->getRole()); +} + +int C_LinkMobile_isConfigurationValid(C_LinkMobileHandle handle) { + return static_cast(handle)->isConfigurationValid(); +} + +bool C_LinkMobile_isConnectedP2P(C_LinkMobileHandle handle) { + return static_cast(handle)->isConnectedP2P(); +} + +bool C_LinkMobile_isConnectedPPP(C_LinkMobileHandle handle) { + return static_cast(handle)->isConnectedPPP(); +} + +bool C_LinkMobile_isSessionActive(C_LinkMobileHandle handle) { + return static_cast(handle)->isSessionActive(); +} + +bool C_LinkMobile_canShutdown(C_LinkMobileHandle handle) { + return static_cast(handle)->canShutdown(); +} + +C_LinkMobile_DataSize C_LinkMobile_getDataSize(C_LinkMobileHandle handle) { + return static_cast( + static_cast(handle)->getDataSize()); +} + +C_LinkMobile_Error C_LinkMobile_getError(C_LinkMobileHandle handle) { + LinkMobile::Error error = static_cast(handle)->getError(); + return {static_cast(error.type), + static_cast(error.state), + error.cmdId, + static_cast(error.cmdResult), + error.cmdErrorCode, + error.cmdIsSending, + error.reqType}; +} + +void C_LinkMobile_onVBlank(C_LinkMobileHandle handle) { + static_cast(handle)->_onVBlank(); +} + +void C_LinkMobile_onSerial(C_LinkMobileHandle handle) { + static_cast(handle)->_onSerial(); +} + +void C_LinkMobile_onTimer(C_LinkMobileHandle handle) { + static_cast(handle)->_onTimer(); +} +} diff --git a/lib/c_bindings/C_LinkMobile.h b/lib/c_bindings/C_LinkMobile.h new file mode 100644 index 0000000..b835184 --- /dev/null +++ b/lib/c_bindings/C_LinkMobile.h @@ -0,0 +1,212 @@ +#ifndef C_BINDINGS_LINK_MOBILE_H +#define C_BINDINGS_LINK_MOBILE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkMobileHandle; + +#define C_LINK_MOBILE_MAX_USER_TRANSFER_LENGTH 254 +#define C_LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH 255 +#define C_LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH 32 +#define C_LINK_MOBILE_MAX_LOGIN_ID_LENGTH 32 +#define C_LINK_MOBILE_MAX_PASSWORD_LENGTH 32 +#define C_LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH 253 +#define C_LINK_MOBILE_COMMAND_TRANSFER_BUFFER \ + (C_LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH + 4) +#define C_LINK_MOBILE_DEFAULT_TIMEOUT (60 * 10) +#define C_LINK_MOBILE_DEFAULT_TIMER_ID 3 + +typedef enum { + C_LINK_MOBILE_STATE_NEEDS_RESET, + C_LINK_MOBILE_STATE_PINGING, + C_LINK_MOBILE_STATE_WAITING_TO_START, + C_LINK_MOBILE_STATE_STARTING_SESSION, + C_LINK_MOBILE_STATE_ACTIVATING_SIO32, + C_LINK_MOBILE_STATE_WAITING_32BIT_SWITCH, + C_LINK_MOBILE_STATE_READING_CONFIGURATION, + C_LINK_MOBILE_STATE_SESSION_ACTIVE, + C_LINK_MOBILE_STATE_CALL_REQUESTED, + C_LINK_MOBILE_STATE_CALLING, + C_LINK_MOBILE_STATE_CALL_ESTABLISHED, + C_LINK_MOBILE_STATE_ISP_CALL_REQUESTED, + C_LINK_MOBILE_STATE_ISP_CALLING, + C_LINK_MOBILE_STATE_PPP_LOGIN, + C_LINK_MOBILE_STATE_PPP_ACTIVE, + C_LINK_MOBILE_STATE_SHUTDOWN_REQUESTED, + C_LINK_MOBILE_STATE_ENDING_SESSION, + C_LINK_MOBILE_STATE_WAITING_8BIT_SWITCH, + C_LINK_MOBILE_STATE_SHUTDOWN +} C_LinkMobile_State; + +enum C_LinkMobile_Role { + C_LINK_MOBILE_ROLE_NO_P2P_CONNECTION, + C_LINK_MOBILE_ROLE_CALLER, + C_LINK_MOBILE_ROLE_RECEIVER +}; + +enum C_LinkMobile_ConnectionType { + C_LINK_MOBILE_CONNECTION_TYPE_TCP, + C_LINK_MOBILE_CONNECTION_TYPE_UDP +}; + +typedef enum { + C_LINK_MOBILE_ERROR_TYPE_NONE, + C_LINK_MOBILE_ERROR_TYPE_ADAPTER_NOT_CONNECTED, + C_LINK_MOBILE_ERROR_TYPE_PPP_LOGIN_FAILED, + C_LINK_MOBILE_ERROR_TYPE_COMMAND_FAILED, + C_LINK_MOBILE_ERROR_TYPE_WEIRD_RESPONSE, + C_LINK_MOBILE_ERROR_TYPE_TIMEOUT, + C_LINK_MOBILE_ERROR_TYPE_WTF +} C_LinkMobile_ErrorType; + +typedef enum { + C_LINK_MOBILE_COMMAND_RESULT_PENDING, + C_LINK_MOBILE_COMMAND_RESULT_SUCCESS, + C_LINK_MOBILE_COMMAND_RESULT_INVALID_DEVICE_ID, + C_LINK_MOBILE_COMMAND_RESULT_INVALID_COMMAND_ACK, + C_LINK_MOBILE_COMMAND_RESULT_INVALID_MAGIC_BYTES, + C_LINK_MOBILE_COMMAND_RESULT_WEIRD_DATA_SIZE, + C_LINK_MOBILE_COMMAND_RESULT_WRONG_CHECKSUM, + C_LINK_MOBILE_COMMAND_RESULT_ERROR_CODE, + C_LINK_MOBILE_COMMAND_RESULT_WEIRD_ERROR_CODE +} C_LinkMobile_CommandResult; + +typedef struct { + C_LinkMobile_ErrorType type; + C_LinkMobile_State state; + u8 cmdId; + C_LinkMobile_CommandResult cmdResult; + u8 cmdErrorCode; + bool cmdIsSending; + int reqType; +} C_LinkMobile_Error; + +typedef struct { + volatile bool completed; + bool success; + + u8 ipv4[4]; +} C_LinkMobile_DNSQuery; + +typedef struct { + volatile bool completed; + bool success; + + u32 connectionId; +} C_LinkMobile_OpenConn; + +typedef struct { + volatile bool completed; + bool success; +} C_LinkMobile_CloseConn; + +typedef struct { + volatile bool completed; + bool success; + + u8 data[C_LINK_MOBILE_MAX_USER_TRANSFER_LENGTH]; + u8 size; +} C_LinkMobile_DataTransfer; + +typedef struct { + char magic[2]; + u8 registrationState; + 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; + + char _ispNumber1[16 + 1]; +} C_LinkMobile_ConfigurationData; + +typedef enum { + C_LINK_MOBILE_SIZE_32BIT, + C_LINK_MOBILE_SIZE_8BIT +} C_LinkMobile_DataSize; + +C_LinkMobileHandle C_LinkMobile_createDefault(); +C_LinkMobileHandle C_LinkMobile_create(u32 timeout, u8 timerId); +void C_LinkMobile_destroy(C_LinkMobileHandle handle); + +bool C_LinkMobile_isActive(C_LinkMobileHandle handle); +void C_LinkMobile_activate(C_LinkMobileHandle handle); +void C_LinkMobile_deactivate(C_LinkMobileHandle handle); +bool C_LinkMobile_shutdown(C_LinkMobileHandle handle); + +bool C_LinkMobile_call(C_LinkMobileHandle handle, const char* phoneNumber); +bool C_LinkMobile_callISP(C_LinkMobileHandle handle, + const char* password, + const char* loginId); + +bool C_LinkMobile_dnsQuery(C_LinkMobileHandle handle, + const char* domainName, + C_LinkMobile_DNSQuery* result); +bool C_LinkMobile_openConnection(C_LinkMobileHandle handle, + const u8* ip, + u16 port, + C_LinkMobile_ConnectionType connectionType, + C_LinkMobile_OpenConn* result); +bool C_LinkMobile_closeConnection(C_LinkMobileHandle handle, + u8 connectionId, + C_LinkMobile_ConnectionType connectionType, + C_LinkMobile_CloseConn* result); +bool C_LinkMobile_transfer(C_LinkMobileHandle handle, + C_LinkMobile_DataTransfer dataToSend, + C_LinkMobile_DataTransfer* result, + u8 connectionId); + +bool C_LinkMobile_waitFor(C_LinkMobileHandle handle, void* asyncRequest); +bool C_LinkMobile_hangUp(C_LinkMobileHandle handle); + +bool C_LinkMobile_readConfiguration( + C_LinkMobileHandle handle, + C_LinkMobile_ConfigurationData* configurationData); + +C_LinkMobile_State C_LinkMobile_getState(C_LinkMobileHandle handle); +C_LinkMobile_Role C_LinkMobile_getRole(C_LinkMobileHandle handle); +int C_LinkMobile_isConfigurationValid(C_LinkMobileHandle handle); +bool C_LinkMobile_isConnectedP2P(C_LinkMobileHandle handle); +bool C_LinkMobile_isConnectedPPP(C_LinkMobileHandle handle); +bool C_LinkMobile_isSessionActive(C_LinkMobileHandle handle); +bool C_LinkMobile_canShutdown(C_LinkMobileHandle handle); +C_LinkMobile_DataSize C_LinkMobile_getDataSize(C_LinkMobileHandle handle); +C_LinkMobile_Error C_LinkMobile_getError(C_LinkMobileHandle handle); + +void C_LinkMobile_onVBlank(C_LinkMobileHandle handle); +void C_LinkMobile_onSerial(C_LinkMobileHandle handle); +void C_LinkMobile_onTimer(C_LinkMobileHandle handle); + +extern C_LinkMobileHandle cLinkMobile; + +inline void C_LINK_MOBILE_ISR_VBLANK() { + C_LinkMobile_onVBlank(cLinkMobile); +} + +inline void C_LINK_MOBILE_ISR_SERIAL() { + C_LinkMobile_onSerial(cLinkMobile); +} + +inline void C_LINK_MOBILE_ISR_TIMER() { + C_LinkMobile_onTimer(cLinkMobile); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_MOBILE_H diff --git a/lib/c_bindings/C_LinkPS2Keyboard.cpp b/lib/c_bindings/C_LinkPS2Keyboard.cpp new file mode 100644 index 0000000..b164b03 --- /dev/null +++ b/lib/c_bindings/C_LinkPS2Keyboard.cpp @@ -0,0 +1,34 @@ +#include "C_LinkPS2Keyboard.h" +#include "../LinkPS2Keyboard.hpp" + +extern "C" { + +C_LinkPS2KeyboardHandle C_LinkPS2Keyboard_create( + C_LinkPS2Keyboard_EventCallback callback) { + return new LinkPS2Keyboard(callback); +} + +void C_LinkPS2Keyboard_destroy(C_LinkPS2KeyboardHandle handle) { + delete static_cast(handle); +} + +bool C_LinkPS2Keyboard_isActive(C_LinkPS2KeyboardHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkPS2Keyboard_activate(C_LinkPS2KeyboardHandle handle) { + static_cast(handle)->activate(); +} + +void C_LinkPS2Keyboard_deactivate(C_LinkPS2KeyboardHandle handle) { + static_cast(handle)->deactivate(); +} + +void C_LinkPS2Keyboard_onVBlank(C_LinkPS2KeyboardHandle handle) { + static_cast(handle)->_onVBlank(); +} + +void C_LinkPS2Keyboard_onSerial(C_LinkPS2KeyboardHandle handle) { + static_cast(handle)->_onSerial(); +} +} diff --git a/lib/c_bindings/C_LinkPS2Keyboard.h b/lib/c_bindings/C_LinkPS2Keyboard.h new file mode 100644 index 0000000..a9721ec --- /dev/null +++ b/lib/c_bindings/C_LinkPS2Keyboard.h @@ -0,0 +1,39 @@ +#ifndef C_BINDINGS_LINK_PS2_KEYBOARD_H +#define C_BINDINGS_LINK_PS2_KEYBOARD_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void (*C_LinkPS2Keyboard_EventCallback)(u8 event); + +typedef void* C_LinkPS2KeyboardHandle; + +C_LinkPS2KeyboardHandle C_LinkPS2Keyboard_create( + C_LinkPS2Keyboard_EventCallback callback); +void C_LinkPS2Keyboard_destroy(C_LinkPS2KeyboardHandle handle); + +bool C_LinkPS2Keyboard_isActive(C_LinkPS2KeyboardHandle handle); +void C_LinkPS2Keyboard_activate(C_LinkPS2KeyboardHandle handle); +void C_LinkPS2Keyboard_deactivate(C_LinkPS2KeyboardHandle handle); + +void C_LinkPS2Keyboard_onVBlank(C_LinkPS2KeyboardHandle handle); +void C_LinkPS2Keyboard_onSerial(C_LinkPS2KeyboardHandle handle); + +extern C_LinkPS2KeyboardHandle cLinkPS2Keyboard; + +inline void C_LINK_PS2_KEYBOARD_ISR_VBLANK() { + C_LinkPS2Keyboard_onVBlank(cLinkPS2Keyboard); +} + +inline void C_LINK_PS2_KEYBOARD_ISR_SERIAL() { + C_LinkPS2Keyboard_onSerial(cLinkPS2Keyboard); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_PS2_KEYBOARD_H diff --git a/lib/c_bindings/C_LinkPS2Mouse.cpp b/lib/c_bindings/C_LinkPS2Mouse.cpp new file mode 100644 index 0000000..557e16e --- /dev/null +++ b/lib/c_bindings/C_LinkPS2Mouse.cpp @@ -0,0 +1,33 @@ +#include "C_LinkPS2Mouse.h" +#include "../LinkPS2Mouse.hpp" + +extern "C" { + +C_LinkPS2MouseHandle C_LinkPS2Mouse_create(u8 waitTimerId) { + return new LinkPS2Mouse(waitTimerId); +} + +void C_LinkPS2Mouse_destroy(C_LinkPS2MouseHandle handle) { + delete static_cast(handle); +} + +bool C_LinkPS2Mouse_isActive(C_LinkPS2MouseHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkPS2Mouse_activate(C_LinkPS2MouseHandle handle) { + static_cast(handle)->activate(); +} + +void C_LinkPS2Mouse_deactivate(C_LinkPS2MouseHandle handle) { + static_cast(handle)->deactivate(); +} + +void C_LinkPS2Mouse_report(C_LinkPS2MouseHandle handle, int data[3]) { + int d[3]; + static_cast(handle)->report(d); + for (u32 i = 0; i < 3; i++) { + data[i] = d[i]; + } +} +} diff --git a/lib/c_bindings/C_LinkPS2Mouse.h b/lib/c_bindings/C_LinkPS2Mouse.h new file mode 100644 index 0000000..876fccc --- /dev/null +++ b/lib/c_bindings/C_LinkPS2Mouse.h @@ -0,0 +1,31 @@ +#ifndef C_BINDINGS_LINK_PS2_MOUSE_H +#define C_BINDINGS_LINK_PS2_MOUSE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkPS2MouseHandle; + +#define C_LINK_PS2_MOUSE_LEFT_CLICK 0b001 +#define C_LINK_PS2_MOUSE_RIGHT_CLICK 0b010 +#define C_LINK_PS2_MOUSE_MIDDLE_CLICK 0b100 + +C_LinkPS2MouseHandle C_LinkPS2Mouse_create(u8 waitTimerId); +void C_LinkPS2Mouse_destroy(C_LinkPS2MouseHandle handle); + +bool C_LinkPS2Mouse_isActive(C_LinkPS2MouseHandle handle); +void C_LinkPS2Mouse_activate(C_LinkPS2MouseHandle handle); +void C_LinkPS2Mouse_deactivate(C_LinkPS2MouseHandle handle); + +void C_LinkPS2Mouse_report(C_LinkPS2MouseHandle handle, int data[3]); + +extern C_LinkPS2MouseHandle cLinkPS2Mouse; + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_PS2_MOUSE_H diff --git a/lib/c_bindings/C_LinkRawCable.cpp b/lib/c_bindings/C_LinkRawCable.cpp new file mode 100644 index 0000000..f9ca9b5 --- /dev/null +++ b/lib/c_bindings/C_LinkRawCable.cpp @@ -0,0 +1,85 @@ +#include "C_LinkRawCable.h" +#include +#include "../LinkRawCable.hpp" + +extern "C" { + +C_LinkRawCableHandle C_LinkRawCable_create() { + return new LinkRawCable(); +} + +void C_LinkRawCable_destroy(C_LinkRawCableHandle handle) { + delete static_cast(handle); +} + +bool C_LinkRawCable_isActive(C_LinkRawCableHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkRawCable_activate(C_LinkRawCableHandle handle, + C_LinkRawCable_BaudRate baudRate) { + static_cast(handle)->activate( + static_cast(baudRate)); +} + +void C_LinkRawCable_deactivate(C_LinkRawCableHandle handle) { + static_cast(handle)->deactivate(); +} + +C_LinkRawCable_Response C_LinkRawCable_transfer(C_LinkRawCableHandle handle, + u16 data) { + auto response = static_cast(handle)->transfer(data); + C_LinkRawCable_Response cResponse; + memcpy(cResponse.data, response.data, sizeof(response.data)); + cResponse.playerId = response.playerId; + return cResponse; +} + +C_LinkRawCable_Response C_LinkRawCable_transferWithCancel( + C_LinkRawCableHandle handle, + u16 data, + bool (*cancel)()) { + auto response = static_cast(handle)->transfer(data, cancel); + C_LinkRawCable_Response cResponse; + memcpy(cResponse.data, response.data, sizeof(response.data)); + cResponse.playerId = response.playerId; + return cResponse; +} + +void C_LinkRawCable_transferAsync(C_LinkRawCableHandle handle, u16 data) { + static_cast(handle)->transferAsync(data); +} + +C_LinkRawCable_AsyncState C_LinkRawCable_getAsyncState( + C_LinkRawCableHandle handle) { + return static_cast( + static_cast(handle)->getAsyncState()); +} + +C_LinkRawCable_Response C_LinkRawCable_getAsyncData( + C_LinkRawCableHandle handle) { + auto response = static_cast(handle)->getAsyncData(); + C_LinkRawCable_Response cResponse; + memcpy(cResponse.data, response.data, sizeof(response.data)); + cResponse.playerId = response.playerId; + return cResponse; +} + +C_LinkRawCable_BaudRate C_LinkRawCable_getBaudRate( + C_LinkRawCableHandle handle) { + return static_cast( + static_cast(handle)->getBaudRate()); +} + +bool C_LinkRawCable_isMaster(C_LinkRawCableHandle handle) { + return static_cast(handle)->isMaster(); +} + +bool C_LinkRawCable_isReady(C_LinkRawCableHandle handle) { + return static_cast(handle)->isReady(); +} + +void C_LinkRawCable_onSerial(C_LinkRawCableHandle handle) { + static_cast(handle)->_onSerial(); +} +} diff --git a/lib/c_bindings/C_LinkRawCable.h b/lib/c_bindings/C_LinkRawCable.h new file mode 100644 index 0000000..ff302b2 --- /dev/null +++ b/lib/c_bindings/C_LinkRawCable.h @@ -0,0 +1,69 @@ +#ifndef C_BINDINGS_LINK_RAW_CABLE_H +#define C_BINDINGS_LINK_RAW_CABLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkRawCableHandle; + +#define C_LINK_RAW_CABLE_MAX_PLAYERS 4 +#define C_LINK_RAW_CABLE_DISCONNECTED 0xffff + +typedef enum { + C_LINK_RAW_CABLE_BAUD_RATE_0, // 9600 bps + C_LINK_RAW_CABLE_BAUD_RATE_1, // 38400 bps + C_LINK_RAW_CABLE_BAUD_RATE_2, // 57600 bps + C_LINK_RAW_CABLE_BAUD_RATE_3 // 115200 bps +} C_LinkRawCable_BaudRate; + +typedef enum { + C_LINK_RAW_CABLE_ASYNC_STATE_IDLE, + C_LINK_RAW_CABLE_ASYNC_STATE_WAITING, + C_LINK_RAW_CABLE_ASYNC_STATE_READY +} C_LinkRawCable_AsyncState; + +typedef struct { + u16 data[4]; + int playerId; +} C_LinkRawCable_Response; + +C_LinkRawCableHandle C_LinkRawCable_create(); +void C_LinkRawCable_destroy(C_LinkRawCableHandle handle); + +bool C_LinkRawCable_isActive(C_LinkRawCableHandle handle); +void C_LinkRawCable_activate(C_LinkRawCableHandle handle, + C_LinkRawCable_BaudRate baudRate); +void C_LinkRawCable_deactivate(C_LinkRawCableHandle handle); + +C_LinkRawCable_Response C_LinkRawCable_transfer(C_LinkRawCableHandle handle, + u16 data); +C_LinkRawCable_Response C_LinkRawCable_transferWithCancel( + C_LinkRawCableHandle handle, + u16 data, + bool (*cancel)()); +void C_LinkRawCable_transferAsync(C_LinkRawCableHandle handle, u16 data); + +C_LinkRawCable_AsyncState C_LinkRawCable_getAsyncState( + C_LinkRawCableHandle handle); +C_LinkRawCable_Response C_LinkRawCable_getAsyncData( + C_LinkRawCableHandle handle); +C_LinkRawCable_BaudRate C_LinkRawCable_getBaudRate(C_LinkRawCableHandle handle); +bool C_LinkRawCable_isMaster(C_LinkRawCableHandle handle); +bool C_LinkRawCable_isReady(C_LinkRawCableHandle handle); + +void C_LinkRawCable_onSerial(C_LinkRawCableHandle handle); + +extern C_LinkRawCableHandle cLinkRawCable; + +inline void C_LINK_RAW_CABLE_ISR_SERIAL() { + C_LinkRawCable_onSerial(cLinkRawCable); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_RAW_CABLE_H diff --git a/lib/c_bindings/C_LinkRawWireless.cpp b/lib/c_bindings/C_LinkRawWireless.cpp new file mode 100644 index 0000000..3ec96f8 --- /dev/null +++ b/lib/c_bindings/C_LinkRawWireless.cpp @@ -0,0 +1,227 @@ +#include "C_LinkRawWireless.h" +#include "../LinkRawWireless.hpp" + +extern "C" { +C_LinkRawWirelessHandle C_LinkRawWireless_create() { + return new LinkRawWireless(); +} + +void C_LinkRawWireless_destroy(C_LinkRawWirelessHandle handle) { + delete static_cast(handle); +} + +bool C_LinkRawWireless_isActive(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->isActive(); +} + +bool C_LinkRawWireless_activate(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->activate(); +} + +bool C_LinkRawWireless_deactivate(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->deactivate(); +} + +bool C_LinkRawWireless_setup(C_LinkRawWirelessHandle handle, + u8 maxPlayers, + u8 maxTransmissions, + u8 waitTimeout, + u32 magic) { + return static_cast(handle)->setup( + maxPlayers, maxTransmissions, waitTimeout, magic); +} + +bool C_LinkRawWireless_broadcast(C_LinkRawWirelessHandle handle, + const char* gameName, + const char* userName, + u16 gameId) { + return static_cast(handle)->broadcast(gameName, userName, + gameId); +} + +bool C_LinkRawWireless_startHost(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->startHost(); +} + +bool C_LinkRawWireless_getSlotStatus( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_SlotStatusResponse* response) { + LinkRawWireless::SlotStatusResponse nativeResponse; + bool success = + static_cast(handle)->getSlotStatus(nativeResponse); + response->nextClientNumber = nativeResponse.nextClientNumber; + response->connectedClientsSize = nativeResponse.connectedClientsSize; + for (u32 i = 0; i < response->connectedClientsSize; i++) { + response->connectedClients[i].deviceId = + nativeResponse.connectedClients[i].deviceId; + response->connectedClients[i].clientNumber = + nativeResponse.connectedClients[i].clientNumber; + } + return success; +} + +bool C_LinkRawWireless_acceptConnections( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_AcceptConnectionsResponse* response) { + LinkRawWireless::AcceptConnectionsResponse nativeResponse; + bool success = + static_cast(handle)->acceptConnections(nativeResponse); + response->connectedClientsSize = nativeResponse.connectedClientsSize; + for (u32 i = 0; i < response->connectedClientsSize; i++) { + response->connectedClients[i].deviceId = + nativeResponse.connectedClients[i].deviceId; + response->connectedClients[i].clientNumber = + nativeResponse.connectedClients[i].clientNumber; + } + return success; +} + +bool C_LinkRawWireless_endHost( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_AcceptConnectionsResponse* response) { + LinkRawWireless::AcceptConnectionsResponse nativeResponse; + bool success = static_cast(handle)->endHost(nativeResponse); + response->connectedClientsSize = nativeResponse.connectedClientsSize; + for (u32 i = 0; i < response->connectedClientsSize; i++) { + response->connectedClients[i].deviceId = + nativeResponse.connectedClients[i].deviceId; + response->connectedClients[i].clientNumber = + nativeResponse.connectedClients[i].clientNumber; + } + return success; +} + +bool C_LinkRawWireless_broadcastReadStart(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->broadcastReadStart(); +} + +bool C_LinkRawWireless_broadcastReadPoll( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_BroadcastReadPollResponse* response) { + LinkRawWireless::BroadcastReadPollResponse nativeResponse; + bool success = + static_cast(handle)->broadcastReadPoll(nativeResponse); + response->serversSize = nativeResponse.serversSize; + for (u32 i = 0; i < response->serversSize; i++) { + response->servers[i].id = nativeResponse.servers[i].id; + response->servers[i].gameId = nativeResponse.servers[i].gameId; + std::memcpy(response->servers[i].gameName, + nativeResponse.servers[i].gameName, + LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH + 1); + std::memcpy(response->servers[i].userName, + nativeResponse.servers[i].userName, + LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH + 1); + response->servers[i].nextClientNumber = + nativeResponse.servers[i].nextClientNumber; + } + return success; +} + +bool C_LinkRawWireless_broadcastReadEnd(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->broadcastReadEnd(); +} + +bool C_LinkRawWireless_connect(C_LinkRawWirelessHandle handle, u16 serverId) { + return static_cast(handle)->connect(serverId); +} + +bool C_LinkRawWireless_keepConnecting( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_ConnectionStatus* response) { + LinkRawWireless::ConnectionStatus nativeResponse; + bool success = + static_cast(handle)->keepConnecting(nativeResponse); + response->phase = + static_cast(nativeResponse.phase); + response->assignedClientNumber = nativeResponse.assignedClientNumber; + return success; +} + +bool C_LinkRawWireless_finishConnection(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->finishConnection(); +} + +bool C_LinkRawWireless_sendData(C_LinkRawWirelessHandle handle, + u32* data, + u32 dataSize, + u32 _bytes) { + std::array dataArray; + std::memcpy(dataArray.data(), data, dataSize * sizeof(u32)); + return static_cast(handle)->sendData(dataArray, dataSize, + _bytes); +} + +bool C_LinkRawWireless_sendDataAndWait( + C_LinkRawWirelessHandle handle, + u32* data, + u32 dataSize, + C_LinkRawWireless_RemoteCommand* remoteCommand, + u32 _bytes) { + std::array dataArray; + LinkRawWireless::RemoteCommand nativeRemoteCommand; + std::memcpy(dataArray.data(), data, dataSize * sizeof(u32)); + bool success = static_cast(handle)->sendDataAndWait( + dataArray, dataSize, nativeRemoteCommand, _bytes); + remoteCommand->success = nativeRemoteCommand.success; + remoteCommand->commandId = nativeRemoteCommand.commandId; + remoteCommand->paramsSize = nativeRemoteCommand.paramsSize; + std::memcpy(remoteCommand->params, nativeRemoteCommand.params, + LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH * sizeof(u32)); + return success; +} + +bool C_LinkRawWireless_receiveData( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_ReceiveDataResponse* response) { + LinkRawWireless::ReceiveDataResponse nativeResponse; + bool success = + static_cast(handle)->receiveData(nativeResponse); + for (u32 i = 0; i < nativeResponse.dataSize; i++) { + response->data[i] = nativeResponse.data[i]; + } + response->dataSize = nativeResponse.dataSize; + for (u32 i = 0; i < LINK_RAW_WIRELESS_MAX_PLAYERS; i++) { + response->sentBytes[i] = nativeResponse.sentBytes[i]; + } + return success; +} + +bool C_LinkRawWireless_wait(C_LinkRawWirelessHandle handle, + C_LinkRawWireless_RemoteCommand* remoteCommand) { + LinkRawWireless::RemoteCommand nativeRemoteCommand; + bool success = + static_cast(handle)->wait(nativeRemoteCommand); + remoteCommand->success = nativeRemoteCommand.success; + remoteCommand->commandId = nativeRemoteCommand.commandId; + remoteCommand->paramsSize = nativeRemoteCommand.paramsSize; + std::memcpy(remoteCommand->params, nativeRemoteCommand.params, + LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH * sizeof(u32)); + return success; +} + +u32 C_LinkRawWireless_getDeviceTransferLength(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->getDeviceTransferLength(); +} + +C_LinkRawWireless_State C_LinkRawWireless_getState( + C_LinkRawWirelessHandle handle) { + return static_cast( + static_cast(handle)->getState()); +} + +bool C_LinkRawWireless_isConnected(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->isConnected(); +} + +bool C_LinkRawWireless_isSessionActive(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->isSessionActive(); +} + +u8 C_LinkRawWireless_playerCount(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->playerCount(); +} + +u8 C_LinkRawWireless_currentPlayerId(C_LinkRawWirelessHandle handle) { + return static_cast(handle)->currentPlayerId(); +} +} diff --git a/lib/c_bindings/C_LinkRawWireless.h b/lib/c_bindings/C_LinkRawWireless.h new file mode 100644 index 0000000..2e79687 --- /dev/null +++ b/lib/c_bindings/C_LinkRawWireless.h @@ -0,0 +1,159 @@ +#ifndef C_BINDINGS_LINK_RAW_WIRELESS_H +#define C_BINDINGS_LINK_RAW_WIRELESS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkRawWirelessHandle; + +#define C_LINK_RAW_WIRELESS_MAX_PLAYERS 5 +#define C_LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30 +#define C_LINK_RAW_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4 +#define C_LINK_RAW_WIRELESS_MAX_GAME_ID 0x7fff +#define C_LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH 14 +#define C_LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH 8 +#define C_LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 23 +#define C_LINK_RAW_WIRELESS_SETUP_MAGIC 0x003c0000 + +#define C_LINK_RAW_WIRELESS_MAX_SERVERS 4 + +typedef enum { + C_LINK_RAW_WIRELESS_STATE_NEEDS_RESET, + C_LINK_RAW_WIRELESS_STATE_AUTHENTICATED, + C_LINK_RAW_WIRELESS_STATE_SEARCHING, + C_LINK_RAW_WIRELESS_STATE_SERVING, + C_LINK_RAW_WIRELESS_STATE_CONNECTING, + C_LINK_RAW_WIRELESS_STATE_CONNECTED +} C_LinkRawWireless_State; + +typedef struct { + bool success; + u32 responses[C_LINK_RAW_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH]; + u32 responsesSize; +} C_LinkRawWireless_CommandResult; + +typedef struct { + bool success; + u8 commandId; + u32 params[C_LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; + u32 paramsSize; +} C_LinkRawWireless_RemoteCommand; + +typedef struct { + u16 id; + u16 gameId; + char gameName[C_LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH + 1]; + char userName[C_LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH + 1]; + u8 nextClientNumber; +} C_LinkRawWireless_Server; + +typedef struct { + u16 deviceId; + u8 clientNumber; +} C_LinkRawWireless_ConnectedClient; + +typedef struct { + u8 nextClientNumber; + C_LinkRawWireless_ConnectedClient + connectedClients[C_LINK_RAW_WIRELESS_MAX_PLAYERS]; + u32 connectedClientsSize; +} C_LinkRawWireless_SlotStatusResponse; + +typedef struct { + C_LinkRawWireless_ConnectedClient + connectedClients[C_LINK_RAW_WIRELESS_MAX_PLAYERS]; + u32 connectedClientsSize; +} C_LinkRawWireless_AcceptConnectionsResponse; + +typedef struct { + C_LinkRawWireless_Server servers[C_LINK_RAW_WIRELESS_MAX_SERVERS]; + u32 serversSize; +} C_LinkRawWireless_BroadcastReadPollResponse; + +typedef enum { + C_LINK_RAW_WIRELESS_CONNECTION_PHASE_STILL_CONNECTING, + C_LINK_RAW_WIRELESS_CONNECTION_PHASE_ERROR, + C_LINK_RAW_WIRELESS_CONNECTION_PHASE_SUCCESS +} C_LinkRawWireless_ConnectionPhase; + +typedef struct { + C_LinkRawWireless_ConnectionPhase phase; + u8 assignedClientNumber; +} C_LinkRawWireless_ConnectionStatus; + +typedef struct { + u32 sentBytes[C_LINK_RAW_WIRELESS_MAX_PLAYERS]; + u32 data[C_LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; + u32 dataSize; +} C_LinkRawWireless_ReceiveDataResponse; + +C_LinkRawWirelessHandle C_LinkRawWireless_create(); +void C_LinkRawWireless_destroy(C_LinkRawWirelessHandle handle); + +bool C_LinkRawWireless_isActive(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_activate(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_deactivate(C_LinkRawWirelessHandle handle); + +bool C_LinkRawWireless_setup(C_LinkRawWirelessHandle handle, + u8 maxPlayers, + u8 maxTransmissions, + u8 waitTimeout, + u32 magic); +bool C_LinkRawWireless_broadcast(C_LinkRawWirelessHandle handle, + const char* gameName, + const char* userName, + u16 gameId); +bool C_LinkRawWireless_startHost(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_getSlotStatus( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_SlotStatusResponse* response); +bool C_LinkRawWireless_acceptConnections( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_AcceptConnectionsResponse* response); +bool C_LinkRawWireless_endHost( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_AcceptConnectionsResponse* response); +bool C_LinkRawWireless_broadcastReadStart(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_broadcastReadPoll( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_BroadcastReadPollResponse* response); +bool C_LinkRawWireless_broadcastReadEnd(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_connect(C_LinkRawWirelessHandle handle, u16 serverId); +bool C_LinkRawWireless_keepConnecting( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_ConnectionStatus* response); +bool C_LinkRawWireless_finishConnection(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_sendData(C_LinkRawWirelessHandle handle, + u32* data, + u32 dataSize, + u32 _bytes); +bool C_LinkRawWireless_sendDataAndWait( + C_LinkRawWirelessHandle handle, + u32* data, + u32 dataSize, + C_LinkRawWireless_RemoteCommand* remoteCommand, + u32 _bytes); +bool C_LinkRawWireless_receiveData( + C_LinkRawWirelessHandle handle, + C_LinkRawWireless_ReceiveDataResponse* response); +bool C_LinkRawWireless_wait(C_LinkRawWirelessHandle handle, + C_LinkRawWireless_RemoteCommand* remoteCommand); + +u32 C_LinkRawWireless_getDeviceTransferLength(C_LinkRawWirelessHandle handle); +C_LinkRawWireless_State C_LinkRawWireless_getState( + C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_isConnected(C_LinkRawWirelessHandle handle); +bool C_LinkRawWireless_isSessionActive(C_LinkRawWirelessHandle handle); +u8 C_LinkRawWireless_playerCount(C_LinkRawWirelessHandle handle); +u8 C_LinkRawWireless_currentPlayerId(C_LinkRawWirelessHandle handle); + +extern C_LinkRawWirelessHandle cRawWireless; + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_RAW_WIRELESS_H diff --git a/lib/c_bindings/C_LinkSPI.cpp b/lib/c_bindings/C_LinkSPI.cpp new file mode 100644 index 0000000..aa7ddf2 --- /dev/null +++ b/lib/c_bindings/C_LinkSPI.cpp @@ -0,0 +1,79 @@ +#include "C_LinkSPI.h" +#include "../LinkSPI.hpp" + +extern "C" { + +C_LinkSPIHandle C_LinkSPI_create() { + return new LinkSPI(); +} + +void C_LinkSPI_destroy(C_LinkSPIHandle handle) { + delete static_cast(handle); +} + +bool C_LinkSPI_isActive(C_LinkSPIHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkSPI_activate(C_LinkSPIHandle handle, + C_LinkSPI_Mode mode, + C_LinkSPI_DataSize dataSize) { + static_cast(handle)->activate( + static_cast(mode), + static_cast(dataSize)); +} + +void C_LinkSPI_deactivate(C_LinkSPIHandle handle) { + static_cast(handle)->deactivate(); +} + +u32 C_LinkSPI_transfer(C_LinkSPIHandle handle, u32 data) { + return static_cast(handle)->transfer(data); +} + +u32 C_LinkSPI_transferWithCancel(C_LinkSPIHandle handle, + u32 data, + bool (*cancel)()) { + return static_cast(handle)->transfer(data, cancel); +} + +void C_LinkSPI_transferAsync(C_LinkSPIHandle handle, u32 data) { + static_cast(handle)->transferAsync(data); +} + +void C_LinkSPI_transferAsyncWithCancel(C_LinkSPIHandle handle, + u32 data, + bool (*cancel)()) { + static_cast(handle)->transferAsync(data, cancel); +} + +C_LinkSPI_AsyncState C_LinkSPI_getAsyncState(C_LinkSPIHandle handle) { + return static_cast( + static_cast(handle)->getAsyncState()); +} + +u32 C_LinkSPI_getAsyncData(C_LinkSPIHandle handle) { + return static_cast(handle)->getAsyncData(); +} + +C_LinkSPI_Mode C_LinkSPI_getMode(C_LinkSPIHandle handle) { + return static_cast(static_cast(handle)->getMode()); +} + +C_LinkSPI_DataSize C_LinkSPI_getDataSize(C_LinkSPIHandle handle) { + return static_cast( + static_cast(handle)->getDataSize()); +} + +void C_LinkSPI_setWaitModeActive(C_LinkSPIHandle handle, bool isActive) { + static_cast(handle)->setWaitModeActive(isActive); +} + +bool C_LinkSPI_isWaitModeActive(C_LinkSPIHandle handle) { + return static_cast(handle)->isWaitModeActive(); +} + +void C_LinkSPI_onSerial(C_LinkSPIHandle handle, bool customAck) { + static_cast(handle)->_onSerial(customAck); +} +} diff --git a/lib/c_bindings/C_LinkSPI.h b/lib/c_bindings/C_LinkSPI.h new file mode 100644 index 0000000..16155d6 --- /dev/null +++ b/lib/c_bindings/C_LinkSPI.h @@ -0,0 +1,70 @@ +#ifndef C_BINDINGS_LINK_SPI_H +#define C_BINDINGS_LINK_SPI_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkSPIHandle; + +#define C_LINK_SPI_NO_DATA_32 0xffffffff +#define C_LINK_SPI_NO_DATA_8 0xff +#define C_LINK_SPI_NO_DATA LINK_SPI_NO_DATA_32 + +typedef enum { + C_LINK_SPI_MODE_SLAVE, + C_LINK_SPI_MODE_MASTER_256KBPS, + C_LINK_SPI_MODE_MASTER_2MBPS +} C_LinkSPI_Mode; + +typedef enum { C_LINK_SPI_SIZE_32BIT, C_LINK_SPI_SIZE_8BIT } C_LinkSPI_DataSize; + +typedef enum { + C_LINK_SPI_ASYNC_STATE_IDLE, + C_LINK_SPI_ASYNC_STATE_WAITING, + C_LINK_SPI_ASYNC_STATE_READY +} C_LinkSPI_AsyncState; + +C_LinkSPIHandle C_LinkSPI_create(); +void C_LinkSPI_destroy(C_LinkSPIHandle handle); + +bool C_LinkSPI_isActive(C_LinkSPIHandle handle); +void C_LinkSPI_activate(C_LinkSPIHandle handle, + C_LinkSPI_Mode mode, + C_LinkSPI_DataSize dataSize); +void C_LinkSPI_deactivate(C_LinkSPIHandle handle); + +u32 C_LinkSPI_transfer(C_LinkSPIHandle handle, u32 data); +u32 C_LinkSPI_transferWithCancel(C_LinkSPIHandle handle, + u32 data, + bool (*cancel)()); + +void C_LinkSPI_transferAsync(C_LinkSPIHandle handle, u32 data); +void C_LinkSPI_transferAsyncWithCancel(C_LinkSPIHandle handle, + u32 data, + bool (*cancel)()); + +C_LinkSPI_AsyncState C_LinkSPI_getAsyncState(C_LinkSPIHandle handle); +u32 C_LinkSPI_getAsyncData(C_LinkSPIHandle handle); + +C_LinkSPI_Mode C_LinkSPI_getMode(C_LinkSPIHandle handle); +C_LinkSPI_DataSize C_LinkSPI_getDataSize(C_LinkSPIHandle handle); + +void C_LinkSPI_setWaitModeActive(C_LinkSPIHandle handle, bool isActive); +bool C_LinkSPI_isWaitModeActive(C_LinkSPIHandle handle); + +void C_LinkSPI_onSerial(C_LinkSPIHandle handle, bool customAck); + +extern C_LinkSPIHandle cLinkSPI; + +inline void C_LINK_SPI_ISR_SERIAL() { + C_LinkSPI_onSerial(cLinkSPI, false); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_SPI_H diff --git a/lib/c_bindings/C_LinkUART.cpp b/lib/c_bindings/C_LinkUART.cpp new file mode 100644 index 0000000..492b31f --- /dev/null +++ b/lib/c_bindings/C_LinkUART.cpp @@ -0,0 +1,92 @@ +#include "C_LinkUART.h" +#include "../LinkUART.hpp" + +extern "C" { + +C_LinkUARTHandle C_LinkUART_create() { + return new LinkUART(); +} + +void C_LinkUART_destroy(C_LinkUARTHandle handle) { + delete static_cast(handle); +} + +bool C_LinkUART_isActive(C_LinkUARTHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkUART_activate(C_LinkUARTHandle handle, + C_LinkUART_BaudRate baudRate, + C_LinkUART_DataSize dataSize, + C_LinkUART_Parity parity, + bool useCTS) { + static_cast(handle)->activate( + static_cast(baudRate), + static_cast(dataSize), + static_cast(parity), useCTS); +} + +void C_LinkUART_deactivate(C_LinkUARTHandle handle) { + static_cast(handle)->deactivate(); +} + +void C_LinkUART_sendLine(C_LinkUARTHandle handle, const char* string) { + static_cast(handle)->sendLine(string); +} + +void C_LinkUART_sendLineWithCancel(C_LinkUARTHandle handle, + const char* string, + bool (*cancel)()) { + static_cast(handle)->sendLine(string, cancel); +} + +bool C_LinkUART_readLine(C_LinkUARTHandle handle, char* string, u32 limit) { + return static_cast(handle)->readLine(string, limit); +} + +bool C_LinkUART_readLineWithCancel(C_LinkUARTHandle handle, + char* string, + bool (*cancel)(), + u32 limit) { + return static_cast(handle)->readLine(string, cancel, limit); +} + +void C_LinkUART_send(C_LinkUARTHandle handle, + const u8* buffer, + u32 size, + u32 offset) { + static_cast(handle)->send(buffer, size, offset); +} + +u32 C_LinkUART_read(C_LinkUARTHandle handle, u8* buffer, u32 size, u32 offset) { + return static_cast(handle)->read(buffer, size, offset); +} + +bool C_LinkUART_canRead(C_LinkUARTHandle handle) { + return static_cast(handle)->canRead(); +} + +bool C_LinkUART_canSend(C_LinkUARTHandle handle) { + return static_cast(handle)->canSend(); +} + +u32 C_LinkUART_availableForRead(C_LinkUARTHandle handle) { + return static_cast(handle)->availableForRead(); +} + +u32 C_LinkUART_availableForSend(C_LinkUARTHandle handle) { + return static_cast(handle)->availableForSend(); +} + +u8 C_LinkUART_readByte(C_LinkUARTHandle handle) { + return static_cast(handle)->read(); +} + +void C_LinkUART_sendByte(C_LinkUARTHandle handle, u8 data) { + static_cast(handle)->send(data); +} + +void C_LinkUART_onSerial(C_LinkUARTHandle handle) { + static_cast(handle)->_onSerial(); +} +} diff --git a/lib/c_bindings/C_LinkUART.h b/lib/c_bindings/C_LinkUART.h new file mode 100644 index 0000000..8803b54 --- /dev/null +++ b/lib/c_bindings/C_LinkUART.h @@ -0,0 +1,78 @@ +#ifndef C_BINDINGS_LINK_UART_H +#define C_BINDINGS_LINK_UART_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkUARTHandle; + +typedef enum { + C_LINK_UART_BAUD_RATE_0, // 9600 bps + C_LINK_UART_BAUD_RATE_1, // 38400 bps + C_LINK_UART_BAUD_RATE_2, // 57600 bps + C_LINK_UART_BAUD_RATE_3 // 115200 bps +} C_LinkUART_BaudRate; + +typedef enum { + C_LINK_UART_SIZE_7_BITS, + C_LINK_UART_SIZE_8_BITS +} C_LinkUART_DataSize; + +typedef enum { + C_LINK_UART_PARITY_NO, + C_LINK_UART_PARITY_EVEN, + C_LINK_UART_PARITY_ODD +} C_LinkUART_Parity; + +C_LinkUARTHandle C_LinkUART_create(); +void C_LinkUART_destroy(C_LinkUARTHandle handle); + +bool C_LinkUART_isActive(C_LinkUARTHandle handle); +void C_LinkUART_activate(C_LinkUARTHandle handle, + C_LinkUART_BaudRate baudRate, + C_LinkUART_DataSize dataSize, + C_LinkUART_Parity parity, + bool useCTS); +void C_LinkUART_deactivate(C_LinkUARTHandle handle); + +void C_LinkUART_sendLine(C_LinkUARTHandle handle, const char* string); +void C_LinkUART_sendLineWithCancel(C_LinkUARTHandle handle, + const char* string, + bool (*cancel)()); + +bool C_LinkUART_readLine(C_LinkUARTHandle handle, char* string, u32 limit); +bool C_LinkUART_readLineWithCancel(C_LinkUARTHandle handle, + char* string, + bool (*cancel)(), + u32 limit); + +void C_LinkUART_send(C_LinkUARTHandle handle, + const u8* buffer, + u32 size, + u32 offset); +u32 C_LinkUART_read(C_LinkUARTHandle handle, u8* buffer, u32 size, u32 offset); + +bool C_LinkUART_canRead(C_LinkUARTHandle handle); +bool C_LinkUART_canSend(C_LinkUARTHandle handle); +u32 C_LinkUART_availableForRead(C_LinkUARTHandle handle); +u32 C_LinkUART_availableForSend(C_LinkUARTHandle handle); + +u8 C_LinkUART_readByte(C_LinkUARTHandle handle); +void C_LinkUART_sendByte(C_LinkUARTHandle handle, u8 data); + +void C_LinkUART_onSerial(C_LinkUARTHandle handle); + +extern C_LinkUARTHandle cLinkUART; + +inline void C_LINK_UART_ISR_SERIAL() { + C_LinkUART_onSerial(cLinkUART); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_UART_H diff --git a/lib/c_bindings/C_LinkUniversal.cpp b/lib/c_bindings/C_LinkUniversal.cpp new file mode 100644 index 0000000..c794a37 --- /dev/null +++ b/lib/c_bindings/C_LinkUniversal.cpp @@ -0,0 +1,128 @@ +#include "C_LinkUniversal.h" +#include "../LinkUniversal.hpp" + +extern "C" { + +C_LinkUniversalHandle C_LinkUniversal_createDefault() { + return new LinkUniversal(); +} + +C_LinkUniversalHandle C_LinkUniversal_create( + C_LinkUniversal_Protocol protocol, + const char* gameName, + C_LinkUniversal_CableOptions cableOptions, + C_LinkUniversal_WirelessOptions wirelessOptions, + int randomSeed) { + return new LinkUniversal( + static_cast(protocol), gameName, + LinkUniversal::CableOptions{ + static_cast(cableOptions.baudRate), + cableOptions.timeout, cableOptions.interval, + cableOptions.sendTimerId}, + LinkUniversal::WirelessOptions{ + wirelessOptions.retransmission, wirelessOptions.maxPlayers, + wirelessOptions.timeout, wirelessOptions.interval, + wirelessOptions.sendTimerId}, + randomSeed); +} + +void C_LinkUniversal_destroy(C_LinkUniversalHandle handle) { + delete static_cast(handle); +} + +bool C_LinkUniversal_isActive(C_LinkUniversalHandle handle) { + return static_cast(handle)->isActive(); +} + +void C_LinkUniversal_activate(C_LinkUniversalHandle handle) { + static_cast(handle)->activate(); +} + +void C_LinkUniversal_deactivate(C_LinkUniversalHandle handle) { + static_cast(handle)->deactivate(); +} + +bool C_LinkUniversal_isConnected(C_LinkUniversalHandle handle) { + return static_cast(handle)->isConnected(); +} + +u8 C_LinkUniversal_playerCount(C_LinkUniversalHandle handle) { + return static_cast(handle)->playerCount(); +} + +u8 C_LinkUniversal_currentPlayerId(C_LinkUniversalHandle handle) { + return static_cast(handle)->currentPlayerId(); +} + +void C_LinkUniversal_sync(C_LinkUniversalHandle handle) { + static_cast(handle)->sync(); +} + +bool C_LinkUniversal_waitFor(C_LinkUniversalHandle handle, u8 playerId) { + return static_cast(handle)->waitFor(playerId); +} + +bool C_LinkUniversal_waitForWithCancel(C_LinkUniversalHandle handle, + u8 playerId, + bool (*cancel)()) { + return static_cast(handle)->waitFor(playerId, cancel); +} + +bool C_LinkUniversal_canRead(C_LinkUniversalHandle handle, u8 playerId) { + return static_cast(handle)->canRead(playerId); +} + +u16 C_LinkUniversal_read(C_LinkUniversalHandle handle, u8 playerId) { + return static_cast(handle)->read(playerId); +} + +u16 C_LinkUniversal_peek(C_LinkUniversalHandle handle, u8 playerId) { + return static_cast(handle)->peek(playerId); +} + +bool C_LinkUniversal_send(C_LinkUniversalHandle handle, u16 data) { + return static_cast(handle)->send(data); +} + +C_LinkUniversal_State C_LinkUniversal_getState(C_LinkUniversalHandle handle) { + return static_cast( + static_cast(handle)->getState()); +} + +C_LinkUniversal_Mode C_LinkUniversal_getMode(C_LinkUniversalHandle handle) { + return static_cast( + static_cast(handle)->getMode()); +} + +C_LinkUniversal_Protocol C_LinkUniversal_getProtocol( + C_LinkUniversalHandle handle) { + return static_cast( + static_cast(handle)->getProtocol()); +} + +void C_LinkUniversal_setProtocol(C_LinkUniversalHandle handle, + C_LinkUniversal_Protocol protocol) { + static_cast(handle)->setProtocol( + static_cast(protocol)); +} + +u32 C_LinkUniversal_getWaitCount(C_LinkUniversalHandle handle) { + return static_cast(handle)->_getWaitCount(); +} + +u32 C_LinkUniversal_getSubWaitCount(C_LinkUniversalHandle handle) { + return static_cast(handle)->_getSubWaitCount(); +} + +void C_LinkUniversal_onVBlank(C_LinkUniversalHandle handle) { + static_cast(handle)->_onVBlank(); +} + +void C_LinkUniversal_onSerial(C_LinkUniversalHandle handle) { + static_cast(handle)->_onSerial(); +} + +void C_LinkUniversal_onTimer(C_LinkUniversalHandle handle) { + static_cast(handle)->_onTimer(); +} +} diff --git a/lib/c_bindings/C_LinkUniversal.h b/lib/c_bindings/C_LinkUniversal.h new file mode 100644 index 0000000..b7b54ab --- /dev/null +++ b/lib/c_bindings/C_LinkUniversal.h @@ -0,0 +1,111 @@ +#ifndef C_BINDINGS_LINK_UNIVERSAL_H +#define C_BINDINGS_LINK_UNIVERSAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkUniversalHandle; + +#define C_LINK_UNIVERSAL_DISCONNECTED 0xffff +#define C_LINK_UNIVERSAL_NO_DATA 0x0 + +typedef enum { + C_LINK_UNIVERSAL_STATE_INITIALIZING, + C_LINK_UNIVERSAL_STATE_WAITING, + C_LINK_UNIVERSAL_STATE_CONNECTED +} C_LinkUniversal_State; + +typedef enum { + C_LINK_UNIVERSAL_MODE_LINK_CABLE, + C_LINK_UNIVERSAL_MODE_LINK_WIRELESS +} C_LinkUniversal_Mode; + +typedef enum { + C_LINK_UNIVERSAL_PROTOCOL_AUTODETECT, + C_LINK_UNIVERSAL_PROTOCOL_CABLE, + C_LINK_UNIVERSAL_PROTOCOL_WIRELESS_AUTO, + C_LINK_UNIVERSAL_PROTOCOL_WIRELESS_SERVER, + C_LINK_UNIVERSAL_PROTOCOL_WIRELESS_CLIENT +} C_LinkUniversal_Protocol; + +typedef struct { + u32 baudRate; + u32 timeout; + u16 interval; + u8 sendTimerId; +} C_LinkUniversal_CableOptions; + +typedef struct { + bool retransmission; + u32 maxPlayers; + u32 timeout; + u16 interval; + u8 sendTimerId; +} C_LinkUniversal_WirelessOptions; + +C_LinkUniversalHandle C_LinkUniversal_createDefault(); +C_LinkUniversalHandle C_LinkUniversal_create( + C_LinkUniversal_Protocol protocol, + const char* gameName, + C_LinkUniversal_CableOptions cableOptions, + C_LinkUniversal_WirelessOptions wirelessOptions, + int randomSeed); +void C_LinkUniversal_destroy(C_LinkUniversalHandle handle); + +bool C_LinkUniversal_isActive(C_LinkUniversalHandle handle); +void C_LinkUniversal_activate(C_LinkUniversalHandle handle); +void C_LinkUniversal_deactivate(C_LinkUniversalHandle handle); + +bool C_LinkUniversal_isConnected(C_LinkUniversalHandle handle); +u8 C_LinkUniversal_playerCount(C_LinkUniversalHandle handle); +u8 C_LinkUniversal_currentPlayerId(C_LinkUniversalHandle handle); + +void C_LinkUniversal_sync(C_LinkUniversalHandle handle); +bool C_LinkUniversal_waitFor(C_LinkUniversalHandle handle, u8 playerId); +bool C_LinkUniversal_waitForWithCancel(C_LinkUniversalHandle handle, + u8 playerId, + bool (*cancel)()); + +bool C_LinkUniversal_canRead(C_LinkUniversalHandle handle, u8 playerId); +u16 C_LinkUniversal_read(C_LinkUniversalHandle handle, u8 playerId); +u16 C_LinkUniversal_peek(C_LinkUniversalHandle handle, u8 playerId); + +bool C_LinkUniversal_send(C_LinkUniversalHandle handle, u16 data); + +C_LinkUniversal_State C_LinkUniversal_getState(C_LinkUniversalHandle handle); +C_LinkUniversal_Mode C_LinkUniversal_getMode(C_LinkUniversalHandle handle); +C_LinkUniversal_Protocol C_LinkUniversal_getProtocol( + C_LinkUniversalHandle handle); + +void C_LinkUniversal_setProtocol(C_LinkUniversalHandle handle, + C_LinkUniversal_Protocol protocol); + +u32 C_LinkUniversal_getWaitCount(C_LinkUniversalHandle handle); +u32 C_LinkUniversal_getSubWaitCount(C_LinkUniversalHandle handle); + +void C_LinkUniversal_onVBlank(C_LinkUniversalHandle handle); +void C_LinkUniversal_onSerial(C_LinkUniversalHandle handle); +void C_LinkUniversal_onTimer(C_LinkUniversalHandle handle); + +extern C_LinkUniversalHandle cLinkUniversal; + +inline void C_LINK_UNIVERSAL_ISR_VBLANK() { + C_LinkUniversal_onVBlank(cLinkUniversal); +} + +inline void C_LINK_UNIVERSAL_ISR_SERIAL() { + C_LinkUniversal_onSerial(cLinkUniversal); +} + +inline void C_LINK_UNIVERSAL_ISR_TIMER() { + C_LinkUniversal_onTimer(cLinkUniversal); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_UNIVERSAL_H diff --git a/lib/c_bindings/C_LinkWireless.cpp b/lib/c_bindings/C_LinkWireless.cpp new file mode 100644 index 0000000..92cd2f5 --- /dev/null +++ b/lib/c_bindings/C_LinkWireless.cpp @@ -0,0 +1,188 @@ +#include "C_LinkWireless.h" +#include "../LinkWireless.hpp" + +extern "C" { + +C_LinkWirelessHandle C_LinkWireless_createDefault() { + return new LinkWireless(); +} + +C_LinkWirelessHandle C_LinkWireless_create(bool forwarding, + bool retransmission, + u8 maxPlayers, + u32 timeout, + u16 interval, + u8 sendTimerId) { + return new LinkWireless(forwarding, retransmission, maxPlayers, timeout, + interval, sendTimerId); +} + +void C_LinkWireless_destroy(C_LinkWirelessHandle handle) { + delete static_cast(handle); +} + +bool C_LinkWireless_activate(C_LinkWirelessHandle handle) { + return static_cast(handle)->activate(); +} + +bool C_LinkWireless_deactivate(C_LinkWirelessHandle handle) { + return static_cast(handle)->deactivate(); +} + +bool C_LinkWireless_deactivateButKeepOn(C_LinkWirelessHandle handle) { + return static_cast(handle)->deactivate(false); +} + +bool C_LinkWireless_serve(C_LinkWirelessHandle handle, + const char* gameName, + const char* userName, + u16 gameId) { + return static_cast(handle)->serve(gameName, userName, gameId); +} + +bool C_LinkWireless_getServers(C_LinkWirelessHandle handle, + C_LinkWireless_Server servers[]) { + LinkWireless::Server cppServers[C_LINK_WIRELESS_MAX_SERVERS]; + bool result = static_cast(handle)->getServers(cppServers); + + for (u32 i = 0; i < C_LINK_WIRELESS_MAX_SERVERS; i++) { + servers[i].id = cppServers[i].id; + servers[i].gameId = cppServers[i].gameId; + strncpy(servers[i].gameName, cppServers[i].gameName, + C_LINK_WIRELESS_MAX_GAME_NAME_LENGTH); + servers[i].gameName[C_LINK_WIRELESS_MAX_GAME_NAME_LENGTH] = '\0'; + strncpy(servers[i].userName, cppServers[i].userName, + C_LINK_WIRELESS_MAX_USER_NAME_LENGTH); + servers[i].userName[C_LINK_WIRELESS_MAX_USER_NAME_LENGTH] = '\0'; + servers[i].currentPlayerCount = cppServers[i].currentPlayerCount; + } + + return result; +} + +bool C_LinkWireless_getServersAsyncStart(C_LinkWirelessHandle handle) { + return static_cast(handle)->getServersAsyncStart(); +} + +bool C_LinkWireless_getServersAsyncEnd(C_LinkWirelessHandle handle, + C_LinkWireless_Server servers[]) { + LinkWireless::Server cppServers[C_LINK_WIRELESS_MAX_SERVERS]; + bool result = + static_cast(handle)->getServersAsyncEnd(cppServers); + + for (u32 i = 0; i < C_LINK_WIRELESS_MAX_SERVERS; i++) { + servers[i].id = cppServers[i].id; + servers[i].gameId = cppServers[i].gameId; + strncpy(servers[i].gameName, cppServers[i].gameName, + C_LINK_WIRELESS_MAX_GAME_NAME_LENGTH); + servers[i].gameName[C_LINK_WIRELESS_MAX_GAME_NAME_LENGTH] = '\0'; + strncpy(servers[i].userName, cppServers[i].userName, + C_LINK_WIRELESS_MAX_USER_NAME_LENGTH); + servers[i].userName[C_LINK_WIRELESS_MAX_USER_NAME_LENGTH] = '\0'; + servers[i].currentPlayerCount = cppServers[i].currentPlayerCount; + } + + return result; +} + +bool C_LinkWireless_connect(C_LinkWirelessHandle handle, u16 serverId) { + return static_cast(handle)->connect(serverId); +} + +bool C_LinkWireless_keepConnecting(C_LinkWirelessHandle handle) { + return static_cast(handle)->keepConnecting(); +} + +bool C_LinkWireless_send(C_LinkWirelessHandle handle, u16 data) { + return static_cast(handle)->send(data); +} + +bool C_LinkWireless_receive(C_LinkWirelessHandle handle, + C_LinkWireless_Message messages[]) { + LinkWireless::Message cppMessages[C_LINK_WIRELESS_MAX_PLAYERS]; + bool result = static_cast(handle)->receive(cppMessages); + + for (int i = 0; i < C_LINK_WIRELESS_MAX_PLAYERS; i++) { + messages[i].packetId = cppMessages[i].packetId; + messages[i].data = cppMessages[i].data; + messages[i].playerId = cppMessages[i].playerId; + } + + return result; +} + +C_LinkWireless_State C_LinkWireless_getState(C_LinkWirelessHandle handle) { + return static_cast( + static_cast(handle)->getState()); +} + +bool C_LinkWireless_isConnected(C_LinkWirelessHandle handle) { + return static_cast(handle)->isConnected(); +} + +bool C_LinkWireless_isSessionActive(C_LinkWirelessHandle handle) { + return static_cast(handle)->isSessionActive(); +} + +u8 C_LinkWireless_playerCount(C_LinkWirelessHandle handle) { + return static_cast(handle)->playerCount(); +} + +u8 C_LinkWireless_currentPlayerId(C_LinkWirelessHandle handle) { + return static_cast(handle)->currentPlayerId(); +} + +C_LinkWireless_Error C_LinkWireless_getLastError(C_LinkWirelessHandle handle, + bool clear) { + return static_cast( + static_cast(handle)->getLastError(clear)); +} + +bool C_LinkWireless_hasActiveAsyncCommand(C_LinkWirelessHandle handle) { + return static_cast(handle)->_hasActiveAsyncCommand(); +} + +bool C_LinkWireless_canSend(C_LinkWirelessHandle handle) { + return static_cast(handle)->_canSend(); +} + +u32 C_LinkWireless_getPendingCount(C_LinkWirelessHandle handle) { + return static_cast(handle)->_getPendingCount(); +} + +u32 C_LinkWireless_lastPacketId(C_LinkWirelessHandle handle) { + return static_cast(handle)->_lastPacketId(); +} + +u32 C_LinkWireless_lastConfirmationFromClient1(C_LinkWirelessHandle handle) { + return static_cast(handle)->_lastConfirmationFromClient1(); +} + +u32 C_LinkWireless_lastPacketIdFromClient1(C_LinkWirelessHandle handle) { + return static_cast(handle)->_lastPacketIdFromClient1(); +} + +u32 C_LinkWireless_lastConfirmationFromServer(C_LinkWirelessHandle handle) { + return static_cast(handle)->_lastConfirmationFromServer(); +} + +u32 C_LinkWireless_lastPacketIdFromServer(C_LinkWirelessHandle handle) { + return static_cast(handle)->_lastPacketIdFromServer(); +} + +u32 C_LinkWireless_nextPendingPacketId(C_LinkWirelessHandle handle) { + return static_cast(handle)->_nextPendingPacketId(); +} + +void C_LinkWireless_onVBlank(C_LinkWirelessHandle handle) { + static_cast(handle)->_onVBlank(); +} + +void C_LinkWireless_onSerial(C_LinkWirelessHandle handle) { + static_cast(handle)->_onSerial(); +} + +void C_LinkWireless_onTimer(C_LinkWirelessHandle handle) { + static_cast(handle)->_onTimer(); +} +} diff --git a/lib/c_bindings/C_LinkWireless.h b/lib/c_bindings/C_LinkWireless.h new file mode 100644 index 0000000..517f88f --- /dev/null +++ b/lib/c_bindings/C_LinkWireless.h @@ -0,0 +1,143 @@ +#ifndef C_BINDINGS_LINK_WIRELESS_H +#define C_BINDINGS_LINK_WIRELESS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkWirelessHandle; + +#define C_LINK_WIRELESS_MAX_PLAYERS 5 +#define C_LINK_WIRELESS_MIN_PLAYERS 2 +#define C_LINK_WIRELESS_END 0 +#define C_LINK_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH 22 +#define C_LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH 30 +#define C_LINK_WIRELESS_BROADCAST_LENGTH 6 +#define C_LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH \ + (1 + C_LINK_WIRELESS_BROADCAST_LENGTH) +#define C_LINK_WIRELESS_MAX_SERVERS \ + (C_LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH / \ + C_LINK_WIRELESS_BROADCAST_RESPONSE_LENGTH) +#define C_LINK_WIRELESS_MAX_GAME_ID 0x7fff +#define C_LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14 +#define C_LINK_WIRELESS_MAX_USER_NAME_LENGTH 8 +#define C_LINK_WIRELESS_DEFAULT_TIMEOUT 10 +#define C_LINK_WIRELESS_DEFAULT_INTERVAL 50 +#define C_LINK_WIRELESS_DEFAULT_SEND_TIMER_ID 3 +#define C_LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID -1 + +typedef enum { + C_LINK_WIRELESS_STATE_NEEDS_RESET, + C_LINK_WIRELESS_STATE_AUTHENTICATED, + C_LINK_WIRELESS_STATE_SEARCHING, + C_LINK_WIRELESS_STATE_SERVING, + C_LINK_WIRELESS_STATE_CONNECTING, + C_LINK_WIRELESS_STATE_CONNECTED +} C_LinkWireless_State; + +typedef enum { + C_LINK_WIRELESS_ERROR_NONE, + C_LINK_WIRELESS_ERROR_WRONG_STATE, + C_LINK_WIRELESS_ERROR_GAME_NAME_TOO_LONG, + C_LINK_WIRELESS_ERROR_USER_NAME_TOO_LONG, + C_LINK_WIRELESS_ERROR_BUFFER_IS_FULL, + C_LINK_WIRELESS_ERROR_COMMAND_FAILED, + C_LINK_WIRELESS_ERROR_CONNECTION_FAILED, + C_LINK_WIRELESS_ERROR_SEND_DATA_FAILED, + C_LINK_WIRELESS_ERROR_RECEIVE_DATA_FAILED, + C_LINK_WIRELESS_ERROR_ACKNOWLEDGE_FAILED, + C_LINK_WIRELESS_ERROR_TIMEOUT, + C_LINK_WIRELESS_ERROR_REMOTE_TIMEOUT, + C_LINK_WIRELESS_ERROR_BUSY_TRY_AGAIN +} C_LinkWireless_Error; + +typedef struct { + u16 packetId; + u16 data; + u8 playerId; +} C_LinkWireless_Message; + +typedef struct { + u16 id; + u16 gameId; + char gameName[C_LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1]; + char userName[C_LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1]; + u8 currentPlayerCount; +} C_LinkWireless_Server; + +C_LinkWirelessHandle C_LinkWireless_createDefault(); +C_LinkWirelessHandle C_LinkWireless_create(bool forwarding, + bool retransmission, + u8 maxPlayers, + u32 timeout, + u16 interval, + u8 sendTimerId); +void C_LinkWireless_destroy(C_LinkWirelessHandle handle); + +bool C_LinkWireless_activate(C_LinkWirelessHandle handle); +bool C_LinkWireless_deactivate(C_LinkWirelessHandle handle); +bool C_LinkWireless_deactivateButKeepOn(C_LinkWirelessHandle handle); + +bool C_LinkWireless_serve(C_LinkWirelessHandle handle, + const char* gameName, + const char* userName, + u16 gameId); + +bool C_LinkWireless_getServers(C_LinkWirelessHandle handle, + C_LinkWireless_Server servers[]); +bool C_LinkWireless_getServersAsyncStart(C_LinkWirelessHandle handle); +bool C_LinkWireless_getServersAsyncEnd(C_LinkWirelessHandle handle, + C_LinkWireless_Server servers[]); + +bool C_LinkWireless_connect(C_LinkWirelessHandle handle, u16 serverId); +bool C_LinkWireless_keepConnecting(C_LinkWirelessHandle handle); + +bool C_LinkWireless_send(C_LinkWirelessHandle handle, u16 data); +bool C_LinkWireless_receive(C_LinkWirelessHandle handle, + C_LinkWireless_Message messages[]); + +C_LinkWireless_State C_LinkWireless_getState(C_LinkWirelessHandle handle); +bool C_LinkWireless_isConnected(C_LinkWirelessHandle handle); +bool C_LinkWireless_isSessionActive(C_LinkWirelessHandle handle); +u8 C_LinkWireless_playerCount(C_LinkWirelessHandle handle); +u8 C_LinkWireless_currentPlayerId(C_LinkWirelessHandle handle); + +C_LinkWireless_Error C_LinkWireless_getLastError(C_LinkWirelessHandle handle, + bool clear); + +bool C_LinkWireless_hasActiveAsyncCommand(C_LinkWirelessHandle handle); +bool C_LinkWireless_canSend(C_LinkWirelessHandle handle); + +u32 C_LinkWireless_getPendingCount(C_LinkWirelessHandle handle); +u32 C_LinkWireless_lastPacketId(C_LinkWirelessHandle handle); +u32 C_LinkWireless_lastConfirmationFromClient1(C_LinkWirelessHandle handle); +u32 C_LinkWireless_lastPacketIdFromClient1(C_LinkWirelessHandle handle); +u32 C_LinkWireless_lastConfirmationFromServer(C_LinkWirelessHandle handle); +u32 C_LinkWireless_lastPacketIdFromServer(C_LinkWirelessHandle handle); +u32 C_LinkWireless_nextPendingPacketId(C_LinkWirelessHandle handle); + +void C_LinkWireless_onVBlank(C_LinkWirelessHandle handle); +void C_LinkWireless_onSerial(C_LinkWirelessHandle handle); +void C_LinkWireless_onTimer(C_LinkWirelessHandle handle); + +extern C_LinkWirelessHandle cLinkWireless; + +inline void C_LINK_WIRELESS_ISR_VBLANK() { + C_LinkWireless_onVBlank(cLinkWireless); +} + +inline void C_LINK_WIRELESS_ISR_SERIAL() { + C_LinkWireless_onSerial(cLinkWireless); +} + +inline void C_LINK_WIRELESS_ISR_TIMER() { + C_LinkWireless_onTimer(cLinkWireless); +} + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_WIRELESS_H diff --git a/lib/c_bindings/C_LinkWirelessMultiboot.cpp b/lib/c_bindings/C_LinkWirelessMultiboot.cpp new file mode 100644 index 0000000..f5c1e86 --- /dev/null +++ b/lib/c_bindings/C_LinkWirelessMultiboot.cpp @@ -0,0 +1,36 @@ +#include "C_LinkWirelessMultiboot.h" +#include "../LinkWirelessMultiboot.hpp" + +extern "C" { + +C_LinkWirelessMultibootHandle C_LinkWirelessMultiboot_create() { + return new LinkWirelessMultiboot(); +} + +void C_LinkWirelessMultiboot_destroy(C_LinkWirelessMultibootHandle handle) { + delete static_cast(handle); +} + +C_LinkWirelessMultiboot_Result C_LinkWirelessMultiboot_sendRom( + C_LinkWirelessMultibootHandle handle, + const u8* rom, + u32 romSize, + const char* gameName, + const char* userName, + u16 gameId, + u8 players, + C_LinkWirelessMultiboot_CancelCallback cancel) { + auto result = static_cast(handle)->sendRom( + rom, romSize, gameName, userName, gameId, players, + [cancel](LinkWirelessMultiboot::MultibootProgress progress) { + C_LinkWirelessMultiboot_Progress cProgress; + cProgress.state = + static_cast(progress.state); + cProgress.connectedClients = progress.connectedClients; + cProgress.percentage = progress.percentage; + return cancel(cProgress); + }); + + return static_cast(result); +} +} diff --git a/lib/c_bindings/C_LinkWirelessMultiboot.h b/lib/c_bindings/C_LinkWirelessMultiboot.h new file mode 100644 index 0000000..ae7910f --- /dev/null +++ b/lib/c_bindings/C_LinkWirelessMultiboot.h @@ -0,0 +1,65 @@ +#ifndef C_BINDINGS_LINK_WIRELESS_MULTIBOOT_H +#define C_BINDINGS_LINK_WIRELESS_MULTIBOOT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef void* C_LinkWirelessMultibootHandle; + +#define C_LINK_WIRELESS_MULTIBOOT_MIN_ROM_SIZE (0x100 + 0xc0) +#define C_LINK_WIRELESS_MULTIBOOT_MAX_ROM_SIZE (256 * 1024) +#define C_LINK_WIRELESS_MULTIBOOT_MIN_PLAYERS 2 +#define C_LINK_WIRELESS_MULTIBOOT_MAX_PLAYERS 5 + +typedef enum { + C_LINK_WIRELESS_MULTIBOOT_SUCCESS, + C_LINK_WIRELESS_MULTIBOOT_INVALID_SIZE, + C_LINK_WIRELESS_MULTIBOOT_INVALID_PLAYERS, + C_LINK_WIRELESS_MULTIBOOT_CANCELED, + C_LINK_WIRELESS_MULTIBOOT_ADAPTER_NOT_DETECTED, + C_LINK_WIRELESS_MULTIBOOT_BAD_HANDSHAKE, + C_LINK_WIRELESS_MULTIBOOT_CLIENT_DISCONNECTED, + C_LINK_WIRELESS_MULTIBOOT_FAILURE +} C_LinkWirelessMultiboot_Result; + +typedef enum { + C_LINK_WIRELESS_MULTIBOOT_STATE_STOPPED, + C_LINK_WIRELESS_MULTIBOOT_STATE_INITIALIZING, + C_LINK_WIRELESS_MULTIBOOT_STATE_WAITING, + C_LINK_WIRELESS_MULTIBOOT_STATE_PREPARING, + C_LINK_WIRELESS_MULTIBOOT_STATE_SENDING, + C_LINK_WIRELESS_MULTIBOOT_STATE_CONFIRMING +} C_LinkWirelessMultiboot_State; + +typedef struct { + C_LinkWirelessMultiboot_State state; + u32 connectedClients; + u32 percentage; +} C_LinkWirelessMultiboot_Progress; + +typedef bool (*C_LinkWirelessMultiboot_CancelCallback)( + C_LinkWirelessMultiboot_Progress progress); + +C_LinkWirelessMultibootHandle C_LinkWirelessMultiboot_create(); +void C_LinkWirelessMultiboot_destroy(C_LinkWirelessMultibootHandle handle); + +C_LinkWirelessMultiboot_Result C_LinkWirelessMultiboot_sendRom( + C_LinkWirelessMultibootHandle handle, + const u8* rom, + u32 romSize, + const char* gameName, + const char* userName, + u16 gameId, + u8 players, + C_LinkWirelessMultiboot_CancelCallback cancel); + +extern C_LinkWirelessMultibootHandle cLinkWirelessMultiboot; + +#ifdef __cplusplus +} +#endif + +#endif // C_BINDINGS_LINK_WIRELESS_MULTIBOOT_H