Analysis of two commercial C64 tape loaders


This document is about commercial C64 tape loaders.

I think a short introduction about the C64 I/O hardware is required in order to deeply understand the meaning of data in the Tap file format, which is not just a hardware-independent array of bytes.

Data encoding

This paragraph describes data transfer between the C64 processor and the cassette unit (aka C2N, aka Datassette).

Each bit of data or program sent to the C2N is encoded by the operating system using audio frequencies. Specifically, square waves with a 50% duty cycle, often referred to as pulses (this is also the term I’ll often be using moving forward), are used.
A sequence of bits is therefore encoded as a sequence of square waves on tape, back-to-back.

The standard Commodore encoding method uses three distinct pulses:

  • “long pulses” with a frequency of 1488 Hz (period = 672 microseconds about),
  • “medium pulses” at 1953 Hz (period = 512 microseconds about),
  • “short pulses” at 2840 Hz (period = 352 microseconds about).

It is evident that each name refers to the period (i.e. 1/frequency) of the square wave rather than to its frequency.
Data bits are encoded by a couple of pulses (medium and short pulses are used). The structure of this loader is discussed in the CBM Loader article.

Sample of pulses coming from C2N READ pin during a CBM file reading:

..      ____        ______      ____   
  |    |    |      |      |    |    | 
  |    |    |      |      |    |    | 
  |____|    |______|      |____|    |..

Since pulse length detection triggers on descending (negative) edges, this sample produces the following sequence of pulses:

   <-352 us-><-  512 us  -><-352 us->
  |         |             |         |
  |         |             |         |
     short       medium      short

On the other side, a Turbo Loader usually uses just two pulses:

  • “short pulse”
  • “long pulse”

whose frequencies are chosen by its designer. The length of a pulse saved on tape decides whether the bit is a 1 or a 0. In fact, these loaders don’t usually use a sequence of pulses to encode a bit: just a single pulse per bit.

NOTE about the hardware

One thing to point out is that during a SAVE operation, on the WRITE line of the Datasette port, “pulse length” is intended as the time distance between two consecutive low-high transitions. During a LOAD operation, pulse length is the time distance between two consecutive high-low transitions, since the 6526 READ line triggers on negative edges. In other words, the signal from C2N to C64 is the negative version of the one from C64 to C2N. That’s why tape duplication hardware consists in an inverter (with a BJT and 2 resistors used in a so called “common emitter” scheme): the signal from the C2N performing LOAD, intended for being sent to C64, needs being inverted before being sent to the C2N doing the SAVE operation.

Check at the end of this article for additional information on CIA 1 + 2 and Vectors.

TAP format

This paragraph summarizes the Tap file format purposes. For a detailed discussion about this topic you may consult the “tapformat.html” file in your CCS64 Emulator folder or online at the “computerbrains” CCS64 home page.

Designed by Per Hakan Sundell (author of the CCS64 C64 emulator) in 1997, this format attempts to duplicate the data stored on a C64 cassette tape, pulse after pulse. The difference from a WAV sampling of any C64 tape and its Tap file data is that Tap file is not a sampled version of the waveforms stored on tape.

Each nonzero byte in the Tap file data area represents the time length of a single pulse. The conversion formula is given here:

Tap Data Byte=Pulse length (expressed in seconds)*C64 PAL Frequency/8

where “C64 Pal Frequency” is 985248 Hz. By calculating the constant 1E-6*C64 PAL Frequency/8, the following equivalent formula can be produced:

Tap Data Byte=Pulse length (microseconds)*0.123156

where “Pulse length” is the time interval between two negative edges of the received square wave. As example, CBM pulses correspond to the following TAP values:

short:  352 * 0.123156 = 43.35 = $2B
medium: 512 * 0.123156 = 63.01 = $3F
long:   672 * 0.123156 = 82.76 = $53

It is clear that this conversion introduces some information alteration due to quantization: two pulses with similar length may produce the same Tap value.

NOTE about CBM pulses size: Tap imports of older tapes show a better consistence with the above mentioned values than younger ones. Anyway the operating system can produce a correction factor which allows a very wide variation in tape speed without affecting reading. In fact, the sequence of short pulses written on the CBM leader is used to synchronize the read routine timing to the timing on the tape.

Commercial tape loaders

We’ll have a general discussion here about Turbo Loaders and C64 I/O dedicated hardware.

Almost every marketed C64 tape software uses some form of Turbo Loader. The origin of these Turbo Loaders is rather obscure since many of the software houses use the same routines.

A Turbo Loader is a routine which must be loaded into C64 RAM before being executed and therefore every Turbo Loader routine is stored in a Standard CBM encoded “boot” file. Usually a part of the Turbo Loader routines is stored in the CBM file Header and therefore loaded in the tape buffer (at $033C-$03FB). CBM file Data is often used both for other Turbo Loader routines and to modify the table of vectors in low RAM, to cause the autostart of the turbo loader itself (eg. it may modify $0326/$0327 where the output vector is located). When the standard LOAD ends, the operating system executes various operations, one of which is printing the “READY.” message on the C64 screen. By default, at $0326/$0327 there’s the start address of the onscreen print routine (remember any of the 64K memory addresses can be identified by 2 bytes, low significant part first and then most significant. As example, the couple of values 01-08 is a pointer to $0801). If CBM loader loads Data block overwriting this vector with the Turbo Loader start address, the operating system, instead of printing the “READY.” message at the end of CBM LOAD, executes the Turbo Loader.

When executed, a Turbo Loader “replaces” the existing LOAD and allows a program or data to be loaded from tape at a faster speed than the normal LOAD. This is achieved by simply reducing the length of pulses stored onto the tape, in order to allow a far greater density of information storage per inch of tape.

Each bit is flagged in the interrupt register on the falling (negative) edge of the pulse. A widely used Turbo loader scheme runs with the interrupts disabled, sets a timer to between the two lengths (which we will refer as “threshold” value), and when the timer runs out, the interrupt register is checked to see if the pulse came in or not. If the falling edge of the pulse sets the relevant interrupt flag before the timer runs out then the pulse was a “short” pulse (usually identified with bit zero), otherwise it was a “long” one (bit one). Bits are then rotated into a byte storage until 8 bits have been read, thereby loading a full byte. This rotation may be to left or right, which establishes endianness: MSbF or LSbF.

Before any byte can be read and stored, the Turbo Loader must set itself to be in sync with the bits on the tape. This is done by writing a certain string of bits at every byte interval. The routine then tries to align itself by recognizing the value of the byte. An example of a header byte for aligning would be the value 64, hex $40 or in binary: 01000000. A series of these bytes is written as the header; only when this byte has been read in a number of times consecutively, the actual program can be read without risk of alignment errors.

Non-IRQ tape loader: Mage-save

We document here a “non-IRQ based” loader step-by-step. Before starting with this reading it’s necessary to have a good knowledge of CIA Timers. I reported in Appendix A and B an extract (I did some changes where needed) from MAPC6410.TXT (the Project 64 etext of the “Mapping The Commodore 64 book”). Those paragraphs just cover the CIAs use we are interested in.
In addition to CIA Timers, you might want to have a copy of the “Commodore 64 Programmer’s Reference Guide” close by, to consult it while studying the ASM code.

Here we’ll see how this Turbo Loader performs the operations we discussed before. Please consult the documentation about this loader (formerly known as CHR Loader T1) coming with Stewart Wilson’t Final TAP. Get a Tap version of “Cauldron” if you want to extract yourself the listings I report here.

CHR Loader T1-T3 routines are partly stored in the CBM file Header. CBM file Data (loaded at $02A7-0303) stores other routines and is used to cause the autostart of this loader. The autostart feature is performed by using the IMAIN Vector at $0302-0303. By default, this vector points to the address of the main BASIC program loop at $A483. This is the routine that is operating when you are in the direct mode (READY.). It executes statements, or stores them as program lines. Cauldron Loader sets the IMAIN Vector to point to $02AE, therefore, when CBM LOAD ends, control is given to the Turbo Loader.

