Skip to content

Commit 66102f2

Browse files
authored
Fix part2/serial-link integration (#145)
1 parent 1fcf7f8 commit 66102f2

File tree

2 files changed

+184
-141
lines changed

2 files changed

+184
-141
lines changed

src/part2/serial-link.md

Lines changed: 140 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ This is a simple sequence that checks that there is a connection and tests that
284284
The handshake can be performed in one of two roles: *A* or *B*.
285285
To be successful, one peer must be *A* and the other must be *B*.
286286
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.
287+
The handshake then involves a number of exchanges, with each peer sending a certain value that the other expects.
288288
If an unexpected value is received, or something goes wrong with the transfer, that handshake is rejected.
289289

290290

@@ -369,18 +369,24 @@ It's time to implement the protocol and build the application-level features on
369369
<!-- Link defs -->
370370
At the top of main.asm, define the constants for keeping track of Link's state:
371371

372-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-defs}}
373-
{{#include ../../unbricked/serial-link/main.asm:serial-demo-defs}}
372+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-defs}}
373+
{{#include ../../unbricked/serial-link/main.asm:link-defs}}
374374
```
375375

376376
<!-- Link state -->
377377
We'll need some variables in WRAM to keep track of things.
378378
Add a section at the bottom of main.asm:
379379

380-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-wram}}
381-
{{#include ../../unbricked/serial-link/main.asm:serial-demo-wram}}
380+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-state}}
381+
{{#include ../../unbricked/serial-link/main.asm:link-state}}
382382
```
383383

384+
- these will make more sense as we use them, but ...
385+
- `wLink` holds the state/status of the Link feature itself.
386+
- the constants prefixed with `LINK_` correspond to `wLink`
387+
- `wShakeFailed` is used to indicate handshake failure, and to delay (re-)connection attempts
388+
389+
<!-- FIX: Doesn't match the unbricked link feature.
384390
`wLocal` and `wRemote` are two identical structures for storing the Link state information of each peer.
385391
- `state` holds the current mode and some flags (the `LINKST_` constants)
386392
- `tx_id` & `rx_id` are for the IDs of the most recently sent & received `MSG_DATA` message
@@ -389,32 +395,24 @@ The contents of application data messages (`MSG_DATA` only) will be stored in th
389395
390396
`wAllowTxAttempts` is the number of transmission attempts remaining for each DATA message.
391397
`wAllowRxFaults` is the "budget" of delivery faults allowed before causing an error.
398+
-->
392399

393400

394401
### LinkInit
395-
Lots of variables means lots of initialisation so let's add a function for that:
402+
We're going to add quite a few functions for the new link feature and they'll all be prefixed with `Link`.
403+
To keep things organised, add a new `ROM0` section for the `Link` implementation:
396404

397-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-init}}
398-
{{#include ../../unbricked/serial-link/main.asm:link-init}}
405+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl}}
406+
{{#include ../../unbricked/serial-link/main.asm:link-impl}}
399407
```
400408

401-
This initialises Sio by calling `SioInit` and then enables something called the serial interrupt which will be explained soon.
402-
Execution continues into `LinkReset`.
403-
404-
`LinkReset` can be called to reset the whole Link feature if something goes wrong.
405-
This resets Sio and then writes default values to all the variables we defined above.
406-
Finally, a function called `HandshakeDefault` is jumped to and for that one you'll have to wait a little bit!
407-
408-
Make sure to call the init routine once before the main loop starts:
409+
First things first: we need to initialise the variables we created, as well as Sio, so create the `LinkInit` function:
409410

410-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-init-callsite}}
411-
{{#include ../../unbricked/serial-link/main.asm:serial-demo-init-callsite}}
411+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-init}}
412+
{{#include ../../unbricked/serial-link/main.asm:link-impl-init}}
412413
```
413414

414-
We'll also add a utility function for handling errors:
415-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-error-stop}}
416-
{{#include ../../unbricked/serial-link/main.asm:link-error-stop}}
417-
```
415+
After calling `SioInit` this enables something called the *serial interrupt* by setting the associated bit (`IEF_SERIAL`) of the `rIE` register.
418416

419417

420418
### Serial Interrupt
@@ -454,156 +452,180 @@ If you would like to continue digging, have a look at [evie's interrupts tutoria
454452

455453

456454
### LinkUpdate
457-
`LinkUpdate` is the main per-frame update function.
455+
`LinkUpdate` is the main per-frame update function for the link feature.
458456

459-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-update}}
460-
{{#include ../../unbricked/serial-link/main.asm:link-update}}
457+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-update}}
458+
{{#include ../../unbricked/serial-link/main.asm:link-impl-update}}
461459
```
462460

463-
The order of each part of this is important -- note the many (conditional) places where execution can exit this procedure.
461+
To follow the code here, it helps to see the whole thing as a state machine.
464462

465-
Check input before anything else so the user can always reset the demo.
463+
- `LINK_ENABLE` flag controls the entire feature
464+
- if its not set, do nothing
465+
- update Sio:
466+
- `SioTick` needs to be called regularly, so we do that here
467+
- check `wSioState` -- if Sio is waiting for a transfer to complete, we wait too.
468+
- the `LINK_CONNECTED` flag is set once we've successfully performed a handshake
469+
- jump to `.conn_up:` if the flag is set
470+
- check `wSioState` to decide what to do
471+
- the implementation of each of these functions is below
472+
- otherwise, continue into `.conn_shake:` to perform a handshake
473+
- `wShakeFailed` is set non-zero when a handshake fails -- the value acts as a countdown timer to delay retry attempts
474+
- decrement it (`dec a`) and store the new value
475+
- if the new value is zero, jump to `LinkStart` to try again
476+
- if `wShakeFailed` is zero, a handshake attempt is already underway
477+
- check `wSioState` to decide what to do
478+
- the implementation of each of these functions is below
466479

467-
The `LINKST_MODE_ERROR` mode is an unrecoverable error state that can only be exited via the reset.
468-
To check the current mode, read the `wLocal.state` byte and use `and a, LINKST_MODE` to keep just the mode bits.
469-
There's nothing else to do in the `LINKST_MODE_ERROR` mode, so simply return from the function if that's the case.
470480

471-
Update Sio by calling `SioTick` and then call a specific function for the current mode.
481+
#### LinkPacketRx
482+
`LinkPacketRx` is used to check for and validate received packets from any state.
472483

473-
`LINKST_MODE_CONNECT` manages the handshake process.
474-
Update the handshake if it's incomplete (`wHandshakeState` is non-zero).
475-
Otherwise, transition to the active connection mode.
484+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-packet-rx}}
485+
{{#include ../../unbricked/serial-link/main.asm:link-impl-packet-rx}}
486+
```
476487

477-
`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.
488+
The first thing to do is flush Sio's state (set it to `SIO_IDLE`) to indicate that the received data has been processed.
489+
Technically the data hasn't actually been processed yet, but this is a promise to do that!
478490

491+
Check that a packet was received and that it arrived intact by calling `SioPacketRxCheck`.
492+
Return here if Sio's checks failed.
479493

480-
### LinkTx
481-
`LinkTx` builds the next message packet and starts transferring it.
494+
The last part checks that the received packet count matches the local one in `wLinkPacketCount`.
495+
This is done to check that both peers are in sync.
482496

483-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-message}}
484-
{{#include ../../unbricked/serial-link/main.asm:link-send-message}}
485-
```
497+
Note that `LinkPacketRx` uses the zero flag to return a status code.
486498

487-
There's two types of message that are sent while the link is active -- SYNC and DATA.
488-
The `LINKST_STEP_SYNC` flag is used to alternate between the two types and ensure at least every second message is a SYNC.
489-
A DATA message will only be sent if the `LINKST_STEP_SYNC` flag is clear and the `LINKST_TX_ACT` flag is set.
499+
:::tip
490500

491-
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`.
501+
Actually we test against `wLinkPacketCount` minus one (`dec a`) because the value stored is the number of packets *sent*.
492502

493-
To make sending DATA messages more convenient, add a utility function to take care of the details:
494-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-tx-start}}
495-
{{#include ../../unbricked/serial-link/main.asm:link-tx-start}}
496-
```
503+
:::
497504

498505

499-
### LinkRx
500-
When a transfer has completed (`SIO_DONE`), process the received data in `LinkRx`:
506+
#### Sending messages
507+
`LinkShakeTx` and `LinkGameTx` are quite simple and work in the same way.
501508

502-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-receive-message}}
503-
{{#include ../../unbricked/serial-link/main.asm:link-receive-message}}
509+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-shake-tx}}
510+
{{#include ../../unbricked/serial-link/main.asm:link-impl-shake-tx}}
504511
```
505512

506-
The first thing to do is flush Sio's state (set it to `SIO_IDLE`) to indicate that the received data has been processed.
507-
Technically the data hasn't actually been processed yet, but this is a promise to do that!
513+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-game-tx}}
514+
{{#include ../../unbricked/serial-link/main.asm:link-impl-game-tx}}
515+
```
508516

509-
Check that a packet was received and that it arrived intact by calling `SioPacketRxCheck`.
510-
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.
517+
To send a packet:
518+
1. `call SioPacketTxPrepare`,
519+
2. write the data to the packet buffer (`HL` was set by Sio),
520+
3. `call SioPacketTxFinalise`.
511521

512-
<!-- Faults -->
513-
If the result of `SioPacketRxCheck` was negative, or the message type is unrecognised, it's considered a delivery *fault*.
514-
In case of a fault, the received data is discarded and the fault counter is updated.
515-
The fault counter state is loaded from `wAllowRxFaults`.
516-
If the value of the counter is zero (i.e. there's zero (more) faults allowed) the error mode is acivated.
517-
If the value of the counter is more than zero, it's decremented and saved.
522+
The contents of the packet
523+
- the packet sequence ID / count (value of `wLinkPacketCount`)
524+
- required to pass the check in `LinkPacketRx`
525+
- one of the `MSG_*` constants
526+
- message-specific data, if any
527+
- `MSG_GAME` includes the local score (`wScore`)
528+
- `MSG_SHAKE` has none
518529

519-
<!-- SYNC -->
520-
`MSG_SYNC` messages contain the sender's Link state, so first we copy the received data to `wRemote`.
521-
Now we want to check if the remote peer has acknowledged delivery of a message sent to them.
522-
Copy the new `wRemote.rx_id` value to register `B`, then load `wLocal.state` and copy it into register `C`
523-
Check the `LINKST_TX_ACT` flag (using the `and` instruction) and return if it's not set.
524-
Otherwise, an outgoing message has not been acknowledged yet, so load `wLocal.tx_id` and compare it to `wRemote.rx_id` (in register `B`).
525-
If the two are equal that means the message was delivered, so clear the `LINKST_TX_ACT` flag and update `wLocal.state`.
526530

527-
<!-- DATA -->
528-
Receiving `MSG_DATA` messages is straightforward.
529-
The first byte is the message ID, so copy that from the packet to `wLocal.rx_id`.
530-
The rest of the packet data is copied straight to the `wRxData` buffer.
531-
Finally, a flag is set to indicate that data was newly received.
531+
#### Completing the handshake
532+
`LinkShakeRx` is responsible for completing the handshake.
532533

534+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-shake-rx}}
535+
{{#include ../../unbricked/serial-link/main.asm:link-impl-shake-rx}}
536+
```
533537

534-
### Main
538+
- `LinkPacketRx`
539+
- check that received MSG_SHAKE
540+
- handshake is complete when `wLinkPacketCount` reaches three
541+
- as in 3 handshake packets have been sent & received successfully
542+
- set the `LINK_CONNECTED` flag if handshake is complete
535543

536-
Demo update routine:
537-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-update}}
538-
{{#include ../../unbricked/serial-link/main.asm:serial-demo-update}}
539-
```
540544

541-
Call the update routine from the main loop:
542-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-demo-update-callsite}}
543-
{{#include ../../unbricked/serial-link/main.asm:serial-demo-update-callsite}}
545+
#### Handshake failure
546+
`LinkShakeFail` ends the handshake attempt in failure.
547+
This is called when a Sio transfer fails during the handshake and when an invalid handshake message is received.
548+
549+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-shake-fail}}
550+
{{#include ../../unbricked/serial-link/main.asm:link-impl-shake-fail}}
544551
```
545552

553+
Set `wShakeFailed` to a non-zero to indicate failure.
554+
The value used depends on the clock source setting of the serial port.
546555

547-
### Implement the handshake protocol
556+
- this is part of the automatic role selection strategy
557+
- because the clock provider transfers will occur immediately...
558+
- makes it more likely that (after a failed handshake) the externally clocked device will enable its serial port before the clock provider does.
548559

549-
/// Establish contact by trading magic numbers
550560

551-
/// Define the codes each device will send:
552-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-codes}}
553-
{{#include ../../unbricked/serial-link/main.asm:handshake-codes}}
554-
```
561+
#### LinkGameRx
555562

556-
///
557-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-state}}
558-
{{#include ../../unbricked/serial-link/main.asm:handshake-state}}
563+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-game-rx}}
564+
{{#include ../../unbricked/serial-link/main.asm:link-impl-game-rx}}
559565
```
560566

561-
/// Routines to begin handshake sequence as either the internally or externally clocked device.
562567

563-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-begin}}
564-
{{#include ../../unbricked/serial-link/main.asm:handshake-begin}}
565-
```
568+
#### LinkStop
566569

567-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-update}}
568-
{{#include ../../unbricked/serial-link/main.asm:handshake-update}}
570+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-stop}}
571+
{{#include ../../unbricked/serial-link/main.asm:link-impl-stop}}
569572
```
570573

571-
The handshake can be forced to restart in the clock provider role by pressing START.
572-
This is included as a fallback and manual override for the automatic role selection implemented below.
573574

574-
If a transfer is completed, process the received data by jumping to `HandshakeMsgRx`.
575+
#### LinkStart
576+
`LinkStart` starts a new handshake attempt.
575577

576-
If the serial port is otherwise inactive, (re)start the handshake.
577-
To automatically determine which device should be the clock provider, we check the lowest bit of the DIV register.
578-
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.
579-
580-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-xfer-complete}}
581-
{{#include ../../unbricked/serial-link/main.asm:handshake-xfer-complete}}
578+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-impl-start}}
579+
{{#include ../../unbricked/serial-link/main.asm:link-impl-start}}
582580
```
583581

584-
Check that a packet was received and that it contains the expected handshake value.
585-
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.
586-
If all is well, decrement the `wHandshakeState` counter.
587-
If the counter is zero, there is nothing left to do.
588-
Otherwise, more exchanges are required so start the next one immediately.
582+
The handshake can be forced to restart in the clock provider role by holding START.
583+
This is included as a fallback and manual override for the automatic role selection described below.
589584

590-
:::tip
585+
To automatically determine which device should be the clock provider, we could use a random number generator, but we don't have one, so we'll just check the lowest bit of the DIV register.
586+
The value in DIV is automatically incremented at around 16 kHz, which is not at all random, but all we really need is a single bit that's unlikely to be the same as the one on the remote device.
591587

592-
This is a trivial example of a handshake protocol.
593-
In a real application, you might want to consider:
594-
- using a longer sequence of codes as a more unique app identifier
595-
- sharing more information about each device and negotiating to decide the preferred clock provider
588+
:::tip DIV is a Pretend Random Number Generator
589+
590+
Not to be confused with a [Pseudorandom Number Generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator).
596591

597592
:::
598593

599594

595+
### Finally!
596+
To integrate the link feature, make some changes to the main loop and entry point:
597+
598+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-main}}
599+
{{#include ../../unbricked/serial-link/main.asm:link-main}}
600+
```
601+
602+
- Call `LinkInit` at startup, just before the `Main:` loop.
603+
- In the main loop,
604+
- call `LinkUpdate`
605+
- `ei`/`di` & code to update the display
606+
- display remote score, & a serial port status icon
607+
- check `wLink` status & skip ball update if not connected
608+
- freezes the game if not connected
609+
610+
Copy this function, which is used to to display the remote score (which is a BCD number).
611+
You don't need to pay attention to this, it just adapts printing code from the BCD lesson.
612+
613+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-print-bcd}}
614+
{{#include ../../unbricked/serial-link/main.asm:link-print-bcd}}
615+
```
616+
Copy these new tiles to the end of the tile data -- they should be immediately after the digits, right before `TilesEnd`.
617+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-tiles}}
618+
{{#include ../../unbricked/serial-link/main.asm:link-tiles}}
619+
```
600620

601-
## /// Running the test ROM
602621

603-
/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different:
622+
## Running the test ROM
623+
Because we have an extra file (sio.asm) to compile now, the build commands will look a little different:
624+
604625
```console
605626
$ rgbasm -o sio.o sio.asm
606627
$ rgbasm -o main.o main.asm
607628
$ rgblink -o unbricked.gb main.o sio.o
608629
$ rgbfix -v -p 0xFF unbricked.gb
609630
```
631+

0 commit comments

Comments
 (0)