Saturday, February 1, 2014

Disassembling the C65 C64-mode Kernel

Now that I have the machine starting with the stock C64 kernel, I want to get it booting with the C65 ROM, which means the modified C65 C64-mode kernel.

This isn't just for C65 compatibility, but it also provides a nice way to integrate microSD storage, because I can override the internal DOS so that it uses the microSD connector instead of the floppy controller.

To do this, I need to implement some of the extra 4510 opcodes, as well as some of the other C65 memory mapper and other control features.

What I didn't know was what extra instructions and features I would need to implement to facilitate this.  So I went looking for a disassembly of the C65 ROMs, but couldn't find any.  So I set about making my own, using Marko Makela's well known C64 kernel disassembly as the reference, and making note of all the changes.

I could have done it automatically (although the extra opcodes would have complicated this), but I also wanted to gain a clear understanding of how the C65 modified kernel works, and how it intercepts DOS.

The whole process took a few hours to exhaustively map the differences.

The changes basically consisted of throwing out the cassette routines and putting in sufficient intercepts.  Other smaller changes include making 8 the default device number:

; set parameters for load/verify/save

E1D4   A9 00      LDA #$00
E1D6   20 BD FF   JSR $FFBD
; DIFF: C65: Make device 8 the default.
; C64: E1D9 A2 01 LDX #$01
E1D9   A2 08      LDX #$08
E1DB   A0 00      LDY #$00


; get open/close parameters

E219   A9 00      LDA #$00
E21B   20 BD FF   JSR $FFBD
E21E   20 11 E2   JSR $E211
E221   20 9E B7   JSR $B79E
E224   86 49      STX $49
E226   8A         TXA
; DIFF: C65: Make device 8 the default
; C64 E227   A2 01      LDX #$01
E227   A2 08      LDX #$08
E229   A0 00      LDY #$00
E22B   20 BA FF   JSR $FFBA

and changing the shift-RUN/STOP text so that it loads the first file from disk and runs it:

; Fill keyboard buffer with LOAD command when shift-RUNSTOP is pressed
E5EE   A2 09      LDX #$0A      ; C64: LDA #$09
E5F0   78         SEI
E5F1   86 C6      STX $C6
; C64: E5F3   BD E6 EC   LDA $ECE6,X
; C65: Copy string L"0:*R into keyboard buffer
E5F3   BD 56 FC   LDA $FC56,X   ; C65
E5F6   9D 76 02   STA $0276,X
E5F9   CA         DEX
E5FA   D0 F7      BNE $E5F3


; C65: key sequence when shift-RUN/STOP is pressed
;  L"0:*R
FC57  .BY $4C, $CF, $22, $30, $3A, $2A, $0D, $52, $D5, $0D

The DOS routines have been intercepted using an interesting approach.  First, $C0 ceases to indicate the tape motor state, and instead indicates whether the current drive is on the serial IEC bus, or handled by the internal 1581 DOS.  This is checked using one of the new 4510 opcodes, BBS7, which branches on whether bit 7 is set in a zero-page byte, without having to use the accumulator:

; C65: call DOS routine and return: send secondary address (talk)
F7E4  FF C0 09    BBS7 $C0,$F7F0  ; branch based on whether current device is internal 1581
F7E7  20 C7 ED    JSR $F72C  ; bank in C65 1581 DOS
F7EA  22 0A 80    JSR ($800A)
F7ED  20 3E F8    JSR $F83E  ; C65: return from DOS context and set $90 status
F7F0  4C C7 ED    JMP $EDC7 ; send secondary address (talk) on serial bus

As can be seen, if the drive is on the IEC bus, then the normal C64 kernel routine is used.  However, if the drive is the internal one, then the C65 1581 DOS is banked in to $8000 - $BFFF (conveniently leaving the C64 kernel still in view), and then the appropriate vector in that ROM is called.  Notice the use of the new indirect mode of the JSR instruction (opcode $22).

Banking involves the use of the MAP instruction, which sets the memory map.  C65 memory mapping is too complex to cover in this post, so I will cover it in a separate post later.  The interesting thing for now is to see how NOP is no longer really NOP.  The MAP instruction prevents both IRQ and NMI interrupts until a NOP instruction is run.  NOP is consequentially also known as End Of Mapping (EOM) on the 4510.

All this and more can be seen in the routine for switching to the C65 1581 DOS memory context.

; C65:  switch to C65 1581 DOS context

F72C   78         SEI
F72D   48         PHA
F72E   A9 A5      LDA #$A5      ; C65: VIC-III enable sequence
F730   8D 2F D0   STA $D02F
F733   A9 96      LDA #$96
F735   8D 2F D0   STA $D02F     ; C65: VIC-III enabled
F739   A9 40
F73A   0C 31 D0   TSB $D031     ; set bit 6 in $D031 to put CPU at 3.5MHz
F73D   A9 21
F73F   0C 30 D0   TSB $D030     ; bank in $C000 interface ROM and remove CIAs from IO map
F742   68         PLA           ; store registers
F743   8D F6 DF   STA $DFF6
F746   8E F7 DF   STX $DFF7
F749   8C F8 DF   STY $DFF8
F74C   9C F9 DF   STZ $DFF9
F74F   68         PLA
F750   8D FB DF   STA $DFFB
F753   68         PLA
F754   8D FC DF   STA $DFFC
F757   BA         TSX
F758   8E FF DF   STX $DFFF
; C65: bank in 1581 DOS
F75B   A9 00      LDA #$00
F75D   A2 11      LDX #$11   ; Map $0000-$1FFF to $10000-$11FFF ($0000+$10000)
F75F   A0 80      LDY #$80
F761   A3 31      LDZ #$31   ; Map $8000-$BFFF to $20000-$23FFF ($8000+$18000)
F763   5C         MAP        ; activate new map
F764   A2 FF      LDX #$FF
F766   9A         TXS
F76A   48         PHA
F76E   48         PHA
F76F   AD F6 DF   LDA $DFF6
F772   AE F7 DF   LDX $DFF7
F775   AC F8 DF   LDY $DFF8
F77B   60         RTS        ; RTS (notice how the return address was copied from old stack to new stack)

