Copy Link
Add to Bookmark
Report

Articles 2 - An IRQ Loader

DrWatson's profile picture
Published in 
Commodore64
 · 16 May 2021
Terminator 2 for the Commodore 64
Pin it
Terminator 2 for the Commodore 64

I'll report here a complete study of the Loader used in Terminator 2

I'll assume you are familiar with hardware interrupts and ISRs (you don't absolutely require to know how they work on a C64, but a small knowledge about interrupts in general is enough). If you know about any data-link layer networking protocol, it can be useful (for understanding things better) to compare the datasette to a network adapter. Import the problems of framing you have on the data-link layer (which is equivalent to our loader) and adapt them to our study.

First, here's a summary of what we need to do when writing an IRQ-based loader:

We first need to disable the system of interrupts, by setting the interrupt disable status bit (this is done by a SEI instruction).

Then we have to disable all interrupts individually (by WRITING to $DC0D, which is an Interrupt Control Register when written to) and clear any latched interrupt request (by READING the clear-on-read register $DC0D, which is an Interrupt Latch Register when read from- eg. bit 1 is set when CIA #1 Timer B countdown expires).

Now we have to set the start value of the timer we'll be using to measure the length of the pulses coming from the tape (CIA #1 Timer A was chosen in the discussed loader). That's done by WRITING the start value in $DC04/$DC05 (which is the CIA #1 16-bit Timer A latch value). The timer will count down to zero starting from the value we just chose (one-shot mode). We'll restart the countdown every time we received a pulse, to measure the pulse that will come after the one we just measured.

Then we have to enable the FLAG line interrupt (the interrupt that triggers when a pulse is read from the datasette). The interrupt won't trigger until we enable the system of interrupts. Before doing that, we have to declare where our Interrupt Service Routine is (by making the vector at $FFFE/$FFFF point to our ISR).

After enabling interrupts (CLI instruction), we are ready to measure the pulses coming from the datasette, align our read routine with the bit stream (using the pilot byte information), syncronize (ie. know where exactly the turbo frame starts), and finally read the header which tells us where to store the following data bytes in the RAM.

Here you find the complete disassembly of the code stored in the CBM Header and Data files:

  
; ********************************************
; * Loader Setup-Part 1 *
; * Description: Hardware setup instructions *
; ********************************************
02A7 78 SEI ; Disable interrupts, since we are about to
; change the vector table at $FFFA-$FFFF, whose
; vectors point to 2 Interrupt Service Routines.

02A8 A9 05 LDA #$05 ; Select ROM at $A000 (bit 0)
02AA 85 01 STA $01 ; and switch in I/O devices (bit 2).

02AC A9 1F LDA #$1F ; CIA #1 Interrupt Control Register reset:
02AE 8D 0D DC STA $DC0D ; disable Timer A interrupt (bit 0)
; disable Timer B interrupt (bit 1)
; disable TOD clock alarm interrupt (bit 2)
; disable serial shift register interrupt (bit 3)
; disable FLAG line interrupt (bit 4)

02B1 AD 0D DC LDA $DC0D ; Clear Interrupt Latch to prevent servicing
; interrupt requests not requested by our program.
; This register is clear-on-read.

02B4 A9 7C LDA #$7C ; CIA #1 Timer A Latch value setup.
02B6 8D 04 DC STA $DC04
02B9 A9 04 LDA #$04
02BB 8D 05 DC STA $DC05 ; (Threshold=$027C clock cycles)

02BE A9 90 LDA #$90 ; CIA #1 Interrupt Control Register setup:
02C0 8D 0D DC STA $DC0D ; enable just FLAG line interrupt (bit 4) (1)

; (1) This FLAG line is connected to the Cassette Read line of the Cassette Port.
; The interrupt triggers on negative edges.

02C3 A9 51 LDA #$51 ; Maskable Interrupt Request Vector setup:
02C5 8D FE FF STA $FFFE ; make this vector point to our IRQ handler (ISR)
02C8 A9 03 LDA #$03 ; located at $0351, so that the only active
02CA 8D FF FF STA $FFFF ; Interrupt (FLAG line) will cause its execution
; on request.

02CD A9 00 LDA #$00 ; Initialization of:
02CF 85 02 STA $02 ; loop_break variable (see later)
02D1 85 03 STA $03 ; buffer where to build a byte, pulse by pulse.

02D3 EA NOP

02D4 4C E5 02 JMP $02E5 ; Jump to Part 2
; ********************************************
; * Loader Setup-Part 1.END *
; ********************************************

; ********************************************
; * Checksum check subroutine *
; * Description: Compares calculated and *
; * read checksum to detect a *
; * load error. *
; ********************************************

02D7 A9 07 LDA #$07
02D9 85 01 STA $01
02DB A5 05 LDA $05
02DD C5 06 CMP $06
02DF D0 01 BNE $02E2
02E1 60 RTS

02E2 4C E2 FC JMP $FCE2 ; On checksum error, reset C64
; ********************************************
; * Checksum check subroutine.END *
; ********************************************

; ********************************************
; * Loader Setup-Part 2 *
; * Description: Hardware setup instructions *
; ********************************************

02E5 A9 E7 LDA #$E7 ; Non-Maskable Interrupt Hardware Vector setup:
02E7 8D FA FF STA $FFFA ; make it point to our Load Loop at $03E7. (2)
02EA A9 03 LDA #$03
02EC 8D FB FF STA $FFFB

; (2) There are two possible sources for an NMI interrupt. The first is the
; RESTORE key, which is connected directly to the 6510 NMI line. The
; second is CIA #2, the interrupt line of which is connected to the 6510
; NMI line.


02EF A9 01 LDA #$01 ; Set CIA #2 Timer A high byte
02F1 8D 05 DD STA $DD05

02F4 A9 81 LDA #$81 ; CIA #2 Interrupt Control Register setup:
02F6 8D 0D DD STA $DD0D ; enable Timer A interrupt (bit 0)

02F9 A9 99 LDA #$99 ; CIA #2 Control Register A setup:
02FB 8D 0E DD STA $DD0E ; start timer A (bit 0)
; Timer A run mode is one-shot (bit 3)
; Force latched value to be
; loaded to Timer A counter (bit 4)

02FE D0 FE BNE $02FE ; C64 should hang here, but CIA #2 Timer A
; expiration causes the NMI request, which makes
; Program Counter move to $03E7.

; ********************************************
; * Loader Setup-Part 2.END *
; ********************************************

; *****************************
; * BASIC RAM vector area (3) *
; *****************************
0300 8B 03 01 E3
0302 A7 02
...
0332 ED F5

; (3) Several important BASIC routines are vectored through RAM. Vectors
; to all of these routines can be found in the indirect vector table.
; The turbo loader changes those vectors to execute itself when the
; CBM file is fully loaded (this is called "AUTOSTART").



; ***************************************************************
; * ISR *
; * Description: Interrupt Service Routine that handles FLAG *
; * line interrupts *
; ***************************************************************

; Each interrupt is triggered by a pulse read from tape, so we need to
; compare it's size (counted by a timer) with a Threshold value, to
; decide if it's a Bit 0 pulse or Bit 1 pulse.

0351 48 PHA ; We'll be using A and Y registers
0352 98 TYA ; so we save them on the processor stack,
0353 48 PHA ; just as every Interrupt Service Routine does.

0354 AD 20 D0 LDA $D020 ; Perform border flash among 2 colors
0357 49 05 EOR #$05
0359 8D 20 D0 STA $D020

035C AD 05 DC LDA $DC05 ; Read the Timer value

035F A0 19 LDY #$19 ; CIA #1 Control Register A re-initialized
0361 8C 0E DC STY $DC0E ; for the next pulse measurement:
; Start Timer A (bit 0)
; Timer A run mode: continuous (bit 3)
; Force latched value to be
; loaded to Timer A counter (bit 4)

0364 49 02 EOR #$02 ; This piece of code subtracts $200 clock cycles
0366 4A LSR ; from the Timer value. (4)
0367 4A LSR ; Carry is set when pulse is bigger than Threshold
; ie. [Latch value - $200] clock cycles.

0368 26 03 ROL $03 ; Group bits with MSb First

036A A5 03 LDA $03
036C 90 02 BCC $0370 ; IF AND ONLY IF the last bit of a byte was just
; read, a 0 will be moved from bit 7 of $03
; to the Carry by the "ROL $03" instruction,
; otherwise the Carry will be set (see code
; at $0379 to understand why).
; Therefore Carry is set IFF a complete byte
; is not yet available.(5)
; If a complete byte is available, it is kept
; by the A register.

; (4) Why not to use the SBC instruction to subtract?
; Answer: with SBC we should invert the carry bit (that holds
; the borrow at the end of the instruction) to use it in the
; following "ROL $03" instruction.
; Also remember that SBC would need a CLC before it and that it
; affects more Processor Status register bits (N, Z, C, and V).

; (5) This is a self-modified Branch which branches to different addresses during load,
; to properly use the available byte just read.
; It's a VERY common thing in IRQ loaders to use a self-modifying branch there.
; When we are waiting for the FIRST Pilot Byte (to align the byte-oriented loader
; to the bit-oriented pulse storage method), this branches to $0370.
; When alignment was done, we need to read in the whole pilot sequence and the
; Sync Byte, so that this branch branches to $0384.
; When Sync Byte is found, we read a single byte we don't even use, at $0399.
; And so on...

036E B0 0D BCS $037D ; Always jumps

; -----------------------------------------------------------------------------------
0370 C9 40 CMP #$40 ; Check if this byte is the FIRST Pilot Byte
0372 D0 09 BNE $037D
0374 A9 16 LDA #$16
0376 8D 6D 03 STA $036D ; Change the branch at $036C, to jump to $0384
; -----------------------------------------------------------------------------------

; This code is executed everytime we exit from the ISR (with the RTI).
;

0379 A9 FE LDA #$FE ; This will cause the "ROL $03" instruction to
037B 85 03 STA $03 ; always set Carry if a whole byte was not yet
; built in the byte buffer at $03.

037D AD 0D DC LDA $DC0D ; Clear Interrupt Latch.
; This register is clear-on-read.

0380 68 PLA ; Pop the values of A and Y registers from
0381 A8 TAY ; the Processor stack before returning.
0382 68 PLA

0383 40 RTI
; ***************************************************************
; * ISR.END *
; ***************************************************************

; ********************************************
; * Read Pilot train and Sync byte *
; ********************************************
0384 C9 40 CMP #$40 ; Read in the whole Pilot Byte sequence
0386 F0 F1 BEQ $0379 ; and stop when we read a different byte,
0388 C9 5A CMP #$5A ; checking if it is the Sync Byte
038A F0 02 BEQ $038E

038C D0 52 BNE $03E0 ; If the Sync Byte doesn't match, retry
; alignment (seek the FIRST Pilot Byte again).

038E A9 2B LDA #$2B
0390 8D 6D 03 STA $036D ; Change the branch at $036C, to jump to $0399
0393 A9 00 LDA #$00
0395 85 05 STA $05
0397 F0 E0 BEQ $0379 ; (6)
; ********************************************
; * Read Pilot train and Sync byte.END *
; ********************************************

; ********************************************
; * Read an unused byte *
; ********************************************
0399 A9 32 LDA #$32 ; Read byte is unused.
039B 8D 6D 03 STA $036D ; Change the branch at $036C, to jump to $03A0
039E D0 D9 BNE $0379 ; (6)
; ********************************************
; * Read an unused byte.END *
; ********************************************

; ********************************************
; * Read Header bytes *
; ********************************************
03A0 85 07 STA $07 ; Load header at $07..$0A:
03A2 EE A1 03 INC $03A1 ; 2 bytes: Load address
03A5 AD A1 03 LDA $03A1 ; 2 bytes: End address+1
03A8 C9 0B CMP #$0B
03AA D0 CD BNE $0379
03AC A9 45 LDA #$45
03AE 8D 6D 03 STA $036D ; Change the branch at $036C, to jump to $03B3
03B1 D0 C6 BNE $0379 ; (6)
; ********************************************
; * Read Header bytes.END *
; ********************************************

; ********************************************
; * Read Data bytes *
; ********************************************
03B3 A0 00 LDY #$00
03B5 91 07 STA ($07),Y ; Load data into memory
03B7 45 05 EOR $05 ; Compute checksum
03B9 85 05 STA $05
03BB E6 07 INC $07
03BD D0 05 BNE $03C4
03BF E6 08 INC $08

03C1 EE 20 D0 INC $D020 ; Change the border flash base colors

03C4 A5 07 LDA $07 ; Check if we finished
03C6 C5 09 CMP $09
03C8 A5 08 LDA $08
03CA E5 0A SBC $0A
03CC 90 AB BCC $0379
03CE A9 67 LDA #$67
03D0 8D 6D 03 STA $036D ; Change the branch at $036C, to jump to $03D5
03D3 D0 A4 BNE $0379 ; (6)
; ********************************************
; * Read data bytes.END *
; ********************************************

; ********************************************
; * Read Checksum byte *
; ********************************************
03D5 85 06 STA $06 ; Load checksum byte

03D7 A9 FF LDA #$FF ; Sets the loop_break variable
03D9 85 02 STA $02

03DB A9 07 LDA #$07 ; Restore the vector to where store next header
03DD 8D A1 03 STA $03A1

03E0 A9 02 LDA #$02 ; Restore the Branch at $036C to seek the FIRST
03E2 8D 6D 03 STA $036D ; Pilot Byte.
03E5 D0 92 BNE $0379 ; (6)
; ********************************************
; * Read Checksum byte.END *
; ********************************************

; (6) This branch is always executed, and it's a trick to avoid using a JMP, which
; is not relocatable (since it requires the hard memory address where to jump to,
; instead of an offset from Program Counter, as the branch instructions do).


; ***************************************************************
; * NMI-ISR *
; * Description: keeps the CPU in a loop during which the FLAG *
; * line interrupts are serviced. *
; * Executes code $0407 as soon as the load loop *
; * is over. *
; ***************************************************************
03E7 58 CLI ; Enable interrupts since we are ready to service
; our FLAG line interrupt requests.

03E8 A9 58 LDA #$58 ; Change the NOP at $02D3 into a CLI
03EA 8D D3 02 STA $02D3 ; to skip Part 2 of setup on next block load.

03ED A9 0B LDA #$0B ; Show screen
03EF 8D 11 D0 STA $D011

03F2 A5 02 LDA $02 ; Load Loop. The CPU loops here, waiting
03F4 F0 FC BEQ $03F2 ; FLAG line interrupts to serve or a
; loop_break instruction (= performed when
; any bit in $02 memory register is set).

03F6 C6 02 DEC $02
03F8 4C 07 04 JMP $0407
03FB 20
; ***************************************************************
; * NMI-ISR.END *
; ***************************************************************

It should now be clear that this loader has the following structure:

  
Threshold: $027C clock cycles (Tap value=$50)
Endianess: MSbF

Pilot Byte: $40
Start of payload Byte (1): $5A

Header:
1 byte: unused
2 bytes: Load address (LSBF)
2 bytes: End address+1 (LSBF)

Data

1 byte: XOR checksum

(1) better known as "Sync Byte".


By looking at the TAP file, we can also say that:

  
Bit 0: $36
Bit 1: $65

A small analisys on the TAP file will also tell us if this loader uses any trailer pulse.
I hope this will be appreciated and useful.

Kind regards,

Luigi

In an effort to clarify the topic, I made a picture showing you the details about the timings for this loader.

Terminator 2 loader
Pin it
Terminator 2 loader
← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT