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