Using Final TAP to exam the mentioned Tap file, you can get the following listings.

* CBM Data block *

--New STOP routine (ignore it now)-
02A7  A9  80      LDA #$80
02A9  05  91      ORA $91
02AB  4C  EF  F6  JMP $F6EF

* Start of this Loader *
02AE  A9  A7      LDA #$A7
02B0  78          SEI
02B1  8D  28  03  STA $0328  ; Changes Vector to "Kernal STOP
02B4  A9  02      LDA #$02   ; Routine" into $02A7
02B6  8D  29  03  STA $0329

02B9  58          CLI

02BA  A0  00      LDY #$00   ; Inits some locations used by
02BC  84  C6      STY $C6    ; this loader
02BE  84  C0      STY $C0
02C0  84  02      STY $02

02C2  AD  11  D0  LDA $D011  ; Blanks screen
02C5  29  EF      AND #$EF
02C7  8D  11  D0  STA $D011

02CA  CA          DEX        ; A small pause here
02CB  D0  FD      BNE $02CA
02CD  88          DEY
02CE  D0  FA      BNE $02CA

02D0  78          SEI        ; Sets interrupt disable
                             ; status bit
02D1  4C  51  03  JMP $0351

--Read Bit subroutine--------------
02D4  AD  0D  DC  LDA $DC0D  ; Checks the interrupt register
02D7  29  10      AND #$10   ; to see if the pulse (negative
02D9  F0  F9      BEQ $02D4  ; edge on a C64) came in or not

02DB  AD  0D  DD  LDA $DD0D  ; Checks the countdown (bit 2 will
                             ; be 1 if countdown runned out)

02DE  8E  07  DD  STX $DD07  ; Sets a new Timer B countdown

02E1  4A          LSR        ; Move bit 2 to the Carry bit
02E2  4A          LSR
02E3  A9  19      LDA #$19   ; Starts Timer B (one shot, force
02E5  8D  0F  DD  STA $DD0F  ; latch value being loaded)
02E8  60          RTS

--Back to prompt-------------------
02E9  20  8E  A6  JSR $A68E  ; Resets the CHRGET pointer

02EC  A9  00      LDA #$00
02EE  A8          TAY
02EF  91  7A      STA ($7A),Y

02F1  4C  74  A4  JMP $A474  ; Prints Ready and then
                             ; processes keyboard buffer

02F4  52 D5 0D    ;"R", "SHIFT+U", "RETURN" (stays for "RUN", followed by RETURN key)

02F7  00 00 00 00 00 00 00 00 00

0300  8B E3       ; default value, not changed

0302  AE 02       ; used to perform the Autostart
* CBM Header block *

033C-0350  File details (see CBM File header)

-Loader's Core---------------------
0351  78          SEI

0352  A9  07      LDA #$07   ; Sets a Threshold value via
0354  8D  06  DD  STA $DD06  ; Timer B countdown
0357  A2  01      LDX #$01

0359  20  D4  02  JSR $02D4  ; Tries to align bits of leader
035C  26  F7      ROL $F7    ; with MSbF until...
035E  A5  F7      LDA $F7
0360  C9  63      CMP #$63   ; ... a Lead-in byte is found.
0362  D0  F5      BNE $0359
0364  A0  64      LDY #$64   ; Sync train start value

0366  20  E7  03  JSR $03E7
0369  C9  63      CMP #$63   ; Reads the whole leader
036B  F0  F9      BEQ $0366

036D  C4  F7      CPY $F7
036F  D0  E8      BNE $0359
0371  20  E7  03  JSR $03E7
0374  C8          INY        ; Reads the whole Sync train
0375  D0  F6      BNE $036D

0377  C9  00      CMP #$00   ; After Sync there's a Check byte
0379  F0  D6      BEQ $0351  ; if it is $00 then Reload

037B  20  E7  03  JSR $03E7
037E  99  2B  00  STA $002B,Y  ; Loads a 10 bytes header
0381  99  F9  00  STA $00F9,Y  ; The following code (at $0392,
0384  C8          INY          ; $039E, $03D1, $03C6) tells us
0385  C0  0A      CPY #$0A     ; us they consist in: Load
0387  D0  F2      BNE $037B    ; address, End address,
                               ; Execution address and 2 flag
                               ; bytes, which state if all
                               ; turbo files were loaded and
                               ; what to do once done

0389  A0  00      LDY #$00   ; Inits locations used to store
038B  84  90      STY $90    ; the Checksum info
038D  84  02      STY $02

--Load Loop------------------------
038F  20  E7  03  JSR $03E7    ; Reads a new byte

0392  91  F9      STA ($F9),Y  ; Stores it into RAM using the
                               ; Load address locations as
                               ; destination pointer

0394  45  02      EOR $02      ; computes the XOR Checksum
0396  85  02      STA $02      ; of Data

0398  E6  F9      INC $F9      ; Increases dest. pointer
039A  D0  02      BNE $039E
039C  E6  FA      INC $FA

039E  A5  F9      LDA $F9      ; Checks if dest. pointer (16
03A0  C5  2D      CMP $2D      ; bits) equals End Address
03A2  A5  FA      LDA $FA
03A4  E5  2E      SBC $2E
03A6  90  E7      BCC $038F    ; not yet finished? Restart!
--End of Load Loop-----------------

03A8  20  E7  03  JSR $03E7  ; Reads a closing byte (Checksum)

03AB  C8          INY
03AC  84  C0      STY $C0    ; Allows control of the motor
                             ; via software
03AE  58          CLI
03AF  18          CLC

03B0  A9  00      LDA #$00
03B2  8D  A0  02  STA $02A0

03B5  20  93  FC  JSR $FC93  ; Restores the Default IRQ
                             ; Routine. This subroutine
                             ; is used to turn the screen
                             ; back on and stop the cassette
                             ; motor.

03B8  20  53  E4  JSR $E453  ; Calls this subroutine to copy
                             ; the table of vectors to
                             ; important BASIC routines
                             ; to RAM, starting at location
                             ; $300. This prevents the Turbo
                             ; loader is executed again if
                             ; control is given back to the
                             ; BASIC interpreter.

03BB  A5  F7      LDA $F7  ; Checks checksum
03BD  45  02      EOR $02
03BF  05  90      ORA $90
03C1  F0  03      BEQ $03C6

03C3  4C  E2  FC  JMP $FCE2  ; A wrong checksum causes a SOFT
                             ; Reset

03C6  A5  31      LDA $31    ; First flag byte: other files
03C8  F0  03      BEQ $03CD  ; need to be loaded?
03CA  4C  B9  02  JMP $02B9

03CD  A5  32      LDA $32    ; Second flag byte: use the Exec.
03CF  F0  03      BEQ $03D4  ; address or give back control
                             ; to BASIC?

03D1  6C  2F  00  JMP ($002F)  ; Jumps to Exec. address

03D4  20  33  A5  JSR $A533  ; Relinks Lines of a BASIC
                             ; Program.

03D7  A2  03      LDX #$03   ; Puts 3 chars in the Keyboard
03D9  86  C6      STX $C6    ; Buffer

03DB  BD  F3  02  LDA $02F3,X  ; Those are "R", "SHIFT+U"
03DE  9D  76  02  STA $0276,X  ; and "RETURN"
03E1  CA          DEX
03E2  D0  F7      BNE $03DB
03E4  4C  E9  02  JMP $02E9

--Read byte subroutine-------------
03E7  A9  07      LDA #$07   ; 8 bits to read...
03E9  85  F8      STA $F8
03EB  20  D4  02  JSR $02D4
03EE  26  F7      ROL $F7    ; ...grouping them with MSbF
                             ; ROL retrieves the Carry
                             ; bit where incoming bit was
                             ; stored (code at $02E1)
