From 84c8f1bba5d69380e2c8c572edd1f9c3c06bc936 Mon Sep 17 00:00:00 2001 From: Quinn <3379314+quinnyo@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:33:14 +1000 Subject: [PATCH 01/20] Add Serial Link lesson to part 2 --- src/SUMMARY.md | 1 + src/part2/serial-link.md | 608 +++++++++++++++++++++ unbricked/serial-link/main.asm | 931 +++++++++++++++++++++++++++++++++ unbricked/serial-link/sio.asm | 328 ++++++++++++ 4 files changed, 1868 insertions(+) create mode 100644 src/part2/serial-link.md create mode 100644 unbricked/serial-link/main.asm create mode 100644 unbricked/serial-link/sio.asm diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9d4aaeef..ea4d9a56 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -30,6 +30,7 @@ - [Collision](part2/collision.md) - [Bricks](part2/bricks.md) - [Decimal Numbers](part2/bcd.md) +- [Serial Link](part2/serial-link.md) - [Work in progress](part2/wip.md) # Part III — Our second game diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md new file mode 100644 index 00000000..8c4408bd --- /dev/null +++ b/src/part2/serial-link.md @@ -0,0 +1,608 @@ +# Serial Link + +--- + +**TODO:** In this lesson... +- learn how to control the Game Boy serial port from code +- build a wrapper over the low-level serial port interface +- implement high-level features to enable reliable data transfers + +--- + + +## Running the code + +Testing the code in this lesson (or any code that uses the serial port) is a bit more complicated than what we've been doing so far. +There's a few things to be aware of. + +You need an emulator that supports the serial port. +Some that *do* are: [Emulicious](https://emulicious.net/), and [GBE+](https://github.com/shonumi/gbe-plus). +The way this works is by having two instances of the emulator connect to each other over network sockets. + +Keep in mind that the emulated serial port is never going to replicate the complexity and breadth of issues that can occur on the real thing. + +Testing on hardware comes with hardware requirements, unfortunately. +You'll need two Game Boys (any combination of models), a link cable to connect them, and a pair of flash carts. + + +## The Game Boy serial port + +:::tip Information overload + +This section is intended as a reasonably complete description of the Game Boy serial port, from a programming perspective. +There's a lot of information packed in here and you don't need to absorb it all to continue. + +::: + +Communication via the serial port is organised as discrete data transfers of one byte each. +Data transfer is bidirectional, with every bit of data written out matched by one read in. +A data transfer can therefore be thought of as *swapping* the data byte in one device's buffer for the byte in the other's. + +The serial port is *idle* by default. +Idle time is used to read received data, configure the port if needed, and load the next value to send. + +Before we can transfer any data, we need to configure the *clock source* of both Game Boys. +To synchronise the two devices, one Game Boy must provide the clock signal that both will use. +Setting bit 0 of the **Serial Control** register (`SC`) enables the Game Boy's *internal* serial clock, and makes it the clock provider. +The other Game Boy must have its clock source set to *external* (`SC` bit 0 cleared). +The externally clocked Game Boy will receive the clock signal via the link cable. + +Before a transfer, the data to transmit is loaded into the **Serial Buffer** register (`SB`). +After a transfer, the `SB` register will contain the received data. + +When ready, the program can set bit 7 of the `SC` register in order to *activate* the port -- instructing it to perform a transfer. +While the serial port is *active*, it sends and receives a data bit on each serial clock pulse. +After 8 pulses (*8 bits!*) the transfer is complete -- the serial port deactivates itself, and the serial interrupt is requested. +Normal execution continues while the serial port is active: the transfer will be performed independently of the program code. + + +## Sio + +Alright, let's write some code! +**Sio** is the **S**erial **i**nput/**o**utput module and we're going to build it in its own file, so open a new file called `sio.asm`. + +At the top of `sio.asm`, include `hardware.inc` and then define a set of constants that represent Sio's main states: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-status-enum}} +{{#include ../../unbricked/serial-link/sio.asm:sio-status-enum}} +``` + +Sio operates as a finite state machine with each of these constants being a unique state. +Sio's job is to manage serial transfers, so Sio's state simultaneously indicates what Sio is doing and the current transfer status. + +:::tip EXPORT quality + +`EXPORT` makes the variables following it available in other source files. +In general, there are better ways to do this -- it shouldn't be your first choice. +The reason `EXPORT` is used in this lesson is to avoid adding (even more) fiddly bits to the project. + +::: + +Below the constants, add a new WRAM section with some variables for Sio's state: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-state}} +{{#include ../../unbricked/serial-link/sio.asm:sio-state}} +``` + +`wSioState` holds one of the state constants we defined above. +The other variables will be discussed as we build the features that use them. + +Add a new code section and an initialisation routine: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-impl-init}} +{{#include ../../unbricked/serial-link/sio.asm:sio-impl-init}} + ret +``` + + +### Buffers +The buffers are a pair of temporary storage locations for all messages sent or received by Sio. +There's a buffer for data to transmit (Tx) and one for receiving data (Rx). +The variable `wSioBufferOffset` holds the current location within *both* data buffers -- Game Boy serial transfers are always symmetrical. + +First we'll need a couple of constants, so add these below the existing constants, near the top of the file. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffer-defs}} +{{#include ../../unbricked/serial-link/sio.asm:sio-buffer-defs}} +``` + +Allocate the buffers, each in their own section, just above the `SioCore State` section we made earlier. +This needs to be specified carefully and uses some unfamiliar syntax, so you might like to copy and paste this code: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffers}} +{{#include ../../unbricked/serial-link/sio.asm:sio-buffers}} +``` + +`ALIGN[8]` causes each section -- and each buffer -- to start at an address with a low byte of zero. +This makes building a pointer to the buffer element at index `i` trivial, as the high byte of the pointer is constant for the entire buffer, and the low byte is simply `i`. +The result is a significant reduction in the amount of work required to access the data and manipulate offsets of both buffers. + +:::tip + +If you would like to learn more about aligning sections -- *which is by no means required to continue this lesson* -- the place to start is the [SECTIONS](https://rgbds.gbdev.io/docs/rgbasm.5#SECTIONS) section in the rgbasm language documenation. + +::: + +At the end of `SioReset`, clear the buffers: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-reset-buffers}} +{{#include ../../unbricked/serial-link/sio.asm:sio-reset-buffers}} +``` + + +### Core implementation +Below `SioInit`, add a function to start a multibyte transfer of the entire data buffer: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-start-transfer}} +{{#include ../../unbricked/serial-link/sio.asm:sio-start-transfer}} +``` + +To initialise the transfer, start from buffer offset zero, set the transfer count, and switch to the `SIO_ACTIVE` state. +The first byte to send is loaded from `wSioBufferTx` before a jump to the next function starts the first transfer immediately. + + +Activating the serial port is a simple matter of setting bit 7 of `rSC`, but we need to do a couple of other things at the same time, so add a function to bundle it all together: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-start}} +{{#include ../../unbricked/serial-link/sio.asm:sio-port-start}} +``` + +The first thing `SioPortStart` does is something called the "catchup delay", but only if the internal clock source is enabled. + +:::tip Delay? Why? + +When a Game Boy serial port is active, it will transfer a data bit whenever it detects clock pulse. +When using the external clock source, the active serial port will wait indefinitely -- until the externally provided clock signal is received. +But when using the internal clock source, bits will start getting transferred as soon as the port is activated. +Because the internally clocked device can't wait once activated, the catchup delay is used to ensure the externally clocked device activates its port first. + +::: + +To check if the internal clock is enabled, read the serial port control register (`rSC`) and check if the clock source bit is set. +We test the clock source bit by *anding* with `SCF_SOURCE`, which is a constant with only the clock source bit set. +The result of this will be `0` except for the clock source bit, which will maintain its original value. +So we can perform a conditional jump and skip the delay if the zero flag is set. +The delay itself is a loop that wastes time by doing nothing -- `nop` is an instruction that has no effect -- a number of times. + +To start the serial port, the constant `SCF_START` is combined with the clock source setting (still in `a`) and the updated value is loaded into the `SC` register. + +Finally, the timeout timer is reset by loading the constant `SIO_TIMEOUT_TICKS` into `wSioTimer`. + +:::tip Timeouts + +We know that the serial port will remain active until it detects eight clock pulses, and performs eight bit transfers. +A side effect of this is that when relying on an *external* clock source, a transfer may never end! +This is most likely to happen if there is no other Game Boy connected, or if both devices are set to use an external clock source. +To avoid having this quirk become a problem, we implement *timeouts*: each byte transfer must be completed within a set period of time or we give up and consider the transfer to have failed. + +::: + +We'd better define the constants that set the catchup delay and timeout duration: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-start-defs}} +{{#include ../../unbricked/serial-link/sio.asm:sio-port-start-defs}} +``` + + +Implement the timeout logic in `SioTick`: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-tick}} +{{#include ../../unbricked/serial-link/sio.asm:sio-tick}} +``` + +`SioTick` checks the current state (`wSioState`) and jumps to a state-specific subroutine (labelled `*_tick`). + +**`SIO_ACTIVE`:** a transfer has been started, if the clock source is *external*, update the timeout timer. + +The timer's state is an unsigned integer stored in `wSioTimer`. +Check that the timer is active (has a non-zero value) with `and a, a`. +Decrement the timer and write the new value back to memory. +If the timer expired (the new value is zero) the transfer should be aborted. +The `dec` instruction sets the zero flag in that case, so all we have to do is `jr z, SioAbort`. + +**`SIO_RESET`:** `SioReset` has been called, change state to `SIO_IDLE`. +This causes a one tick delay after `SioReset` is called. + + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-abort}} +{{#include ../../unbricked/serial-link/sio.asm:sio-abort}} +``` + +`SioAbort` brings the serial port down and sets the current state to `SIO_FAILED`. +The aborted transfer state is intentionally left intact (or as intact as it was, at least) so it can be used to inform error handling and debugging. + + +The last part of the core implementation handles the end of each byte transfer: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-end}} +{{#include ../../unbricked/serial-link/sio.asm:sio-port-end}} +``` + +`SioPortEnd` starts by checking that a transfer was started (the `SIO_ACTIVE` state). +We're receiving a byte, so the transfer counter (`wSioCount`) is reduced by one. +The received value is copied from the serial port (`rSB`) to Sio's buffer (`wSioBufferRx`). +If there are still bytes to transfer (transfer counter is greater than zero) the next value is loaded from `wSioBufferTx` and the transfer is started by `SioPortStart`. +Otherwise, if the transfer counter is zero, enter the `SIO_DONE` state. + + +## Interval + +So far we've written a bunch of code that, unfortunately, doesn't do anything on its own. +It works though, I promise! +The good news is that Sio -- the code that interfaces directly with the serial port -- is complete. + +:::tip 🤖 Take a break! + +Suggested break enrichment activity: CONSUME REFRESHMENT + +Naturally, yours, &c.\, + +A. Hughman + +::: + + +## Reliable Communication + +Sio by itself offers very little in terms of *reliability*. +For our purposes, reliability is all about dealing with errors. +The errors that we're concerned with are data replication errors -- any case where the data transmitted is not replicated correctly in the receiver. + + +The first step is detection. +The receiver needs to test the integrity of every incoming data packet, before doing anything else with it. +We'll use a checksum for this: +- The sender calculates a checksum of the outgoing packet and the result is transmitted as part of the packet transfer. +- The receiver preforms the same calculation and compares the result with the value from the sender. +- If the values match, the packet is intact. + + +With the packet integrity checksum, the receiving end can detect packet data corruption and discard packets that don't pass the test. +When a packet is not delivered successfully, it should be transmitted again by the sender. +Unfortunately, the sender has no idea if the packet it sent was delivered intact. + +To keep the sender in the loop, and manage retransmission, we need a *protocol* -- a set of rules that govern communication. +The protocol follows the principle: +> The sender of a packet will assume the transfer failed, *unless the receiver reports success*. + +Let's define two classes of packet: +- **Application Messages:** critical data that must be delivered, retransmit if delivery failed + - contains application-specific data +- **Protocol Metadata:** do not retransmit (always send the latest state) + - contains link state information (including last packet received) + + +:::tip Corruption? In my Game Boy? + +Yep, there's any number of possible causes of transfer data replication errors when working with the Game Boy serial port. +Some examples include: old or damaged hardware, luck, cosmic interference, and user actions (hostile and accidental). + +::: + + + +There's one more thing our protocol needs: some way to get both devices on the same page and kick things off. +We need a *handshake* that must be completed before doing anything else. +This is a simple sequence that checks that there is a connection and tests that the connection is working. +The handshake can be performed in one of two roles: *A* or *B*. +To be successful, one peer must be *A* and the other must be *B*. +Which role to perform is determined by the clock source setting of the serial port. +In each exchange, each peer sends a number associated with its role and expects to receive a number associated with the other role. +If an unexpected value is received, or something goes wrong with the transfer, that handshake attempt is aborted. + + +## SioPacket + +SioPacket is a thin layer over Sio buffer transfers. +- The most important addition is a checksum based integrity test. +- Several convenience routines are also provided. + +Packets fill a Sio buffer with the following structure: +```rgbasm +PacketLayout: + .start_mark: db ; The constant SIO_PACKET_START. + .checksum: db ; Packet checksum, set before transmission. + .data: ds SIO_BUFFER_SIZE - 2 ; Packet data (user defined). + ; Unused space in .data is filled with SIO_PACKET_END. +``` + +At the top of `sio.asm` define some constants: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-defs}} +{{#include ../../unbricked/serial-link/sio.asm:sio-packet-defs}} +``` + +`SioPacketTxPrepare` creates a new empty packet in the Tx buffer: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-prepare}} +{{#include ../../unbricked/serial-link/sio.asm:sio-packet-prepare}} +``` + +- The checksum is set to zero for the initial checksum calculation. +- The data section is cleared by filling it with the constant `SIO_PACKET_END`. + +After calling `SioPacketTxPrepare`, the payload data can be written to the packet. +Then, the function `SioPacketTxFinalise` should be called: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-finalise}} +{{#include ../../unbricked/serial-link/sio.asm:sio-packet-finalise}} +``` + +- Call `SioPacketChecksum` to calculate the packet checksum. + - It's important that the value of the checksum field is zero when performing this initial checksum calculation. +- Write the correct checksum to the packet header. +- Start the transfer. + + +Implement the packet integrity test for received packets: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-check}} +{{#include ../../unbricked/serial-link/sio.asm:sio-packet-check}} +``` + +- Check that the packet begins with the magic number `SIO_PACKET_START`. +- Calculate the checksum of the received data. + - This includes the packet checksum calculated by the sender. + - The result of this calculation will be zero if the data is the same as it was when sent. + +Finally, implement the checksum: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-checksum}} +{{#include ../../unbricked/serial-link/sio.asm:sio-checksum}} +``` + +- start with the size of the buffer (effectively -1 for each byte summed) +- subtract each byte in the buffer from the sum + +:::tip + +The checksum implemented here has been kept very simple for this tutorial. +It's probably worth looking into better solutions for real-world projects. + +::: + + +## Connecting it all together +It's time to implement the protocol and build the application-level features on top of everything we've done so far. + + +At the top of main.asm, define the constants for keeping track of Link's state: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-defs}} +{{#include ../../unbricked/serial-link/main.asm:serial-demo-defs}} +``` + + +We'll need some variables in WRAM to keep track of things. +Add a section at the bottom of main.asm: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-wram}} +{{#include ../../unbricked/serial-link/main.asm:serial-demo-wram}} +``` + +`wLocal` and `wRemote` are two identical structures for storing the Link state information of each peer. +- `state` holds the current mode and some flags (the `LINKST_` constants) +- `tx_id` & `rx_id` are for the IDs of the most recently sent & received `MSG_DATA` message + +The contents of application data messages (`MSG_DATA` only) will be stored in the buffers `wTxData` and `wRxData`. + +`wAllowTxAttempts` is the number of transmission attempts remaining for each DATA message. +`wAllowRxFaults` is a budget of delivery faults allowed before causing an error. + + +### LinkInit +Lots of variables means lots of initialisation so let's add a function for that: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-init}} +{{#include ../../unbricked/serial-link/main.asm:link-init}} +``` + +This initialises Sio by calling `SioInit` and then enables something called the serial interrupt which will be explained soon. +Execution continues into `LinkReset`. + +`LinkReset` can be called to reset the whole Link feature if something goes wrong. +This resets Sio and then writes default values to all the variables we defined above. +Finally, a function called `HandshakeDefault` is jumped to and for that one you'll have to wait a little bit! + +Make sure to call the init routine once before the main loop starts: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-init-callsite}} +{{#include ../../unbricked/serial-link/main.asm:serial-demo-init-callsite}} +``` + +We'll also add a utility function for handling errors: +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-error-stop}} +{{#include ../../unbricked/serial-link/main.asm:link-error-stop}} +``` + + +### Serial Interrupt +Sio needs to be told when to process each completed byte transfer. +The best way to do this is by using the serial interrupt. +Copy this code (it needs to be exact) to `main.asm`, just above the `"Header"` section: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-interrupt-vector}} +{{#include ../../unbricked/serial-link/main.asm:serial-interrupt-vector}} +``` + +A proper and complete explanation of this is beyond the scope of this lesson. +You can continue the lesson understanding that: +- This is the serial interrupt handler. It gets called automatically after each serial transfer. +- The relevant stuff is in `SioPortEnd` but it's necessary to jump through some hoops to call it. + +A detailed and rather dense explanation is included for completeness. + +:::tip + +*You can just use the code as explained above and skip past this box.* + +An interrupt handler is a piece of code at a specific address that gets called automatically under certain conditions. +The serial interrupt handler begins at address `$58` so a section just for this function is defined at that location using `ROM0[$58]`. +Note that the function is labelled by convention and for debugging purposes -- it isn't technically meaningful and the function isn't intended to be called manually. + +Whatever code was running when an interrupt occurs literally gets paused until the interrupt handler returns. +The registers used by `SioPortEnd` need to be preserved so the code that got interrupted doesn't break. +We use the stack to do this -- using `push` before the call and `pop` afterwards. +Note that the order of the registers when pushing is the opposite of the order when popping, due to the stack being a LIFO (last-in, first-out) container. + +`reti` returns from the function (like `ret`) and enables interrupts (like `ei`) which is necessary because interrupts are disabled automatically when calling an interrupt handler. + +If you would like to continue digging, have a look at [evie's interrupts tutorial](https://evie.gbdev.io/resources/interrupts) and [on pandocs](https://gbdev.io/pandocs/Interrupts.html). + +::: + + +### LinkUpdate +`LinkUpdate` is the main per-frame update function. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-update}} +{{#include ../../unbricked/serial-link/main.asm:link-update}} +``` + +The order of each part of this is important -- note the many (conditional) places where execution can exit this procedure. + +Check input before anything else so the user can always reset the demo. + +The `LINKST_MODE_ERROR` mode is an unrecoverable error state that can only be exited via the reset. +To check the current mode, read the `wLocal.state` byte and use `and a, LINKST_MODE` to keep just the mode bits. +There's nothing else to do in the `LINKST_MODE_ERROR` mode, so simply return from the function if that's the case. + +Update Sio by calling `SioTick` and then call a specific function for the current mode. + +`LINKST_MODE_CONNECT` manages the handshake process. +Update the handshake if it's incomplete (`wHandshakeState` is non-zero). +Otherwise, transition to the active connection mode. + +`LINKST_MODE_UP` just checks the current state of the Sio state machine in order to jump to an appropriate function to handle certain cases. + + +### LinkTx +`LinkTx` builds the next message packet and starts transferring it. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-message}} +{{#include ../../unbricked/serial-link/main.asm:link-send-message}} +``` + +There's two types of message that are sent while the link is active -- SYNC and DATA. +The `LINKST_STEP_SYNC` flag is used to alternate between the two types and ensure at least every second message is a SYNC. +A DATA message will only be sent if the `LINKST_STEP_SYNC` flag is clear and the `LINKST_TX_ACT` flag is set. + +Both cases then send a packet in much the same way -- `call SioPacketPrepare`, write the data to the packet (starting at `HL`), and then `call SioPacketFinalise`. + +To make sending DATA messages more convenient, add a utility function to take care of the details: +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-tx-start}} +{{#include ../../unbricked/serial-link/main.asm:link-tx-start}} +``` + + +### LinkRx +When a transfer has completed (`SIO_DONE`), process the received data in `LinkRx`: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-receive-message}} +{{#include ../../unbricked/serial-link/main.asm:link-receive-message}} +``` + +The first thing to do is flush Sio's state (set it to `SIO_IDLE`) to indicate that the received data has been processed. +Technically the data hasn't actually been processed yet, but this is a promise to do that! + +Check that a packet was received and that it arrived intact by calling `SioPacketRxCheck`. +If the packet checks out OK, read the message type from the packet data and jump to the appropriate routine to handle messages of that type. + + +If the result of `SioPacketRxCheck` was negative, or the message type is unrecognised, it's considered a delivery *fault*. +In case of a fault, the received data is discarded and the fault counter is updated. +The fault counter state is loaded from `wAllowRxFaults`. +If the value of the counter is zero (i.e. there's zero (more) faults allowed) the error mode is acivated. +If the value of the counter is more than zero, it's decremented and saved. + + +`MSG_SYNC` messages contain the sender's Link state, so first we copy the received data to `wRemote`. +Now we want to check if the remote peer has acknowledged delivery of a message sent to them. +Copy the new `wRemote.rx_id` value to register `B`, then load `wLocal.state` and copy it into register `C` +Check the `LINKST_TX_ACT` flag (using the `and` instruction) and return if it's not set. +Otherwise, an outgoing message has not been acknowledged yet, so load `wLocal.tx_id` and compare it to `wRemote.rx_id` (in register `B`). +If the two are equal that means the message was delivered, so clear the `LINKST_TX_ACT` flag and update `wLocal.state`. + + +Receiving `MSG_DATA` messages is straightforward. +The first byte is the message ID, so copy that from the packet to `wLocal.rx_id`. +The rest of the packet data is copied straight to the `wRxData` buffer. +Finally, a flag is set to indicate that data was newly received. + + +### Main + +Demo update routine: +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-update}} +{{#include ../../unbricked/serial-link/main.asm:serial-demo-update}} +``` + +Call the update routine from the main loop: +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-update-callsite}} +{{#include ../../unbricked/serial-link/main.asm:serial-demo-update-callsite}} +``` + + +### Implement the handshake protocol + +/// Establish contact by trading magic numbers + +/// Define the codes each device will send: +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-codes}} +{{#include ../../unbricked/serial-link/main.asm:handshake-codes}} +``` + +/// +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-state}} +{{#include ../../unbricked/serial-link/main.asm:handshake-state}} +``` + +/// Routines to begin handshake sequence as either the internally or externally clocked device. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-begin}} +{{#include ../../unbricked/serial-link/main.asm:handshake-begin}} +``` + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-update}} +{{#include ../../unbricked/serial-link/main.asm:handshake-update}} +``` + +The handshake can be forced to restart in the clock provider role by pressing START. +This is included as a fallback and manual override for the automatic role selection implemented below. + +If a transfer is completed, process the received data by jumping to `HandshakeMsgRx`. + +If the serial port is otherwise inactive, (re)start the handshake. +To automatically determine which device should be the clock provider, we check the lowest bit of the DIV register. +This value increments at around 16 kHz which, for our purposes and because we only check it every now and then, is close enough to random. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-xfer-complete}} +{{#include ../../unbricked/serial-link/main.asm:handshake-xfer-complete}} +``` + +Check that a packet was received and that it contains the expected handshake value. +The state of the serial port clock source bit is used to determine which value to expect -- `SHAKE_A` if set to use an external clock and `SHAKE_B` if using the internal clock. +If all is well, decrement the `wHandshakeState` counter. +If the counter is zero, there is nothing left to do. +Otherwise, more exchanges are required so start the next one immediately. + +:::tip + +This is a trivial example of a handshake protocol. +In a real application, you might want to consider: +- using a longer sequence of codes as a more unique app identifier +- sharing more information about each device and negotiating to decide the preferred clock provider + +::: + + + +## /// Running the test ROM + +/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different: +```console +$ rgbasm -o sio.o sio.asm +$ rgbasm -o main.o main.asm +$ rgblink -o unbricked.gb main.o sio.o +$ rgbfix -v -p 0xFF unbricked.gb +``` diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm new file mode 100644 index 00000000..c65d168b --- /dev/null +++ b/unbricked/serial-link/main.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/sio.asm b/unbricked/serial-link/sio.asm new file mode 100644 index 00000000..132c5d26 --- /dev/null +++ b/unbricked/serial-link/sio.asm @@ -0,0 +1,328 @@ +; :::::::::::::::::::::::::::::::::::::: +; :: :: +; :: ______. :: +; :: _ |````` || :: +; :: _/ \__@_ |[- - ]|| :: +; :: / `--<[|]= |[ 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 60 +; ANCHOR_END: sio-timeout-duration + +; ANCHOR: sio-catchup-duration +; Catchup delay duration +DEF SIO_CATCHUP_SLEEP_DURATION EQU 200 +; 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 32 +; 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 + +DEF SIO_PACKET_START EQU $70 +DEF SIO_PACKET_END EQU $7F +; 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: + nop + nop + 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:: + ld hl, wSioBufferTx + ; packet always starts with constant ID + ld a, SIO_PACKET_START + ld [hl+], a + ; checksum = 0 for initial calculation + ld a, 0 + ld [hl+], a + ; clear packet data + ld a, SIO_PACKET_END + ld c, SIO_PACKET_DATA_SIZE +: + ld [hl+], a + dec c + jr nz, :- + ld hl, wSioBufferTx + SIO_PACKET_HEAD_SIZE + 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:: + ld hl, wSioBufferRx + ; expect constant + ld a, [hl] + cp a, SIO_PACKET_START + ret nz + + ; check the sum + call SioPacketChecksum + and a, a + ld hl, wSioBufferRx + SIO_PACKET_HEAD_SIZE + ret ; F.Z already set (or not) +; 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 From d5065c718b0dae2742046f30d900f1e0eb349db5 Mon Sep 17 00:00:00 2001 From: Quinn <3379314+quinnyo@users.noreply.github.com> Date: Tue, 6 May 2025 09:43:50 +1000 Subject: [PATCH 02/20] Move serial link standalone demo to separate file --- unbricked/serial-link/{main.asm => demo.asm} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename unbricked/serial-link/{main.asm => demo.asm} (100%) diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/demo.asm similarity index 100% rename from unbricked/serial-link/main.asm rename to unbricked/serial-link/demo.asm From e855ec7cda78c5672f2c5d5d752420e4eab32007 Mon Sep 17 00:00:00 2001 From: Quinn <3379314+quinnyo@users.noreply.github.com> Date: Tue, 6 May 2025 09:45:05 +1000 Subject: [PATCH 03/20] Add serial link Unbricked integration --- unbricked/serial-link/main.asm | 1102 ++++++++++++++++++++++++++++++++ unbricked/serial-link/sio.asm | 50 +- 2 files changed, 1129 insertions(+), 23 deletions(-) create mode 100644 unbricked/serial-link/main.asm 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 index 132c5d26..f29354f1 100644 --- a/unbricked/serial-link/sio.asm +++ b/unbricked/serial-link/sio.asm @@ -30,18 +30,18 @@ EXPORT SIO_IDLE, SIO_DONE, SIO_FAILED, SIO_ACTIVE ; ANCHOR: sio-port-start-defs ; ANCHOR: sio-timeout-duration ; Duration of timeout period in ticks -DEF SIO_TIMEOUT_TICKS EQU 60 +DEF SIO_TIMEOUT_TICKS EQU 10 ; ANCHOR_END: sio-timeout-duration ; ANCHOR: sio-catchup-duration ; Catchup delay duration -DEF SIO_CATCHUP_SLEEP_DURATION EQU 200 +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 32 +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 @@ -49,9 +49,7 @@ DEF SIO_BUFFER_CLEAR EQU $EE ; ANCHOR: sio-packet-defs DEF SIO_PACKET_HEAD_SIZE EQU 2 DEF SIO_PACKET_DATA_SIZE EQU SIO_BUFFER_SIZE - SIO_PACKET_HEAD_SIZE - -DEF SIO_PACKET_START EQU $70 -DEF SIO_PACKET_END EQU $7F +EXPORT SIO_PACKET_DATA_SIZE ; ANCHOR_END: sio-packet-defs @@ -201,8 +199,6 @@ SioPortStart: jr z, .start_xfer ld l, SIO_CATCHUP_SLEEP_DURATION .catchup_sleep_loop: - nop - nop dec l jr nz, .catchup_sleep_loop .start_xfer: @@ -260,21 +256,25 @@ SECTION "SioPacket Impl", ROM0 ; @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 - ; packet always starts with constant ID - ld a, SIO_PACKET_START ld [hl+], a - ; checksum = 0 for initial calculation - ld a, 0 - ld [hl+], a - ; clear packet data - ld a, SIO_PACKET_END - ld c, SIO_PACKET_DATA_SIZE + and a, $F0 + ld c, SIO_BUFFER_SIZE - 1 : ld [hl+], a dec c jr nz, :- - ld hl, wSioBufferTx + SIO_PACKET_HEAD_SIZE + ; checksum = 0 for initial calculation + ld hl, wSioBufferTx + 1 + ld a, 0 + ld [hl+], a ret ; ANCHOR_END: sio-packet-prepare @@ -296,17 +296,21 @@ SioPacketTxFinalise:: ; @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 - ; expect constant - ld a, [hl] - cp a, SIO_PACKET_START + cp a, [hl] ret nz - ; check the sum call SioPacketChecksum - and a, a + and a, a ; set the Z flag if checksum matches ld hl, wSioBufferRx + SIO_PACKET_HEAD_SIZE - ret ; F.Z already set (or not) + ret ; ANCHOR_END: sio-packet-check From d618855fcff7ee8443075517b8bb2e8729729cf5 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:32:18 +0000 Subject: [PATCH 04/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 8c4408bd..ac8a79ce 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -15,7 +15,7 @@ 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. +You need an emulator that supports the serial port, such as: Some that *do* are: [Emulicious](https://emulicious.net/), and [GBE+](https://github.com/shonumi/gbe-plus). The way this works is by having two instances of the emulator connect to each other over network sockets. From 34872d3982c8223c430549553995db9014ce4c25 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:32:34 +0000 Subject: [PATCH 05/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index ac8a79ce..c41b4d60 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -16,7 +16,7 @@ Testing the code in this lesson (or any code that uses the serial port) is a bit There's a few things to be aware of. You need an emulator that supports the serial port, such as: -Some that *do* are: [Emulicious](https://emulicious.net/), and [GBE+](https://github.com/shonumi/gbe-plus). +[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. From 142e461a5977b8552d95fcefa2b2fc3850dc9551 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:33:00 +0000 Subject: [PATCH 06/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index c41b4d60..ac8c496b 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -74,7 +74,7 @@ Sio's job is to manage serial transfers, so Sio's state simultaneously indicates `EXPORT` makes the variables following it available in other source files. In general, there are better ways to do this -- it shouldn't be your first choice. -The reason `EXPORT` is used in this lesson is to avoid adding (even more) fiddly bits to the project. +`EXPORT` is used here for simplicity, so we can stay focused on the concept being taught. ::: From 7eb9d34c216967ab8c0734efd7e8639eb261837a Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:33:20 +0000 Subject: [PATCH 07/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index ac8c496b..30e85978 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -100,7 +100,7 @@ The buffers are a pair of temporary storage locations for all messages sent or r There's a buffer for data to transmit (Tx) and one for receiving data (Rx). The variable `wSioBufferOffset` holds the current location within *both* data buffers -- Game Boy serial transfers are always symmetrical. -First we'll need a couple of constants, so add these below the existing constants, near the top of the file. +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}} From 9883e0a89d7401db3b783901e0c193570b255ae4 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:34:28 +0000 Subject: [PATCH 08/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 30e85978..8b0523c4 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -117,7 +117,7 @@ This needs to be specified carefully and uses some unfamiliar syntax, so you mig 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 +:::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. From 8b27e7dd1e0c285a8012b097804255848f375964 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:34:58 +0000 Subject: [PATCH 09/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 8b0523c4..54535c81 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -253,7 +253,7 @@ The first step is detection. The receiver needs to test the integrity of every incoming data packet, before doing anything else with it. We'll use a checksum for this: - The sender calculates a checksum of the outgoing packet and the result is transmitted as part of the packet transfer. -- The receiver preforms the same calculation and compares the result with the value from the sender. +- The receiver performs the same calculation and compares the result with the value from the sender. - If the values match, the packet is intact. From 80ac372d84ae88acf8f59cb279785f62d3d3bea5 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:35:29 +0000 Subject: [PATCH 10/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 54535c81..7f2ec7a4 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -209,7 +209,7 @@ This causes a one tick delay after `SioReset` is called. ``` `SioAbort` brings the serial port down and sets the current state to `SIO_FAILED`. -The aborted transfer state is intentionally left intact (or as intact as it was, at least) so it can be used to inform error handling and debugging. +The 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: From 13b1321539698fcdeaed81301d8c0fbb7ff82701 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:35:46 +0000 Subject: [PATCH 11/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 7f2ec7a4..4362d55c 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -387,7 +387,7 @@ Add a section at the bottom of main.asm: The contents of application data messages (`MSG_DATA` only) will be stored in the buffers `wTxData` and `wRxData`. `wAllowTxAttempts` is the number of transmission attempts remaining for each DATA message. -`wAllowRxFaults` is a budget of delivery faults allowed before causing an error. +`wAllowRxFaults` is the "budget" of delivery faults allowed before causing an error. ### LinkInit From da406d7d24c634e4de9a1a07e80d88f7744e0305 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:37:18 +0000 Subject: [PATCH 12/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 4362d55c..7d26122b 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -447,7 +447,7 @@ Note that the order of the registers when pushing is the opposite of the order w `reti` returns from the function (like `ret`) and enables interrupts (like `ei`) which is necessary because interrupts are disabled automatically when calling an interrupt handler. -If you would like to continue digging, have a look at [evie's interrupts tutorial](https://evie.gbdev.io/resources/interrupts) and [on pandocs](https://gbdev.io/pandocs/Interrupts.html). +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). ::: From 28fec9cd12ec925c20589ae1e9827c807ebbbfd8 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Tue, 6 Jan 2026 00:37:44 +0000 Subject: [PATCH 13/20] Update src/part2/serial-link.md --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 7d26122b..861a2ea8 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -275,7 +275,7 @@ Let's define two classes of packet: :::tip Corruption? In my Game Boy? Yep, there's any number of possible causes of transfer data replication errors when working with the Game Boy serial port. -Some examples include: old or damaged hardware, luck, cosmic interference, and user actions (hostile and accidental). +Some examples include: old or damaged hardware, luck, [cosmic rays](https://en.wikipedia.org/wiki/Single-event_upset), and user actions (hostile and accidental). ::: From ab92be9751fdfc40b7e16f1a18b742c8ec72b959 Mon Sep 17 00:00:00 2001 From: Quinn <3379314+quinnyo@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:04:41 +1000 Subject: [PATCH 14/20] Update src/part2/serial-link.md Co-authored-by: Antonio Vivace --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 861a2ea8..5af023fd 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -288,7 +288,7 @@ The handshake can be performed in one of two roles: *A* or *B*. To be successful, one peer must be *A* and the other must be *B*. Which role to perform is determined by the clock source setting of the serial port. In each exchange, each peer sends a number associated with its role and expects to receive a number associated with the other role. -If an unexpected value is received, or something goes wrong with the transfer, that handshake attempt is aborted. +If an unexpected value is received, or something goes wrong with the transfer, that handshake is rejected. ## SioPacket From 67548197cbfecf60297f7b9db0ad1536dc29198d Mon Sep 17 00:00:00 2001 From: Quinn <3379314+quinnyo@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:11:21 +1000 Subject: [PATCH 15/20] Update src/part2/serial-link.md Co-authored-by: Antonio Vivace --- src/part2/serial-link.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 5af023fd..53ed4d6a 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -251,7 +251,7 @@ The errors that we're concerned with are data replication errors -- any case whe The first step is detection. The receiver needs to test the integrity of every incoming data packet, before doing anything else with it. -We'll use a checksum for this: +We'll use a [checksumming](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. From 64a48cae5c6ab0c937eede2b62d4b218b714d745 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Wed, 7 Jan 2026 23:37:56 +0100 Subject: [PATCH 16/20] add intro text --- src/part2/serial-link.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 53ed4d6a..d5855c92 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -1,14 +1,10 @@ # Serial Link ---- - -**TODO:** In this lesson... -- learn how to control the Game Boy serial port from code -- build a wrapper over the low-level serial port interface -- implement high-level features to enable reliable data transfers - ---- +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 @@ -251,7 +247,7 @@ The errors that we're concerned with are data replication errors -- any case whe 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 [checksumming](https://en.wikipedia.org/wiki/Checksum) mechanism for this: +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. From 7f8d01e4ae2d6a90e5c22c67cfa6ab707ae32d79 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Wed, 7 Jan 2026 23:46:38 +0100 Subject: [PATCH 17/20] add build script for the cicd --- unbricked/serial-link/build.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 unbricked/serial-link/build.sh diff --git a/unbricked/serial-link/build.sh b/unbricked/serial-link/build.sh new file mode 100644 index 00000000..7a629ce5 --- /dev/null +++ b/unbricked/serial-link/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +rgbasm -o main.o main.asm +rgblink -o unbricked.gb main.o +rgbfix -v -p 0xFF unbricked.gb \ No newline at end of file From 234e01ac6b3681b2a258240a251d2db7f99a8752 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Sat, 10 Jan 2026 20:02:51 +0100 Subject: [PATCH 18/20] implement suggestions and fix discussions --- src/part2/serial-link.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index d5855c92..091071bd 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -39,7 +39,8 @@ Idle time is used to read received data, configure the port if needed, and load Before we can transfer any data, we need to configure the *clock source* of both Game Boys. To synchronise the two devices, one Game Boy must provide the clock signal that both will use. -Setting bit 0 of the **Serial Control** register (`SC`) enables the Game Boy's *internal* serial clock, and makes it the clock provider. +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. @@ -355,6 +356,9 @@ Finally, implement the checksum: 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. ::: @@ -424,7 +428,7 @@ Copy this code (it needs to be exact) to `main.asm`, just above the `"Header"` s A proper and complete explanation of this is beyond the scope of this lesson. You can continue the lesson understanding that: - This is the serial interrupt handler. It gets called automatically after each serial transfer. -- The relevant stuff is in `SioPortEnd` but it's necessary to jump through some hoops to call it. +- 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. From c5884892a31520c8b754b08abfc81514d2a15aee Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Sat, 10 Jan 2026 20:06:51 +0100 Subject: [PATCH 19/20] older hardware.inc --- unbricked/serial-link/hardware.inc | 1022 ++++++++++++++++++++++++++++ 1 file changed, 1022 insertions(+) create mode 100644 unbricked/serial-link/hardware.inc 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 From 642165e8d9119d60d3b73bfb30fc68ec5c5bf8f4 Mon Sep 17 00:00:00 2001 From: Antonio Vivace Date: Sat, 10 Jan 2026 20:07:14 +0100 Subject: [PATCH 20/20] update build commands --- unbricked/serial-link/build.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unbricked/serial-link/build.sh b/unbricked/serial-link/build.sh index 7a629ce5..14a60772 100644 --- a/unbricked/serial-link/build.sh +++ b/unbricked/serial-link/build.sh @@ -1,5 +1,6 @@ #!/bin/sh +rgbasm -o sio.o sio.asm rgbasm -o main.o main.asm -rgblink -o unbricked.gb main.o +rgblink -o unbricked.gb main.o sio.o rgbfix -v -p 0xFF unbricked.gb \ No newline at end of file