Skip to content

Commit 7c1edc1

Browse files
committed
Add Serial Link lesson (draft/WIP)
- work-in-progress lesson text - most of core impl covered - sio.asm: anchors and minor changes for tutorial friendliness
1 parent 431baff commit 7c1edc1

File tree

3 files changed

+402
-92
lines changed

3 files changed

+402
-92
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- [Input](part2/input.md)
3030
- [Collision](part2/collision.md)
3131
- [Bricks](part2/bricks.md)
32+
- [Serial Link](part2/serial-link.md)
3233
- [Work in progress](part2/wip.md)
3334

3435
# Part III — Our second game

src/part2/serial-link.md

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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

Comments
 (0)