03F0  EE  20  D0  INC $D020  ; Performs border flashing
03F3  C6  F8      DEC $F8
03F5  10  F4      BPL $03EB
03F7  A5  F7      LDA $F7
03F9  60          RTS
03FA  00 00

IRQ-based tape loader: Ocean New loader

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 essential). 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- e.g. bit 1 reads 1 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), synchronize (i.e. 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.

Disassembly of the code stored in the CBM Header and Data files of Terminator 2:

; ********************************************
; * 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


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


  N bytes: Loaded into RAM


  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 quick analysis on the TAP file will also tell us if this loader uses any trailer pulse.

Loader timings

The following picture shows the details about the timings for this loader:

Terminator 2 loader timings

Terminator 2 loader timings

Figuring out the threshold value

I’m referring to those loaders using an IRQ handler routine and FLAG line(1) interrupt. For some reason, the Threshold value for them is often omitted in docs. Here is a note about how to extract it. I will refer to the ASM code of a loader I just found, which uses CIA #1, Timer B. Other loaders may use a different combination, but the results are the same.

Before changing the vector to main IRQ handler (by writing to $FFFE/$FFFF) we have:

LDA #$1F       ; disable Timer A interrupt
STA $DC0D      ; disable Timer B interrupt
               ; disable TOD clock alarm interrupt
               ; disable serial shift register interrupt
               ; disable FLAG line(1) interrupt
LDA #$A0       ; Timer B Countdown start value
LDA #$03

The loader’s own IRQ handler looks this way:


LDY #$11       ; Re-Start Timer B
STY $DC0F      ; Force latched value to be loaded to Timer B counter
               ; Timer B counts microprocessor cycles

INC $D020

EOR #$02       ; Revert bit
ROR $A9        ; Move it to MSb of $A9 (Endianess: LSbF)
BCC done       ; Whole byte read?

What this code does is to compare the Timer B Countdown value with $0200. Since the initial value is $03A0 (clock cycles), and it counts DOWN to 0, if the Countdown is at a value greater than $0200 clock cycles when a pulse received on FLAG line(1), the latter is shorter than $01A0 clock cycles. $01A0 is therefore the Threshold value (in clock cycles).

In our example, the TAP value is then:

TAP threshold byte = Threshold (in microseconds) * 0.123156 = $34

where the Threshold (in microseconds) is Threshold * 1e6/CPUFrequency.

(1) on CIA #1, this FLAG line is connected to the Cassette Read line of the Cassette Port.

CIA + Vector information

Appendix A (CIA 1)

56320-56335 $DC00-$DC0F
Complex Interface Adapter (CIA) #1 Registers

Locations 56320-56335 ($DC00-$DC0F) are used to communicate with the Complex Interface Adapter chip #1 (CIA #1). This chip allows the 6510 microprocessor to communicate with peripheral input and output devices. The specific devices that CIA #1 reads data from and sends data to are the joystick controllers, the paddle fire buttons, and the keyboard.

In addition to its two data ports, CIA #1 has two timers, each of which can count an interval from a millionth of a second to a fifteenth of a second. Or the timers can be hooked together to count much longer intervals. CIA #1 has an interrupt line which is connected to the 6510 IRQ line. These two timers can be used to generate interrupts at specified intervals (such as the 1/60 second interrupt used for keyboard scanning, or the more complexly timed interrupts that drive the tape read and write routines).

Location Range: 56320-56321 ($DC00-$DC01)
CIA #1 Data Ports A and B

Data Port B can be used as an output by either Timer A or B. It is possible to set a mode in which the timers do not cause an interrupt when they run down (see the descriptions of Control Registers A and B at 56334-5 ($DC0E-F)). Instead, they cause the output on Bit 6 or 7 of Data Port B to change. Timer A can be set either to pulse the output of Bit 6 for one machine cycle, or to toggle that bit from 1 to 0 or 0 to 1. Timer B can use Bit 7 of this register for the same purpose.

Location Range: 56324-56327 ($DC04-$DC07)
Timers A and B Low and High Bytes

These four timer registers (two for each timer) have different functions depending on whether you are reading from them or writing to them. When you read from these registers, you get the present value of the Timer Counter (which counts down from its initial value to 0). When you write data to these registers, it is stored in the Timer Latch, and from there it can be used to load the Timer Counter using the Force Load bit of Control Register A or B (see 56334-5 ($DC0E-F) below).

These interval timers can hold a 16-bit number from 0 to 65535, in normal 6510 low-byte, high-byte format (VALUE=LOW BYTE+256*HIGH BYTE). Once the Timer Counter is set to an initial value, and the timer is started, the timer will count down one number every microprocessor clock cycle. Since the clock speed of the 64 (using the American NTSC television standard) is 1,022,730 cycles per second, every count takes approximately a millionth of a second. The formula for calculating the amount of time it will take for the timer to count down from its latch value to 0 is:


where LATCH VALUE is the value written to the low and high timer registers (LATCH VALUE=TIMER LOW+256*TIMER HIGH), and CLOCK SPEED is 1,022,370 cycles per second for American (NTSC) standard television monitors, or 985,250 for European (PAL) monitors.

When Timer Counter A or B gets to 0, it will set Bit 0 or 1 in the Interrupt Control Register at 56333 ($DC0D). If the timer interrupt has been enabled (see 56333 ($DC0D)), an IRQ will take place, and the high bit of the Interrupt Control Register will be set to 1. Alternately, if the Port B output bit is set, the timer will write data to Bit 6 or 7 of Port B. After the timer gets to 0, it will reload the Timer Latch Value, and either stop or count down again, depending on whether it is in one-shot or continuous mode (determined by Bit 3 of the Control Register).

Although usually a timer will be used to count the microprocessor cycles, Timer A can count either the microprocessor clock cycles or external pulses on the CTN line, which is connected to pin 4 of the User Port.

Timer B is even more versatile. In addition to these two sources, Timer B can count the number of times that Timer A goes to 0. By setting Timer A to count the microprocessor clock, and setting Timer B to count the number of times that Timer A zeros, you effectively link the two timers into one 32-bit timer that can count up to 70 minutes with accuracy within 1/15 second.

In the 64, CIA #1 Timer A is used to generate the interrupt which drives the routine for reading the keyboard and updating the software clock. Both Timers A and B are also used for the timing of the routines that read and write tape data. Normally, Timer A is set for continuous operation, and latched with a value of 149 in the low byte and 66 in the high byte, for a total Latch Value of 17045. This means that it is set to count to 0 every 17045/1022730 seconds, or approximately 1/60 second.

For tape reads and writes, the tape routines take over the IRQ vectors. Even though the tape write routines use the on-chip I/O port at location 1 for the actual data output to the cassette, reading and writing to the cassette uses both CIA #1 Timer A and Timer B for timing the I/O routines.

56324 $DC04 TIMALO
Timer A (low byte)

56325 $DC05 TIMAHI
Timer A (high byte)

56326 $DC06 TIMBLO
Timer B (low byte)

56327 $DC07 TIMBHI
Timer B (high byte)

56333 $DC0D CIAICR
Interrupt Control Register
Bit 0: Read / did Timer A count down to 0? (1=yes)
Write/ enable or disable Timer A interrupt (1=enable, 0=disable)
Bit 1: Read / did Timer B count down to 0? (1=yes)
Write/ enable or disable Timer B interrupt (1=enable, 0=disable)
Bit 2: Read / did Time of Day Clock reach the alarm time? (1=yes)
Write/ enable or disable TOD clock alarm interrupt (1=enable, 0=disable)
Bit 3: Read / did the serial shift register finish a byte? (1=yes)
Write/ enable or disable serial shift register interrupt (1=enable, 0=disable)
Bit 4: Read / was a signal sent on the flag line? (1=yes)
Write/ enable or disable FLAG line interrupt (1=enable, 0=disable)
Bit 5: Not used
Bit 6: Not used
Bit 7: Read / did any CIA #1 source cause an interrupt? (1=yes)
Write/ set or clear bits of this register (1=bits written with 1 will be set, 0=bits written with 1 will be cleared)

This register is used to control the five interrupt sources on the 6526 CIA chip. These sources are Timer A, Timer B, the Time of Day Clock, the Serial Register, and the FLAG line. Timers A and B cause an interrupt when they count down to 0. The Time of Day Clock generates an interrupt when it reaches the ALARM time. The Serial Shift Register interrupts when it compiles eight bits of input or output. An external signal pulling the CIA hardware line called FLAG low will also cause an interrupt (on CIA #1, this FLAG line is connected to the Cassette Read line of the Cassette Port).

Even if the condition for a particular interrupt is satisfied, the interrupt must still be enabled for an IRQ actually to occur. This is done by writing to the Interrupt Control Register. What happens when you write to this register depends on the way that you set Bit 7. If you set it to 0, any other bit that was written to with a 1 will be cleared, and the corresponding interrupt will be disabled. If you set Bit 7 to 1, any bit written to with a 1 will be set, and the corresponding interrupt will be enabled. In either case, the interrupt enable flags for those bits written to with a 0 will not be affected.

For example, in order to disable all interrupts from BASIC, you could POKE 56333, 127. This sets Bit 7 to 0, which clears all of the other bits, since they are all written with 1’s. Don’t try this from BASIC immediate mode, as it will turn off Timer A which causes the IRQ for reading the keyboard, so that it will in effect turn off the keyboard.

To turn on the Timer A interrupt, a program could POKE 56333,129. Bit 7 is set to 1 and so is Bit 0, so the interrupt which corresponds to Bit 0 (Timer A) is enabled.

When you read this register, you can tell if any of the conditions for a CIA Interrupt were satisfied because the corresponding bit will be set to a 1. For example, if Timer A counts down to 0, Bit 0 of this register will be set to 1. If, in addition, the mask bit that corresponds to that interrupt source is set to 1, and an interrupt occurs, Bit 7 will also be set. This allows a multi-interrupt system to read one bit and see if the source of a particular interrupt was CIA #1. You should note, however, that reading this register clears it, so you should preserve its contents in RAM if you want to test more than one bit.

56334 $DC0E CIACRA
Control Register A

Bit 0: Start Timer A (1=start, 0=stop)
Bit 1: Select Timer A output on Port B (1=Timer A output appears on Bit 6 of Port B)
Bit 2: Port B output mode (1=toggle Bit 6, 0=pulse Bit 6 for one cycle)
Bit 3: Timer A run mode (1=one-shot, 0=continuous)
Bit 4: Force latched value to be loaded to Timer A counter (1=force load strobe)
Bit 5: Timer A input mode (1=count microprocessor cycles, 0=count signals on CNT line at pin 4 of User Port)
Bit 6: Serial Port (56332, $DC0C) mode (1=output, 0=input)
Bit 7: Time of Day Clock frequency (1=50 Hz required on TOD pin, 0=60 Hz)

Bits 0-3. This nybble controls Timer A. Bit 0 is set to 1 to start the timer counting down, and set to 0 to stop it. Bit 3 sets the timer for one-shot or continuous mode.

In one-shot mode, the timer counts down to 0, sets the counter value back to the latch value, and then sets Bit 0 back to 0 to stop the timer. In continuous mode, it reloads the latch value and starts all over again.

Bits 1 and 2 allow you to send a signal on Bit 6 of Data Port B when the timer counts. Setting Bit 1 to 1 forces this output (which overrides the Data Direction Register B Bit 6, and the normal Data Port B value). Bit 2 allows you to choose the form this output to Bit 6 of Data Port B will take. Setting Bit 2 to a value of 1 will cause Bit 6 to toggle to the opposite value when the timer runs down (a value of 1 will change to 0, and a value of 0 will change to 1). Setting Bit 2 to a value of 0 will cause a single pulse of a one machine-cycle duration (about a millionth of a second) to occur.

Bit 4. This bit is used to load the Timer A counter with the value that was previously written to the Timer Low and High Byte Registers. Writing a 1 to this bit will force the load (although there is no data stored here, and the bit has no significance on a read).

Bit 5. Bit 5 is used to control just what it is Timer A is counting. If this bit is set to 1, it counts the microprocessor machine cycles (which occur at the rate of 1,022,730 cycles per second). If the bit is set to 0, the timer counts pulses on the CNT line, which is connected to pin 4 of the User Port. This allows you to use the CIA as a frequency counter or an event counter, or to measure pulse width or delay times of external signals.

Bit 6. Whether the Serial Port Register is currently inputting or outputting data (see the entry for that register at 56332 ($DC0C) for more information) is controlled by this bit.

Bit 7. This bit allows you to select from software whether the Time of Day Clock will use a 50 Hz or 60 Hz signal on the TOD pin in order to keep accurate time (the 64 uses a 60 Hz signal on that pin).

56335 $DC0F CIACRB
Control Register B

Bit 0: Start Timer B (1=start, 0=stop)
Bit 1: Select Timer B output on Port B (1=Timer B output appears on Bit 7 of Port B)
Bit 2: Port B output mode (1=toggle Bit 7, 0=pulse Bit 7 for one cycle)
Bit 3: Timer B run mode (1=one-shot, 0=continuous)
Bit 4: Force latched value to be loaded to Timer B counter (1=force load strobe)
Bits 5-6: Timer B input mode
00 = Timer B counts microprocessor cycles
01 = Count signals on CNT line at pin 4 of User Port
10 = Count each time that Timer A counts down to 0
11 = Count Timer A 0's when CNT pulses are also present
Bit 7: Select Time of Day write (0=writing to TOD registers sets alarm, 1=writing to TOD registers sets clock)

Bits 0-3. This nybble performs the same functions for Timer B that Bits 0-3 of Control Register A perform for Timer A, except that Timer B output on Data Port B appears at Bit 7, and not Bit 6.

Bits 5 and 6. These two bits are used to select what Timer B counts. If both bits are set to 0, Timer B counts the microprocessor machine cycles (which occur at the rate of 1,022,730 cycles per second). If Bit 6 is set to 0 and Bit 5 is set to 1, Timer B counts pulses on the CNT line, which is connected to pin 4 of the User Port. If Bit 6 is set to 1 and Bit 5 is set to 0, Timer B counts Timer A underflow pulses, which is to say that it counts the number of times that Timer A counts down to 0. This is used to link the two numbers into one 32-bit timer that can count up to 70 minutes with accuracy to within 1/15 second. Finally, if both bits are set to 1, Timer B counts the number of times that Timer A counts down to 0 and there is a signal on the CNT line (pin 4 of the User Port).

Bit 7. Bit 7 controls what happens when you write to the Time of Day registers. If this bit is set to 1, writing to the TOD registers sets the ALARM time. If this bit is cleared to 0, writing to the TOD registers sets the TOD clock.

Appendix B (CIA 2)

Locations 56576-56591 ($DD00-$DD0F) are used to address the Complex Interface Adapter chip #2 (CIA #2). Since the chip itself is identical to CIA #1, which is addressed at 56320 ($DC00), the discussion here will be limited to the use which the 64 makes of this particular chip. For more general information on the chip registers, please see the corresponding entries for CIA #1.

A significant (for our purposes) difference between CIA chips #1 and #2 is that the interrupt line of CIA #1 is wired to the 6510 IRQ line, while that of CIA #2 is wired to the NMI line. This means that interrupts from this chip cannot be masked by setting the Interrupt disable flag (SEI). They can be disabled from CIA’s Mask Register, though. Be sure to use the NMI vector when setting up routines to be driven by interrupts generated by this chip.

Appendix C (VECTORS)

792-793 $318-$319 NMINV
Vector: Non-Maskable Interrupt

This vector points to the address of the routine that will be executed when a Non-Maskable Interrupt (NMI) occurs (currently at 65095 ($FE47)).

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.

When an NMI interrupt occurs, a ROM routine sets the Interrupt disable flag, and then jumps through this RAM vector. The default vector points to an interrupt routine which checks to see what the cause of the NMI was.

If the cause was CIA #2, the routine checks to see if one of the RS-232 routines should be called. If the source was the RESTORE key, it checks for a cartridge, and if present, the cartridge is entered at the warm start entry point. If there is no cartridge, the STOP key is tested. If the STOP key was pressed at the same time as the RESTORE key, several of the Kernal initialization routines such as RESTOR, IOINIT and part of CINT are executed, and BASIC is entered through its warm start vector at 40962. If the STOP key was not pressed simultaneously with the RESTORE, the interrupt will end without letting the user know that anything happened at all when the RESTORE key was pressed.

Since this vector controls the outcome of pressing the RESTORE key, it can be used to disable the STOP/RESTORE sequence. A simple way to do this is to change this vector to point to the RTI instruction. A simple

LDA #$C1
STA $0318

will accomplish this. To set the vector back:

LDA #$47
STA $0318

Note that this will cut out all NMIs, including those required for RS-232 I/O.

Location Range: 65530-65535 ($FFFA-$FFFF)
6510 Hardware Vectors

The last six locations in memory are reserved by the 6510 processor chip for three fixed vectors. These vectors let the chip know at what address to start executing machine language program code when an NMI interrupt occurs, when the computer is turned on, or when an IRQ interrupt or BRK occurs.

65530 $FFFA
Non-Maskable Interrupt Hardware Vector

This vector points to the main NMI routine at 65091 ($FE43).

65532 $FFFC
System Reset (RES) Hardware Vector

This vector points to the power-on routine at 64738 ($FCE2).

65534 $FFFE
Maskable Interrupt Request and Break Hardware Vectors

This vector points to the main IRQ handler routine at 65352 ($FF48).

Commodore’s ROM loader


On the Commodore 64 CBM’s ROM Loader uses 3 pulse types whose values have been observed to be close to:

  (S)hort  : TAP value $30
  (M)edium : TAP value $42
  (L)ong   : TAP value $56

In some of the literature from the 80s (e.g. Nick Hampshire’s “Commodore 64 Kernal and Hardware Revealed”) the following durations are defined, which seem to apply to VIC 20 rather than to the Commodore 64:

  (S)hort  : 2840 Hz
  (M)edium : 1953 Hz
  (L)ong   : 1488 Hz

Any definition of these durations would be more appropriately expressed in clock cycles, rather than absolute timings. The reason is that the number of clock cycles that make up each pulse is independent of the machine, where the actual duration in seconds depends on the CPU frequency of the machine itself.

Field observations suggest that either the number of clock cycles has been changed in different versions of the CBM Kernal, or the tape deck circuitry has changed, or (very unlikely but still a possibility) some files for a machine have been recorded on a different machine. In fact, we have examples of C64 files (mainly old ones) using pulses whose duration is typical of VIC 20 files. In support of these speculations we also have examples of files that miss the “end-of-data marker” -see later- at the end of certain CBM files.

Pulses are always interpreted as a pair:

  (S,M) = 0 bit
  (M,S) = 1 bit
  (L,M) = new-data marker
  (L,S) = end-of-data marker


Each data byte is organized as follows:

  (?,?) (?,?) (?,?) (?,?) (?,?) (?,?) (?,?) (?,?) (?,?) (?,?)
    |     |     |     |     |     |     |     |     |     |
    |    bit0  bit1  bit2  bit3  bit4  bit5  bit6  bit7   |
    |                                                     |
data marker                                             checkbit

So that, each byte is encoded as a sequence of 20 pulses (10 pairs):

  1 data marker:

    data finishes when an "end-of-data marker" (L,S) is met.
    Older Kernal SAVE routines do not seem to save the "end-of-data marker",
    so it has to be assumed as a non-mandatory field.

The following data is NOT present if data marker is "end-of-data marker":

  8 bits of information in LSbF format.

  1 checkbit which is computed as:

    1 XOR bit0 XOR bit1 XOR bit2 XOR bit3 XOR bit4 XOR bit5 XOR bit6 XOR bit7. 


When a VIC20 or a C64 save a file to tape with the following:

  SAVE "MY PROGRAM", 1    (relocatable program file: secondary address being
                           0 or any even number, i.e. bit 0 clear)
  SAVE "MY PROGRAM", 1, 1 (non-relocatable program file: secondary address
                           being 1 or any odd number, i.e. bit 0 set)

they create 4 files:

  silence (roughly 0.333 seconds, which allows the motor to reach full speed before recording data)


  silence (roughly 0.333 seconds, which allows the motor to reach full speed)


Additionally, if bit 1 of the secondary address is set, an End-of-tape marker is
saved after the DATA REPEATED file (load address/end address and filename are the
ones used in the first two HEADER files):

  silence (roughly 0.333 seconds, which allows the motor to reach full speed)

  HEADER - End-of-tape marker
  HEADER - End-of-tape marker, REPEATED

When a SEQuential file is saved to tape with:

  OPEN N, 1, 1, "MY SEQ DATA" (no "End-of-tape marker" is saved)
  OPEN N, 1, 2, "MY SEQ DATA" ("End-of-tape marker" is saved after all files)

  PRINT# N, "... DATA..."


it's segmented, if required, and encapsulated into HEADER files. A padding is
automatically done, if required, since HEADER payload has a standard length (191
bytes). An empty HEADER (all "File name" and "body" bytes are $20) comes before

  HEADER - SEQ file header
  HEADER - SEQ file header, REPEATED

One or more of these follow, depending on the SEQ data size:

  silence (duration is variable)

  HEADER - Data block for SEQ file
  HEADER - Data block for SEQ file, REPEATED

If an "End-of-tape marker" is requested, the OS saves an additional empty HEADER
just after the last "Data block for SEQ file":

  silence (roughly 0.333 seconds, which allows the motor to reach full speed)

  HEADER - End-of-tape marker
  HEADER - End-of-tape marker, REPEATED 


A ten second leader is written on the tape before recording of the data or program commences. This leader has two functions; first it allows the tape motor to reach the correct speed, and secondly the sequence of short pulses written on the leader is used to synchronize the read routine timing to the timing on the tape. The operating system can thus produce a correction factor which allows a very wide variation in tape speed without affecting reading.

The exact amount of short pulses is:

  - $6A00 (10 seconds) for HEADER 

Inter-record gaps

Inter-record gaps are primarily used in ASCII files and their function is to allow the tape motor time to decelerate after being turned off and accelerate to the correct speed when turned on prior to a block read or write. Each inter-record gap is approximately two seconds long and is recorded as a sequence of short pulses in the same manner as the ten second leader.

The exact amount of short pulses is:

  - $1500 (2 seconds) for DATA, and HEADER when it contains "Data block
    for SEQ file" 

Interblock gaps

There is also a gap between each file and its replication.

The exact amount of short pulses is:



It consists in a sync train (9 bytes).

Both HEADER and DATA blocks have the following sequence:

    $89 $88 $87 $86 $85 $84 $83 $82 $81

Both HEADER REPEATED and DATA REPEATED blocks have the same sequence with bit 7 clear:

    $09 $08 $07 $06 $05 $04 $03 $02 $01


For any HEADER the following information is sent after the sync sequence:

  1 Byte   : File type.

    $01= relocatable program
    $02= Data block for SEQ file
    $03= non-relocatable program
    $04= SEQ file header
    $05= End-of-tape marker

  Here starts what I refer to as HEADER "payload".
  In case File type is not $02, the following bytes have this meaning:

    2 Bytes  : Start Address (LSBF).
    2 Bytes  : End Address+1 (LSBF).
    16 Bytes : File Name (PETSCII format, padded with blanks).

  When File type is $02, SEQ file data starts immediately after File Type thus
  allowing the use of those 20 bytes to store additional data.

  After the File Name there is HEADER "body": 171 bytes, often used by commercial
  loaders to store executable loader code or any additional data and code the
  loader or program may require.
  It encapsulates Data for segmented SEQ files too, as discussed before.

  The default behaviour of the Kernal SAVE command is to pad the File Name with
  blanks so that the total length of the name portion equals 187 bytes.

  Last Byte: Data checkbyte, computed as:

    0 XOR all other HEADER bytes, from "File type" to end of "body".

  After the checkbyte there may or may not be an "end-of-data marker".


For any DATA the following information is sent after the sync sequence:

  DATA body

  Last Byte: Data checkbyte, computed as:

    0 XOR all DATA "body" bytes.

  After the checkbyte there may or may not be an "end-of-data marker".


Some trailing short pulses follow both HEADER REPEATED and DATA REPEATED. The standard amount is $4E pulses.

C64 Notes

HEADER blocks always load into the Tape Buffer at $033C.

If the File Type is relocatable program the start address for loading will be $0801 regardless of what may be written in the ‘Start Address’ field.

Header and SEQ overview

ROM loader header structure

ROM loader header structure

ROM loader SEQ data arrangement

ROM loader SEQ data arrangement


  ;[Generated by 6510 Dasm v2.1b (c)2004-05 Luigi Di Fraia]

  ;load RAM from a device
  JF49E  86  C3        STX $C3         ;set destination address from XY
         84  C4        STY $C4         
         6C  30  03    JMP ($0330)     ;load RAM (normally F4A5)

  ;standard load RAM entry
  WF4A5  85  93        STA $93         ;set load/verify switch to load
         A9  00        LDA #$00       
         85  90        STA $90         ;clear ST
         A5  BA        LDA $BA         ;if current device is keyboard (0)
         D0  03        BNE $F4B2       
  BF4AF  4C  13  F7    JMP $F713       ;indicate Illegal Device # Error

  BF4B2  C9  03        CMP #$03        ;if current device is the screen
         F0  F9        BEQ $F4AF       ;indicate error
         90  7B        BCC $F533       ;if not serial bus device
         A4  B7        LDY $B7         ;and if no filename,
         D0  03        BNE $F4BF       
         4C  10  F7    JMP $F710       ;indicate File Name Missing Error

  BF4BF  A6  B9        LDX $B9         ;move X to secondary address
         20  AF  F5    JSR $F5AF       ;handle load messages
         A9  60        LDA #$60        ;set current secondary address
         85  B9        STA $B9         
         20  D5  F3    JSR $F3D5       ;perform open of serial bus device
         A5  BA        LDA $BA         ;let A = current device
         20  09  ED    JSR $ED09       ;send TALK on serial bus
         A5  B9        LDA $B9         ;fetch secondary address
         20  C7  ED    JSR $EDC7       ;and send on serial bus
         20  13  EE    JSR $EE13       ;input a byte on serial bus
         85  AE        STA $AE         ;set I/O end address
         A5  90        LDA $90         
         4A            LSR             
         4A            LSR             
         B0  50        BCS $F530       ;if ST doesn't indicate a timeout (read)
         20  13  EE    JSR $EE13       ;input a byte on serial bus
         85  AF        STA $AF         ;set high byte of end address
         8A            TXA             
         D0  08        BNE $F4F0       ;if EOI is not low,
         A5  C3        LDA $C3         ;use destination address
         85  AE        STA $AE         ;as end address
         A5  C4        LDA $C4         ;ditto for high byte
         85  AF        STA $AF         
  BF4F0  20  D2  F5    JSR $F5D2       ;print LOAD or VERIFY
  BF4F3  A9  FD        LDA #$FD        ;clear timeout (read) bit
         25  90        AND $90         ;in ST
         85  90        STA $90         
         20  E1  FF    JSR $FFE1       ;check for Stop key
         D0  03        BNE $F501       ;if depressed
         4C  33  F6    JMP $F633       ;abort load

  BF501  20  13  EE    JSR $EE13       ;input a byte on serial bus
         AA            TAX             
         A5  90        LDA $90         ;if Timeout (read) set in ST
         4A            LSR             
         4A            LSR             
         B0  E8        BCS $F4F3       ;abort load
         8A            TXA             
         A4  93        LDY $93         ;if in verify mode
         F0  0C        BEQ $F51C       
         A0  00        LDY #$00       
         D1  AE        CMP ($AE),Y     ;compare byte read to memory
         F0  08        BEQ $F51E       
         A9  10        LDA #$10       
         20  1C  FE    JSR $FE1C       ;and set verify error on mismatch
         2C            .BYTE $2C       ;skip next instruction
  BF51C  91  AE        STA ($AE),Y     ;load byte to memory
  BF51E  E6  AE        INC $AE         ;bump load address
         D0  02        BNE $F524       
         E6  AF        INC $AF         
  BF524  24  90        BIT $90         ;if not end of file
         50  CB        BVC $F4F3       ;repeat
         20  EF  ED    JSR $EDEF       ;else send TALK on serial bus
         20  42  F6    JSR $F642       ;close serial bus
         90  79        BCC $F5A9       ;and exit
  BF530  4C  04  F7    JMP $F704       ;indicate File Not Found Error

  BF533  4A            LSR             ;if input device is not 1 (cassette)
         B0  03        BCS $F539       
         4C  13  F7    JMP $F713       ;indicate Illegal Device #

  BF539  20  D0  F7    JSR $F7D0       ;fetch tape buffer pointer
         B0  03        BCS $F541       
         4C  13  F7    JMP $F713       ;if invalid, indicate Illegal Device #

  BF541  20  17  F8    JSR $F817       ;display msgs and test buttons for read
         B0  68        BCS $F5AE       
         20  AF  F5    JSR $F5AF       ;handle load messages
  BF549  A5  B7        LDA $B7         ;if file name present
         F0  09        BEQ $F556       
         20  EA  F7    JSR $F7EA       ;search tape for file name
         90  0B        BCC $F55D       ;if no errors, continue
         F0  5A        BEQ $F5AE       ;exit if end of tape
         B0  DA        BCS $F530       ;error if not found
  BF556  20  2C  F7    JSR $F72C       ;since no file name, get next tape hdr
         F0  53        BEQ $F5AE       ;exit if end of tape found
         B0  D3        BCS $F530       ;indicate File Not found Error
  BF55D  A5  90        LDA $90         ;check ST for unrecoverable read error
         29  10        AND #$10       
         38            SEC             
         D0  4A        BNE $F5AE       ;and exit if so
         E0  01        CPX #$01        ;if not Program Header
         F0  11        BEQ $F579       
         E0  03        CPX #$03       
         D0  DD        BNE $F549       
  BF56C  A0  01        LDY #$01       
         B1  B2        LDA ($B2),Y     
         85  C3        STA $C3         ;reset load address from tape buffer
         C8            INY             
         B1  B2        LDA ($B2),Y     ;high byte also
         85  C4        STA $C4         
         B0  04        BCS $F57D       
  BF579  A5  B9        LDA $B9         
         D0  EF        BNE $F56C       
  BF57D  A0  03        LDY #$03        ;index low byte of end address
         B1  B2        LDA ($B2),Y     
         A0  01        LDY #$01       
         F1  B2        SBC ($B2),Y     ;compute length of block to load
         AA            TAX             
         A0  04        LDY #$04       
         B1  B2        LDA ($B2),Y     
         A0  02        LDY #$02       
         F1  B2        SBC ($B2),Y     
         A8            TAY             
         18            CLC             
         8A            TXA             
         65  C3        ADC $C3         
         85  AE        STA $AE         ;and set the end address of I/O area
         98            TYA             
         65  C4        ADC $C4         
         85  AF        STA $AF         
         A5  C3        LDA $C3         
         85  C1        STA $C1         ;set tape load address
         A5  C4        LDA $C4         
         85  C2        STA $C2         
         20  D2  F5    JSR $F5D2       ;display load messages
         20  4A  F8    JSR $F84A       ;load from cassette
         24            .BYTE $24       ;skip next instruction
  BF5A9  18            CLC             ;clear error flag
         A6  AE        LDX $AE         ;exit with end address in XY
         A4  AF        LDY $AF         
  BF5AE  60            RTS             


  ;get next file header from cassette
  SF72C  A5  93        LDA $93         ;save load/verify switch on stack
         48            PHA             
         20  41  F8    JSR $F841       ;read a block from tape
         68            PLA             
         85  93        STA $93         ;restore load/verify flag
         B0  32        BCS $F769       ;exit if read error
         A0  00        LDY #$00       
         B1  B2        LDA ($B2),Y     ;get first character in tape buffer
         C9  05        CMP #$05        ;if code for End of Tape
         F0  2A        BEQ $F769       ;return
         C9  01        CMP #$01       
         F0  08        BEQ $F74B       ;if not code for Program Header
         C9  03        CMP #$03        ;or "?"
         F0  04        BEQ $F74B       
         C9  04        CMP #$04       
         D0  E1        BNE $F72C       ;or Data Header, try next block
  BF74B  AA            TAX             
         24  9D        BIT $9D         ;if in direct mode,
         10  17        BPL $F767       
         A0  63        LDY #$63        ;point to message FOUND
         20  2F  F1    JSR $F12F       ;and print it
         A0  05        LDY #$05       
  BF757  B1  B2        LDA ($B2),Y     
         20  D2  FF    JSR $FFD2       ;print a file name character
         C8            INY             
         C0  15        CPY #$15        ;and repeat
         D0  F6        BNE $F757       ;for all characters
         A5  A1        LDA $A1         
         20  E0  E4    JSR $E4E0       ;pause
         EA            NOP             ;filler for patch
         18            CLC             
         88            DEY             
         60            RTS             


  ;read a block from cassette
  SF841  A9  00        LDA #$00       
         85  90        STA $90         ;clear ST
         85  93        STA $93         ;set load/verify switch to load
         20  D7  F7    JSR $F7D7       ;set tape buffer to I/O area
  SF84A  20  17  F8    JSR $F817       ;handle msgs and test sense for read
         B0  1F        BCS $F86E       
         78            SEI             ;disable IRQ
         A9  00        LDA #$00       
         85  AA        STA $AA         ;set gap
         85  B4        STA $B4         ;set no sync estabilished
         85  B0        STA $B0         ;set no special speed correction yet
         85  9E        STA $9E         ;initialize error log index for pass 1
         85  9F        STA $9F         ;and pass2
         85  9C        STA $9C         ;set no byte available yet
         A9  90        LDA #$90        ;set Flag mask
         A2  0E        LDX #$0E        ;index for cassette read IRQ address
         D0  11        BNE $F875       ;JMP

  ;write a block to cassette
  SF864  20  D7  F7    JSR $F7D7       ;initialize tape buffer pointer
  SF867  A9  14        LDA #$14
         85  AB        STA $AB         ;20 sync patterns
  SF86B  20  38  F8    JSR $F838       ;test sense and display msgs for output
  BF86E  B0  6C        BCS $F8DC
         78            SEI
         A9  82        LDA #$82        ;mask for ICR1 to honor TB1
         A2  08        LDX #$08        ;IRQ index for cassette write, part 1

  ;common code for cassette read & write
  BF875  A0  7F        LDY #$7F       
         8C  0D  DC    STY $DC0D       ;clear any pending mask in ICR1
         8D  0D  DC    STA $DC0D       ;then set mask for TB1
         AD  0E  DC    LDA $DC0E
         09  19        ORA #$19        ;+force load, one shot and TB1 to CRA1
         8D  0F  DC    STA $DC0F       ;to form CRB1
         29  91        AND #$91
         8D  A2  02    STA $02A2       ;and CRB1 activity register
         20  A4  F0    JSR $F0A4       ;condition flag bit in ICR2
         AD  11  D0    LDA $D011       
         29  EF        AND #$EF       
         8D  11  D0    STA $D011       ;disable the screen
         AD  14  03    LDA $0314       ;save standard IRQ vector
         8D  9F  02    STA $029F
         AD  15  03    LDA $0315
         8D  A0  02    STA $02A0
         20  BD  FC    JSR $FCBD       ;set new IRQ for cassette depending on X
         A9  02        LDA #$02
         85  BE        STA $BE         ;select phase 2
         20  97  FB    JSR $FB97       ;initialize cassette I/O variables
         A5  01        LDA $01
         29  1F        AND #$1F
         85  01        STA $01         ;start cassette motor
         85  C0        STA $C0         ;set tape motor interlock
         A2  FF        LDX #$FF       
  BF8B5  A0  FF        LDY #$FF       
  BF8B7  88            DEY             
         D0  FD        BNE $F8B7       ;delay 0.3 seconds
         CA            DEX             
         D0  F8        BNE $F8B5       
         58            CLI             
  BF8BE  AD  A0  02    LDA $02A0       ;test high byte of IRQ save area
         CD  15  03    CMP $0315       ;to determine if end of I/O
         18            CLC             
         F0  15        BEQ $F8DC       ;exit if so
         20  D0  F8    JSR $F8D0       ;else test Stop key
         20  BC  F6    JSR $F6BC       ;scan keyboard
         4C  BE  F8    JMP $F8BE       ;repeat


  ;set IRQ vector depending upon X
  SFCDB  BD  93  FD    LDA $FD9B-8,X   ;move low byte of address
         8D  14  03    STA $0314       ;into low byte of IRQ vector
         BD  94  FD    LDA $FD9B-7,X   ;then do high byte
         8D  15  03    STA $0315       
         60            RTS


  ;IRQ vectors
         .WORD $FBDC
         .WORD $EA31
         .WORD $F92C


  ;cassette read IRQ routine
  BF92C  AE  07  DC    LDX $DC07       ;get TBH1
         A0  FF        LDY #$FF       
         98            TYA             ;and the complement of TBL1
         ED  06  DC    SBC $DC06       ;(time elapsed)
         EC  07  DC    CPX $DC07       ;if high byte not steady,
         D0  F2        BNE $F92C       ;repeat
         86  B1        STX $B1         ;else save high byte
         AA            TAX             
         8C  06  DC    STY $DC06       ;reset TBL1 to maximum
         8C  07  DC    STY $DC07       ;ditto TBH1
         A9  19        LDA #$19        ;force load, one-shot and Timer B
         8D  0F  DC    STA $DC0F       ;into CRB1
         AD  0D  DC    LDA $DC0D       
         8D  A3  02    STA $02A3       ;save ICR1
         98            TYA             
         E5  B1        SBC $B1         ;complement high byte
         86  B1        STX $B1         ;save low byte
         4A            LSR             ;elapsed time in A, ZB1
         66  B1        ROR $B1         ;/ 2
         4A            LSR             
         66  B1        ROR $B1         ;/ 4
         A5  B0        LDA $B0         ;get speed correction
         18            CLC             
         69  3C        ADC #$3C        ;+240 microseconds
         C5  B1        CMP $B1         ;if cycle shorter
         B0  4A        BCS $F9AC       ;dismiss
         A6  9C        LDX $9C         ;if byte available
         F0  03        BEQ $F969       
         4C  60  FA    JMP $FA60       ;receive it

  BF969  A6  A3        LDX $A3         ;test bit count and if beyond last bit,
         30  1B        BMI $F988       ;do end of byte
         A2  00        LDX #$00        ;assume bit value of 0
         69  30        ADC #$30        ;add 432 microseconds
         65  B0        ADC $B0         ;+ 2 * speed correction
         C5  B1        CMP $B1         ;if cycle shorter
         B0  1C        BCS $F993       ;record a 0
         E8            INX             ;assume bit value of 1
         69  26        ADC #$26        ;get 584 microseconds
         65  B0        ADC $B0         ;+ 3 * speed correction
         C5  B1        CMP $B1         ;if cycle shorter
         B0  17        BCS $F997       ;record a 1
         69  2C        ADC #$2C        ;get 760 microseconds
         65  B0        ADC $B0         ;+ 4 * speed correction
         C5  B1        CMP $B1         ;if cycle shorter
         90  03        BCC $F98B       
  BF988  4C  10  FA    JMP $FA10       ;go do end of byte

  BF98B  A5  B4        LDA $B4         ;if sync estabilished
         F0  1D        BEQ $F9AC       
         85  A8        STA $A8         ;set erroneous bits
         D0  19        BNE $F9AC       
  BF993  E6  A9        INC $A9         ;for a 0, increment 0/1 balance
         B0  02        BCS $F999       
  BF997  C6  A9        DEC $A9         ;for a 1, decrement 0/1 balance
  BF999  38            SEC             
         E9  13        SBC #$13        ;0/1 cutoff level
         E5  B1        SBC $B1         ;-cycle width
         65  92        ADC $92         
         85  92        STA $92         ;accumulated for speed correction
         A5  A4        LDA $A4         
         49  01        EOR #$01        ;flip cycle indication
         85  A4        STA $A4         
         F0  2B        BEQ $F9D5       ;if first cycle,
         86  D7        STX $D7         ;save bit value
  BF9AC  A5  B4        LDA $B4         ;if no sync yet
         F0  22        BEQ $F9D2       ;return from IRQ
         AD  A3  02    LDA $02A3       ;if ICR1 mask
         29  01        AND #$01       
         D0  05        BNE $F9BC       
         AD  A4  02    LDA $02A4       ;and last CRA1 mask shows no TA1 flag
         D0  16        BNE $F9D2       ;exit from IRQ
  BF9BC  A9  00        LDA #$00       
         85  A4        STA $A4         ;clear cycle count
         8D  A4  02    STA $02A4       ;and last CRA1 mask
         A5  A3        LDA $A3         ;if bit count indicated end of byte,
         10  30        BPL $F9F7       
         30  BF        BMI $F988       ;go do end of byte
  BF9C9  A2  A6        LDX #$A6       
         20  E2  F8    JSR $F8E2       ;schedule timer
         A5  9B        LDA $9B         ;if parity calculated does not match
         D0  B9        BNE $F98B       ;set erroneous bit flag
  BF9D2  4C  BC  FE    JMP $FEBC       ;exit from IRQ

  BF9D5  A5  92        LDA $92         ;if second cycle
         F0  07        BEQ $F9E0       ;check accumulated over/under time
         30  03        BMI $F9DE       
         C6  B0        DEC $B0         
         2C            .BYTE $2C       ;skip next instruction
  BF9DE  E6  B0        INC $E0         ;adapt speed correction accordingly
  BF9E0  A9  00        LDA #$00       
         85  92        STA $92         ;reset accumulated over/under time
         E4  D7        CPX $D7         ;if 2nd cycle = complement of cycle 1
         D0  0F        BNE $F9F7       ;include bit
         8A            TXA             
         D0  A0        BNE $F98B       ;if two 0 cycles
         A5  A9        LDA $A9         ;and 0/1 balance
         30  BD        BMI $F9AC       
         C9  10        CMP #$10        ;at least 16 "0" cycles extra
         90  B9        BCC $F9AC       
         85  96        STA $96         ;set sync detected
         B0  B5        BCS $F9AC       
  BF9F7  8A            TXA             
         45  9B        EOR $9B         ;calculate parity
         85  9B        STA $9B         
         A5  B4        LDA $B4         ;if no sync yet
         F0  D2        BEQ $F9D2       ;exit
         C6  A3        DEC $A3         ;decrement pending bit count
         30  C5        BMI $F9C9       ;after last bit, check parity
         46  D7        LSR $D7         ;include bit
         66  BF        ROR $BF         ;in byte being read
         A2  DA        LDX #$DA       
         20  E2  F8    JSR $F8E2       ;schedule timer
         4C  BC  FE    JMP $FEBC       ;exit from IRQ

  BFA10  A5  96        LDA $96         ;if sync detected
         F0  04        BEQ $FA18       
         A5  B4        LDA $B4         ;and not yet estabilished
         F0  07        BEQ $FA1F       
  BFA18  A5  A3        LDA $A3         ;or last bit done
         30  03        BMI $FA1F       
         4C  97  F9    JMP $F997       ;allow byte reception

  BFA1F  46  B1        LSR $B1         ;compute new speed correction value
         A9  93        LDA #$93       
         38            SEC             
         E5  B1        SBC $B1         
         65  B0        ADC $B0         
         0A            ASL             
         AA            TAX             
         20  E2  F8    JSR $F8E2       ;schedule timer
         E6  9C        INC $9C         ;indicate byte available
         A5  B4        LDA $B4         ;if not yet estabilished
         D0  11        BNE $FA44       
         A5  96        LDA $96         ;but sync detected
         F0  26        BEQ $FA5D       
         85  A8        STA $A8         ;set error bits
         A9  00        LDA #$00       
         85  96        STA $96         ;clear sync detected
         A9  81        LDA #$81        ;set TA1 bit
         8D  0D  DC    STA $DC0D       ;in ICR1
         85  B4        STA $B4         ;set sync estabilished
  BFA44  A5  96        LDA $96         ;move sync status
         85  B5        STA $B5         ;to saved sync status
         F0  09        BEQ $FA53       
         A9  00        LDA #$00        ;if not detected,
         85  B4        STA $B4         ;indicate sync not estabilished
         A9  01        LDA #$01       
         8D  0D  DC    STA $DC0D       
  BFA53  A5  BF        LDA $BF         ;clear TA mask in ICR1
         85  BD        STA $BD         ;save byte read
         A5  A8        LDA $A8         
         05  A9        ORA $A9         ;accumulate possible errors
         85  B6        STA $B6         
  BFA5D  4C  BC  FE    JMP $FEBC       ;exit from IRQ


  ;schedule CIA1 Timer A depending on parameter in X
  SF8E2  86  B1        STX $B1         ;save entry parameter
         A5  B0        LDA $B0         ;get speed correction
         0A            ASL             ;* 2
         0A            ASL             ;* 4
         18            CLC             
         65  B0        ADC $B0         ;add speed correction
         18            CLC             
         65  B1        ADC $B1         ;and parameter
         85  B1        STA $B1         ;save low order
         A9  00        LDA #$00       
         24  B0        BIT $B0         ;if speed correction is positive
         30  01        BMI $F8F7       
         2A            ROL             ;set high oreder in A
  BF8F7  06  B1        ASL $B1         ;* 2
         2A            ROL             
         06  B1        ASL $B1         ;* 4
         2A            ROL             
         AA            TAX             
  BF8FE  AD  06  DC    LDA $DC06       ;wait until no change of
         C9  16        CMP #$16        ;TBL1 changing
         90  F9        BCC $F8FE       ;while it still must be read
         65  B1        ADC $B1         :add low order offset to TBL1
         8D  04  DC    STA $DC04       ;and store in TAL1
         8A            TXA             
         6D  07  DC    ADC $DC07       ;add high order offset to TBH1
         8D  05  DC    STA $DC05       ;and store in TAH1
         AD  A2  02    LDA $02A2       
         8D  0E  DC    STA $DC0E       ;set CRA1 from CRB1 activity register
         8D  A4  02    STA $02A4       ;and save it
         AD  0D  DC    LDA $DC0D       
         29  10        AND #$10       
         F0  09        BEQ $F92A       ;if Flag bit is not set
         A9  F9        LDA #$F9        ;set exit address on stack
         48            PHA             
         A9  2A        LDA #$2A       
         48            PHA             
         4C  43  FF    JMP $FF43       ;and simulate an IRQ

  BF92A  58            CLI             ;else allow IRQ and exit
         60            RTS

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s