Skip to content

Commit 63e5ea1

Browse files
committed
Update part2/serial-link lesson
1 parent 5649f40 commit 63e5ea1

File tree

2 files changed

+103
-126
lines changed

2 files changed

+103
-126
lines changed

src/part2/serial-link.md

Lines changed: 96 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,9 @@
33
---
44

55
**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
6+
- learn how to control the Game Boy serial port from code
7+
- build a wrapper over the low-level serial port interface
8+
- implement high-level features to enable reliable data transfers
189

1910
---
2011

@@ -34,21 +25,18 @@ So if you're connecting a DMG with a later model, make sure you have an adapter
3425
:::tip Can I just use an emulator?
3526

3627
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... -->
3928

4029
:::
4130

4231

4332
## The Game Boy serial port
4433

45-
---
34+
:::tip Information overload
4635

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)
36+
This section is intended as a reasonably complete description of the Game Boy serial port, from a programming perspective.
37+
There's a lot of information packed in here and you don't need to absorb it all to continue.
5038

51-
---
39+
:::
5240

5341
Communication via the serial port is organised as discrete data transfers of one byte each.
5442
Data transfer is bidirectional, with every bit of data written out matched by one read in.
@@ -71,26 +59,15 @@ While the serial port is *active*, it sends and receives a data bit on each seri
7159
After 8 pulses (*8 bits!*) the transfer is complete -- the serial port deactivates itself, and the serial interrupt is requested.
7260
Normal execution continues while the serial port is active: the transfer will be performed independently of the program code.
7361

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-
8562

8663
## Sio
87-
Let's start building **Sio**, a serial I/O guy.
88-
89-
---
9064

91-
**TODO:** Create a file, sio.asm? (And complicate the build process) ... Just stick it in main.asm?
65+
<!-- /// Connectivity features tend to have a multiplicative effect on a developer's workload, particularly in testing and debugging.
66+
/// This is why we're going to work outside of the unbricked project for a while.
67+
/// create a separate folder for this program? -->
9268

93-
---
69+
/// Let's start building **Sio**, our serial I/O system.
70+
/// Create a new source file called `sio.asm`. ... reuseable code ... & avoid gigantic `main.asm`
9471

9572
First, define the constants that represent Sio's main states/status:
9673

