From 431baff4ec7399152cf4b0fab0acce6ec3a58fbb Mon Sep 17 00:00:00 2001 From: quinnyo <3379314+quinnyo@users.noreply.github.com> Date: Sun, 28 Apr 2024 06:34:18 +1000 Subject: [PATCH 1/6] Add core serial IO implementation --- unbricked/serial-link/sio.asm | 283 ++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 unbricked/serial-link/sio.asm diff --git a/unbricked/serial-link/sio.asm b/unbricked/serial-link/sio.asm new file mode 100644 index 00000000..8f1fe472 --- /dev/null +++ b/unbricked/serial-link/sio.asm @@ -0,0 +1,283 @@ +; :::::::::::::::::::::::::::::::::::::: +; :: :: +; :: ______. :: +; :: _ |````` || :: +; :: _/ \__@_ |[- - ]|| :: +; :: / `--<[|]= |[ m ]|| :: +; :: \ .______ | ```` || :: +; :: / !| `````| | + oo|| :: +; :: ( ||[ ^u^]| | .. #|| :: +; :: `-<[|]=|[ ]| `______// :: +; :: || ```` | :: +; :: || + oo| :: +; :: || .. #| :: +; :: !|______/ :: +; :: :: +; :: :: +; :::::::::::::::::::::::::::::::::::::: + +INCLUDE "hardware.inc" + +; Duration of timeout period in ticks. (for externally clocked device) +DEF SIO_TIMEOUT_TICKS EQU 60 + +; Catchup delay duration +DEF SIO_CATCHUP_SLEEP_DURATION EQU 100 + +DEF SIO_CONFIG_INTCLK EQU SCF_SOURCE +DEF SIO_CONFIG_RESERVED EQU $02 +DEF SIO_CONFIG_DEFAULT EQU $00 +EXPORT SIO_CONFIG_INTCLK + +; SioStatus transfer state enum +RSRESET +DEF SIO_IDLE RB 1 +DEF SIO_FAILED RB 1 +DEF SIO_DONE RB 1 +DEF SIO_BUSY RB 0 +DEF SIO_XFER_STARTED RB 1 +EXPORT SIO_IDLE, SIO_FAILED, SIO_DONE +EXPORT SIO_BUSY, SIO_XFER_STARTED + +DEF SIO_BUFFER_SIZE EQU 32 + + +; PACKET + +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 + + +SECTION "SioBufferRx", WRAM0, ALIGN[8] +wSioBufferRx:: ds SIO_BUFFER_SIZE + + +SECTION "SioBufferTx", WRAM0, ALIGN[8] +wSioBufferTx:: ds SIO_BUFFER_SIZE + + +SECTION "SioCore State", WRAM0 +; Sio config flags +wSioConfig:: db +; Sio state machine current state +wSioState:: db +; Number of transfers to perform (bytes to transfer) +wSioCount:: db +wSioBufferOffset:: db +; Timer state (as ticks remaining, expires at zero) for timeouts and delays. +; HACK: this is only "public" (::) for access by debug display code. +wSioTimer:: db + + +SECTION "Sio Serial Interrupt", ROM0[$58] +SerialInterrupt: + jp SioInterruptHandler + + +SECTION "SioCore Impl", ROM0 +; Initialise/reset Sio to the ready to use 'IDLE' state. +; NOTE: Enables the serial interrupt. +; @mut: AF, [IE] +SioInit:: + ld a, SIO_CONFIG_DEFAULT + ld [wSioConfig], a + ld a, SIO_IDLE + ld [wSioState], a + ld a, 0 + ld [wSioTimer], a + ld a, 0 + ld [wSioCount], a + ld [wSioBufferOffset], a + + ; enable serial interrupt + ldh a, [rIE] + or a, IEF_SERIAL + ldh [rIE], a + ret + + +; @mut: AF, HL +SioTick:: + ld a, [wSioState] + cp a, SIO_XFER_STARTED + jr z, .xfer_started + ; anything else: do nothing + ret +.xfer_started: + ld a, [wSioCount] + and a, a + jr nz, :+ + ld a, SIO_DONE + ld [wSioState], a + ret +: + ; 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 + + +; 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 + + +SioInterruptHandler: + push af + push hl + + ; check that we were expecting a transfer (to end) + ld hl, wSioCount + ld a, [hl] + and a + jr z, .end + dec [hl] + ; Get buffer pointer offset (low byte) + ld a, [wSioBufferOffset] + ld l, a + ; Get received value + ld h, HIGH(wSioBufferRx) + ldh a, [rSB] + ; NOTE: incrementing L here + 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 z, .end + ; Next value to send + ld h, HIGH(wSioBufferTx) + ld a, [hl] + ldh [rSB], a + call SioPortStart + +.end: + ld a, SIO_TIMEOUT_TICKS + ld [wSioTimer], a + pop hl + pop af + reti + + +; @mut: AF +SioTransferStart:: + ; TODO: something if SIO_BUSY ...? + + ld a, SIO_BUFFER_SIZE + ld [wSioCount], a + ld a, 0 + ld [wSioBufferOffset], a + + ; set the clock source (do this first & separately from starting the transfer!) + ld a, [wSioConfig] + and a, SCF_SOURCE ; the sio config byte uses the same bit for the clock source + ldh [rSC], a + ; reset timeout + ld a, SIO_TIMEOUT_TICKS + ld [wSioTimer], a + ; send first byte + ld a, [wSioBufferTx] + ldh [rSB], a + call SioPortStart + ld a, SIO_XFER_STARTED + ld [wSioState], a + ret + + +; @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 + ret + + +SECTION "SioPacket Impl", ROM0 +; 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 + + +; @mut: AF, C, HL +SioPacketTxFinalise:: + ld hl, wSioBufferTx + call SioPacketChecksum + ld [wSioBufferTx + 1], a + ret + + +; @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 + ret ; F.Z already set (or not) + + +; 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 From 7c1edc169c1eadc6be8153d4639d3270bc5ad35d Mon Sep 17 00:00:00 2001 From: Quinn <3379314+quinnyo@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:04:20 +1000 Subject: [PATCH 2/6] 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 --- src/SUMMARY.md | 1 + src/part2/serial-link.md | 308 ++++++++++++++++++++++++++++++++++ unbricked/serial-link/sio.asm | 185 ++++++++++---------- 3 files changed, 402 insertions(+), 92 deletions(-) create mode 100644 src/part2/serial-link.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 80d15ab1..5e2f9fa7 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -29,6 +29,7 @@ - [Input](part2/input.md) - [Collision](part2/collision.md) - [Bricks](part2/bricks.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..81ce34ed --- /dev/null +++ b/src/part2/serial-link.md @@ -0,0 +1,308 @@ +# Serial Link + +--- + +**TODO:** In this lesson... +- learn about the Game Boy serial port... + - how it works, how to use it + - pitfalls and challenges +- build a thing, Sio Core: + - multibyte + convenience wrapper over GB serial + - incl. sync catchup delays, timeouts +- do something with Sio: + - integrate/use Sio + - ? manually choose clock provider + - ? send some data ... +- ? build a thing, 'Packets': + - adds data integrity test with simple checksum + +--- + + +## Running the code +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. +There are no special cartridge requirements -- the most basic ROM-only carts will work. + +You can use any combination of Game Boy models, *provided you have the appropriate cable/adapter to connect them*. +The only thing to look out for is that a different (smaller) connector was introduced with the MGB. +So if you're connecting a DMG with a later model, make sure you have an adapter or a cable with both connectors. + + + + + +:::tip Can I just use an emulator? + +Emulators should not be relied upon as a substitute for the real thing, especially when working with the serial port. + + + +::: + + +## The Game Boy serial port + +--- + +**TODO:** about this section +- this section = crash course on GB serial port theory and operation +- programmer's mental model (not a description of the hardware implementation) + +--- + +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. + +--- + +**TODO:** something about the challenges posed... +- GB serial is not "unreliable"... But it's also "not reliable"... +- some notable things for reliable communication that GB doesn't provide: + - connection detection, status: can't be truly solved in software, work around with error detection + - delivery report / ACK: software can make improvements with careful design + - error detection: software implementation can be effective + +--- + + +## Sio +Let's start building **Sio**, a serial I/O guy. + +--- + +**TODO:** Create a file, sio.asm? (And complicate the build process) ... Just stick it in main.asm? + +--- + +First, define the constants that represent Sio's main states/status: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-status-enum}} +{{#include ../../unbricked/serial-link/sio.asm:sio-status-enum}} +``` + +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}} +``` + +We'll discuss each of these variables as we build the features that use them. + +Add a new code section and an init routine: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-impl-init}} +{{#include ../../unbricked/serial-link/sio.asm:sio-impl-init}} +``` + + +### 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). +Both buffers will be the same size, which is set via a constant: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffer-defs}} +{{#include ../../unbricked/serial-link/sio.asm:sio-buffer-defs}} +``` + +:::tip + +Blocks of memory can be allocated using `ds N`, where `N` is the size of the block in bytes. +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. + +::: + +Define the buffers, each in its own WRAM section: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-buffers}} +{{#include ../../unbricked/serial-link/sio.asm:sio-buffers}} +``` + +:::tip ALIGN + +For the purpose of this lesson, `ALIGN[8]` causes the section to start at an address with a lower byte of zero. +The reason that these sections are *aligned* like this is explained below. + +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. + +::: + +Each buffer is aligned to start at an address with a low byte of zero. +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`. + +The variable `wSioBufferOffset` holds the current location within *both* data buffers and can be used as an offset/index and directly in a pointer. + +The result is a significant reduction in the amount of work required to access the data and manipulate offsets of both 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 `SioTick` to update the timeout and `SioAbort` to cancel the ongoing transfer: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-tick}} +{{#include ../../unbricked/serial-link/sio.asm:sio-tick}} +``` + +Check that a transfer has been started, and that the clock source is set to *external*. +Before *ticking* the timer, check that the timer hasn't already expired with `and a, a`. +Do nothing if the timer value is already zero. +Decrement the timer and save the new value before jumping to `SioAbort` if new value is zero. + + +The last part of the core implementation handles the end of a transfer: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-port-end}} +{{#include ../../unbricked/serial-link/sio.asm:sio-port-end}} +``` + +--- + +**TODO:** walkthrough SioPortEnd + +this one is a little bit more involved... + +- check that Sio is in the **ACTIVE** state before continuing +- use `ld a, [hl+]` to access `wSioState` and advance `hl` to `wSioCount` +- update `wSioCount` using `dec [hl]` + - which you might not have seen before? + - this works out a bit faster than reading number into `a`, decrementing it, storing it again + +- 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. + +- construct a buffer Rx pointer using `wSioBufferOffset` + - load the value from wram into the `l` register + - load the `h` register with the constant high byte of the buffer Rx address space + +- grab the received value from `rSB` and copy it to the buffer Rx + - we need to increment the buffer offset ... + - `hl` is incremented here but we know only `l` will be affected because of the buffer alignment + - the updated buffer pointer is stored + +- now we check the transfer count remaining + - the `z` flag was updated by the `dec` instruction earlier -- none of the instructions in between modify the flags. + +- if the count is more than zero (i.e. more bytes to transfer) start the next byte transfer + - 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. + - load the next tx value into `rSB` and activate the serial port! + +- otherwise the count is zero, we just completed the final byte transfer, so set `SIO_DONE` and return. + +--- + +`SioPortEnd` must be called once after each byte transfer. +To do this we'll use the serial interrupt: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}} +{{#include ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}} +``` + +--- + +**TODO:** explain something about interrupts? but don't be weird about it, I guess... + +--- + + +## Using Sio + +--- + +**TODO:** + +/// initialise Sio +Before doing anything else with Sio, `SioInit` needs to be called. + +```rgbasm + call SioInit + + ; enable interrupts! + ei +``` + +/// update Sio every frame... +```rgbasm + call SioTick +``` + +/// set clock source +```rgbasm + ld a, SCF_SOURCE + ldh [rSC], a +``` + +/// do handshakey thing? +/// whoever presses KEY attempts to do a transfer as the clock provider +```rgbasm +``` + +--- diff --git a/unbricked/serial-link/sio.asm b/unbricked/serial-link/sio.asm index 8f1fe472..6b8284c5 100644 --- a/unbricked/serial-link/sio.asm +++ b/unbricked/serial-link/sio.asm @@ -16,79 +16,87 @@ ; :: :: ; :::::::::::::::::::::::::::::::::::::: +; ANCHOR: sio-status-enum INCLUDE "hardware.inc" -; Duration of timeout period in ticks. (for externally clocked device) +DEF SIO_IDLE EQU $00 +DEF SIO_DONE EQU $01 +DEF SIO_FAILED EQU $02 +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 100 +; ANCHOR_END: sio-catchup-duration +; ANCHOR_END: sio-port-start-defs -DEF SIO_CONFIG_INTCLK EQU SCF_SOURCE -DEF SIO_CONFIG_RESERVED EQU $02 -DEF SIO_CONFIG_DEFAULT EQU $00 -EXPORT SIO_CONFIG_INTCLK - -; SioStatus transfer state enum -RSRESET -DEF SIO_IDLE RB 1 -DEF SIO_FAILED RB 1 -DEF SIO_DONE RB 1 -DEF SIO_BUSY RB 0 -DEF SIO_XFER_STARTED RB 1 -EXPORT SIO_IDLE, SIO_FAILED, SIO_DONE -EXPORT SIO_BUSY, SIO_XFER_STARTED - +; ANCHOR: sio-buffer-defs +; Allocated size in bytes of the Tx and Rx data buffers. DEF SIO_BUFFER_SIZE EQU 32 +; ANCHOR_END: sio-buffer-defs - -; PACKET - +; 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 config flags -wSioConfig:: db ; 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 and delays. -; HACK: this is only "public" (::) for access by debug display code. +; Timer state (as ticks remaining, expires at zero) for timeouts. wSioTimer:: db +; ANCHOR_END: sio-state +; ANCHOR: sio-serial-interrupt-vector SECTION "Sio Serial Interrupt", ROM0[$58] SerialInterrupt: - jp SioInterruptHandler + push af + push hl + call SioPortEnd + pop hl + pop af + reti +; ANCHOR_END: sio-serial-interrupt-vector +; ANCHOR: sio-impl-init SECTION "SioCore Impl", ROM0 ; Initialise/reset Sio to the ready to use 'IDLE' state. ; NOTE: Enables the serial interrupt. ; @mut: AF, [IE] SioInit:: - ld a, SIO_CONFIG_DEFAULT - ld [wSioConfig], a ld a, SIO_IDLE ld [wSioState], a ld a, 0 ld [wSioTimer], a - ld a, 0 ld [wSioCount], a ld [wSioBufferOffset], a @@ -97,23 +105,16 @@ SioInit:: or a, IEF_SERIAL ldh [rIE], a ret +; ANCHOR_END: sio-impl-init -; @mut: AF, HL +; ANCHOR: sio-tick +; Per-frame update +; @mut: AF SioTick:: ld a, [wSioState] - cp a, SIO_XFER_STARTED - jr z, .xfer_started - ; anything else: do nothing - ret -.xfer_started: - ld a, [wSioCount] - and a, a - jr nz, :+ - ld a, SIO_DONE - ld [wSioState], a - ret -: + cp a, SIO_ACTIVE + ret nz ; update timeout on external clock ldh a, [rSC] and a, SCF_SOURCE @@ -136,71 +137,30 @@ SioAbort:: res SCB_START, a ldh [rSC], a ret +; ANCHOR_END: sio-tick -SioInterruptHandler: - push af - push hl - - ; check that we were expecting a transfer (to end) - ld hl, wSioCount - ld a, [hl] - and a - jr z, .end - dec [hl] - ; Get buffer pointer offset (low byte) - ld a, [wSioBufferOffset] - ld l, a - ; Get received value - ld h, HIGH(wSioBufferRx) - ldh a, [rSB] - ; NOTE: incrementing L here - 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 z, .end - ; Next value to send - ld h, HIGH(wSioBufferTx) - ld a, [hl] - ldh [rSB], a - call SioPortStart - -.end: - ld a, SIO_TIMEOUT_TICKS - ld [wSioTimer], a - pop hl - pop af - reti - - -; @mut: AF +; ANCHOR: sio-start-transfer +; Start a whole-buffer transfer. +; @mut: AF, L SioTransferStart:: - ; TODO: something if SIO_BUSY ...? - ld a, SIO_BUFFER_SIZE ld [wSioCount], a ld a, 0 ld [wSioBufferOffset], a - - ; set the clock source (do this first & separately from starting the transfer!) - ld a, [wSioConfig] - and a, SCF_SOURCE ; the sio config byte uses the same bit for the clock source - ldh [rSC], a - ; reset timeout - ld a, SIO_TIMEOUT_TICKS - ld [wSioTimer], a ; send first byte ld a, [wSioBufferTx] ldh [rSB], a - call SioPortStart - ld a, SIO_XFER_STARTED + ld a, SIO_ACTIVE ld [wSioState], a - ret + 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 @@ -217,7 +177,48 @@ SioPortStart: .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 transfer, if there is any. +; To be called after the serial port deactivates itself / 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 From 7eb499fff6bf439d8d233b156c0dbc3dfac16e1f Mon Sep 17 00:00:00 2001 From: quinnyo <3379314+quinnyo@users.noreply.github.com> Date: Tue, 13 Aug 2024 02:59:20 +1000 Subject: [PATCH 3/6] Initial serial link demo code --- src/part2/serial-link.md | 73 +++- unbricked/serial-link/main.asm | 689 +++++++++++++++++++++++++++++++++ unbricked/serial-link/sio.asm | 3 +- 3 files changed, 756 insertions(+), 9 deletions(-) create mode 100644 unbricked/serial-link/main.asm diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 81ce34ed..61693514 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -279,6 +279,22 @@ To do this we'll use the serial interrupt: **TODO:** +/// building serial link test program, separate to unbricked main.asm? + +/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different: +```console +$ rgbasm -L -o sio.o sio.asm +$ rgbasm -L -o main.o main.asm +$ rgblink -o unbricked.gb main.o sio.o +$ rgbfix -v -p 0xFF unbricked.gb +``` + +/// tiles + +/// defs + +/// init/reset + /// initialise Sio Before doing anything else with Sio, `SioInit` needs to be called. @@ -294,15 +310,56 @@ Before doing anything else with Sio, `SioInit` needs to be called. call SioTick ``` -/// set clock source -```rgbasm - ld a, SCF_SOURCE - ldh [rSC], a +--- + +### Handshake + +/// 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}} ``` -/// do handshakey thing? -/// whoever presses KEY attempts to do a transfer as the clock provider -```rgbasm +/// +```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}} +``` + +/// Every frame, handshake update + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-update}} +{{#include ../../unbricked/serial-link/main.asm:handshake-update}} +``` + +/// If `wHandshakeState` is zero, handshake is complete + +/// If the user has pressed START, abort the current handshake and start again as the clock provider. + +/// 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. + +:::tip The DIV register + +/// is not particularly random... + +/// but we just need the value to be different when each device reads it, and for the value to occasionally be an odd number + +::: + +/// If a transfer is complete (`SIO_DONE`), jump to `HandshakeMsgRx` (described below) to check the received value. + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-xfer-complete}} +{{#include ../../unbricked/serial-link/main.asm:handshake-xfer-complete}} +``` + +/// First byte must be `MSG_SHAKE` + +/// Second byte must be `wHandshakeExpect` + +/// If the received message is correct, set `wHandshakeState` to zero diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm new file mode 100644 index 00000000..9f52af67 --- /dev/null +++ b/unbricked/serial-link/main.asm @@ -0,0 +1,689 @@ +INCLUDE "hardware.inc" + +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_END RB 1 +DEF BG_NEXT 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_SIO RB 1 + +DEF DISPLAY_CLOCK_SOURCE EQU $9800 + 32 * 1 + 0 +DEF DISPLAY_TX EQU $9800 + 32 * 14 +DEF DISPLAY_RX EQU $9800 + 32 * 16 +DEF DISPLAY_RX_STATE EQU $9800 + 32 * 17 +DEF DISPLAY_HANDSHAKE EQU $9800 + 19 + +DEF MESSAGE_LENGTH EQU 8 + +DEF DOWN EQU $00 +DEF INIT EQU $01 +DEF READY EQU $02 +DEF RUNNING EQU $03 +DEF FINISHED EQU $04 +DEF PANIC EQU $05 + +DEF MSG_SYNC EQU $A0 +DEF MSG_SHAKE EQU $B0 +DEF MSG_TEST_DATA EQU $C0 + +; 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 +; ANCHOR_END: handshake-codes + + +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 + + xor a, a + ld b, 160 + ld hl, _OAMRAM +.clear_oam + ld [hli], a + dec b + jp nz, .clear_oam + + call SioInit + ei ; Sio requires interrupts to be enabled. + call LinkInit + + ; 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 + +Main: + ld a, [rLY] + cp 144 + jp nc, Main +WaitVBlank2: + ld a, [rLY] + cp 144 + jp c, WaitVBlank2 + + call Input + call LinkUpdate + call LinkDisplay + ld a, [wFrameCounter] + inc a + ld [wFrameCounter], a + jp Main + + +LinkInit: +LinkReset: + ld a, INIT + ld [wState], a + ld a, 0 + ld [wPacketCount], a + ld [wErrorCount], a + ld [wDelay], a + jp HandshakeDefault + + +LinkUpdate: + ld a, [wState] + cp a, DOWN + ret z + call SioTick + ld a, [wState] + cp a, INIT + jr z, .link_init + call ProcessInput + call CheckSioState + ret +.link_init + ld a, [wHandshakeState] + and a, a + jr nz, .handshake + ; handshake complete + ld hl, DISPLAY_HANDSHAKE + ld a, BG_TICK + ld [hl+], a + ld a, READY + ld [wState], a + jp SendStatusMsg +.handshake: + call HandshakeUpdate + ld a, [wFrameCounter] + and a, %0101_0000 + jr z, .cross + ld a, BG_EMPTY + ld [DISPLAY_HANDSHAKE], a + ret +.cross + ld a, BG_CROSS + ld [DISPLAY_HANDSHAKE], a + ret + + +LinkDisplay: +; ld hl, $9800 + 32 * 4 +; ld de, wSioBufferRx +; ld c, 8 +; : +; ld a, [de] +; inc de +; ld b, a +; call PrintHex +; dec c +; jr nz, :- + + ld hl, $9800 + 32 * 2 + ld a, [wState] :: ld [hl+], a + inc hl + ld a, BG_EMPTY :: ld [hl+], a + ld a, [wPacketCount] + ld b, a + call PrintHex + ld a, BG_CROSS :: ld [hl+], a + ld a, [wErrorCount] + ld b, a + call PrintHex + + call DrawClockSource + ret + + +CheckSioState: + ld a, [wSioState] + cp a, SIO_DONE + jp z, MsgRx + cp a, SIO_FAILED + jp z, MsgFailed + cp a, SIO_IDLE + jp z, SendNextMessage + ret + + +SendNextMessage: + ld a, [wState] + cp a, RUNNING + jp z, SendSequenceMsg + cp a, FINISHED + ret nc + jp SendStatusMsg + + +SendStatusMsg: + ld hl, wSioBufferTx + ld a, MSG_SYNC + ld [hl+], a + ld c, MESSAGE_LENGTH - 1 + ld a, [wState] +.loop_tx: + ld [hl+], a + dec c + jr nz, .loop_tx + call DrawBufferTx + ; jp SioTransferStart + ld a, MESSAGE_LENGTH + jp SioTransferStart.CustomCount + + +SendSequenceMsg: + ld hl, wSioBufferTx + ld a, MSG_TEST_DATA + ld [hl+], a + ld c, MESSAGE_LENGTH - 1 + ld a, [wPacketCount] +.loop_tx: + ld [hl+], a + dec c + jr nz, .loop_tx + call DrawBufferTx + ; jp SioTransferStart + ld a, MESSAGE_LENGTH + jp SioTransferStart.CustomCount + + +MsgRx: + ld a, SIO_IDLE + ld [wSioState], a + + call DrawBufferRx + + ld hl, wSioBufferRx + ld a, [hl+] + cp a, MSG_SYNC + jp z, .sync_msg + cp a, MSG_TEST_DATA + jp z, .seq_msg + ; UNKNOWN BAD TIMES + ld b, a + ld a, " " + ld [DISPLAY_RX_STATE], a + ld [DISPLAY_RX_STATE + 3], a + ld hl, DISPLAY_RX_STATE + 6 + ld a, BG_SOLID_2 :: ld [hl+], a + call PrintHex + ld a, PANIC + ld [wState], a + ret +.sync_msg: + ld a, [hl+] + ld [wRxStatus], a + ld b, a + ld hl, DISPLAY_RX_STATE + ld a, BG_SOLID_2 :: ld [hl+], a + call PrintHex + ld a, " " :: ld [hl+], a + ld [DISPLAY_RX_STATE + 6], a + +;;;;;;;;;;;; remote status updated + ld a, [wRxStatus] + ld b, a + ld a, [wState] + cp a, READY + ret nz + ; A = READY, B = [wRxStatus] + cp a, b + ret nz + ld a, RUNNING + ld [wState], a + ld a, 0 + ld [wPacketCount], a + ld [wErrorCount], a + call SendSequenceMsg + ret +.seq_msg: + ld a, [hl+] + ld b, a + + ld a, " " :: ld [DISPLAY_RX_STATE], a + ld hl, DISPLAY_RX_STATE + 3 + ld a, BG_SOLID_2 :: ld [hl+], a + call PrintHex + ld a, " " :: ld [hl+], a + +;;;;;;;;;;;;; process data packet + ld a, [wState] + cp a, RUNNING + jp z, .running + ret +.running: + ld a, [wPacketCount] + inc a + ld [wPacketCount], a + ret nz + ld a, FINISHED + ld [wState], a + ret + + +MsgFailed: + ld a, SIO_IDLE + ld [wSioState], a + ld a, READY + ld [wState], a + call SendStatusMsg + ret + + +ProcessInput: + ld a, [wNewKeys] + bit PADB_B, a + jp nz, LinkReset + ret + + +DrawClockSource: + ldh a, [rSC] + and SCF_SOURCE + ld a, BG_EXTERNAL + jr z, :+ + ld a, BG_INTERNAL +: + ld [DISPLAY_CLOCK_SOURCE], a + ret + + +DrawBufferTx: + ld de, wSioBufferTx + ld hl, DISPLAY_TX + ld c, 2 +.loop_tx + ld a, [de] + inc de + ld b, a + call PrintHex + dec c + jr nz, .loop_tx + ret + + +DrawBufferRx: + ld de, wSioBufferRx + ld hl, DISPLAY_RX + ld c, 2 +.loop_rx + ld a, [de] + inc de + ld b, a + call PrintHex + dec c + jr nz, .loop_rx + 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 + + ; end + dw `30000330 + dw `03000330 + dw `00300330 + dw `00030330 + dw `00300330 + dw `03000330 + dw `30000330 + dw `00000000 + + ; next + dw `00003000 + dw `00003300 + dw `33333330 + dw `33333333 + dw `33333330 + dw `00003300 + dw `00003000 + dw `00000000 + + ; 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 `03333330 + dw `00033000 + dw `00033000 + dw `00033000 + dw `00033000 + dw `00033000 + dw `00033000 + dw `03333330 + + ; external + dw `03333330 + dw `03300000 + dw `03300000 + dw `03333300 + dw `03300000 + dw `03300000 + dw `03300000 + dw `03333330 + + ; Sio + dw `22223332 + dw `22232223 + dw `20223322 + dw `22220032 + dw `20202023 + dw `20202023 + dw `20200023 + dw `33333332 +TilesEnd: + + +SECTION "Counter", WRAM0 +wFrameCounter: db + +SECTION "Input Variables", WRAM0 +wCurKeys: db +wNewKeys: db + +SECTION "SioTest", WRAM0 +wState: db + +wPacketCount: db +wErrorCount: db +wDelay: db + +wRxStatus: db + + +; ANCHOR: handshake-state +SECTION "Handshake State", WRAM0 +wHandshakeState:: db +wHandshakeExpect: 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 b, SHAKE_B + ld c, SHAKE_A + jp HandshakeBegin + + +; Begin handshake as the clock provider / internally clocked device. +HandshakeAsClockProvider: + call SioAbort + ld a, SCF_SOURCE + ldh [rSC], a + ld b, SHAKE_A + ld c, SHAKE_B + jp HandshakeBegin + + +; Begin handshake +; @param B: code to send +; @param C: code to expect +HandshakeBegin: + ld a, 1 + ld [wHandshakeState], a + ld a, c + ld [wHandshakeExpect], a + ld hl, wSioBufferTx + ld a, MSG_SHAKE + ld [hl+], a + ld [hl], b + ld a, 2 + jp SioTransferStart.CustomCount +; ANCHOR_END: handshake-begin + + +; ANCHOR: handshake-update +HandshakeUpdate: + ld a, [wHandshakeState] + and a, a + ret z + ; 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 + ; Check received value + ld hl, wSioBufferRx + ld a, [hl+] + cp a, MSG_SHAKE + ret nz + ld a, [wHandshakeExpect] + ld b, a + ld a, [hl+] + cp a, b + ret nz + ld a, 0 + ld [wHandshakeState], a + ret +; ANCHOR_END: handshake-xfer-complete diff --git a/unbricked/serial-link/sio.asm b/unbricked/serial-link/sio.asm index 6b8284c5..8f5ea755 100644 --- a/unbricked/serial-link/sio.asm +++ b/unbricked/serial-link/sio.asm @@ -34,7 +34,7 @@ DEF SIO_TIMEOUT_TICKS EQU 60 ; ANCHOR: sio-catchup-duration ; Catchup delay duration -DEF SIO_CATCHUP_SLEEP_DURATION EQU 100 +DEF SIO_CATCHUP_SLEEP_DURATION EQU 200 ; ANCHOR_END: sio-catchup-duration ; ANCHOR_END: sio-port-start-defs @@ -145,6 +145,7 @@ SioAbort:: ; @mut: AF, L SioTransferStart:: ld a, SIO_BUFFER_SIZE +.CustomCount:: ld [wSioCount], a ld a, 0 ld [wSioBufferOffset], a From 88923c367c41d26036f2d81ad14d26f849fe3311 Mon Sep 17 00:00:00 2001 From: quinnyo <3379314+quinnyo@users.noreply.github.com> Date: Thu, 22 Aug 2024 04:41:06 +1000 Subject: [PATCH 4/6] Send messages with packets, checksums, delivery reports. --- src/part2/serial-link.md | 195 ++++++++++++++++++++++++--- unbricked/serial-link/main.asm | 239 ++++++++++++++++++++++----------- unbricked/serial-link/sio.asm | 14 +- 3 files changed, 351 insertions(+), 97 deletions(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index 61693514..f5968f57 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -266,20 +266,123 @@ To do this we'll use the serial interrupt: {{#include ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}} ``` ---- - **TODO:** explain something about interrupts? but don't be weird about it, I guess... --- -## Using Sio +### A little protocol + +Before diving into implementation, let's take a minute to describe a *protocol*. +A protocol is a set of rules that govern communication. + +The most critical communications are those that support the application's features, which we'll call *messages*. + +/// Transmission errors: do not want. Transmission errors: cannot be eliminated. +/// Lots of possible ways to deal with damaged message packets. +/// Need to *detect* errors before you can deal with them. + +There's always a possibility that a message will be damaged in transmission or even due to a bug. +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). +To check that a message arrived intact, we'll use checksums. +Every packet sent will include a checksum of itself. +At the receiving end, the checksum can be computed again and checked against the one sent with the packet. + +:::tip Checksums, a checksummary + +A checksum is a computed value that depends on the value of some *input data*. +In our case, the input data is all the bytes that make up a packet. +In other words, every byte of the packet influences the sum. + + +The packet includes a field for such a checksum, which is initialised to `0`. +The checksum is computed using the whole packet -- including the zero -- and the result is written to the checksum field. +When the packet checksum is recomputed now, the result will be zero! +This is a common feature of popular checksums because it makes checking if data is intact so simple. + +::: + +Checking the packet checksum will indicate if the message was damaged, but only the receiver will have this information. +To inform the sender we'll make a rule that every message transfer must be followed by a delivery *report*. +In terms of information, a report is a boolean value -- either the message was received intact, or not. + +Because reports are so simple -- but very important -- we'll employ a simple technique to deliver them reliably. +Define two magic numbers -- one to send when the packet checksum matched and another for if it didn't. +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*. + +To increase the likelihood of the report getting interpreted correctly, we'll simply repeat the value multiple times. +At the receiving end, check each received byte -- finding just one byte equal to `STATUS_OK` will be interpreted as *success*. + +:::tip + +The binary values used here should be far apart in terms of [*hamming distance*](https://en.wikipedia.org/wiki/Hamming_distance). +In essence, either value should be very hard to confuse for the other, even if some bits were randomly changed. + +::: + + ---- -**TODO:** +### SioPacket +We'll implement some functions to facilitate constructing, sending, receiving, and checking packets. +The packet functions will operate on the existing serial data buffers. -/// building serial link test program, separate to unbricked main.asm? +The packets follow a simple structure: starting with a header containing a magic number and the packet checksum, followed by the payload data. +The magic number is a constant that marks the start of a packet. + +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}} +``` + +/// function to call to start building a new packet: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-prepare}} +{{#include ../../unbricked/serial-link/sio.asm:sio-packet-prepare}} +``` + +/// returns packet data pointer in `hl` + +/// After calling `SioPacketTxPrepare`, the payload data can be added to the packet. + +Once the desired data has been copied to the packet, the checksum needs to be calculated before the packet can be transferred. +We call this *finalising* the packet and this is implemented in `SioPacketTxFinalise`: + +```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 checksum and write the result to the packet. + +/// a function to perform the checksum test when receiving a packet, `SioPacketRxCheck`: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-packet-check}} +{{#include ../../unbricked/serial-link/sio.asm:sio-packet-check}} +``` + +/// Checks that the packet begins with the magic number `SIO_PACKET_START`, before checking the checksum. +/// For convenience, a pointer to the start of packet data is returned in `hl`. + +/// 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}} +``` + +:::tip + +The checksum implemented here has been kept very simple for this tutorial. +It's probably not very suitable for real-world projects. + +::: + + +## Using Sio /// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different: ```console @@ -289,28 +392,79 @@ $ rgblink -o unbricked.gb main.o sio.o $ rgbfix -v -p 0xFF unbricked.gb ``` + +/// serial link features: *Link* + /// tiles /// defs -/// init/reset + +/// one function to initialise basic serial link state. + +/// Implement `LinkInit`: -/// initialise Sio -Before doing anything else with Sio, `SioInit` needs to be called. +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-init}} +{{#include ../../unbricked/serial-link/main.asm:link-init}} +``` + +Calling `SioInit` prepares Sio for use, except for one thing: **e**nabling **i**nterrupts with the `ei` instruction. + +:::tip + +If interrupts must be enabled for Sio to work fully, you might be wondering why we don't just do it in `SioInit`. +Sio is in control of the serial interrupt, but `ei` enables interrupts globally. +Other interrupts may be in use by other parts of the code, which are clearly outside of Sio's responsibility. + +/// Sio doesn't enable or disable interrupts because side effects ... + +/// [Interrupts](https://gbdev.io/pandocs/Interrupts.html) + +::: + +Note that `LinkReset` starts part way through `LinkInit`. +This way the two functions can share code without zero overhead and `LinkReset` can be called without performing the startup initialisation again. +This pattern is often referred to as *fallthrough*: `LinkInit` *falls through* to `LinkReset`. + +Call the init routine once before the main loop starts: ```rgbasm - call SioInit + call LinkInit +``` + - ; enable interrupts! - ei +### Link impl go + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-status}} +{{#include ../../unbricked/serial-link/main.asm:link-send-status}} ``` -/// update Sio every frame... -```rgbasm - call SioTick +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-test-data}} +{{#include ../../unbricked/serial-link/main.asm:link-send-test-data}} ``` ---- + +/// Implement `LinkUpdate`: + +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-update}} +{{#include ../../unbricked/serial-link/main.asm:link-update}} +``` + +/// update Sio every frame... + +/// in the `INIT` state, do handshake until its done. + +Once the handshake is complete, change to the `READY` state and notify the other device. + +/// in any of the other active states, reset if the B button is pressed + +/// finally we jump to different routines based on Sio's transfer state + + + +/// **(very) TODO:** handling received messages... + + ### Handshake @@ -363,3 +517,12 @@ Before doing anything else with Sio, `SioInit` needs to be called. /// Second byte must be `wHandshakeExpect` /// If the received message is correct, set `wHandshakeState` to zero + +:::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 + +::: diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm index 9f52af67..de8adad0 100644 --- a/unbricked/serial-link/main.asm +++ b/unbricked/serial-link/main.asm @@ -1,5 +1,6 @@ INCLUDE "hardware.inc" +; BG Tile IDs RSSET 16 DEF BG_SOLID_0 RB 1 DEF BG_SOLID_1 RB 1 @@ -13,15 +14,21 @@ DEF BG_CROSS RB 1 DEF BG_INTERNAL RB 1 DEF BG_EXTERNAL RB 1 DEF BG_SIO RB 1 - -DEF DISPLAY_CLOCK_SOURCE EQU $9800 + 32 * 1 + 0 -DEF DISPLAY_TX EQU $9800 + 32 * 14 -DEF DISPLAY_RX EQU $9800 + 32 * 16 -DEF DISPLAY_RX_STATE EQU $9800 + 32 * 17 -DEF DISPLAY_HANDSHAKE EQU $9800 + 19 - -DEF MESSAGE_LENGTH EQU 8 - +DEF BG_INBOX RB 1 +DEF BG_OUTBOX RB 1 + +; BG map positions (addresses) of various info +DEF DISPLAY_LINK EQU $9800 +DEF DISPLAY_CLOCK_SRC EQU DISPLAY_LINK + 18 +DEF DISPLAY_HANDSHAKE EQU DISPLAY_LINK + 19 +DEF DISPLAY_TX EQU DISPLAY_LINK + 32 * 2 +DEF DISPLAY_TX_STATE EQU DISPLAY_TX + 1 +DEF DISPLAY_TX_BUFFER EQU DISPLAY_TX + 32 +DEF DISPLAY_RX EQU DISPLAY_LINK + 32 * 5 +DEF DISPLAY_RX_STATE EQU DISPLAY_RX + 1 +DEF DISPLAY_RX_BUFFER EQU DISPLAY_RX + 32 + +; Link finite state machine states enum DEF DOWN EQU $00 DEF INIT EQU $01 DEF READY EQU $02 @@ -33,6 +40,10 @@ DEF MSG_SYNC EQU $A0 DEF MSG_SHAKE EQU $B0 DEF MSG_TEST_DATA EQU $C0 +DEF STATUS_OK EQU $11 +DEF STATUS_ERROR EQU $EE +DEF STATUS_REPORT_COPIES EQU 4 + ; ANCHOR: handshake-codes ; Handshake code sent by internally clocked device (clock provider) DEF SHAKE_A EQU $88 @@ -87,8 +98,6 @@ WaitVBlank: dec b jp nz, .clear_oam - call SioInit - ei ; Sio requires interrupts to be enabled. call LinkInit ; Turn the LCD on @@ -125,17 +134,29 @@ WaitVBlank2: jp Main +; ANCHOR: link-init LinkInit: + ld a, BG_OUTBOX + ld [DISPLAY_TX], a + ld a, BG_INBOX + ld [DISPLAY_RX], a + call SioInit + ei ; Sio requires interrupts to be enabled. LinkReset: ld a, INIT ld [wState], a ld a, 0 + ld [wCheckPacket], a + ld [wTxValue], a + ld [wRxValue], a ld [wPacketCount], a ld [wErrorCount], a ld [wDelay], a jp HandshakeDefault +; ANCHOR_END: link-init +; ANCHOR: link-update LinkUpdate: ld a, [wState] cp a, DOWN @@ -144,8 +165,18 @@ LinkUpdate: ld a, [wState] cp a, INIT jr z, .link_init - call ProcessInput - call CheckSioState + ; if B is pressed, reset + ld a, [wNewKeys] + and a, PADF_B + jp nz, LinkReset + ; handle Sio transfer states + ld a, [wSioState] + cp a, SIO_DONE + jp z, MsgRx + cp a, SIO_FAILED + jp z, MsgFailed + cp a, SIO_IDLE + jp z, SendNextMessage ret .link_init ld a, [wHandshakeState] @@ -170,24 +201,13 @@ LinkUpdate: ld a, BG_CROSS ld [DISPLAY_HANDSHAKE], a ret +; ANCHOR_END: link-update LinkDisplay: -; ld hl, $9800 + 32 * 4 -; ld de, wSioBufferRx -; ld c, 8 -; : -; ld a, [de] -; inc de -; ld b, a -; call PrintHex -; dec c -; jr nz, :- - - ld hl, $9800 + 32 * 2 + ld hl, DISPLAY_LINK ld a, [wState] :: ld [hl+], a inc hl - ld a, BG_EMPTY :: ld [hl+], a ld a, [wPacketCount] ld b, a call PrintHex @@ -197,17 +217,11 @@ LinkDisplay: call PrintHex call DrawClockSource - ret - -CheckSioState: - ld a, [wSioState] - cp a, SIO_DONE - jp z, MsgRx - cp a, SIO_FAILED - jp z, MsgFailed - cp a, SIO_IDLE - jp z, SendNextMessage + ld hl, DISPLAY_TX_STATE + ld a, [wTxValue] + ld b, a + call PrintHex ret @@ -220,45 +234,95 @@ SendNextMessage: jp SendStatusMsg +; ANCHOR: link-send-status SendStatusMsg: - ld hl, wSioBufferTx + call SioPacketTxPrepare ld a, MSG_SYNC ld [hl+], a - ld c, MESSAGE_LENGTH - 1 ld a, [wState] -.loop_tx: ld [hl+], a - dec c - jr nz, .loop_tx - call DrawBufferTx - ; jp SioTransferStart - ld a, MESSAGE_LENGTH - jp SioTransferStart.CustomCount + jr PacketTransferStart +; ANCHOR_END: link-send-status +; ANCHOR: link-send-test-data SendSequenceMsg: - ld hl, wSioBufferTx + call SioPacketTxPrepare ld a, MSG_TEST_DATA ld [hl+], a - ld c, MESSAGE_LENGTH - 1 + ld a, [wTxValue] + ld [hl+], a + jr PacketTransferStart +; ANCHOR_END: link-send-test-data + + +PacketTransferStart: + call SioPacketTxFinalise + ld a, 1 + ld [wCheckPacket], a ld a, [wPacketCount] -.loop_tx: + inc a + ld [wPacketCount], a + jp DrawBufferTx + + +SendStatusReport: + ld hl, wSioBufferTx + REPT STATUS_REPORT_COPIES ld [hl+], a - dec c - jr nz, .loop_tx - call DrawBufferTx - ; jp SioTransferStart - ld a, MESSAGE_LENGTH - jp SioTransferStart.CustomCount + ENDR + ld a, STATUS_REPORT_COPIES + call SioTransferStart.CustomCount + jp DrawBufferTx +; Process received data +; @mut: AF, BC, HL MsgRx: ld a, SIO_IDLE ld [wSioState], a call DrawBufferRx + ld a, [wCheckPacket] + and a, a + jr nz, .rx_packet + ; no packet check -- status report ld hl, wSioBufferRx + ld c, STATUS_REPORT_COPIES +.read_report_loop + ld a, [hl+] + cp a, STATUS_OK + jr z, .status_ok + dec c + jr nz, .read_report_loop + ld a, [wErrorCount] + inc a + ld [wErrorCount], a + ret +.status_ok + ; Advance tx value if delivered successfully + ld a, [wTxValue] + inc a + ret z + ld [wTxValue], a + ret +.rx_packet: + ld a, 0 + ld [wCheckPacket], a + call SioPacketRxCheck + jr z, .check_ok + ; packet checksum didn't match + ld a, STATUS_ERROR + jp SendStatusReport +.check_ok + call ProcessMsg + ld a, STATUS_OK + call SendStatusReport + ret + + +ProcessMsg: ld a, [hl+] cp a, MSG_SYNC jp z, .sync_msg @@ -297,12 +361,14 @@ MsgRx: ld a, RUNNING ld [wState], a ld a, 0 + ld [wTxValue], a + ld [wRxValue], a ld [wPacketCount], a ld [wErrorCount], a - call SendSequenceMsg ret .seq_msg: ld a, [hl+] + ld [wRxValue], a ld b, a ld a, " " :: ld [DISPLAY_RX_STATE], a @@ -310,19 +376,6 @@ MsgRx: ld a, BG_SOLID_2 :: ld [hl+], a call PrintHex ld a, " " :: ld [hl+], a - -;;;;;;;;;;;;; process data packet - ld a, [wState] - cp a, RUNNING - jp z, .running - ret -.running: - ld a, [wPacketCount] - inc a - ld [wPacketCount], a - ret nz - ld a, FINISHED - ld [wState], a ret @@ -335,13 +388,6 @@ MsgFailed: ret -ProcessInput: - ld a, [wNewKeys] - bit PADB_B, a - jp nz, LinkReset - ret - - DrawClockSource: ldh a, [rSC] and SCF_SOURCE @@ -349,14 +395,14 @@ DrawClockSource: jr z, :+ ld a, BG_INTERNAL : - ld [DISPLAY_CLOCK_SOURCE], a + ld [DISPLAY_CLOCK_SRC], a ret DrawBufferTx: ld de, wSioBufferTx - ld hl, DISPLAY_TX - ld c, 2 + ld hl, DISPLAY_TX_BUFFER + ld c, 4 .loop_tx ld a, [de] inc de @@ -369,8 +415,8 @@ DrawBufferTx: DrawBufferRx: ld de, wSioBufferRx - ld hl, DISPLAY_RX - ld c, 2 + ld hl, DISPLAY_RX_BUFFER + ld c, 4 .loop_rx ld a, [de] inc de @@ -579,6 +625,36 @@ Tiles: dw `20202023 dw `20200023 dw `33333332 + + ; 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 + + ; link + dw `03330000 + dw `30003000 + dw `30023200 + dw `30203020 + dw `30203020 + dw `03230020 + dw `00200020 + dw `00022200 TilesEnd: @@ -591,6 +667,9 @@ wNewKeys: db SECTION "SioTest", WRAM0 wState: db +wCheckPacket: db +wTxValue: db +wRxValue: db wPacketCount: db wErrorCount: db diff --git a/unbricked/serial-link/sio.asm b/unbricked/serial-link/sio.asm index 8f5ea755..765996ef 100644 --- a/unbricked/serial-link/sio.asm +++ b/unbricked/serial-link/sio.asm @@ -223,6 +223,7 @@ SioPortEnd: 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 @@ -244,16 +245,23 @@ SioPacketTxPrepare:: 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 - ret + 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:: @@ -266,9 +274,12 @@ SioPacketRxCheck:: ; 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 @@ -283,3 +294,4 @@ SioPacketChecksum: dec c jr nz, :- ret +; ANCHOR_END: sio-checksum From 5649f40ccfad44dcb29b668ed78c24e6a948f7f0 Mon Sep 17 00:00:00 2001 From: quinnyo <3379314+quinnyo@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:51:04 +1000 Subject: [PATCH 5/6] Use SioPacket for all message types. Message receipt must be acknowledged by ID. - errors: packet check failed, invalid message type - display error counts - tidier display layout --- unbricked/serial-link/main.asm | 540 ++++++++++++++++----------------- unbricked/serial-link/sio.asm | 54 ++++ 2 files changed, 317 insertions(+), 277 deletions(-) diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm index de8adad0..a61986c0 100644 --- a/unbricked/serial-link/main.asm +++ b/unbricked/serial-link/main.asm @@ -6,49 +6,43 @@ 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_END RB 1 -DEF BG_NEXT 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_SIO 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_HANDSHAKE EQU DISPLAY_LINK + 19 DEF DISPLAY_TX EQU DISPLAY_LINK + 32 * 2 DEF DISPLAY_TX_STATE EQU DISPLAY_TX + 1 DEF DISPLAY_TX_BUFFER EQU DISPLAY_TX + 32 -DEF DISPLAY_RX EQU DISPLAY_LINK + 32 * 5 +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 ; Link finite state machine states enum -DEF DOWN EQU $00 -DEF INIT EQU $01 -DEF READY EQU $02 -DEF RUNNING EQU $03 -DEF FINISHED EQU $04 -DEF PANIC EQU $05 +DEF LINK_DOWN EQU $00 +DEF LINK_INIT EQU $01 +DEF LINK_UP EQU $02 +DEF LINK_ERROR EQU $03 DEF MSG_SYNC EQU $A0 DEF MSG_SHAKE EQU $B0 DEF MSG_TEST_DATA EQU $C0 -DEF STATUS_OK EQU $11 -DEF STATUS_ERROR EQU $EE -DEF STATUS_REPORT_COPIES EQU 4 - ; 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 ; ANCHOR_END: handshake-codes @@ -120,13 +114,15 @@ Main: ld a, [rLY] cp 144 jp nc, Main + + call Input + call LinkUpdate + WaitVBlank2: ld a, [rLY] cp 144 jp c, WaitVBlank2 - call Input - call LinkUpdate call LinkDisplay ld a, [wFrameCounter] inc a @@ -140,254 +136,260 @@ LinkInit: ld [DISPLAY_TX], a ld a, BG_INBOX ld [DISPLAY_RX], a + ld a, BG_CROSS + ld [DISPLAY_RX_ERRORS - 1], a call SioInit ei ; Sio requires interrupts to be enabled. LinkReset: - ld a, INIT - ld [wState], a - ld a, 0 - ld [wCheckPacket], a - ld [wTxValue], a - ld [wRxValue], a - ld [wPacketCount], a - ld [wErrorCount], a - ld [wDelay], a + call SioReset + ld a, LINK_INIT + ld [wLocal.state], a + ld a, LINK_DOWN + 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 + call ClearTestSequenceResults jp HandshakeDefault ; ANCHOR_END: link-init ; ANCHOR: link-update LinkUpdate: - ld a, [wState] - cp a, DOWN - ret z - call SioTick - ld a, [wState] - cp a, INIT - jr z, .link_init ; if B is pressed, reset ld a, [wNewKeys] and a, PADF_B jp nz, LinkReset + + call SioTick + ld a, [wLocal.state] + cp a, LINK_INIT + jr z, .link_init + cp a, LINK_UP + jr z, .link_up + ret + +.link_up ; handle Sio transfer states ld a, [wSioState] cp a, SIO_DONE - jp z, MsgRx + jp z, LinkRx cp a, SIO_FAILED - jp z, MsgFailed + jp z, LinkError cp a, SIO_IDLE jp z, SendNextMessage ret .link_init ld a, [wHandshakeState] and a, a - jr nz, .handshake + jp nz, HandshakeUpdate ; handshake complete - ld hl, DISPLAY_HANDSHAKE - ld a, BG_TICK - ld [hl+], a - ld a, READY - ld [wState], a - jp SendStatusMsg -.handshake: - call HandshakeUpdate - ld a, [wFrameCounter] - and a, %0101_0000 - jr z, .cross - ld a, BG_EMPTY - ld [DISPLAY_HANDSHAKE], a - ret -.cross - ld a, BG_CROSS - ld [DISPLAY_HANDSHAKE], a + ld a, LINK_UP + ld [wLocal.state], a + ld a, 0 + ld [wPacketCount], a ret ; ANCHOR_END: link-update LinkDisplay: - ld hl, DISPLAY_LINK - ld a, [wState] :: ld [hl+], a - inc hl + ld hl, DISPLAY_CLOCK_SRC - 3 ld a, [wPacketCount] ld b, a call PrintHex - ld a, BG_CROSS :: ld [hl+], a - ld a, [wErrorCount] + 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 - call DrawClockSource + 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, [wTxValue] ld b, a call PrintHex - ret - - -SendNextMessage: - ld a, [wState] - cp a, RUNNING - jp z, SendSequenceMsg - cp a, FINISHED - ret nc - jp SendStatusMsg + ld hl, DISPLAY_RX_STATE + ld a, [wRxValue] + ld b, a + call PrintHex + ld hl, DISPLAY_RX_ERRORS + ld a, [wErrorsRx] + ld b, a + call PrintHex + ld hl, DISPLAY_RX_ERRORS - 3 + ld a, [wErrorsRxMsg] + ld b, a + call PrintHex -; ANCHOR: link-send-status -SendStatusMsg: - call SioPacketTxPrepare - ld a, MSG_SYNC - ld [hl+], a - ld a, [wState] - ld [hl+], a - jr PacketTransferStart -; ANCHOR_END: link-send-status + ld a, [wFrameCounter] + and a, $01 + jp z, DrawBufferTx + jp DrawBufferRx -; ANCHOR: link-send-test-data -SendSequenceMsg: +; ANCHOR: link-send-message +SendNextMessage: + ld hl, wPacketCount + ld a, [hl] + inc [hl] + and a, $01 + jr z, .sync +.data: call SioPacketTxPrepare ld a, MSG_TEST_DATA ld [hl+], a + ld a, [wLocal.tx_id] + ld [hl+], a ld a, [wTxValue] ld [hl+], a - jr PacketTransferStart -; ANCHOR_END: link-send-test-data - - -PacketTransferStart: call SioPacketTxFinalise - ld a, 1 - ld [wCheckPacket], a - ld a, [wPacketCount] - inc a - ld [wPacketCount], a - jp DrawBufferTx - - -SendStatusReport: - ld hl, wSioBufferTx - REPT STATUS_REPORT_COPIES + ret +.sync: + call SioPacketTxPrepare + ld a, MSG_SYNC ld [hl+], a - ENDR - ld a, STATUS_REPORT_COPIES - call SioTransferStart.CustomCount - jp DrawBufferTx + ld a, [wLocal.state] + ld [hl+], a + ld a, [wLocal.rx_id] + ld [hl+], a + call SioPacketTxFinalise + ret +; ANCHOR_END: link-send-message ; Process received data ; @mut: AF, BC, HL -MsgRx: +LinkRx: ld a, SIO_IDLE ld [wSioState], a - call DrawBufferRx - - ld a, [wCheckPacket] - and a, a - jr nz, .rx_packet - ; no packet check -- status report - ld hl, wSioBufferRx - ld c, STATUS_REPORT_COPIES -.read_report_loop - ld a, [hl+] - cp a, STATUS_OK - jr z, .status_ok - dec c - jr nz, .read_report_loop - ld a, [wErrorCount] - inc a - ld [wErrorCount], a - ret -.status_ok - ; Advance tx value if delivered successfully - ld a, [wTxValue] - inc a - ret z - ld [wTxValue], a - ret -.rx_packet: - ld a, 0 - ld [wCheckPacket], a call SioPacketRxCheck jr z, .check_ok - ; packet checksum didn't match - ld a, STATUS_ERROR - jp SendStatusReport -.check_ok - call ProcessMsg - ld a, STATUS_OK - call SendStatusReport + ld hl, wErrorsRx + call u8ptr_IncrementToMax ret - - -ProcessMsg: +.check_ok ld a, [hl+] cp a, MSG_SYNC - jp z, .sync_msg + jr z, .rx_sync cp a, MSG_TEST_DATA - jp z, .seq_msg - ; UNKNOWN BAD TIMES - ld b, a - ld a, " " - ld [DISPLAY_RX_STATE], a - ld [DISPLAY_RX_STATE + 3], a - ld hl, DISPLAY_RX_STATE + 6 - ld a, BG_SOLID_2 :: ld [hl+], a - call PrintHex - ld a, PANIC - ld [wState], a + jr z, .rx_test_data + ld hl, wErrorsRxMsg + call u8ptr_IncrementToMax ret -.sync_msg: +; handle MSG_SYNC +.rx_sync: + ; always take latest state value ld a, [hl+] - ld [wRxStatus], a - ld b, a - ld hl, DISPLAY_RX_STATE - ld a, BG_SOLID_2 :: ld [hl+], a - call PrintHex - ld a, " " :: ld [hl+], a - ld [DISPLAY_RX_STATE + 6], a - -;;;;;;;;;;;; remote status updated - ld a, [wRxStatus] + ld [wRemote.state], a + ; does remote ack the ID we sent? + ld a, [wLocal.tx_id] ld b, a - ld a, [wState] - cp a, READY - ret nz - ; A = READY, B = [wRxStatus] + ld a, [hl+] cp a, b ret nz - ld a, RUNNING - ld [wState], a - ld a, 0 + ; save ack'd ID + ld [wRemote.rx_id], a + inc b + ld a, b + ld [wLocal.tx_id], a + ld a, [wTxValue] + inc a ld [wTxValue], a - ld [wRxValue], a - ld [wPacketCount], a - ld [wErrorCount], a ret -.seq_msg: +; handle MSG_TEST_DATA +.rx_test_data: + ; valid data packets must have sequential IDs + ld a, [wLocal.rx_id] + ld b, a + inc b + ld a, [hl+] + ld [wRemote.tx_id], a + cp a, b + ret nz + ld [wLocal.rx_id], a ld a, [hl+] ld [wRxValue], a - ld b, a + ret - ld a, " " :: ld [DISPLAY_RX_STATE], a - ld hl, DISPLAY_RX_STATE + 3 - ld a, BG_SOLID_2 :: ld [hl+], a - call PrintHex - ld a, " " :: ld [hl+], a + +LinkError: + call SioReset + ld a, LINK_ERROR + ld [wLocal.state], a ret -MsgFailed: - ld a, SIO_IDLE - ld [wSioState], a - ld a, READY - ld [wState], a - call SendStatusMsg +ClearTestSequenceResults: + ld a, 0 + ld [wTxValue], a + ld [wRxValue], a + ld [wPacketCount], a + ld [wErrorsRx], a + ld [wErrorsRxMsg], a + ret + + +; Draw Link state +; @param A: value +; @param HL: dest +; @mut: AF, B, HL +DrawLinkState: + cp a, LINK_INIT + jr nz, :+ + ld a, [wHandshakeState] + and $0F + ld [hl+], a + ret +: + ld b, BG_EMPTY + cp a, LINK_DOWN + jr z, .end + ld b, BG_TICK + cp a, LINK_UP + jr z, .end + ld b, BG_CROSS + cp a, LINK_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 @@ -395,14 +397,15 @@ DrawClockSource: jr z, :+ ld a, BG_INTERNAL : - ld [DISPLAY_CLOCK_SRC], a + ld [hl+], a ret +; @mut: AF, BC, DE, HL DrawBufferTx: ld de, wSioBufferTx ld hl, DISPLAY_TX_BUFFER - ld c, 4 + ld c, 8 .loop_tx ld a, [de] inc de @@ -413,10 +416,11 @@ DrawBufferTx: ret +; @mut: AF, BC, DE, HL DrawBufferRx: ld de, wSioBufferRx ld hl, DISPLAY_RX_BUFFER - ld c, 4 + ld c, 8 .loop_rx ld a, [de] inc de @@ -427,6 +431,15 @@ DrawBufferRx: ret +; Increment the byte at [hl], if it's less than 255. +u8ptr_IncrementToMax: + ld a, [hl] + inc a + ret z + ld [hl], a + ret + + ; @param B: value ; @param HL: dest ; @mut: AF, HL @@ -546,26 +559,6 @@ Tiles: dw `33333333 dw `33333333 - ; end - dw `30000330 - dw `03000330 - dw `00300330 - dw `00030330 - dw `00300330 - dw `03000330 - dw `30000330 - dw `00000000 - - ; next - dw `00003000 - dw `00003300 - dw `33333330 - dw `33333333 - dw `33333330 - dw `00003300 - dw `00003000 - dw `00000000 - ; empty dw `00000000 dw `01111110 @@ -597,34 +590,24 @@ Tiles: dw `22222200 ; internal - dw `03333330 - dw `00033000 - dw `00033000 - dw `00033000 - dw `00033000 - dw `00033000 - dw `00033000 - dw `03333330 + dw `03333333 + dw `01223333 + dw `00033300 + dw `00033300 + dw `00023300 + dw `00023300 + dw `03333333 + dw `01223333 ; external - dw `03333330 - dw `03300000 - dw `03300000 - dw `03333300 - dw `03300000 - dw `03300000 + dw `03333221 + dw `03333333 dw `03300000 + dw `03333210 dw `03333330 - - ; Sio - dw `22223332 - dw `22232223 - dw `20223322 - dw `22220032 - dw `20202023 - dw `20202023 - dw `20200023 - dw `33333332 + dw `03300000 + dw `03333221 + dw `03333333 ; inbox dw `33330003 @@ -645,16 +628,6 @@ Tiles: dw `30000003 dw `30000003 dw `33333333 - - ; link - dw `03330000 - dw `30003000 - dw `30023200 - dw `30203020 - dw `30203020 - dw `03230020 - dw `00200020 - dw `00022200 TilesEnd: @@ -665,23 +638,31 @@ SECTION "Input Variables", WRAM0 wCurKeys: db wNewKeys: db -SECTION "SioTest", WRAM0 -wState: db -wCheckPacket: db +SECTION "Link", WRAM0 wTxValue: db wRxValue: db wPacketCount: db -wErrorCount: db -wDelay: db - -wRxStatus: db +; inbound errors (count packets that fail integrity checks) +wErrorsRx: db +; invalid/unexpected message (packet content) +wErrorsRxMsg: db + +; Local Link state +wLocal: + .state: db + .tx_id: db + .rx_id: db +; Remote Link state +wRemote: + .state: db + .tx_id: db + .rx_id: db ; ANCHOR: handshake-state SECTION "Handshake State", WRAM0 wHandshakeState:: db -wHandshakeExpect: db ; ANCHOR_END: handshake-state @@ -692,9 +673,9 @@ HandshakeDefault: call SioAbort ld a, 0 ldh [rSC], a - ld b, SHAKE_B - ld c, SHAKE_A - jp HandshakeBegin + ld a, HANDSHAKE_COUNT + ld [wHandshakeState], a + jr HandshakeSendPacket ; Begin handshake as the clock provider / internally clocked device. @@ -702,33 +683,28 @@ HandshakeAsClockProvider: call SioAbort ld a, SCF_SOURCE ldh [rSC], a - ld b, SHAKE_A - ld c, SHAKE_B - jp HandshakeBegin + ld a, HANDSHAKE_COUNT + ld [wHandshakeState], a + jr HandshakeSendPacket -; Begin handshake -; @param B: code to send -; @param C: code to expect -HandshakeBegin: - ld a, 1 - ld [wHandshakeState], a - ld a, c - ld [wHandshakeExpect], a - ld hl, wSioBufferTx +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 - ld a, 2 - jp SioTransferStart.CustomCount + jp SioPacketTxFinalise ; ANCHOR_END: handshake-begin ; ANCHOR: handshake-update HandshakeUpdate: - ld a, [wHandshakeState] - and a, a - ret z ; press START: perform handshake as clock provider ld a, [wNewKeys] bit PADB_START, a @@ -752,17 +728,27 @@ HandshakeMsgRx: ; flush sio status ld a, SIO_IDLE ld [wSioState], a - ; Check received value - ld hl, wSioBufferRx + call SioPacketRxCheck + jr nz, .failed ld a, [hl+] cp a, MSG_SHAKE - ret nz - ld a, [wHandshakeExpect] - ld b, a + 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 - ret nz - ld a, 0 + jr nz, .failed + ld a, [wHandshakeState] + dec a + ld [wHandshakeState], a + jr nz, HandshakeSendPacket + ret +.failed + ld a, $FF ld [wHandshakeState], a ret ; ANCHOR_END: handshake-xfer-complete diff --git a/unbricked/serial-link/sio.asm b/unbricked/serial-link/sio.asm index 765996ef..7135d314 100644 --- a/unbricked/serial-link/sio.asm +++ b/unbricked/serial-link/sio.asm @@ -22,6 +22,7 @@ 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 @@ -41,6 +42,8 @@ DEF SIO_CATCHUP_SLEEP_DURATION EQU 200 ; 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 @@ -99,12 +102,43 @@ SioInit:: ld [wSioTimer], a ld [wSioCount], a ld [wSioBufferOffset], a + call SioClearBufferRx + call SioClearBufferTx ; enable serial interrupt ldh a, [rIE] or a, IEF_SERIAL ldh [rIE], a ret + + +; @mut: AF, C, HL +SioClearBufferRx: + ld hl, wSioBufferRx + ld c, SIO_BUFFER_SIZE + ld a, SIO_BUFFER_CLEAR + jr SioMemFill + + +; @mut: AF, C, HL +SioClearBufferTx: + ld hl, wSioBufferTx + ld c, SIO_BUFFER_SIZE + ld a, SIO_BUFFER_CLEAR + jr SioMemFill + + +; Fill a contiguous block of memory with the same value. +; @param A: clear value +; @param C: size of region to clear (WARN: size = 0 is equivalent to 256) +; @param HL: start address +; @mut: AF, C, HL +SioMemFill: +: + ld [hl+], a + dec c + jr nz, :- + ret ; ANCHOR_END: sio-impl-init @@ -113,6 +147,8 @@ SioInit:: ; @mut: AF SioTick:: ld a, [wSioState] + cp a, SIO_RESET + jr z, .reset_tick cp a, SIO_ACTIVE ret nz ; update timeout on external clock @@ -126,6 +162,10 @@ SioTick:: ld [wSioTimer], a jr z, SioAbort ret +.reset_tick + ld a, SIO_IDLE + ld [wSioState], a + ret ; Abort the ongoing transfer (if any) and enter the FAILED state. @@ -137,6 +177,20 @@ SioAbort:: res SCB_START, a ldh [rSC], a ret + + +SioReset:: + ldh a, [rSC] + res SCB_START, a + ldh [rSC], a + ld a, SIO_RESET + ld [wSioState], a + ld a, 0 + ld [wSioTimer], a + ld [wSioCount], a + ld [wSioBufferOffset], a + call SioClearBufferRx + jp SioClearBufferTx ; ANCHOR_END: sio-tick From 63e5ea1eb14827592548b8564ef0ea325b726f1a Mon Sep 17 00:00:00 2001 From: quinnyo <3379314+quinnyo@users.noreply.github.com> Date: Sat, 7 Sep 2024 07:46:14 +1000 Subject: [PATCH 6/6] Update part2/serial-link lesson --- src/part2/serial-link.md | 219 +++++++++++++++------------------ unbricked/serial-link/main.asm | 10 +- 2 files changed, 103 insertions(+), 126 deletions(-) diff --git a/src/part2/serial-link.md b/src/part2/serial-link.md index f5968f57..f4fa8559 100644 --- a/src/part2/serial-link.md +++ b/src/part2/serial-link.md @@ -3,18 +3,9 @@ --- **TODO:** In this lesson... -- learn about the Game Boy serial port... - - how it works, how to use it - - pitfalls and challenges -- build a thing, Sio Core: - - multibyte + convenience wrapper over GB serial - - incl. sync catchup delays, timeouts -- do something with Sio: - - integrate/use Sio - - ? manually choose clock provider - - ? send some data ... -- ? build a thing, 'Packets': - - adds data integrity test with simple checksum +- 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 --- @@ -34,21 +25,18 @@ So if you're connecting a DMG with a later model, make sure you have an adapter :::tip Can I just use an emulator? Emulators should not be relied upon as a substitute for the real thing, especially when working with the serial port. - - ::: ## The Game Boy serial port ---- +:::tip Information overload -**TODO:** about this section -- this section = crash course on GB serial port theory and operation -- programmer's mental model (not a description of the hardware implementation) +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. @@ -71,26 +59,15 @@ While the serial port is *active*, it sends and receives a data bit on each seri 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. ---- - -**TODO:** something about the challenges posed... -- GB serial is not "unreliable"... But it's also "not reliable"... -- some notable things for reliable communication that GB doesn't provide: - - connection detection, status: can't be truly solved in software, work around with error detection - - delivery report / ACK: software can make improvements with careful design - - error detection: software implementation can be effective - ---- - ## Sio -Let's start building **Sio**, a serial I/O guy. - ---- -**TODO:** Create a file, sio.asm? (And complicate the build process) ... Just stick it in main.asm? + ---- +/// Let's start building **Sio**, our serial I/O system. +/// Create a new source file called `sio.asm`. ... reuseable code ... & avoid gigantic `main.asm` First, define the constants that represent Sio's main states/status: @@ -266,68 +243,53 @@ To do this we'll use the serial interrupt: {{#include ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}} ``` -**TODO:** explain something about interrupts? but don't be weird about it, I guess... - ---- +All this short routine really *does* is `call SioPortEnd`. +But because this is an interrupt handler we have to do a little dance to prevent *bad things* from happening. +We need to preserve the values in the registers that will be modified by `SioPortEnd`, `af` and `hl`. +/// 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. +*The stack* is the perfect way to do this. +`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. +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`. +:::tip We interrupt this broadcast to briefly explain what interrupts are -### A little protocol - -Before diving into implementation, let's take a minute to describe a *protocol*. -A protocol is a set of rules that govern communication. - -The most critical communications are those that support the application's features, which we'll call *messages*. - -/// Transmission errors: do not want. Transmission errors: cannot be eliminated. -/// Lots of possible ways to deal with damaged message packets. -/// Need to *detect* errors before you can deal with them. - -There's always a possibility that a message will be damaged in transmission or even due to a bug. -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). -To check that a message arrived intact, we'll use checksums. -Every packet sent will include a checksum of itself. -At the receiving end, the checksum can be computed again and checked against the one sent with the packet. - -:::tip Checksums, a checksummary - -A checksum is a computed value that depends on the value of some *input data*. -In our case, the input data is all the bytes that make up a packet. -In other words, every byte of the packet influences the sum. - - -The packet includes a field for such a checksum, which is initialised to `0`. -The checksum is computed using the whole packet -- including the zero -- and the result is written to the checksum field. -When the packet checksum is recomputed now, the result will be zero! -This is a common feature of popular checksums because it makes checking if data is intact so simple. +An interrupt is a way to run a certain piece of code when an external event occurs. +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. +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. ::: -Checking the packet checksum will indicate if the message was damaged, but only the receiver will have this information. -To inform the sender we'll make a rule that every message transfer must be followed by a delivery *report*. -In terms of information, a report is a boolean value -- either the message was received intact, or not. -Because reports are so simple -- but very important -- we'll employ a simple technique to deliver them reliably. -Define two magic numbers -- one to send when the packet checksum matched and another for if it didn't. -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*. +### A little protocol + +Before diving into (more) implementation, let's take a minute to describe a *protocol*. +A protocol is a set of rules that govern communication. -To increase the likelihood of the report getting interpreted correctly, we'll simply repeat the value multiple times. -At the receiving end, check each received byte -- finding just one byte equal to `STATUS_OK` will be interpreted as *success*. +/// Everything is packets. +/// Packets: small chunks of data with rudimentary error detection -:::tip +For reliable data transfer, alternate between two message types: +protocol metadata in *SYNC* packets, and application data in *DATA* packets. +DATA packets are required to include a sequential message ID -- the rest is up to the application. +SYNC packets include the ID of the most recently received DATA packet. -The binary values used here should be far apart in terms of [*hamming distance*](https://en.wikipedia.org/wiki/Hamming_distance). -In essence, either value should be very hard to confuse for the other, even if some bits were randomly changed. +Packet integrity can be tested on the receiving end using a checksum. +Damaged packets of either type are discarded. +Successful delivery of a DATA packet is acknowledged when its ID is included in a SYNC packet. +The sender should retransmit the packet if successful delivery is not acknowledged. -::: +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 We'll implement some functions to facilitate constructing, sending, receiving, and checking packets. The packet functions will operate on the existing serial data buffers. @@ -382,7 +344,7 @@ It's probably not very suitable for real-world projects. ::: -## Using Sio +## /// Using Sio /// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different: ```console @@ -423,7 +385,7 @@ Other interrupts may be in use by other parts of the code, which are clearly out ::: Note that `LinkReset` starts part way through `LinkInit`. -This way the two functions can share code without zero overhead and `LinkReset` can be called without performing the startup initialisation again. +This way the two functions can share code with zero overhead and `LinkReset` can be called without performing the startup initialisation again. This pattern is often referred to as *fallthrough*: `LinkInit` *falls through* to `LinkReset`. Call the init routine once before the main loop starts: @@ -432,41 +394,61 @@ Call the init routine once before the main loop starts: call LinkInit ``` +/// `LinkTx`, alternate between sending the two types of packet: -### Link impl go - -```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-status}} -{{#include ../../unbricked/serial-link/main.asm:link-send-status}} -``` - -```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-test-data}} -{{#include ../../unbricked/serial-link/main.asm:link-send-test-data}} +```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-message}} +{{#include ../../unbricked/serial-link/main.asm:link-send-message}} ``` - /// Implement `LinkUpdate`: ```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-update}} {{#include ../../unbricked/serial-link/main.asm:link-update}} ``` -/// update Sio every frame... +/// reset the demo if the B button is pressed + +/// update Sio every frame + +In the `LINK_INIT` state, do handshake until its done. +Once the handshake is complete, change to the `LINK_UP` state. + +/// In the `LINK_UP` state, ... + +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}} +``` + +/// We flush Sio's state (set it to `SIO_IDLE`) here so ... LinkTx ... next update... + +Check that we received a packet and its checksum matches. +If the packet integrity test comes back negative, increment the inbound transmission errors counter. + +:::tip + +/// This class of error -- which we're calling a *transmission errors* -- are very significant. +/// assuming code works -- & have reason to believe it does -- this means the data was damaged during transmission, if a valid packet was sent -/// in the `INIT` state, do handshake until its done. +/// You might want to do something a bit more sophisticated than counting errors in a real-world application. -Once the handshake is complete, change to the `READY` state and notify the other device. +::: -/// in any of the other active states, reset if the B button is pressed +/// with the packet checking out OK, move on to process the message it contains +/// jump to the appropriate handler ... +/// if the msg type is unknown/unexpected, increment the message/protocol error counter. -/// finally we jump to different routines based on Sio's transfer state +/// SYNC messages... +--- - -/// **(very) TODO:** handling received messages... +/// TEST_DATA messages... +--- -### Handshake +### Implement the handshake protocol /// Establish contact by trading magic numbers @@ -486,37 +468,28 @@ Once the handshake is complete, change to the `READY` state and notify the other {{#include ../../unbricked/serial-link/main.asm:handshake-begin}} ``` -/// Every frame, handshake update - ```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:handshake-update}} {{#include ../../unbricked/serial-link/main.asm:handshake-update}} ``` -/// If `wHandshakeState` is zero, handshake is complete - -/// If the user has pressed START, abort the current handshake and start again as the clock provider. - -/// 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. +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. -:::tip The DIV register +If a transfer is completed, process the received data by jumping to `HandshakeMsgRx`. -/// is not particularly random... - -/// but we just need the value to be different when each device reads it, and for the value to occasionally be an odd number - -::: - -/// If a transfer is complete (`SIO_DONE`), jump to `HandshakeMsgRx` (described below) to check the received value. +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}} ``` -/// First byte must be `MSG_SHAKE` - -/// Second byte must be `wHandshakeExpect` - -/// If the received message is correct, set `wHandshakeState` to zero +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 diff --git a/unbricked/serial-link/main.asm b/unbricked/serial-link/main.asm index a61986c0..158234a8 100644 --- a/unbricked/serial-link/main.asm +++ b/unbricked/serial-link/main.asm @@ -43,6 +43,7 @@ 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 @@ -179,7 +180,7 @@ LinkUpdate: cp a, SIO_FAILED jp z, LinkError cp a, SIO_IDLE - jp z, SendNextMessage + jp z, LinkTx ret .link_init ld a, [wHandshakeState] @@ -257,7 +258,7 @@ LinkDisplay: ; ANCHOR: link-send-message -SendNextMessage: +LinkTx: ld hl, wPacketCount ld a, [hl] inc [hl] @@ -286,6 +287,7 @@ SendNextMessage: ; ANCHOR_END: link-send-message +; ANCHOR: link-receive-message ; Process received data ; @mut: AF, BC, HL LinkRx: @@ -340,6 +342,7 @@ LinkRx: ld a, [hl+] ld [wRxValue], a ret +; ANCHOR_END: link-receive-message LinkError: @@ -748,7 +751,8 @@ HandshakeMsgRx: jr nz, HandshakeSendPacket ret .failed - ld a, $FF + ld a, [wHandshakeState] + or a, HANDSHAKE_FAILED ld [wHandshakeState], a ret ; ANCHOR_END: handshake-xfer-complete