Notice that there is no NOP or EOM in this routine.  This prevents any interrupts occurring while the internal DOS is operating in its special memory map.  The EOM appears in the routine for returning from the C65 1581 DOS context:
; C65: return from DOS call and set status in $90
F83E  68          PLA
F842  68          PLA
F843  8D FE DF    STA $DFFE
F846  20 7C F7    JSR $F77C ; restore C64 memory map
F849  77 C0       RMB7 $C0  ; clear bit 7 in $C0
F84B  6B          TZA
F84C  10 0C       BPL $F85A
F84E  A9 00       LDA #$00
F850  F7 C0       SMB7 $C0  ; set bit 7 in $C0
F855  DA          PHX
F859  DA          PHX
F85A  04 90       TSB $90   ; Set bits in $90 (status) if required
F85C  AE F7 DF    LDX $DFF7
F85F  AC F8 DF    LDY $DFF8
F862  AB F9 DF    LDZ $DFF9
F865  AD F6 DF    LDA $DFF6
F868  48          PHA
F869  A9 21       LDA #$21
F86B  1C 30 D0    TRB $D030 ; bank out $C000 ROM and bank CIAs back in.
F86E  A9 40       LDA #$40
F870  1C 31 D0    TRB $D031 ; return CPU to 1MHz.
F873  8D 2F D0    STA $D02f ; return to VIC-II mode
F876  68          PLA
F877  EA          EOM       ; release IRQ & NMI after MAP change triggered at $F846
F878  58          CLI
F879  18          CLC
F87A  60          RTS

The $DFFx memory accesses are not to the CIAs, but to the end of screen RAM.  Setting bit 0 in $D030 replaces $DC00-$DFFF with an extra 1KB of colour RAM, which is in fact the last 2KB of the 128KB of main RAM of a C65, and hence is 8 bit RAM, unlike the 4-bit colour RAM on the C64.

The $D030 flag is primarily for making the 2KB colour RAM conveniently available to the kernel when working with an 80-column, and hence 2,000 byte screen.  Of course this leaves a few bytes spare at the end that are nicely used here to save and restore registers when the stack cannot be used because memory is being remapped.

The last interesting piece is to explore is the reset process.  The reset vector has been changed:

FFFA   .WD $FE43   ; NMI vector
; C64: FFFC   .WD $FCE2   ; RESET vector
; C65 new reset vector
FFFC   .WD $E4B8   ; RESET vector
FFFE   .WD $FF48   ; IRQ/BRK vector

Reset proceeds from $E4B8, instead of $FCE2.   The $E4B8 routine is quite simple, if a little curious:

; C65: CPU reset entry point.
; Check for cartridge, else normal reset sequence.
; (this is a little strance, since $FCE2 routine also calls $FD02
E4B8   20 02 FD   JSR $FD02  ; check for cartridge
E4BB   D0 03      BNE $E4C0
E4BD   4C E2 FC   JMP $FCE2  ; RESET routine

; C65: Enable VIC-III mode, jump to interface
E4C0   78         SEI
E4C1   A9 A5      LDA #$A5
E4C3   8D 2F D0   STA $D02F
E4C6   A9 96      LDA #$96
E4C8   8D 2F D0   STA $D02F
E4CB   A9 20      LDA #$20
E4CD   8D 30 D0   STA $D030 ; bank interface ROM in @ $C000
E4D0   4C 00 C8   JMP $C800 ; interface ROM entry point

E4D3   85 A9      STA $A9

E4D5   A9 01      LDA #$01
E4D7   85 AB      STA $AB
E4D9   60         RTS

If a C64 cartridge is detected, then the usual C64 reset process is followed.  If not, then the machine switches to C65 mode by banking in the interface ROM at $C000-$CFFF and jumping to the entry point there.  Also, at another point the C65 1581 ROM is mapped into $8000 - $BFFF and the DOS setup routine is called.  This means that I need to disassemble and examine those two ROMs as well I am to fully understand what is going on.

But for now, the complete C65 C64-mode kernel disassembly is available here.


  1. Fascinating. It seems it should be relatively simple to disable the internal drive with a custom ROM and use the IEC for device 8. It would be nice to be able to use an SD2IEC device instead of those hard to find floppies. Great work!

    1. Hello,
      Yes, it could be done that way. In fact, it is even easier in some ways. All I need to do is implement the F011B floppy controller in VHDL so that it actually accesses the on-board microSD slot.


    2. I was thinking more along the lines of using an SD device with vintage C65 hardware. It would be nice to be able to disable the internal drive and use the IEC port for device 8. However in a full FPGA C65, including the FDC chip would be fantastic. I believe the F011B and F011C versions were very similar. As far as I have been able to discover, just a bit of the logic from Elmer (PAL16L8) was moved to the FDC chip in the C version. I have the PAL equations from the Rev2A Elmer chip if you would like me to email them to you.

    3. Hello, disabling the internal drive would be very easy if you wanted to use a SD2iec adapter. I'd be interested in getting the Elmer equations.


  2. This comment has been removed by the author.