@@ -266,68 +243,53 @@ To do this we'll use the serial interrupt:
266243
{{#include ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}}
267244
```
268245

269-
**TODO:** explain something about interrupts? but don't be weird about it, I guess...
270-
271-
---
246+
All this short routine really *does* is `call SioPortEnd`.
247+
But because this is an interrupt handler we have to do a little dance to prevent *bad things* from happening.
248+
We need to preserve the values in the registers that will be modified by `SioPortEnd`, `af` and `hl`.
249+
/// make sure the registers are in the same state as when the interrupt occured so that when the interrupted code is resumed, it can continue as if nothing happened.
250+
*The stack* is the perfect way to do this.
251+
`push` copies the value from the register to the top of the stack and `pop` takes the top value off of the stack and moves it to the register.
252+
Note that a stack is a first-in first-out (FIFO) container, so we push `af` then `hl` -- leaving `hl` on the top -- and pop `hl` then `af`.
272253

254+
:::tip We interrupt this broadcast to briefly explain what interrupts are
273255

274-
### A little protocol
275-
276-
Before diving into implementation, let's take a minute to describe a *protocol*.
277-
A protocol is a set of rules that govern communication.
278-
279-
The most critical communications are those that support the application's features, which we'll call *messages*.
280-
281-
/// Transmission errors: do not want. Transmission errors: cannot be eliminated.
282-
/// Lots of possible ways to deal with damaged message packets.
283-
/// Need to *detect* errors before you can deal with them.
284-
285-
There's always a possibility that a message will be damaged in transmission or even due to a bug.
286-
The most important step to take in dealing with this reality is *detection* -- the application needs to know if a message was delivered successfully (or not).
287-
To check that a message arrived intact, we'll use checksums.
288-
Every packet sent will include a checksum of itself.
289-
At the receiving end, the checksum can be computed again and checked against the one sent with the packet.
290-
291-
:::tip Checksums, a checksummary
292-
293-
A checksum is a computed value that depends on the value of some *input data*.
294-
In our case, the input data is all the bytes that make up a packet.
295-
In other words, every byte of the packet influences the sum.
296-
297-
<!-- A checksum of a packet can be sent alongside the packet, which the receiver can use to check if the packet arrived intact. -->
298-
The packet includes a field for such a checksum, which is initialised to `0`.
299-
The checksum is computed using the whole packet -- including the zero -- and the result is written to the checksum field.
300-
When the packet checksum is recomputed now, the result will be zero!
301-
This is a common feature of popular checksums because it makes checking if data is intact so simple.
256+
An interrupt is a way to run a certain piece of code when an external event occurs.
257+
Interrupts are requested by *peripherals* (hardware connected to the CPU) and the CPU literally interrupts whatever it was doing to go an execute some different code instead.
258+
In this case, the event is the serial port counting to eight (meaning a whole byte was transferred), and the code that will be executed is whatever is at memory address `$58` -- the routine above.
302259

303260
:::
304261

305-
Checking the packet checksum will indicate if the message was damaged, but only the receiver will have this information.
306-
To inform the sender we'll make a rule that every message transfer must be followed by a delivery *report*.
307-
In terms of information, a report is a boolean value -- either the message was received intact, or not.
308262

309-
Because reports are so simple -- but very important -- we'll employ a simple technique to deliver them reliably.
310-
Define two magic numbers -- one to send when the packet checksum matched and another for if it didn't.
311-
For this tutorial we'll use `DEF STATUS_OK EQU $11` for *success* and flip every bit, giving `DEF STATUS_ERROR EQU $EE` to mean *failed*.
263+
### A little protocol
264+
265+
Before diving into (more) implementation, let's take a minute to describe a *protocol*.
266+
A protocol is a set of rules that govern communication.
312267

313-
To increase the likelihood of the report getting interpreted correctly, we'll simply repeat the value multiple times.
314-
At the receiving end, check each received byte -- finding just one byte equal to `STATUS_OK` will be interpreted as *success*.
268+
/// Everything is packets.
269+
/// Packets: small chunks of data with rudimentary error detection
315270

316-
:::tip
271+
For reliable data transfer, alternate between two message types:
272+
protocol metadata in *SYNC* packets, and application data in *DATA* packets.
273+
DATA packets are required to include a sequential message ID -- the rest is up to the application.
274+
SYNC packets include the ID of the most recently received DATA packet.
317275

318-
The binary values used here should be far apart in terms of [*hamming distance*](https://en.wikipedia.org/wiki/Hamming_distance).
319-
In essence, either value should be very hard to confuse for the other, even if some bits were randomly changed.
276+
Packet integrity can be tested on the receiving end using a checksum.
277+
Damaged packets of either type are discarded.
278+
Successful delivery of a DATA packet is acknowledged when its ID is included in a SYNC packet.
279+
The sender should retransmit the packet if successful delivery is not acknowledged.
320280

321-
:::
281+
There's one more thing our protocol needs: some way to get both devices on the same page and kick things off.
282+
We need a *handshake* that must be completed before doing anything else.
283+
This is a simple sequence that checks that there is a connection and tests that the connection is working.
284+
The handshake can be performed in one of two roles: *A* or *B*.
285+
To be successful, one peer must be *A* and the other must be *B*.
286+
Which role to perform is determined by the clock source setting of the serial port.
287+
In each exchange, each peer sends a number associated with its role and expects to receive a number associated with the other role.
288+
If an unexpected value is received, or something goes wrong with the transfer, that handshake attempt is aborted.
322289

323-
<!-- PROTOCOL RULES
324-
- two communication "channels": application (messages) & meta (reports)
325-
- message packet includes a checksum of itself to be validated by receiver
326-
- every message packet is followed by a delivery report
327-
- message begins with 'MessageType' code -->
328290

291+
### /// SioPacket
329292

330-
### SioPacket
331293
We'll implement some functions to facilitate constructing, sending, receiving, and checking packets.
332294
The packet functions will operate on the existing serial data buffers.
333295

@@ -382,7 +344,7 @@ It's probably not very suitable for real-world projects.
382344
:::
383345

384346

385-
## Using Sio
347+
## /// Using Sio
386348

387349
/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different:
388350
```console
@@ -423,7 +385,7 @@ Other interrupts may be in use by other parts of the code, which are clearly out
423385
:::
424386

425387
Note that `LinkReset` starts part way through `LinkInit`.
426-
This way the two functions can share code without zero overhead and `LinkReset` can be called without performing the startup initialisation again.
388+
This way the two functions can share code with zero overhead and `LinkReset` can be called without performing the startup initialisation again.
427389
This pattern is often referred to as *fallthrough*: `LinkInit` *falls through* to `LinkReset`.
428390

429391
Call the init routine once before the main loop starts:
@@ -432,41 +394,61 @@ Call the init routine once before the main loop starts:
432394
call LinkInit
433395
```
434396

397+
/// `LinkTx`, alternate between sending the two types of packet:
435398

436-
### Link impl go
437-
438-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-status}}
439-
{{#include ../../unbricked/serial-link/main.asm:link-send-status}}
440-
```
441-
442-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-test-data}}
443-
{{#include ../../unbricked/serial-link/main.asm:link-send-test-data}}
399+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-message}}
400+
{{#include ../../unbricked/serial-link/main.asm:link-send-message}}
444401
```
445402

446-
<!-- LinkUpdate -->
447403
/// Implement `LinkUpdate`:
448404

449405
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-update}}
450406
{{#include ../../unbricked/serial-link/main.asm:link-update}}
451407
```
452408

453-
/// update Sio every frame...
409+
/// reset the demo if the B button is pressed
410+
411+
/// update Sio every frame
412+
413+
In the `LINK_INIT` state, do handshake until its done.
414+
Once the handshake is complete, change to the `LINK_UP` state.
415+
416+
/// In the `LINK_UP` state, ...
417+
418+
When a transfer has completed (`SIO_DONE`), process the received data in `LinkRx`:
419+
420+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-receive-message}}
421+
{{#include ../../unbricked/serial-link/main.asm:link-receive-message}}
422+
```
423+
424+
/// We flush Sio's state (set it to `SIO_IDLE`) here so ... LinkTx ... next update...
425+
426+
Check that we received a packet and its checksum matches.
427+
If the packet integrity test comes back negative, increment the inbound transmission errors counter.
428+
429+
:::tip
430+
431+
/// This class of error -- which we're calling a *transmission errors* -- are very significant.
432+
/// assuming code works -- & have reason to believe it does -- this means the data was damaged during transmission, if a valid packet was sent
454433

455-
/// in the `INIT` state, do handshake until its done.
434+
/// You might want to do something a bit more sophisticated than counting errors in a real-world application.
456435

457-
Once the handshake is complete, change to the `READY` state and notify the other device.
436+
:::
458437

459-
/// in any of the other active states, reset if the B button is pressed
438+
/// with the packet checking out OK, move on to process the message it contains
439+
/// jump to the appropriate handler ...
440+
/// if the msg type is unknown/unexpected, increment the message/protocol error counter.
460441

461-
/// finally we jump to different routines based on Sio's transfer state
442+
/// SYNC messages...
462443

444+
---
463445

464-
<!-- handle received message -->
465-
/// **(very) TODO:** handling received messages...
446+
/// TEST_DATA messages...
466447

448+
---
467449

468450

469-
### Handshake
451+
### Implement the handshake protocol
470452

471453
/// Establish contact by trading magic numbers
472454

@@ -486,37 +468,28 @@ Once the handshake is complete, change to the `READY` state and notify the other
486468
{{#include ../../unbricked/serial-link/main.asm:handshake-begin}}
487469
```
488470

489-
/// Every frame, handshake update
490-
491471
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-update}}
492472
{{#include ../../unbricked/serial-link/main.asm:handshake-update}}
493473
```
494474

495-
/// If `wHandshakeState` is zero, handshake is complete
496-
497-
/// If the user has pressed START, abort the current handshake and start again as the clock provider.
498-
499-
/// Monitor Sio. If the serial port is not busy, start the handshake, using the DIV register as a pseudorandom value to decide if we should be the clock or not.
475+
The handshake can be forced to restart in the clock provider role by pressing START.
476+
This is included as a fallback and manual override for the automatic role selection implemented below.
500477

501-
:::tip The DIV register
478+
If a transfer is completed, process the received data by jumping to `HandshakeMsgRx`.
502479

503-
/// is not particularly random...
504-
505-
/// but we just need the value to be different when each device reads it, and for the value to occasionally be an odd number
506-
507-
:::
508-
509-
/// If a transfer is complete (`SIO_DONE`), jump to `HandshakeMsgRx` (described below) to check the received value.
480+
If the serial port is otherwise inactive, (re)start the handshake.
481+
To automatically determine which device should be the clock provider, we check the lowest bit of the DIV register.
482+
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.
510483

511484
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-xfer-complete}}
512485
{{#include ../../unbricked/serial-link/main.asm:handshake-xfer-complete}}
513486
```
514487

515-
/// First byte must be `MSG_SHAKE`
516-
517-
/// Second byte must be `wHandshakeExpect`
518-
519-
/// If the received message is correct, set `wHandshakeState` to zero
488+
Check that a packet was received and that it contains the expected handshake value.
489+
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.
490+
If all is well, decrement the `wHandshakeState` counter.
491+
If the counter is zero, there is nothing left to do.
492+
Otherwise, more exchanges are required so start the next one immediately.
520493

521494
:::tip
522495

unbricked/serial-link/main.asm

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ DEF SHAKE_A EQU $88
4343
; Handshake code sent by externally clocked device
4444
DEF SHAKE_B EQU $77
4545
DEF HANDSHAKE_COUNT EQU 5
46+
DEF HANDSHAKE_FAILED EQU $F0
4647
; ANCHOR_END: handshake-codes
4748

4849

@@ -179,7 +180,7 @@ LinkUpdate:
179180
cp a, SIO_FAILED
180181
jp z, LinkError
181182
cp a, SIO_IDLE
182-
jp z, SendNextMessage
183+
jp z, LinkTx
183184
ret
184185
.link_init
185186
ld a, [wHandshakeState]
@@ -257,7 +258,7 @@ LinkDisplay:
257258

258259

259260
; ANCHOR: link-send-message
260-
SendNextMessage:
261+
LinkTx:
261262
ld hl, wPacketCount
262263
ld a, [hl]
263264
inc [hl]
@@ -286,6 +287,7 @@ SendNextMessage:
286287
; ANCHOR_END: link-send-message
287288

288289

290+
; ANCHOR: link-receive-message
289291
; Process received data
290292
; @mut: AF, BC, HL
291293
LinkRx:
@@ -340,6 +342,7 @@ LinkRx:
340342
ld a, [hl+]
341343
ld [wRxValue], a
342344
ret
345+
; ANCHOR_END: link-receive-message
343346

344347

345348
LinkError:
@@ -748,7 +751,8 @@ HandshakeMsgRx:
748751
jr nz, HandshakeSendPacket
749752
ret
750753
.failed
751-
ld a, $FF
754+
ld a, [wHandshakeState]
755+
or a, HANDSHAKE_FAILED
752756
ld [wHandshakeState], a
753757
ret
754758
; ANCHOR_END: handshake-xfer-complete

0 commit comments

Comments
 (0)