|
| 1 | +# Serial Link |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +**TODO:** In this lesson... |
| 6 | +- learn about the Game Boy serial port... |
| 7 | + - how it works, how to use it |
| 8 | + - pitfalls and challenges |
| 9 | +- build a thing, Sio Core: |
| 10 | + - multibyte + convenience wrapper over GB serial |
| 11 | + - incl. sync catchup delays, timeouts |
| 12 | +- do something with Sio: |
| 13 | + - integrate/use Sio |
| 14 | + - ? manually choose clock provider |
| 15 | + - ? send some data ... |
| 16 | +- ? build a thing, 'Packets': |
| 17 | + - adds data integrity test with simple checksum |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | + |
| 22 | +## Running the code |
| 23 | +To test the code in this lesson, you'll need a link cable, two Game Boys, and a way to load the ROM on both devices at once, e.g. two flash carts. |
| 24 | +There are no special cartridge requirements -- the most basic ROM-only carts will work. |
| 25 | + |
| 26 | +You can use any combination of Game Boy models, *provided you have the appropriate cable/adapter to connect them*. |
| 27 | +The only thing to look out for is that a different (smaller) connector was introduced with the MGB. |
| 28 | +So if you're connecting a DMG with a later model, make sure you have an adapter or a cable with both connectors. |
| 29 | + |
| 30 | +<!-- TODO: Perhaps somebody can confirm if AGB (& SP?) can be used for testing? --> |
| 31 | +<!-- You can also use an original Game Boy Advance or SP for testing purposes as they're backwards compatible. --> |
| 32 | +<!-- The AGB introduced another connector ... you can't use an AGB link cable with the older devices, but the MGB link cable works to connect to AGB. --> |
| 33 | + |
| 34 | +:::tip Can I just use an emulator? |
| 35 | + |
| 36 | +Emulators should not be relied upon as a substitute for the real thing, especially when working with the serial port. |
| 37 | +<!-- With that said, gbe-plus seems promising... --> |
| 38 | +<!-- Also, avoid Emulicious... --> |
| 39 | + |
| 40 | +::: |
| 41 | + |
| 42 | + |
| 43 | +## The Game Boy serial port |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +**TODO:** about this section |
| 48 | +- this section = crash course on GB serial port theory and operation |
| 49 | +- programmer's mental model (not a description of the hardware implementation) |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +Communication via the serial port is organised as discrete data transfers of one byte each. |
| 54 | +Data transfer is bidirectional, with every bit of data written out matched by one read in. |
| 55 | +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. |
| 56 | + |
| 57 | +The serial port is *idle* by default. |
| 58 | +Idle time is used to read received data, configure the port if needed, and load the next value to send. |
| 59 | + |
| 60 | +Before we can transfer any data, we need to configure the *clock source* of both Game Boys. |
| 61 | +To synchronise the two devices, one Game Boy must provide the clock signal that both will use. |
| 62 | +Setting bit 0 of the **Serial Control** register (`SC`) enables the Game Boy's *internal* serial clock, and makes it the clock provider. |
| 63 | +The other Game Boy must have its clock source set to *external* (`SC` bit 0 cleared). |
| 64 | +The externally clocked Game Boy will receive the clock signal via the link cable. |
| 65 | + |
| 66 | +Before a transfer, the data to transmit is loaded into the **Serial Buffer** register (`SB`). |
| 67 | +After a transfer, the `SB` register will contain the received data. |
| 68 | + |
| 69 | +When ready, the program can set bit 7 of the `SC` register in order to *activate* the port -- instructing it to perform a transfer. |
| 70 | +While the serial port is *active*, it sends and receives a data bit on each serial clock pulse. |
| 71 | +After 8 pulses (*8 bits!*) the transfer is complete -- the serial port deactivates itself, and the serial interrupt is requested. |
| 72 | +Normal execution continues while the serial port is active: the transfer will be performed independently of the program code. |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +**TODO:** something about the challenges posed... |
| 77 | +- GB serial is not "unreliable"... But it's also "not reliable"... |
| 78 | +- some notable things for reliable communication that GB doesn't provide: |
| 79 | + - connection detection, status: can't be truly solved in software, work around with error detection |
| 80 | + - delivery report / ACK: software can make improvements with careful design |
| 81 | + - error detection: software implementation can be effective |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | + |
| 86 | +## Sio |
| 87 | +Let's start building **Sio**, a serial I/O guy. |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +**TODO:** Create a file, sio.asm? (And complicate the build process) ... Just stick it in main.asm? |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +First, define the constants that represent Sio's main states/status: |
| 96 | + |
| 97 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-status-enum}} |
| 98 | +{{#include ../../unbricked/serial-link/sio.asm:sio-status-enum}} |
| 99 | +``` |
| 100 | + |
| 101 | +Add a new WRAM section with some variables for Sio's state: |
| 102 | + |
| 103 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-state}} |
| 104 | +{{#include ../../unbricked/serial-link/sio.asm:sio-state}} |
| 105 | +``` |
| 106 | + |
| 107 | +We'll discuss each of these variables as we build the features that use them. |
| 108 | + |
| 109 | +Add a new code section and an init routine: |
| 110 | + |
| 111 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-impl-init}} |
| 112 | +{{#include ../../unbricked/serial-link/sio.asm:sio-impl-init}} |
| 113 | +``` |
| 114 | + |
| 115 | + |
| 116 | +### Buffers |
| 117 | +The buffers are a pair of temporary storage locations for all messages sent or received by Sio. |
| 118 | +There's a buffer for data to transmit (Tx) and one for receiving data (Rx). |
| 119 | +Both buffers will be the same size, which is set via a constant: |
| 120 | + |
| 121 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffer-defs}} |
| 122 | +{{#include ../../unbricked/serial-link/sio.asm:sio-buffer-defs}} |
| 123 | +``` |
| 124 | + |
| 125 | +:::tip |
| 126 | + |
| 127 | +Blocks of memory can be allocated using `ds N`, where `N` is the size of the block in bytes. |
| 128 | +For more about `ds`, see [Statically allocating space in RAM](https://rgbds.gbdev.io/docs/rgbasm.5#Statically_allocating_space_in_RAM) in the rgbasm language manual. |
| 129 | + |
| 130 | +::: |
| 131 | + |
| 132 | +Define the buffers, each in its own WRAM section: |
| 133 | + |
| 134 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffers}} |
| 135 | +{{#include ../../unbricked/serial-link/sio.asm:sio-buffers}} |
| 136 | +``` |
| 137 | + |
| 138 | +:::tip ALIGN |
| 139 | + |
| 140 | +For the purpose of this lesson, `ALIGN[8]` causes the section to start at an address with a lower byte of zero. |
| 141 | +The reason that these sections are *aligned* like this is explained below. |
| 142 | + |
| 143 | +If you want to learn more -- *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. |
| 144 | + |
| 145 | +::: |
| 146 | + |
| 147 | +Each buffer is aligned to start at an address with a low byte of zero. |
| 148 | +This makes building a pointer to the 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`. |
| 149 | + |
| 150 | +The variable `wSioBufferOffset` holds the current location within *both* data buffers and can be used as an offset/index and directly in a pointer. |
| 151 | + |
| 152 | +The result is a significant reduction in the amount of work required to access the data and manipulate offsets of both buffers. |
| 153 | + |
| 154 | + |
| 155 | +### Core implementation |
| 156 | +<!-- TransferStart --> |
| 157 | +Below `SioInit`, add a function to start a multibyte transfer of the entire data buffer: |
| 158 | + |
| 159 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-start-transfer}} |
| 160 | +{{#include ../../unbricked/serial-link/sio.asm:sio-start-transfer}} |
| 161 | +``` |
| 162 | + |
| 163 | +To initialise the transfer, start from buffer offset zero, set the transfer count, and switch to the `SIO_ACTIVE` state. |
| 164 | +The first byte to send is loaded from `wSioBufferTx` before a jump to the next function starts the first transfer immediately. |
| 165 | + |
| 166 | +<!-- PortStart --> |
| 167 | +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: |
| 168 | + |
| 169 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-start}} |
| 170 | +{{#include ../../unbricked/serial-link/sio.asm:sio-port-start}} |
| 171 | +``` |
| 172 | + |
| 173 | +The first thing `SioPortStart` does is something called the "catchup delay", but only if the internal clock source is enabled. |
| 174 | + |
| 175 | +:::tip Delay? Why? |
| 176 | + |
| 177 | +When a Game Boy serial port is active, it will transfer a data bit whenever it detects clock pulse. |
| 178 | +When using the external clock source, the active serial port will wait indefinitely -- until the externally provided clock signal is received. |
| 179 | +But when using the internal clock source, bits will start getting transferred as soon as the port is activated. |
| 180 | +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. |
| 181 | + |
| 182 | +::: |
| 183 | + |
| 184 | +To check if the internal clock is enabled, read the serial port control register (`rSC`) and check if the clock source bit is set. |
| 185 | +We test the clock source bit by *anding* with `SCF_SOURCE`, which is a constant with only the clock source bit set. |
| 186 | +The result of this will be `0` except for the clock source bit, which will maintain its original value. |
| 187 | +So we can perform a conditional jump and skip the delay if the zero flag is set. |
| 188 | +The delay itself is a loop that wastes time by doing nothing -- `nop` is an instruction that has no effect -- a number of times. |
| 189 | + |
| 190 | +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. |
| 191 | + |
| 192 | +Finally, the timeout timer is reset by loading the constant `SIO_TIMEOUT_TICKS` into `wSioTimer`. |
| 193 | + |
| 194 | +:::tip Timeouts |
| 195 | + |
| 196 | +We know that the serial port will remain active until it detects eight clock pulses, and performs eight bit transfers. |
| 197 | +A side effect of this is that when relying on an *external* clock source, a transfer may never end! |
| 198 | +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. |
| 199 | +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. |
| 200 | + |
| 201 | +::: |
| 202 | + |
| 203 | +We'd better define the constants that set the catchup delay and timeout duration: |
| 204 | + |
| 205 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-start-defs}} |
| 206 | +{{#include ../../unbricked/serial-link/sio.asm:sio-port-start-defs}} |
| 207 | +``` |
| 208 | + |
| 209 | +<!-- Tick --> |
| 210 | +Implement `SioTick` to update the timeout and `SioAbort` to cancel the ongoing transfer: |
| 211 | + |
| 212 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-tick}} |
| 213 | +{{#include ../../unbricked/serial-link/sio.asm:sio-tick}} |
| 214 | +``` |
| 215 | + |
| 216 | +Check that a transfer has been started, and that the clock source is set to *external*. |
| 217 | +Before *ticking* the timer, check that the timer hasn't already expired with `and a, a`. |
| 218 | +Do nothing if the timer value is already zero. |
| 219 | +Decrement the timer and save the new value before jumping to `SioAbort` if new value is zero. |
| 220 | + |
| 221 | +<!-- PortEnd --> |
| 222 | +The last part of the core implementation handles the end of a transfer: |
| 223 | + |
| 224 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-end}} |
| 225 | +{{#include ../../unbricked/serial-link/sio.asm:sio-port-end}} |
| 226 | +``` |
| 227 | + |
| 228 | +--- |
| 229 | + |
| 230 | +**TODO:** walkthrough SioPortEnd |
| 231 | + |
| 232 | +this one is a little bit more involved... |
| 233 | + |
| 234 | +- check that Sio is in the **ACTIVE** state before continuing |
| 235 | +- use `ld a, [hl+]` to access `wSioState` and advance `hl` to `wSioCount` |
| 236 | +- update `wSioCount` using `dec [hl]` |
| 237 | + - which you might not have seen before? |
| 238 | + - this works out a bit faster than reading number into `a`, decrementing it, storing it again |
| 239 | + |
| 240 | +- NOTE: at this point we are avoiding using opcodes that set the zero flag as we want to check the result of decrementing `wSioCount` shortly. |
| 241 | + |
| 242 | +- construct a buffer Rx pointer using `wSioBufferOffset` |
| 243 | + - load the value from wram into the `l` register |
| 244 | + - load the `h` register with the constant high byte of the buffer Rx address space |
| 245 | + |
| 246 | +- grab the received value from `rSB` and copy it to the buffer Rx |
| 247 | + - we need to increment the buffer offset ... |
| 248 | + - `hl` is incremented here but we know only `l` will be affected because of the buffer alignment |
| 249 | + - the updated buffer pointer is stored |
| 250 | + |
| 251 | +- now we check the transfer count remaining |
| 252 | + - the `z` flag was updated by the `dec` instruction earlier -- none of the instructions in between modify the flags. |
| 253 | + |
| 254 | +- if the count is more than zero (i.e. more bytes to transfer) start the next byte transfer |
| 255 | + - construct a buffer Tx pointer in `hl` by setting `h` to the high byte of the buffer Tx address. keep `l`, which has the updated buffer position. |
| 256 | + - load the next tx value into `rSB` and activate the serial port! |
| 257 | + |
| 258 | +- otherwise the count is zero, we just completed the final byte transfer, so set `SIO_DONE` and return. |
| 259 | + |
| 260 | +--- |
| 261 | + |
| 262 | +`SioPortEnd` must be called once after each byte transfer. |
| 263 | +To do this we'll use the serial interrupt: |
| 264 | + |
| 265 | +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}} |
| 266 | +{{#include ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}} |
| 267 | +``` |
| 268 | + |
| 269 | +--- |
| 270 | + |
| 271 | +**TODO:** explain something about interrupts? but don't be weird about it, I guess... |
| 272 | + |
| 273 | +--- |
| 274 | + |
| 275 | + |
| 276 | +## Using Sio |
| 277 | + |
| 278 | +--- |
| 279 | + |
| 280 | +**TODO:** |
| 281 | + |
| 282 | +/// initialise Sio |
| 283 | +Before doing anything else with Sio, `SioInit` needs to be called. |
| 284 | + |
| 285 | +```rgbasm |
| 286 | + call SioInit |
| 287 | +
|
| 288 | + ; enable interrupts! |
| 289 | + ei |
| 290 | +``` |
| 291 | + |
| 292 | +/// update Sio every frame... |
| 293 | +```rgbasm |
| 294 | + call SioTick |
| 295 | +``` |
| 296 | + |
| 297 | +/// set clock source |
| 298 | +```rgbasm |
| 299 | + ld a, SCF_SOURCE |
| 300 | + ldh [rSC], a |
| 301 | +``` |
| 302 | + |
| 303 | +/// do handshakey thing? |
| 304 | +/// whoever presses KEY attempts to do a transfer as the clock provider |
| 305 | +```rgbasm |
| 306 | +``` |
| 307 | + |
| 308 | +--- |
0 commit comments