diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index 9d4aaeef..ea4d9a56 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -30,6 +30,7 @@
- [Collision](part2/collision.md)
- [Bricks](part2/bricks.md)
- [Decimal Numbers](part2/bcd.md)
+- [Serial Link](part2/serial-link.md)
- [Work in progress](part2/wip.md)
# Part III — Our second game
diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md
new file mode 100644
index 00000000..091071bd
--- /dev/null
+++ b/src/part2/serial-link.md
@@ -0,0 +1,608 @@
+# Serial Link
+
+In this lesson, we will:
+
+- Learn how to control the Game Boy serial port from code;
+- Build a wrapper over the low-level serial port interface;
+- Implement checksums to verify data integrity and enable reliable data transfers.
+
+## Running the code
+
+Testing the code in this lesson (or any code that uses the serial port) is a bit more complicated than what we've been doing so far.
+There's a few things to be aware of.
+
+You need an emulator that supports the serial port, such as:
+[Emulicious](https://emulicious.net/) and [GBE+](https://github.com/shonumi/gbe-plus).
+The way this works is by having two instances of the emulator connect to each other over network sockets.
+
+Keep in mind that the emulated serial port is never going to replicate the complexity and breadth of issues that can occur on the real thing.
+
+Testing on hardware comes with hardware requirements, unfortunately.
+You'll need two Game Boys (any combination of models), a link cable to connect them, and a pair of flash carts.
+
+
+## The Game Boy serial port
+
+:::tip Information overload
+
+This section is intended as a reasonably complete description of the Game Boy serial port, from a programming perspective.
+There's a lot of information packed in here and you don't need to absorb it all to continue.
+
+:::
+
+Communication via the serial port is organised as discrete data transfers of one byte each.
+Data transfer is bidirectional, with every bit of data written out matched by one read in.
+A data transfer can therefore be thought of as *swapping* the data byte in one device's buffer for the byte in the other's.
+
+The serial port is *idle* by default.
+Idle time is used to read received data, configure the port if needed, and load the next value to send.
+
+Before we can transfer any data, we need to configure the *clock source* of both Game Boys.
+To synchronise the two devices, one Game Boy must provide the clock signal that both will use.
+Setting bit 0 of the [Serial Control register](https://gbdev.io/pandocs/Serial_Data_Transfer_(Link_Cable).html#ff02--sc-serial-transfer-control) (`rSC` as it's defined in hardware.inc)
+enables the Game Boy's *internal* serial clock, and makes it the clock provider.
+The other Game Boy must have its clock source set to *external* (`SC` bit 0 cleared).
+The externally clocked Game Boy will receive the clock signal via the link cable.
+
+Before a transfer, the data to transmit is loaded into the **Serial Buffer** register (`SB`).
+After a transfer, the `SB` register will contain the received data.
+
+When ready, the program can set bit 7 of the `SC` register in order to *activate* the port -- instructing it to perform a transfer.
+While the serial port is *active*, it sends and receives a data bit on each serial clock pulse.
+After 8 pulses (*8 bits!*) the transfer is complete -- the serial port deactivates itself, and the serial interrupt is requested.
+Normal execution continues while the serial port is active: the transfer will be performed independently of the program code.
+
+
+## Sio
+
+Alright, let's write some code!
+**Sio** is the **S**erial **i**nput/**o**utput module and we're going to build it in its own file, so open a new file called `sio.asm`.
+
+At the top of `sio.asm`, include `hardware.inc` and then define a set of constants that represent Sio's main states:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-status-enum}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-status-enum}}
+```
+
+Sio operates as a finite state machine with each of these constants being a unique state.
+Sio's job is to manage serial transfers, so Sio's state simultaneously indicates what Sio is doing and the current transfer status.
+
+:::tip EXPORT quality
+
+`EXPORT` makes the variables following it available in other source files.
+In general, there are better ways to do this -- it shouldn't be your first choice.
+`EXPORT` is used here for simplicity, so we can stay focused on the concept being taught.
+
+:::
+
+Below the constants, add a new WRAM section with some variables for Sio's state:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-state}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-state}}
+```
+
+`wSioState` holds one of the state constants we defined above.
+The other variables will be discussed as we build the features that use them.
+
+Add a new code section and an initialisation routine:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-impl-init}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-impl-init}}
+ ret
+```
+
+
+### Buffers
+The buffers are a pair of temporary storage locations for all messages sent or received by Sio.
+There's a buffer for data to transmit (Tx) and one for receiving data (Rx).
+The variable `wSioBufferOffset` holds the current location within *both* data buffers -- Game Boy serial transfers are always symmetrical.
+
+First we'll need a couple of constants, so add these below the existing ones, near the top of the file.
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffer-defs}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-buffer-defs}}
+```
+
+Allocate the buffers, each in their own section, just above the `SioCore State` section we made earlier.
+This needs to be specified carefully and uses some unfamiliar syntax, so you might like to copy and paste this code:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffers}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-buffers}}
+```
+
+`ALIGN[8]` causes each section -- and each buffer -- to start at an address with a low byte of zero.
+This makes building a pointer to the buffer element at index `i` trivial, as the high byte of the pointer is constant for the entire buffer, and the low byte is simply `i`.
+The result is a significant reduction in the amount of work required to access the data and manipulate offsets of both buffers.
+
+:::tip Aligning Sections
+
+If you would like to learn more about aligning sections -- *which is by no means required to continue this lesson* -- the place to start is the [SECTIONS](https://rgbds.gbdev.io/docs/rgbasm.5#SECTIONS) section in the rgbasm language documenation.
+
+:::
+
+At the end of `SioReset`, clear the buffers:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-reset-buffers}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-reset-buffers}}
+```
+
+
+### Core implementation
+Below `SioInit`, add a function to start a multibyte transfer of the entire data buffer:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-start-transfer}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-start-transfer}}
+```
+
+To initialise the transfer, start from buffer offset zero, set the transfer count, and switch to the `SIO_ACTIVE` state.
+The first byte to send is loaded from `wSioBufferTx` before a jump to the next function starts the first transfer immediately.
+
+
+Activating the serial port is a simple matter of setting bit 7 of `rSC`, but we need to do a couple of other things at the same time, so add a function to bundle it all together:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-start}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-port-start}}
+```
+
+The first thing `SioPortStart` does is something called the "catchup delay", but only if the internal clock source is enabled.
+
+:::tip Delay? Why?
+
+When a Game Boy serial port is active, it will transfer a data bit whenever it detects clock pulse.
+When using the external clock source, the active serial port will wait indefinitely -- until the externally provided clock signal is received.
+But when using the internal clock source, bits will start getting transferred as soon as the port is activated.
+Because the internally clocked device can't wait once activated, the catchup delay is used to ensure the externally clocked device activates its port first.
+
+:::
+
+To check if the internal clock is enabled, read the serial port control register (`rSC`) and check if the clock source bit is set.
+We test the clock source bit by *anding* with `SCF_SOURCE`, which is a constant with only the clock source bit set.
+The result of this will be `0` except for the clock source bit, which will maintain its original value.
+So we can perform a conditional jump and skip the delay if the zero flag is set.
+The delay itself is a loop that wastes time by doing nothing -- `nop` is an instruction that has no effect -- a number of times.
+
+To start the serial port, the constant `SCF_START` is combined with the clock source setting (still in `a`) and the updated value is loaded into the `SC` register.
+
+Finally, the timeout timer is reset by loading the constant `SIO_TIMEOUT_TICKS` into `wSioTimer`.
+
+:::tip Timeouts
+
+We know that the serial port will remain active until it detects eight clock pulses, and performs eight bit transfers.
+A side effect of this is that when relying on an *external* clock source, a transfer may never end!
+This is most likely to happen if there is no other Game Boy connected, or if both devices are set to use an external clock source.
+To avoid having this quirk become a problem, we implement *timeouts*: each byte transfer must be completed within a set period of time or we give up and consider the transfer to have failed.
+
+:::
+
+We'd better define the constants that set the catchup delay and timeout duration:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-start-defs}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-port-start-defs}}
+```
+
+
+Implement the timeout logic in `SioTick`:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-tick}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-tick}}
+```
+
+`SioTick` checks the current state (`wSioState`) and jumps to a state-specific subroutine (labelled `*_tick`).
+
+**`SIO_ACTIVE`:** a transfer has been started, if the clock source is *external*, update the timeout timer.
+
+The timer's state is an unsigned integer stored in `wSioTimer`.
+Check that the timer is active (has a non-zero value) with `and a, a`.
+Decrement the timer and write the new value back to memory.
+If the timer expired (the new value is zero) the transfer should be aborted.
+The `dec` instruction sets the zero flag in that case, so all we have to do is `jr z, SioAbort`.
+
+**`SIO_RESET`:** `SioReset` has been called, change state to `SIO_IDLE`.
+This causes a one tick delay after `SioReset` is called.
+
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-abort}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-abort}}
+```
+
+`SioAbort` brings the serial port down and sets the current state to `SIO_FAILED`.
+The aborted transfer state is intentionally left intact so it can be used to instruct error handling and aid debugging.
+
+
+The last part of the core implementation handles the end of each byte transfer:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-end}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-port-end}}
+```
+
+`SioPortEnd` starts by checking that a transfer was started (the `SIO_ACTIVE` state).
+We're receiving a byte, so the transfer counter (`wSioCount`) is reduced by one.
+The received value is copied from the serial port (`rSB`) to Sio's buffer (`wSioBufferRx`).
+If there are still bytes to transfer (transfer counter is greater than zero) the next value is loaded from `wSioBufferTx` and the transfer is started by `SioPortStart`.
+Otherwise, if the transfer counter is zero, enter the `SIO_DONE` state.
+
+
+## Interval
+
+So far we've written a bunch of code that, unfortunately, doesn't do anything on its own.
+It works though, I promise!
+The good news is that Sio -- the code that interfaces directly with the serial port -- is complete.
+
+:::tip 🤖 Take a break!
+
+Suggested break enrichment activity: CONSUME REFRESHMENT
+
+Naturally, yours, &c.\,
+
+A. Hughman
+
+:::
+
+
+## Reliable Communication
+
+Sio by itself offers very little in terms of *reliability*.
+For our purposes, reliability is all about dealing with errors.
+The errors that we're concerned with are data replication errors -- any case where the data transmitted is not replicated correctly in the receiver.
+
+
+The first step is detection.
+The receiver needs to test the integrity of every incoming data packet, before doing anything else with it.
+We'll use a [checksum](https://en.wikipedia.org/wiki/Checksum) mechanism for this:
+- The sender calculates a checksum of the outgoing packet and the result is transmitted as part of the packet transfer.
+- The receiver performs the same calculation and compares the result with the value from the sender.
+- If the values match, the packet is intact.
+
+
+With the packet integrity checksum, the receiving end can detect packet data corruption and discard packets that don't pass the test.
+When a packet is not delivered successfully, it should be transmitted again by the sender.
+Unfortunately, the sender has no idea if the packet it sent was delivered intact.
+
+To keep the sender in the loop, and manage retransmission, we need a *protocol* -- a set of rules that govern communication.
+The protocol follows the principle:
+> The sender of a packet will assume the transfer failed, *unless the receiver reports success*.
+
+Let's define two classes of packet:
+- **Application Messages:** critical data that must be delivered, retransmit if delivery failed
+ - contains application-specific data
+- **Protocol Metadata:** do not retransmit (always send the latest state)
+ - contains link state information (including last packet received)
+
+
+:::tip Corruption? In my Game Boy?
+
+Yep, there's any number of possible causes of transfer data replication errors when working with the Game Boy serial port.
+Some examples include: old or damaged hardware, luck, [cosmic rays](https://en.wikipedia.org/wiki/Single-event_upset), and user actions (hostile and accidental).
+
+:::
+
+
+
+There's one more thing our protocol needs: some way to get both devices on the same page and kick things off.
+We need a *handshake* that must be completed before doing anything else.
+This is a simple sequence that checks that there is a connection and tests that the connection is working.
+The handshake can be performed in one of two roles: *A* or *B*.
+To be successful, one peer must be *A* and the other must be *B*.
+Which role to perform is determined by the clock source setting of the serial port.
+In each exchange, each peer sends a number associated with its role and expects to receive a number associated with the other role.
+If an unexpected value is received, or something goes wrong with the transfer, that handshake is rejected.
+
+
+## SioPacket
+
+SioPacket is a thin layer over Sio buffer transfers.
+- The most important addition is a checksum based integrity test.
+- Several convenience routines are also provided.
+
+Packets fill a Sio buffer with the following structure:
+```rgbasm
+PacketLayout:
+ .start_mark: db ; The constant SIO_PACKET_START.
+ .checksum: db ; Packet checksum, set before transmission.
+ .data: ds SIO_BUFFER_SIZE - 2 ; Packet data (user defined).
+ ; Unused space in .data is filled with SIO_PACKET_END.
+```
+
+At the top of `sio.asm` define some constants:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-defs}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-packet-defs}}
+```
+
+`SioPacketTxPrepare` creates a new empty packet in the Tx buffer:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-prepare}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-packet-prepare}}
+```
+
+- The checksum is set to zero for the initial checksum calculation.
+- The data section is cleared by filling it with the constant `SIO_PACKET_END`.
+
+After calling `SioPacketTxPrepare`, the payload data can be written to the packet.
+Then, the function `SioPacketTxFinalise` should be called:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-finalise}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-packet-finalise}}
+```
+
+- Call `SioPacketChecksum` to calculate the packet checksum.
+ - It's important that the value of the checksum field is zero when performing this initial checksum calculation.
+- Write the correct checksum to the packet header.
+- Start the transfer.
+
+
+Implement the packet integrity test for received packets:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-check}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-packet-check}}
+```
+
+- Check that the packet begins with the magic number `SIO_PACKET_START`.
+- Calculate the checksum of the received data.
+ - This includes the packet checksum calculated by the sender.
+ - The result of this calculation will be zero if the data is the same as it was when sent.
+
+Finally, implement the checksum:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-checksum}}
+{{#include ../../unbricked/serial-link/sio.asm:sio-checksum}}
+```
+
+- start with the size of the buffer (effectively -1 for each byte summed)
+- subtract each byte in the buffer from the sum
+
+:::tip
+
+The checksum implemented here has been kept very simple for this tutorial.
+It's probably worth looking into better solutions for real-world projects.
+
+Check Ben Eater's lessons on [Reliable Data Transmission](https://www.youtube.com/watch?v=eq5YpKHXJDM),
+[Error Detection: Parity Checking](https://www.youtube.com/watch?v=MgkhrBSjhag), [Checksums and Hamming Distance](https://www.youtube.com/watch?v=ppU41c15Xho),
+[How Do CRCs Work?](https://www.youtube.com/watch?v=izG7qT0EpBw) to explore further this topic.
+:::
+
+
+## Connecting it all together
+It's time to implement the protocol and build the application-level features on top of everything we've done so far.
+
+
+At the top of main.asm, define the constants for keeping track of Link's state:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-defs}}
+{{#include ../../unbricked/serial-link/main.asm:serial-demo-defs}}
+```
+
+
+We'll need some variables in WRAM to keep track of things.
+Add a section at the bottom of main.asm:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-wram}}
+{{#include ../../unbricked/serial-link/main.asm:serial-demo-wram}}
+```
+
+`wLocal` and `wRemote` are two identical structures for storing the Link state information of each peer.
+- `state` holds the current mode and some flags (the `LINKST_` constants)
+- `tx_id` & `rx_id` are for the IDs of the most recently sent & received `MSG_DATA` message
+
+The contents of application data messages (`MSG_DATA` only) will be stored in the buffers `wTxData` and `wRxData`.
+
+`wAllowTxAttempts` is the number of transmission attempts remaining for each DATA message.
+`wAllowRxFaults` is the "budget" of delivery faults allowed before causing an error.
+
+
+### LinkInit
+Lots of variables means lots of initialisation so let's add a function for that:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-init}}
+{{#include ../../unbricked/serial-link/main.asm:link-init}}
+```
+
+This initialises Sio by calling `SioInit` and then enables something called the serial interrupt which will be explained soon.
+Execution continues into `LinkReset`.
+
+`LinkReset` can be called to reset the whole Link feature if something goes wrong.
+This resets Sio and then writes default values to all the variables we defined above.
+Finally, a function called `HandshakeDefault` is jumped to and for that one you'll have to wait a little bit!
+
+Make sure to call the init routine once before the main loop starts:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-init-callsite}}
+{{#include ../../unbricked/serial-link/main.asm:serial-demo-init-callsite}}
+```
+
+We'll also add a utility function for handling errors:
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-error-stop}}
+{{#include ../../unbricked/serial-link/main.asm:link-error-stop}}
+```
+
+
+### Serial Interrupt
+Sio needs to be told when to process each completed byte transfer.
+The best way to do this is by using the serial interrupt.
+Copy this code (it needs to be exact) to `main.asm`, just above the `"Header"` section:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-interrupt-vector}}
+{{#include ../../unbricked/serial-link/main.asm:serial-interrupt-vector}}
+```
+
+A proper and complete explanation of this is beyond the scope of this lesson.
+You can continue the lesson understanding that:
+- This is the serial interrupt handler. It gets called automatically after each serial transfer.
+- The significant implementation is in `SioPortEnd` but it's necessary to jump through some hoops to call it.
+
+A detailed and rather dense explanation is included for completeness.
+
+:::tip
+
+*You can just use the code as explained above and skip past this box.*
+
+An interrupt handler is a piece of code at a specific address that gets called automatically under certain conditions.
+The serial interrupt handler begins at address `$58` so a section just for this function is defined at that location using `ROM0[$58]`.
+Note that the function is labelled by convention and for debugging purposes -- it isn't technically meaningful and the function isn't intended to be called manually.
+
+Whatever code was running when an interrupt occurs literally gets paused until the interrupt handler returns.
+The registers used by `SioPortEnd` need to be preserved so the code that got interrupted doesn't break.
+We use the stack to do this -- using `push` before the call and `pop` afterwards.
+Note that the order of the registers when pushing is the opposite of the order when popping, due to the stack being a LIFO (last-in, first-out) container.
+
+`reti` returns from the function (like `ret`) and enables interrupts (like `ei`) which is necessary because interrupts are disabled automatically when calling an interrupt handler.
+
+If you would like to continue digging, have a look at [evie's interrupts tutorial](https://evie.gbdev.io/resources/interrupts) and Pan Docs page on [Interrupts](https://gbdev.io/pandocs/Interrupts.html).
+
+:::
+
+
+### LinkUpdate
+`LinkUpdate` is the main per-frame update function.
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-update}}
+{{#include ../../unbricked/serial-link/main.asm:link-update}}
+```
+
+The order of each part of this is important -- note the many (conditional) places where execution can exit this procedure.
+
+Check input before anything else so the user can always reset the demo.
+
+The `LINKST_MODE_ERROR` mode is an unrecoverable error state that can only be exited via the reset.
+To check the current mode, read the `wLocal.state` byte and use `and a, LINKST_MODE` to keep just the mode bits.
+There's nothing else to do in the `LINKST_MODE_ERROR` mode, so simply return from the function if that's the case.
+
+Update Sio by calling `SioTick` and then call a specific function for the current mode.
+
+`LINKST_MODE_CONNECT` manages the handshake process.
+Update the handshake if it's incomplete (`wHandshakeState` is non-zero).
+Otherwise, transition to the active connection mode.
+
+`LINKST_MODE_UP` just checks the current state of the Sio state machine in order to jump to an appropriate function to handle certain cases.
+
+
+### LinkTx
+`LinkTx` builds the next message packet and starts transferring it.
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-message}}
+{{#include ../../unbricked/serial-link/main.asm:link-send-message}}
+```
+
+There's two types of message that are sent while the link is active -- SYNC and DATA.
+The `LINKST_STEP_SYNC` flag is used to alternate between the two types and ensure at least every second message is a SYNC.
+A DATA message will only be sent if the `LINKST_STEP_SYNC` flag is clear and the `LINKST_TX_ACT` flag is set.
+
+Both cases then send a packet in much the same way -- `call SioPacketPrepare`, write the data to the packet (starting at `HL`), and then `call SioPacketFinalise`.
+
+To make sending DATA messages more convenient, add a utility function to take care of the details:
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-tx-start}}
+{{#include ../../unbricked/serial-link/main.asm:link-tx-start}}
+```
+
+
+### LinkRx
+When a transfer has completed (`SIO_DONE`), process the received data in `LinkRx`:
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-receive-message}}
+{{#include ../../unbricked/serial-link/main.asm:link-receive-message}}
+```
+
+The first thing to do is flush Sio's state (set it to `SIO_IDLE`) to indicate that the received data has been processed.
+Technically the data hasn't actually been processed yet, but this is a promise to do that!
+
+Check that a packet was received and that it arrived intact by calling `SioPacketRxCheck`.
+If the packet checks out OK, read the message type from the packet data and jump to the appropriate routine to handle messages of that type.
+
+
+If the result of `SioPacketRxCheck` was negative, or the message type is unrecognised, it's considered a delivery *fault*.
+In case of a fault, the received data is discarded and the fault counter is updated.
+The fault counter state is loaded from `wAllowRxFaults`.
+If the value of the counter is zero (i.e. there's zero (more) faults allowed) the error mode is acivated.
+If the value of the counter is more than zero, it's decremented and saved.
+
+
+`MSG_SYNC` messages contain the sender's Link state, so first we copy the received data to `wRemote`.
+Now we want to check if the remote peer has acknowledged delivery of a message sent to them.
+Copy the new `wRemote.rx_id` value to register `B`, then load `wLocal.state` and copy it into register `C`
+Check the `LINKST_TX_ACT` flag (using the `and` instruction) and return if it's not set.
+Otherwise, an outgoing message has not been acknowledged yet, so load `wLocal.tx_id` and compare it to `wRemote.rx_id` (in register `B`).
+If the two are equal that means the message was delivered, so clear the `LINKST_TX_ACT` flag and update `wLocal.state`.
+
+
+Receiving `MSG_DATA` messages is straightforward.
+The first byte is the message ID, so copy that from the packet to `wLocal.rx_id`.
+The rest of the packet data is copied straight to the `wRxData` buffer.
+Finally, a flag is set to indicate that data was newly received.
+
+
+### Main
+
+Demo update routine:
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-update}}
+{{#include ../../unbricked/serial-link/main.asm:serial-demo-update}}
+```
+
+Call the update routine from the main loop:
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-update-callsite}}
+{{#include ../../unbricked/serial-link/main.asm:serial-demo-update-callsite}}
+```
+
+
+### Implement the handshake protocol
+
+/// Establish contact by trading magic numbers
+
+/// Define the codes each device will send:
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-codes}}
+{{#include ../../unbricked/serial-link/main.asm:handshake-codes}}
+```
+
+///
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-state}}
+{{#include ../../unbricked/serial-link/main.asm:handshake-state}}
+```
+
+/// Routines to begin handshake sequence as either the internally or externally clocked device.
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-begin}}
+{{#include ../../unbricked/serial-link/main.asm:handshake-begin}}
+```
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-update}}
+{{#include ../../unbricked/serial-link/main.asm:handshake-update}}
+```
+
+The handshake can be forced to restart in the clock provider role by pressing START.
+This is included as a fallback and manual override for the automatic role selection implemented below.
+
+If a transfer is completed, process the received data by jumping to `HandshakeMsgRx`.
+
+If the serial port is otherwise inactive, (re)start the handshake.
+To automatically determine which device should be the clock provider, we check the lowest bit of the DIV register.
+This value increments at around 16 kHz which, for our purposes and because we only check it every now and then, is close enough to random.
+
+```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-xfer-complete}}
+{{#include ../../unbricked/serial-link/main.asm:handshake-xfer-complete}}
+```
+
+Check that a packet was received and that it contains the expected handshake value.
+The state of the serial port clock source bit is used to determine which value to expect -- `SHAKE_A` if set to use an external clock and `SHAKE_B` if using the internal clock.
+If all is well, decrement the `wHandshakeState` counter.
+If the counter is zero, there is nothing left to do.
+Otherwise, more exchanges are required so start the next one immediately.
+
+:::tip
+
+This is a trivial example of a handshake protocol.
+In a real application, you might want to consider:
+- using a longer sequence of codes as a more unique app identifier
+- sharing more information about each device and negotiating to decide the preferred clock provider
+
+:::
+
+
+
+## /// Running the test ROM
+
+/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different:
+```console
+$ rgbasm -o sio.o sio.asm
+$ rgbasm -o main.o main.asm
+$ rgblink -o unbricked.gb main.o sio.o
+$ rgbfix -v -p 0xFF unbricked.gb
+```
diff --git a/unbricked/serial-link/build.sh b/unbricked/serial-link/build.sh
new file mode 100644
index 00000000..14a60772
--- /dev/null
+++ b/unbricked/serial-link/build.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+rgbasm -o sio.o sio.asm
+rgbasm -o main.o main.asm
+rgblink -o unbricked.gb main.o sio.o
+rgbfix -v -p 0xFF unbricked.gb
\ No newline at end of file
diff --git a/unbricked/serial-link/demo.asm b/unbricked/serial-link/demo.asm
new file mode 100644
index 00000000..c65d168b
--- /dev/null
+++ b/unbricked/serial-link/demo.asm
@@ -0,0 +1,931 @@
+INCLUDE "hardware.inc"
+
+; BG Tile IDs
+RSSET 16
+DEF BG_SOLID_0 RB 1
+DEF BG_SOLID_1 RB 1
+DEF BG_SOLID_2 RB 1
+DEF BG_SOLID_3 RB 1
+DEF BG_EMPTY RB 1
+DEF BG_TICK RB 1
+DEF BG_CROSS RB 1
+DEF BG_INTERNAL RB 1
+DEF BG_EXTERNAL RB 1
+DEF BG_INBOX RB 1
+DEF BG_OUTBOX RB 1
+
+; BG map positions (addresses) of various info
+DEF DISPLAY_LINK EQU $9800
+DEF DISPLAY_LOCAL EQU DISPLAY_LINK
+DEF DISPLAY_REMOTE EQU DISPLAY_LOCAL + 32
+DEF DISPLAY_CLOCK_SRC EQU DISPLAY_LINK + 18
+DEF DISPLAY_TX EQU DISPLAY_LINK + 32 * 2
+DEF DISPLAY_TX_STATE EQU DISPLAY_TX + 1
+DEF DISPLAY_TX_ERRORS EQU DISPLAY_TX + 18
+DEF DISPLAY_TX_BUFFER EQU DISPLAY_TX + 32
+DEF DISPLAY_RX EQU DISPLAY_LINK + 32 * 6
+DEF DISPLAY_RX_STATE EQU DISPLAY_RX + 1
+DEF DISPLAY_RX_ERRORS EQU DISPLAY_RX + 18
+DEF DISPLAY_RX_BUFFER EQU DISPLAY_RX + 32
+
+; ANCHOR: serial-demo-defs
+; Link finite state machine modes
+DEF LINKST_MODE EQU $03 ; Mask mode bits
+DEF LINKST_MODE_DOWN EQU $00 ; Inactive / disconnected
+DEF LINKST_MODE_CONNECT EQU $01 ; Establishing link (handshake)
+DEF LINKST_MODE_UP EQU $02 ; Connected
+DEF LINKST_MODE_ERROR EQU $03 ; Fatal error occurred.
+; Indicates current msg type (SYNC / DATA). If set, the next message sent will be SYNC.
+DEF LINKST_STEP_SYNC EQU $08
+; Set when transmitting a DATA packet. Cleared when remote sends acknowledgement via SYNC.
+DEF LINKST_TX_ACT EQU $10
+; Flag set when a MSG_DATA packet is received. Automatically cleared next LinkUpdate.
+DEF LINKST_RX_DATA EQU $20
+; Default/initial Link state
+DEF LINKST_DEFAULT EQU LINKST_MODE_CONNECT
+
+; Maximum number of times to attempt TxData packet transmission.
+DEF LINK_ALLOW_TX_ATTEMPTS EQU 4
+; Rx fault error threshold
+DEF LINK_ALLOW_RX_FAULTS EQU 4
+
+DEF MSG_SYNC EQU $A0
+DEF MSG_SHAKE EQU $B0
+DEF MSG_DATA EQU $C0
+; ANCHOR_END: serial-demo-defs
+
+; ANCHOR: handshake-codes
+; Handshake code sent by internally clocked device (clock provider)
+DEF SHAKE_A EQU $88
+; Handshake code sent by externally clocked device
+DEF SHAKE_B EQU $77
+DEF HANDSHAKE_COUNT EQU 5
+DEF HANDSHAKE_FAILED EQU $F0
+; ANCHOR_END: handshake-codes
+
+
+; ANCHOR: serial-interrupt-vector
+SECTION "Serial Interrupt", ROM0[$58]
+SerialInterrupt:
+ push af
+ push hl
+ call SioPortEnd
+ pop hl
+ pop af
+ reti
+; ANCHOR_END: serial-interrupt-vector
+
+
+SECTION "Header", ROM0[$100]
+
+ jp EntryPoint
+
+ ds $150 - @, 0 ; Make room for the header
+
+EntryPoint:
+ ; Do not turn the LCD off outside of VBlank
+WaitVBlank:
+ ld a, [rLY]
+ cp 144
+ jp c, WaitVBlank
+
+ ; Turn the LCD off
+ ld a, 0
+ ld [rLCDC], a
+
+ ; Copy the tile data
+ ld de, Tiles
+ ld hl, $9000
+ ld bc, TilesEnd - Tiles
+ call Memcopy
+
+ ; clear BG tilemap
+ ld hl, $9800
+ ld b, 32
+ xor a, a
+ ld a, BG_SOLID_0
+.clear_row
+ ld c, 32
+.clear_tile
+ ld [hl+], a
+ dec c
+ jr nz, .clear_tile
+ xor a, 1
+ dec b
+ jr nz, .clear_row
+
+ ; display static elements
+ ld a, BG_OUTBOX
+ ld [DISPLAY_TX], a
+ ld a, BG_INBOX
+ ld [DISPLAY_RX], a
+ ld a, BG_CROSS
+ ld [DISPLAY_RX_ERRORS - 1], a
+
+ xor a, a
+ ld b, 160
+ ld hl, _OAMRAM
+.clear_oam
+ ld [hli], a
+ dec b
+ jp nz, .clear_oam
+
+ ; Turn the LCD on
+ ld a, LCDCF_ON | LCDCF_BGON | LCDCF_OBJON
+ ld [rLCDC], a
+
+ ; During the first (blank) frame, initialize display registers
+ ld a, %11100100
+ ld [rBGP], a
+ ld a, %11100100
+ ld [rOBP0], a
+
+ ; Initialize global variables
+ ld a, 0
+ ld [wFrameCounter], a
+ ld [wCurKeys], a
+ ld [wNewKeys], a
+
+; ANCHOR: serial-demo-init-callsite
+ call LinkInit
+
+Main:
+; ANCHOR_END: serial-demo-init-callsite
+ ld a, [rLY]
+ cp 144
+ jp nc, Main
+
+; ANCHOR: serial-demo-update-callsite
+ call Input
+ call MainUpdate
+; ANCHOR_END: serial-demo-update-callsite
+WaitVBlank2:
+ ld a, [rLY]
+ cp 144
+ jp c, WaitVBlank2
+
+ call LinkDisplay
+ ld a, [wFrameCounter]
+ inc a
+ ld [wFrameCounter], a
+ jp Main
+
+
+; ANCHOR: serial-demo-update
+MainUpdate:
+ ; if B is pressed, reset Link
+ ld a, [wNewKeys]
+ and a, PADF_B
+ jp nz, LinkReset
+
+ call LinkUpdate
+ ; If Link in error state, do nothing
+ ld a, [wLocal.state]
+ and a, LINKST_MODE
+ cp a, LINKST_MODE_ERROR
+ ret z
+ ; send the next data packet if Link is ready
+ ld a, [wLocal.state]
+ and a, LINKST_TX_ACT
+ ret nz
+ ; Write next message to TxData
+ ld hl, wTxData
+ ld a, [wCurKeys]
+ and a, PADF_RIGHT | PADF_LEFT | PADF_UP | PADF_DOWN
+ ld [hl+], a
+ ld a, [hl]
+ rlca
+ inc a
+ ld [hl+], a
+ jp LinkTxStart
+; ANCHOR_END: serial-demo-update
+
+
+; ANCHOR: link-init
+LinkInit:
+ call SioInit
+
+ ; enable the serial interrupt
+ ldh a, [rIE]
+ or a, IEF_SERIAL
+ ldh [rIE], a
+ ; enable interrupt processing globally
+ ei
+
+LinkReset:
+ call SioReset
+ ; reset peers
+ ld a, LINKST_DEFAULT
+ ld [wLocal.state], a
+ ld [wRemote.state], a
+ ld a, $FF
+ ld [wLocal.tx_id], a
+ ld [wLocal.rx_id], a
+ ld [wRemote.tx_id], a
+ ld [wRemote.rx_id], a
+ ; clear faults and retry counter
+ ld a, 0
+ ld [wAllowTxAttempts], a
+ ld a, LINK_ALLOW_RX_FAULTS
+ ld [wAllowRxFaults], a
+ ; clear message buffers
+ ld a, 0
+ ld hl, wTxData
+ ld c, wTxData.end - wTxData
+ call Memfill
+ ld hl, wRxData
+ ld c, wRxData.end - wRxData
+ call Memfill
+ ; go straight to handshake
+ jp HandshakeDefault
+; ANCHOR_END: link-init
+
+
+; ANCHOR: link-error-stop
+; Stop Link because of an unrecoverable error.
+; @mut: AF
+LinkErrorStop:
+ ld a, [wLocal.state]
+ and a, $FF ^ LINKST_MODE
+ or a, LINKST_MODE_ERROR
+ ld [wLocal.state], a
+ jp SioAbort
+; ANCHOR_END: link-error-stop
+
+
+; ANCHOR: link-update
+LinkUpdate:
+ ld a, [wLocal.state]
+ and a, LINKST_MODE
+ cp a, LINKST_MODE_ERROR
+ ret z
+
+ ; clear data received flag
+ ld a, [wLocal.state]
+ and a, $FF ^ LINKST_RX_DATA
+ ld [wLocal.state], a
+
+ call SioTick
+ ld a, [wLocal.state]
+ and a, LINKST_MODE
+ cp a, LINKST_MODE_CONNECT
+ jr z, .link_connect
+ cp a, LINKST_MODE_UP
+ jr z, .link_up
+ ret
+
+.link_up
+ ; handle Sio transfer states
+ ld a, [wSioState]
+ cp a, SIO_DONE
+ jp z, LinkRx
+ cp a, SIO_FAILED
+ jp z, LinkErrorStop
+ cp a, SIO_IDLE
+ jp z, LinkTx
+ ret
+.link_connect
+ ld a, [wHandshakeState]
+ and a, a
+ jp nz, HandshakeUpdate
+ ; handshake complete, enter UP state
+ ld a, [wLocal.state]
+ and a, $FF ^ LINKST_MODE
+ or a, LINKST_MODE_UP
+ ld [wLocal.state], a
+ ld a, 0
+ ret
+; ANCHOR_END: link-update
+
+
+; ANCHOR: link-tx-start
+; Request transmission of TxData.
+; @mut: AF
+LinkTxStart::
+ ld a, [wLocal.state]
+ or a, LINKST_TX_ACT
+ ld [wLocal.state], a
+ ld a, LINK_ALLOW_TX_ATTEMPTS
+ ld [wAllowTxAttempts], a
+ ld a, [wLocal.tx_id]
+ inc a
+ ld [wLocal.tx_id], a
+ ret
+; ANCHOR_END: link-tx-start
+
+
+; ANCHOR: link-send-message
+LinkTx:
+ ld a, [wLocal.state]
+ ld c, a
+ ; if STEP_SYNC flag, do sync
+ and a, LINKST_STEP_SYNC
+ jr nz, .sync
+ ; if nothing to send, do sync
+ ld a, c
+ and a, LINKST_TX_ACT
+ jr z, .sync
+
+ ld a, [wAllowTxAttempts]
+ and a, a
+ jp z, LinkErrorStop
+ dec a
+ ld [wAllowTxAttempts], a
+ ; ensure sync follows
+ ld a, c
+ or a, LINKST_STEP_SYNC
+ ld [wLocal.state], a
+.data:
+ call SioPacketTxPrepare
+ ld a, MSG_DATA
+ ld [hl+], a
+ ld a, [wLocal.tx_id]
+ ld [hl+], a
+ ; copy from wTxData buffer
+ ld de, wTxData
+ ld c, wTxData.end - wTxData
+:
+ ld a, [de]
+ inc de
+ ld [hl+], a
+ dec c
+ jr nz, :-
+ call SioPacketTxFinalise
+ ret
+.sync:
+ ld a, c
+ and a, $FF ^ LINKST_STEP_SYNC
+ ld [wLocal.state], a
+
+ call SioPacketTxPrepare
+ ld a, MSG_SYNC
+ ld [hl+], a
+ ld a, [wLocal.state]
+ ld [hl+], a
+ ld a, [wLocal.tx_id]
+ ld [hl+], a
+ ld a, [wLocal.rx_id]
+ ld [hl+], a
+ call SioPacketTxFinalise
+ ret
+; ANCHOR_END: link-send-message
+
+
+; ANCHOR: link-receive-message
+; Process received packet
+; @mut: AF, BC, HL
+LinkRx:
+ ld a, SIO_IDLE
+ ld [wSioState], a
+
+ call SioPacketRxCheck
+ jr nz, .fault
+.check_passed:
+ ld a, [hl+]
+ cp a, MSG_SYNC
+ jr z, .rx_sync
+ cp a, MSG_DATA
+ jr z, .rx_data
+ ; invalid message type
+.fault:
+ ld a, [wAllowRxFaults]
+ and a, a
+ jp z, LinkErrorStop
+ dec a
+ ld [wAllowRxFaults], a
+ ret
+; handle MSG_SYNC
+.rx_sync:
+ ; Update remote state (always to newest)
+ ld a, [hl+]
+ ld [wRemote.state], a
+ ld a, [hl+]
+ ld [wRemote.tx_id], a
+ ld a, [hl+]
+ ld [wRemote.rx_id], a
+ ld b, a
+
+ ld a, [wLocal.state]
+ ld c, a
+ and a, LINKST_TX_ACT
+ ret z ; not waiting
+ ld a, [wLocal.tx_id]
+ cp a, b
+ ret nz
+ ld a, c
+ and a, $FF ^ LINKST_TX_ACT
+ ld [wLocal.state], a
+ ret
+; handle MSG_DATA
+.rx_data:
+ ; save message ID
+ ld a, [hl+]
+ ld [wLocal.rx_id], a
+
+ ; copy data to buffer
+ ld de, wRxData
+ ld c, wRxData.end - wRxData
+:
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, :-
+
+ ; set data received flag
+ ld a, [wLocal.state]
+ or a, LINKST_RX_DATA
+ ld [wLocal.state], a
+ ret
+; ANCHOR_END: link-receive-message
+
+
+; @param A: value
+; @param C: length
+; @param HL: from address
+; @mut: C, HL
+Memfill:
+ ld [hl+], a
+ dec c
+ jr nz, Memfill
+ ret
+
+
+LinkDisplay:
+ ld hl, DISPLAY_CLOCK_SRC
+ call DrawClockSource
+ ld a, [wFrameCounter]
+ rrca
+ rrca
+ and 2
+ add BG_SOLID_1
+ ld [hl+], a
+
+ ld hl, DISPLAY_LOCAL
+ ld a, [wLocal.state]
+ call DrawLinkState
+ inc hl
+ ld a, [wLocal.tx_id]
+ ld b, a
+ call PrintHex
+ inc hl
+ ld a, [wLocal.rx_id]
+ ld b, a
+ call PrintHex
+
+ ld hl, DISPLAY_REMOTE
+ ld a, [wRemote.state]
+ call DrawLinkState
+ inc hl
+ ld a, [wRemote.tx_id]
+ ld b, a
+ call PrintHex
+ inc hl
+ ld a, [wRemote.rx_id]
+ ld b, a
+ call PrintHex
+
+ ld hl, DISPLAY_TX_STATE
+ ld a, [wTxData.id]
+ ld b, a
+ call PrintHex
+ inc hl
+ ld a, [wTxData.value]
+ ld b, a
+ call PrintHex
+ ld hl, DISPLAY_TX_ERRORS
+ ld a, [wAllowTxAttempts]
+ ld b, a
+ call PrintHex
+
+ ld hl, DISPLAY_RX_STATE
+ ld a, [wRxData.id]
+ ld b, a
+ call PrintHex
+ inc hl
+ ld a, [wRxData.value]
+ ld b, a
+ call PrintHex
+ ld hl, DISPLAY_RX_ERRORS
+ ld a, [wAllowRxFaults]
+ ld b, a
+ call PrintHex
+
+ ld a, [wFrameCounter]
+ and a, $01
+ jp z, DrawBufferTx
+ jp DrawBufferRx
+
+
+; Draw Link state
+; @param A: value
+; @param HL: dest
+; @mut: AF, B, HL
+DrawLinkState:
+ and a, LINKST_MODE
+ cp a, LINKST_MODE_CONNECT
+ jr nz, :+
+ ld a, [wHandshakeState]
+ and $0F
+ ld [hl+], a
+ ret
+:
+ ld b, BG_EMPTY
+ cp a, LINKST_MODE_DOWN
+ jr z, .end
+ ld b, BG_TICK
+ cp a, LINKST_MODE_UP
+ jr z, .end
+ ld b, BG_CROSS
+ cp a, LINKST_MODE_ERROR
+ jr z, .end
+ ld b, a
+ jp PrintHex
+.end
+ ld a, b
+ ld [hl+], a
+ ret
+
+
+; @param HL: dest
+; @mut AF, HL
+DrawClockSource:
+ ldh a, [rSC]
+ and SCF_SOURCE
+ ld a, BG_EXTERNAL
+ jr z, :+
+ ld a, BG_INTERNAL
+:
+ ld [hl+], a
+ ret
+
+
+; @mut: AF, BC, DE, HL
+DrawBufferTx:
+ ld de, wSioBufferTx
+ ld hl, DISPLAY_TX_BUFFER
+ ld c, 8
+.loop_tx
+ ld a, [de]
+ inc de
+ ld b, a
+ call PrintHex
+ dec c
+ jr nz, .loop_tx
+ ret
+
+
+; @mut: AF, BC, DE, HL
+DrawBufferRx:
+ ld de, wSioBufferRx
+ ld hl, DISPLAY_RX_BUFFER
+ ld c, 8
+.loop_rx
+ ld a, [de]
+ inc de
+ ld b, a
+ call PrintHex
+ dec c
+ jr nz, .loop_rx
+ ret
+
+
+; Increment the byte at [HL], if it's less than the upper bound (B).
+; Input values greater than (B) will be clamped.
+; @param B: upper bound (inclusive)
+; @param HL: pointer to value
+; @return F.Z: (result == bound)
+; @return F.C: (result < bound)
+u8ptr_IncrementTo:
+ ld a, [hl]
+ inc a
+ jr z, .clampit ; catch overflow (value was 255)
+ cp a, b
+ jr nc, .clampit ; value >= bound
+ ret c ; value < bound
+.clampit
+ ld [hl], b
+ xor a, a ; return Z, NC
+ ret
+
+
+; @param B: value
+; @param HL: dest
+; @mut: AF, HL
+PrintHex:
+ ld a, b
+ swap a
+ and a, $0F
+ ld [hl+], a
+ ld a, b
+ and a, $0F
+ ld [hl+], a
+ ret
+
+
+Input:
+ ; Poll half the controller
+ ld a, P1F_GET_BTN
+ call .onenibble
+ ld b, a ; B7-4 = 1; B3-0 = unpressed buttons
+
+ ; Poll the other half
+ ld a, P1F_GET_DPAD
+ call .onenibble
+ swap a ; A3-0 = unpressed directions; A7-4 = 1
+ xor a, b ; A = pressed buttons + directions
+ ld b, a ; B = pressed buttons + directions
+
+ ; And release the controller
+ ld a, P1F_GET_NONE
+ ldh [rP1], a
+
+ ; Combine with previous wCurKeys to make wNewKeys
+ ld a, [wCurKeys]
+ xor a, b ; A = keys that changed state
+ and a, b ; A = keys that changed to pressed
+ ld [wNewKeys], a
+ ld a, b
+ ld [wCurKeys], a
+ ret
+
+.onenibble
+ ldh [rP1], a ; switch the key matrix
+ call .knownret ; burn 10 cycles calling a known ret
+ ldh a, [rP1] ; ignore value while waiting for the key matrix to settle
+ ldh a, [rP1]
+ ldh a, [rP1] ; this read counts
+ or a, $F0 ; A7-4 = 1; A3-0 = unpressed keys
+.knownret
+ ret
+
+; Copy bytes from one area to another.
+; @param de: Source
+; @param hl: Destination
+; @param bc: Length
+Memcopy:
+ ld a, [de]
+ ld [hli], a
+ inc de
+ dec bc
+ ld a, b
+ or a, c
+ jp nz, Memcopy
+ ret
+
+Tiles:
+ ; Hexadecimal digits (0123456789ABCDEF)
+ dw $0000, $1c1c, $2222, $2222, $2a2a, $2222, $2222, $1c1c
+ dw $0000, $0c0c, $0404, $0404, $0404, $0404, $0404, $0e0e
+ dw $0000, $1c1c, $2222, $0202, $0202, $1c1c, $2020, $3e3e
+ dw $0000, $1c1c, $2222, $0202, $0c0c, $0202, $2222, $1c1c
+ dw $0000, $2020, $2020, $2828, $2828, $3e3e, $0808, $0808
+ dw $0000, $3e3e, $2020, $3e3e, $0202, $0202, $0404, $3838
+ dw $0000, $0c0c, $1010, $2020, $3c3c, $2222, $2222, $1c1c
+ dw $0000, $3e3e, $2222, $0202, $0202, $0404, $0808, $1010
+ dw $0000, $1c1c, $2222, $2222, $1c1c, $2222, $2222, $1c1c
+ dw $0000, $1c1c, $2222, $2222, $1e1e, $0202, $0202, $0202
+ dw $0000, $1c1c, $2222, $2222, $4242, $7e7e, $4242, $4242
+ dw $0000, $7c7c, $2222, $2222, $2424, $3a3a, $2222, $7c7c
+ dw $0000, $1c1c, $2222, $4040, $4040, $4040, $4242, $3c3c
+ dw $0000, $7c7c, $2222, $2222, $2222, $2222, $2222, $7c7c
+ dw $0000, $7c7c, $4040, $4040, $4040, $7878, $4040, $7c7c
+ dw $0000, $7c7c, $4040, $4040, $4040, $7878, $4040, $4040
+
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+
+ dw `11111111
+ dw `11111111
+ dw `11111111
+ dw `11111111
+ dw `11111111
+ dw `11111111
+ dw `11111111
+ dw `11111111
+
+ dw `22222222
+ dw `22222222
+ dw `22222222
+ dw `22222222
+ dw `22222222
+ dw `22222222
+ dw `22222222
+ dw `22222222
+
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+
+ ; empty
+ dw `00000000
+ dw `01111110
+ dw `21000210
+ dw `21000210
+ dw `21000210
+ dw `21000210
+ dw `21111110
+ dw `22222200
+
+ ; tick
+ dw `00000000
+ dw `01111113
+ dw `21000233
+ dw `21000330
+ dw `33003310
+ dw `21333110
+ dw `21131110
+ dw `22222200
+
+ ; cross
+ dw `03000000
+ dw `03311113
+ dw `21330330
+ dw `21033210
+ dw `21333210
+ dw `33003310
+ dw `21111310
+ dw `22222200
+
+ ; internal
+ dw `03333333
+ dw `01223333
+ dw `00033300
+ dw `00033300
+ dw `00023300
+ dw `00023300
+ dw `03333333
+ dw `01223333
+
+ ; external
+ dw `03333221
+ dw `03333333
+ dw `03300000
+ dw `03333210
+ dw `03333330
+ dw `03300000
+ dw `03333221
+ dw `03333333
+
+ ; inbox
+ dw `33330003
+ dw `30000030
+ dw `30030300
+ dw `30033000
+ dw `30033303
+ dw `30000003
+ dw `30000003
+ dw `33333333
+
+ ; outbox
+ dw `33330333
+ dw `30000033
+ dw `30000303
+ dw `30003000
+ dw `30030003
+ dw `30000003
+ dw `30000003
+ dw `33333333
+TilesEnd:
+
+
+SECTION "Counter", WRAM0
+wFrameCounter: db
+
+SECTION "Input Variables", WRAM0
+wCurKeys: db
+wNewKeys: db
+
+; ANCHOR: serial-demo-wram
+SECTION "Link", WRAM0
+; Local peer state
+wLocal:
+ .state: db
+ .tx_id: db
+ .rx_id: db
+; Remote peer state
+wRemote:
+ .state: db
+ .tx_id: db
+ .rx_id: db
+
+; Buffer for outbound MSG_DATA
+wTxData:
+ .id: db
+ .value: db
+ .end:
+; Buffer for inbound MSG_DATA
+wRxData:
+ .id: db
+ .value: db
+ .end:
+
+wAllowTxAttempts: db
+wAllowRxFaults: db
+; ANCHOR_END: serial-demo-wram
+
+
+; ANCHOR: handshake-state
+SECTION "Handshake State", WRAM0
+wHandshakeState:: db
+; ANCHOR_END: handshake-state
+
+
+; ANCHOR: handshake-begin
+SECTION "Handshake Impl", ROM0
+; Begin handshake as the default externally clocked device.
+HandshakeDefault:
+ call SioAbort
+ ld a, 0
+ ldh [rSC], a
+ ld a, HANDSHAKE_COUNT
+ ld [wHandshakeState], a
+ jr HandshakeSendPacket
+
+
+; Begin handshake as the clock provider / internally clocked device.
+HandshakeAsClockProvider:
+ call SioAbort
+ ld a, SCF_SOURCE
+ ldh [rSC], a
+ ld a, HANDSHAKE_COUNT
+ ld [wHandshakeState], a
+ jr HandshakeSendPacket
+
+
+HandshakeSendPacket:
+ call SioPacketTxPrepare
+ ld a, MSG_SHAKE
+ ld [hl+], a
+ ld b, SHAKE_A
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ jr nz, :+
+ ld b, SHAKE_B
+:
+ ld [hl], b
+ jp SioPacketTxFinalise
+; ANCHOR_END: handshake-begin
+
+
+; ANCHOR: handshake-update
+HandshakeUpdate:
+ ; press START: perform handshake as clock provider
+ ld a, [wNewKeys]
+ bit PADB_START, a
+ jr nz, HandshakeAsClockProvider
+ ; Check if transfer has completed.
+ ld a, [wSioState]
+ cp a, SIO_DONE
+ jr z, HandshakeMsgRx
+ cp a, SIO_ACTIVE
+ ret z
+ ; Use DIV to "randomly" try being the clock provider
+ ldh a, [rDIV]
+ rrca
+ jr c, HandshakeAsClockProvider
+ jr HandshakeDefault
+; ANCHOR_END: handshake-update
+
+
+; ANCHOR: handshake-xfer-complete
+HandshakeMsgRx:
+ ; flush sio status
+ ld a, SIO_IDLE
+ ld [wSioState], a
+ call SioPacketRxCheck
+ jr nz, .failed
+ ld a, [hl+]
+ cp a, MSG_SHAKE
+ jr nz, .failed
+ ld b, SHAKE_A
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ jr z, :+
+ ld b, SHAKE_B
+:
+ ld a, [hl+]
+ cp a, b
+ jr nz, .failed
+ ld a, [wHandshakeState]
+ dec a
+ ld [wHandshakeState], a
+ jr nz, HandshakeSendPacket
+ ret
+.failed
+ ld a, [wHandshakeState]
+ or a, HANDSHAKE_FAILED
+ ld [wHandshakeState], a
+ ret
+; ANCHOR_END: handshake-xfer-complete
diff --git a/unbricked/serial-link/hardware.inc b/unbricked/serial-link/hardware.inc
new file mode 100644
index 00000000..1ba2c220
--- /dev/null
+++ b/unbricked/serial-link/hardware.inc
@@ -0,0 +1,1022 @@
+;******************************************************************************
+; Game Boy hardware constant definitions
+; https://github.com/gbdev/hardware.inc
+;******************************************************************************
+
+; To the extent possible under law, the authors of this work have
+; waived all copyright and related or neighboring rights to the work.
+; See https://creativecommons.org/publicdomain/zero/1.0/ for details.
+; SPDX-License-Identifier: CC0-1.0
+
+; If this file was already included, don't do it again
+if !def(HARDWARE_INC)
+
+; Check for the minimum supported RGBDS version
+if !def(__RGBDS_MAJOR__) || !def(__RGBDS_MINOR__) || !def(__RGBDS_PATCH__)
+ fail "This version of 'hardware.inc' requires RGBDS version 0.5.0 or later"
+endc
+if __RGBDS_MAJOR__ == 0 && __RGBDS_MINOR__ < 5
+ fail "This version of 'hardware.inc' requires RGBDS version 0.5.0 or later."
+endc
+
+; Define the include guard and the current hardware.inc version
+; (do this after the RGBDS version check since the `def` syntax depends on it)
+def HARDWARE_INC equ 1
+def HARDWARE_INC_VERSION equs "4.10.0"
+
+; Usage: rev_Check_hardware_inc
+; Examples:
+; rev_Check_hardware_inc 1.2.3
+; rev_Check_hardware_inc 1.2 (equivalent to 1.2.0)
+; rev_Check_hardware_inc 1 (equivalent to 1.0.0)
+MACRO rev_Check_hardware_inc
+ def hw_inc_cur_ver\@ equs strrpl("{HARDWARE_INC_VERSION}", ".", ",")
+ def hw_inc_min_ver\@ equs strrpl("\1", ".", ",")
+ def hw_inc_def_check\@ equs """MACRO hw_inc_check\@
+ if \\1 != \\4 || (\\2 < \\5 || (\\2 == \\5 && \\3 < \\6))
+ fail "Version \\1.\\2.\\3 of 'hardware.inc' is incompatible with requested version \\4.\\5.\\6"
+ endc
+ \nENDM"""
+ hw_inc_def_check\@
+ hw_inc_check\@ {hw_inc_cur_ver\@}, {hw_inc_min_ver\@}, 0, 0
+ purge hw_inc_cur_ver\@, hw_inc_min_ver\@, hw_inc_def_check\@, hw_inc_check\@
+ENDM
+
+
+;******************************************************************************
+; Memory-mapped registers ($FFxx range)
+;******************************************************************************
+
+; -- JOYP / P1 ($FF00) --------------------------------------------------------
+; Joypad face buttons
+def rJOYP equ $FF00
+
+def JOYPB_GET_BTN equ 5 ; 0 = reading buttons [r/w]
+def JOYPB_GET_DPAD equ 4 ; 0 = reading Control Pad [r/w]
+ def JOYPF_GET equ %00_11_0000 ; select which inputs to read from the lower nybble
+ def JOYP_GET_BTN equ %00_01_0000 ; reading A/B/Select/Start buttons
+ def JOYP_GET_DPAD equ %00_10_0000 ; reading Control Pad directions
+ def JOYP_GET_NONE equ %00_11_0000 ; reading nothing
+
+def JOYPB_START equ 3 ; 0 = Start is pressed (if reading buttons) [ro]
+def JOYPB_SELECT equ 2 ; 0 = Select is pressed (if reading buttons) [ro]
+def JOYPB_B equ 1 ; 0 = B is pressed (if reading buttons) [ro]
+def JOYPB_A equ 0 ; 0 = A is pressed (if reading buttons) [ro]
+def JOYPB_DOWN equ 3 ; 0 = Down is pressed (if reading Control Pad) [ro]
+def JOYPB_UP equ 2 ; 0 = Up is pressed (if reading Control Pad) [ro]
+def JOYPB_LEFT equ 1 ; 0 = Left is pressed (if reading Control Pad) [ro]
+def JOYPB_RIGHT equ 0 ; 0 = Right is pressed (if reading Control Pad) [ro]
+ def JOYPF_INPUTS equ %0000_1111
+ def JOYPF_START equ 1 << JOYPB_START
+ def JOYPF_SELECT equ 1 << JOYPB_SELECT
+ def JOYPF_B equ 1 << JOYPB_B
+ def JOYPF_A equ 1 << JOYPB_A
+ def JOYPF_DOWN equ 1 << JOYPB_DOWN
+ def JOYPF_UP equ 1 << JOYPB_UP
+ def JOYPF_LEFT equ 1 << JOYPB_LEFT
+ def JOYPF_RIGHT equ 1 << JOYPB_RIGHT
+
+; Combined input byte, with Control Pad in high nybble (conventional order)
+def PADB_DOWN equ 7
+def PADB_UP equ 6
+def PADB_LEFT equ 5
+def PADB_RIGHT equ 4
+def PADB_START equ 3
+def PADB_SELECT equ 2
+def PADB_B equ 1
+def PADB_A equ 0
+ def PADF_DOWN equ 1 << PADB_DOWN
+ def PADF_UP equ 1 << PADB_UP
+ def PADF_LEFT equ 1 << PADB_LEFT
+ def PADF_RIGHT equ 1 << PADB_RIGHT
+ def PADF_START equ 1 << PADB_START
+ def PADF_SELECT equ 1 << PADB_SELECT
+ def PADF_B equ 1 << PADB_B
+ def PADF_A equ 1 << PADB_A
+
+; Combined input byte, with Control Pad in low nybble (swapped order)
+def PADB_SWAP_START equ 7
+def PADB_SWAP_SELECT equ 6
+def PADB_SWAP_B equ 5
+def PADB_SWAP_A equ 4
+def PADB_SWAP_DOWN equ 3
+def PADB_SWAP_UP equ 2
+def PADB_SWAP_LEFT equ 1
+def PADB_SWAP_RIGHT equ 0
+ def PADF_SWAP_START equ 1 << PADB_SWAP_START
+ def PADF_SWAP_SELECT equ 1 << PADB_SWAP_SELECT
+ def PADF_SWAP_B equ 1 << PADB_SWAP_B
+ def PADF_SWAP_A equ 1 << PADB_SWAP_A
+ def PADF_SWAP_DOWN equ 1 << PADB_SWAP_DOWN
+ def PADF_SWAP_UP equ 1 << PADB_SWAP_UP
+ def PADF_SWAP_LEFT equ 1 << PADB_SWAP_LEFT
+ def PADF_SWAP_RIGHT equ 1 << PADB_SWAP_RIGHT
+
+; -- SB ($FF01) ---------------------------------------------------------------
+; Serial transfer data [r/w]
+def rSB equ $FF01
+
+; -- SC ($FF02) ---------------------------------------------------------------
+; Serial transfer control
+def rSC equ $FF02
+
+def SCB_START equ 7 ; reading 1 = transfer in progress, writing 1 = start transfer [r/w]
+def SCB_SPEED equ 1 ; (CGB only) 1 = use faster internal clock [r/w]
+def SCB_SOURCE equ 0 ; 0 = use external clock ("slave"), 1 = use internal clock ("master") [r/w]
+ def SCF_START equ 1 << SCB_START
+ def SCF_SPEED equ 1 << SCB_SPEED
+ def SC_SLOW equ 0 << SCB_SPEED
+ def SC_FAST equ 1 << SCB_SPEED
+ def SCF_SOURCE equ 1 << SCB_SOURCE
+ def SC_EXTERNAL equ 0 << SCB_SOURCE
+ def SC_INTERNAL equ 1 << SCB_SOURCE
+
+; -- $FF03 is unused ----------------------------------------------------------
+
+; -- DIV ($FF04) --------------------------------------------------------------
+; Divider register [r/w]
+def rDIV equ $FF04
+
+; -- TIMA ($FF05) -------------------------------------------------------------
+; Timer counter [r/w]
+def rTIMA equ $FF05
+
+; -- TMA ($FF06) --------------------------------------------------------------
+; Timer modulo [r/w]
+def rTMA equ $FF06
+
+; -- TAC ($FF07) --------------------------------------------------------------
+; Timer control
+def rTAC equ $FF07
+
+def TACB_START equ 2 ; enable incrementing TIMA [r/w]
+ def TACF_STOP equ 0 << TACB_START
+ def TACF_START equ 1 << TACB_START
+
+def TACF_CLOCK equ %000000_11 ; the frequency at which TIMER_CNT increments [r/w]
+ def TACF_4KHZ equ %000000_00 ; every 256 M-cycles = ~4 KHz on DMG
+ def TACF_262KHZ equ %000000_01 ; every 4 M-cycles = ~262 KHz on DMG
+ def TACF_65KHZ equ %000000_10 ; every 16 M-cycles = ~65 KHz on DMG
+ def TACF_16KHZ equ %000000_11 ; every 64 M-cycles = ~16 KHz on DMG
+
+; -- $FF08-$FF0E are unused ---------------------------------------------------
+
+; -- IF ($FF0F) ---------------------------------------------------------------
+; Pending interrupts
+def rIF equ $FF0F
+
+def IFB_JOYPAD equ 4 ; 1 = joypad interrupt is pending [r/w]
+def IFB_SERIAL equ 3 ; 1 = serial interrupt is pending [r/w]
+def IFB_TIMER equ 2 ; 1 = timer interrupt is pending [r/w]
+def IFB_STAT equ 1 ; 1 = STAT interrupt is pending [r/w]
+def IFB_VBLANK equ 0 ; 1 = VBlank interrupt is pending [r/w]
+ def IFF_JOYPAD equ 1 << IFB_JOYPAD
+ def IFF_SERIAL equ 1 << IFB_SERIAL
+ def IFF_TIMER equ 1 << IFB_TIMER
+ def IFF_STAT equ 1 << IFB_STAT
+ def IFF_VBLANK equ 1 << IFB_VBLANK
+
+; -- AUD1SWEEP / NR10 ($FF10) -------------------------------------------------
+; Audio channel 1 sweep
+def rAUD1SWEEP equ $FF10
+
+def AUD1SWEEPF_TIME equ %0_111_0000 ; how long between sweep iterations
+ ; (in 128 Hz ticks, ~7.8 ms apart) [r/w]
+
+def AUD1SWEEPB_DIR equ 3 ; sweep direction [r/w]
+ def AUD1SWEEPF_DIR equ 1 << AUD1SWEEPB_DIR
+ def AUD1SWEEP_UP equ 0 << AUD1SWEEPB_DIR
+ def AUD1SWEEP_DOWN equ 1 << AUD1SWEEPB_DIR
+
+def AUD1SWEEP_SHIFT equ %00000_111 ; how much the period increases/decreases per iteration [r/w]
+
+; -- AUD1LEN / NR11 ($FF11) ---------------------------------------------------
+; Audio channel 1 length timer and duty cycle
+def rAUD1LEN equ $FF11
+
+; These values are also applicable to AUD2LEN
+def AUDLENF_DUTY equ %11_000000 ; ratio of time spent high vs. time spent low [r/w]
+ def AUDLEN_DUTY_12_5 equ %00_000000 ; 12.5%
+ def AUDLEN_DUTY_25 equ %01_000000 ; 25%
+ def AUDLEN_DUTY_50 equ %10_000000 ; 50%
+ def AUDLEN_DUTY_75 equ %11_000000 ; 75%
+
+; This value is also applicable to AUD2LEN and AUD4LEN
+def AUDLENF_TIMER equ %00_111111 ; initial length timer (0-63) [wo]
+
+; -- AUD1ENV / NR12 ($FF12) ---------------------------------------------------
+; Audio channel 1 volume and envelope
+def rAUD1ENV equ $FF12
+
+; Values are also applicable to AUD2ENV and AUD4ENV
+
+def AUDENVF_INIT_VOL equ %1111_0000 ; initial volume [r/w]
+
+def AUDENVB_DIR equ 3 ; direction of volume envelope [r/w]
+ def AUDENVF_DIR equ 1 << AUDENVB_DIR
+ def AUDENV_DOWN equ 0 << AUDENVB_DIR
+ def AUDENV_UP equ 1 << AUDENVB_DIR
+
+def AUDENVF_PACE equ %00000_111 ; how long between envelope iterations
+ ; (in 64 Hz ticks, ~15.6 ms apart) [r/w]
+
+; -- AUD1LOW / NR13 ($FF13) ---------------------------------------------------
+; Audio channel 1 period (low 8 bits) [r/w]
+def rAUD1LOW equ $FF13
+
+; -- AUD1HIGH / NR14 ($FF14) --------------------------------------------------
+; Audio channel 1 period (high 3 bits) and control
+def rAUD1HIGH equ $FF14
+
+; Values are also applicable to AUD2HIGH and AUD3HIGH
+
+def AUDHIGHB_RESTART equ 7 ; 1 = restart the channel [wo]
+def AUDHIGHB_LEN_ENABLE equ 6 ; 1 = reset the channel after the length timer expires [r/w]
+ def AUDHIGH_RESTART equ 1 << AUDHIGHB_RESTART
+ def AUDHIGH_LENGTH_OFF equ 0 << AUDHIGHB_LEN_ENABLE
+ def AUDHIGH_LENGTH_ON equ 1 << AUDHIGHB_LEN_ENABLE
+
+def AUDHIGHF_PERIOD_HIGH equ %00000_111 ; upper 3 bits of the channel's period [r/w]
+
+; -- $FF15 is unused ----------------------------------------------------------
+
+; -- AUD2LEN / NR21 ($FF16) ---------------------------------------------------
+; Audio channel 2 length timer and duty cycle
+def rAUD2LEN equ $FF16
+
+; Values are reused from AUD1LEN
+
+; -- AUD2ENV / NR22 ($FF17) ---------------------------------------------------
+; Audio channel 2 volume and envelope
+def rAUD2ENV equ $FF17
+
+; Values are reused from AUD1ENV
+
+; -- AUD2LOW / NR23 ($FF18) ---------------------------------------------------
+; Audio channel 2 period (low 8 bits) [r/w]
+def rAUD2LOW equ $FF18
+
+; -- AUD2HIGH / NR24 ($FF19) --------------------------------------------------
+; Audio channel 2 period (high 3 bits) and control
+def rAUD2HIGH equ $FF19
+
+; Values are reused from AUD1HIGH
+
+; -- AUD3ENA / NR30 ($FF1A) ---------------------------------------------------
+; Audio channel 3 enable
+def rAUD3ENA equ $FF1A
+
+def AUD3ENAB_ENABLE equ 7 ; 1 = channel is active [r/w]
+ def AUD3ENA_OFF equ 0 << AUD3ENAB_ENABLE
+ def AUD3ENA_ON equ 1 << AUD3ENAB_ENABLE
+
+; -- AUD3LEN / NR31 ($FF1B) ---------------------------------------------------
+; Audio channel 3 length timer [wo]
+def rAUD3LEN equ $FF1B
+
+; -- AUD3LEVEL / NR32 ($FF1C) -------------------------------------------------
+; Audio channel 3 volume
+def rAUD3LEVEL equ $FF1C
+
+def AUD3LEVELF_VOLUME equ %0_11_00000 ; volume level [r/w]
+ def AUD3LEVEL_MUTE equ %0_00_00000 ; 0% (muted)
+ def AUD3LEVEL_100 equ %0_01_00000 ; 100%
+ def AUD3LEVEL_50 equ %0_10_00000 ; 50%
+ def AUD3LEVEL_25 equ %0_11_00000 ; 25%
+
+; -- AUD3LOW / NR33 ($FF1D) ---------------------------------------------------
+; Audio channel 3 period (low 8 bits) [r/w]
+def rAUD3LOW equ $FF1D
+
+; -- AUD3HIGH / NR34 ($FF1E) --------------------------------------------------
+; Audio channel 3 period (high 3 bits) and control
+def rAUD3HIGH equ $FF1E
+
+; Values are reused from AUD1HIGH
+
+; -- $FF1F is unused ----------------------------------------------------------
+
+; -- AUD4LEN / NR41 ($FF20) ---------------------------------------------------
+; Audio channel 4 length timer [wo]
+def rAUD4LEN equ $FF20
+
+; AUDLENF_TIMER value is reused from AUD1LEN
+
+; -- AUD4ENV / NR42 ($FF21) ---------------------------------------------------
+; Audio channel 4 volume and envelope
+def rAUD4ENV equ $FF21
+
+; Values are reused from AUD1ENV
+
+; -- AUD4POLY / NR43 ($FF22) --------------------------------------------------
+; Audio channel 4 period and randomness
+def rAUD4POLY equ $FF22
+
+def AUD4POLYF_SHIFT equ %1111_0000 ; coarse control of the channel's period [r/w]
+
+def AUD4POLYB_WIDTH equ 3 ; controls the noise generator (LFSR)'s step width [r/w]
+ def AUD4POLY_15STEP equ 0 << AUD4POLYB_WIDTH
+ def AUD4POLY_7STEP equ 1 << AUD4POLYB_WIDTH
+
+def AUD4POLYF_DIV equ %00000_111 ; fine control of the channel's period [r/w]
+
+; -- AUD4GO / NR44 ($FF23) ----------------------------------------------------
+; Audio channel 4 control
+def rAUD4GO equ $FF23
+
+def AUD4GOB_RESTART equ 7 ; 1 = restart the channel [wo]
+def AUD4GOB_LEN_ENABLE equ 6 ; 1 = reset the channel after the length timer expires [r/w]
+ def AUD4GO_RESTART equ 1 << AUD4GOB_RESTART
+ def AUD4GO_LENGTH_OFF equ 0 << AUD4GOB_LEN_ENABLE
+ def AUD4GO_LENGTH_ON equ 1 << AUD4GOB_LEN_ENABLE
+
+; -- AUDVOL / NR50 ($FF24) ----------------------------------------------------
+; Audio master volume and VIN mixer
+def rAUDVOL equ $FF24
+
+def AUDVOLB_VIN_LEFT equ 7 ; 1 = output VIN to left ear (SO2, speaker 2) [r/w]
+ def AUDVOL_VIN_LEFT equ 1 << AUDVOLB_VIN_LEFT
+
+def AUDVOLF_LEFT equ %0_111_0000 ; 0 = barely audible, 7 = full volume [r/w]
+
+def AUDVOLB_VIN_RIGHT equ 3 ; 1 = output VIN to right ear (SO1, speaker 1) [r/w]
+ def AUDVOL_VIN_RIGHT equ 1 << AUDVOLB_VIN_RIGHT
+
+def AUDVOLF_RIGHT equ %00000_111 ; 0 = barely audible, 7 = full volume [r/w]
+
+; -- AUDTERM / NR51 ($FF25) ---------------------------------------------------
+; Audio channel mixer
+def rAUDTERM equ $FF25
+
+def AUDTERMB_4_LEFT equ 7 ; 1 = output channel 4 to left ear [r/w]
+def AUDTERMB_3_LEFT equ 6 ; 1 = output channel 3 to left ear [r/w]
+def AUDTERMB_2_LEFT equ 5 ; 1 = output channel 2 to left ear [r/w]
+def AUDTERMB_1_LEFT equ 4 ; 1 = output channel 1 to left ear [r/w]
+def AUDTERMB_4_RIGHT equ 3 ; 1 = output channel 4 to right ear [r/w]
+def AUDTERMB_3_RIGHT equ 2 ; 1 = output channel 3 to right ear [r/w]
+def AUDTERMB_2_RIGHT equ 1 ; 1 = output channel 2 to right ear [r/w]
+def AUDTERMB_1_RIGHT equ 0 ; 1 = output channel 1 to right ear [r/w]
+ def AUDTERM_4_LEFT equ 1 << AUDTERMB_4_LEFT
+ def AUDTERM_3_LEFT equ 1 << AUDTERMB_3_LEFT
+ def AUDTERM_2_LEFT equ 1 << AUDTERMB_2_LEFT
+ def AUDTERM_1_LEFT equ 1 << AUDTERMB_1_LEFT
+ def AUDTERM_4_RIGHT equ 1 << AUDTERMB_4_RIGHT
+ def AUDTERM_3_RIGHT equ 1 << AUDTERMB_3_RIGHT
+ def AUDTERM_2_RIGHT equ 1 << AUDTERMB_2_RIGHT
+ def AUDTERM_1_RIGHT equ 1 << AUDTERMB_1_RIGHT
+
+; -- AUDENA / NR52 ($FF26) ----------------------------------------------------
+; Audio master enable
+def rAUDENA equ $FF26
+
+def AUDENAB_ENABLE equ 7 ; 0 = disable the APU (resets all audio registers to 0!) [r/w]
+def AUDENAB_ENABLE_CH4 equ 3 ; 1 = channel 4 is running [ro]
+def AUDENAB_ENABLE_CH3 equ 2 ; 1 = channel 3 is running [ro]
+def AUDENAB_ENABLE_CH2 equ 1 ; 1 = channel 2 is running [ro]
+def AUDENAB_ENABLE_CH1 equ 0 ; 1 = channel 1 is running [ro]
+ def AUDENA_OFF equ 0 << AUDENAB_ENABLE
+ def AUDENA_ON equ 1 << AUDENAB_ENABLE
+ def AUDENAF_CH4_OFF equ 0 << AUDENAB_ENABLE_CH4
+ def AUDENAF_CH4_ON equ 1 << AUDENAB_ENABLE_CH4
+ def AUDENAF_CH3_OFF equ 0 << AUDENAB_ENABLE_CH3
+ def AUDENAF_CH3_ON equ 1 << AUDENAB_ENABLE_CH3
+ def AUDENAF_CH2_OFF equ 0 << AUDENAB_ENABLE_CH2
+ def AUDENAF_CH2_ON equ 1 << AUDENAB_ENABLE_CH2
+ def AUDENAF_CH1_OFF equ 0 << AUDENAB_ENABLE_CH1
+ def AUDENAF_CH1_ON equ 1 << AUDENAB_ENABLE_CH1
+
+; -- $FF27-$FF2F are unused ---------------------------------------------------
+
+; -- AUD3WAVE ($FF30-$FF3F) ---------------------------------------------------
+; Audio channel 3 wave pattern RAM [r/w]
+def _AUD3WAVERAM equ $FF30 ; $FF30-$FF3F
+
+def rAUD3WAVE_0 equ $FF30
+def rAUD3WAVE_1 equ $FF31
+def rAUD3WAVE_2 equ $FF32
+def rAUD3WAVE_3 equ $FF33
+def rAUD3WAVE_4 equ $FF34
+def rAUD3WAVE_5 equ $FF35
+def rAUD3WAVE_6 equ $FF36
+def rAUD3WAVE_7 equ $FF37
+def rAUD3WAVE_8 equ $FF38
+def rAUD3WAVE_9 equ $FF39
+def rAUD3WAVE_A equ $FF3A
+def rAUD3WAVE_B equ $FF3B
+def rAUD3WAVE_C equ $FF3C
+def rAUD3WAVE_D equ $FF3D
+def rAUD3WAVE_E equ $FF3E
+def rAUD3WAVE_F equ $FF3F
+
+def AUD3WAVE_SIZE equ 16
+
+; -- LCDC ($FF40) -------------------------------------------------------------
+; PPU graphics control
+def rLCDC equ $FF40
+
+def LCDCB_ON equ 7 ; whether the PPU (and LCD) are turned on [r/w]
+def LCDCB_WIN9C00 equ 6 ; which tilemap the Window reads from [r/w]
+def LCDCB_WINON equ 5 ; whether the Window is enabled [r/w]
+def LCDCB_BLKS equ 4 ; which "tile blocks" the BG and Window use [r/w]
+def LCDCB_BG9C00 equ 3 ; which tilemap the BG reads from [r/w]
+def LCDCB_OBJ16 equ 2 ; how many pixels tall each OBJ is [r/w]
+def LCDCB_OBJON equ 1 ; whether OBJs are enabled [r/w]
+def LCDCB_BGON equ 0 ; (DMG only) whether the BG is enabled [r/w]
+def LCDCB_PRION equ 0 ; (CGB only) whether OBJ priority bits are enabled [r/w]
+ def LCDCF_OFF equ 0 << LCDCB_ON
+ def LCDCF_ON equ 1 << LCDCB_ON
+ def LCDCF_WIN9800 equ 0 << LCDCB_WIN9C00
+ def LCDCF_WIN9C00 equ 1 << LCDCB_WIN9C00
+ def LCDCF_WINOFF equ 0 << LCDCB_WINON
+ def LCDCF_WINON equ 1 << LCDCB_WINON
+ def LCDCF_BLKS equ 1 << LCDCB_BLKS
+ def LCDCF_BLK21 equ 0 << LCDCB_BLKS
+ def LCDCF_BLK01 equ 1 << LCDCB_BLKS
+ def LCDCF_BG9800 equ 0 << LCDCB_BG9C00
+ def LCDCF_BG9C00 equ 1 << LCDCB_BG9C00
+ def LCDCF_OBJ8 equ 0 << LCDCB_OBJ16
+ def LCDCF_OBJ16 equ 1 << LCDCB_OBJ16
+ def LCDCF_OBJOFF equ 0 << LCDCB_OBJON
+ def LCDCF_OBJON equ 1 << LCDCB_OBJON
+ def LCDCF_BGOFF equ 0 << LCDCB_BGON
+ def LCDCF_BGON equ 1 << LCDCB_BGON
+ def LCDCF_PRIOFF equ 0 << LCDCB_PRION
+ def LCDCF_PRION equ 1 << LCDCB_PRION
+
+; -- STAT ($FF41) -------------------------------------------------------------
+; Graphics status and interrupt control
+def rSTAT equ $FF41
+
+def STATB_LYC equ 6 ; 1 = LY match triggers the STAT interrupt [r/w]
+def STATB_MODE10 equ 5 ; 1 = OAM Scan triggers the PPU interrupt [r/w]
+def STATB_MODE01 equ 4 ; 1 = VBlank triggers the PPU interrupt [r/w]
+def STATB_MODE00 equ 3 ; 1 = HBlank triggers the PPU interrupt [r/w]
+def STATB_LYCF equ 2 ; 1 = LY is currently equal to LYC [ro]
+def STATB_BUSY equ 1 ; 1 = the PPU is currently accessing VRAM [ro]
+ def STATF_LYC equ 1 << STATB_LYC
+ def STATF_MODE10 equ 1 << STATB_MODE10
+ def STATF_MODE01 equ 1 << STATB_MODE01
+ def STATF_MODE00 equ 1 << STATB_MODE00
+ def STATF_LYCF equ 1 << STATB_LYCF
+ def STATF_BUSY equ 1 << STATB_BUSY
+
+def STATF_MODE equ %000000_11 ; PPU's current status [ro]
+ def STATF_HBL equ %000000_00 ; waiting after a line's rendering (HBlank)
+ def STATF_VBL equ %000000_01 ; waiting between frames (VBlank)
+ def STATF_OAM equ %000000_10 ; checking which OBJs will be rendered on this line (OAM scan)
+ def STATF_LCD equ %000000_11 ; pushing pixels to the LCD
+
+; -- SCY ($FF42) --------------------------------------------------------------
+; Background Y scroll offset (in pixels) [r/w]
+def rSCY equ $FF42
+
+; -- SCX ($FF43) --------------------------------------------------------------
+; Background X scroll offset (in pixels) [r/w]
+def rSCX equ $FF43
+
+; -- LY ($FF44) ---------------------------------------------------------------
+; Y coordinate of the line currently processed by the PPU (0-153) [ro]
+def rLY equ $FF44
+
+def LY_VBLANK equ 144 ; 144-153 is the VBlank period
+
+; -- LYC ($FF45) --------------------------------------------------------------
+; Value that LY is constantly compared to [r/w]
+def rLYC equ $FF45
+
+; -- DMA ($FF46) --------------------------------------------------------------
+; OAM DMA start address (high 8 bits) and start [wo]
+def rDMA equ $FF46
+
+; -- BGP ($FF47) --------------------------------------------------------------
+; (DMG only) Background color mapping [r/w]
+def rBGP equ $FF47
+
+def BGP_SGB_TRANSFER equ %11_10_01_00 ; set BGP to this value before SGB VRAM transfer
+
+; -- OBP0 ($FF48) -------------------------------------------------------------
+; (DMG only) OBJ color mapping #0 [r/w]
+def rOBP0 equ $FF48
+
+; -- OBP1 ($FF49) -------------------------------------------------------------
+; (DMG only) OBJ color mapping #1 [r/w]
+def rOBP1 equ $FF49
+
+; -- WY ($FF4A) ---------------------------------------------------------------
+; Y coordinate of the Window's top-left pixel (0-143) [r/w]
+def rWY equ $FF4A
+
+; -- WX ($FF4B) ---------------------------------------------------------------
+; X coordinate of the Window's top-left pixel, plus 7 (7-166) [r/w]
+def rWX equ $FF4B
+
+def WX_OFS equ 7 ; subtract this to get the actual Window Y coordinate
+
+; -- KEY0 ($FF4C) -------------------------------------------------------------
+; (CGB boot ROM only) CPU mode select
+def rKEY0 equ $FF4C
+
+; KEY0 is known as the "CPU mode register" in Fig. 11 of this patent:
+; https://patents.google.com/patent/US6322447B1/en?oq=US6322447bi
+; "OBJ priority mode designating register" in the same patent
+; Credit to @mattcurrie for this finding!
+
+def KEY0F_MODE equ %0000_11_00 ; current system mode [r/w]
+ def KEY0F_CGB equ %0000_00_00 ; CGB mode
+ def KEY0F_DMG equ %0000_01_00 ; DMG compatibility mode
+ def KEY0F_PGB1 equ %0000_10_00 ; LCD is driven externally, CPU is stopped
+ def KEY0F_PGB2 equ %0000_11_00 ; LCD is driven externally, CPU is running
+
+; -- SPD / KEY1 ($FF4D) -------------------------------------------------------
+; (CGB only) Double-speed mode control
+def rSPD equ $FF4D
+
+def SPDB_DBLSPEED equ 7 ; current clock speed [ro]
+def SPDB_PREPARE equ 0 ; 1 = next `stop` instruction will switch clock speeds [r/w]
+ def SPDF_DBLSPEED equ 1 << SPDB_DBLSPEED
+ def SPDF_PREPARE equ 1 << SPDB_PREPARE
+
+; -- $FF4E is unused ----------------------------------------------------------
+
+; -- VBK ($FF4F) --------------------------------------------------------------
+; (CGB only) VRAM bank number (0 or 1)
+def rVBK equ $FF4F
+
+def VBK_BANK equ %0000000_1 ; mapped VRAM bank [r/w]
+
+; -- BANK ($FF50) -------------------------------------------------------------
+; (boot ROM only) Boot ROM mapping control
+def rBANK equ $FF50
+
+def BANKB_ON equ 0 ; whether the boot ROM is mapped [wo]
+ def BANKF_ON equ 0 << BANKB_ON
+ def BANKF_OFF equ 1 << BANKB_ON
+
+; -- VDMA_SRC_HIGH / HDMA1 ($FF51) --------------------------------------------
+; (CGB only) VRAM DMA source address (high 8 bits) [wo]
+def rVDMA_SRC_HIGH equ $FF51
+
+; -- VDMA_SRC_LO / HDMA2 ($FF52) ----------------------------------------------
+; (CGB only) VRAM DMA source address (low 8 bits) [wo]
+def rVDMA_SRC_LOW equ $FF52
+
+; -- VDMA_DEST_HIGH / HDMA3 ($FF53) -------------------------------------------
+; (CGB only) VRAM DMA destination address (high 8 bits) [wo]
+def rVDMA_DEST_HIGH equ $FF53
+
+; -- VDMA_DEST_LOW / HDMA3 ($FF54) --------------------------------------------
+; (CGB only) VRAM DMA destination address (low 8 bits) [wo]
+def rVDMA_DEST_LOW equ $FF54
+
+; -- VDMA_LEN / HDMA5 ($FF55) -------------------------------------------------
+; (CGB only) VRAM DMA length, mode, and start
+def rVDMA_LEN equ $FF55
+
+def VDMA_LENB_MODE equ 7 ; on write: VRAM DMA mode [wo]
+ def VDMA_LENF_MODE equ 1 << VDMA_LENB_MODE
+ def VDMA_LENF_MODE_GP equ 0 << VDMA_LENB_MODE ; GDMA (general-purpose)
+ def VDMA_LENF_MODE_HBL equ 1 << VDMA_LENB_MODE ; HDMA (HBlank)
+
+def VDMA_LENB_BUSY equ 7 ; on read: is a VRAM DMA active?
+ def VDMA_LENF_BUSY equ 1 << VDMA_LENB_BUSY
+ def VDMA_LENF_NO equ 0 << VDMA_LENB_BUSY
+ def VDMA_LENF_YES equ 1 << VDMA_LENB_BUSY
+
+def VDMA_LENB_SIZE equ %0_1111111 ; how many 16-byte blocks (minus 1) to transfer [r/w]
+
+; -- RP ($FF56) ---------------------------------------------------------------
+; (CGB only) Infrared communications port
+def rRP equ $FF56
+
+def RPF_READ equ %11_000000 ; whether the IR read is enabled [r/w]
+ def RPF_DISREAD equ %00_000000
+ def RPF_ENREAD equ %11_000000
+
+def RPB_DATAIN equ 1 ; 0 = IR light is being received [ro]
+def RPB_LED_ON equ 0 ; 1 = IR light is being sent [r/w]
+ def RPF_DATAIN equ 1 << RPB_DATAIN
+ def RPF_LED_ON equ 1 << RPB_LED_ON
+ def RPF_WRITE_LO equ 0 << RPB_LED_ON
+ def RPF_WRITE_HI equ 1 << RPB_LED_ON
+
+; -- $FF57-$FF67 are unused ---------------------------------------------------
+
+; -- BGPI / BCPS ($FF68) ------------------------------------------------------
+; (CGB only) Background palette I/O index
+def rBGPI equ $FF68
+
+def BGPIB_AUTOINC equ 7 ; whether the index field is incremented after each write to BCPD [r/w]
+ def BGPIF_AUTOINC equ 1 << BGPIB_AUTOINC
+
+def BGPIF_INDEX equ %00_111111 ; the index within Palette RAM accessed via BCPD [r/w]
+
+; -- BGPD / BCPD ($FF69) ------------------------------------------------------
+; (CGB only) Background palette I/O access [r/w]
+def rBGPD equ $FF69
+
+; -- OBPI / OCPS ($FF6A) ------------------------------------------------------
+; (CGB only) OBJ palette I/O index
+def rOBPI equ $FF6A
+
+def OBPIB_AUTOINC equ 7 ; whether the index field is incremented after each write to OBPD [r/w]
+ def OBPIF_AUTOINC equ 1 << OBPIB_AUTOINC
+
+def OBPIF_INDEX equ %00_111111 ; the index within Palette RAM accessed via OBPD [r/w]
+
+; -- OBPD / OCPD ($FF6B) ------------------------------------------------------
+; (CGB only) OBJ palette I/O access [r/w]
+def rOBPD equ $FF6B
+
+; -- OPRI ($FF6C) -------------------------------------------------------------
+; (CGB boot ROM only) OBJ draw priority mode
+def rOPRI equ $FF6C
+
+def OPRIB_PRI equ 0 ; which drawing priority is used for OBJs [r/w]
+ def OPRIF_PRI equ 1 << OPRIB_PRI
+ def OPRI_OAM equ 0 << OPRIB_PRI ; CGB mode default: earliest OBJ in OAM wins
+ def OPRI_COORD equ 1 << OPRIB_PRI ; DMG mode default: leftmost OBJ wins
+
+; -- $FF6D-$FF6F are unused ---------------------------------------------------
+
+; -- WBK / SVBK / SMBK ($FF70) ------------------------------------------------
+; (CGB only) WRAM bank number
+def rWBK equ $FF70
+
+def WBKF_BANK equ %00000_111 ; mapped WRAM bank (0-7) [r/w]
+
+; -- $FF71-$FF75 are unused ---------------------------------------------------
+
+; -- PCM12 ($FF76) ------------------------------------------------------------
+; Audio channels 1 and 2 output
+def rPCM12 equ $FF76
+
+def PCM12F_CH2 equ %1111_0000 ; audio channel 2 output [ro]
+def PCM12F_CH1 equ %0000_1111 ; audio channel 1 output [ro]
+
+; -- PCM34 ($FF77) ------------------------------------------------------------
+; Audio channels 3 and 4 output
+def rPCM34 equ $FF77
+
+def PCM34F_CH4 equ %1111_0000 ; audio channel 4 output [ro]
+def PCM34F_CH3 equ %0000_1111 ; audio channel 3 output [ro]
+
+; -- $FF78-$FF7F are unused ---------------------------------------------------
+
+; -- IE ($FFFF) ---------------------------------------------------------------
+; Interrupt enable
+def rIE equ $FFFF
+
+def IEB_JOYPAD equ 4 ; 1 = joypad interrupt is enabled [r/w]
+def IEB_SERIAL equ 3 ; 1 = serial interrupt is enabled [r/w]
+def IEB_TIMER equ 2 ; 1 = timer interrupt is enabled [r/w]
+def IEB_STAT equ 1 ; 1 = STAT interrupt is enabled [r/w]
+def IEB_VBLANK equ 0 ; 1 = VBlank interrupt is enabled [r/w]
+ def IEF_JOYPAD equ 1 << IEB_JOYPAD
+ def IEF_SERIAL equ 1 << IEB_SERIAL
+ def IEF_TIMER equ 1 << IEB_TIMER
+ def IEF_STAT equ 1 << IEB_STAT
+ def IEF_VBLANK equ 1 << IEB_VBLANK
+
+
+;******************************************************************************
+; Cartridge registers (MBC)
+;******************************************************************************
+
+; Note that these "registers" are each actually accessible at an entire address range;
+; however, one address for each of these ranges is considered the "canonical" one, and
+; these addresses are what's provided here.
+
+; -- RAMG ($0000-$1FFF) -------------------------------------------------------
+; Whether SRAM can be accessed [wo]
+def rRAMG equ $0000
+
+; Common values
+def CART_SRAM_DISABLE equ $00
+def CART_SRAM_ENABLE equ $0A ; some MBCs accept any value whose low nybble is $A
+
+; -- ROMB0 ($2000-$3FFF) ------------------------------------------------------
+; ROM bank number (low 8 bits when applicable) [wo]
+def rROMB0 equ $2000
+
+; -- ROMB1 ($3000-$3FFF) ------------------------------------------------------
+; (MBC5 only) ROM bank number high bit (bit 8) [wo]
+def rROMB1 equ $3000
+
+; -- RAMB ($4000-$5FFF) -------------------------------------------------------
+ ; SRAM bank number [wo]
+def rRAMB equ $4000
+
+; (MBC3-only) Special RAM bank numbers that actually map values into RTCREG
+def RTC_S equ $08 ; seconds counter (0-59)
+def RTC_M equ $09 ; minutes counter (0-59)
+def RTC_H equ $0A ; hours counter (0-23)
+def RTC_DL equ $0B ; days counter, low byte (0-255)
+def RTC_DH equ $0C ; days counter, high bit and other flags
+ def RTC_DHB_CARRY equ 7 ; 1 = days counter overflowed [wo]
+ def RTC_DHB_HALT equ 6 ; 0 = run timer, 1 = stop timer [wo]
+ def RTC_DHB_HIGH equ 0 ; days counter, high bit (bit 8) [wo]
+ def RTC_DHF_CARRY equ 1 << RTC_DHB_CARRY
+ def RTC_DHF_HALT equ 1 << RTC_DHB_HALT
+ def RTC_DHF_HIGH equ 1 << RTC_DHB_HIGH
+
+def CARTB_RUMBLE_ON equ 3 ; (MBC5 and MBC7 only) enable the rumble motor (if any)
+ def CARTF_RUMBLE_ON equ 1 << CARTB_RUMBLE_ON
+ def CART_RUMBLE_OFF equ 0 << CARTB_RUMBLE_ON
+ def CART_RUMBLE_ON equ 1 << CARTB_RUMBLE_ON
+
+; -- RTCLATCH ($6000-$7FFF) ---------------------------------------------------
+; (MBC3 only) RTC latch clock data [wo]
+def rRTCLATCH equ $6000
+
+; Write $00 then $01 to latch the current time into RTCREG
+def RTCLATCH_START equ $00
+def RTCLATCH_FINISH equ $01
+
+; -- RTCREG ($A000-$BFFF) ---------------------------------------------------
+; (MBC3 only) RTC register [r/w]
+def rRTCREG equ $A000
+
+
+;******************************************************************************
+; Screen-related constants
+;******************************************************************************
+
+def SCRN_X equ 160 ; width of screen in pixels
+def SCRN_Y equ 144 ; height of screen in pixels
+def SCRN_X_B equ 20 ; width of screen in bytes
+def SCRN_Y_B equ 18 ; height of screen in bytes
+
+def SCRN_VX equ 256 ; width of tilemap in pixels
+def SCRN_VY equ 256 ; height of tilemap in pixels
+def SCRN_VX_B equ 32 ; width of tilemap in bytes
+def SCRN_VY_B equ 32 ; height of tilemap in bytes
+
+def TILE_X equ 8 ; width of tile in pixels
+def TILE_Y equ 8 ; height of tile in pixels
+def TILE_B equ 16 ; size of tile in bytes (2 bits/pixel)
+
+def COLOR_B equ 2 ; size of color in bytes (little-endian BGR555)
+ def COLORF_GREEN_LOW equ %111_00000 ; for the low byte
+ def COLORF_RED equ %000_11111 ; for the low byte
+ def COLORF_BLUE equ %0_11111_00 ; for the high byte
+ def COLORF_GREEN_HIGH equ %000000_11 ; for the high byte
+def PAL_COLORS equ 4 ; colors per palette
+def PAL_B equ COLOR_B * PAL_COLORS ; size of palette in bytes
+
+; Tilemaps the BG or Window can read from (controlled by LCDC)
+def _SCRN0 equ $9800 ; $9800-$9BFF
+def _SCRN1 equ $9C00 ; $9C00-$9FFF
+
+
+;******************************************************************************
+; OBJ-related constants
+;******************************************************************************
+
+; OAM attribute field offsets
+rsreset
+def OAMA_Y rb ; 0
+ def OAM_Y_OFS equ 16 ; subtract 16 from what's written to OAM to get the real Y position
+def OAMA_X rb ; 1
+ def OAM_X_OFS equ 8 ; subtract 8 from what's written to OAM to get the real X position
+def OAMA_TILEID rb ; 2
+def OAMA_FLAGS rb ; 3
+ def OAMB_PRI equ 7 ; whether the OBJ is drawn above BG colors 1-3
+ def OAMB_YFLIP equ 6 ; whether the whole OBJ is flipped vertically
+ def OAMB_XFLIP equ 5 ; whether the whole OBJ is flipped horizontally
+ def OAMB_PAL1 equ 4 ; (DMG only) which of the two palettes the OBJ uses
+ def OAMB_BANK1 equ 3 ; (CGB only) which VRAM bank the OBJ takes its tile(s) from
+ def OAMF_PALMASK equ %00000_111 ; (CGB only) which palette the OBJ uses
+ def OAMF_PRI equ 1 << OAMB_PRI
+ def OAMF_YFLIP equ 1 << OAMB_YFLIP
+ def OAMF_XFLIP equ 1 << OAMB_XFLIP
+ def OAMF_PAL0 equ 0 << OAMB_PAL1
+ def OAMF_PAL1 equ 1 << OAMB_PAL1
+ def OAMF_BANK0 equ 0 << OAMB_BANK1
+ def OAMF_BANK1 equ 1 << OAMB_BANK1
+def OBJ_B rb 0 ; size of OBJ in bytes = 4
+
+def OAM_COUNT equ 40 ; how many OBJs there are room for in OAM
+def OAM_B equ OBJ_B * OAM_COUNT
+
+
+;******************************************************************************
+; Interrupt vector addresses
+;******************************************************************************
+
+def INT_HANDLER_VBLANK equ $0040 ; VBlank interrupt handler address
+def INT_HANDLER_STAT equ $0048 ; STAT interrupt handler address
+def INT_HANDLER_TIMER equ $0050 ; timer interrupt handler address
+def INT_HANDLER_SERIAL equ $0058 ; serial interrupt handler address
+def INT_HANDLER_JOYPAD equ $0060 ; joypad interrupt handler address
+
+
+;******************************************************************************
+; Boot-up register values
+;******************************************************************************
+
+; Register A = CPU type
+def BOOTUP_A_DMG equ $01
+def BOOTUP_A_CGB equ $11 ; CGB or AGB
+def BOOTUP_A_MGB equ $FF
+ def BOOTUP_A_SGB equ BOOTUP_A_DMG
+ def BOOTUP_A_SGB2 equ BOOTUP_A_MGB
+
+; Register B = CPU qualifier (if A is BOOTUP_A_CGB)
+def BOOTUPB_B_AGB equ 0
+ def BOOTUP_B_CGB equ 0 << BOOTUPB_B_AGB
+ def BOOTUP_B_AGB equ 1 << BOOTUPB_B_AGB
+
+
+;******************************************************************************
+; Aliases
+;******************************************************************************
+
+; Prefer the standard names to these aliases, which may be official but are
+; less directly meaningful or human-readable.
+
+def rP1 equ rJOYP
+ def P1F_GET_BTN equ JOYP_GET_BTN
+ def P1F_GET_DPAD equ JOYP_GET_DPAD
+ def P1F_GET_NONE equ JOYP_GET_NONE
+ def P1F_5 equ JOYP_GET_DPAD
+ def P1F_4 equ JOYP_GET_BTN
+ def P1F_3 equ JOYPF_DOWN
+ def P1F_2 equ JOYPF_UP
+ def P1F_1 equ JOYPF_LEFT
+ def P1F_0 equ JOYPF_RIGHT
+
+def rNR10 equ rAUD1SWEEP
+def rNR11 equ rAUD1LEN
+def rNR12 equ rAUD1ENV
+def rNR13 equ rAUD1LOW
+def rNR14 equ rAUD1HIGH
+def rNR21 equ rAUD2LEN
+def rNR22 equ rAUD2ENV
+def rNR23 equ rAUD2LOW
+def rNR24 equ rAUD2HIGH
+def rNR30 equ rAUD3ENA
+def rNR31 equ rAUD3LEN
+def rNR32 equ rAUD3LEVEL
+def rNR33 equ rAUD3LOW
+def rNR34 equ rAUD3HIGH
+def rNR41 equ rAUD4LEN
+def rNR42 equ rAUD4ENV
+def rNR43 equ rAUD4POLY
+def rNR44 equ rAUD4GO
+def rNR50 equ rAUDVOL
+def rNR51 equ rAUDTERM
+def rNR52 equ rAUDENA
+
+def rKEY1 equ rSPD
+ def KEY1F_DBLSPEED equ SPDF_DBLSPEED
+ def KEY1F_PREPARE equ SPDF_PREPARE
+
+def rHDMA1 equ rVDMA_SRC_HIGH
+def rHDMA2 equ rVDMA_SRC_LOW
+def rHDMA3 equ rVDMA_DEST_HIGH
+def rHDMA4 equ rVDMA_DEST_LOW
+def rHDMA5 equ rVDMA_LEN
+ def HDMA5B_MODE equ VDMA_LENB_MODE
+ def HDMA5F_MODE_GP equ VDMA_LENF_MODE_GP
+ def HDMA5F_MODE_HBL equ VDMA_LENF_MODE_HBL
+ def HDMA5F_BUSY equ VDMA_LENF_BUSY
+
+def rBCPS equ rBGPI
+ def BCPSB_AUTOINC equ BGPIB_AUTOINC
+ def BCPSF_AUTOINC equ BGPIF_AUTOINC
+def rBCPD equ rBGPD
+
+def rOCPS equ rOBPI
+ def OCPSB_AUTOINC equ OBPIB_AUTOINC
+ def OCPSF_AUTOINC equ OBPIF_AUTOINC
+def rOCPD equ rOBPD
+
+def rSVBK equ rWBK
+def rSMBK equ rWBK
+
+
+;******************************************************************************
+; (deprecated) Memory regions
+;******************************************************************************
+
+; These values are deprecated; please use RGBASM and RGBLINK features instead.
+; Note that the value of `STARTOF()` is determined at link time.
+
+def _ROM equ $0000 ; $0000-$3FFF / $0000-$7FFF (prefer `STARTOF(ROM0)`)
+def _ROMBANK equ $4000 ; $4000-$7FFF (prefer `STARTOF(ROMX)`)
+def _VRAM equ $8000 ; $8000-$9FFF (prefer `STARTOF(VRAM)`)
+def _SRAM equ $A000 ; $A000-$BFFF (prefer `STARTOF(SRAM)`)
+def _RAM equ $C000 ; $C000-$CFFF / $C000-$DFFF (prefer `STARTOF(WRAM0)`)
+def _RAMBANK equ $D000 ; $D000-$DFFF (prefer `STARTOF(WRAMX)`)
+def _OAMRAM equ $FE00 ; $FE00-$FE9F (prefer `STARTOF(OAM)`)
+def _IO equ $FF00 ; $FF00-$FF7F, $FFFF (prefer `ldh [c]` to `ld [_IO+c]`)
+def _HRAM equ $FF80 ; $FF80-$FFFE (prefer `STARTOF(HRAM)`)
+
+def _VRAM8000 equ _VRAM
+def _VRAM8800 equ _VRAM + $800
+def _VRAM9000 equ _VRAM + $1000
+
+
+;******************************************************************************
+; (deprecated) Cartridge header
+;******************************************************************************
+
+; These values are deprecated; please use RGBFIX instead.
+; Zero-filled space can be reserved for fixable header values like this:
+;
+; SECTION "Cartridge header", ROM0[$0100]
+; nop :: jp $0150 ; Entry point ($0100-$0104)
+; ds $150 - @, $00 ; Header ($0104-$014FF) filled with $00s for RGBFIX to populate
+
+; -- Nintendo logo ($0104-$0133) ----------------------------------------------
+; Prefer `rgbfix -f/--fix-spec l` for the official logo, or `rgbfix -L ` for a custom one
+MACRO NINTENDO_LOGO
+ db $CE,$ED,$66,$66,$CC,$0D,$00,$0B,$03,$73,$00,$83,$00,$0C,$00,$0D
+ db $00,$08,$11,$1F,$88,$89,$00,$0E,$DC,$CC,$6E,$E6,$DD,$DD,$D9,$99
+ db $BB,$BB,$67,$63,$6E,$0E,$EC,$CC,$DD,$DC,$99,$9F,$BB,$B9,$33,$3E
+ENDM
+
+; -- CGB compatibility code ($0143) -------------------------------------------
+def CART_COMPATIBLE_DMG equ $00 ; default value if header is zero-filled
+def CART_COMPATIBLE_DMG_GBC equ $80 ; prefer `rgbfix -c/--color-compatible`
+def CART_COMPATIBLE_GBC equ $C0 ; prefer `rgbfix -C/--color-only`
+
+; -- SGB flag ($0146) ---------------------------------------------------------
+def CART_INDICATOR_GB equ $00 ; default value if header is zero-filled
+def CART_INDICATOR_SGB equ $03 ; prefer `rgblink -s/--sgb-compatible`
+
+; -- Cartridge type ($0147) ---------------------------------------------------
+; Prefer `rgblink -m/--mbc_type `
+def CART_ROM equ $00
+def CART_ROM_MBC1 equ $01
+def CART_ROM_MBC1_RAM equ $02
+def CART_ROM_MBC1_RAM_BAT equ $03
+def CART_ROM_MBC2 equ $05
+def CART_ROM_MBC2_BAT equ $06
+def CART_ROM_RAM equ $08
+def CART_ROM_RAM_BAT equ $09
+def CART_ROM_MMM01 equ $0B
+def CART_ROM_MMM01_RAM equ $0C
+def CART_ROM_MMM01_RAM_BAT equ $0D
+def CART_ROM_MBC3_BAT_RTC equ $0F
+def CART_ROM_MBC3_RAM_BAT_RTC equ $10
+def CART_ROM_MBC3 equ $11
+def CART_ROM_MBC3_RAM equ $12
+def CART_ROM_MBC3_RAM_BAT equ $13
+def CART_ROM_MBC5 equ $19
+def CART_ROM_MBC5_RAM equ $1A
+def CART_ROM_MBC5_RAM_BAT equ $1B
+def CART_ROM_MBC5_RUMBLE equ $1C
+def CART_ROM_MBC5_RAM_RUMBLE equ $1D
+def CART_ROM_MBC5_RAM_BAT_RUMBLE equ $1E
+def CART_ROM_MBC7_RAM_BAT_GYRO equ $22
+def CART_ROM_POCKET_CAMERA equ $FC
+def CART_ROM_BANDAI_TAMA5 equ $FD
+def CART_ROM_HUDSON_HUC3 equ $FE
+def CART_ROM_HUDSON_HUC1 equ $FF
+
+; -- ROM size ($0148) ---------------------------------------------------------
+; Prefer `rgbfix -p/--pad_value `, which pads to the smallest valid size
+def CART_ROM_32KB equ $00 ; 2 banks
+def CART_ROM_64KB equ $01 ; 4 banks
+def CART_ROM_128KB equ $02 ; 8 banks
+def CART_ROM_256KB equ $03 ; 16 banks
+def CART_ROM_512KB equ $04 ; 32 banks
+def CART_ROM_1024KB equ $05 ; 64 banks
+def CART_ROM_2048KB equ $06 ; 128 banks
+def CART_ROM_4096KB equ $07 ; 256 banks
+def CART_ROM_8192KB equ $08 ; 512 banks
+def CART_ROM_1152KB equ $52 ; 72 banks
+def CART_ROM_1280KB equ $53 ; 80 banks
+def CART_ROM_1536KB equ $54 ; 96 banks
+
+; -- SRAM size ($0149) --------------------------------------------------------
+; Prefer `rgbfix -r/--ram_size `
+def CART_SRAM_NONE equ 0 ; none
+def CART_SRAM_2KB equ 1 ; 1 incomplete bank (homebrew only)
+def CART_SRAM_8KB equ 2 ; 1 bank
+def CART_SRAM_32KB equ 3 ; 4 banks
+def CART_SRAM_128KB equ 4 ; 16 banks
+
+; -- Destination code ($014A) -------------------------------------------------
+def CART_DEST_JAPANESE equ $00 ; default value if header is zero-filled
+def CART_DEST_NON_JAPANESE equ $01 ; prefer `rgbfix -j/--non-japanese`
+
+
+;******************************************************************************
+; Deprecated constants
+;******************************************************************************
+
+; These values are deprecated; please avoid using them.
+
+def LCDCB_BG8000 equ LCDCB_BLKS
+ def LCDCF_BG8800 equ LCDCF_BLK21
+ def LCDCF_BG8000 equ LCDCF_BLK01
+
+def IEB_HILO equ IEB_JOYPAD
+ def IEF_HILO equ IEF_JOYPAD
+def IEF_LCDC equ IEF_STAT
+
+def sizeof_OAM_ATTRS equ OBJ_B
+
+endc ; HARDWARE_INC
diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm
new file mode 100644
index 00000000..66dc9953
--- /dev/null
+++ b/unbricked/serial-link/main.asm
@@ -0,0 +1,1102 @@
+INCLUDE "hardware.inc"
+
+DEF BRICK_LEFT EQU $05
+DEF BRICK_RIGHT EQU $06
+DEF BLANK_TILE EQU $08
+DEF DIGIT_OFFSET EQU $1A
+
+DEF SCORE_TENS EQU $9870
+DEF SCORE_ONES EQU $9871
+; ANCHOR: serial-link-defs
+; Icon tiles start after the digits
+RSSET DIGIT_OFFSET + 10
+DEF ICON_EXTCLK RB 1
+DEF ICON_EXTCLK_ACT RB 1
+DEF ICON_INTCLK RB 1
+DEF ICON_INTCLK_ACT RB 1
+DEF ICON_NO RB 1
+DEF ICON_OK RB 1
+; Tilemap position of the remote player's score
+DEF SCORE_REMOTE EQU $98B0
+; Tilemap position of Link/Sio status icons
+DEF STATUS_BAR EQU $9813
+
+DEF LINK_ENABLE EQU $80
+DEF LINK_CONNECTED EQU $40
+
+DEF MSG_SHAKE EQU $80
+DEF MSG_GAME EQU $81
+; ANCHOR_END: serial-link-defs
+
+
+; ANCHOR: serial-interrupt-vector
+SECTION "Serial Interrupt", ROM0[$58]
+SerialInterrupt:
+ push af
+ push hl
+ call SioPortEnd
+ pop hl
+ pop af
+ reti
+; ANCHOR_END: serial-interrupt-vector
+
+
+SECTION "Link Impl", ROM0
+; ANCHOR: link-impl-start
+LinkStart:
+ call SioAbort
+ ld a, SIO_IDLE
+ ld [wSioState], a
+
+ ld a, LINK_ENABLE
+ ld [wLink], a
+ ld a, 0
+ ld [wLinkPacketCount], a
+ ld [wShakeFailed], a
+ ld a, [wCurKeys]
+ ld b, a
+ ldh a, [rDIV]
+ or a, b
+ and PADF_START
+ ld a, 0
+ jr z, :+
+ ld a, SCF_SOURCE
+:
+ ldh [rSC], a
+ jp LinkShakeTx
+
+
+LinkUpdate:
+ ; Only update if enabled
+ ld a, [wLink]
+ and a, LINK_ENABLE
+ ret z
+
+ ; Update Sio
+ call SioTick
+ ld a, [wSioState]
+ cp a, SIO_ACTIVE
+ ret z ; Nothing to do while a transfer is active
+
+ ld a, [wLink]
+ and a, LINK_CONNECTED
+ jr nz, .conn_up
+
+ ; Attempt to connect (handshake)
+.conn_shake:
+ ld a, [wShakeFailed]
+ and a, a
+ jr z, :+
+ dec a
+ ld [wShakeFailed], a
+ jr z, LinkStart
+ ret
+:
+ ld a, [wSioState]
+ cp a, SIO_DONE
+ jr z, LinkShakeRx
+ cp a, SIO_IDLE
+ jr z, LinkShakeTx
+ cp a, SIO_FAILED
+ jr z, LinkShakeFail
+ ret
+.conn_up:
+ ld a, [wSioState]
+ cp a, SIO_DONE
+ jr z, LinkGameRx
+ cp a, SIO_IDLE
+ jr z, LinkGameTx
+ cp a, SIO_FAILED
+ jp z, LinkStop
+ ret
+
+
+; @return F.Z: if received packet passes checks
+; @return HL: pointer to first byte of received packet data
+LinkPacketRx:
+ ld a, SIO_IDLE
+ ld [wSioState], a
+
+ call SioPacketRxCheck
+ ret nz
+
+ ld a, [wLinkPacketCount]
+ dec a
+ ld b, a
+ ld a, [hl+]
+ cp a, b
+ ret
+
+
+LinkShakeFail:
+ ; Delay for longer if we were INTCLK
+ ld b, 1
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ jr z, :+
+ ld b, 3
+:
+ ld a, b
+ ld [wShakeFailed], a
+ ret
+
+
+LinkShakeTx:
+ call SioPacketTxPrepare
+
+ ld a, [wLinkPacketCount]
+ ld [hl+], a
+ inc a
+ ld [wLinkPacketCount], a
+
+ ld a, MSG_SHAKE
+ ld [hl+], a
+
+ call SioPacketTxFinalise
+ ret
+
+
+LinkShakeRx:
+ call LinkPacketRx
+ jr nz, LinkShakeFail
+
+ ld a, [hl+]
+ cp a, MSG_SHAKE
+ jr nz, LinkShakeFail
+
+ ld a, [wLinkPacketCount]
+ cp a, 3
+ ret nz
+.complete
+ ld a, [wLink]
+ or a, LINK_CONNECTED
+ ld [wLink], a
+ ret
+
+
+LinkGameTx:
+ call SioPacketTxPrepare
+
+ ld a, [wLinkPacketCount]
+ ld [hl+], a
+ inc a
+ ld [wLinkPacketCount], a
+
+ ld a, MSG_GAME
+ ld [hl+], a
+
+ ld a, [wScore]
+ ld [hl+], a
+
+ call SioPacketTxFinalise
+ ret
+
+
+LinkGameRx:
+ call LinkPacketRx
+ jr nz, LinkStop
+
+ ld a, [hl+]
+ cp a, MSG_GAME
+ jr nz, LinkStop
+
+ ld a, [hl+]
+ ld [wRemoteScore], a
+ ret
+
+
+LinkStop:
+ ld a, [wLink]
+ and a, $FF ^ LINK_ENABLE
+ ld [wLink], a
+ call SioAbort
+ ret
+
+
+SECTION "Header", ROM0[$100]
+Header:
+ jp EntryPoint
+
+ ds $150 - @, 0 ; Make room for the header
+
+EntryPoint:
+ ; Do not turn the LCD off outside of VBlank
+.wait_vblank
+ ld a, [rLY]
+ cp 144
+ jp c, .wait_vblank
+
+ ; Turn the LCD off
+ ld a, 0
+ ld [rLCDC], a
+
+ ; Copy the tile data
+ ld de, Tiles
+ ld hl, $9000
+ ld bc, TilesEnd - Tiles
+ call Memcopy
+
+ ; Copy the tilemap
+ ld de, Tilemap
+ ld hl, $9800
+ ld bc, TilemapEnd - Tilemap
+ call Memcopy
+
+ ; Copy the paddle tile
+ ld de, Paddle
+ ld hl, $8000
+ ld bc, PaddleEnd - Paddle
+ call Memcopy
+
+ ; Copy the ball tile
+ ld de, Ball
+ ld hl, $8010
+ ld bc, BallEnd - Ball
+ call Memcopy
+
+ xor a, a
+ ld b, 160
+ ld hl, _OAMRAM
+.clear_oam
+ ld [hli], a
+ dec b
+ jp nz, .clear_oam
+
+ ; Initialize the paddle sprite in OAM
+ ld hl, _OAMRAM
+ ld a, 128 + 16
+ ld [hli], a
+ ld a, 16 + 8
+ ld [hli], a
+ ld a, 0
+ ld [hli], a
+ ld [hli], a
+ ; Now initialize the ball sprite
+ ld a, 100 + 16
+ ld [hli], a
+ ld a, 32 + 8
+ ld [hli], a
+ ld a, 1
+ ld [hli], a
+ ld a, 0
+ ld [hli], a
+
+ ; The ball starts out going up and to the right
+ ld a, 1
+ ld [wBallMomentumX], a
+ ld a, -1
+ ld [wBallMomentumY], a
+
+ ; Turn the LCD on
+ ld a, LCDCF_ON | LCDCF_BGON | LCDCF_OBJON
+ ld [rLCDC], a
+
+ ; During the first (blank) frame, initialize display registers
+ ld a, %11100100
+ ld [rBGP], a
+ ld a, %11100100
+ ld [rOBP0], a
+ ; Initialize global variables
+ ld a, 0
+ ld [wFrameCounter], a
+ ld [wCurKeys], a
+ ld [wNewKeys], a
+ ld [wScore], a
+
+; ANCHOR: link-init
+ ld a, 0
+ ld [wShakeFailed], a
+ ld [wLinkPacketCount], a
+ ld [wRemoteScore], a
+ ld a, LINK_ENABLE
+ ld [wLink], a
+ call SioInit
+ ldh a, [rIE]
+ or a, IEF_SERIAL
+ ldh [rIE], a
+ ei
+; ANCHOR_END: link-init
+
+
+; ANCHOR: link-update
+Main:
+ ei ; enable interrupts to process transfers
+ call LinkUpdate
+
+.wait_vblank_end
+ ldh a, [rLY]
+ cp 144
+ jr nc, .wait_vblank_end
+
+.wait_vblank_start
+ ldh a, [rLY]
+ cp 144
+ jr c, .wait_vblank_start
+
+ di ; disable interrupts for OAM/VRAM access
+
+ ld a, [wRemoteScore]
+ ld b, a
+ ld hl, SCORE_REMOTE
+ call PrintBCD
+
+ ld hl, STATUS_BAR
+ ; Serial port status
+ ldh a, [rSC]
+ and a, SCF_START | SCF_SOURCE
+ rlca
+ add a, ICON_EXTCLK
+ ld [hl-], a
+ ; Link
+ ld b, ICON_NO
+ ld a, [wLink]
+ cp a, LINK_ENABLE | LINK_CONNECTED
+ jr nz, :+
+ inc b ; ICON_OK
+:
+ ld a, b
+ ld [hl-], a
+
+ ; Skip ball update if not connected
+ ld a, [wLink]
+ cp a, LINK_ENABLE | LINK_CONNECTED
+ jp nz, PaddleBounceDone
+; ANCHOR_END: link-update
+
+ ; Add the ball's momentum to its position in OAM.
+ ld a, [wBallMomentumX]
+ ld b, a
+ ld a, [_OAMRAM + 5]
+ add a, b
+ ld [_OAMRAM + 5], a
+
+ ld a, [wBallMomentumY]
+ ld b, a
+ ld a, [_OAMRAM + 4]
+ add a, b
+ ld [_OAMRAM + 4], a
+
+BounceOnTop:
+ ; Remember to offset the OAM position!
+ ; (8, 16) in OAM coordinates is (0, 0) on the screen.
+ ld a, [_OAMRAM + 4]
+ sub a, 16 + 1
+ ld c, a
+ ld a, [_OAMRAM + 5]
+ sub a, 8
+ ld b, a
+ call GetTileByPixel ; Returns tile address in hl
+ ld a, [hl]
+ call IsWallTile
+ jp nz, BounceOnRight
+ call CheckAndHandleBrick
+ ld a, 1
+ ld [wBallMomentumY], a
+
+BounceOnRight:
+ ld a, [_OAMRAM + 4]
+ sub a, 16
+ ld c, a
+ ld a, [_OAMRAM + 5]
+ sub a, 8 - 1
+ ld b, a
+ call GetTileByPixel
+ ld a, [hl]
+ call IsWallTile
+ jp nz, BounceOnLeft
+ call CheckAndHandleBrick
+ ld a, -1
+ ld [wBallMomentumX], a
+
+BounceOnLeft:
+ ld a, [_OAMRAM + 4]
+ sub a, 16
+ ld c, a
+ ld a, [_OAMRAM + 5]
+ sub a, 8 + 1
+ ld b, a
+ call GetTileByPixel
+ ld a, [hl]
+ call IsWallTile
+ jp nz, BounceOnBottom
+ call CheckAndHandleBrick
+ ld a, 1
+ ld [wBallMomentumX], a
+
+BounceOnBottom:
+ ld a, [_OAMRAM + 4]
+ sub a, 16 - 1
+ ld c, a
+ ld a, [_OAMRAM + 5]
+ sub a, 8
+ ld b, a
+ call GetTileByPixel
+ ld a, [hl]
+ call IsWallTile
+ jp nz, BounceDone
+ call CheckAndHandleBrick
+ ld a, -1
+ ld [wBallMomentumY], a
+BounceDone:
+
+ ; First, check if the ball is low enough to bounce off the paddle.
+ ld a, [_OAMRAM]
+ ld b, a
+ ld a, [_OAMRAM + 4]
+ cp a, b
+ jp nz, PaddleBounceDone
+ ; Now let's compare the X positions of the objects to see if they're touching.
+ ld a, [_OAMRAM + 1]
+ ld b, a
+ ld a, [_OAMRAM + 5]
+ add a, 16
+ cp a, b
+ jp c, PaddleBounceDone
+ sub a, 16 + 8
+ cp a, b
+ jp nc, PaddleBounceDone
+
+ ld a, -1
+ ld [wBallMomentumY], a
+
+PaddleBounceDone:
+
+ ; Check the current keys every frame and move left or right.
+ call Input
+
+ ; First, check if the left button is pressed.
+CheckLeft:
+ ld a, [wCurKeys]
+ and a, PADF_LEFT
+ jp z, CheckRight
+Left:
+ ; Move the paddle one pixel to the left.
+ ld a, [_OAMRAM + 1]
+ dec a
+ ; If we've already hit the edge of the playfield, don't move.
+ cp a, 15
+ jp z, Main
+ ld [_OAMRAM + 1], a
+ jp Main
+
+; Then check the right button.
+CheckRight:
+ ld a, [wCurKeys]
+ and a, PADF_RIGHT
+ jp z, Main
+Right:
+ ; Move the paddle one pixel to the right.
+ ld a, [_OAMRAM + 1]
+ inc a
+ ; If we've already hit the edge of the playfield, don't move.
+ cp a, 105
+ jp z, Main
+ ld [_OAMRAM + 1], a
+ jp Main
+
+; Convert a pixel position to a tilemap address
+; hl = $9800 + X + Y * 32
+; @param b: X
+; @param c: Y
+; @return hl: tile address
+GetTileByPixel:
+ ; First, we need to divide by 8 to convert a pixel position to a tile position.
+ ; After this we want to multiply the Y position by 32.
+ ; These operations effectively cancel out so we only need to mask the Y value.
+ ld a, c
+ and a, %11111000
+ ld l, a
+ ld h, 0
+ ; Now we have the position * 8 in hl
+ add hl, hl ; position * 16
+ add hl, hl ; position * 32
+ ; Just add the X position and offset to the tilemap, and we're done.
+ ld a, b
+ srl a ; a / 2
+ srl a ; a / 4
+ srl a ; a / 8
+ add a, l
+ ld l, a
+ adc a, h
+ sub a, l
+ ld h, a
+ ld bc, $9800
+ add hl, bc
+ ret
+
+; @param a: tile ID
+; @return z: set if a is a wall.
+IsWallTile:
+ cp a, $00
+ ret z
+ cp a, $01
+ ret z
+ cp a, $02
+ ret z
+ cp a, $04
+ ret z
+ cp a, $05
+ ret z
+ cp a, $06
+ ret z
+ cp a, $07
+ ret
+
+
+; ANCHOR: print-bcd
+; @param B: BCD score to print
+; @param HL: Destination address
+; @mut: AF, HL
+PrintBCD:
+ ld a, b
+ and $F0
+ swap a
+ add a, DIGIT_OFFSET
+ ld [hl+], a
+ ld a, b
+ and $0F
+ add a, DIGIT_OFFSET
+ ld [hl+], a
+ ret
+; ANCHOR_END: print-bcd
+
+
+; Increase score by 1 and store it as a 1 byte packed BCD number
+; changes A and HL
+IncreaseScorePackedBCD:
+ xor a ; clear carry flag and a
+ inc a ; a = 1
+ ld hl, wScore ; load score
+ adc [hl] ; add 1
+ daa ; convert to BCD
+ ld [hl], a ; store score
+ call UpdateScoreBoard
+ ret
+
+
+; Read the packed BCD score from wScore and updates the score display
+UpdateScoreBoard:
+ ld a, [wScore] ; Get the Packed score
+ and %11110000 ; Mask the lower nibble
+ rrca ; Move the upper nibble to the lower nibble (divide by 16)
+ rrca
+ rrca
+ rrca
+ add a, DIGIT_OFFSET ; Offset + add to get the digit tile
+ ld [SCORE_TENS], a ; Show the digit on screen
+
+ ld a, [wScore] ; Get the packed score again
+ and %00001111 ; Mask the upper nibble
+ add a, DIGIT_OFFSET ; Offset + add to get the digit tile again
+ ld [SCORE_ONES], a ; Show the digit on screen
+ ret
+
+; ANCHOR: check-for-brick
+; Checks if a brick was collided with and breaks it if possible.
+; @param hl: address of tile.
+CheckAndHandleBrick:
+ ld a, [hl]
+ cp a, BRICK_LEFT
+ jr nz, CheckAndHandleBrickRight
+ ; Break a brick from the left side.
+ ld [hl], BLANK_TILE
+ inc hl
+ ld [hl], BLANK_TILE
+ call IncreaseScorePackedBCD
+CheckAndHandleBrickRight:
+ cp a, BRICK_RIGHT
+ ret nz
+ ; Break a brick from the right side.
+ ld [hl], BLANK_TILE
+ dec hl
+ ld [hl], BLANK_TILE
+ call IncreaseScorePackedBCD
+ ret
+; ANCHOR_END: check-for-brick
+
+Input:
+ ; Poll half the controller
+ ld a, P1F_GET_BTN
+ call .onenibble
+ ld b, a ; B7-4 = 1; B3-0 = unpressed buttons
+
+ ; Poll the other half
+ ld a, P1F_GET_DPAD
+ call .onenibble
+ swap a ; A3-0 = unpressed directions; A7-4 = 1
+ xor a, b ; A = pressed buttons + directions
+ ld b, a ; B = pressed buttons + directions
+
+ ; And release the controller
+ ld a, P1F_GET_NONE
+ ldh [rP1], a
+
+ ; Combine with previous wCurKeys to make wNewKeys
+ ld a, [wCurKeys]
+ xor a, b ; A = keys that changed state
+ and a, b ; A = keys that changed to pressed
+ ld [wNewKeys], a
+ ld a, b
+ ld [wCurKeys], a
+ ret
+
+.onenibble
+ ldh [rP1], a ; switch the key matrix
+ call .knownret ; burn 10 cycles calling a known ret
+ ldh a, [rP1] ; ignore value while waiting for the key matrix to settle
+ ldh a, [rP1]
+ ldh a, [rP1] ; this read counts
+ or a, $F0 ; A7-4 = 1; A3-0 = unpressed keys
+.knownret
+ ret
+
+; Copy bytes from one area to another.
+; @param de: Source
+; @param hl: Destination
+; @param bc: Length
+Memcopy:
+ ld a, [de]
+ ld [hli], a
+ inc de
+ dec bc
+ ld a, b
+ or a, c
+ jp nz, Memcopy
+ ret
+
+Tiles:
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33322222
+ dw `33322222
+ dw `33322222
+ dw `33322211
+ dw `33322211
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `22222222
+ dw `22222222
+ dw `22222222
+ dw `11111111
+ dw `11111111
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `22222333
+ dw `22222333
+ dw `22222333
+ dw `11222333
+ dw `11222333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33322211
+ dw `33322211
+ dw `33322211
+ dw `33322211
+ dw `33322211
+ dw `33322211
+ dw `33322211
+ dw `33322211
+ dw `22222222
+ dw `20000000
+ dw `20111111
+ dw `20111111
+ dw `20111111
+ dw `20111111
+ dw `22222222
+ dw `33333333
+ dw `22222223
+ dw `00000023
+ dw `11111123
+ dw `11111123
+ dw `11111123
+ dw `11111123
+ dw `22222223
+ dw `33333333
+ dw `11222333
+ dw `11222333
+ dw `11222333
+ dw `11222333
+ dw `11222333
+ dw `11222333
+ dw `11222333
+ dw `11222333
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `11001100
+ dw `11111111
+ dw `11111111
+ dw `21212121
+ dw `22222222
+ dw `22322232
+ dw `23232323
+ dw `33333333
+ ; My custom logo (tail)
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33302333
+ dw `33333133
+ dw `33300313
+ dw `33300303
+ dw `33013330
+ dw `30333333
+ dw `03333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `03333333
+ dw `30333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333330
+ dw `33333320
+ dw `33333013
+ dw `33330333
+ dw `33100333
+ dw `31001333
+ dw `20001333
+ dw `00000333
+ dw `00000033
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33330333
+ dw `33300333
+ dw `33333333
+ dw `33033333
+ dw `33133333
+ dw `33303333
+ dw `33303333
+ dw `33303333
+ dw `33332333
+ dw `33332333
+ dw `33333330
+ dw `33333300
+ dw `33333300
+ dw `33333100
+ dw `33333000
+ dw `33333000
+ dw `33333100
+ dw `33333300
+ dw `00000001
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `10000333
+ dw `00000033
+ dw `00000003
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `33332333
+ dw `33302333
+ dw `32003333
+ dw `00003333
+ dw `00003333
+ dw `00013333
+ dw `00033333
+ dw `00033333
+ dw `33333300
+ dw `33333310
+ dw `33333330
+ dw `33333332
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `30000000
+ dw `33000000
+ dw `33333000
+ dw `33333333
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000003
+ dw `00000033
+ dw `00003333
+ dw `02333333
+ dw `33333333
+ dw `00333333
+ dw `03333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+
+ ; digits
+ ; 0
+ dw `33333333
+ dw `33000033
+ dw `30033003
+ dw `30033003
+ dw `30033003
+ dw `30033003
+ dw `33000033
+ dw `33333333
+ ; 1
+ dw `33333333
+ dw `33300333
+ dw `33000333
+ dw `33300333
+ dw `33300333
+ dw `33300333
+ dw `33000033
+ dw `33333333
+ ; 2
+ dw `33333333
+ dw `33000033
+ dw `30330003
+ dw `33330003
+ dw `33000333
+ dw `30003333
+ dw `30000003
+ dw `33333333
+ ; 3
+ dw `33333333
+ dw `30000033
+ dw `33330003
+ dw `33000033
+ dw `33330003
+ dw `33330003
+ dw `30000033
+ dw `33333333
+ ; 4
+ dw `33333333
+ dw `33000033
+ dw `30030033
+ dw `30330033
+ dw `30330033
+ dw `30000003
+ dw `33330033
+ dw `33333333
+ ; 5
+ dw `33333333
+ dw `30000033
+ dw `30033333
+ dw `30000033
+ dw `33330003
+ dw `30330003
+ dw `33000033
+ dw `33333333
+ ; 6
+ dw `33333333
+ dw `33000033
+ dw `30033333
+ dw `30000033
+ dw `30033003
+ dw `30033003
+ dw `33000033
+ dw `33333333
+ ; 7
+ dw `33333333
+ dw `30000003
+ dw `33333003
+ dw `33330033
+ dw `33300333
+ dw `33000333
+ dw `33000333
+ dw `33333333
+ ; 8
+ dw `33333333
+ dw `33000033
+ dw `30333003
+ dw `33000033
+ dw `30333003
+ dw `30333003
+ dw `33000033
+ dw `33333333
+ ; 9
+ dw `33333333
+ dw `33000033
+ dw `30330003
+ dw `30330003
+ dw `33000003
+ dw `33330003
+ dw `33000033
+ dw `33333333
+; ANCHOR: link-tiles
+ ; External Clock
+ dw `33333333
+ dw `30000333
+ dw `30033333
+ dw `30003333
+ dw `30033333
+ dw `30000333
+ dw `33333333
+ dw `33333333
+ ; External Clock -- Active
+ dw `33333333
+ dw `30000333
+ dw `30033333
+ dw `30003333
+ dw `30033330
+ dw `30000300
+ dw `33333000
+ dw `33330000
+ ; Internal Clock
+ dw `33333333
+ dw `33333333
+ dw `33300003
+ dw `33330033
+ dw `33330033
+ dw `33330033
+ dw `33300003
+ dw `33333333
+ ; Internal Clock -- Active
+ dw `00003333
+ dw `00033333
+ dw `00300003
+ dw `03330033
+ dw `33330033
+ dw `33330033
+ dw `33300003
+ dw `33333333
+ ; X/No
+ dw `33333333
+ dw `30333033
+ dw `33030333
+ dw `33303333
+ dw `33030333
+ dw `30333033
+ dw `33333333
+ dw `33333333
+ ; O/Ok
+ dw `33333333
+ dw `33000033
+ dw `30333303
+ dw `30333303
+ dw `30333303
+ dw `30333303
+ dw `33000033
+ dw `33333333
+ ;
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+ dw `33333333
+; ANCHOR_END: link-tiles
+TilesEnd:
+
+Tilemap:
+ db $00, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $02, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $07, $03, $03, $1A, $1A, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $05, $06, $05, $06, $05, $06, $05, $06, $05, $06, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $0A, $0B, $0C, $0D, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $0E, $0F, $10, $11, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $12, $13, $14, $15, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $08, $07, $03, $16, $17, $18, $19, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+ db $04, $09, $09, $09, $09, $09, $09, $09, $09, $09, $09, $09, $09, $07, $03, $03, $03, $03, $03, $03, 0,0,0,0,0,0,0,0,0,0,0,0
+TilemapEnd:
+
+Paddle:
+ dw `33333333
+ dw `32222223
+ dw `33333333
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+ dw `00000000
+PaddleEnd:
+
+Ball:
+ dw `00330000
+ dw `03223000
+ dw `32222300
+ dw `32222300
+ dw `03223000
+ dw `00330000
+ dw `00000000
+ dw `00000000
+BallEnd:
+
+SECTION "Counter", WRAM0
+wFrameCounter: db
+
+SECTION "Input Variables", WRAM0
+wCurKeys: db
+wNewKeys: db
+
+SECTION "Ball Data", WRAM0
+wBallMomentumX: db
+wBallMomentumY: db
+
+; ANCHOR: score-variable
+SECTION "Score", WRAM0
+wScore: db
+wRemoteScore: db
+; ANCHOR_END: score-variable
+
+; ANCHOR: link-state
+SECTION "Link State", WRAM0
+wLink: db
+wLinkPacketCount: db
+wShakeFailed: db
+; ANCHOR_END: link-state
+
diff --git a/unbricked/serial-link/sio.asm b/unbricked/serial-link/sio.asm
new file mode 100644
index 00000000..f29354f1
--- /dev/null
+++ b/unbricked/serial-link/sio.asm
@@ -0,0 +1,332 @@
+; ::::::::::::::::::::::::::::::::::::::
+; :: ::
+; :: ______. ::
+; :: _ |````` || ::
+; :: _/ \__@_ |[- - ]|| ::
+; :: / `--<[|]= |[ m ]|| ::
+; :: \ .______ | ```` || ::
+; :: / !| `````| | + oo|| ::
+; :: ( ||[ ^u^]| | .. #|| ::
+; :: `-<[|]=|[ ]| `______// ::
+; :: || ```` | ::
+; :: || + oo| ::
+; :: || .. #| ::
+; :: !|______/ ::
+; :: ::
+; :: ::
+; ::::::::::::::::::::::::::::::::::::::
+
+; ANCHOR: sio-status-enum
+INCLUDE "hardware.inc"
+
+DEF SIO_IDLE EQU $00
+DEF SIO_DONE EQU $01
+DEF SIO_FAILED EQU $02
+DEF SIO_RESET EQU $03
+DEF SIO_ACTIVE EQU $80
+EXPORT SIO_IDLE, SIO_DONE, SIO_FAILED, SIO_ACTIVE
+; ANCHOR_END: sio-status-enum
+
+; ANCHOR: sio-port-start-defs
+; ANCHOR: sio-timeout-duration
+; Duration of timeout period in ticks
+DEF SIO_TIMEOUT_TICKS EQU 10
+; ANCHOR_END: sio-timeout-duration
+
+; ANCHOR: sio-catchup-duration
+; Catchup delay duration
+DEF SIO_CATCHUP_SLEEP_DURATION EQU 30
+; ANCHOR_END: sio-catchup-duration
+; ANCHOR_END: sio-port-start-defs
+
+; ANCHOR: sio-buffer-defs
+; Allocated size in bytes of the Tx and Rx data buffers.
+DEF SIO_BUFFER_SIZE EQU 16
+; A slightly identifiable value to clear the buffers to.
+DEF SIO_BUFFER_CLEAR EQU $EE
+; ANCHOR_END: sio-buffer-defs
+
+; ANCHOR: sio-packet-defs
+DEF SIO_PACKET_HEAD_SIZE EQU 2
+DEF SIO_PACKET_DATA_SIZE EQU SIO_BUFFER_SIZE - SIO_PACKET_HEAD_SIZE
+EXPORT SIO_PACKET_DATA_SIZE
+; ANCHOR_END: sio-packet-defs
+
+
+; ANCHOR: sio-buffers
+SECTION "SioBufferRx", WRAM0, ALIGN[8]
+wSioBufferRx:: ds SIO_BUFFER_SIZE
+
+
+SECTION "SioBufferTx", WRAM0, ALIGN[8]
+wSioBufferTx:: ds SIO_BUFFER_SIZE
+; ANCHOR_END: sio-buffers
+
+
+; ANCHOR: sio-state
+SECTION "SioCore State", WRAM0
+; Sio state machine current state
+wSioState:: db
+; Number of transfers to perform (bytes to transfer)
+wSioCount:: db
+; Current position in the tx/rx buffers
+wSioBufferOffset:: db
+; Timer state (as ticks remaining, expires at zero) for timeouts.
+wSioTimer:: db
+; ANCHOR_END: sio-state
+
+
+; ANCHOR: sio-impl-init
+SECTION "SioCore Impl", ROM0
+; Initialise/reset Sio to the ready to use 'IDLE' state.
+; @mut: AF, C, HL
+SioInit::
+ call SioReset
+ ld a, SIO_IDLE
+ ld [wSioState], a
+ ret
+
+
+; Completely reset Sio. Any active transfer will be stopped.
+; Sio will return to the `SIO_IDLE` state on the next call to `SioTick`.
+; @mut: AF, C, HL
+SioReset::
+ ; bring the serial port down
+ ldh a, [rSC]
+ res SCB_START, a
+ ldh [rSC], a
+ ; reset Sio state variables
+ ld a, SIO_RESET
+ ld [wSioState], a
+ ld a, 0
+ ld [wSioTimer], a
+ ld [wSioCount], a
+ ld [wSioBufferOffset], a
+; ANCHOR_END: sio-impl-init
+; ANCHOR: sio-reset-buffers
+ ; clear the Tx buffer
+ ld hl, wSioBufferTx
+ ld c, SIO_BUFFER_SIZE
+ ld a, SIO_BUFFER_CLEAR
+:
+ ld [hl+], a
+ dec c
+ jr nz, :-
+ ; clear the Rx buffer
+ ld hl, wSioBufferRx
+ ld c, SIO_BUFFER_SIZE
+ ld a, SIO_BUFFER_CLEAR
+:
+ ld [hl+], a
+ dec c
+ jr nz, :-
+ ret
+; ANCHOR_END: sio-reset-buffers
+
+
+; ANCHOR: sio-tick
+; Per-frame update
+; @mut: AF
+SioTick::
+ ; jump to state-specific tick routine
+ ld a, [wSioState]
+ cp a, SIO_ACTIVE
+ jr z, .active_tick
+ cp a, SIO_RESET
+ jr z, .reset_tick
+ ret
+.active_tick
+ ; update timeout on external clock
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ ret nz
+ ld a, [wSioTimer]
+ and a, a
+ ret z ; timer == 0, timeout disabled
+ dec a
+ ld [wSioTimer], a
+ jr z, SioAbort
+ ret
+.reset_tick
+ ; delayed reset to IDLE state
+ ld a, SIO_IDLE
+ ld [wSioState], a
+ ret
+; ANCHOR_END: sio-tick
+
+
+; ANCHOR: sio-abort
+; Abort the ongoing transfer (if any) and enter the FAILED state.
+; @mut: AF
+SioAbort::
+ ld a, SIO_FAILED
+ ld [wSioState], a
+ ldh a, [rSC]
+ res SCB_START, a
+ ldh [rSC], a
+ ret
+; ANCHOR_END: sio-abort
+
+
+; ANCHOR: sio-start-transfer
+; Start a whole-buffer transfer.
+; @mut: AF, L
+SioTransferStart::
+ ld a, SIO_BUFFER_SIZE
+.CustomCount::
+ ld [wSioCount], a
+ ld a, 0
+ ld [wSioBufferOffset], a
+ ; send first byte
+ ld a, [wSioBufferTx]
+ ldh [rSB], a
+ ld a, SIO_ACTIVE
+ ld [wSioState], a
+ jr SioPortStart
+; ANCHOR_END: sio-start-transfer
+
+
+; ANCHOR: sio-port-start
+; Enable the serial port, starting a transfer.
+; If internal clock is enabled, performs catchup delay before enabling the port.
+; Resets the transfer timeout timer.
+; @mut: AF, L
+SioPortStart:
+ ; If internal clock source, do catchup delay
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ ; NOTE: preserve `A` to be used after the loop
+ jr z, .start_xfer
+ ld l, SIO_CATCHUP_SLEEP_DURATION
+.catchup_sleep_loop:
+ dec l
+ jr nz, .catchup_sleep_loop
+.start_xfer:
+ or a, SCF_START
+ ldh [rSC], a
+ ; reset timeout
+ ld a, SIO_TIMEOUT_TICKS
+ ld [wSioTimer], a
+ ret
+; ANCHOR_END: sio-port-start
+
+
+; ANCHOR: sio-port-end
+; Collects the received value and starts the next byte transfer, if there is more to do.
+; Sets wSioState to SIO_DONE when the last expected byte is received.
+; Must be called after each serial port transfer (ideally from the serial interrupt).
+; @mut: AF, HL
+SioPortEnd::
+ ; Check that we were expecting a transfer (to end)
+ ld hl, wSioState
+ ld a, [hl+]
+ cp SIO_ACTIVE
+ ret nz
+ ; Update wSioCount
+ dec [hl]
+ ; Get buffer pointer offset (low byte)
+ ld a, [wSioBufferOffset]
+ ld l, a
+ ld h, HIGH(wSioBufferRx)
+ ldh a, [rSB]
+ ; NOTE: increments L only
+ ld [hl+], a
+ ; Store updated buffer offset
+ ld a, l
+ ld [wSioBufferOffset], a
+ ; If completing the last transfer, don't start another one
+ ; NOTE: We are checking the zero flag as set by `dec [hl]` up above!
+ jr nz, .next
+ ld a, SIO_DONE
+ ld [wSioState], a
+ ret
+.next:
+ ; Construct a Tx buffer pointer (keeping L from above)
+ ld h, HIGH(wSioBufferTx)
+ ld a, [hl]
+ ldh [rSB], a
+ jr SioPortStart
+; ANCHOR_END: sio-port-end
+
+
+SECTION "SioPacket Impl", ROM0
+; ANCHOR: sio-packet-prepare
+; Initialise the Tx buffer as a packet, ready for data.
+; Returns a pointer to the packet data section.
+; @return HL: packet data pointer
+; @mut: AF, C, HL
+SioPacketTxPrepare::
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ ld a, $AA
+ jr nz, :+
+ ld a, $BB
+:
+
+ ld hl, wSioBufferTx
+ ld [hl+], a
+ and a, $F0
+ ld c, SIO_BUFFER_SIZE - 1
+:
+ ld [hl+], a
+ dec c
+ jr nz, :-
+ ; checksum = 0 for initial calculation
+ ld hl, wSioBufferTx + 1
+ ld a, 0
+ ld [hl+], a
+ ret
+; ANCHOR_END: sio-packet-prepare
+
+
+; ANCHOR: sio-packet-finalise
+; Close the packet and start the transfer.
+; @mut: AF, C, HL
+SioPacketTxFinalise::
+ ld hl, wSioBufferTx
+ call SioPacketChecksum
+ ld [wSioBufferTx + 1], a
+ jp SioTransferStart
+; ANCHOR_END: sio-packet-finalise
+
+
+; ANCHOR: sio-packet-check
+; Check if a valid packet has been received by Sio.
+; @return HL: packet data pointer (only valid if packet found)
+; @return F.Z: if check OK
+; @mut: AF, C, HL
+SioPacketRxCheck::
+ ldh a, [rSC]
+ and a, SCF_SOURCE
+ ld a, $BB
+ jr nz, :+
+ ld a, $AA
+:
+
+ ld hl, wSioBufferRx
+ cp a, [hl]
+ ret nz
+
+ call SioPacketChecksum
+ and a, a ; set the Z flag if checksum matches
+ ld hl, wSioBufferRx + SIO_PACKET_HEAD_SIZE
+ ret
+; ANCHOR_END: sio-packet-check
+
+
+; ANCHOR: sio-checksum
+; Calculate a simple 1 byte checksum of a Sio data buffer.
+; sum(buffer + sum(buffer + 0)) == 0
+; @param HL: &buffer
+; @return A: sum
+; @mut: AF, C, HL
+SioPacketChecksum:
+ ld c, SIO_BUFFER_SIZE
+ ld a, c
+:
+ sub [hl]
+ inc hl
+ dec c
+ jr nz, :-
+ ret
+; ANCHOR_END: sio-checksum