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..8c4408bd --- /dev/null +++ b/src/part2/serial-link.md @@ -0,0 +1,608 @@ +# Serial Link + +--- + +**TODO:** In this lesson... +- learn how to control the Game Boy serial port from code +- build a wrapper over the low-level serial port interface +- implement high-level features to 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. +Some that *do* are: [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 (`SC`) 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. +The reason `EXPORT` is used in this lesson is to avoid adding (even more) fiddly bits to the project. + +::: + +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 constants, 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 + +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 (or as intact as it was, at least) so it can be used to inform error handling and 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 for this: +- The sender calculates a checksum of the outgoing packet and the result is transmitted as part of the packet transfer. +- The receiver preforms 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 interference, 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 attempt is aborted. + + +## 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. + +::: + + +## 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 a 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 relevant stuff 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 [on pandocs](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/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/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