Skip to content

Commit 0864987

Browse files
committed
Move serial interrupt handler out of Sio
- All interrupt stuff now in one place (Link / main.asm) - Consolidate interrupt explanation in the Link section - Explain interrupts too much and too little
1 parent 84b17eb commit 0864987

File tree

3 files changed

+84
-62
lines changed

3 files changed

+84
-62
lines changed

src/part2/serial-link.md

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,6 @@ The received value is copied from the serial port (`rSB`) to Sio's buffer (`wSio
224224
If there are still bytes to transfer (transfer counter is greater than zero) the next value is loaded from `wSioBufferTx` and the transfer is started by `SioPortStart`.
225225
Otherwise, if the transfer counter is zero, enter the `SIO_DONE` state.
226226

227-
`SioPortEnd` must be called once after each byte transfer.
228-
To do this we'll use the serial interrupt:
229-
230-
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}}
231-
{{#include ../../unbricked/serial-link/sio.asm:sio-serial-interrupt-vector}}
232-
```
233-
234-
This is an interrupt handler -- a special piece of code that gets called by the CPU under certain conditions.
235-
The serial interrupt occurs when the serial port completes a transfer.
236-
237-
All this code does is invoke `SioPortEnd` safely.
238-
The state of the CPU registers that `SioPortEnd` modifies are preserved by *pushing* them to the stack before the `call`, then restored by *popping* them off the stack afterwards.
239-
If we didn't do this, the interrupted code would likely break when the registers are suddenly modified out of nowhere.
240-
241227

242228
## Interval
243229

@@ -376,45 +362,31 @@ It's probably worth looking into better solutions for real-world projects.
376362
:::
377363

378364

379-
## /// Using Sio
380-
381-
/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different:
382-
```console
383-
$ rgbasm -o sio.o sio.asm
384-
$ rgbasm -o main.o main.asm
385-
$ rgblink -o unbricked.gb main.o sio.o
386-
$ rgbfix -v -p 0xFF unbricked.gb
387-
```
365+
## Connecting it all together
388366

389367
<!-- "Link" -->
390-
/// serial link features: *Link*
368+
It's time to build the application-level link features.
369+
We're going to
370+
- implement the protocol logic
371+
- implement the handshake
372+
- build a demo/test program
391373

392374
/// tiles
393375

394376
/// defs
395377

378+
396379
<!-- LinkInit -->
397-
/// one function to initialise basic serial link state.
380+
In main.asm (code section)...
398381

399-
/// Implement `LinkInit`:
382+
Implement `LinkInit`:
400383

401384
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-init}}
402385
{{#include ../../unbricked/serial-link/main.asm:link-init}}
403386
```
404387

405-
Calling `SioInit` prepares Sio for use, except for one thing: **e**nabling **i**nterrupts with the `ei` instruction.
406-
407-
:::tip
408-
409-
If interrupts must be enabled for Sio to work fully, you might be wondering why we don't just do it in `SioInit`.
410-
Sio is in control of the serial interrupt, but `ei` enables interrupts globally.
411-
Other interrupts may be in use by other parts of the code, which are clearly outside of Sio's responsibility.
412-
413-
/// Sio doesn't enable or disable interrupts because side effects ...
414-
415-
/// [Interrupts](https://gbdev.io/pandocs/Interrupts.html)
416-
417-
:::
388+
- Initialise Sio by calling `SioInit`.
389+
- enable serial interrupt and interrupts globally
418390

419391
Note that `LinkReset` starts part way through `LinkInit`.
420392
This way the two functions can share code with zero overhead and `LinkReset` can be called without performing the startup initialisation again.
@@ -426,6 +398,43 @@ Call the init routine once before the main loop starts:
426398
call LinkInit
427399
```
428400

401+
402+
<!-- Serial interrupt -->
403+
Sio needs to be told when to process each completed byte transfer.
404+
The best way to do this is by using the serial interrupt.
405+
Copy this code (it needs to be exact) to `main.asm`, just above the `"Header"` section:
406+
407+
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:serial-interrupt-vector}}
408+
{{#include ../../unbricked/serial-link/main.asm:serial-interrupt-vector}}
409+
```
410+
411+
A proper and complete explanation of this is beyond the scope of this lesson.
412+
You can continue the lesson understanding that:
413+
- This is the serial interrupt handler. It gets called automatically after each serial transfer.
414+
- The relevant stuff is in `SioPortEnd` but it's necessary to jump through some hoops to call it.
415+
416+
A detailed and rather dense explanation is included for completeness.
417+
418+
:::tip
419+
420+
*You can just use the code as explained above and skip past this box.*
421+
422+
An interrupt handler is a piece of code at a specific address that gets called automatically under certain conditions.
423+
The serial interrupt handler begins at address `$58` so a section just for this function is defined at that location using `ROM0[$58]`.
424+
Note that the function is labelled by convention and for debugging purposes -- it isn't technically meaningful and the function isn't intended to be called manually.
425+
426+
Whatever code was running when an interrupt occurs literally gets paused until the interrupt handler returns.
427+
The registers used by `SioPortEnd` need to be preserved so the code that got interrupted doesn't break.
428+
We use the stack to do this -- using `push` before the call and `pop` afterwards.
429+
Note that the order of the registers when pushing is the opposite of the order when popping, due to the stack being a LIFO (last-in, first-out) container.
430+
431+
`reti` returns from the function (like `ret`) and enables interrupts (like `ei`) which is necessary because interrupts are disabled automatically when calling an interrupt handler.
432+
433+
If you would like to continue digging, have a look at [evie's interrupts tutorial](https://evie.gbdev.io/resources/interrupts) and [on pandocs](https://gbdev.io/pandocs/Interrupts.html).
434+
435+
:::
436+
437+
429438
/// `LinkTx`, alternate between sending the two types of packet:
430439

431440
```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/serial-link/main.asm:link-send-message}}
@@ -531,3 +540,14 @@ In a real application, you might want to consider:
531540
- sharing more information about each device and negotiating to decide the preferred clock provider
532541

533542
:::
543+
544+
545+
## /// Running the test ROM
546+
547+
/// Because we have an extra file (sio.asm) to compile now, the build commands will look a little different:
548+
```console
549+
$ rgbasm -o sio.o sio.asm
550+
$ rgbasm -o main.o main.asm
551+
$ rgblink -o unbricked.gb main.o sio.o
552+
$ rgbfix -v -p 0xFF unbricked.gb
553+
```

unbricked/serial-link/main.asm

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ DEF HANDSHAKE_FAILED EQU $F0
4747
; ANCHOR_END: handshake-codes
4848

4949

50+
; ANCHOR: serial-interrupt-vector
51+
SECTION "Serial Interrupt", ROM0[$58]
52+
SerialInterrupt:
53+
push af
54+
push hl
55+
call SioPortEnd
56+
pop hl
57+
pop af
58+
reti
59+
; ANCHOR_END: serial-interrupt-vector
60+
61+
5062
SECTION "Header", ROM0[$100]
5163

5264
jp EntryPoint
@@ -140,7 +152,14 @@ LinkInit:
140152
ld a, BG_CROSS
141153
ld [DISPLAY_RX_ERRORS - 1], a
142154
call SioInit
143-
ei ; Sio requires interrupts to be enabled.
155+
156+
; enable the serial interrupt
157+
ldh a, [rIE]
158+
or a, IEF_SERIAL
159+
ldh [rIE], a
160+
; enable interrupt processing globally
161+
ei
162+
144163
LinkReset:
145164
call SioReset
146165
ld a, LINK_INIT

unbricked/serial-link/sio.asm

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,32 +78,14 @@ wSioTimer:: db
7878
; ANCHOR_END: sio-state
7979

8080

81-
; ANCHOR: sio-serial-interrupt-vector
82-
SECTION "Sio Serial Interrupt", ROM0[$58]
83-
SerialInterrupt:
84-
push af
85-
push hl
86-
call SioPortEnd
87-
pop hl
88-
pop af
89-
reti
90-
; ANCHOR_END: sio-serial-interrupt-vector
91-
92-
9381
; ANCHOR: sio-impl-init
9482
SECTION "SioCore Impl", ROM0
9583
; Initialise/reset Sio to the ready to use 'IDLE' state.
96-
; NOTE: Enables the serial interrupt.
97-
; @mut: AF, [IE]
84+
; @mut: AF, C, HL
9885
SioInit::
9986
call SioReset
10087
ld a, SIO_IDLE
10188
ld [wSioState], a
102-
103-
; enable serial interrupt
104-
ldh a, [rIE]
105-
or a, IEF_SERIAL
106-
ldh [rIE], a
10789
ret
10890

10991

@@ -238,10 +220,11 @@ SioPortStart:
238220

239221

240222
; ANCHOR: sio-port-end
241-
; Collects the received value and starts the next transfer, if there is any.
242-
; To be called after the serial port deactivates itself / serial interrupt.
223+
; Collects the received value and starts the next byte transfer, if there is more to do.
224+
; Sets wSioState to SIO_DONE when the last expected byte is received.
225+
; Must be called after each serial port transfer (ideally from the serial interrupt).
243226
; @mut: AF, HL
244-
SioPortEnd:
227+
SioPortEnd::
245228
; Check that we were expecting a transfer (to end)
246229
ld hl, wSioState
247230
ld a, [hl+]

0 commit comments

Comments
 (0)